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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7bff07a0b8aceb0d5caa131285b00cd98198e8149496b512d22e8009c69f2614
|
|
4
|
+
data.tar.gz: fc8c5a631fd8a3e93d39fb0bdd8f11c7bab64d201bd1c1b3173455590e72a1c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd5c7400e36ce7ef56787061ddd9470ef58994f1fdb49328cb0cdf22c4501510c718eceda73e0cd0ce6c7fde2acc749bbdba05e91e8d09c1c16e0e1cc4a9e64c
|
|
7
|
+
data.tar.gz: a7ab03721da396f87a387709edd5466da2bc97cb08ad1464808dd4a7cb2ca24035b2da13718b84734617052678bc76c3492e4958db203e2d1782f82142183740
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ described ("games 0.10.0" and the like).
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.23.0
|
|
9
|
+
|
|
10
|
+
Asterism wire protocol 2 (asterism 0.14.0; docs/install.md "From 0.22.0
|
|
11
|
+
to 0.23.0"). **Not compatible with protocol 1**: the host, the console
|
|
12
|
+
(0.4.0), the boards' firmware and the browser build move together. A
|
|
13
|
+
protocol 1 board or browser page still sees the tables' values, but its
|
|
14
|
+
calls (sit, play, view) are refused with the error kind `protocol`.
|
|
15
|
+
|
|
16
|
+
- Needs asterism 0.14.0 (`~> 0.14`): the host, `bin/mahjong-bots`,
|
|
17
|
+
`asterism-games play` and the commentary go through the connection
|
|
18
|
+
object `Asterism.connect` returns.
|
|
19
|
+
- The tables' keys are protocol 2 topics with their types:
|
|
20
|
+
`<table>/state` and what a game tells (`<table>/strokes` of the board)
|
|
21
|
+
are declared when the host connects; the games' types
|
|
22
|
+
(`asterism-games/othello.state`, `mahjong.state`, `mahjong.spectate`,
|
|
23
|
+
`board.strokes`, ...) are defined by the host, field types by name,
|
|
24
|
+
with `any` and repeated `[]` for the members, the final ranks and pao.
|
|
25
|
+
A reader finds them with `asterism topic list` before a value comes.
|
|
26
|
+
- The spectate views move to the node's shared place:
|
|
27
|
+
`asterism/<node>/@shared/spectate/<table>/state` (was
|
|
28
|
+
`asterism/<node>/spectate/<table>/state`). Outside the rooms the host's
|
|
29
|
+
own application `games` declares them; a room's host whose certificate
|
|
30
|
+
is its own opens the application `room<slot>` of the host's node outside
|
|
31
|
+
the room and declares its tables there (`spectate/room<slot>-<table>/state`).
|
|
32
|
+
Each value carries its sender.
|
|
33
|
+
- The present value is asked, not kept on the network: `games` and each
|
|
34
|
+
`room<slot>` expose the object `spectate`, whose `state(table)` answers
|
|
35
|
+
the last spectate view (as delayed as the key). The commentary asks it
|
|
36
|
+
at a sender's first value, after lost values, when the sender started
|
|
37
|
+
again and after it left; the play clients ask their view when a state
|
|
38
|
+
value says values were lost or the host started again.
|
|
39
|
+
- `record(id)`'s head gives `time` in ms since 1970 (was seconds). The
|
|
40
|
+
record book keeps seconds in its own files and database.
|
|
41
|
+
- The roles in the console (docs/install.md section 7): `games-host` adds
|
|
42
|
+
`asterism/${node}/*/@profile`, `asterism/${node}/*/@pubs`,
|
|
43
|
+
`asterism/${node}/@shared/**`, `asterism/${node}/room$*`,
|
|
44
|
+
`asterism/${node}/room$*/**` and Read `asterism/*/game/protocol/*`;
|
|
45
|
+
`spectator` reads `asterism/*/@shared/spectate/**`, the senders'
|
|
46
|
+
`@profile`, `@pubs` and liveliness tokens (`games/protocol/*`,
|
|
47
|
+
`room$*/protocol/*`: its `asterism topic list` shows the spectate keys),
|
|
48
|
+
and calls `spectate` under its own name.
|
|
49
|
+
asterism-console 0.4.0's update adds all of these to roles made before.
|
|
50
|
+
- The Gemfile takes asterism from a checkout with `ASTERISM_DIR` (a lock
|
|
51
|
+
of its own, `Gemfile.asterism_dir.lock`, not committed).
|
|
52
|
+
- The image can be built before an asterism release is published:
|
|
53
|
+
`ASTERISM_REF=<commit>` (a build arg of the Dockerfile and of
|
|
54
|
+
`docker-compose.build.yml`) takes asterism from its git repository at
|
|
55
|
+
that commit, locked in the image's copy of `Gemfile.lock`
|
|
56
|
+
(docs/install.md "Building the image yourself"). The image keeps the lock
|
|
57
|
+
its build resolved (`COPY . .` no longer puts the checkout's back).
|
|
58
|
+
|
|
8
59
|
## 0.22.0
|
|
9
60
|
|
|
10
61
|
- Needs CRuby 3.4 or newer (`required_ruby_version >= 3.4`; 3.2 is past its end of life). The image is built on `ruby:3.4.11-slim`.
|
data/README.md
CHANGED
|
@@ -166,6 +166,15 @@ seats and tokens). When the router goes away, it connects again every 3 s.
|
|
|
166
166
|
|
|
167
167
|
asterism comes from rubygems at a fixed version (0.8.0); riichi_engine from
|
|
168
168
|
the `fmrb` branch of the fork on GitHub (see "riichi_engine" below).
|
|
169
|
+
`ASTERISM_DIR=<a checkout of asterism>` takes asterism from that checkout
|
|
170
|
+
instead, for developing both at once: the Gemfile then uses a lock file of
|
|
171
|
+
its own, `Gemfile.asterism_dir.lock` (not committed; Bundler 2.6 or newer),
|
|
172
|
+
and `Gemfile.lock` keeps the released version.
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
ASTERISM_DIR=../asterism bundle install
|
|
176
|
+
ASTERISM_DIR=../asterism bundle exec rake test
|
|
177
|
+
```
|
|
169
178
|
|
|
170
179
|
## Settings
|
|
171
180
|
|
|
@@ -235,8 +244,10 @@ An Asterism console (0.2.2 and later) makes rooms: a key space of its own
|
|
|
235
244
|
beyond the one a room needs anyway. The people in the room open the link
|
|
236
245
|
and see the table in the screen's list, as on a home network
|
|
237
246
|
(`games/games/mahjong1`).
|
|
238
|
-
- **One process per room.**
|
|
239
|
-
|
|
247
|
+
- **One process per room.** Every room's host is the same application
|
|
248
|
+
(`<node>/games`, each in its room's namespace), which one program of the
|
|
249
|
+
asterism gem opens once, so the supervisor forks a host process (Host)
|
|
250
|
+
per room. It joins
|
|
240
251
|
the router in the room's namespace (`room/<slot>`): its keys are the
|
|
241
252
|
room's, and nothing of it reaches other rooms or the registered side (the
|
|
242
253
|
router's ACL). **Which certificate** (0.5.0): when `rooms.json` lists the
|
|
@@ -248,7 +259,10 @@ An Asterism console (0.2.2 and later) makes rooms: a key space of its own
|
|
|
248
259
|
router. With a console before 0.3.0 it joins with the slot's certificate
|
|
249
260
|
(`certs/room-<slot>.tar`, in the same volume, read-only), as before. A
|
|
250
261
|
console that lists the host while the host has no certificate set: the
|
|
251
|
-
log says so and the rooms get no table that works.
|
|
262
|
+
log says so and the rooms get no table that works. With its own
|
|
263
|
+
certificate a room's host whose tables give a spectate view (mahjong)
|
|
264
|
+
is also the application `room<slot>` of the host's node outside the
|
|
265
|
+
room, for its tables' spectate keys ("Spectate"). At most `rooms.max` rooms have tables (3); a room
|
|
252
266
|
beyond that gets none (the status says so). A host process that stops is
|
|
253
267
|
started again after 1, 2, 4 ... 30 s.
|
|
254
268
|
- **A slot used again.** The tables of a room are saved under
|
|
@@ -440,7 +454,7 @@ Whether the host sees the others come and go is the router's business:
|
|
|
440
454
|
|---|---|---|
|
|
441
455
|
| a home network (a plain zenohd, no access control) | yes | away at once (measured 10-20 ms after the node left; 6-14 ms after the table app closed while its node kept another application, games 0.10.0) |
|
|
442
456
|
| a room (console 0.3.0, seats) | yes: a room's host reads the whole room, the seats' liveliness tokens included | away at once (measured 6-7 ms) |
|
|
443
|
-
| the permanent tables (console roles `games-host` and `games`) | yes, when the role `games-host` has `Read asterism/*/game` (the console's docs/rooms.md): the router sends the host the players' table apps' liveliness tokens | away at once (measured 13 ms on the console's rehearsal) |
|
|
457
|
+
| the permanent tables (console roles `games-host` and `games`) | yes, when the role `games-host` has `Read asterism/*/game` and `asterism/*/game/protocol/*` (the console's docs/rooms.md; protocol 2's token is `asterism/<node>/game/protocol/2`): the router sends the host the players' table apps' liveliness tokens | away at once (measured 13 ms on the console's rehearsal) |
|
|
444
458
|
|
|
445
459
|
A role `games-host` made before that line (with only `Watch asterism/**`)
|
|
446
460
|
gets just the tokens on its own keys: the host then falls back to the time
|
|
@@ -456,6 +470,49 @@ sent); at a table, a host that leaves shows the connection lost, the
|
|
|
456
470
|
screen stops asking its view (one every 30 s, should the word not come)
|
|
457
471
|
and asks again as soon as the host is back.
|
|
458
472
|
|
|
473
|
+
## Topics and types
|
|
474
|
+
|
|
475
|
+
What the host puts on its application's keys are topics of protocol 2
|
|
476
|
+
(asterism's `docs/protocol.md` 9-11): each table's `<table>/state` and
|
|
477
|
+
what a game tells (the board's `<table>/strokes`), declared with
|
|
478
|
+
`net.publish` on the host's connection when it connects, with their
|
|
479
|
+
types, so a reader finds them before any value comes (`asterism topic
|
|
480
|
+
list games/games`, `net.pubs_of("games", "games")`), and each value
|
|
481
|
+
carries its sequence number (a reader counts what it missed). The types
|
|
482
|
+
are defined with `net.define_type` (`asterism topic info
|
|
483
|
+
games/games/othello1/state`, `net.types_of`):
|
|
484
|
+
|
|
485
|
+
| Topic | Type | |
|
|
486
|
+
|---|---|---|
|
|
487
|
+
| `<table>/state` of othello | `asterism-games/othello.state` | the public view ("Views" of othello); `res` an `asterism-games/othello.result` |
|
|
488
|
+
| `<table>/state` of mahjong | `asterism-games/mahjong.state` | the public view ("Views"); `res` an `asterism-games/mahjong.result` with `wn` of `asterism-games/mahjong.win` |
|
|
489
|
+
| `<table>/state` of the board | `asterism-games/board.state` | the board's state |
|
|
490
|
+
| `<table>/strokes` of the board | `asterism-games/board.strokes` | each change of the picture ("The drawing board") |
|
|
491
|
+
|
|
492
|
+
A field of mixed parts (`mb`, the members as `[name, seat, flags]`) is
|
|
493
|
+
an `any[]`, an array of arrays `int[][]` (mahjong's `fin`) or `any[][]`
|
|
494
|
+
(a winner's `pa`). A game declares its types in its class (`Game::TYPES`, `STATE_TYPE`,
|
|
495
|
+
`TOLD_TYPES`).
|
|
496
|
+
|
|
497
|
+
The spectate keys (`asterism/<node>/@shared/spectate/<table>/state`,
|
|
498
|
+
"Spectate") are in the node's shared place (asterism's `docs/protocol.md`
|
|
499
|
+
17), not under the host's application: the host declares each one on its
|
|
500
|
+
own connection with `net.publish("spectate/<table>/state", shared: :node,
|
|
501
|
+
type: ...)`, and a room's tables are declared by the room's application
|
|
502
|
+
`room<slot>` ("Spectate", "Outside the rooms"), each with the types
|
|
503
|
+
defined on its own connection (a reader reads a shared key's type from
|
|
504
|
+
its sender, `info.from`; `asterism topic list games/games`, `asterism
|
|
505
|
+
topic info games/@shared/spectate/mahjong1/state`):
|
|
506
|
+
|
|
507
|
+
| Shared topic | Type | |
|
|
508
|
+
|---|---|---|
|
|
509
|
+
| `spectate/<table>/state` of mahjong | `asterism-games/mahjong.spectate` | the spectate view ("Spectate"); `seats` of `asterism-games/mahjong.seat` |
|
|
510
|
+
|
|
511
|
+
A game declares them in its class too (`Game::SPECTATE_TYPE`,
|
|
512
|
+
`SPECTATE_TYPES`). Every value carries its sender and its sequence number
|
|
513
|
+
(`[seq, nil, "<node>/<app>"]`): a reader counts what it missed per
|
|
514
|
+
sender.
|
|
515
|
+
|
|
459
516
|
## Status
|
|
460
517
|
|
|
461
518
|
The host tells its tables' counts on the network (games 0.9.0), as
|
|
@@ -501,18 +558,18 @@ the host offers: its money of the day (`usd`), the limit it stops at
|
|
|
501
558
|
- **Who reads it.** `asterism watch` and `asterism tree` (the asterism
|
|
502
559
|
gem's command line), its MCP bridge (`asterism_status`), the console's
|
|
503
560
|
details of the host's node (console 0.3.4), or anyone with
|
|
504
|
-
`
|
|
561
|
+
`net.status_of("games", "games")` (which asks the application's
|
|
562
|
+
profile, `asterism/<node>/<app>/@profile?status`).
|
|
505
563
|
- **Where.** On a home network and on the permanent tables. The role
|
|
506
|
-
`games-host` needs `asterism/${node}/*/@status`
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
queryable on the key and a put when the counts change).
|
|
564
|
+
`games-host` needs `asterism/${node}/*/@status` and
|
|
565
|
+
`asterism/${node}/*/@pubs` in "Read and write" and the queryable of
|
|
566
|
+
`asterism/${node}/*/@profile` (the console's docs/rooms.md): a `**` does
|
|
567
|
+
not match a chunk that starts with `@`. A room's host tells none (the
|
|
568
|
+
rooms' rules let it write only its own place, which `@status` is not
|
|
569
|
+
part of, and the console only watches who is in a room).
|
|
570
|
+
- **The asterism gem.** The host uses Asterism's status (`net.status`,
|
|
571
|
+
`Host::TablesStatus`): Asterism puts it when it changes and answers the
|
|
572
|
+
application's profile.
|
|
516
573
|
- **Size.** Five tables take about 300 bytes (the largest put is in the
|
|
517
574
|
log's `largest message` with the others).
|
|
518
575
|
|
|
@@ -526,22 +583,33 @@ asterism gem's command line and MCP bridge). The mahjong table puts the
|
|
|
526
583
|
four hands there; othello and the drawing board put nothing (they hide
|
|
527
584
|
nothing: their state is the whole game).
|
|
528
585
|
|
|
529
|
-
- **The key.** `asterism/<node
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
586
|
+
- **The key.** `asterism/<node>/@shared/spectate/<table>/state`: the
|
|
587
|
+
host's node and its shared place (asterism's `docs/protocol.md` 17: data
|
|
588
|
+
with no single owner application, a chunk `@shared` that the players'
|
|
589
|
+
role `asterism/*/games/**` and every `**` do not match), and the table. A
|
|
590
|
+
room's table is `room<slot>-<table>` there
|
|
591
|
+
(`asterism/games/@shared/spectate/room01-mahjong1/state`), so the rooms'
|
|
592
|
+
tables and the permanent ones never meet. The value is MessagePack, like
|
|
593
|
+
the state.
|
|
594
|
+
- **Who declares it.** The permanent tables' host and a host of its own
|
|
595
|
+
declare their tables' keys on their own application's connection
|
|
596
|
+
(`games`, with `shared: :node`), with the game's spectate types; they
|
|
597
|
+
are in its `@pubs`. Each value carries its sender (`"games/games"`), so
|
|
598
|
+
a reader tells who put it and counts what it missed per sender.
|
|
535
599
|
- **Outside the rooms.** Every key a room's host writes on its own
|
|
536
600
|
connection gets the room's namespace (`room/<slot>/...`), which every
|
|
537
|
-
seat of the room reads whole. So a room's host opens a second
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
own
|
|
601
|
+
seat of the room reads whole. So a room's host opens a second Asterism
|
|
602
|
+
connection, the application `room<slot>` of the host's node (`room01`;
|
|
603
|
+
asterism: one application a connection, several in one program), with
|
|
604
|
+
its own certificate and without the namespace: it declares the room's
|
|
605
|
+
tables' keys in the node's shared place, on the registered side, where
|
|
606
|
+
the seats' rules do not reach, with its own types, liveliness token and
|
|
607
|
+
profile. Its values' sender is `"games/room01"`. It does so only with
|
|
608
|
+
the host's own certificate (a console from 0.3.0 lists the host in
|
|
609
|
+
`rooms.json`'s `"hosts"`); with the slot's certificate of an older
|
|
610
|
+
console it puts nothing (that certificate is the room's). It goes and
|
|
611
|
+
comes back with the room's host's connection (tried again every 30 s
|
|
612
|
+
when it cannot open).
|
|
545
613
|
- **What the mahjong table puts.** The table in words, as `look` gives it
|
|
546
614
|
("Playing from a program") to a member without a seat, with each seat's
|
|
547
615
|
`hand` and `drawn` tile, the same four hands a spectator member's view
|
|
@@ -553,7 +621,7 @@ nothing: their state is the whole game).
|
|
|
553
621
|
member's node or token (`test/test_spectate.rb` looks for them):
|
|
554
622
|
|
|
555
623
|
```
|
|
556
|
-
games/spectate/mahjong1/state
|
|
624
|
+
games/@shared/spectate/mahjong1/state
|
|
557
625
|
{"table":"mahjong1","status":"play","sv":212,
|
|
558
626
|
"seats":[{"seat":0,"name":"CPU1","kind":"cpu","score":21000,"wind":"north","riichi":false,
|
|
559
627
|
"river":["2z","4z","5z","1m",...],"melds":[{"type":"pon","tiles":["7z","7z","7z"],"from":1}],
|
|
@@ -572,25 +640,42 @@ nothing: their state is the whole game).
|
|
|
572
640
|
At most 8 MB is kept per table; past that the oldest is dropped (never
|
|
573
641
|
shown rather than shown early). The last one put is put again every
|
|
574
642
|
5 s, so a new subscriber has it within that.
|
|
643
|
+
- **The value now.** Asterism keeps no current value on the network
|
|
644
|
+
(protocol 2): a reader that missed values (`info.lost > 0`) or saw the
|
|
645
|
+
sender start again (`info.restarted?`, counted per sender) asks the
|
|
646
|
+
sender. The application that declares the keys exposes the object
|
|
647
|
+
`spectate` (`games/games/spectate`, a room's `games/room01/spectate`):
|
|
648
|
+
`state(table)` answers the value last put on
|
|
649
|
+
`@shared/spectate/<table>/state` (the table as the key names it, a room's
|
|
650
|
+
`room01-mahjong1`; as delayed as the key), nil before the first one. The
|
|
651
|
+
commentary does so ("Commentary"); `asterism call
|
|
652
|
+
games/games/spectate state '"mahjong1"'` too. A table named `spectate` keeps
|
|
653
|
+
its name, and its host does not expose the object.
|
|
575
654
|
- **Who reads it.** The console's docs/rooms.md, "Watching the tables
|
|
576
|
-
from outside": the host's role
|
|
577
|
-
`
|
|
578
|
-
and
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
`
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
`
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
655
|
+
from outside", and `docs/install.md` section 7: the host's role
|
|
656
|
+
`games-host` needs `asterism/${node}/@shared/**` (the keys),
|
|
657
|
+
`asterism/${node}/room$*` and `asterism/${node}/room$*/**` (the rooms'
|
|
658
|
+
applications) in "Read and write" beside its lines for `games` (without
|
|
659
|
+
them the router drops the puts, and nothing else changes); a role
|
|
660
|
+
`spectator` reads `asterism/*/@shared/spectate/**` (Watch
|
|
661
|
+
`asterism/**`), and, for the types (`asterism topic info`,
|
|
662
|
+
`net.type_of`), the senders' declarations (Read
|
|
663
|
+
`asterism/*/games/@profile`, `asterism/*/games/@pubs`,
|
|
664
|
+
`asterism/*/room$*/@profile`, `asterism/*/room$*/@pubs`; Ask
|
|
665
|
+
`asterism/*/games/@profile`, `asterism/*/room$*/@profile`), and, to
|
|
666
|
+
ask the value now, Read and Ask `asterism/*/games/spectate/call/${node}`
|
|
667
|
+
and `asterism/*/room$*/spectate/call/${node}`, and, for `asterism topic
|
|
668
|
+
list` to find the senders, Read `asterism/*/games/protocol/*` and
|
|
669
|
+
`asterism/*/room$*/protocol/*` (their liveliness tokens only). The
|
|
670
|
+
players' role `games` and the rooms' seats do not reach them.
|
|
671
|
+
- **Looking.** With the asterism gem's command line: `asterism object echo
|
|
672
|
+
'games/@shared/spectate/*/state' --credentials spectator.tar` (each put
|
|
673
|
+
as it comes, with its sender; `-o json` a line each), `asterism object
|
|
674
|
+
get games/@shared/spectate/mahjong1/state --credentials viewer.tar` (the
|
|
675
|
+
next one). The MCP bridge: `asterism_values` with the pattern
|
|
676
|
+
`games/@shared/spectate/*/state` (each sender's latest value). `asterism
|
|
677
|
+
topic list games/games` and `asterism topic list games/room01` show who
|
|
678
|
+
declares them.
|
|
594
679
|
- **Writing a game.** `spectate_view(now)` (nil, the default: nothing) and
|
|
595
680
|
`spectate_delay_ms` (0) of `AsterismGames::Game`.
|
|
596
681
|
|
|
@@ -673,7 +758,7 @@ What the framework gives a game:
|
|
|
673
758
|
| `changed`, `dirty?`, `unsaved?` | a change to save and put; a change only one member's view shows (`@unsaved = true`) is saved, not put |
|
|
674
759
|
| `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 |
|
|
675
760
|
| `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) |
|
|
676
|
-
| `spectate_view(now)`, `spectate_delay_ms` | what the host puts on `asterism/<node
|
|
761
|
+
| `spectate_view(now)`, `spectate_delay_ms` | what the host puts on `asterism/<node>/@shared/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 |
|
|
677
762
|
| `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` |
|
|
678
763
|
| `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 |
|
|
679
764
|
|
|
@@ -844,7 +929,7 @@ hand.
|
|
|
844
929
|
spectators are. A host started again starts the delay over (the past is
|
|
845
930
|
not saved).
|
|
846
931
|
- **From outside the table.** The same four hands, in words and as old
|
|
847
|
-
as the spectators' view, go on `asterism/<node
|
|
932
|
+
as the spectators' view, go on `asterism/<node>/@shared/spectate/<table>/state`
|
|
848
933
|
for a console's spectators and viewers ("Spectate"), a key the players
|
|
849
934
|
cannot read.
|
|
850
935
|
- **The default is no delay.** The spectators see the hands as they are:
|
|
@@ -1218,7 +1303,7 @@ assistant through `asterism mcp`).
|
|
|
1218
1303
|
| Method | Arguments | Returns |
|
|
1219
1304
|
|---|---|---|
|
|
1220
1305
|
| `records` | from (optional, 0) | `{"records" => [...], "total", "next"}`: the games, newest first, from the from-th, as many as fit one message; `next` is the from of the next page (none: the end). Each: `id`, `at` (the host's local time, `"2026-10-18 21:05"`), `game` (`hanchan` / `tonpuu`), `names`, `ranks`, `scores` (in seat order), `watch` (`true` for a watch game, "Watch games"; else absent) |
|
|
1221
|
-
| `record` | id | the game's head: `id`, `at`, `time` (
|
|
1306
|
+
| `record` | id | the game's head: `id`, `at`, `time` (ms since 1970), `table`, `game`, `rules` (as `look`'s), `names`, `kinds` (human / cpu / ai), `final` (`[[seat, rank, score, points x 10], ...]`), `reason` (as the view's `fr`), `rounds` (`[[wind, number, honba, kind, [four changes]], ...]`) |
|
|
1222
1307
|
| `record` | id, round | round `round` (from 0) as below, with `id`, `round`, `of` (how many rounds), `part`, `parts` |
|
|
1223
1308
|
| `record` | id, round, part | a round too big for one message comes in parts (`parts` > 1): part 0 has everything with the first moves, part k the moves from `move_from` on |
|
|
1224
1309
|
|
|
@@ -1543,7 +1628,7 @@ when it is taken), prints the address and opens it in the browser (macOS
|
|
|
1543
1628
|
|---|---|
|
|
1544
1629
|
| `--router LOCATOR`, `--credentials FILE` | as `play` (with neither, `ASTERISM_ROUTER` / `ASTERISM_CREDENTIALS`, all or nothing; with nothing, `tcp/127.0.0.1:7447`) |
|
|
1545
1630
|
| `--namespace PREFIX` | a room's tables: `room/01` shows `room01-<table>` (the spectate keys are outside the room, so the connection has no namespace) |
|
|
1546
|
-
| `--table TABLE` | the table to show first: `mahjong1`, `games/mahjong1` or `games/spectate/mahjong1`; without it, every table that puts its spectate key is offered and the first one shown |
|
|
1631
|
+
| `--table TABLE` | the table to show first: `mahjong1`, `games/mahjong1` or `games/@shared/spectate/mahjong1`; without it, every table that puts its spectate key is offered and the first one shown |
|
|
1547
1632
|
| `--node NODE` | the node ID (default `commentate-<host>-<pid>`, or the `.tar`'s name) |
|
|
1548
1633
|
| `--port N` | the page's port (default 7470) |
|
|
1549
1634
|
| `--public` | hide the hands: the views sent to the page lose `hand` and `drawn`, and no line tells a hand |
|
|
@@ -1555,10 +1640,22 @@ when it is taken), prints the address and opens it in the browser (macOS
|
|
|
1555
1640
|
| `--ai-model MODEL` | the model they ask (default `claude-haiku-5-5`) |
|
|
1556
1641
|
| `--ai-daily-usd USD` | what they may cost a day (default 3); then the fixed commentary alone |
|
|
1557
1642
|
|
|
1558
|
-
- **The role.** The spectate keys are
|
|
1559
|
-
|
|
1560
|
-
|
|
1643
|
+
- **The role.** The spectate keys are
|
|
1644
|
+
`asterism/<host>/@shared/spectate/<table>/state` (it subscribes
|
|
1645
|
+
`*/@shared/spectate/*/state`; the host of a value is its sender's node,
|
|
1646
|
+
`info.from`, the permanent tables' `games` or a room's `room<slot>`).
|
|
1647
|
+
On a console's network a `spectator` (Read
|
|
1648
|
+
`asterism/*/@shared/spectate/**`) or a `viewer` whose role reads the
|
|
1649
|
+
nodes' shared places reads them ("Spectate", "Who reads it"); the
|
|
1561
1650
|
players' role `games` does not. On a plain zenohd anyone does.
|
|
1651
|
+
- **Values lost, a host started again.** At a table's first value from
|
|
1652
|
+
its sender (the host, a room), when a value comes after lost ones, when
|
|
1653
|
+
the sender started again, and when it left the network and came back,
|
|
1654
|
+
the command asks the sender for the table now (`<node>/<app>/spectate`,
|
|
1655
|
+
`state`, "Spectate", "The value now") and shows that, unless a newer
|
|
1656
|
+
value came meanwhile; one ask at a time per table, on a thread of its
|
|
1657
|
+
own. Without the role's lines for the call it says so and goes on with
|
|
1658
|
+
the next value.
|
|
1562
1659
|
- **The page.** A bar of states on top (the table, the voice, the delay the
|
|
1563
1660
|
host puts on the table, a host that is not there), the table in a
|
|
1564
1661
|
canvas (the round, honba, riichi sticks, the wall and the dora
|
|
@@ -2293,7 +2390,8 @@ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 bundle exec rake test # also through a
|
|
|
2293
2390
|
letter), the countdown rewritten in place on a terminal (only the time
|
|
2294
2391
|
line, between ESC 7 and ESC 8), and the screens (a round in kanji with
|
|
2295
2392
|
its two-row hand and a river that goes on below, emoji and MPSZ; the lobby, a result,
|
|
2296
|
-
the end, a call, a spectator)
|
|
2393
|
+
the end, a call, a spectator), the view asked again after lost state
|
|
2394
|
+
values or a host that started again. With `ASTERISM_TEST_ROUTER`, a host in a
|
|
2297
2395
|
process of its own and three clients at once (kanji by default, emoji,
|
|
2298
2396
|
MPSZ) that play a
|
|
2299
2397
|
tonpuu game against three CPUs each to the end from scripted input
|
|
@@ -2311,7 +2409,9 @@ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 bundle exec rake test # also through a
|
|
|
2311
2409
|
frames, `--public`, `--json`, a page that opens late, a host that leaves
|
|
2312
2410
|
and comes back), the web server (the page, the sheets byte for byte,
|
|
2313
2411
|
404 / 405 / a foreign Host refused, 127.0.0.1 only, the events stream, a
|
|
2314
|
-
port taken), the options and `--help
|
|
2412
|
+
port taken), the options and `--help`, the table asked again after lost
|
|
2413
|
+
values or a sender that started again (once at a time, a newer value
|
|
2414
|
+
wins, a failed ask); with the table there, a CPU
|
|
2315
2415
|
round played here through the hub.
|
|
2316
2416
|
- `test_commentate_voice.rb`: the commentary's voice (in `rake test:gem`
|
|
2317
2417
|
too), against a small VOICEVOX engine started in the test: a character
|
|
@@ -2332,8 +2432,13 @@ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 bundle exec rake test # also through a
|
|
|
2332
2432
|
board), the keys of the permanent tables and of a room, what the rooms
|
|
2333
2433
|
give their hosts, the mahjong view (the four hands, no token, node, wall
|
|
2334
2434
|
or ura dora), a CPU game on the host, the delay (0, and 3 s across a
|
|
2335
|
-
round's end, in order, never sooner),
|
|
2336
|
-
|
|
2435
|
+
round's end, in order, never sooner), the keys in the node's shared
|
|
2436
|
+
place declared by the host's application and numbered with their
|
|
2437
|
+
sender, a room's table declared and put by the room's application
|
|
2438
|
+
`room<slot>` outside the room only, who opens that application, the
|
|
2439
|
+
last put again after 5 s, and the object `spectate` that answers the
|
|
2440
|
+
value last put (the host's and a room's application; none for a table
|
|
2441
|
+
named `spectate`).
|
|
2337
2442
|
- `test_play_commands.rb`: `--commands "sit,cpu,start"` against a table
|
|
2338
2443
|
in this process: they wait for the lobby when a game is going on as
|
|
2339
2444
|
the client joins (and then sit, fill and start), the owner's wait for
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.23.0
|
|
@@ -56,6 +56,20 @@ module AsterismGames
|
|
|
56
56
|
# A page of picture: at most this many points.
|
|
57
57
|
PAGE_POINTS = 400
|
|
58
58
|
KEY = "strokes"
|
|
59
|
+
# The state's type (public_view's keys; mb, the members as [name,
|
|
60
|
+
# flags], an any[]) and the strokes' (the messages above).
|
|
61
|
+
STATE_TYPE = "#{TYPE_PREFIX}board.state".freeze
|
|
62
|
+
STROKES_TYPE = "#{TYPE_PREFIX}board.strokes".freeze
|
|
63
|
+
TOLD_TYPES = { KEY => STROKES_TYPE }.freeze
|
|
64
|
+
TYPES = {
|
|
65
|
+
STATE_TYPE => [[%w[v int], %w[t str], %w[st str], %w[n int], %w[np int], %w[last int], %w[mb any[]],
|
|
66
|
+
%w[lim int[]]],
|
|
67
|
+
"A drawing board: its version, the strokes and points on it, the last stroke's id, the " \
|
|
68
|
+
"members as [name, flags], the limits"],
|
|
69
|
+
STROKES_TYPE => [[%w[o str], %w[id int], %w[c int], %w[w int], %w[p int[]], %w[k int], %w[v int]],
|
|
70
|
+
"A change of a board: o \"a\" points of stroke id (k 0 a new stroke), \"u\" stroke id " \
|
|
71
|
+
"taken back, \"c\" the board cleared; v the board's version"]
|
|
72
|
+
}.freeze
|
|
59
73
|
|
|
60
74
|
attr_reader :strokes, :points, :owner
|
|
61
75
|
|
|
@@ -266,10 +266,111 @@ module AsterismGames
|
|
|
266
266
|
end
|
|
267
267
|
end
|
|
268
268
|
|
|
269
|
+
# Asterism's protocol 2 keeps no current value on the network: a
|
|
270
|
+
# reader asks the sender for it (Host#spectate_now, the object
|
|
271
|
+
# <node>/<app>/spectate) at the first value of a table from a sender,
|
|
272
|
+
# and again when it missed values (info.lost > 0) or saw the sender
|
|
273
|
+
# start again (info.restarted?; both counted per sender on the shared
|
|
274
|
+
# place). A sender that left the network (on_app_leave) is forgotten:
|
|
275
|
+
# Asterism then forgets its numbers too, so its next value comes as a
|
|
276
|
+
# first one (lost 0, not restarted), and is asked for again. The
|
|
277
|
+
# answer goes to the hub unless a newer value came while asking. The
|
|
278
|
+
# asking is on a thread of its own: the values come on the receiving
|
|
279
|
+
# thread, where no call may wait.
|
|
280
|
+
class Refetch
|
|
281
|
+
attr_reader :asked, :taken
|
|
282
|
+
|
|
283
|
+
# call: ->(from, name) { the current value } ("<node>/<app>" of the
|
|
284
|
+
# sender, the table's name under spectate/); log: ->(message).
|
|
285
|
+
def initialize(hub, call:, log: ->(_m) {})
|
|
286
|
+
@hub = hub
|
|
287
|
+
@call = call
|
|
288
|
+
@log = log
|
|
289
|
+
@lock = Monitor.new
|
|
290
|
+
@seen = Hash.new(0) # table id => values given to the hub
|
|
291
|
+
@from = {} # table id => the sender of its last value
|
|
292
|
+
@want = {} # table id => [from, host, name, @seen when asked]
|
|
293
|
+
@queue = Thread::Queue.new
|
|
294
|
+
@asked = 0
|
|
295
|
+
@taken = 0
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# A value of a table came (the receiving thread): blk gives it to
|
|
299
|
+
# the hub; at a sender's first value, after lost values or when the
|
|
300
|
+
# sender started again, the current one is asked for (once at a time
|
|
301
|
+
# per table). Returns blk's.
|
|
302
|
+
def came(info, host, name, &blk)
|
|
303
|
+
id = MahjongHub.id(host, name)
|
|
304
|
+
why = nil
|
|
305
|
+
out = @lock.synchronize do
|
|
306
|
+
r = blk.call
|
|
307
|
+
@seen[id] += 1
|
|
308
|
+
from = info.from.to_s
|
|
309
|
+
first = @from[id] != from
|
|
310
|
+
@from[id] = from
|
|
311
|
+
why = if !from.include?("/") || @want.key?(id) then nil
|
|
312
|
+
elsif info.restarted? then "#{from} started again"
|
|
313
|
+
elsif info.lost.to_i.positive? then "#{info.lost} value(s) lost"
|
|
314
|
+
elsif first then "the first value from #{from}"
|
|
315
|
+
end
|
|
316
|
+
@want[id] = [from, host, name, @seen[id]] if why
|
|
317
|
+
r
|
|
318
|
+
end
|
|
319
|
+
if why
|
|
320
|
+
@log.call("#{id}: #{why}; asking #{info.from}/#{Commentate::MahjongRunner::SPECTATE_OBJECT} for the table now")
|
|
321
|
+
@queue << id
|
|
322
|
+
end
|
|
323
|
+
out
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# A sender ("<node>/<app>") left the network: its next value is a
|
|
327
|
+
# first one.
|
|
328
|
+
def forget(from)
|
|
329
|
+
@lock.synchronize { @from.delete_if { |_id, f| f == from } }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Asks for one table's current value and gives it to the hub (a
|
|
333
|
+
# newer value that came meanwhile wins). :taken, :newer, :none (the
|
|
334
|
+
# sender has nothing yet) or :error.
|
|
335
|
+
def run_one(id)
|
|
336
|
+
from, host, name, at = @lock.synchronize { @want[id] }
|
|
337
|
+
return :none unless from
|
|
338
|
+
@asked += 1
|
|
339
|
+
v = @call.call(from, name)
|
|
340
|
+
@lock.synchronize do
|
|
341
|
+
@want.delete(id)
|
|
342
|
+
return :newer if @seen[id] != at
|
|
343
|
+
return :none unless v.is_a?(Hash)
|
|
344
|
+
@hub.update(host, name, v)
|
|
345
|
+
@seen[id] += 1
|
|
346
|
+
end
|
|
347
|
+
@taken += 1
|
|
348
|
+
@log.call("#{id}: took the table now from #{from}")
|
|
349
|
+
:taken
|
|
350
|
+
rescue StandardError => e
|
|
351
|
+
@lock.synchronize { @want.delete(id) }
|
|
352
|
+
@log.call("#{id}: asking #{from}/#{Commentate::MahjongRunner::SPECTATE_OBJECT}: #{e.class}: #{e.message}")
|
|
353
|
+
:error
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def start
|
|
357
|
+
@thread = Thread.new { loop { run_one(@queue.pop) } }
|
|
358
|
+
self
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def stop
|
|
362
|
+
@thread&.kill
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
269
366
|
# `asterism-games commentate mahjong`: the connection, the spectate
|
|
270
367
|
# keys, the web server and the browser.
|
|
271
368
|
module MahjongRunner
|
|
272
|
-
|
|
369
|
+
# The spectate keys in every node's shared place (README "Spectate").
|
|
370
|
+
SPECTATE = "*/@shared/spectate/*/state"
|
|
371
|
+
# The sender's object that answers a table's current spectate value
|
|
372
|
+
# (Host::SPECTATE_OBJECT): <node>/<app>/spectate, state(table).
|
|
373
|
+
SPECTATE_OBJECT = "spectate"
|
|
273
374
|
STALE_S = 15
|
|
274
375
|
|
|
275
376
|
module_function
|
|
@@ -289,18 +390,20 @@ module AsterismGames
|
|
|
289
390
|
info.puts "commentate: hands hidden (--public)" if opts[:public]
|
|
290
391
|
open_browser(server.url, err) if opts[:open]
|
|
291
392
|
prefix = room_prefix(opts[:namespace])
|
|
393
|
+
refetch = nil
|
|
292
394
|
connect(opts, err) do |net|
|
|
293
395
|
net.on_leave { |n| hub.host_left(n) }
|
|
294
396
|
net.on_join { |n| hub.host_back(n) }
|
|
397
|
+
refetch = Refetch.new(hub, call: ->(from, name) { net["#{from}/#{SPECTATE_OBJECT}", timeout: 2].state(name) },
|
|
398
|
+
log: ->(m) { err.puts "commentate: #{m}" }).start
|
|
399
|
+
net.on_app_leave { |n, a| refetch.forget("#{n}/#{a}") }
|
|
295
400
|
net.start
|
|
296
|
-
net.
|
|
297
|
-
|
|
298
|
-
_, host, _, name, = sample.key.to_s.split("/")
|
|
401
|
+
net.subscribe(SPECTATE) do |v, info|
|
|
402
|
+
host, name = spectate_source(info)
|
|
299
403
|
next unless wanted?(host, name, opts[:table], prefix)
|
|
300
|
-
|
|
301
|
-
hub.update(host, name, v)
|
|
404
|
+
refetch.came(info, host, name) { hub.update(host, name, v) }
|
|
302
405
|
rescue StandardError => e
|
|
303
|
-
err.puts "commentate: #{
|
|
406
|
+
err.puts "commentate: #{info.key}: #{e.class}: #{e.message}"
|
|
304
407
|
end
|
|
305
408
|
stop = Thread::Queue.new
|
|
306
409
|
%w[INT TERM].each { |s| trap(s) { stop << true } }
|
|
@@ -310,6 +413,7 @@ module AsterismGames
|
|
|
310
413
|
end
|
|
311
414
|
end
|
|
312
415
|
ensure
|
|
416
|
+
refetch&.stop
|
|
313
417
|
server&.stop
|
|
314
418
|
voice&.stop
|
|
315
419
|
ai&.stop
|
|
@@ -349,6 +453,17 @@ module AsterismGames
|
|
|
349
453
|
end
|
|
350
454
|
end
|
|
351
455
|
|
|
456
|
+
# [host node, table] of a spectate value: the node of its sender
|
|
457
|
+
# (info.from, "<node>/<app>": the host's games or a room's room<slot>,
|
|
458
|
+
# the same node), else the key's; the table from the name under
|
|
459
|
+
# @shared/ (spectate/<table>/state).
|
|
460
|
+
def spectate_source(info)
|
|
461
|
+
from = info.from.to_s
|
|
462
|
+
host = from.include?("/") ? from.split("/").first : info.node
|
|
463
|
+
parts = info.name.to_s.split("/")
|
|
464
|
+
[host, parts[0] == "spectate" ? parts[1] : nil]
|
|
465
|
+
end
|
|
466
|
+
|
|
352
467
|
# --namespace room/01: the room's tables are room01-<table> outside it.
|
|
353
468
|
def room_prefix(ns)
|
|
354
469
|
return nil if ns.to_s.empty?
|
|
@@ -363,8 +478,8 @@ module AsterismGames
|
|
|
363
478
|
parts.size >= 3 ? "#{parts[0]}/#{parts[-1]}" : t
|
|
364
479
|
end
|
|
365
480
|
|
|
366
|
-
# --table: <table>, <host>/<table>, <host
|
|
367
|
-
# play's <host>/games/<table>.
|
|
481
|
+
# --table: <table>, <host>/<table>, <host>/@shared/spectate/<table>
|
|
482
|
+
# or the play's <host>/games/<table>.
|
|
368
483
|
def wanted?(host, name, table, prefix)
|
|
369
484
|
return false if host.nil? || name.nil?
|
|
370
485
|
return false if prefix && !name.start_with?(prefix)
|