asterism-games 0.17.1 → 0.18.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 +27 -0
- data/README.md +90 -0
- data/VERSION +1 -1
- data/lib/asterism_games/game.rb +19 -1
- data/lib/asterism_games/host.rb +140 -1
- data/lib/asterism_games/rooms.rb +7 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8555b735e1a25aff6167fe382f450dd367c4c6d81d13497c932265cf3a61a5f4
|
|
4
|
+
data.tar.gz: 5185c71e72d56963cebb26d480bf647763dd51156a0285db1104f6a9f600ae22
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48a9fc1196c4f3fb5b2b2b8cf8c9f3c4f0682a4b1beee018ec0518c2c131068c5113ccb309d008f97a8ce479dd2ba994a2e268b5bcd50869bca5ee692144a030
|
|
7
|
+
data.tar.gz: 8227908a95b02825a6e53ba1c1d75d9008f33b802fca25c82b41c03b0194b3d58e3f7c317ab3e6df7f770140b9a34410467ec3c0ea0a2be189ddc56ffdabca5b
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
Versions before 0.11.1 are noted in the README where each feature is
|
|
4
4
|
described ("games 0.10.0" and the like).
|
|
5
5
|
|
|
6
|
+
## 0.18.0
|
|
7
|
+
|
|
8
|
+
- **Spectate** (README "Spectate"): the host puts a table's spectate view
|
|
9
|
+
on `asterism/<node>/spectate/<table>/state` (a room's table as
|
|
10
|
+
`room<slot>-<table>`), a key outside the players' role
|
|
11
|
+
(`asterism/*/games/**`) and outside every room, for a console's
|
|
12
|
+
spectators and viewers and for the asterism gem's command line and MCP
|
|
13
|
+
bridge. The framework's `Game#spectate_view` (nil by default: nothing
|
|
14
|
+
is put) and `Game#spectate_delay_ms`; the host takes the view when it
|
|
15
|
+
puts the state, keeps it for the delay and puts each one in order, and
|
|
16
|
+
puts the last one again every 5 s. A room's host puts its tables' views
|
|
17
|
+
on a second connection of its own (the host's own certificate, no
|
|
18
|
+
namespace), never into the room; with a slot's certificate (a console
|
|
19
|
+
before 0.3.0) nothing. The application `spectate` is on the network (a
|
|
20
|
+
liveliness token) while the host puts.
|
|
21
|
+
- The mahjong table's spectate view: the table in words as `look` gives
|
|
22
|
+
it, with every seat's hand and drawn tile (the hands a spectator member
|
|
23
|
+
sees); never the wall, the ura dora, an indicator not turned yet, a node
|
|
24
|
+
or a token. Held for the table's spectator delay (none by default).
|
|
25
|
+
Othello and the drawing board put nothing.
|
|
26
|
+
- The console's role `games-host` needs two more lines for it (Read and
|
|
27
|
+
write `asterism/${node}/spectate`, `asterism/${node}/spectate/**`); the
|
|
28
|
+
console's docs/rooms.md has them and the `spectator` role. Without them
|
|
29
|
+
the router drops the puts and nothing else changes.
|
|
30
|
+
- The host's last log line counts the spectate puts and their largest
|
|
31
|
+
size apart from the players' messages.
|
|
32
|
+
|
|
6
33
|
## 0.17.1
|
|
7
34
|
|
|
8
35
|
- `bin/othello-tty` is gone: `asterism-games play othello` is the same
|
data/README.md
CHANGED
|
@@ -512,6 +512,84 @@ the host offers: its money of the day (`usd`), the limit it stops at
|
|
|
512
512
|
- **Size.** Five tables take about 300 bytes (the largest put is in the
|
|
513
513
|
log's `largest message` with the others).
|
|
514
514
|
|
|
515
|
+
## Spectate
|
|
516
|
+
|
|
517
|
+
A table can also be watched from outside it, with what the players do not
|
|
518
|
+
see of each other (games 0.18.0): the host puts a game's spectate view on
|
|
519
|
+
a key no player's role reads and no room reaches, for an admin, a
|
|
520
|
+
spectator's row of the console, or an AI assistant that only looks (the
|
|
521
|
+
asterism gem's command line and MCP bridge). The mahjong table puts the
|
|
522
|
+
four hands there; othello and the drawing board put nothing (they hide
|
|
523
|
+
nothing: their state is the whole game).
|
|
524
|
+
|
|
525
|
+
- **The key.** `asterism/<node>/spectate/<table>/state`: the host's node,
|
|
526
|
+
the application `spectate` (not the host's `games`, so the players' role
|
|
527
|
+
`asterism/*/games/**` does not match it), and the table. A room's table
|
|
528
|
+
is `room<slot>-<table>` there (`asterism/games/spectate/room01-mahjong1/state`),
|
|
529
|
+
so the rooms' tables and the permanent ones never meet. The value is
|
|
530
|
+
MessagePack, like the state.
|
|
531
|
+
- **Outside the rooms.** Every key a room's host writes on its own
|
|
532
|
+
connection gets the room's namespace (`room/<slot>/...`), which every
|
|
533
|
+
seat of the room reads whole. So a room's host opens a second connection
|
|
534
|
+
of its own for the spectate keys, with its own certificate and without
|
|
535
|
+
the namespace: the key is then on the registered side, where the seats'
|
|
536
|
+
rules do not reach. It does so only with the host's own certificate (a
|
|
537
|
+
console from 0.3.0 lists the host in `rooms.json`'s `"hosts"`); with the
|
|
538
|
+
slot's certificate of an older console it puts nothing (that
|
|
539
|
+
certificate is the room's). The permanent tables' host and a host of its
|
|
540
|
+
own put on their one connection.
|
|
541
|
+
- **What the mahjong table puts.** The table in words, as `look` gives it
|
|
542
|
+
("Playing from a program") to a member without a seat, with each seat's
|
|
543
|
+
`hand` and `drawn` tile, the same four hands a spectator member's view
|
|
544
|
+
has (`sh`, `sd`): the seats (name, kind, wind, score, riichi, river,
|
|
545
|
+
melds, hand count, hand, drawn), the round (wind, number, honba, riichi
|
|
546
|
+
sticks, dealer, wall left, turn, phase), the dora indicators turned so
|
|
547
|
+
far, the last discard, the result and the final ranks, the rules, and
|
|
548
|
+
`delay_s`. Never the wall, the ura dora, an indicator not turned yet, a
|
|
549
|
+
member's node or token (`test/test_spectate.rb` looks for them):
|
|
550
|
+
|
|
551
|
+
```
|
|
552
|
+
games/spectate/mahjong1/state
|
|
553
|
+
{"table":"mahjong1","status":"play","sv":212,
|
|
554
|
+
"seats":[{"seat":0,"name":"CPU1","kind":"cpu","score":21000,"wind":"north","riichi":false,
|
|
555
|
+
"river":["2z","4z","5z","1m",...],"melds":[{"type":"pon","tiles":["7z","7z","7z"],"from":1}],
|
|
556
|
+
"hand_count":10,"hand":["7m","7m","2p","3p","3p","4p","4p","5s","6s","7s"],"drawn":null}, ...],
|
|
557
|
+
"round":{"wind":"east","number":2,"honba":0,"riichi_sticks":1,"dealer":1,"wall_left":19,"turn":3,"phase":"discard"},
|
|
558
|
+
"dora_indicators":["9p"],"last_discard":{...},"rules":{...},"delay_s":0}
|
|
559
|
+
```
|
|
560
|
+
- **The delay.** The table's spectator delay ("Spectators": the setting
|
|
561
|
+
`spectator_delay_minutes`, or the owner's choice in the lobby; none by
|
|
562
|
+
default). The host takes the view when it puts the state (after a change,
|
|
563
|
+
and every 5 s), keeps each one in order and puts it once it is the delay
|
|
564
|
+
old, each in turn: what is put is what the table was the delay ago,
|
|
565
|
+
never sooner, also across a round's end or when the delay is chosen
|
|
566
|
+
again (a shorter delay lets out what is old enough at once). A host
|
|
567
|
+
started again starts with nothing kept (nothing is put for the delay).
|
|
568
|
+
At most 8 MB is kept per table; past that the oldest is dropped (never
|
|
569
|
+
shown rather than shown early). The last one put is put again every
|
|
570
|
+
5 s, so a new subscriber has it within that.
|
|
571
|
+
- **Who reads it.** The console's docs/rooms.md, "Watching the tables
|
|
572
|
+
from outside": the host's role `games-host` needs
|
|
573
|
+
`asterism/${node}/spectate` and `asterism/${node}/spectate/**` in "Read
|
|
574
|
+
and write" (without them the router drops the puts, and nothing else
|
|
575
|
+
changes); a role `spectator` (Read `asterism/*/spectate` and
|
|
576
|
+
`asterism/*/spectate/**`, Watch `asterism/**`) reads them with the
|
|
577
|
+
command line, and the `viewer` role (Read `asterism/**`) with the
|
|
578
|
+
command line and the MCP bridge. The players' role `games` and the
|
|
579
|
+
rooms' seats do not reach them.
|
|
580
|
+
- **Looking.** With the asterism gem's command line (asterism 0.10.0's
|
|
581
|
+
`object` commands): `asterism object echo 'games/spectate/*' --credentials
|
|
582
|
+
spectator.tar` (each put as it comes; `-o json` a line each), `asterism
|
|
583
|
+
object get games/spectate/mahjong1 --credentials viewer.tar` (the next
|
|
584
|
+
one). The MCP bridge (`asterism mcp --credentials viewer.tar`):
|
|
585
|
+
`asterism_values` with the pattern `games/spectate/*/state`. The bridge
|
|
586
|
+
subscribes to `asterism/**`, which a `spectator` row may not, so it
|
|
587
|
+
needs the viewer role. The host's application `spectate` is on the
|
|
588
|
+
network (a liveliness token) while it puts, so the values show as an
|
|
589
|
+
application that is there.
|
|
590
|
+
- **Writing a game.** `spectate_view(now)` (nil, the default: nothing) and
|
|
591
|
+
`spectate_delay_ms` (0) of `AsterismGames::Game`.
|
|
592
|
+
|
|
515
593
|
## Writing a game
|
|
516
594
|
|
|
517
595
|
A game is two classes on the framework: the table (`AsterismGames::Game`:
|
|
@@ -591,6 +669,7 @@ What the framework gives a game:
|
|
|
591
669
|
| `changed`, `dirty?`, `unsaved?` | a change to save and put; a change only one member's view shows (`@unsaved = true`) is saved, not put |
|
|
592
670
|
| `to_saved` / `restore` / `resume_clock`, `SAVE_FORMAT` | the saved form (one JSON file per table, `state_dir/<table>.json`, written to a temporary file and renamed); a state of another format is not read. Times are saved as the time left and resumed with the host's clock |
|
|
593
671
|
| `tell(sub, value)`, `SAVE_HOLD_MS` | more than the state: the host puts `value` on `asterism/<node>/<app>/<table>/<sub>` on its next step, in the order told, before the state (the board's strokes). A game that changes many times a second writes its saved state at most once per `SAVE_HOLD_MS` (the host writes what is held when it stops) |
|
|
672
|
+
| `spectate_view(now)`, `spectate_delay_ms` | what the host puts on `asterism/<node>/spectate/<table>/state` for those who watch from outside the table ("Spectate"), held for the delay; nil (the default): nothing. It may hold what the players do not see of each other, never what nobody knows yet |
|
|
594
673
|
| `summary(now)` | the counts the status shows (`st`, `members`, `humans`, `cpus`, `away`, and the game's own): no names, no nodes, no tokens. `busy` (`bin/asterism-games status --idle`) is `humans - away` |
|
|
595
674
|
| `settings`, `tables`, `screens`, `DEFAULTS` | the table keys of the settings file and the environment, the tables a host serves by default, the table apps that get the tables in a room, the table's options |
|
|
596
675
|
|
|
@@ -758,6 +837,10 @@ hand.
|
|
|
758
837
|
CPU) or every 20 s (a person) does not change how far behind the
|
|
759
838
|
spectators are. A host started again starts the delay over (the past is
|
|
760
839
|
not saved).
|
|
840
|
+
- **From outside the table.** The same four hands, in words and as old
|
|
841
|
+
as the spectators' view, go on `asterism/<node>/spectate/<table>/state`
|
|
842
|
+
for a console's spectators and viewers ("Spectate"), a key the players
|
|
843
|
+
cannot read.
|
|
761
844
|
- **The default is no delay.** The spectators see the hands as they are:
|
|
762
845
|
good for watching a game, and for an audience in the same room. A seated
|
|
763
846
|
player who joins the same table from a second screen sees the others'
|
|
@@ -1818,6 +1901,13 @@ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 bundle exec rake test # also through a
|
|
|
1818
1901
|
tonpuu game against three CPUs each to the end from scripted input
|
|
1819
1902
|
(letters, tiles, calls, passes, OKs; `PLAY_TRANSCRIPT_DIR` keeps what
|
|
1820
1903
|
they showed).
|
|
1904
|
+
- `test_spectate.rb`: the spectate view (README "Spectate"; its
|
|
1905
|
+
framework part in `rake test:gem` too): nothing by default (othello, the
|
|
1906
|
+
board), the keys of the permanent tables and of a room, what the rooms
|
|
1907
|
+
give their hosts, the mahjong view (the four hands, no token, node, wall
|
|
1908
|
+
or ura dora), a CPU game on the host, the delay (0, and 3 s across a
|
|
1909
|
+
round's end, in order, never sooner), a room's table on the connection
|
|
1910
|
+
outside the room only, and the last put again after 5 s.
|
|
1821
1911
|
- `test_play_commands.rb`: `--commands "sit,cpu,start"` against a table
|
|
1822
1912
|
in this process: they wait for the lobby when a game is going on as
|
|
1823
1913
|
the client joins (and then sit, fill and start), the owner's wait for
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.18.0
|
data/lib/asterism_games/game.rb
CHANGED
|
@@ -38,7 +38,9 @@ module AsterismGames
|
|
|
38
38
|
# (the board's strokes) holds its writes
|
|
39
39
|
#
|
|
40
40
|
# and writes the rules: tick(now), public_view(now) (and spectator_view,
|
|
41
|
-
# summary when they differ
|
|
41
|
+
# summary when they differ; spectate_view and spectate_delay_ms for what
|
|
42
|
+
# the host puts outside the players' keys, README "Spectate"),
|
|
43
|
+
# to_saved(now) / restore(data, now) and the
|
|
42
44
|
# methods its Remote calls. A game that hands out more than its state
|
|
43
45
|
# (the board's strokes) tells it with tell(sub, value): the host puts
|
|
44
46
|
# value on asterism/<node>/<app>/<table>/<sub>, in the order told, on its
|
|
@@ -153,6 +155,22 @@ module AsterismGames
|
|
|
153
155
|
spectator_view(now)
|
|
154
156
|
end
|
|
155
157
|
|
|
158
|
+
# 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
|
|
160
|
+
# table: room<slot>-<table>), a key no player's role reads and outside
|
|
161
|
+
# every room, after spectate_delay_ms. It may hold what the players do
|
|
162
|
+
# not see of each other (the mahjong hands), never what nobody knows
|
|
163
|
+
# yet (a wall) nor a token. nil (the default): nothing is put.
|
|
164
|
+
def spectate_view(_now)
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# How old what spectate_view gives is when the host puts it (ms; 0: at
|
|
169
|
+
# once). A game with a spectator delay gives it here.
|
|
170
|
+
def spectate_delay_ms
|
|
171
|
+
0
|
|
172
|
+
end
|
|
173
|
+
|
|
156
174
|
# Counts for the host's status (GET /status of the rooms' host, README
|
|
157
175
|
# "Rooms"): no names, no nodes, no tokens. "st" (the game's status),
|
|
158
176
|
# "members", "humans" (members in a seat), "cpus", "away" (humans not
|
data/lib/asterism_games/host.rb
CHANGED
|
@@ -52,6 +52,7 @@ module AsterismGames
|
|
|
52
52
|
@methods = {}
|
|
53
53
|
@docs = {}
|
|
54
54
|
@put_at = {}
|
|
55
|
+
@spec = {} # table => {"q" => [[ms, bytes], ...], "bytes", "last", "out", "at"} (README "Spectate")
|
|
55
56
|
@stats = Hash.new(0)
|
|
56
57
|
config["tables"].each do |t|
|
|
57
58
|
klass = AsterismGames.game(t["game"]) or raise ArgumentError, "unknown game #{t['game'].inspect}"
|
|
@@ -132,7 +133,8 @@ module AsterismGames
|
|
|
132
133
|
end
|
|
133
134
|
save_all
|
|
134
135
|
@ai&.stop
|
|
135
|
-
@log.call("games: largest message #{@stats['max_bytes']} B (#{@stats['max_what']})"
|
|
136
|
+
@log.call("games: largest message #{@stats['max_bytes']} B (#{@stats['max_what']})" \
|
|
137
|
+
"#{"; spectate #{@stats['spectate']} puts, at most #{@stats['spectate_max_bytes']} B" if @stats['spectate'].positive?}")
|
|
136
138
|
if @ai&.any?
|
|
137
139
|
n, avg, max = @ai.times
|
|
138
140
|
@log.call("games: AI players: #{@ai.stats.to_h.inspect}; #{n} decisions, #{avg} ms on average, #{max} ms at most; " \
|
|
@@ -195,6 +197,7 @@ module AsterismGames
|
|
|
195
197
|
@log.call("games: connection lost: #{net.lost_reason}")
|
|
196
198
|
return
|
|
197
199
|
end
|
|
200
|
+
open_spectate
|
|
198
201
|
::Asterism::LOCK.synchronize { step }
|
|
199
202
|
sleep LOOP_S
|
|
200
203
|
end
|
|
@@ -234,6 +237,7 @@ module AsterismGames
|
|
|
234
237
|
end
|
|
235
238
|
|
|
236
239
|
def down
|
|
240
|
+
close_spectate
|
|
237
241
|
@net = nil
|
|
238
242
|
@tables_status = nil
|
|
239
243
|
@connected = false
|
|
@@ -277,12 +281,15 @@ module AsterismGames
|
|
|
277
281
|
changed = true
|
|
278
282
|
g.save(now)
|
|
279
283
|
put_state(name, g, now)
|
|
284
|
+
take_spectate(name, g, now)
|
|
280
285
|
elsif g.unsaved?
|
|
281
286
|
# A change only one member's own view shows: saved, not put.
|
|
282
287
|
g.save(now)
|
|
283
288
|
elsif now - @put_at.fetch(name, 0) >= PUT_EVERY_MS
|
|
284
289
|
put_state(name, g, now)
|
|
290
|
+
take_spectate(name, g, now)
|
|
285
291
|
end
|
|
292
|
+
put_spectate(name, g, now)
|
|
286
293
|
end
|
|
287
294
|
write_status(now) if @config["status_file"] && now - @status_at.to_i >= STATUS_EVERY_MS
|
|
288
295
|
tell_tables(now, changed: changed)
|
|
@@ -368,6 +375,138 @@ module AsterismGames
|
|
|
368
375
|
@log.call("games: put #{name}/#{sub}: #{e.message}")
|
|
369
376
|
end
|
|
370
377
|
|
|
378
|
+
# ------------------------------------------------------------ spectate
|
|
379
|
+
#
|
|
380
|
+
# What a game gives for those who watch from outside the table
|
|
381
|
+
# (Game#spectate_view, README "Spectate"): taken when the state is put
|
|
382
|
+
# (a change, and every PUT_EVERY_MS), kept in order and put once it is
|
|
383
|
+
# Game#spectate_delay_ms old, each in turn, on spectate_key. Nothing is
|
|
384
|
+
# put sooner than the delay asks, whatever happens between (a round
|
|
385
|
+
# ends, the delay is chosen again); a host started again starts with
|
|
386
|
+
# nothing kept. The last one put is put again every PUT_EVERY_MS, so a
|
|
387
|
+
# new subscriber has it within that.
|
|
388
|
+
|
|
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
|
+
# What one table keeps for its delay at most (bytes); the oldest goes
|
|
393
|
+
# first (it is then never shown, rather than shown early).
|
|
394
|
+
SPECTATE_KEEP_BYTES = 8 * 1024 * 1024
|
|
395
|
+
# How often a room's host tries again to open its connection outside
|
|
396
|
+
# the room (ms).
|
|
397
|
+
SPECTATE_RETRY_MS = 30_000
|
|
398
|
+
|
|
399
|
+
# asterism/<node>/spectate/<table>/state; a room's table is
|
|
400
|
+
# room<slot>-<table> there, so the rooms' tables and the permanent ones
|
|
401
|
+
# never meet.
|
|
402
|
+
def spectate_key(name)
|
|
403
|
+
table = @config["namespace"] ? "#{spectate_label}-#{name}" : name
|
|
404
|
+
"asterism/#{node}/#{SPECTATE_APP}/#{table}/state"
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# A room's part of its tables' spectate names: the setting (Rooms gives
|
|
408
|
+
# room<slot>), else the namespace without its slashes.
|
|
409
|
+
def spectate_label
|
|
410
|
+
@config["spectate_label"] || @config["namespace"].to_s.delete("/")
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Takes what the game gives now (nothing when it gives nil, or the same
|
|
414
|
+
# as last time).
|
|
415
|
+
def take_spectate(name, game, now)
|
|
416
|
+
v = game.spectate_view(now)
|
|
417
|
+
return if v.nil?
|
|
418
|
+
bytes = ::Asterism::Codec.pack(v)
|
|
419
|
+
s = (@spec[name] ||= { "q" => [], "bytes" => 0 })
|
|
420
|
+
return if bytes == s["last"]
|
|
421
|
+
s["last"] = bytes
|
|
422
|
+
s["q"] << [now, bytes]
|
|
423
|
+
s["bytes"] += bytes.bytesize
|
|
424
|
+
while s["bytes"] > SPECTATE_KEEP_BYTES && s["q"].size > 1
|
|
425
|
+
s["bytes"] -= s["q"].shift[1].bytesize
|
|
426
|
+
@stats["spectate_dropped"] += 1
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Puts what is old enough now, in order; or the last one again.
|
|
431
|
+
def put_spectate(name, game, now)
|
|
432
|
+
s = @spec[name] or return
|
|
433
|
+
q = s["q"]
|
|
434
|
+
unless q.empty?
|
|
435
|
+
delay = game.spectate_delay_ms.to_i
|
|
436
|
+
while q.first && q.first[0] <= now - delay
|
|
437
|
+
bytes = q.shift[1]
|
|
438
|
+
s["bytes"] -= bytes.bytesize
|
|
439
|
+
s["out"] = bytes
|
|
440
|
+
send_spectate(name, bytes, now)
|
|
441
|
+
@stats["spectate"] += 1
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
send_spectate(name, s["out"], now) if s["out"] && now - s["at"].to_i >= PUT_EVERY_MS
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def send_spectate(name, bytes, now)
|
|
448
|
+
@spec[name]["at"] = now
|
|
449
|
+
sess = spectate_session or return
|
|
450
|
+
spectate_token(sess)
|
|
451
|
+
key = spectate_key(name)
|
|
452
|
+
# Counted apart from the players' messages (the hands make it larger).
|
|
453
|
+
@stats["spectate_max_bytes"] = [@stats["spectate_max_bytes"], key.bytesize + bytes.bytesize].max
|
|
454
|
+
sess.put(key, bytes)
|
|
455
|
+
rescue ::Asterism::Error => e
|
|
456
|
+
@log.call("games: put spectate #{name}: #{e.message}")
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# The application asterism/<node>/spectate on the network (a liveliness
|
|
460
|
+
# token on the connection that puts), so that the command line and the
|
|
461
|
+
# MCP bridge take its keys for the values of an application that is
|
|
462
|
+
# there. Once per connection.
|
|
463
|
+
def spectate_token(sess)
|
|
464
|
+
return if @spectate_token_on.equal?(sess) || !sess.respond_to?(:liveliness)
|
|
465
|
+
@spectate_token_on = sess
|
|
466
|
+
@spectate_token = sess.liveliness("asterism/#{node}/#{SPECTATE_APP}")
|
|
467
|
+
rescue ::Asterism::Error => e
|
|
468
|
+
@log.call("games: spectate: token: #{e.message}")
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Where the spectate keys are put: the host's own connection outside
|
|
472
|
+
# the rooms; in a room a connection of its own without the room's
|
|
473
|
+
# namespace (open_spectate), when the host joins with its own
|
|
474
|
+
# certificate (spectate_outside), else nowhere.
|
|
475
|
+
def spectate_session
|
|
476
|
+
return nil unless @net && !::Asterism.reconnecting?
|
|
477
|
+
return @net.session unless @config["namespace"]
|
|
478
|
+
c = @spectate_conn
|
|
479
|
+
c && !c.reconnecting? ? c : nil
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# A room's host opens its connection outside the room (the host's own
|
|
483
|
+
# certificate, no namespace) once connected, and again after a failure
|
|
484
|
+
# every SPECTATE_RETRY_MS. Outside the lock: opening waits for the router.
|
|
485
|
+
def open_spectate(now = @clock.call)
|
|
486
|
+
return unless @config["namespace"] && @config["spectate_outside"] && @config["credentials"]
|
|
487
|
+
return if @spectate_conn || now < @spectate_retry_at.to_i
|
|
488
|
+
return unless @games.values.any? { |g| g.respond_to?(:spectate_view) && g.method(:spectate_view).owner != Game }
|
|
489
|
+
@spectate_retry_at = now + SPECTATE_RETRY_MS
|
|
490
|
+
@spectate_conn = ::Asterism::Zenoh.open(locator, credentials: @config["credentials"])
|
|
491
|
+
@log.call("games: spectate: #{node}/#{SPECTATE_APP} outside #{@config['namespace']} " \
|
|
492
|
+
"(#{@games.keys.map { |n| spectate_key(n).split('/')[3] }.join(', ')})")
|
|
493
|
+
rescue ::Asterism::Error, SystemCallError => e
|
|
494
|
+
@spectate_conn = nil
|
|
495
|
+
@log.call("games: spectate: cannot connect outside #{@config['namespace']}: #{e.message}; " \
|
|
496
|
+
"again in #{SPECTATE_RETRY_MS / 1000} s")
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def close_spectate
|
|
500
|
+
@spectate_token = nil # withdrawn with its connection
|
|
501
|
+
@spectate_token_on = nil
|
|
502
|
+
@spectate_retry_at = nil # a new connection tries at once
|
|
503
|
+
c = @spectate_conn
|
|
504
|
+
@spectate_conn = nil
|
|
505
|
+
c&.close
|
|
506
|
+
rescue ::Asterism::Error
|
|
507
|
+
nil
|
|
508
|
+
end
|
|
509
|
+
|
|
371
510
|
# The largest message seen so far, key and payload together.
|
|
372
511
|
def note_size(what, bytes)
|
|
373
512
|
return unless bytes > @stats["max_bytes"]
|
data/lib/asterism_games/rooms.rb
CHANGED
|
@@ -264,6 +264,7 @@ module AsterismGames
|
|
|
264
264
|
# "Rooms", "Who joins"); trust: trust_for's; app: what the room's link
|
|
265
265
|
# opens (its game's tables; nil: every table of rooms.tables); group:
|
|
266
266
|
# its invite link's group (nil: none), kept with its games' records.
|
|
267
|
+
# spectate_label / spectate_outside: README "Spectate".
|
|
267
268
|
def child_config(slot, tar, dir, room_id = nil, trust = { "own" => false, "seats" => [], "prefix" => nil }, app = nil,
|
|
268
269
|
group = nil)
|
|
269
270
|
tables = app ? Config.tables_for_app(@rooms["tables"], app) : @rooms["tables"]
|
|
@@ -273,7 +274,12 @@ module AsterismGames
|
|
|
273
274
|
"status_file" => File.join(dir, "host.json"), "tables" => tables, "room_id" => room_id,
|
|
274
275
|
"trusted_nodes" => trust["seats"], "trusted_prefix" => trust["prefix"],
|
|
275
276
|
"seat_reuse_ms" => @rooms["seat_reuse_seconds"].to_i * 1000, "ai_usage_file" => ai_usage_file,
|
|
276
|
-
"records_db" => records_db, "records_place" => "room", "records_group" => group
|
|
277
|
+
"records_db" => records_db, "records_place" => "room", "records_group" => group,
|
|
278
|
+
# README "Spectate": the tables' spectate keys, outside the room
|
|
279
|
+
# (asterism/<node>/spectate/room<slot>-<table>/state), put on a
|
|
280
|
+
# connection without the namespace, with the host's own certificate
|
|
281
|
+
# only (the slot's certificate of a console before 0.3.0 is the room's).
|
|
282
|
+
"spectate_label" => "room#{slot}", "spectate_outside" => trust["own"] ? true : false }
|
|
277
283
|
end
|
|
278
284
|
|
|
279
285
|
# What the AI players cost, shared by every host process (README "AI
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: asterism-games
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Katsuhiko Kageyama
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: asterism
|