asterism-games 0.22.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 +51 -0
- data/README.md +164 -59
- 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 +3 -9
|
@@ -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
|
data/lib/asterism_games/host.rb
CHANGED
|
@@ -13,14 +13,24 @@ module AsterismGames
|
|
|
13
13
|
# table in the settings, moves the games on, saves them after every change
|
|
14
14
|
# and puts their public state on asterism/<node>/<app>/<table>/state.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# a
|
|
16
|
+
# What it puts on its application's keys are topics (asterism's
|
|
17
|
+
# docs/protocol.md 9-11): <table>/state and what a game tells
|
|
18
|
+
# (<table>/<sub>), declared with the game's types (Game::STATE_TYPE,
|
|
19
|
+
# TOLD_TYPES) and the types defined (Game::TYPES), so a reader finds them
|
|
20
|
+
# with @profile?pubs;types before a value comes, and each value carries
|
|
21
|
+
# its sequence number.
|
|
22
|
+
#
|
|
23
|
+
# Every game lives under the host's application (config "app"), each as
|
|
24
|
+
# an object of its own name. The games come from the registry
|
|
25
|
+
# (AsterismGames.games): a table's "game" names one. What spectators see
|
|
26
|
+
# from outside the tables goes on the node's shared place
|
|
27
|
+
# (asterism/<node>/@shared/spectate/..., README "Spectate"), declared by
|
|
28
|
+
# the host's application, and in a room by the room's application
|
|
29
|
+
# room<slot> outside the room.
|
|
20
30
|
#
|
|
21
31
|
# The connection is Asterism's block API (Asterism.connect { |net| }):
|
|
22
32
|
# the tables are exposed with caller: :from, and the calls are answered
|
|
23
|
-
# on the
|
|
33
|
+
# on the connection's receiving thread. The games move on in this host's own
|
|
24
34
|
# loop (step: timeouts, CPU turns, the puts), which takes Asterism::LOCK,
|
|
25
35
|
# the lock every tick of the receiving thread holds, so a call and a step
|
|
26
36
|
# never meet inside a game.
|
|
@@ -180,6 +190,7 @@ module AsterismGames
|
|
|
180
190
|
net.expose(name, @remotes[name], methods: @methods[name], caller: :from, **extra)
|
|
181
191
|
end
|
|
182
192
|
@net = net
|
|
193
|
+
declare_topics(net)
|
|
183
194
|
@connected = true
|
|
184
195
|
@tables_status = TablesStatus.new(net, node, @config["app"]) if tables_status?
|
|
185
196
|
write_status
|
|
@@ -197,7 +208,7 @@ module AsterismGames
|
|
|
197
208
|
@log.call("games: connection lost: #{net.lost_reason}")
|
|
198
209
|
return
|
|
199
210
|
end
|
|
200
|
-
|
|
211
|
+
open_room_app
|
|
201
212
|
::Asterism::LOCK.synchronize { step }
|
|
202
213
|
sleep LOOP_S
|
|
203
214
|
end
|
|
@@ -208,21 +219,11 @@ module AsterismGames
|
|
|
208
219
|
# runs other applications too is away as soon as its table app closes.
|
|
209
220
|
PLAYER_APP = "game"
|
|
210
221
|
|
|
211
|
-
# Who is there: Asterism's on_app_join / on_app_leave
|
|
212
|
-
# 0.7.1), else a liveliness watch of asterism/*/game on the session.
|
|
222
|
+
# Who is there: Asterism's on_app_join / on_app_leave.
|
|
213
223
|
def follow_players(net)
|
|
214
224
|
app = @config["player_app"] || PLAYER_APP
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
net.on_app_leave { |n, a| node_left(n) if a == app }
|
|
218
|
-
else
|
|
219
|
-
own = "asterism/#{node}/"
|
|
220
|
-
net.session.liveliness_watch("asterism/*/#{app}") do |key, alive|
|
|
221
|
-
next if key.start_with?(own)
|
|
222
|
-
n = key.split("/")[1]
|
|
223
|
-
alive ? node_joined(n) : node_left(n)
|
|
224
|
-
end
|
|
225
|
-
end
|
|
225
|
+
net.on_app_join { |n, a| node_joined(n) if a == app }
|
|
226
|
+
net.on_app_leave { |n, a| node_left(n) if a == app }
|
|
226
227
|
end
|
|
227
228
|
|
|
228
229
|
# A node's table app left the network: its members are away at every
|
|
@@ -237,8 +238,9 @@ module AsterismGames
|
|
|
237
238
|
end
|
|
238
239
|
|
|
239
240
|
def down
|
|
240
|
-
|
|
241
|
+
close_room_app
|
|
241
242
|
@net = nil
|
|
243
|
+
@topics = {}
|
|
242
244
|
@tables_status = nil
|
|
243
245
|
@connected = false
|
|
244
246
|
write_status
|
|
@@ -251,7 +253,7 @@ module AsterismGames
|
|
|
251
253
|
# no nodes, no tokens (the host's own node ID only, once known).
|
|
252
254
|
def status_now(now = @clock.call)
|
|
253
255
|
{ "pid" => Process.pid, "at" => Time.now.to_i, "node" => @node, "connected" => @connected ? true : false,
|
|
254
|
-
"reconnecting" => (@connected &&
|
|
256
|
+
"reconnecting" => (@connected && @net&.reconnecting?) ? true : false,
|
|
255
257
|
"tables" => @games.transform_values { |g| g.summary(now) } }.tap do |h|
|
|
256
258
|
h["ai"] = @ai.budget.summary if @ai&.any?
|
|
257
259
|
end
|
|
@@ -342,7 +344,7 @@ module AsterismGames
|
|
|
342
344
|
# Asterism puts them only when they differ from the last put.
|
|
343
345
|
def tell_tables(now, changed: false)
|
|
344
346
|
ts = @tables_status
|
|
345
|
-
return unless ts && @net &&
|
|
347
|
+
return unless ts && @net && !@net.reconnecting?
|
|
346
348
|
return unless changed || now - @tables_at.to_i >= TABLES_STATUS_EVERY_MS
|
|
347
349
|
@tables_at = now
|
|
348
350
|
bytes = ts.set(tables_status(now))
|
|
@@ -353,28 +355,58 @@ module AsterismGames
|
|
|
353
355
|
|
|
354
356
|
def put_state(name, game, now)
|
|
355
357
|
@put_at[name] = now
|
|
356
|
-
return unless @net &&
|
|
357
|
-
key = "asterism/#{node}/#{@config['app']}/#{name}/state"
|
|
358
|
+
return unless @net && !@net.reconnecting?
|
|
358
359
|
# What everybody may see, as a spectator sees it (README "Spectators").
|
|
359
|
-
|
|
360
|
-
note_size("put #{name}/state",
|
|
361
|
-
|
|
360
|
+
view = game.state_view(now)
|
|
361
|
+
note_size("put #{name}/state", "asterism/#{node}/#{@config['app']}/#{name}/state".bytesize +
|
|
362
|
+
::Asterism::Codec.pack(view).bytesize)
|
|
363
|
+
topic(name, "state").put(view)
|
|
362
364
|
rescue ::Asterism::Error => e
|
|
363
365
|
@log.call("games: put #{name}: #{e.message}")
|
|
364
366
|
end
|
|
365
367
|
|
|
366
368
|
# What a game told (Game#tell): put on asterism/<node>/<app>/<table>/<sub>.
|
|
367
369
|
def put_told(name, sub, value)
|
|
368
|
-
return unless @net &&
|
|
370
|
+
return unless @net && !@net.reconnecting?
|
|
369
371
|
key = "asterism/#{node}/#{@config['app']}/#{name}/#{sub}"
|
|
370
|
-
|
|
371
|
-
note_size("put #{name}/#{sub}", key.bytesize + payload.bytesize)
|
|
372
|
+
note_size("put #{name}/#{sub}", key.bytesize + ::Asterism::Codec.pack(value).bytesize)
|
|
372
373
|
@stats["told"] += 1
|
|
373
|
-
|
|
374
|
+
topic(name, sub).put(value)
|
|
374
375
|
rescue ::Asterism::Error => e
|
|
375
376
|
@log.call("games: put #{name}/#{sub}: #{e.message}")
|
|
376
377
|
end
|
|
377
378
|
|
|
379
|
+
# ------------------------------------------------------------ topics
|
|
380
|
+
|
|
381
|
+
# Connected: the games' types defined on the connection, and every
|
|
382
|
+
# table's state and told keys declared as topics of this application,
|
|
383
|
+
# and outside the rooms the tables' spectate keys (the types and the
|
|
384
|
+
# topics end with the connection).
|
|
385
|
+
def declare_topics(net)
|
|
386
|
+
@topics = {}
|
|
387
|
+
@games.values.map(&:class).uniq.each do |klass|
|
|
388
|
+
klass::TYPES.each { |tname, (fields, text)| net.define_type(tname, fields, desc: text) }
|
|
389
|
+
end
|
|
390
|
+
@games.each do |name, g|
|
|
391
|
+
topic(name, "state")
|
|
392
|
+
g.class::TOLD_TYPES.each_key { |sub| topic(name, sub) }
|
|
393
|
+
end
|
|
394
|
+
# Outside the rooms the spectate keys are this application's too
|
|
395
|
+
# (in the node's shared place); a room's go on its own application.
|
|
396
|
+
declare_spectate(net) unless @config["namespace"]
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# The topic <table>/<sub> of this connection (declared the first time,
|
|
400
|
+
# with the game's type for it).
|
|
401
|
+
def topic(name, sub)
|
|
402
|
+
@topics ||= {}
|
|
403
|
+
@topics[[name, sub]] ||= begin
|
|
404
|
+
klass = @games[name].class
|
|
405
|
+
type = sub == "state" ? klass::STATE_TYPE : klass::TOLD_TYPES[sub]
|
|
406
|
+
type ? @net.publish("#{name}/#{sub}", type: type) : @net.publish("#{name}/#{sub}")
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
378
410
|
# ------------------------------------------------------------ spectate
|
|
379
411
|
#
|
|
380
412
|
# What a game gives for those who watch from outside the table
|
|
@@ -385,31 +417,45 @@ module AsterismGames
|
|
|
385
417
|
# ends, the delay is chosen again); a host started again starts with
|
|
386
418
|
# nothing kept. The last one put is put again every PUT_EVERY_MS, so a
|
|
387
419
|
# new subscriber has it within that.
|
|
420
|
+
#
|
|
421
|
+
# The keys are in the node's shared place (asterism's docs/protocol.md
|
|
422
|
+
# 17): asterism/<node>/@shared/spectate/<table>/state, a chunk no
|
|
423
|
+
# player's role (asterism/*/games/**) and no room reaches. Each is a
|
|
424
|
+
# topic declared with shared: :node by the application that puts it,
|
|
425
|
+
# with the game's SPECTATE_TYPE defined on that connection
|
|
426
|
+
# (Game::SPECTATE_TYPES): outside the rooms the host's own application
|
|
427
|
+
# (config "app"); in a room, the room's application room<slot>
|
|
428
|
+
# (room_app), a connection of its own outside the room's namespace.
|
|
388
429
|
|
|
389
|
-
# The application chunk of the spectate keys: not the host's app (the
|
|
390
|
-
# players' role reads asterism/*/<app>/**), and outside every room.
|
|
391
|
-
SPECTATE_APP = "spectate"
|
|
392
430
|
# What one table keeps for its delay at most (bytes); the oldest goes
|
|
393
431
|
# first (it is then never shown, rather than shown early).
|
|
394
432
|
SPECTATE_KEEP_BYTES = 8 * 1024 * 1024
|
|
395
|
-
# How often a room's host tries again to open its
|
|
433
|
+
# How often a room's host tries again to open its application outside
|
|
396
434
|
# the room (ms).
|
|
397
435
|
SPECTATE_RETRY_MS = 30_000
|
|
398
436
|
|
|
399
|
-
#
|
|
400
|
-
# room<slot>-<table> there,
|
|
401
|
-
# never meet.
|
|
402
|
-
def
|
|
437
|
+
# The name of a table's spectate topic in the node's shared place:
|
|
438
|
+
# spectate/<table>/state; a room's table is room<slot>-<table> there,
|
|
439
|
+
# so the rooms' tables and the permanent ones never meet.
|
|
440
|
+
def spectate_name(name)
|
|
403
441
|
table = @config["namespace"] ? "#{spectate_label}-#{name}" : name
|
|
404
|
-
"
|
|
442
|
+
"spectate/#{table}/state"
|
|
405
443
|
end
|
|
406
444
|
|
|
407
|
-
#
|
|
408
|
-
|
|
445
|
+
# The whole key: asterism/<node>/@shared/spectate/<table>/state.
|
|
446
|
+
def spectate_key(name)
|
|
447
|
+
"asterism/#{node}/@shared/#{spectate_name(name)}"
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# A room's part of its tables' spectate names, and the name of its
|
|
451
|
+
# application outside the room: the setting (Rooms gives room<slot>),
|
|
452
|
+
# else the namespace without its slashes.
|
|
409
453
|
def spectate_label
|
|
410
454
|
@config["spectate_label"] || @config["namespace"].to_s.delete("/")
|
|
411
455
|
end
|
|
412
456
|
|
|
457
|
+
alias room_app spectate_label
|
|
458
|
+
|
|
413
459
|
# Takes what the game gives now (nothing when it gives nil, or the same
|
|
414
460
|
# as last time).
|
|
415
461
|
def take_spectate(name, game, now)
|
|
@@ -446,63 +492,161 @@ module AsterismGames
|
|
|
446
492
|
|
|
447
493
|
def send_spectate(name, bytes, now)
|
|
448
494
|
@spec[name]["at"] = now
|
|
449
|
-
sess = spectate_session or return
|
|
450
|
-
spectate_token(sess)
|
|
451
|
-
key = spectate_key(name)
|
|
452
495
|
# Counted apart from the players' messages (the hands make it larger).
|
|
453
|
-
@stats["spectate_max_bytes"] = [@stats["spectate_max_bytes"],
|
|
454
|
-
|
|
496
|
+
@stats["spectate_max_bytes"] = [@stats["spectate_max_bytes"], spectate_key(name).bytesize + bytes.bytesize].max
|
|
497
|
+
topic = spectate_topic(name) or return
|
|
498
|
+
topic.put(::Asterism::Codec.unpack(bytes))
|
|
455
499
|
rescue ::Asterism::Error => e
|
|
456
500
|
@log.call("games: put spectate #{name}: #{e.message}")
|
|
457
501
|
end
|
|
458
502
|
|
|
459
|
-
#
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
#
|
|
472
|
-
#
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
@
|
|
495
|
-
@
|
|
503
|
+
# Whether the game puts something for those who watch from outside.
|
|
504
|
+
def spectates?(game)
|
|
505
|
+
Host.spectates?(game.class)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Whether a game class gives a spectate view (Game#spectate_view
|
|
509
|
+
# replaced).
|
|
510
|
+
def self.spectates?(klass)
|
|
511
|
+
klass.instance_method(:spectate_view).owner != Game
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# The connection that puts the spectate keys, when it can put now: the
|
|
515
|
+
# host's own outside the rooms, the room's application in a room (nil
|
|
516
|
+
# while it is not open, or with the slot's certificate).
|
|
517
|
+
def spectate_conn
|
|
518
|
+
c = @config["namespace"] ? @room : @net
|
|
519
|
+
c && c.connected? && !c.reconnecting? ? c : nil
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# The topic spectate/<table>/state of the node's shared place on the
|
|
523
|
+
# spectate connection (declared the first time, with the game's type
|
|
524
|
+
# for it), or nil when that connection cannot put now.
|
|
525
|
+
def spectate_topic(name)
|
|
526
|
+
conn = spectate_conn or return nil
|
|
527
|
+
@spectate_topics ||= {}
|
|
528
|
+
@spectate_topics[name] ||= begin
|
|
529
|
+
type = @games[name].class::SPECTATE_TYPE
|
|
530
|
+
type ? conn.publish(spectate_name(name), shared: :node, type: type) : conn.publish(spectate_name(name), shared: :node)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Declares on conn the spectate types and the topics of the tables
|
|
535
|
+
# that give a spectate view, and exposes the object that answers their
|
|
536
|
+
# current values (called when conn is up).
|
|
537
|
+
def declare_spectate(conn)
|
|
538
|
+
@spectate_topics = {}
|
|
539
|
+
spectating = @games.select { |_n, g| spectates?(g) }
|
|
540
|
+
return if spectating.empty?
|
|
541
|
+
spectating.values.map(&:class).uniq.each do |klass|
|
|
542
|
+
klass::SPECTATE_TYPES.each { |tname, (fields, text)| conn.define_type(tname, fields, desc: text) }
|
|
543
|
+
end
|
|
544
|
+
spectating.each_key { |name| spectate_topic(name) }
|
|
545
|
+
expose_spectate(conn)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# The object <node>/<app>/spectate of the application that puts the
|
|
549
|
+
# spectate keys (the host's own outside the rooms, room<slot> in a
|
|
550
|
+
# room): state(table) answers the value last put on
|
|
551
|
+
# spectate/<table>/state (README "Spectate"). Asterism's protocol 2
|
|
552
|
+
# keeps no current value on the network: a reader that comes late, or
|
|
553
|
+
# missed values (info.lost) or saw the sender start again
|
|
554
|
+
# (info.restarted?), asks here (the commentary does).
|
|
555
|
+
SPECTATE_OBJECT = "spectate"
|
|
556
|
+
SPECTATE_DOC = "The tables watched from outside (README \"Spectate\"): their spectate values."
|
|
557
|
+
SPECTATE_METHODS = {
|
|
558
|
+
"state" => { args: ["table"], doc: "The value last put on @shared/spectate/<table>/state (as old as the " \
|
|
559
|
+
"table's spectator delay), or nil when none was put yet." }
|
|
560
|
+
}.freeze
|
|
561
|
+
|
|
562
|
+
def expose_spectate(conn)
|
|
563
|
+
if @games.key?(SPECTATE_OBJECT)
|
|
564
|
+
@log.call("games: a table named #{SPECTATE_OBJECT}: its current spectate values are not answered")
|
|
565
|
+
return
|
|
566
|
+
end
|
|
567
|
+
methods = META_DOCS ? SPECTATE_METHODS : { "state" => 1 }
|
|
568
|
+
extra = META_DOCS ? { doc: SPECTATE_DOC } : {}
|
|
569
|
+
conn.expose(SPECTATE_OBJECT, SpectateRemote.new(self), methods: methods, **extra)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# The value last put on spectate/<table>/state (unpacked), nil when
|
|
573
|
+
# there is none (no such table, nothing put since the host started).
|
|
574
|
+
# table: the part of the name under spectate/ (room<slot>-<table> in a
|
|
575
|
+
# room). Called on a receiving thread, which holds Asterism::LOCK.
|
|
576
|
+
def spectate_now(table)
|
|
577
|
+
name = @games.keys.find { |n| spectate_name(n) == "spectate/#{table}/state" } or return nil
|
|
578
|
+
out = @spec.dig(name, "out") or return nil
|
|
579
|
+
::Asterism::Codec.unpack(out)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# The exposed spectate object (Host#spectate_now).
|
|
583
|
+
class SpectateRemote
|
|
584
|
+
def initialize(host)
|
|
585
|
+
@host = host
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def state(table)
|
|
589
|
+
@host.spectate_now(table.to_s)
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# ---- in a room: the room's application outside the room
|
|
594
|
+
#
|
|
595
|
+
# Every key a room's host writes on its own connection gets the room's
|
|
596
|
+
# namespace, which every seat of the room reads. Its tables' spectate
|
|
597
|
+
# keys go on a second Asterism connection without the namespace, the
|
|
598
|
+
# application <node>/room<slot> (asterism's docs/protocol.md 1.1: one
|
|
599
|
+
# application a connection, several in one program), with the host's
|
|
600
|
+
# own certificate only (the slot's certificate of a console before
|
|
601
|
+
# 0.3.0 is the room's: spectate_outside false, nothing is put). Its
|
|
602
|
+
# liveliness token, @profile and @pubs are its own, so every room is
|
|
603
|
+
# an application of its own beside the permanent tables' games.
|
|
604
|
+
|
|
605
|
+
# The room's application, when the host is connected and one of its
|
|
606
|
+
# games gives a spectate view; opened again after a failure (or a lost
|
|
607
|
+
# connection) every SPECTATE_RETRY_MS. Outside the lock: opening waits
|
|
608
|
+
# for the router.
|
|
609
|
+
def open_room_app(now = @clock.call)
|
|
610
|
+
# Rooms sets spectate_outside only for the host's own certificate.
|
|
611
|
+
return unless @config["namespace"] && @config["spectate_outside"]
|
|
612
|
+
return unless @net && @games.values.any? { |g| spectates?(g) }
|
|
613
|
+
if @room && !@room.running?
|
|
614
|
+
@log.call("games: #{node}/#{room_app}: connection lost: #{@room.lost_reason}")
|
|
615
|
+
close_room_app
|
|
616
|
+
end
|
|
617
|
+
return if @room || now < @room_retry_at.to_i
|
|
618
|
+
@room_retry_at = now + SPECTATE_RETRY_MS
|
|
619
|
+
opts = { node: node, app: room_app }
|
|
620
|
+
opts[:credentials] = @config["credentials"] if @config["credentials"]
|
|
621
|
+
conn = ::Asterism.connect(locator, **opts)
|
|
622
|
+
begin
|
|
623
|
+
conn.on_error { |e, where| @log.call("games: #{node}/#{room_app}: #{where}: #{e.class}: #{e.message}") }
|
|
624
|
+
::Asterism::LOCK.synchronize { room_up(conn) }
|
|
625
|
+
conn.start
|
|
626
|
+
rescue StandardError
|
|
627
|
+
@room = nil
|
|
628
|
+
conn.close
|
|
629
|
+
raise
|
|
630
|
+
end
|
|
631
|
+
@log.call("games: #{node}/#{room_app} up outside #{@config['namespace']} " \
|
|
632
|
+
"(#{@spectate_topics.keys.map { |n| "@shared/#{spectate_name(n)}" }.join(', ')})")
|
|
633
|
+
rescue ::Asterism::Error, ArgumentError, SystemCallError => e
|
|
634
|
+
@log.call("games: cannot open #{node}/#{room_app} outside #{@config['namespace']}: #{e.message}; " \
|
|
496
635
|
"again in #{SPECTATE_RETRY_MS / 1000} s")
|
|
497
636
|
end
|
|
498
637
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
@
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
638
|
+
# Takes conn as the room's application: its types and topics.
|
|
639
|
+
def room_up(conn)
|
|
640
|
+
@room = conn
|
|
641
|
+
declare_spectate(conn)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def close_room_app
|
|
645
|
+
@room_retry_at = nil # a new connection tries at once
|
|
646
|
+
app = @room
|
|
647
|
+
@room = nil
|
|
648
|
+
@spectate_topics = {}
|
|
649
|
+
app&.close
|
|
506
650
|
rescue ::Asterism::Error
|
|
507
651
|
nil
|
|
508
652
|
end
|
|
@@ -520,33 +664,21 @@ module AsterismGames
|
|
|
520
664
|
@games.each_value { |g| g.save(now, force: true) }
|
|
521
665
|
end
|
|
522
666
|
|
|
523
|
-
# The host's status: Asterism's (net.status,
|
|
524
|
-
#
|
|
525
|
-
#
|
|
526
|
-
# the same thing by hand on the connection's session (a queryable on
|
|
527
|
-
# the key and a put when the counts change).
|
|
667
|
+
# The host's status: Asterism's (net.status), which puts it on
|
|
668
|
+
# asterism/<node>/<app>/@status when it changes and answers who asks its
|
|
669
|
+
# profile (@profile?status, asterism's docs/protocol.md 8).
|
|
528
670
|
class TablesStatus
|
|
529
671
|
def initialize(net, node, app)
|
|
530
672
|
@net = net
|
|
531
673
|
@key = "asterism/#{node}/#{app}/@status"
|
|
532
|
-
return if @net.respond_to?(:status)
|
|
533
|
-
@bytes = nil
|
|
534
|
-
@qa = net.session.queryable(@key) { |q| q.reply(@key, @bytes) if @bytes }
|
|
535
674
|
end
|
|
536
675
|
|
|
537
676
|
# Sets the status; returns the size of the put (key and payload) when
|
|
538
677
|
# it changed, nil when it did not.
|
|
539
678
|
def set(st)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
@net.status(st)
|
|
544
|
-
else
|
|
545
|
-
return nil if bytes == @bytes
|
|
546
|
-
@bytes = bytes
|
|
547
|
-
@net.session.put(@key, bytes)
|
|
548
|
-
end
|
|
549
|
-
@key.bytesize + bytes.bytesize
|
|
679
|
+
return nil if @net.status == st
|
|
680
|
+
@net.status(st)
|
|
681
|
+
@key.bytesize + ::Asterism::Codec.pack(st).bytesize
|
|
550
682
|
end
|
|
551
683
|
end
|
|
552
684
|
|
|
@@ -39,6 +39,18 @@ module AsterismGames
|
|
|
39
39
|
"OTHELLO_AI_TURN_SECONDS" => ["ai_turn_ms", 1000] }
|
|
40
40
|
|
|
41
41
|
SAVE_FORMAT = 1
|
|
42
|
+
# The state's type (public_view's keys; mb, the members as [name,
|
|
43
|
+
# seat, flags], an any[]).
|
|
44
|
+
STATE_TYPE = "#{TYPE_PREFIX}othello.state".freeze
|
|
45
|
+
TYPES = {
|
|
46
|
+
STATE_TYPE => [[%w[v int], %w[t str], %w[st str], %w[nm str[]], %w[sk int[]], %w[aw int], %w[ow int],
|
|
47
|
+
%w[mb any[]], %w[ts int], %w[ai str[]], %w[ak str[]], %w[bd str], %w[tn int], %w[n int], %w[last str],
|
|
48
|
+
%w[lg str], %w[cnt int[]], %w[dl int], ["res", "#{TYPE_PREFIX}othello.result"]],
|
|
49
|
+
"An othello table as everybody sees it (asterism-games README \"Views\" of othello); " \
|
|
50
|
+
"mb: the members as [name, seat, flags]"],
|
|
51
|
+
"#{TYPE_PREFIX}othello.result" => [[%w[w int], %w[why str], %w[b int], %w[wc int]],
|
|
52
|
+
"How a game ended: the seat that won (-1 a draw), why, the counts"]
|
|
53
|
+
}.freeze
|
|
42
54
|
SEATS = 2
|
|
43
55
|
DEFAULTS = AsterismGames::Game::DEFAULTS.merge(
|
|
44
56
|
"turn_ms" => 0, # a human's time for a move (0: none); then the CPU plays it
|
|
@@ -176,9 +176,10 @@ module AsterismGames
|
|
|
176
176
|
host, app, tname = @path.split("/")
|
|
177
177
|
# The state the host puts after every change: a newer one wakes the
|
|
178
178
|
# ticker (no call waits on the receiving thread).
|
|
179
|
-
@net.
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
@net.subscribe("#{host}/#{app}/#{tname}/state") do |v, info|
|
|
180
|
+
# Values lost, or the host started again (protocol 2 keeps no
|
|
181
|
+
# current value): the view is asked again whatever this one says.
|
|
182
|
+
@wake << true if Play.ask_again?(info) || (v.is_a?(Hash) && (!@view.is_a?(Hash) || v["v"] != @view["v"]))
|
|
182
183
|
end
|
|
183
184
|
refresh
|
|
184
185
|
@timer = Thread.new { timer_loop } if @out.tty?
|
|
@@ -106,9 +106,10 @@ module AsterismGames
|
|
|
106
106
|
# The state the host puts after every change (and every 5 s): a newer
|
|
107
107
|
# one wakes the thread that asks the view (no call waits on the
|
|
108
108
|
# receiving thread).
|
|
109
|
-
@net.
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
@net.subscribe("#{host}/#{app}/#{tname}/state") do |v, info|
|
|
110
|
+
# Values lost, or the host started again (protocol 2 keeps no
|
|
111
|
+
# current value): the view is asked again whatever this one says.
|
|
112
|
+
@wake << true if Play.ask_again?(info) || (v.is_a?(Hash) && (@view.nil? || v["v"] != @view["v"]))
|
|
112
113
|
end
|
|
113
114
|
refresh
|
|
114
115
|
# And a view every VIEW_EVERY_S anyway: the host sees that this member
|
data/lib/asterism_games/play.rb
CHANGED
|
@@ -154,6 +154,15 @@ module AsterismGames
|
|
|
154
154
|
end
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
+
# Whether a value of a table's state says to ask the table again
|
|
158
|
+
# (Asterism's protocol 2 keeps no current value on the network): values
|
|
159
|
+
# were lost before it (info.lost) or the host started a new run
|
|
160
|
+
# (info.restarted?). The play clients then ask their view at once.
|
|
161
|
+
def ask_again?(info)
|
|
162
|
+
return false unless info.respond_to?(:lost)
|
|
163
|
+
info.lost.to_i.positive? || info.restarted? ? true : false
|
|
164
|
+
end
|
|
165
|
+
|
|
157
166
|
# The first table whose name starts with prefix (looking again whenever
|
|
158
167
|
# a node joins, for wait seconds); nil when none.
|
|
159
168
|
def find_tables(net, prefix, joined, wait: 10)
|
data/lib/asterism_games/rooms.rb
CHANGED
|
@@ -23,7 +23,8 @@ module AsterismGames
|
|
|
23
23
|
# for the room, which joins the router in the room's key space
|
|
24
24
|
# (namespace room/<slot>) and serves the tables there, as browsers in the
|
|
25
25
|
# room see them (asterism/games/games/<table>). One process per room,
|
|
26
|
-
# since
|
|
26
|
+
# since every room's host is the same <node>/<app>, which one program
|
|
27
|
+
# opens once (the asterism gem refuses a second copy). Other rooms
|
|
27
28
|
# (another app, or an app no configured table's game shows) get nothing.
|
|
28
29
|
#
|
|
29
30
|
# Which certificate (console 0.3.0, its docs/rooms.md "Seats"): when
|
|
@@ -264,7 +265,8 @@ module AsterismGames
|
|
|
264
265
|
# "Rooms", "Who joins"); trust: trust_for's; app: what the room's link
|
|
265
266
|
# opens (its game's tables; nil: every table of rooms.tables); group:
|
|
266
267
|
# its invite link's group (nil: none), kept with its games' records.
|
|
267
|
-
# spectate_label
|
|
268
|
+
# spectate_label (the room's application and its tables' spectate
|
|
269
|
+
# names) / spectate_outside: README "Spectate".
|
|
268
270
|
def child_config(slot, tar, dir, room_id = nil, trust = { "own" => false, "seats" => [], "prefix" => nil }, app = nil,
|
|
269
271
|
group = nil)
|
|
270
272
|
tables = app ? Config.tables_for_app(@rooms["tables"], app) : @rooms["tables"]
|
|
@@ -275,10 +277,11 @@ module AsterismGames
|
|
|
275
277
|
"trusted_nodes" => trust["seats"], "trusted_prefix" => trust["prefix"],
|
|
276
278
|
"seat_reuse_ms" => @rooms["seat_reuse_seconds"].to_i * 1000, "ai_usage_file" => ai_usage_file,
|
|
277
279
|
"records_db" => records_db, "records_place" => "room", "records_group" => group,
|
|
278
|
-
# README "Spectate": the tables' spectate keys
|
|
279
|
-
# (asterism/<node
|
|
280
|
-
#
|
|
281
|
-
#
|
|
280
|
+
# README "Spectate": the tables' spectate keys in the node's shared
|
|
281
|
+
# place (asterism/<node>/@shared/spectate/room<slot>-<table>/state),
|
|
282
|
+
# put by the room's application room<slot> on a connection without
|
|
283
|
+
# the namespace, with the host's own certificate only (the slot's
|
|
284
|
+
# certificate of a console before 0.3.0 is the room's).
|
|
282
285
|
"spectate_label" => "room#{slot}", "spectate_outside" => trust["own"] ? true : false }
|
|
283
286
|
end
|
|
284
287
|
|