asterism-games 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +166 -61
- data/VERSION +1 -1
- data/lib/asterism_games/board/table.rb +14 -0
- data/lib/asterism_games/commentate/mahjong.rb +124 -9
- data/lib/asterism_games/commentate.rb +1 -1
- data/lib/asterism_games/game.rb +20 -1
- data/lib/asterism_games/host.rb +241 -109
- data/lib/asterism_games/othello/table.rb +12 -0
- data/lib/asterism_games/play/mahjong.rb +4 -3
- data/lib/asterism_games/play/othello.rb +4 -3
- data/lib/asterism_games/play.rb +9 -0
- data/lib/asterism_games/rooms.rb +9 -6
- metadata +6 -16
|
@@ -266,10 +266,111 @@ module AsterismGames
|
|
|
266
266
|
end
|
|
267
267
|
end
|
|
268
268
|
|
|
269
|
+
# Asterism's protocol 2 keeps no current value on the network: a
|
|
270
|
+
# reader asks the sender for it (Host#spectate_now, the object
|
|
271
|
+
# <node>/<app>/spectate) at the first value of a table from a sender,
|
|
272
|
+
# and again when it missed values (info.lost > 0) or saw the sender
|
|
273
|
+
# start again (info.restarted?; both counted per sender on the shared
|
|
274
|
+
# place). A sender that left the network (on_app_leave) is forgotten:
|
|
275
|
+
# Asterism then forgets its numbers too, so its next value comes as a
|
|
276
|
+
# first one (lost 0, not restarted), and is asked for again. The
|
|
277
|
+
# answer goes to the hub unless a newer value came while asking. The
|
|
278
|
+
# asking is on a thread of its own: the values come on the receiving
|
|
279
|
+
# thread, where no call may wait.
|
|
280
|
+
class Refetch
|
|
281
|
+
attr_reader :asked, :taken
|
|
282
|
+
|
|
283
|
+
# call: ->(from, name) { the current value } ("<node>/<app>" of the
|
|
284
|
+
# sender, the table's name under spectate/); log: ->(message).
|
|
285
|
+
def initialize(hub, call:, log: ->(_m) {})
|
|
286
|
+
@hub = hub
|
|
287
|
+
@call = call
|
|
288
|
+
@log = log
|
|
289
|
+
@lock = Monitor.new
|
|
290
|
+
@seen = Hash.new(0) # table id => values given to the hub
|
|
291
|
+
@from = {} # table id => the sender of its last value
|
|
292
|
+
@want = {} # table id => [from, host, name, @seen when asked]
|
|
293
|
+
@queue = Thread::Queue.new
|
|
294
|
+
@asked = 0
|
|
295
|
+
@taken = 0
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# A value of a table came (the receiving thread): blk gives it to
|
|
299
|
+
# the hub; at a sender's first value, after lost values or when the
|
|
300
|
+
# sender started again, the current one is asked for (once at a time
|
|
301
|
+
# per table). Returns blk's.
|
|
302
|
+
def came(info, host, name, &blk)
|
|
303
|
+
id = MahjongHub.id(host, name)
|
|
304
|
+
why = nil
|
|
305
|
+
out = @lock.synchronize do
|
|
306
|
+
r = blk.call
|
|
307
|
+
@seen[id] += 1
|
|
308
|
+
from = info.from.to_s
|
|
309
|
+
first = @from[id] != from
|
|
310
|
+
@from[id] = from
|
|
311
|
+
why = if !from.include?("/") || @want.key?(id) then nil
|
|
312
|
+
elsif info.restarted? then "#{from} started again"
|
|
313
|
+
elsif info.lost.to_i.positive? then "#{info.lost} value(s) lost"
|
|
314
|
+
elsif first then "the first value from #{from}"
|
|
315
|
+
end
|
|
316
|
+
@want[id] = [from, host, name, @seen[id]] if why
|
|
317
|
+
r
|
|
318
|
+
end
|
|
319
|
+
if why
|
|
320
|
+
@log.call("#{id}: #{why}; asking #{info.from}/#{Commentate::MahjongRunner::SPECTATE_OBJECT} for the table now")
|
|
321
|
+
@queue << id
|
|
322
|
+
end
|
|
323
|
+
out
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# A sender ("<node>/<app>") left the network: its next value is a
|
|
327
|
+
# first one.
|
|
328
|
+
def forget(from)
|
|
329
|
+
@lock.synchronize { @from.delete_if { |_id, f| f == from } }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Asks for one table's current value and gives it to the hub (a
|
|
333
|
+
# newer value that came meanwhile wins). :taken, :newer, :none (the
|
|
334
|
+
# sender has nothing yet) or :error.
|
|
335
|
+
def run_one(id)
|
|
336
|
+
from, host, name, at = @lock.synchronize { @want[id] }
|
|
337
|
+
return :none unless from
|
|
338
|
+
@asked += 1
|
|
339
|
+
v = @call.call(from, name)
|
|
340
|
+
@lock.synchronize do
|
|
341
|
+
@want.delete(id)
|
|
342
|
+
return :newer if @seen[id] != at
|
|
343
|
+
return :none unless v.is_a?(Hash)
|
|
344
|
+
@hub.update(host, name, v)
|
|
345
|
+
@seen[id] += 1
|
|
346
|
+
end
|
|
347
|
+
@taken += 1
|
|
348
|
+
@log.call("#{id}: took the table now from #{from}")
|
|
349
|
+
:taken
|
|
350
|
+
rescue StandardError => e
|
|
351
|
+
@lock.synchronize { @want.delete(id) }
|
|
352
|
+
@log.call("#{id}: asking #{from}/#{Commentate::MahjongRunner::SPECTATE_OBJECT}: #{e.class}: #{e.message}")
|
|
353
|
+
:error
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def start
|
|
357
|
+
@thread = Thread.new { loop { run_one(@queue.pop) } }
|
|
358
|
+
self
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def stop
|
|
362
|
+
@thread&.kill
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
269
366
|
# `asterism-games commentate mahjong`: the connection, the spectate
|
|
270
367
|
# keys, the web server and the browser.
|
|
271
368
|
module MahjongRunner
|
|
272
|
-
|
|
369
|
+
# The spectate keys in every node's shared place (README "Spectate").
|
|
370
|
+
SPECTATE = "*/@shared/spectate/*/state"
|
|
371
|
+
# The sender's object that answers a table's current spectate value
|
|
372
|
+
# (Host::SPECTATE_OBJECT): <node>/<app>/spectate, state(table).
|
|
373
|
+
SPECTATE_OBJECT = "spectate"
|
|
273
374
|
STALE_S = 15
|
|
274
375
|
|
|
275
376
|
module_function
|
|
@@ -289,18 +390,20 @@ module AsterismGames
|
|
|
289
390
|
info.puts "commentate: hands hidden (--public)" if opts[:public]
|
|
290
391
|
open_browser(server.url, err) if opts[:open]
|
|
291
392
|
prefix = room_prefix(opts[:namespace])
|
|
393
|
+
refetch = nil
|
|
292
394
|
connect(opts, err) do |net|
|
|
293
395
|
net.on_leave { |n| hub.host_left(n) }
|
|
294
396
|
net.on_join { |n| hub.host_back(n) }
|
|
397
|
+
refetch = Refetch.new(hub, call: ->(from, name) { net["#{from}/#{SPECTATE_OBJECT}", timeout: 2].state(name) },
|
|
398
|
+
log: ->(m) { err.puts "commentate: #{m}" }).start
|
|
399
|
+
net.on_app_leave { |n, a| refetch.forget("#{n}/#{a}") }
|
|
295
400
|
net.start
|
|
296
|
-
net.
|
|
297
|
-
|
|
298
|
-
_, host, _, name, = sample.key.to_s.split("/")
|
|
401
|
+
net.subscribe(SPECTATE) do |v, info|
|
|
402
|
+
host, name = spectate_source(info)
|
|
299
403
|
next unless wanted?(host, name, opts[:table], prefix)
|
|
300
|
-
|
|
301
|
-
hub.update(host, name, v)
|
|
404
|
+
refetch.came(info, host, name) { hub.update(host, name, v) }
|
|
302
405
|
rescue StandardError => e
|
|
303
|
-
err.puts "commentate: #{
|
|
406
|
+
err.puts "commentate: #{info.key}: #{e.class}: #{e.message}"
|
|
304
407
|
end
|
|
305
408
|
stop = Thread::Queue.new
|
|
306
409
|
%w[INT TERM].each { |s| trap(s) { stop << true } }
|
|
@@ -310,6 +413,7 @@ module AsterismGames
|
|
|
310
413
|
end
|
|
311
414
|
end
|
|
312
415
|
ensure
|
|
416
|
+
refetch&.stop
|
|
313
417
|
server&.stop
|
|
314
418
|
voice&.stop
|
|
315
419
|
ai&.stop
|
|
@@ -349,6 +453,17 @@ module AsterismGames
|
|
|
349
453
|
end
|
|
350
454
|
end
|
|
351
455
|
|
|
456
|
+
# [host node, table] of a spectate value: the node of its sender
|
|
457
|
+
# (info.from, "<node>/<app>": the host's games or a room's room<slot>,
|
|
458
|
+
# the same node), else the key's; the table from the name under
|
|
459
|
+
# @shared/ (spectate/<table>/state).
|
|
460
|
+
def spectate_source(info)
|
|
461
|
+
from = info.from.to_s
|
|
462
|
+
host = from.include?("/") ? from.split("/").first : info.node
|
|
463
|
+
parts = info.name.to_s.split("/")
|
|
464
|
+
[host, parts[0] == "spectate" ? parts[1] : nil]
|
|
465
|
+
end
|
|
466
|
+
|
|
352
467
|
# --namespace room/01: the room's tables are room01-<table> outside it.
|
|
353
468
|
def room_prefix(ns)
|
|
354
469
|
return nil if ns.to_s.empty?
|
|
@@ -363,8 +478,8 @@ module AsterismGames
|
|
|
363
478
|
parts.size >= 3 ? "#{parts[0]}/#{parts[-1]}" : t
|
|
364
479
|
end
|
|
365
480
|
|
|
366
|
-
# --table: <table>, <host>/<table>, <host
|
|
367
|
-
# play's <host>/games/<table>.
|
|
481
|
+
# --table: <table>, <host>/<table>, <host>/@shared/spectate/<table>
|
|
482
|
+
# or the play's <host>/games/<table>.
|
|
368
483
|
def wanted?(host, name, table, prefix)
|
|
369
484
|
return false if host.nil? || name.nil?
|
|
370
485
|
return false if prefix && !name.start_with?(prefix)
|
|
@@ -34,7 +34,7 @@ module AsterismGames
|
|
|
34
34
|
--namespace PREFIX a room's tables (room/01: their spectate keys room01-<table>,
|
|
35
35
|
outside the room; the connection itself has no namespace)
|
|
36
36
|
--table TABLE the table to show first (<table>, <host>/<table>, or
|
|
37
|
-
<host
|
|
37
|
+
<host>/@shared/spectate/<table>); without it every table that puts
|
|
38
38
|
its spectate key is listed and the first one shown
|
|
39
39
|
--node NODE the node ID (default: commentate-<host>-<pid>, or the .tar's name)
|
|
40
40
|
--port N the page's port on 127.0.0.1 (default %<port>d; taken: the next free)
|
data/lib/asterism_games/game.rb
CHANGED
|
@@ -36,6 +36,17 @@ module AsterismGames
|
|
|
36
36
|
# SAVE_HOLD_MS at most one write of the saved state this often (0: after
|
|
37
37
|
# every change); a game that changes many times a second
|
|
38
38
|
# (the board's strokes) holds its writes
|
|
39
|
+
# TYPES the types of what the host puts (asterism's docs/protocol.md
|
|
40
|
+
# 11.2): {name => [[[field, type], ...], text]}, told to the
|
|
41
|
+
# readers by the host's @profile?types
|
|
42
|
+
# STATE_TYPE the type of <table>/state (a name of TYPES; nil: none)
|
|
43
|
+
# TOLD_TYPES {sub => type} of what the game tells (tell(sub, value))
|
|
44
|
+
# SPECTATE_TYPE the type of what spectate_view gives (a name of
|
|
45
|
+
# SPECTATE_TYPES; nil: none), the topic
|
|
46
|
+
# spectate/<table>/state of the node's shared place
|
|
47
|
+
# (README "Spectate")
|
|
48
|
+
# SPECTATE_TYPES the types of it, as TYPES; defined on the connection
|
|
49
|
+
# that puts it (a connection's types are its own)
|
|
39
50
|
#
|
|
40
51
|
# and writes the rules: tick(now), public_view(now) (and spectator_view,
|
|
41
52
|
# summary when they differ; spectate_view and spectate_delay_ms for what
|
|
@@ -60,6 +71,13 @@ module AsterismGames
|
|
|
60
71
|
class Game
|
|
61
72
|
SAVE_FORMAT = 1
|
|
62
73
|
SAVE_HOLD_MS = 0
|
|
74
|
+
TYPES = {}.freeze
|
|
75
|
+
STATE_TYPE = nil
|
|
76
|
+
TOLD_TYPES = {}.freeze
|
|
77
|
+
SPECTATE_TYPE = nil
|
|
78
|
+
SPECTATE_TYPES = {}.freeze
|
|
79
|
+
# The namespace of the types' names (asterism-games/<game>.<what>).
|
|
80
|
+
TYPE_PREFIX = "asterism-games/"
|
|
63
81
|
MAX_MEMBERS = 16
|
|
64
82
|
NAME_MAX = 32
|
|
65
83
|
|
|
@@ -156,7 +174,8 @@ module AsterismGames
|
|
|
156
174
|
end
|
|
157
175
|
|
|
158
176
|
# What those who watch from outside the table see (README "Spectate"):
|
|
159
|
-
# the host puts it on asterism/<node
|
|
177
|
+
# the host puts it on asterism/<node>/@shared/spectate/<table>/state
|
|
178
|
+
# (a topic of the node's shared place, of type SPECTATE_TYPE; a room's
|
|
160
179
|
# table: room<slot>-<table>), a key no player's role reads and outside
|
|
161
180
|
# every room, after spectate_delay_ms. It may hold what the players do
|
|
162
181
|
# not see of each other (the mahjong hands), never what nobody knows
|