asterism-games 0.17.0 → 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 +35 -0
- data/README.md +95 -7
- data/VERSION +1 -1
- data/lib/asterism_games/game.rb +19 -1
- data/lib/asterism_games/host.rb +140 -1
- data/lib/asterism_games/play/othello.rb +2 -2
- data/lib/asterism_games/rooms.rb +7 -1
- metadata +2 -4
- data/bin/othello-tty +0 -14
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,41 @@
|
|
|
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
|
+
|
|
33
|
+
## 0.17.1
|
|
34
|
+
|
|
35
|
+
- `bin/othello-tty` is gone: `asterism-games play othello` is the same
|
|
36
|
+
client, and the gem installs `asterism-games` only.
|
|
37
|
+
- The first version published on rubygems.org: `gem install
|
|
38
|
+
asterism-games` gives `asterism-games play` with the asterism gem and
|
|
39
|
+
nothing else. The host and the tables are 0.17.0's.
|
|
40
|
+
|
|
6
41
|
## 0.17.0
|
|
7
42
|
|
|
8
43
|
- **`asterism-games play <game>`** (README "Playing in a terminal"): the
|
data/README.md
CHANGED
|
@@ -46,7 +46,7 @@ and the image"): the framework with the two sample games is the gem
|
|
|
46
46
|
└──requires──> asterism_games <───┘
|
|
47
47
|
(Host, Rooms, Config, Store, Game, Remote) ──> asterism
|
|
48
48
|
|
|
49
|
-
the gem asterism-games: lib/ (framework, othello, board, the terminal clients), bin/asterism-games
|
|
49
|
+
the gem asterism-games: lib/ (framework, othello, board, the terminal clients), bin/asterism-games
|
|
50
50
|
the image: all of the above, app/ (mahjong), config/, the Gemfile's gems
|
|
51
51
|
```
|
|
52
52
|
|
|
@@ -62,7 +62,6 @@ the console ("Permanent tables"; the server's setup: docs/install.md).
|
|
|
62
62
|
| `asterism-games.gemspec` | the gem: what it holds and needs ("The gem and the image") |
|
|
63
63
|
| `bin/asterism-games` | the host (loads the framework and the games); `status`; `play <game>`, a table in this terminal ("Playing in a terminal"); `help` |
|
|
64
64
|
| `bin/mahjong-bots` | players without screens, for trying a mahjong table (one process each; not in the gem) |
|
|
65
|
-
| `bin/othello-tty` | `asterism-games play othello`, under its old name |
|
|
66
65
|
| `lib/asterism_games/play.rb`, `lib/asterism_games/play/` | the terminal clients ("Playing in a terminal"): the options, `othello.rb`, `mahjong.rb` with `mahjong_text.rb` (tiles as text, widths, reading the input) and `mahjong_screen.rb` (the screen); they need neither the framework nor riichi_engine |
|
|
67
66
|
| `lib/asterism_games.rb` | the framework, without any game |
|
|
68
67
|
| `lib/asterism_games/game.rb` | the base of a game: the registry, the members and tokens, who joins in a room, the views, the time, saving ("Writing a game") |
|
|
@@ -90,7 +89,7 @@ the console ("Permanent tables"; the server's setup: docs/install.md).
|
|
|
90
89
|
game host of your own: the framework, the two sample games that need
|
|
91
90
|
nothing else (othello, the drawing board), `bin/asterism-games` (with
|
|
92
91
|
`play`, the terminal clients of othello and mahjong: a mahjong table needs
|
|
93
|
-
riichi_engine, its client does not)
|
|
92
|
+
riichi_engine, its client does not). It depends on
|
|
94
93
|
the asterism gem only (0.7.1 or later in
|
|
95
94
|
0.x; what came after 0.7.1, such as `@status` and `on_app_join`, is used
|
|
96
95
|
when there). Not in it: mahjong (`app/`, it needs the riichi_engine fork
|
|
@@ -402,7 +401,7 @@ list's flag), a mahjong seat is played for after a short wait, the status
|
|
|
402
401
|
counts it. The host learns it from Asterism, not from the calls (games
|
|
403
402
|
0.8.0), and by the player's table app (games 0.10.0): a table app runs as
|
|
404
403
|
the application **`game`** of its node (the boards' and the browsers'
|
|
405
|
-
table apps, `
|
|
404
|
+
table apps, `asterism-games play`, `bin/mahjong-bots` do), and the host
|
|
406
405
|
follows the liveliness token `asterism/<node>/game`. The Net's
|
|
407
406
|
`on_app_leave` tells it that the table app left the network (a closed
|
|
408
407
|
browser tab, a board's app that closed, a process killed, a line that
|
|
@@ -447,7 +446,7 @@ without a call (away after `away_seconds`, measured 13.6 s with the default
|
|
|
447
446
|
nothing in the host changes with it.
|
|
448
447
|
|
|
449
448
|
The screens follow the host the same way: the tables' apps (fmruby-core
|
|
450
|
-
`flash/app/game/`) and `
|
|
449
|
+
`flash/app/game/`) and `asterism-games play` take a host's tables off their
|
|
451
450
|
list when its node leaves and look for them again when a node joins
|
|
452
451
|
(Asterism's `each` reads the liveliness they already follow: no message is
|
|
453
452
|
sent); at a table, a host that leaves shows the connection lost, the
|
|
@@ -513,6 +512,84 @@ the host offers: its money of the day (`usd`), the limit it stops at
|
|
|
513
512
|
- **Size.** Five tables take about 300 bytes (the largest put is in the
|
|
514
513
|
log's `largest message` with the others).
|
|
515
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
|
+
|
|
516
593
|
## Writing a game
|
|
517
594
|
|
|
518
595
|
A game is two classes on the framework: the table (`AsterismGames::Game`:
|
|
@@ -592,6 +669,7 @@ What the framework gives a game:
|
|
|
592
669
|
| `changed`, `dirty?`, `unsaved?` | a change to save and put; a change only one member's view shows (`@unsaved = true`) is saved, not put |
|
|
593
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 |
|
|
594
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 |
|
|
595
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` |
|
|
596
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 |
|
|
597
675
|
|
|
@@ -759,6 +837,10 @@ hand.
|
|
|
759
837
|
CPU) or every 20 s (a person) does not change how far behind the
|
|
760
838
|
spectators are. A host started again starts the delay over (the past is
|
|
761
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.
|
|
762
844
|
- **The default is no delay.** The spectators see the hands as they are:
|
|
763
845
|
good for watching a game, and for an audience in the same room. A seated
|
|
764
846
|
player who joins the same table from a second screen sees the others'
|
|
@@ -1584,8 +1666,7 @@ squares next to a corner last), then the most discs turned.
|
|
|
1584
1666
|
|
|
1585
1667
|
### In a terminal: `asterism-games play othello`
|
|
1586
1668
|
|
|
1587
|
-
(`bin/othello-tty` until games 0.17.0; it is
|
|
1588
|
-
with the same options.) CRuby and the asterism gem only (no Family
|
|
1669
|
+
(`bin/othello-tty` until games 0.17.0; it is gone in 0.17.1.) CRuby and the asterism gem only (no Family
|
|
1589
1670
|
mruby). The options are every client's ("Playing in a terminal"). It joins the table,
|
|
1590
1671
|
draws the board as text at every change (it takes the state the host puts
|
|
1591
1672
|
and asks its view; X black, O white, `*` the squares it may play) and on a
|
|
@@ -1820,6 +1901,13 @@ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 bundle exec rake test # also through a
|
|
|
1820
1901
|
tonpuu game against three CPUs each to the end from scripted input
|
|
1821
1902
|
(letters, tiles, calls, passes, OKs; `PLAY_TRANSCRIPT_DIR` keeps what
|
|
1822
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.
|
|
1823
1911
|
- `test_play_commands.rb`: `--commands "sit,cpu,start"` against a table
|
|
1824
1912
|
in this process: they wait for the lobby when a game is going on as
|
|
1825
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"]
|
|
@@ -5,7 +5,7 @@ require "asterism"
|
|
|
5
5
|
module AsterismGames
|
|
6
6
|
module Play
|
|
7
7
|
# An othello table in a terminal (README "The othello table"):
|
|
8
|
-
# `asterism-games play othello
|
|
8
|
+
# `asterism-games play othello`.
|
|
9
9
|
# It joins the table, shows the board as text at every change (the
|
|
10
10
|
# state the host puts) and reads commands: watch, or sit and type the
|
|
11
11
|
# squares ("d3"). When the table's host leaves the network (Asterism's
|
|
@@ -100,7 +100,7 @@ module AsterismGames
|
|
|
100
100
|
def start
|
|
101
101
|
r = @table.join(@name)
|
|
102
102
|
# "identity": in a room, only the seats and the host's names join.
|
|
103
|
-
abort "othello
|
|
103
|
+
abort "play othello: #{@path}: join refused: #{r.is_a?(Hash) ? r['error'] : r.inspect}" unless r.is_a?(Hash) && r["token"]
|
|
104
104
|
@token = r["token"]
|
|
105
105
|
host, app, tname = @path.split("/")
|
|
106
106
|
# The state the host puts after every change (and every 5 s): a newer
|
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
|
|
@@ -39,7 +39,6 @@ description: 'A framework for the server side of a game on an Asterism network:
|
|
|
39
39
|
email:
|
|
40
40
|
executables:
|
|
41
41
|
- asterism-games
|
|
42
|
-
- othello-tty
|
|
43
42
|
extensions: []
|
|
44
43
|
extra_rdoc_files: []
|
|
45
44
|
files:
|
|
@@ -48,7 +47,6 @@ files:
|
|
|
48
47
|
- README.md
|
|
49
48
|
- VERSION
|
|
50
49
|
- bin/asterism-games
|
|
51
|
-
- bin/othello-tty
|
|
52
50
|
- lib/asterism_games.rb
|
|
53
51
|
- lib/asterism_games/ai.rb
|
|
54
52
|
- lib/asterism_games/ai_hub.rb
|
data/bin/othello-tty
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# An othello table in a terminal: kept for the scripts and the habits of
|
|
5
|
-
# before games 0.17.0. It is `asterism-games play othello` (README
|
|
6
|
-
# "Playing in a terminal"), with the same options:
|
|
7
|
-
#
|
|
8
|
-
# bundle exec bin/othello-tty [--router tcp/127.0.0.1:7447] [--table games/games/othello1]
|
|
9
|
-
# [--name NAME] [--node NODE] [--credentials alice.tar]
|
|
10
|
-
# [--namespace room/01] [--commands "sit,cpu,start,auto"] [--until-over]
|
|
11
|
-
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
12
|
-
require "asterism_games/play"
|
|
13
|
-
|
|
14
|
-
exit AsterismGames::Play.main(["othello", *ARGV])
|