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.
@@ -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
- SPECTATE = "asterism/*/spectate/*/state"
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.session.subscribe(SPECTATE) do |sample|
297
- next if sample.respond_to?(:kind) && sample.kind == :delete
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
- v = Asterism::Codec.unpack(sample.payload)
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: #{sample.key}: #{e.class}: #{e.message}"
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>/spectate/<table> or the
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>/spectate/<table>); without it every table that puts
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)
@@ -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>/spectate/<table>/state (a room's
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