asterism-games 0.17.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 +7 -0
- data/CHANGELOG.md +282 -0
- data/LICENSE +21 -0
- data/README.md +2009 -0
- data/VERSION +1 -0
- data/bin/asterism-games +116 -0
- data/bin/othello-tty +14 -0
- data/lib/asterism_games/ai.rb +457 -0
- data/lib/asterism_games/ai_hub.rb +171 -0
- data/lib/asterism_games/board/remote.rb +77 -0
- data/lib/asterism_games/board/table.rb +280 -0
- data/lib/asterism_games/board.rb +11 -0
- data/lib/asterism_games/config.rb +228 -0
- data/lib/asterism_games/game.rb +456 -0
- data/lib/asterism_games/host.rb +436 -0
- data/lib/asterism_games/othello/board.rb +161 -0
- data/lib/asterism_games/othello/remote.rb +85 -0
- data/lib/asterism_games/othello/table.rb +533 -0
- data/lib/asterism_games/othello.rb +12 -0
- data/lib/asterism_games/play/keys.rb +160 -0
- data/lib/asterism_games/play/mahjong.rb +508 -0
- data/lib/asterism_games/play/mahjong_screen.rb +607 -0
- data/lib/asterism_games/play/mahjong_text.rb +456 -0
- data/lib/asterism_games/play/othello.rb +456 -0
- data/lib/asterism_games/play.rb +254 -0
- data/lib/asterism_games/record_book.rb +468 -0
- data/lib/asterism_games/remote.rb +100 -0
- data/lib/asterism_games/rooms.rb +509 -0
- data/lib/asterism_games/store.rb +39 -0
- data/lib/asterism_games/version.rb +5 -0
- data/lib/asterism_games.rb +14 -0
- metadata +105 -0
data/README.md
ADDED
|
@@ -0,0 +1,2009 @@
|
|
|
1
|
+
# asterism-games
|
|
2
|
+
|
|
3
|
+
A showcase of [Asterism](https://github.com/ruby-asterism/asterism): how
|
|
4
|
+
to write the server side of an application in Ruby on an Asterism
|
|
5
|
+
network. A game host is a CRuby process that joins a Zenoh network as an
|
|
6
|
+
Asterism node, exposes each table as an Asterism object, holds the state
|
|
7
|
+
of the games and lets players on boards (Family mruby), browsers and PCs
|
|
8
|
+
play through Asterism calls.
|
|
9
|
+
|
|
10
|
+
It is two parts, in this one repository, handed out two ways ("The gem
|
|
11
|
+
and the image"): the framework with the two sample games is the gem
|
|
12
|
+
`asterism-games`, and the whole host with mahjong is a docker image.
|
|
13
|
+
|
|
14
|
+
- **The framework** (`lib/asterism_games.rb`, `lib/asterism_games/*.rb`):
|
|
15
|
+
everything a game host needs whatever the game is. The connection
|
|
16
|
+
(Asterism's block API, the tables exposed with `caller: :from`), the
|
|
17
|
+
host's loop, the puts of each table's state, saving and resuming, the
|
|
18
|
+
members and their tokens, who may join in a room, the rooms' supervisor
|
|
19
|
+
(one host process per room of an Asterism console), the permanent tables
|
|
20
|
+
next to the rooms, the status (`GET /status`) and the settings. A game
|
|
21
|
+
subclasses `AsterismGames::Game` and `AsterismGames::Remote` and writes
|
|
22
|
+
only its rules and the methods it exposes ("Writing a game"). The
|
|
23
|
+
framework requires no game.
|
|
24
|
+
- **The games**: riichi mahjong (`app/asterism_games/mahjong/`: four
|
|
25
|
+
players, seats nobody takes are played by CPUs), built on the
|
|
26
|
+
[riichi_engine](https://github.com/HenrikStephensen/riichi_engine) gem (a
|
|
27
|
+
fork with fixes, see "riichi_engine" below), which only that game needs,
|
|
28
|
+
so it is an application on the gem (`app/`) and not in it;
|
|
29
|
+
othello (`lib/asterism_games/othello/`: two seats and the spectators,
|
|
30
|
+
the smallest game on the framework, "The othello table"); and a shared
|
|
31
|
+
drawing board (`lib/asterism_games/board/`: no winner, one picture every
|
|
32
|
+
member draws on, handed out as strokes that a console can record and
|
|
33
|
+
play back, "The drawing board"). The table screens are Family mruby apps
|
|
34
|
+
(fmruby-core's `flash/app/game/mahjong/`, `flash/app/game/othello/` and
|
|
35
|
+
`flash/app/game/board/`), on the boards and in the browser build;
|
|
36
|
+
`asterism-games play othello` and `asterism-games play mahjong` are the
|
|
37
|
+
tables in a terminal, with CRuby and the asterism gem only ("Playing in a
|
|
38
|
+
terminal").
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
bin/asterism-games ──requires──> asterism_games/mahjong (app/, when there) ──> riichi_engine
|
|
42
|
+
│ ──requires──> asterism_games/othello
|
|
43
|
+
│ ──requires──> asterism_games/board
|
|
44
|
+
│ ──requires──> your game (--require FILE)
|
|
45
|
+
│ │ (Table < Game, Remote < Remote)
|
|
46
|
+
└──requires──> asterism_games <───┘
|
|
47
|
+
(Host, Rooms, Config, Store, Game, Remote) ──> asterism
|
|
48
|
+
|
|
49
|
+
the gem asterism-games: lib/ (framework, othello, board, the terminal clients), bin/asterism-games, bin/othello-tty
|
|
50
|
+
the image: all of the above, app/ (mahjong), config/, the Gemfile's gems
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Status: up to four players at a table (a lobby to sit and start, calls by
|
|
54
|
+
several players decided together, players that drop out played for after a
|
|
55
|
+
short wait until they are back), two tables at once on a local network, or
|
|
56
|
+
a table in each room of an Asterism console on a server ("Rooms") and
|
|
57
|
+
permanent tables next to them for the boards and browsers registered with
|
|
58
|
+
the console ("Permanent tables"; the server's setup: docs/install.md).
|
|
59
|
+
|
|
60
|
+
| Path | What |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `asterism-games.gemspec` | the gem: what it holds and needs ("The gem and the image") |
|
|
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
|
+
| `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
|
+
| `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
|
+
| `lib/asterism_games.rb` | the framework, without any game |
|
|
68
|
+
| `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") |
|
|
69
|
+
| `lib/asterism_games/remote.rb` | the base of the exposed object: the methods and their numbers of arguments, the caller (`from:`), `join` / `mine` / `leave` |
|
|
70
|
+
| `lib/asterism_games/host.rb` | the connection (Asterism's `Net`), the loop, the puts, the status file |
|
|
71
|
+
| `lib/asterism_games/rooms.rb` | the rooms' supervisor: one host process per room that opens a table ("Rooms"), and one for the permanent tables ("Permanent tables"); `GET /status` |
|
|
72
|
+
| `lib/asterism_games/config.rb` | the settings (file and environment; each game's table keys come from the game) |
|
|
73
|
+
| `lib/asterism_games/store.rb` | saved states, one JSON file per table |
|
|
74
|
+
| `app/asterism_games/mahjong.rb` | the mahjong game, outside the gem: requiring it registers `mahjong` |
|
|
75
|
+
| `app/asterism_games/mahjong/` | `table.rb` (seats, rounds, views, time, the table's settings), `engine.rb` (the calls into riichi_engine), `remote.rb` (the exposed object), `tiles.rb` (tile numbers) |
|
|
76
|
+
| `lib/asterism_games/othello.rb` | the othello game: requiring it registers `othello` |
|
|
77
|
+
| `lib/asterism_games/othello/` | `board.rb` (the rules, a game as a history, the CPU), `table.rb` (seats, the owner, time, views, saving), `remote.rb` (the exposed object) |
|
|
78
|
+
| `lib/asterism_games/board.rb` | the drawing board: requiring it registers `board` |
|
|
79
|
+
| `lib/asterism_games/board/` | `table.rb` (the strokes, the owner, the limits, the picture in pages, saving), `remote.rb` (the exposed object) |
|
|
80
|
+
| `config/example.yml` | settings to copy |
|
|
81
|
+
| `config/rooms.yml` | the settings of the server's image (rooms) |
|
|
82
|
+
| `deploy/vps/` | the server's image, compose files, `update.sh` (docs/install.md) |
|
|
83
|
+
| `.github/workflows/image.yml` | builds the image for each tag `v<version>` and publishes it on ghcr.io |
|
|
84
|
+
| `docs/install.md` | putting the host on a server next to an Asterism console |
|
|
85
|
+
| `test/` | the tests (`rake test`, `rake test:gem`); `test/framework/` a small game on the framework alone |
|
|
86
|
+
|
|
87
|
+
## The gem and the image
|
|
88
|
+
|
|
89
|
+
**The gem** `asterism-games` (`require "asterism_games"`) is for writing a
|
|
90
|
+
game host of your own: the framework, the two sample games that need
|
|
91
|
+
nothing else (othello, the drawing board), `bin/asterism-games` (with
|
|
92
|
+
`play`, the terminal clients of othello and mahjong: a mahjong table needs
|
|
93
|
+
riichi_engine, its client does not) and `bin/othello-tty`. It depends on
|
|
94
|
+
the asterism gem only (0.7.1 or later in
|
|
95
|
+
0.x; what came after 0.7.1, such as `@status` and `on_app_join`, is used
|
|
96
|
+
when there). Not in it: mahjong (`app/`, it needs the riichi_engine fork
|
|
97
|
+
from git), `bin/mahjong-bots`, `deploy/`, `config/` and the tests. The AI
|
|
98
|
+
players' code is in it, and is offered only with a key; Claude needs the
|
|
99
|
+
anthropic gem as well, which is not a dependency (`gem install anthropic`,
|
|
100
|
+
"AI players").
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
gem install asterism-games # CRuby 3.2 or newer and a C compiler (asterism-zenoh)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A host of the sample games on a plain zenohd (`games.yml`):
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
router: tcp/127.0.0.1:7447
|
|
110
|
+
node: games
|
|
111
|
+
state_dir: var/state
|
|
112
|
+
tables:
|
|
113
|
+
- name: othello1
|
|
114
|
+
game: othello
|
|
115
|
+
- name: board1
|
|
116
|
+
game: board
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
asterism-games --config games.yml # games/games/othello1, games/games/board1
|
|
121
|
+
asterism-games play othello --commands "sit,cpu,start" # in another terminal: play against the CPU
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Without a settings file the host serves the first game's tables: with the
|
|
125
|
+
gem alone that is othello (`othello1`). A game of your own ("Writing a
|
|
126
|
+
game") is a file that requires `asterism_games` and registers its table;
|
|
127
|
+
the host loads it with `--require`, and the settings name its tables:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
asterism-games --config my.yml --require ./counter.rb
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**The image** `ghcr.io/ruby-asterism/asterism-games:<version>` is the
|
|
134
|
+
whole host as the server runs it: the gem's files, mahjong (`app/`), the
|
|
135
|
+
settings of the rooms (`config/rooms.yml`) and the gems of `Gemfile.lock`
|
|
136
|
+
(riichi_engine at a pinned revision, anthropic). The CI
|
|
137
|
+
(`.github/workflows/image.yml`) builds it for each tag `v<version>` (the
|
|
138
|
+
number of `VERSION`) for amd64 and arm64. `docker pull
|
|
139
|
+
ghcr.io/ruby-asterism/asterism-games:0.12.0`; next to a console:
|
|
140
|
+
docs/install.md (`deploy/vps`, which pulls it, or builds it with
|
|
141
|
+
`docker-compose.build.yml`).
|
|
142
|
+
|
|
143
|
+
## Running
|
|
144
|
+
|
|
145
|
+
From this checkout (everything, mahjong too). CRuby 3.2 or newer and a C
|
|
146
|
+
compiler (asterism-zenoh compiles its extension). Bundler fetches the
|
|
147
|
+
riichi_engine fork from GitHub (see "riichi_engine"). The `Gemfile` is for
|
|
148
|
+
this checkout and the image (mahjong, the AI players); the gem's own needs
|
|
149
|
+
are in the gemspec.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
bundle config set --local path vendor/bundle
|
|
153
|
+
bundle install
|
|
154
|
+
ASTERISM_GAMES_CONFIG=config/example.yml bundle exec bin/asterism-games
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The host connects to the router in the settings (by default
|
|
158
|
+
`tcp/127.0.0.1:7447`, a zenohd on this machine) as the node `games` and the
|
|
159
|
+
application `games`, and exposes each table as an object:
|
|
160
|
+
`games/games/mahjong1` and `games/games/mahjong2` (the default settings;
|
|
161
|
+
`config/example.yml` adds `games/games/othello1`). Ctrl-C (or SIGTERM) saves and stops. Started again,
|
|
162
|
+
it goes on from the saved state (the same round, the same turn, the same
|
|
163
|
+
seats and tokens). When the router goes away, it connects again every 3 s.
|
|
164
|
+
|
|
165
|
+
asterism comes from rubygems at a fixed version (0.8.0); riichi_engine from
|
|
166
|
+
the `fmrb` branch of the fork on GitHub (see "riichi_engine" below).
|
|
167
|
+
|
|
168
|
+
## Settings
|
|
169
|
+
|
|
170
|
+
A YAML file (`--config PATH` or `ASTERISM_GAMES_CONFIG`; see
|
|
171
|
+
`config/example.yml`), with the environment over it:
|
|
172
|
+
|
|
173
|
+
| Key | Environment | Default | |
|
|
174
|
+
|---|---|---|---|
|
|
175
|
+
| `router` | `ASTERISM_GAMES_ROUTER` | `tcp/127.0.0.1:7447` | the Zenoh router |
|
|
176
|
+
| `node` | `ASTERISM_GAMES_NODE` | `games` | this host's node ID |
|
|
177
|
+
| `app` | `ASTERISM_GAMES_APP` | `games` | the application every game lives under |
|
|
178
|
+
| `credentials` | `ASTERISM_GAMES_CREDENTIALS` | none | a console's `.tar`, for a router over mutual TLS (the router comes from it, and the node ID too unless `node` is given) |
|
|
179
|
+
| `state_dir` | `ASTERISM_GAMES_STATE_DIR` | `var/state` | where the tables are saved |
|
|
180
|
+
| `records_db` | `ASTERISM_GAMES_RECORDS_DB` | `state_dir/records.sqlite3` | the games' records ("Game records"): the SQLite database, or `json` for the JSON files of before 0.14.0 (also what a host without the sqlite3 gem uses) |
|
|
181
|
+
| `tables` | | the first game's tables (`mahjong1`, `mahjong2`; with the gem alone `othello1`) | each `{name, game, ...}`; `game` defaults to the first game registered (`mahjong`; with the gem alone `othello`) |
|
|
182
|
+
|
|
183
|
+
The other keys of a table are its game's (`settings` in the game's class,
|
|
184
|
+
"Writing a game"). A mahjong table takes (the environment applies to every
|
|
185
|
+
mahjong table):
|
|
186
|
+
|
|
187
|
+
| Key | Environment | Default | |
|
|
188
|
+
|---|---|---|---|
|
|
189
|
+
| `turn_seconds` | `MAHJONG_TURN_SECONDS` | 5 | a player's time for a move (a discard, riichi, a kan, a win); a move that takes longer goes on into the bank ("Time") |
|
|
190
|
+
| `bank_seconds` | `MAHJONG_BANK_SECONDS` | 20 | each player's bank, full again every round; when the move's time and the bank are used up, the drawn tile is discarded (0: no bank, `turn_seconds` a move and no more) |
|
|
191
|
+
| `call_seconds` | `MAHJONG_CALL_SECONDS` | 8 | the time to call a discard, the same for every player who may; after it the call is passed |
|
|
192
|
+
| `result_seconds` | `MAHJONG_RESULT_SECONDS` | 8 | a table without humans (CPUs and AI players only): a round's result stays this long. A table with humans waits for their OKs instead ("Result", games 0.15.0) |
|
|
193
|
+
| `result_auto_seconds` | `MAHJONG_RESULT_AUTO_SECONDS` | 10 | a table with humans: a round's result moves on by itself this long after its count, whoever has not pressed OK ("Result") |
|
|
194
|
+
| `deal_pause_seconds` | `MAHJONG_DEAL_PAUSE_SECONDS` | 2 | a table with humans: after the deal the round's name shows this long; no clock runs and no CPU or AI player plays ("Result"; 0: none) |
|
|
195
|
+
| `over_seconds` | `MAHJONG_OVER_SECONDS` | 60 | the end of a game stays this long (or until the owner presses next), then the table is back in its lobby |
|
|
196
|
+
| `away_seconds` | `MAHJONG_AWAY_SECONDS` | 10 | a member whose node leaves the network is away at once; one that has not called the table this long is away too (where the host does not see the nodes come and go: "Who is there") |
|
|
197
|
+
| `away_wait_seconds` | `MAHJONG_AWAY_WAIT_SECONDS` | 2 | an away member's time to discard or to call, instead of `turn_seconds` and the bank / `call_seconds` |
|
|
198
|
+
| `cpu_ms` | `MAHJONG_CPU_MS` | 700 | a CPU's pause before it acts |
|
|
199
|
+
| `cpu_level` | `MAHJONG_CPU_LEVEL` | `normal` | `easy` / `normal` / `hard` (riichi_engine's CPU levels) |
|
|
200
|
+
| `ai_turn_seconds` | `MAHJONG_AI_TURN_SECONDS` | 30 | an AI player's time for a move; then the CPU plays it ("AI players") |
|
|
201
|
+
| `ai_call_seconds` | `MAHJONG_AI_CALL_SECONDS` | 15 | an AI player's time to answer a discard it may call |
|
|
202
|
+
| `humans` | | 4 | seats members may take (0: four CPUs that play at once, for tests) |
|
|
203
|
+
| `spectators` | `MAHJONG_SPECTATORS` | 12 | members without a seat at most ("Spectators") |
|
|
204
|
+
| `spectator_delay_minutes` | `MAHJONG_SPECTATOR_DELAY_MINUTES` | 0 | spectators see the game (and the four hands) this much later ("Spectators"); `spectator_delay_seconds` / `MAHJONG_SPECTATOR_DELAY_SECONDS` as before, in seconds |
|
|
205
|
+
| `rule` | | (below) | riichi_engine rule keys over the host's defaults ("Rules"); the owner's local rules go over them ("Local rules") |
|
|
206
|
+
| `records_kept` | `MAHJONG_RECORDS_KEPT` | 1000 (SQLite), 20 (JSON) | the newest games kept of a table ("Game records") |
|
|
207
|
+
| `owner_handover_seconds` | `MAHJONG_OWNER_HANDOVER_SECONDS` | 60 | the owner away this long: the role goes to a member who is there ("Players that drop out"; 0: never) |
|
|
208
|
+
| `abandon_seconds` | `MAHJONG_ABANDON_SECONDS` | 180 | a game whose seated humans have all been away this long (or have left) is abandoned: back to the lobby, not recorded ("Players that drop out"; 0: never) |
|
|
209
|
+
|
|
210
|
+
An othello table's keys: "The othello table".
|
|
211
|
+
|
|
212
|
+
## Rooms
|
|
213
|
+
|
|
214
|
+
An Asterism console (0.2.2 and later) makes rooms: a key space of its own
|
|
215
|
+
(`room/<slot>/**`) that people join from a browser with the room's link
|
|
216
|
+
(the console's docs/rooms.md). With a `rooms` section (or
|
|
217
|
+
`ASTERISM_GAMES_ROOMS_DIR`) the host serves tables in rooms instead of the
|
|
218
|
+
`tables` above.
|
|
219
|
+
|
|
220
|
+
- **Which rooms, which tables.** The console writes the open rooms to
|
|
221
|
+
`rooms.json` in its "rooms" volume, with the app each room's link opens.
|
|
222
|
+
A room that opens one of `rooms.apps` (by default every game's table
|
|
223
|
+
screen, `Game.screens`: `/app/game/mahjong/mahjong.app.rb`,
|
|
224
|
+
`/app/game/othello/othello.app.rb`, `/app/game/board/board.app.rb`) gets
|
|
225
|
+
the tables of `rooms.tables` whose game has that screen: a room opening
|
|
226
|
+
the mahjong screen gets the mahjong tables, one opening the othello
|
|
227
|
+
screen the othello tables, one opening the board's screen the board. Without
|
|
228
|
+
`rooms.tables` each game's first table is there (`mahjong1`, `othello1`),
|
|
229
|
+
so each room gets one table of its game. Other rooms (another app, or a
|
|
230
|
+
game with no table in `rooms.tables`) get nothing. The admin chooses the
|
|
231
|
+
app when making the room (Admin > Rooms, "Opens": Mahjong; the console's
|
|
232
|
+
`ASTERISM_ROOM_APPS` lists what may be chosen), so a table needs no step
|
|
233
|
+
beyond the one a room needs anyway. The people in the room open the link
|
|
234
|
+
and see the table in the screen's list, as on a home network
|
|
235
|
+
(`games/games/mahjong1`).
|
|
236
|
+
- **One process per room.** The asterism gem keeps one connection a
|
|
237
|
+
process, so the supervisor forks a host process (Host) per room. It joins
|
|
238
|
+
the router in the room's namespace (`room/<slot>`): its keys are the
|
|
239
|
+
room's, and nothing of it reaches other rooms or the registered side (the
|
|
240
|
+
router's ACL). **Which certificate** (0.5.0): when `rooms.json` lists the
|
|
241
|
+
host's node in `"hosts"` (a console 0.3.0, its `ASTERISM_ROOM_HOSTS`),
|
|
242
|
+
the router keeps the rooms' `asterism/games...` for the host's own
|
|
243
|
+
certificate, and the process joins with it: `rooms.credentials`, else the
|
|
244
|
+
permanent tables' `.tar` (`permanent.credentials`; the same row `games`
|
|
245
|
+
of the console's registry), at `rooms.router` or the permanent tables'
|
|
246
|
+
router. With a console before 0.3.0 it joins with the slot's certificate
|
|
247
|
+
(`certs/room-<slot>.tar`, in the same volume, read-only), as before. A
|
|
248
|
+
console that lists the host while the host has no certificate set: the
|
|
249
|
+
log says so and the rooms get no table that works. At most `rooms.max` rooms have tables (3); a room
|
|
250
|
+
beyond that gets none (the status says so). A host process that stops is
|
|
251
|
+
started again after 1, 2, 4 ... 30 s.
|
|
252
|
+
- **A slot used again.** The tables of a room are saved under
|
|
253
|
+
`state_dir/rooms/<slot>/` with the room's ID (`room.json`). When the room
|
|
254
|
+
is closed or has ended (the file lists it as revoked, or its end has
|
|
255
|
+
passed), or the slot holds another room, the host process stops and the
|
|
256
|
+
directory is removed: the next room on the slot starts from an empty
|
|
257
|
+
lobby, with no seats or tokens of the last one. A file that cannot be read
|
|
258
|
+
changes nothing (it is not taken as every room closed).
|
|
259
|
+
- **Restarts.** Stopped (SIGTERM) and started again, the supervisor finds
|
|
260
|
+
the open rooms and each host reads its saved tables: a game goes on from
|
|
261
|
+
the same turn. When the router restarts, each host connects again within
|
|
262
|
+
3 s.
|
|
263
|
+
- **Status.** `rooms.status_listen` (`0.0.0.0:7460` in the image) answers
|
|
264
|
+
`GET /status`: the rooms with tables, each table's state and counts
|
|
265
|
+
(seated, CPUs, at the table, away; no names, nodes or tokens), whether the
|
|
266
|
+
host can read `rooms.json`, the rooms without a table, `busy` (someone
|
|
267
|
+
sits at a table and is there: away seats do not count) and the RSS. The console's rooms page shows it
|
|
268
|
+
(`ASTERISM_GAMES_STATUS_URL`); `bin/asterism-games status [--idle]` prints
|
|
269
|
+
it (`--idle` exits 0 when nobody sits at a table: update.sh's
|
|
270
|
+
`--when-idle`). In the console's graph each room's frame shows the host's
|
|
271
|
+
node (`games`) and its tables, like any node in a room.
|
|
272
|
+
|
|
273
|
+
### Who joins
|
|
274
|
+
|
|
275
|
+
A seat at a table goes with the node ID of the member that took it: a
|
|
276
|
+
member's `join` and `mine` give back its token, and with it its hand. So
|
|
277
|
+
the table must know that a call's caller (`from:`, which Asterism fills
|
|
278
|
+
in from the last chunk of the call's key: the tables are exposed with
|
|
279
|
+
`caller: :from`) is who it says.
|
|
280
|
+
|
|
281
|
+
**With seats** (a console 0.3.0: `rooms.json` says how many seats the
|
|
282
|
+
room's slot has, `"seats"`, and the seats' node prefix): the console's door
|
|
283
|
+
gives each page in a room a seat and connects it with the seat's
|
|
284
|
+
certificate, and the router lets a seat call only under its own node ID,
|
|
285
|
+
`<prefix><slot><seat>` (`wasm-0103`), and lets only the host's certificate
|
|
286
|
+
call under the host's names (`games...`, the host and its bots). The room's
|
|
287
|
+
host takes those callers as they come, with no secret, as a board's
|
|
288
|
+
certificate is taken on the permanent tables. The supervisor hands the
|
|
289
|
+
room's host the seats' node IDs (`wasm-0101` .. `wasm-0116`) and the host's
|
|
290
|
+
name; a room whose seats or hosts change in `rooms.json` (the console was
|
|
291
|
+
updated during the room) gets its host started again. A seat may go to
|
|
292
|
+
another page after nobody was on it for a while (the door's
|
|
293
|
+
`WS_DOOR_SEAT_IDLE`, 30 minutes): a seat's node ID that has not called the
|
|
294
|
+
table for `rooms.seat_reuse_seconds` (1800) is a new page, and the old
|
|
295
|
+
member leaves (its seat is played by a CPU in a game) before the new one
|
|
296
|
+
joins. A page opened again in the same browser comes back with its secret
|
|
297
|
+
and gets the same seat (the console 0.3.9: the door cuts what is left of
|
|
298
|
+
the old connection), so it is the same member, at its seat, with its hand:
|
|
299
|
+
the same person within `seat_reuse_seconds`. That time stays at the door's
|
|
300
|
+
30 minutes (games 0.16.2 looked at it again): the door gives a seat to
|
|
301
|
+
another page only after as long without a connection, and a longer time
|
|
302
|
+
here would hand the old member's seat and hand to that new page.
|
|
303
|
+
|
|
304
|
+
Nobody else joins a room's table: a `join` or `mine` from another caller
|
|
305
|
+
answers `{"error" => "identity"}` (the screen says to open the room's link
|
|
306
|
+
again). The pages of the browser build from before the seats (a node ID
|
|
307
|
+
worked out from a secret) are not taken any more (games 0.7.0).
|
|
308
|
+
|
|
309
|
+
Outside the rooms nothing is checked (a home network; the permanent
|
|
310
|
+
tables, where a board's certificate is its node ID).
|
|
311
|
+
|
|
312
|
+
- **Bots in a room.** `bin/mahjong-bots --credentials games.tar --namespace
|
|
313
|
+
room/01` (the host's own certificate): the bots are `games-bot<pid>-<i>`,
|
|
314
|
+
names the router keeps for that certificate, so the table takes them as
|
|
315
|
+
they are.
|
|
316
|
+
- **What the router now keeps closed** (console 0.3.0): a member cannot
|
|
317
|
+
declare a queryable, put a state or a liveliness token, or reply on the
|
|
318
|
+
host's keys (`room/<slot>/asterism/games...`), so it cannot pose as the
|
|
319
|
+
table and collect what other pages send; nor can a page from before the
|
|
320
|
+
seats (the slot's certificate has a deny on the host's and the seats'
|
|
321
|
+
names).
|
|
322
|
+
|
|
323
|
+
| Key (`rooms:`) | Environment | Default | |
|
|
324
|
+
|---|---|---|---|
|
|
325
|
+
| `dir` | `ASTERISM_GAMES_ROOMS_DIR` | none (no rooms) | the console's rooms volume: `rooms.json`, `certs/` |
|
|
326
|
+
| `apps` | `ASTERISM_GAMES_ROOM_APPS` (comma separated) | the games' screens (`/app/game/mahjong/mahjong.app.rb`, `/app/game/othello/othello.app.rb`, `/app/game/board/board.app.rb`) | a room whose link opens one of these gets the tables of the game with that screen |
|
|
327
|
+
| `max` | `ASTERISM_GAMES_ROOM_MAX` | 3 | rooms with tables at once |
|
|
328
|
+
| `node` | `ASTERISM_GAMES_ROOM_NODE` | `games` | the host's node ID in a room |
|
|
329
|
+
| `router` | `ASTERISM_GAMES_ROOM_ROUTER` | the certificate's (`tls/zenohd:7448`) | the router |
|
|
330
|
+
| `status_listen` | `ASTERISM_GAMES_STATUS_LISTEN` | none | host:port of `GET /status` |
|
|
331
|
+
| `tables` | | each game's first table (`mahjong1`, `othello1`) | as `tables` above; a room gets those of the game its link opens; the `MAHJONG_*` and `OTHELLO_*` environment applies |
|
|
332
|
+
| `credentials` | `ASTERISM_GAMES_ROOM_CREDENTIALS` | `permanent.credentials` | the host's own `.tar` for the rooms (used when `rooms.json` lists the host in `"hosts"`) |
|
|
333
|
+
| `seat_reuse_seconds` | `ASTERISM_GAMES_ROOM_SEAT_REUSE_SECONDS` | 1800 | a seat's node ID silent this long at a table is a new page |
|
|
334
|
+
|
|
335
|
+
Memory (CRuby 3.2, the image, measured): the supervisor about 43 MB RSS,
|
|
336
|
+
each room's host about 45-50 MB more (the forked process shares the loaded
|
|
337
|
+
code with the supervisor at first).
|
|
338
|
+
|
|
339
|
+
## Permanent tables
|
|
340
|
+
|
|
341
|
+
Next to the rooms, the supervisor can keep tables of its own on the
|
|
342
|
+
registered side of the console's router: the boards that connect with
|
|
343
|
+
their own certificates (P4-Nano, Tab5: `/home/asterism/config.toml` and
|
|
344
|
+
their `.tar`) and the browsers with a pass see them in the table screen's
|
|
345
|
+
list (`games/games/mahjong1`, `games/games/mahjong2`), as on a home network.
|
|
346
|
+
|
|
347
|
+
- **The host's certificate.** The console registers the host as a row of
|
|
348
|
+
its own (kind `client`, name `games`, the role `games-host`) and issues
|
|
349
|
+
its certificate; the `.tar` ("Save all (.tar)") goes to the host
|
|
350
|
+
(`permanent.credentials`). The router's ACL knows the host by that name,
|
|
351
|
+
so the node ID is the certificate's name unless `permanent.node` says
|
|
352
|
+
otherwise. The players' rows get the role `games`. The roles' templates
|
|
353
|
+
and what they keep closed: the console's docs/rooms.md, "A game host's
|
|
354
|
+
permanent tables".
|
|
355
|
+
- **One more process.** The supervisor forks one more host process
|
|
356
|
+
(Host) for the tables of `permanent.tables` (`mahjong1` and `mahjong2`),
|
|
357
|
+
joins the router with that certificate, outside every room (no
|
|
358
|
+
namespace), and puts the host's profile with the name `permanent tables`
|
|
359
|
+
(games 0.7.1; `asterism-games` before). A room's host joins with the same
|
|
360
|
+
certificate, so both have the node ID `games`: the console's graph shows
|
|
361
|
+
this one as `permanent tables (games)` and a room's as `games (room 05)`
|
|
362
|
+
(console 0.3.3). It is started
|
|
363
|
+
again like a room's host (1, 2, 4 ... 30 s). Without the certificate file
|
|
364
|
+
(or not readable) the supervisor waits for it and says so once in the
|
|
365
|
+
log; the rooms go on.
|
|
366
|
+
- **Saving.** The tables are saved under `state_dir/permanent/` after every
|
|
367
|
+
change and taken back at every start; the rooms' clean-up never touches
|
|
368
|
+
them. A game goes on from the same turn after a restart of the host.
|
|
369
|
+
- **Status.** `GET /status` has `permanent` (the host's node, `state`: `up`,
|
|
370
|
+
`starting`, `restarting` or `no certificate`, each table's counts; null
|
|
371
|
+
when not set up); `busy` counts its seats too, and the RSS its process.
|
|
372
|
+
The console (0.2.3) shows it on the rooms' page under the game host's
|
|
373
|
+
line; the graph shows the node `games` and its tables on the registered
|
|
374
|
+
side.
|
|
375
|
+
- **Hands.** A seat's view needs the seat's token, which `join` gives to
|
|
376
|
+
the node that joined. A board's node ID is its certificate's name and the
|
|
377
|
+
router lets only that certificate call under it, so another member cannot
|
|
378
|
+
get a board's token. The browsers are one subject at the router (any
|
|
379
|
+
browser may call under any `wasm-*` node ID, the console's docs/g1_5.md),
|
|
380
|
+
so a browser's seat is as safe as the other pass holders are trusted.
|
|
381
|
+
|
|
382
|
+
| Key (`permanent:`) | Environment | Default | |
|
|
383
|
+
|---|---|---|---|
|
|
384
|
+
| `credentials` | `ASTERISM_GAMES_PERMANENT_CREDENTIALS` | none (no permanent tables) | the host's `.tar` from the console |
|
|
385
|
+
| `router` | `ASTERISM_GAMES_PERMANENT_ROUTER` | the certificate's (`tls/zenohd:7448` in the image) | the router |
|
|
386
|
+
| `node` | `ASTERISM_GAMES_PERMANENT_NODE` | the certificate's name | the host's node ID |
|
|
387
|
+
| `tables` | `ASTERISM_GAMES_PERMANENT_TABLES` (names, comma separated; a name finds its game, or `name:game`) | `mahjong1`, `mahjong2` (the image's `config/rooms.yml`: and `othello1`) | as `tables` above; the `MAHJONG_*` and `OTHELLO_*` environment applies |
|
|
388
|
+
|
|
389
|
+
The section is read only with `rooms` (the supervisor). Without rooms, the
|
|
390
|
+
host itself does the same with `credentials` and `tables`.
|
|
391
|
+
|
|
392
|
+
Memory (the image, measured with a room's table and both permanent tables
|
|
393
|
+
in play): the permanent tables' process 51 MB RSS (24 MB PSS: the rest is
|
|
394
|
+
shared with the supervisor), the container 66 MiB in all with the
|
|
395
|
+
supervisor and one room. With `rooms.max` (3) rooms as well, about 250 MB
|
|
396
|
+
RSS (about 120 MB PSS).
|
|
397
|
+
|
|
398
|
+
## Who is there
|
|
399
|
+
|
|
400
|
+
A member is away when it is gone: its seat shows it (`"aw"`, the member
|
|
401
|
+
list's flag), a mahjong seat is played for after a short wait, the status
|
|
402
|
+
counts it. The host learns it from Asterism, not from the calls (games
|
|
403
|
+
0.8.0), and by the player's table app (games 0.10.0): a table app runs as
|
|
404
|
+
the application **`game`** of its node (the boards' and the browsers'
|
|
405
|
+
table apps, `bin/othello-tty`, `bin/mahjong-bots` do), and the host
|
|
406
|
+
follows the liveliness token `asterism/<node>/game`. The Net's
|
|
407
|
+
`on_app_leave` tells it that the table app left the network (a closed
|
|
408
|
+
browser tab, a board's app that closed, a process killed, a line that
|
|
409
|
+
dropped) even when the node stays with other applications (a board that
|
|
410
|
+
runs another Asterism app next to the table), and `on_app_join` that it
|
|
411
|
+
is there again. With asterism 0.7.1 (no `on_app_*`) the host watches the
|
|
412
|
+
tokens `asterism/*/game` itself on the session. The host passes both to
|
|
413
|
+
every table (`Game#node_left` / `node_joined`):
|
|
414
|
+
|
|
415
|
+
- a member whose node leaves is away at once, and back at once when its
|
|
416
|
+
node joins again (before it calls anything);
|
|
417
|
+
- a member whose node the host has seen join is there for as long as the
|
|
418
|
+
node is, calling or not (a screen on the list of tables, a terminal
|
|
419
|
+
watching);
|
|
420
|
+
- a member whose node the host has never seen (see below), or that does
|
|
421
|
+
not run as the application `game` (an `asterism mcp` client started
|
|
422
|
+
without `--app game`, a script with an app name of its own), is away when
|
|
423
|
+
it has not called the table for `away_seconds`, as before 0.8.0: the
|
|
424
|
+
screens still ask their view every few seconds for that.
|
|
425
|
+
|
|
426
|
+
Members gone for long are forgotten six times `away_seconds` after their
|
|
427
|
+
last call or after their node left, whichever is later (seated members are
|
|
428
|
+
kept); in a room a seat's node ID gone for `seat_reuse_seconds` is a new
|
|
429
|
+
page when it comes back ("Who joins"). Presence is not saved: a host that
|
|
430
|
+
starts again counts its members as just seen, and the nodes there join on
|
|
431
|
+
its first polls. When the host's own connection drops, every node leaves
|
|
432
|
+
and joins again once it is back (a second or so, "When the connection is
|
|
433
|
+
lost" of asterism).
|
|
434
|
+
|
|
435
|
+
Whether the host sees the others come and go is the router's business:
|
|
436
|
+
|
|
437
|
+
| Where | The host sees the players' nodes | So |
|
|
438
|
+
|---|---|---|
|
|
439
|
+
| 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) |
|
|
440
|
+
| 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) |
|
|
441
|
+
| 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) |
|
|
442
|
+
|
|
443
|
+
A role `games-host` made before that line (with only `Watch asterism/**`)
|
|
444
|
+
gets just the tokens on its own keys: the host then falls back to the time
|
|
445
|
+
without a call (away after `away_seconds`, measured 13.6 s with the default
|
|
446
|
+
10 s). Add the line to the role (Admin > Relay > Roles, Edit) and Apply;
|
|
447
|
+
nothing in the host changes with it.
|
|
448
|
+
|
|
449
|
+
The screens follow the host the same way: the tables' apps (fmruby-core
|
|
450
|
+
`flash/app/game/`) and `bin/othello-tty` take a host's tables off their
|
|
451
|
+
list when its node leaves and look for them again when a node joins
|
|
452
|
+
(Asterism's `each` reads the liveliness they already follow: no message is
|
|
453
|
+
sent); at a table, a host that leaves shows the connection lost, the
|
|
454
|
+
screen stops asking its view (one every 30 s, should the word not come)
|
|
455
|
+
and asks again as soon as the host is back.
|
|
456
|
+
|
|
457
|
+
## Status
|
|
458
|
+
|
|
459
|
+
The host tells its tables' counts on the network (games 0.9.0), as
|
|
460
|
+
`asterism/<node>/<app>/@status` (asterism's `docs/protocol.md` 8: an
|
|
461
|
+
application's status, a map it puts when it changes and gives to whoever
|
|
462
|
+
asks). One entry per table, with the counts of `GET /status` (no node IDs
|
|
463
|
+
or tokens), and for a mahjong table the result of its most recent game
|
|
464
|
+
(games 0.13.0, "Game records"):
|
|
465
|
+
|
|
466
|
+
```
|
|
467
|
+
asterism/games/games/@status
|
|
468
|
+
{"mahjong1" => {"st" => "play", "members" => 4, "humans" => 1, "cpus" => 3, "ais" => 1, "away" => 0,
|
|
469
|
+
"last" => {"id" => 12, "at" => "2026-10-18 21:05", "game" => "tonpuu",
|
|
470
|
+
"result" => [["kishima", 1, 38200], ["CPU2", 2, 27100], ["Claude", 3, 21900], ["CPU1", 4, 12800]]}},
|
|
471
|
+
"othello1" => {"st" => "wait", "members" => 0, "humans" => 0, "cpus" => 0, "away" => 0},
|
|
472
|
+
"ai_today" => {"calls" => 84, "usd" => 0.02, "limit_usd" => 3.0}}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`last` is the four in rank order as `[name, rank, score]`, by the names
|
|
476
|
+
the table shows its members anyway (a table app shows them to whoever
|
|
477
|
+
joins); it is there once a game has ended. The rooms' status file
|
|
478
|
+
(`GET /status` of the rooms' host) stays counts only.
|
|
479
|
+
|
|
480
|
+
`ais` counts the AI players' seats (they are counted in `cpus` too), and
|
|
481
|
+
`ai_today` is the AI players' totals of the day (README "AI players"),
|
|
482
|
+
only on a host that can seat them. When some kind has a daily limit of its
|
|
483
|
+
own (games 0.15.0) `ai_today` also has `kinds`, one entry for each kind
|
|
484
|
+
the host offers: its money of the day (`usd`), the limit it stops at
|
|
485
|
+
(`limit_usd`: its own, or the total's), whether that limit is its own
|
|
486
|
+
(`own`) and whether it has stopped for today (`over`):
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
"ai_today" => {"calls" => 84, "usd" => 0.02, "limit_usd" => 3.0,
|
|
490
|
+
"kinds" => {"claude" => {"usd" => 0.0198, "limit_usd" => 10.0, "own" => true, "over" => false},
|
|
491
|
+
"jev" => {"usd" => 0.0002, "limit_usd" => 2.0, "own" => true, "over" => false}}}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
- **When.** After a table changes (a join, a seat taken or left, a move)
|
|
495
|
+
and once a second (an away seat changes the counts with time alone); a
|
|
496
|
+
put goes out only when the counts differ from the last one. A host puts
|
|
497
|
+
it as soon as it is connected.
|
|
498
|
+
- **Who reads it.** `asterism watch` and `asterism tree` (the asterism
|
|
499
|
+
gem's command line), its MCP bridge (`asterism_status`), the console's
|
|
500
|
+
details of the host's node (console 0.3.4), or anyone with
|
|
501
|
+
`Asterism.status_of("games", "games")`.
|
|
502
|
+
- **Where.** On a home network and on the permanent tables. The role
|
|
503
|
+
`games-host` needs `asterism/${node}/*/@status` in "Read and write" (the
|
|
504
|
+
console's docs/rooms.md): a `**` does not match the chunk `@status`. A
|
|
505
|
+
room's host tells none (the rooms' rules let it write only its own
|
|
506
|
+
place, which `@status` is not part of, and the console only watches who
|
|
507
|
+
is in a room).
|
|
508
|
+
- **The asterism gem.** With an asterism that has `Asterism.status`
|
|
509
|
+
(0.8.0, the version this repository is pinned to) the host uses it
|
|
510
|
+
(`net.status`); with an asterism 0.7.1 (the gem's lowest) it does the
|
|
511
|
+
same by hand on the connection's session (`Host::TablesStatus`: a
|
|
512
|
+
queryable on the key and a put when the counts change).
|
|
513
|
+
- **Size.** Five tables take about 300 bytes (the largest put is in the
|
|
514
|
+
log's `largest message` with the others).
|
|
515
|
+
|
|
516
|
+
## Writing a game
|
|
517
|
+
|
|
518
|
+
A game is two classes on the framework: the table (`AsterismGames::Game`:
|
|
519
|
+
the rules, the views, the saved form) and the object the host exposes for
|
|
520
|
+
each table (`AsterismGames::Remote`: the methods players call). Everything
|
|
521
|
+
else is the framework's: the connection, the members and their tokens, who
|
|
522
|
+
may join in a room, the host's loop, the puts, saving and resuming, the
|
|
523
|
+
rooms, the permanent tables, the status and the settings. A complete small
|
|
524
|
+
game is `test/framework/tally.rb`, and a real one the othello table
|
|
525
|
+
(`lib/asterism_games/othello/`, about 600 lines with its rules and CPU);
|
|
526
|
+
the shape of one:
|
|
527
|
+
|
|
528
|
+
```ruby
|
|
529
|
+
require "asterism_games"
|
|
530
|
+
|
|
531
|
+
module Counter
|
|
532
|
+
class Remote < AsterismGames::Remote
|
|
533
|
+
exposes press: 2, view: 1 # name => number of arguments (join, mine, leave are there)
|
|
534
|
+
|
|
535
|
+
def press(token, n, from:) # from: the caller's node ID (caller: :from)
|
|
536
|
+
mark(token, from) # a call from the member's own node: it is there
|
|
537
|
+
@game.press(token, n.to_i, now)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def view(token, from:)
|
|
541
|
+
mark(token, from)
|
|
542
|
+
@game.member_view(token, now) || { "error" => "token" }
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
class Table < AsterismGames::Game
|
|
547
|
+
register "counter" # tables: [{name: counter1, game: counter}]
|
|
548
|
+
remote Remote
|
|
549
|
+
tables "counter1" # the tables when the settings name none
|
|
550
|
+
screens "/app/game/counter/counter.app.rb" # a room whose link opens this gets them
|
|
551
|
+
settings file: { "goal" => ["goal", 1] }, env: { "COUNTER_GOAL" => ["goal", 1] }
|
|
552
|
+
DEFAULTS = AsterismGames::Game::DEFAULTS.merge("goal" => 10).freeze
|
|
553
|
+
|
|
554
|
+
def initialize(...)
|
|
555
|
+
super
|
|
556
|
+
@n = 0
|
|
557
|
+
load_saved(0) # restore and resume_clock when a saved state is there
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def press(token, n, now)
|
|
561
|
+
return "token" unless node_of(token)
|
|
562
|
+
@n += n
|
|
563
|
+
changed # saved and put by the host
|
|
564
|
+
"ok"
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def tick(now) = super.tap { expire_members(now) } # time: called on the host's loop
|
|
568
|
+
def public_view(now) = { "v" => @ver, "t" => @name, "n" => @n } # put on .../<table>/state
|
|
569
|
+
def member_view(token, now) = node_of(token) && public_view(now)
|
|
570
|
+
def to_saved(now) = super.merge("n" => @n)
|
|
571
|
+
def restore(data, now) = super.then { @n = data["n"].to_i }
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
`bin/asterism-games` requires the framework and the games it serves
|
|
577
|
+
(`asterism_games/mahjong` from `app/` when it is there, then
|
|
578
|
+
`asterism_games/othello` and `asterism_games/board`), then the files given
|
|
579
|
+
with `--require FILE` (a game of your own, with the gem or this checkout);
|
|
580
|
+
the settings name a game per table. The mahjong of `app/` is written the
|
|
581
|
+
same way: it requires the framework by its name, as your game would.
|
|
582
|
+
|
|
583
|
+
What the framework gives a game:
|
|
584
|
+
|
|
585
|
+
| | |
|
|
586
|
+
|---|---|
|
|
587
|
+
| `Remote.exposes` | the exposed methods and their numbers of arguments (-1: any), checked by Asterism before a call; the host exposes them with `caller: :from`, so every method takes `from:` (Asterism fills it in from the call's key and refuses a call that brings it). `join(name)`, `mine`, `leave(token)` are the base's |
|
|
588
|
+
| `Game#join` / `mine` / `leave` | a member per node with a token (`SecureRandom`, 32 hex digits); the same node gets its token back. In a room the caller is checked ("Who joins"). A game adds its own through `member_reply`, `join_refusal`, `renamed`, `forget` (free what the member held) and `keep_member?` |
|
|
589
|
+
| `node_of(token)`, `touch`, `member_away?`, `node_left` / `node_joined`, `expire_members` | the token's member; who is there ("Who is there": the host calls `node_left` / `node_joined` from Asterism's `on_leave` / `on_join`, a member's call is `touch`, and `away_ms` without one is away too); members gone for long (six times `away_ms` from their last call or from when their node left) |
|
|
590
|
+
| `tick(now)` | the host calls it on its loop (every 10 ms) and the game moves on by the time given: deadlines, CPU turns. Never waits |
|
|
591
|
+
| `public_view` / `spectator_view` / `state_view` / a member's view | what everybody, a spectator, the state key and one member see. What only one member may see (a hand) goes in the answer of that member's call, never on the state key (anyone in the key space may subscribe to it) |
|
|
592
|
+
| `changed`, `dirty?`, `unsaved?` | a change to save and put; a change only one member's view shows (`@unsaved = true`) is saved, not put |
|
|
593
|
+
| `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
|
+
| `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) |
|
|
595
|
+
| `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
|
+
| `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
|
+
|
|
598
|
+
The host (`Host`) is written with Asterism's block API:
|
|
599
|
+
`Asterism.connect(...) do |net| ... end` exposes the tables, sets the
|
|
600
|
+
profile (`name: asterism-games` for a host of its own, `permanent tables`
|
|
601
|
+
for the permanent tables' host, no name in a room) and starts the Net's
|
|
602
|
+
receiving thread, which answers the calls. The games move on in the
|
|
603
|
+
host's own loop (`step`: `tick`, saving, the puts, every 10 ms), which
|
|
604
|
+
holds `Asterism::LOCK` while it runs: the receiving thread holds the same
|
|
605
|
+
lock for each of its ticks, so a game never sees a call and a step at
|
|
606
|
+
once, and a game needs no lock of its own. When the connection is lost
|
|
607
|
+
(the receiving thread ends; `net.lost_reason` says why) the host saves and
|
|
608
|
+
connects again after 3 s; while a client of a router only reconnects
|
|
609
|
+
(`reconnecting?`) the tables stay and the puts wait.
|
|
610
|
+
|
|
611
|
+
## The mahjong table
|
|
612
|
+
|
|
613
|
+
### Methods
|
|
614
|
+
|
|
615
|
+
| Method | Arguments | Returns |
|
|
616
|
+
|---|---|---|
|
|
617
|
+
| `join` | name | `{"token", "seat" (-1: none), "owner"}`, or `{"error" => "full"}` (16 members) or `{"error" => "spectators"}` ("Spectators"); a join again from the same node keeps the membership and takes the name given (a screen whose player changes its name; at the seat too, during a game as well); at most 32 bytes, no control characters, empty: the node ID |
|
|
618
|
+
| `mine` | none | the same for a node that joined, or nil (nothing is created) |
|
|
619
|
+
| `sit` | token, seat | `"ok"` or why not (`"taken"`, `"not waiting"`, ...); seat -1: the first free one. In the lobby only; a seated member moves |
|
|
620
|
+
| `stand` | token | `"ok"` or why not (lobby only) |
|
|
621
|
+
| `cpu` | token, seat, on | the owner: a CPU into an empty seat (on) or out of it (a CPU or an AI player); seat -1 with on fills every empty seat; on as a name the lobby offers (`"claude"`, `"jev"`) puts that AI player in ("AI players"), `"no ai"` when it is not offered; on one seat that holds a CPU or an AI player of another kind, on turns it into the one asked for (games 0.12.1) |
|
|
622
|
+
| `start` | token | the owner, once the four seats are taken and one is a member: the seats are drawn (who deals first) and the game starts |
|
|
623
|
+
| `rule` | token, item, value | the owner, in the lobby: a local rule ("Local rules"); `"ok"` or why not (`"not owner"`, `"not waiting"`, `"no rule"`, `"value"`, `"no change"`) |
|
|
624
|
+
| `view` | token, assist | the member's view (below), or `{"error" => "token"}`; assist: the help its screen shows (Assist, 0-3) |
|
|
625
|
+
| `act` | token, sv, type, tiles | `"ok"`, or why not: `"token"`, `"not seated"`, `"stale"` (sv is not the current one), `"not offered"`, `"not playing"` |
|
|
626
|
+
| `table` | none | the public view (no hand), as a spectator sees it |
|
|
627
|
+
| `leave` | token | `true` / `false`; in the lobby the seat empties, during a game it goes to a CPU |
|
|
628
|
+
| `describe`, `look`, `choose`, `points` | | for programs ("Playing from a program") |
|
|
629
|
+
| `records`, `record` | from / id, round, part | the games played to the end and their records ("Game records") |
|
|
630
|
+
| `stats` | days, game, scope, from (all optional) | the players' stats ("Stats") |
|
|
631
|
+
|
|
632
|
+
- People join a table and wait in its lobby (`"st" => "wait"`): a member
|
|
633
|
+
sits in a seat or watches. The first member who sat is the owner (when the
|
|
634
|
+
owner stands or leaves, the member seated longest takes over). The owner
|
|
635
|
+
fills empty seats with CPUs and starts. At the end of a game the owner's
|
|
636
|
+
next (or `over_seconds`) brings the table back to the lobby with the same
|
|
637
|
+
seats, for another game.
|
|
638
|
+
- The node that calls is the caller in the call's key (`from:`, filled
|
|
639
|
+
in by Asterism; a call that brings `from:` itself is refused). The
|
|
640
|
+
arguments are as before: a table app that knows nothing of `from:`
|
|
641
|
+
calls as it did. The same node gets its membership and
|
|
642
|
+
token back from `join` (and `mine`), so a player that restarts its
|
|
643
|
+
application finds its table again.
|
|
644
|
+
- The token is 32 hex digits from `SecureRandom`. Everything about a hand
|
|
645
|
+
needs it: without it, or with another, `view` and `act` refuse. A member
|
|
646
|
+
without a seat (a spectator) gets the public view. The public view never
|
|
647
|
+
has a hand (except the winners', shown at a win).
|
|
648
|
+
- `act` takes only what the seat's list (`"a"` in its view) has: the
|
|
649
|
+
host asks riichi_engine for the seat's actions and matches the request
|
|
650
|
+
against them. `sv` is the state version the player saw.
|
|
651
|
+
- After every change the host saves the table and puts the public view on
|
|
652
|
+
`asterism/<node>/<app>/<table>/state` (and every 5 s anyway). A player
|
|
653
|
+
that sees a newer `"v"` there asks its view again; a screen also asks every
|
|
654
|
+
3 s, which tells the host it is still there.
|
|
655
|
+
|
|
656
|
+
### Assist
|
|
657
|
+
|
|
658
|
+
The help the table screen shows its player, chosen by the player and sent
|
|
659
|
+
with each `view` (a screen's setting, not the table's: every seat may ask
|
|
660
|
+
for another level). Worked out once per state version, seat and level, only
|
|
661
|
+
from what that seat can see, and only for a seat that asks.
|
|
662
|
+
|
|
663
|
+
| Level | Help |
|
|
664
|
+
|---|---|
|
|
665
|
+
| 0 | none |
|
|
666
|
+
| 1 (default) | when the seat may declare riichi: `wt` for each discard that declares it |
|
|
667
|
+
| 2 | at the seat's discard: `wt` for each discard that leaves the hand tenpai, with or without riichi (an open hand, a hand that stays dama). Whether a wait has a yaku is not judged |
|
|
668
|
+
| 3 | as 2; when no discard leaves the hand tenpai, `uk` for every discard |
|
|
669
|
+
|
|
670
|
+
- Shanten: riichi_engine's own count (`Cpu::Analysis::Shanten.calculate(t34,
|
|
671
|
+
melds:)`, the fork's `fix/shanten-melds`; `Engine.shanten` calls it). It
|
|
672
|
+
takes the melds as sets already made, counts seven pairs and kokushi for
|
|
673
|
+
a closed hand only, and works out each suit once per suit pattern. The
|
|
674
|
+
host had its own count of the regular form until 0.1.1.fmrb3 (0.1.0's
|
|
675
|
+
took the hand as all there is, wrong once there are melds, and took about
|
|
676
|
+
50 ms a turn for the acceptance); the engine's gives the same help (on
|
|
677
|
+
6,623 states of CPU hanchan, the level 3 help was the same byte for byte)
|
|
678
|
+
and is about 10 % quicker. The tests compare it with the engine's count
|
|
679
|
+
for closed hands (3000 random hands) and, with melds, tenpai with the
|
|
680
|
+
wait judge.
|
|
681
|
+
- Time (CRuby, one seat's help at one state, one process): over 8 CPU
|
|
682
|
+
hanchan with seats at levels 1, 2 and 3, median 1.3 ms, 95 % 4.8 ms, at
|
|
683
|
+
most about 60 ms (the first ones, before the suit patterns are known, or
|
|
684
|
+
a garbage collection; `rake test` prints the `assist:` line).
|
|
685
|
+
|
|
686
|
+
### Local rules
|
|
687
|
+
|
|
688
|
+
The owner chooses the table's rules in the lobby, before the start; every
|
|
689
|
+
member sees them there (the view's `ru`), and the game is played by the
|
|
690
|
+
rules it started with (`rule` answers `"not waiting"` once it has started).
|
|
691
|
+
They are saved with the table and stay for the next game at the table. The
|
|
692
|
+
host's rules ("Rules": `Engine::RULE_DEFAULTS` and the table's `rule`
|
|
693
|
+
setting) are where they start, and choosing a host's value again drops the
|
|
694
|
+
choice. A new room (a slot used again) starts from the host's rules.
|
|
695
|
+
|
|
696
|
+
`ru` has one number per item for items 0-10, `rx` for items 11-14 and `ry`
|
|
697
|
+
for items 15-16, in this order (the table screen puts the three together;
|
|
698
|
+
the numbers of the items never change, and a screen shows them in an order
|
|
699
|
+
of its own); `rule(token, item, value)` takes one of the values listed (or
|
|
700
|
+
the host's own value for the item). Item 10 is the time ("Time"): its value is
|
|
701
|
+
a code, the seconds for a move plus 1000 times the seconds of the bank
|
|
702
|
+
(5+20 is 20005, a fixed 20 s is 20). `ru` gives its seconds for a move
|
|
703
|
+
only, `tb` its bank (the screen adds them up again), and in the lobby `tc` the codes to choose from, in order (the
|
|
704
|
+
host's own value among them):
|
|
705
|
+
|
|
706
|
+
| Item | | riichi_engine key | Values |
|
|
707
|
+
|---|---|---|---|
|
|
708
|
+
| 0 | the game | `game_type` | 0 hanchan, 1 tonpuu |
|
|
709
|
+
| 1 | open tanyao | `kuitan` | 0 / 1 |
|
|
710
|
+
| 2 | red fives | `aka_dora` | 0 none, 3 one each of 5m 5p 5s, 4 two of 5p |
|
|
711
|
+
| 3 | double ron | `double_ron` | 0 (the head bump) / 1 |
|
|
712
|
+
| 4 | three rons a draw | `triple_ron_draw` | 0 (all three win) / 1 (sanchahou) |
|
|
713
|
+
| 5 | several yakuman added up | `multiple_yakuman` | 0 / 1 |
|
|
714
|
+
| 6 | kazoe yakuman | `kazoe_yakuman` | 0 / 1 |
|
|
715
|
+
| 7 | kiriage mangan | `kiriage_mangan` | 0 / 1 |
|
|
716
|
+
| 8 | tobi | `tobi` | 0 / 1 |
|
|
717
|
+
| 9 | the extra round ("延長"): west for a hanchan, south for a tonpuu game | `nishi_iri`, `nan_iri` together | 0 / 1 (the value is the key of the game chosen; a table app that calls the item the west round sets both) |
|
|
718
|
+
| 10 | the time for a move and the bank ("持ち時間") | (the host's `turn_seconds`, `bank_seconds`) | `tc`: 5+20 (20005), 5+0 (5), a fixed 30 s (30), 60 s (60) and 600 s (600), and the host's own |
|
|
719
|
+
| 11 | the abortive draws besides nine kinds (four winds, four kans, four riichi) | `suufon_renda`, `suukaikan`, `suucha_riichi` together | 0 / 1 (1 only when all three are on; a host setting with some of them shows 0, and 0 chosen again goes back to it) |
|
|
720
|
+
| 12 | nagashi mangan | `nagashi_mangan` | 0 / 1 |
|
|
721
|
+
| 13 | double yakuman (suuankou tanki, kokushi 13-wait, junsei chuuren) | `double_yakuman` | 0 / 1 |
|
|
722
|
+
| 14 | tenpai-yame | `tenpai_yame` | 0 / 1 |
|
|
723
|
+
| 15 | the CPUs' level | (the host's `cpu_level`) | 0 easy, 1 normal, 2 hard: the CPUs of the table's next game play at it |
|
|
724
|
+
| 16 | the spectators' delay | (the host's `spectator_delay_minutes`) | seconds: 0 (none), 60, 180, 300, and the host's own; in the lobby `dc` lists them in order ("Spectators") |
|
|
725
|
+
|
|
726
|
+
Left out: `atozuke` (riichi_engine has the key but does not use it), the
|
|
727
|
+
starting points, the first place target and the uma (they change the final
|
|
728
|
+
points, not the play: the table's `rule` setting still sets them),
|
|
729
|
+
`renpuu_jantou_fu` (the fu of a pair of the double wind; it is not sure
|
|
730
|
+
what Mahjong Soul does), `pao` (which yakuman; a list, not a switch),
|
|
731
|
+
`minkan_dora_after_discard` (when the kan dora of an open kan shows) and
|
|
732
|
+
`agari_yame` (the screen's column has room for fifteen rows at 640x360).
|
|
733
|
+
|
|
734
|
+
### Spectators
|
|
735
|
+
|
|
736
|
+
A member without a seat watches: its view is the public one with the four
|
|
737
|
+
hands besides (`sh`, `sd`: each seat's hand and drawn tile, as a seat's
|
|
738
|
+
own `h` and `dt`), and no offers, no help and no sign of who may call. The
|
|
739
|
+
put on `.../state` and `table` have no hand (except the winners', shown at
|
|
740
|
+
a win), so the seated players, who read the put too, never see another
|
|
741
|
+
hand.
|
|
742
|
+
|
|
743
|
+
- **How many.** At most `spectators` members without a seat (12: a room
|
|
744
|
+
of an Asterism console lets 16 pages in, four of them at the seats; the
|
|
745
|
+
host itself connects to the router directly). In the lobby a member that
|
|
746
|
+
could still sit counts in addition, so people can come in and sit. A
|
|
747
|
+
member beyond that gets `{"error" => "spectators"}`; a member gets its
|
|
748
|
+
place back as before.
|
|
749
|
+
- **The delay.** With a delay (`spectator_delay_minutes`, or chosen by the
|
|
750
|
+
owner in the lobby: "Local rules", item 16), the spectators' view, the
|
|
751
|
+
put and `table` show the game as it was that long ago: the host keeps the
|
|
752
|
+
game's part of the public view (`st sv r sc ri dora kw fu hc ls ev res
|
|
753
|
+
fin fr`) and the four hands at every change for that long, and the
|
|
754
|
+
people's part (`nm hu sk aw ow mb ru rx ry`) and `v` are now's (so a
|
|
755
|
+
screen asks again when the delayed picture moves on). There is no
|
|
756
|
+
deadline (`dl`) in a delayed view. The seated players' own views are never
|
|
757
|
+
delayed. Counted in time, not in actions: a round's result then stays as
|
|
758
|
+
long for the spectators as for the players, and an action every 0.7 s (a
|
|
759
|
+
CPU) or every 20 s (a person) does not change how far behind the
|
|
760
|
+
spectators are. A host started again starts the delay over (the past is
|
|
761
|
+
not saved).
|
|
762
|
+
- **The default is no delay.** The spectators see the hands as they are:
|
|
763
|
+
good for watching a game, and for an audience in the same room. A seated
|
|
764
|
+
player who joins the same table from a second screen sees the others'
|
|
765
|
+
hands too; where that matters, choose a delay (a minute or more: the
|
|
766
|
+
hands it shows are then of a past turn).
|
|
767
|
+
|
|
768
|
+
### Calls on a discard
|
|
769
|
+
|
|
770
|
+
When several seats may call a discard (ron, pon, chi, open kan), every one
|
|
771
|
+
of them answers once: a CPU at once, a player by `act` (a call or pass), or a
|
|
772
|
+
pass when `call_seconds` (counted from the discard, the same deadline for
|
|
773
|
+
everybody) run out. An answer is final (the seat's list is empty after it).
|
|
774
|
+
Once all have answered, riichi_engine's priority decides: ron over pon and
|
|
775
|
+
open kan over chi; several rons by the rule (by default every ron wins,
|
|
776
|
+
two or three; `triple_ron_draw` makes three a draw, sanchahou), equal calls in turn order from the discarder. The players
|
|
777
|
+
see the choices exactly as the engine lists them.
|
|
778
|
+
|
|
779
|
+
Nobody learns who else may call, or who has answered:
|
|
780
|
+
|
|
781
|
+
- A seat sees its own choices only (`a`). Its `dl` has its own bit while it
|
|
782
|
+
still has to answer; every other seat's `dl` has the bits of the three
|
|
783
|
+
others whoever they are (the screen says "the others are answering"),
|
|
784
|
+
with the same time left. The public view (the put, `table`, the
|
|
785
|
+
spectators) has no `dl` while the calls are open.
|
|
786
|
+
- An answer changes only the answering seat's view: its `v` is the table's
|
|
787
|
+
plus one until the calls are decided (its buttons go), the table's own
|
|
788
|
+
version does not move and nothing is put (the table is saved). The
|
|
789
|
+
change that follows the calls moves the version by two, past what that
|
|
790
|
+
seat saw.
|
|
791
|
+
- When everybody passed, the last event (`ev`) is a pass by seat -1.
|
|
792
|
+
- What still shows: that the host waits (the phase `r[7]` is the calls,
|
|
793
|
+
for a human the time of the window), not for whom.
|
|
794
|
+
|
|
795
|
+
### Time
|
|
796
|
+
|
|
797
|
+
Mahjong Soul's "5+20" by default: a move (a discard, riichi, a kan, a win,
|
|
798
|
+
nine kinds) has `turn_seconds` (5), and each player has a bank of
|
|
799
|
+
`bank_seconds` (20). What a move takes beyond its 5 s comes off the bank;
|
|
800
|
+
when the 5 s and the bank are both used up, the move is played for (the
|
|
801
|
+
drawn tile is discarded). The bank is full again at the start of every
|
|
802
|
+
round. A bank of 0 is
|
|
803
|
+
a fixed time a move (`turn_seconds` and no more). The calls on a discard
|
|
804
|
+
keep `call_seconds` and do not touch the bank. An away player keeps its
|
|
805
|
+
short wait ("Players that drop out"; it takes nothing off the bank while
|
|
806
|
+
`away_wait_seconds` is shorter than `turn_seconds`). The owner chooses the
|
|
807
|
+
time in the lobby ("Local rules", item 10). The banks are saved with the
|
|
808
|
+
table; a table saved before the bank starts its game with full banks (and
|
|
809
|
+
a fixed time the owner had chosen stays fixed).
|
|
810
|
+
|
|
811
|
+
The player sees the time on its own move: `dl` is `[seats, ms left, the
|
|
812
|
+
bank's ms]` when the move has a bank, the bank being the last part of the
|
|
813
|
+
ms left (the move's own time is what comes before it).
|
|
814
|
+
|
|
815
|
+
### Result
|
|
816
|
+
|
|
817
|
+
(games 0.15.0) A table with a human at it:
|
|
818
|
+
|
|
819
|
+
- **The start of a round.** After the deal the host waits
|
|
820
|
+
`deal_pause_seconds` (2) while every screen shows the round's name in
|
|
821
|
+
big letters ("東1局 0本場"). Nobody's time runs (the dealer's first move
|
|
822
|
+
has its full time from the end of the pause), no CPU or AI player plays,
|
|
823
|
+
and `act` answers `"not offered"`. The view says it with `in`.
|
|
824
|
+
- **The result waits for every human's OK.** A round's result (a win or
|
|
825
|
+
a draw) moves on to the next round once every human seated and there
|
|
826
|
+
(not away, "Players that drop out") has pressed OK: the action next
|
|
827
|
+
(12), so a screen made before 0.15.0 gives its OK with its next as it
|
|
828
|
+
did. A seat that has given it has nothing more in `a` (a second next is
|
|
829
|
+
`"ok"` and changes nothing). An away human is not waited for; with
|
|
830
|
+
nobody there the time decides.
|
|
831
|
+
- **Or by itself after the count.** Whoever does not press, the result
|
|
832
|
+
moves on `result_auto_seconds` (10) after the end of its count. The
|
|
833
|
+
count is the time a screen takes to show the result one step a second,
|
|
834
|
+
the same rule on both sides: **per winner, one step for each line of
|
|
835
|
+
its yaku (`y`) and one for its points; with two or three winners their
|
|
836
|
+
steps one after the other (the sum); a round without a win (a draw, an
|
|
837
|
+
abortive draw, three rons) one step.** A step is 1 s. The host counts
|
|
838
|
+
from the moment of the result; a screen that shows all at once (a
|
|
839
|
+
player who skips the steps) does not change the host's time. The view
|
|
840
|
+
says whom the result waits for and how long with `ok`.
|
|
841
|
+
- The end of a game (`over`) is as before: the owner's next (or
|
|
842
|
+
`over_seconds`) takes the table back to the lobby.
|
|
843
|
+
|
|
844
|
+
A table without humans (`humans` 0, or every human replaced by a CPU)
|
|
845
|
+
has neither: its result stays `result_seconds` (8) and its rounds start
|
|
846
|
+
at once, as before 0.15.0. A table saved with a result before 0.15.0
|
|
847
|
+
moves on by its saved time.
|
|
848
|
+
|
|
849
|
+
### Players that drop out
|
|
850
|
+
|
|
851
|
+
A member whose node leaves the network is away at once ("Who is there"),
|
|
852
|
+
and so is one that has not called the table for `away_seconds` (its screen
|
|
853
|
+
asks its view every 3 s), the way to tell where the host does not see the
|
|
854
|
+
nodes come and go: the public view marks its seat (`"aw"`) and the member
|
|
855
|
+
list. While it is away its seat is played for after a short wait:
|
|
856
|
+
`away_wait_seconds` from the start of its turn or of the calls (the drawn
|
|
857
|
+
tile is discarded, a call is passed), instead of `turn_seconds` and
|
|
858
|
+
`call_seconds`, so the others are not held up a whole turn every time. The
|
|
859
|
+
other players keep their own times (in the calls, the away seat passes
|
|
860
|
+
early and the rest go on waiting). A wait that started before the member
|
|
861
|
+
went away ends as soon as it is seen away, if its short wait is over by
|
|
862
|
+
then. Once its node is back or it calls again it has the full times again;
|
|
863
|
+
its screen asks its view and goes on. A seat is never handed to a CPU because its player is
|
|
864
|
+
away; a member that leaves (`leave`) during a game is.
|
|
865
|
+
|
|
866
|
+
**The owner away** (games 0.16.2): when the owner has been away for
|
|
867
|
+
`owner_handover_seconds` (60), in the lobby or during a game, the role
|
|
868
|
+
goes to a member who is there: the seated one who sat first, else the
|
|
869
|
+
one who joined first. With nobody there it stays where it is, and passes
|
|
870
|
+
as soon as someone is. The old owner coming back does not take it back.
|
|
871
|
+
The log says `the owner <node> was away for 60 s: <node> is the owner
|
|
872
|
+
now`. In a room a page opened again in the same browser comes back as the
|
|
873
|
+
same seat (the console 0.3.9's door, "Who joins"), and so as the owner if
|
|
874
|
+
the role has not passed yet.
|
|
875
|
+
|
|
876
|
+
**A table left alone** (games 0.16.2): during a game (`play`, `result`),
|
|
877
|
+
when none of the seated humans counts any more, the game is abandoned. A
|
|
878
|
+
seated human counts while it is there and for `abandon_seconds` (180)
|
|
879
|
+
after it went away; a member that left the table (`leave`) counts no
|
|
880
|
+
more at once (its seat is a CPU's already). The table goes back to the
|
|
881
|
+
lobby as at the end of a game: the CPUs and AI players keep their seats,
|
|
882
|
+
the away humans are taken off theirs (they stay members, watching, and
|
|
883
|
+
may sit again), the owner's role passes as when an owner stands, and
|
|
884
|
+
nothing of the unfinished game is kept: no record, nothing in the stats.
|
|
885
|
+
The log says `game abandoned: no human there for 180 s (seats [0, 2]
|
|
886
|
+
emptied; not recorded)`. One human back within the time keeps the game
|
|
887
|
+
going for everyone (the other away seats are played for as before). A
|
|
888
|
+
table of CPUs only (`humans: 0`, the permanent spectator tables) is never
|
|
889
|
+
abandoned.
|
|
890
|
+
|
|
891
|
+
### Views
|
|
892
|
+
|
|
893
|
+
A Hash (MessagePack); tiles are one byte each in binary strings (bin).
|
|
894
|
+
|
|
895
|
+
| Key | |
|
|
896
|
+
|---|---|
|
|
897
|
+
| `v` / `sv` | the view's version (any change; in a seat's own view one more after its answer to open calls, "Calls on a discard") / the state version (actions) |
|
|
898
|
+
| `t`, `st` | the table's name; `wait` (the lobby), `play`, `result` (a round's result), `over` |
|
|
899
|
+
| `hv` | the game host's version (games 0.16.1), shown by the table apps on the list and the lobby |
|
|
900
|
+
| `nm`, `hu` | the four names (`""` for an empty seat); the human seats as bits |
|
|
901
|
+
| `sk` | the four seats: 0 empty, 1 a member, 2 a CPU (an AI player too) |
|
|
902
|
+
| `ai`, `ak` | when an AI player sits: each seat's kind (`"claude"`, `"jev"`, `""` for the others); in the lobby, the AI players the owner may seat ("AI players") |
|
|
903
|
+
| `aw` | the seats whose member is away, as bits |
|
|
904
|
+
| `ow` | the owner's seat (-1: nobody seated) |
|
|
905
|
+
| `mb` | the members in the order they joined: `[name, seat (-1 none), flags (1 away, 2 owner)]` |
|
|
906
|
+
| `r` | `[bakaze (0 east, 1 south, 2 west), kyoku, honba, riichi sticks, dealer seat, tiles left, seat to act, phase (0 draw, 1 discard, 2 calls, 3 end)]` |
|
|
907
|
+
| `sc`, `ri` | the scores; riichi as bits |
|
|
908
|
+
| `dora` | the dora indicators |
|
|
909
|
+
| `kw` | four rivers, one byte a tile: `0x40` the riichi tile, `0x80` called away |
|
|
910
|
+
| `fu` | four meld lists: per meld `[kind, from, n, n tiles]`; kind 1 chi, 2 pon, 3 closed kan, 4 open kan, 5 added kan; from 1 right, 2 across, 3 left, 0 none. The tiles are in the order a table shows them: the called tile is first when it came from the left, second from across, last from the right (an added kan: the last of the first three, the added tile fourth) |
|
|
911
|
+
| `hc` | the four hand sizes |
|
|
912
|
+
| `ls`, `ev` | the last discard `[seat, tile]`; the last action `[type, seat]` |
|
|
913
|
+
| `cl` | the last declaration, `[type, the state version it came with, seat, ...]` (games 0.13.0): riichi (2), tsumo (3), a kan (4, 5, 8), pon (6), chi (7), ron (9; every winner of a double ron), nine kinds (11), three rons (13; the three seats). A screen shows it for a moment at the seat that made it; a new state version is a new declaration (`ev` is gone by the next draw, a riichi's or a kan's at once). In the public view too (everybody hears a declaration), as old as the rest for the spectators |
|
|
914
|
+
| `dl` | `[seats as bits, ms]`: whom the host waits for and how long it still waits: the seat whose turn it is; in the calls on a discard, in a seat's own view only, its own bit while it has to answer, else the three others' ("Calls on a discard"); on a player's own move with a bank, a third number, the bank's ms in it ("Time") |
|
|
915
|
+
| `res` | at a round's end: `k` (`tsumo_agari`, `ron_agari`, `ryuukyoku`, and the abortive draws `kyuushu_kyuuhai`, `sanchahou`, `suufon_renda`, `suukaikan`, `suucha_riichi`), `rd` (the round: `[bakaze, kyoku, honba]`), `d` (the four score changes), `f` (who dealt in), `wn` (one entry per winner, in turn order from the discarder: `w` seat, `y` the yaku as riichi_engine names them, in Japanese, `yh` the han of each (-1: a yakuman, -2: a double yakuman), `pt` fu, han and points, `h` and `ag` the hand and the winning tile, `pa` pao: `[[the seat that pays, the yakuman], ...]`), `tp` (tenpai seats at a draw), `ng` (the seats with nagashi mangan at a draw; paid in place of the no-ten payments), `rs` (the seats of the three rons at a sanchahou) |
|
|
916
|
+
| `ru`, `rx`, `ry` | the local rules ("Local rules"), one number per item: items 0-10, 11-14 and 15-16 (item 10 the seconds for a move) |
|
|
917
|
+
| `tb`, `tc`, `dc` | item 10's bank, seconds; in the lobby, item 10's choices as codes and item 16's choices in seconds ("Local rules") |
|
|
918
|
+
| `in` | while the round's name shows after the deal ("Result"): the ms left (games 0.15.0) |
|
|
919
|
+
| `ok` | on a result of a table with humans ("Result"): `[the seats that gave their OK as bits, the seats it still waits for as bits (seated, there, no OK yet), the ms until it moves on by itself]`; a screen shows "OK を待っています (n/m)" with n the first's bits and m both together (games 0.15.0) |
|
|
920
|
+
| `fin` | at the end: `[[seat, rank, score, point x 10], ...]` |
|
|
921
|
+
| `fr` | at the end, why the game ended: `normal_end` (the last round played, the fourth round of the extra round too), `tobi`, `all_last_oya_agari` (agari-yame), `all_last_oya_tenpai` (tenpai-yame), `sudden_death` (somebody reached 30,000 in the extra round), `west` (after west 4, the host's, only without `sudden_death`), `rounds` (the host's guard against a loop); none for a game saved before it was added |
|
|
922
|
+
| `me`, `h`, `dt`, `a` | the member's view only: its seat (-1: watching), its hand (sorted, without the drawn tile), the drawn tile (-1 none), what it may do: `[[type, tiles], ...]` |
|
|
923
|
+
| `wt` | the member's view only, the help (Assist, levels 1-3): one binary string, per discard the tile, the number of waits n and n pairs `[wait, left]` (the waits as riichi_engine's wait judge gives them; left: 4 less what the seat can see of the tile: its own hand, the rivers, every meld, the dora indicators) |
|
|
924
|
+
| `sh`, `sd` | a spectator's own view only: the four hands (sorted, without the drawn tile) and the four drawn tiles (-1 none) ("Spectators") |
|
|
925
|
+
| `uk` | the member's view only, level 3 when no discard leaves the hand tenpai: one binary string, per discard the tile, the shanten after it, the number n of tiles that bring it down and n pairs `[tile, left]` (left as in `wt`; a tile the seat sees all four of is left out) |
|
|
926
|
+
|
|
927
|
+
Tile numbers: `0..8` 1m..9m, `9` 0m (red five), `10..18` 1p..9p, `19` 0p,
|
|
928
|
+
`20..28` 1s..9s, `29` 0s, `30..36` 1z..7z (east south west north white
|
|
929
|
+
green red), `37` back. The same order as the tile sheets of Family mruby's
|
|
930
|
+
table screen.
|
|
931
|
+
|
|
932
|
+
Action types: 1 discard (tiles: what may be discarded; send one), 2 riichi
|
|
933
|
+
(tiles: the discards that declare it; send one), 3 tsumo, 4 closed kan, 5
|
|
934
|
+
added kan, 6 pon, 7 chi (one entry per pair), 8 open kan, 9 ron, 10 pass,
|
|
935
|
+
11 nine kinds, 12 next (after a result, the seat's OK, "Result"; after the
|
|
936
|
+
end of a game, the owner's next goes back to the lobby). 13 is only an event (`ev`): three rons, a draw.
|
|
937
|
+
|
|
938
|
+
### Message size
|
|
939
|
+
|
|
940
|
+
A board and the browser build (zenoh-pico) take at most 4096 B in one
|
|
941
|
+
message, Zenoh's headers and the key included, and drop a larger one without
|
|
942
|
+
a word. The views stay far below: the largest seen in 20 CPU hanchan was
|
|
943
|
+
about 570 B (the reply with its call key); a view built by hand with full
|
|
944
|
+
rivers, four kans each, sixteen members with long names and three winners
|
|
945
|
+
with fourteen yaku each (and pao, and the reason the game ended) is about 2.1 KB. The riichi help adds at most
|
|
946
|
+
14 x (2 + 13 x 2) = 392 B (fourteen discards with thirteen waits each, more
|
|
947
|
+
than a hand can have; a nine-sided chuuren gives 80 B); a view built by hand
|
|
948
|
+
with it, full rivers, the other seats' kans and the long names is about
|
|
949
|
+
1.5 KB. The acceptance (`uk`, level 3) adds at most 14 x (3 + 34 x 2) =
|
|
950
|
+
994 B (every kind after every discard, more than a hand can have; random
|
|
951
|
+
hands give at most about 250 B, 20 CPU hanchan at most 762 B with a view of
|
|
952
|
+
1,043 B); the same view built by hand with it is about 2.1 KB. `wt` and `uk` never come together. A spectator's view of the
|
|
953
|
+
largest result adds the four hands (59 B): about 2.2 KB. The tests hold every view
|
|
954
|
+
under 3800 B.
|
|
955
|
+
|
|
956
|
+
### Time and saving
|
|
957
|
+
|
|
958
|
+
- The host draws for the player. A player that does not move within
|
|
959
|
+
`turn_seconds` and what is left of its bank ("Time") discards the drawn
|
|
960
|
+
tile (the last tile after a call); a call not made within `call_seconds`
|
|
961
|
+
is passed.
|
|
962
|
+
- One host process holds every table of its settings (two by default); each
|
|
963
|
+
is saved to its own file and goes on by itself.
|
|
964
|
+
- After every change the table is written to `state_dir/<table>.json`
|
|
965
|
+
(written to a temporary file and renamed), with the members, the seats and
|
|
966
|
+
the owner (a restarted host keeps the lobby too). The deadline is saved as the
|
|
967
|
+
time left; a host stopped with SIGTERM saves the time left at that moment,
|
|
968
|
+
a host killed gives the player the time left at the last change.
|
|
969
|
+
|
|
970
|
+
## Rules
|
|
971
|
+
|
|
972
|
+
The host's defaults follow Mahjong Soul's default rules (its four-player
|
|
973
|
+
ranked games) wherever riichi_engine can choose; the engine's own defaults stay as they are and the host passes its
|
|
974
|
+
values (`Engine::RULE_DEFAULTS`). A table's `rule` setting goes over them,
|
|
975
|
+
and the owner's choices in the lobby go over that ("Local rules": the game,
|
|
976
|
+
open tanyao, red fives, double ron, three rons, several yakuman, kazoe
|
|
977
|
+
yakuman, kiriage mangan, tobi, the extra round, the time and the bank, the
|
|
978
|
+
abortive draws, nagashi mangan, double yakuman, tenpai-yame).
|
|
979
|
+
Where the engine cannot choose, the table says so: those rules are left for
|
|
980
|
+
later. "Not sure" marks what could not be confirmed.
|
|
981
|
+
|
|
982
|
+
| Rule | Mahjong Soul's default | riichi_engine | Choosable | The host's default |
|
|
983
|
+
|---|---|---|---|---|
|
|
984
|
+
| Open tanyao (kuitan) | yes | `kuitan` (true) | yes | true |
|
|
985
|
+
| Atozuke | yes | `atozuke` (true) | key only (not used by the engine) | (true) |
|
|
986
|
+
| Red fives | one each of 5m 5p 5s | `aka_dora` (1/1/1) | yes | 1/1/1 |
|
|
987
|
+
| Ippatsu, ura dora, kan dora, kan ura | yes | always | no | yes |
|
|
988
|
+
| When a kan dora shows | at once for a closed kan, after the discard for an open or added kan | `minkan_dora_after_discard` (false: at once for every kan) | key only | true |
|
|
989
|
+
| Double ron | yes | `double_ron` (true) | yes (fork `fix/multiple-ron`) | true |
|
|
990
|
+
| Three rons | all three win (no sanchahou) | `triple_ron_draw` (true) | yes (fork `fix/multiple-ron`) | **false: all three win** (as Mahjong Soul) |
|
|
991
|
+
| Honba and sticks at a double ron | to the first winner from the discarder | the host's scoring | (host) | to the first winner |
|
|
992
|
+
| Dealer at a double ron | keeps the seat if one of the winners | `GameFlow` (winners) | (engine) | as the engine |
|
|
993
|
+
| Abortive draws | nine kinds, four winds, four kans, four riichi; the dealer keeps the seat | nine kinds always; `suufon_renda`, `suukaikan`, `suucha_riichi` (false) | yes (one item for the three) | all |
|
|
994
|
+
| Double yakuman hands (suuankou tanki, kokushi 13-wait, junsei chuuren, daisuushii) | count double | `double_yakuman` (false) | yes | true (daisuushii is a double yakuman in riichi_engine without the key) |
|
|
995
|
+
| Several yakuman in one hand | added up | `multiple_yakuman` (false) | yes (fork `feat/multiple-yakuman`) | true |
|
|
996
|
+
| Kazoe yakuman (13 han) | yes | `kazoe_yakuman` (true) | yes | true |
|
|
997
|
+
| Kiriage mangan | no | `kiriage_mangan` (false) | yes | false |
|
|
998
|
+
| Nagashi mangan | yes; in place of the no-ten payments | `nagashi_mangan` (false) | yes | true |
|
|
999
|
+
| Pao (liability) | daisangen, daisuushii (not suukantsu) | `pao` (a list of yakuman, []; `suukantsu` too) | key only | daisangen, daisuushii |
|
|
1000
|
+
| Tobi | yes; 0 points goes on, below 0 ends | `tobi` (true, below 0) | yes | true |
|
|
1001
|
+
| The extra round: west for a hanchan, south for a tonpuu game | when nobody has 30,000 after the last round | `nishi_iri` (false), `nan_iri` (false), `nishi_iri_threshold` (30,000) | yes (one item for both) | true, true, 30,000 |
|
|
1002
|
+
| Sudden death in the extra round | it ends when somebody has 30,000 (a ron with several winners where the dealer keeps the deal goes on, before agari-yame too); one round at most | `sudden_death` (false: the round goes on until the deal passes with somebody at 30,000, west 4 back to west 1) | key only | true (without it the host ends the game after west 4) |
|
|
1003
|
+
| Agari-yame, tenpai-yame | both, for the last dealer in the lead with 30,000 or more | `agari_yame` (true), `tenpai_yame` (false); the dealer in the lead, above 0 (30,000 when there is an extra round) | tenpai-yame yes, agari-yame key only | both true |
|
|
1004
|
+
| Starting points, return, first place target | 25,000, 25,000 (no oka), 30,000 | `initial_score` (25,000), `oka_origin` (30,000), `nishi_iri_threshold` (30,000) | key only | 25,000, 25,000, 30,000 |
|
|
1005
|
+
| Uma | 5-15 (+15/+5/-5/-15), rank uma only | `uma` (20/10/-10/-20) | key only | 15/5/-5/-15 |
|
|
1006
|
+
| Riichi sticks left at the end | to the top | none (`calculate_ranked_results` takes the points as given) | (host) | to the top |
|
|
1007
|
+
| Equal points at the end | the seat nearer the first dealer ranks higher | by seat number | (engine) | as the engine (the first dealer is always seat 0) |
|
|
1008
|
+
| A pair of the double wind | 4 fu | `renpuu_jantou_fu` (2) | key only | 4 |
|
|
1009
|
+
| Kuikae (swap-calling) | banned (the called tile and, after a chi, the other end) | `kuikae` (true: allowed) | key only | false (banned; a chi that leaves nothing to discard is not offered) |
|
|
1010
|
+
| A closed kan after riichi | only of the drawn tile, when the waits stay the same | `riichi_ankan` (false) | key only | true |
|
|
1011
|
+
| Robbing a closed kan | for kokushi musou only | `kokushi_ankan_chankan` (false) | key only | true |
|
|
1012
|
+
| Honba at a pao ron | paid by the seat liable | `pao_honba` (`"discarder"`) | key only | `"liable"` |
|
|
1013
|
+
| The winning tile of a tenhou | the one that scores most | `tenhou_best_tile` (false: the tile drawn) | key only | true |
|
|
1014
|
+
| Kan ura dora | yes | always (`fix/kan-uradora`) | no | yes |
|
|
1015
|
+
| Thinking time | 5+20: 5 s a move plus a 20 s bank | none (the host's timers) | yes (item 10) | 5 s a move, a 20 s bank every round, 8 s a call (`turn_seconds`, `bank_seconds`, `call_seconds`; "Time") |
|
|
1016
|
+
| Riichi on a tile that is ronned | not made: the 1000 points come back, no stick (a pon or kan of the tile lets it stand) | `riichi_void_on_ron` (false: the stick stays, the winner takes it) | key only | true |
|
|
1017
|
+
| Same-turn furiten | after passing a winning tile (robbing a kan too), until one's next draw | on a pass of a ron offered, and on a winning tile with no ron offered (no yaku on it, or furiten already; `fix/dojun-furiten-no-yaku`) | no | as the engine |
|
|
1018
|
+
| Formal tenpai (karaten) | yes, except a single wait on a tile the hand holds four of | the wait judge leaves out only a tile the hand holds four of (a single wait on the fourth tile of a pon is tenpai; `test/karaten-tenpai`) | no | as the engine |
|
|
1019
|
+
|
|
1020
|
+
## Game records
|
|
1021
|
+
|
|
1022
|
+
Every game a mahjong table plays to its end is kept as a record (games
|
|
1023
|
+
0.13.0): the deals, the dora indicators, every draw and discard, the calls,
|
|
1024
|
+
riichi, the wins and draws and the points. The table apps list a table's
|
|
1025
|
+
recent games and step through a record with the four hands face up; a
|
|
1026
|
+
program reads the same with `records` and `record` (the CLI, an AI
|
|
1027
|
+
assistant through `asterism mcp`).
|
|
1028
|
+
|
|
1029
|
+
- **Where they are kept** (games 0.14.0). With the sqlite3 gem (the image,
|
|
1030
|
+
and a checkout with the Gemfile) every game goes into one SQLite
|
|
1031
|
+
database in the state volume, `state_dir/records.sqlite3` (`records_db`),
|
|
1032
|
+
written once at the end of each game. The rooms' and the permanent
|
|
1033
|
+
tables' hosts are processes of their own and all write into the
|
|
1034
|
+
supervisor's database (WAL; a process that finds it locked waits up to
|
|
1035
|
+
10 s). The newest 1000 games of each table are kept (`records_kept`);
|
|
1036
|
+
the rooms' tables of one name count together (the 1000 newest of every
|
|
1037
|
+
room's `mahjong1`), so the database stays bounded however many rooms
|
|
1038
|
+
open. Without the sqlite3 gem (the gem alone, `records_db: json`) the
|
|
1039
|
+
records stay as before 0.14.0: the 20 newest of each table in
|
|
1040
|
+
`<table>.records.json` next to the table's saved state.
|
|
1041
|
+
- **A room's games** stay in the database when the room closes (its saved
|
|
1042
|
+
tables go). They are given out (`records`, `record`, the recent games of
|
|
1043
|
+
the table apps) only by the room's own host while the room is open: no
|
|
1044
|
+
other room, no permanent table and no LAN table lists them. The stats
|
|
1045
|
+
count them ("Stats": names and numbers, never a record).
|
|
1046
|
+
- **The rooms of one invite link** (games 0.16.0, with a console 0.3.8):
|
|
1047
|
+
a room made through a console's invite link has the link's group
|
|
1048
|
+
(`rooms.json` `"group"`, a random string that is not the link's secret).
|
|
1049
|
+
The room's host keeps it with each game the room plays, and its
|
|
1050
|
+
`records` and `record` give out the room's own games and those of every
|
|
1051
|
+
other room of the same group, open or closed (the same table name:
|
|
1052
|
+
`mahjong1`), newest first. So the next room of the same link lists and
|
|
1053
|
+
replays the games of the last one. A room without a group (made by an
|
|
1054
|
+
admin, or by a console before 0.3.8) gives out its own games only, as
|
|
1055
|
+
before; another group's rooms, the permanent tables and the LAN tables
|
|
1056
|
+
give out none of them. The stats (`"table"` is the room's own games) and
|
|
1057
|
+
`@status`'s `last` stay the room's own. The games played before 0.16.0
|
|
1058
|
+
have no group and stay their own room's. A room's group is read when
|
|
1059
|
+
its host starts (a room open across the console's update gets it at
|
|
1060
|
+
the host's next start). The newest kept are counted as before (the
|
|
1061
|
+
rooms' tables of one name together, whatever their group). In the
|
|
1062
|
+
database: the column `grp` of `games` (NULL outside such rooms), added
|
|
1063
|
+
in place on the first start of 0.16.0; the format stays 1, so a host of
|
|
1064
|
+
0.14.0 or 0.15.0 put back reads and writes the same database.
|
|
1065
|
+
- **Taken in once.** A host of 0.14.0 that finds a table's
|
|
1066
|
+
`<table>.records.json` (0.13.0) next to a database takes its games in
|
|
1067
|
+
(the same games, in their order, with their times) and renames the file
|
|
1068
|
+
to `<table>.records.json.imported`; nothing reads it again (a host of
|
|
1069
|
+
0.13.0 put back would start with no records). A room's file is taken in
|
|
1070
|
+
by the room's host, with the room's ID.
|
|
1071
|
+
- **Ids.** A game's `id` is its row in the database: it grows across every
|
|
1072
|
+
table, and a table's ids are not consecutive. An id is never used twice.
|
|
1073
|
+
- **Backups.** After each game the host makes a consistent copy next to
|
|
1074
|
+
the database, `records-backup.sqlite3` (`VACUUM INTO` a `*.tmp` name,
|
|
1075
|
+
then renamed over the last copy), so a backup that copies the files of
|
|
1076
|
+
the state volume (the console's daily backup with
|
|
1077
|
+
`docker-compose.games.yml`) has a whole database whatever the host was
|
|
1078
|
+
writing at that moment. A host that finds the copy but no database
|
|
1079
|
+
starts from the copy: that is how a restore puts it back
|
|
1080
|
+
(docs/install.md, "Backups and restoring").
|
|
1081
|
+
- A game in progress is never given out; its record so far is in the
|
|
1082
|
+
table's saved state (JSON, as before), so a host started again goes on
|
|
1083
|
+
with it.
|
|
1084
|
+
- **Who reads them.** Anyone who can call the table (no token): the router's
|
|
1085
|
+
rules decide, as for joining.
|
|
1086
|
+
|
|
1087
|
+
| Method | Arguments | Returns |
|
|
1088
|
+
|---|---|---|
|
|
1089
|
+
| `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) |
|
|
1090
|
+
| `record` | id | the game's head: `id`, `at`, `time` (Unix seconds), `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]], ...]`) |
|
|
1091
|
+
| `record` | id, round | round `round` (from 0) as below, with `id`, `round`, `of` (how many rounds), `part`, `parts` |
|
|
1092
|
+
| `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 |
|
|
1093
|
+
|
|
1094
|
+
`{"error" => "no record"}` (an id not kept), `"no round"`, `"no part"`.
|
|
1095
|
+
|
|
1096
|
+
A round:
|
|
1097
|
+
|
|
1098
|
+
| Key | |
|
|
1099
|
+
|---|---|
|
|
1100
|
+
| `wind`, `number`, `honba`, `riichi_sticks`, `dealer` | the round (`east` .. `north`, 1-4) as it starts |
|
|
1101
|
+
| `scores` | the four scores as it starts |
|
|
1102
|
+
| `dora_indicators` | the indicator(s) as it starts (a kan's come as moves) |
|
|
1103
|
+
| `hands` | the four deals, sorted |
|
|
1104
|
+
| `moves` | in order, each an array starting with the seat (-1: the table's own) |
|
|
1105
|
+
| `result` | `kind` (as the view's `res`), `changes` (the four), `from`, `wins` (`seat`, `yaku`, `yaku_han`, `han`, `fu`, `points`, `hand` (without the winning tile), `tile`, `pao`), `ura_indicators`, `tenpai`, `nagashi`, `ron_seats` |
|
|
1106
|
+
| `end_scores`, `end_sticks` | the scores and the sticks on the table after it |
|
|
1107
|
+
|
|
1108
|
+
The moves:
|
|
1109
|
+
|
|
1110
|
+
| Move | |
|
|
1111
|
+
|---|---|
|
|
1112
|
+
| `[seat, "draw", tile]` | a draw (a kan's replacement tile too) |
|
|
1113
|
+
| `[seat, "discard", tile]`, `[seat, "discard", tile, true]` | a discard; `true`: the tile just drawn (tsumogiri) |
|
|
1114
|
+
| `[seat, "riichi", tile]` (`, true`) | riichi, declared with that discard |
|
|
1115
|
+
| `[seat, "chi" / "pon" / "open_kan", tile, [tiles], from]` | a call of the discard `tile` of seat `from` with `tiles` from the hand |
|
|
1116
|
+
| `[seat, "closed_kan", [four tiles]]`, `[seat, "added_kan", tile]` | a closed kan; a tile added to a pon |
|
|
1117
|
+
| `[-1, "dora", tile]` | a new dora indicator (after a kan, when the rule shows it) |
|
|
1118
|
+
| `[seat, "tsumo", tile]`, `[seat, "ron", tile, from]` | a win (one `ron` per winner of a double ron; three at a sanchahou) |
|
|
1119
|
+
| `[seat, "nine_kinds"]` | the abortive draw with nine kinds |
|
|
1120
|
+
|
|
1121
|
+
Tiles are `look`'s names: `"1m".."9m"`, `"1p".."9p"`, `"1s".."9s"`,
|
|
1122
|
+
`"0m"` / `"0p"` / `"0s"` the red fives, `"1z".."7z"` east, south, west,
|
|
1123
|
+
north, white, green, red. A whole hanchan takes about 25 KB of JSON (16 KB
|
|
1124
|
+
of MessagePack); a round's answer about 2 KB, the largest seen 2.2 KB
|
|
1125
|
+
(the tests hold every answer under 3800 B and split longer rounds). In
|
|
1126
|
+
the database a tonpuu game takes about 17 KB and a hanchan about 23 KB:
|
|
1127
|
+
1000 games of a table 17-23 MB, and the copy for the backup as much again.
|
|
1128
|
+
|
|
1129
|
+
From the command line and an AI assistant:
|
|
1130
|
+
|
|
1131
|
+
```
|
|
1132
|
+
asterism call --router tcp/192.0.2.2:7447 games/games/mahjong1 records
|
|
1133
|
+
asterism call --router tcp/192.0.2.2:7447 games/games/mahjong1 record 12
|
|
1134
|
+
asterism call --router tcp/192.0.2.2:7447 games/games/mahjong1 record 12 0
|
|
1135
|
+
|
|
1136
|
+
claude mcp add records -- asterism mcp --router tcp/192.0.2.2:7447 \
|
|
1137
|
+
--allow 'games/mahjong1.records' --allow 'games/mahjong1.record'
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
Then ask the assistant, for example: "Read the last game of mahjong1
|
|
1141
|
+
(records, then record with its id and each round) and go through the
|
|
1142
|
+
second player's discards: where did it deal in, and what would have been
|
|
1143
|
+
safer?" Every move of a finished game is there, the four hands included,
|
|
1144
|
+
so an assistant can review a game as a whole.
|
|
1145
|
+
|
|
1146
|
+
## Stats
|
|
1147
|
+
|
|
1148
|
+
`stats` counts the players' games from the records (games 0.14.0, the
|
|
1149
|
+
SQLite form; the JSON form counts the table's own 20). The table apps show
|
|
1150
|
+
it from their recent games ("Stats", S).
|
|
1151
|
+
|
|
1152
|
+
| Argument | | |
|
|
1153
|
+
|---|---|---|
|
|
1154
|
+
| days | 0 | 0: every game kept; N: the games of the last N days |
|
|
1155
|
+
| game | `""` | `"hanchan"`, `"tonpuu"`, `""` both |
|
|
1156
|
+
| scope | `"all"` | `"all"`: every game in the database (every table of every place, the rooms' too, closed ones included); `"table"`: this table's games (in a room, the room's) |
|
|
1157
|
+
| from | 0 | the page: the `next` of the answer before |
|
|
1158
|
+
|
|
1159
|
+
`{"rows" => [...], "games", "total", "next", "days", "game", "scope",
|
|
1160
|
+
"store"}`: `games` the games counted, `total` the rows, `next` when there
|
|
1161
|
+
are more than fit one message (about 13 rows a page), `store` `sqlite` or
|
|
1162
|
+
`json`. A row per player:
|
|
1163
|
+
|
|
1164
|
+
- **Who.** A person is a name (`"kind" => "human"`): the name entered at the
|
|
1165
|
+
table, so the same name is the same person whichever board or page it
|
|
1166
|
+
came from, and a new name a new person (anyone may take a name; nothing
|
|
1167
|
+
checks it). The CPUs are one row (`"cpu"`, `"CPU"`), each AI player one
|
|
1168
|
+
row (`"claude"`, `"jev"`). Persons first, by games, then the kinds.
|
|
1169
|
+
- **The numbers.** `games`, `ranks` (`[1st, 2nd, 3rd, 4th]`), `avg_rank`,
|
|
1170
|
+
`top_rate` (1st / games), `rounds` (the rounds of the games played),
|
|
1171
|
+
`wins`, `dealins` (a ron from this seat: once a round, a double ron too),
|
|
1172
|
+
`riichi`, `win_rate` / `dealin_rate` / `riichi_rate` (of the rounds),
|
|
1173
|
+
`avg_score` (the final score), `points` (the sum of the rank points,
|
|
1174
|
+
`final`'s points / 10, with the uma).
|
|
1175
|
+
- **What a room sees.** Names and numbers only, never a record: a room's
|
|
1176
|
+
table gives `"all"` like any other (the people of the whole host), and
|
|
1177
|
+
`"table"` for the room's own games. Outside the room, a room's games are
|
|
1178
|
+
in the counts of `"all"` and nowhere else.
|
|
1179
|
+
|
|
1180
|
+
```
|
|
1181
|
+
asterism call --router tcp/192.0.2.2:7447 games/games/mahjong1 stats
|
|
1182
|
+
asterism call --router tcp/192.0.2.2:7447 games/games/mahjong1 stats 30 tonpuu
|
|
1183
|
+
asterism mcp --router tcp/192.0.2.2:7447 --allow 'games/mahjong1.stats'
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
The counts are made once per game at its end (the seats' rows of the
|
|
1187
|
+
database), so a stats call reads no record: 1000 games take 20-50 ms.
|
|
1188
|
+
|
|
1189
|
+
## Playing in a terminal
|
|
1190
|
+
|
|
1191
|
+
(games 0.17.0) `asterism-games play <game>` sits at a table from a
|
|
1192
|
+
terminal: CRuby and the asterism gem only (no curses: the screen is drawn
|
|
1193
|
+
again as text whenever the table changes, cleared with ANSI escapes on a
|
|
1194
|
+
terminal, one screen after another in a pipe or a file).
|
|
1195
|
+
|
|
1196
|
+
```
|
|
1197
|
+
asterism-games play mahjong --router tcp/127.0.0.1:7447 --name Alice # a home network
|
|
1198
|
+
asterism-games play mahjong --credentials alice.tar # a console's permanent tables
|
|
1199
|
+
asterism-games play mahjong --tiles emoji # the tiles as emoji (🀇🀈🀉)
|
|
1200
|
+
asterism-games play mahjong --tiles mpsz # or as 1m 4p 3s
|
|
1201
|
+
asterism-games play othello --commands "sit,cpu,start" # "The othello table"
|
|
1202
|
+
asterism-games play --help
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
| Option | |
|
|
1206
|
+
|---|---|
|
|
1207
|
+
| `--router LOCATOR` | the Zenoh router |
|
|
1208
|
+
| `--credentials FILE` | a console's `.tar` (a router over mutual TLS; the router and the node ID come from it) |
|
|
1209
|
+
| `--namespace PREFIX` | inside a room's key space (`room/01`) |
|
|
1210
|
+
| `--table PATH` | the table (`<host>/games/<table>`); without it the first one found (mahjong: several are listed to choose from on a terminal) |
|
|
1211
|
+
| `--name NAME` | the name at the table (default: the node ID) |
|
|
1212
|
+
| `--node NODE` | the node ID (default `tty-<host>-<pid>`, or the `.tar`'s name) |
|
|
1213
|
+
| `--commands LIST` | commands run first, comma separated (`"sit,cpu,tonpuu,start"`) |
|
|
1214
|
+
| `--until-over` | quit when a game it saw played is over |
|
|
1215
|
+
| `--tiles kanji\|emoji\|mpsz` | mahjong: the tiles in kanji (default), as emoji or as text |
|
|
1216
|
+
|
|
1217
|
+
With neither `--router` nor `--credentials` it takes `ASTERISM_ROUTER` and
|
|
1218
|
+
`ASTERISM_CREDENTIALS` from the environment, as the asterism command line
|
|
1219
|
+
does (all or nothing: a router on the line never meets a `.tar` from the
|
|
1220
|
+
environment); with nothing at all, a zenohd on this machine
|
|
1221
|
+
(`tcp/127.0.0.1:7447`). When the table's host leaves the network
|
|
1222
|
+
(Asterism's `on_leave`) the screen says the connection is lost and the
|
|
1223
|
+
client waits; when the host is back (`on_join`) it asks again (and joins
|
|
1224
|
+
again when the host forgot it).
|
|
1225
|
+
|
|
1226
|
+
### Keys
|
|
1227
|
+
|
|
1228
|
+
On a terminal (the input is a tty) both clients read one key at a time,
|
|
1229
|
+
the keys of the table apps (fmruby-core's `flash/app/game/mahjong/` and
|
|
1230
|
+
`flash/app/game/othello/`); the last line of the screen says which keys
|
|
1231
|
+
work now, each in `[ ]` before what it does (`[R]リーチ [T]ツモ`). In a
|
|
1232
|
+
pipe or a file (and the tests) they read lines as before, and
|
|
1233
|
+
`--commands` runs first either way.
|
|
1234
|
+
|
|
1235
|
+
| Mahjong | |
|
|
1236
|
+
|---|---|
|
|
1237
|
+
| Left, Right | choose a tile of the hand (`^^` under it; the drawn tile is chosen whenever the hand changes) |
|
|
1238
|
+
| Enter, Space | discard the chosen tile; the result's OK; at the end of the game, the owner: back to the lobby |
|
|
1239
|
+
| `R` | riichi: then Left / Right and Enter discard with it (a tile that does not declare it: the first that does), Esc gives it up |
|
|
1240
|
+
| `T` | tsumo or ron |
|
|
1241
|
+
| `P` | pon |
|
|
1242
|
+
| `C` `V` `B` | the first, second and third chi offered (the last line names each one's tiles: `[C]チー 3m4m [V]チー 4m6m`) |
|
|
1243
|
+
| `K` `L` `J` | the first, second and third kan offered |
|
|
1244
|
+
| `N` | nine kinds (an abortive draw) |
|
|
1245
|
+
| Esc | pass (do not call) |
|
|
1246
|
+
| `1`..`4` | the lobby: sit there (an empty seat or a CPU's) |
|
|
1247
|
+
| `S` | the lobby: stand |
|
|
1248
|
+
| `C` | the lobby, the owner: CPUs into the empty seats |
|
|
1249
|
+
| Enter | the lobby, the owner: start (four seats taken) |
|
|
1250
|
+
|
|
1251
|
+
| Othello | |
|
|
1252
|
+
|---|---|
|
|
1253
|
+
| the arrows | move the cursor on the board (`[ ]` around the square; it stops at the edges; it starts on a legal square) |
|
|
1254
|
+
| Enter, Space | play there (a square that is not legal: a short note, nothing sent) |
|
|
1255
|
+
| Esc | during this member's own game: resign, after `resign? [Y] yes [N] no` on the last line (`Y` resigns, `N` or Esc cancels; as the othello app). `:resign` also works |
|
|
1256
|
+
| `S` | the lobby (or after a game): sit, or stand |
|
|
1257
|
+
| `C` | the owner: the CPU into the empty seat |
|
|
1258
|
+
| Enter | the lobby (or after a game), the owner: start |
|
|
1259
|
+
|
|
1260
|
+
| Both | |
|
|
1261
|
+
|---|---|
|
|
1262
|
+
| `Q`, Ctrl+Q, Ctrl+C | quit |
|
|
1263
|
+
| `:` or `/` | a line to type a command in, as in a pipe (`tonpuu`, `rule 3 1`, `chi 2`, `6s`, `resign`, `auto` ...): the terminal is back in its line mode for it, the screen shows the letters and the words again, Enter sends it (an empty line sends nothing) |
|
|
1264
|
+
|
|
1265
|
+
The terminal is put in raw mode while the client reads keys (`io/console`;
|
|
1266
|
+
ESC alone is told from an arrow by waiting 50 ms for the rest of the
|
|
1267
|
+
sequence) and the cursor is hidden; however the client ends (`Q`, Ctrl+C,
|
|
1268
|
+
an exception, a signal) the terminal is put back as it was and the cursor
|
|
1269
|
+
shown again. In raw mode the terminal does not turn `\n` into a new line,
|
|
1270
|
+
so the client writes `\r\n`.
|
|
1271
|
+
|
|
1272
|
+
### Mahjong
|
|
1273
|
+
|
|
1274
|
+
The lobby (sit, stand, CPUs, the game's length, start), the rounds, the
|
|
1275
|
+
results and the end of the game, in the table app's words (東1局, 本場,
|
|
1276
|
+
供託, ドラ, リーチ, ポン ...). A member without a seat watches, with the
|
|
1277
|
+
four hands ("Spectators"). The screen is drawn again when the table
|
|
1278
|
+
changes; on a terminal the time left counts down in place once a second
|
|
1279
|
+
(below), and a line being typed is never wiped by it.
|
|
1280
|
+
|
|
1281
|
+
```
|
|
1282
|
+
東2局 0本場 供託1 残り32 ドラ 2筒
|
|
1283
|
+
南 CPU2 25000* 南 白 中 9索 3筒 9筒 8筒 [六萬] 西
|
|
1284
|
+
西 CPU3 25000 白 1索 九萬 2索 北 8筒 3筒 3索 5索 3索
|
|
1285
|
+
鳴き 發 發 發
|
|
1286
|
+
北 CPU1 25000 白 1索 中 九萬 東 東 三萬 二萬 南
|
|
1287
|
+
東 自分 24000 3索 白 一萬 九萬 2筒 東 8索 發 四萬
|
|
1288
|
+
南 CPU2: リーチ
|
|
1289
|
+
|
|
1290
|
+
五 1 1 4 9 9 1 2 2 2 7 8 西 3
|
|
1291
|
+
萬 筒 筒 筒 筒 筒 索 索 索 索 索 索 索
|
|
1292
|
+
a b c d e f g h i j k l m n
|
|
1293
|
+
自分の番: 打つ牌 (文字か 6s) あと 5+20秒
|
|
1294
|
+
>
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
- The first line: the round, the honba, the riichi sticks on the table
|
|
1298
|
+
(供託), the tiles left and the dora indicators. Then the four seats,
|
|
1299
|
+
the next one first and this one (自分) last: the seat wind, the name, the
|
|
1300
|
+
points (`*`: riichi), the river (the riichi tile in `[ ]`; a tile called
|
|
1301
|
+
away is in the meld, not the river; a river longer than the line goes
|
|
1302
|
+
on below, so a line stays within 80 columns) and under it the melds
|
|
1303
|
+
(鳴き; a closed kan with its ends face down, 裏). Then the hand, the
|
|
1304
|
+
drawn tile apart at the end, and a letter under each tile; `赤:` names
|
|
1305
|
+
the letters of the red fives (on a terminal they are drawn in red too),
|
|
1306
|
+
`待ち:` the waits after each discard that declares riichi. The last line
|
|
1307
|
+
says what can be done now, with the time left (the move's and the
|
|
1308
|
+
bank's, "Time").
|
|
1309
|
+
- **The tiles** (`--tiles`), three ways:
|
|
1310
|
+
- `kanji` (the default): as the table app draws a tile. In the hand (and
|
|
1311
|
+
a spectator's hands, a winning hand) two rows, the number on top and
|
|
1312
|
+
the suit below: 一..九 over 萬, full-width 1..9 over 筒 and 索 (not
|
|
1313
|
+
①②③, which many fonts draw small), an honour as its one character
|
|
1314
|
+
(東 南 西 北 白 發 中). In the rivers, the melds and the dora one line
|
|
1315
|
+
per tile: 一萬 4筒 3索 東. Every glyph is a CJK one, two columns
|
|
1316
|
+
wide in any terminal, so this is the readable choice.
|
|
1317
|
+
- `emoji`: Unicode's mahjong tiles (🀇🀈🀉 ...). How they look is up to
|
|
1318
|
+
the terminal's fonts: 🀄 (the red dragon) is often drawn as a colour
|
|
1319
|
+
emoji, hard to read next to the others, and some fonts draw the
|
|
1320
|
+
tiles small or wider than their column.
|
|
1321
|
+
- `mpsz`: the suits as `1m`..`9m`, `1p`.., `1s`.. (`0m` `0p` `0s` the
|
|
1322
|
+
red fives), the honours as 東 南 西 北 白 發 中.
|
|
1323
|
+
|
|
1324
|
+
```
|
|
1325
|
+
北 CPU3 6800* 🀀 🀁 🀇 🀂 🀑 🀆 🀃 🀗 [🀔 ] 🀘 🀝 🀄🀄 (--tiles emoji)
|
|
1326
|
+
🀇 🀊 🀌 🀎 🀞 🀠 🀑 🀓 🀖 🀗 🀘 🀃 🀅 🀛
|
|
1327
|
+
a b c d e f g h i j k l m n
|
|
1328
|
+
|
|
1329
|
+
東 CPU1 24000* 南 6s 6s 中 中 2m 8s 0p 9m [8p] 9s 2s 2s 1p 北 (--tiles mpsz)
|
|
1330
|
+
7m 8m 6p 7p 1s 1s 3s 8s 西 北 6s
|
|
1331
|
+
a b c d e f g h i j k
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
- **The columns.** The kanji and the full-width digits are two columns
|
|
1335
|
+
wide everywhere. The mahjong emoji are one column wide by Unicode's
|
|
1336
|
+
widths except 🀄, which is two (it is an emoji shown in colour by
|
|
1337
|
+
default), and fonts often draw them wider than the column they get.
|
|
1338
|
+
Every tile in the hand has a slot of two columns for an emoji, three
|
|
1339
|
+
for the kanji or `1m` (the tile and room after it), the letters the
|
|
1340
|
+
same slots. On a terminal the client puts each tile, each
|
|
1341
|
+
letter and the text after them at its column with the cursor (`ESC [ n
|
|
1342
|
+
G`), so a tile drawn wider never pushes the letters or the next tile
|
|
1343
|
+
along; in a pipe or a file it pads with spaces by the widths above (🀄
|
|
1344
|
+
fills its slot). The letters stay under their tiles either way; with
|
|
1345
|
+
the emoji a font that draws a tile wider than two columns overlaps the
|
|
1346
|
+
next one (the kanji never do).
|
|
1347
|
+
- **The time** counts down on a terminal: about once a second the client
|
|
1348
|
+
works the screen out again with the time gone since the view came,
|
|
1349
|
+
and rewrites only the lines that changed (the last line's `あと 5+20秒`,
|
|
1350
|
+
a result's `OK を待っています ... あと N 秒`) where they are: ESC 7 saves
|
|
1351
|
+
the cursor where the player types, the line is cleared and written,
|
|
1352
|
+
ESC 8 puts the cursor back. The line being typed is not touched. In a
|
|
1353
|
+
pipe or a file nothing is rewritten.
|
|
1354
|
+
- **`--commands`** run in order, each when the table can take it: the
|
|
1355
|
+
lobby commands (`sit`, `cpu`, `start`, `tonpuu` ...) wait for the lobby
|
|
1356
|
+
(a game may still be going on, or its end shown, when the client
|
|
1357
|
+
joins), the owner's for this member to be seated and the owner, `start`
|
|
1358
|
+
for the four seats; the screen says what each waits for (`--commands:
|
|
1359
|
+
cpu (卓の主を待っています)`). One the host answers "no" is not tried
|
|
1360
|
+
again.
|
|
1361
|
+
|
|
1362
|
+
Typed (in a pipe or a file each line; on a terminal after `:`):
|
|
1363
|
+
|
|
1364
|
+
| Typed | |
|
|
1365
|
+
|---|---|
|
|
1366
|
+
| `a` .. `n` | discard that tile of the hand (the drawn tile is the last letter) |
|
|
1367
|
+
| `6s`, `0p`, `東` (or `1z`..`7z`), `4筒`, `一萬` | discard that tile (`0`: the red five; `5p` takes the red five when the hand has no other five) |
|
|
1368
|
+
| `riichi` | riichi: alone with one choice, else the client lists the discards that declare it and the next line chooses (a letter or a tile); `riichi e` / `riichi 6s` at once |
|
|
1369
|
+
| `tsumo`, `ron` | win |
|
|
1370
|
+
| `pon`, `chi`, `kan` | call; with several ways the client lists them numbered and the next line chooses (`2`, or the tiles: `3m4m`, `34m`, letters); `chi 2`, `chi 3m4m` at once |
|
|
1371
|
+
| `pass` | do not call |
|
|
1372
|
+
| `kyuushu` | nine kinds (an abortive draw) |
|
|
1373
|
+
| `ok` (or an empty line) | the result's OK ("Result": the screen says `OK を待っています (n/m)`); at the end of the game, the owner: back to the lobby |
|
|
1374
|
+
| `sit [1-4]`, `stand` | take a seat (a CPU's too), leave it (the lobby) |
|
|
1375
|
+
| `cpu [1-4]`, `nocpu 1-4`, `ai 1-4 KIND` | the owner: CPUs into the empty seats (or one), a CPU out, an AI player in when the lobby offers one ("AI players") |
|
|
1376
|
+
| `tonpuu`, `hanchan`, `rule ITEM VALUE` | the owner: the game's length, any local rule ("Local rules") |
|
|
1377
|
+
| `start` | the owner: start |
|
|
1378
|
+
| `auto` | the client plays for you: wins when offered, passes calls, discards the drawn tile, OKs the results (again to stop) |
|
|
1379
|
+
| `tables`, `view`, `help`, `quit` | |
|
|
1380
|
+
|
|
1381
|
+
The host's answers are shown in the table app's words (`今はできない`,
|
|
1382
|
+
`卓の主だけができる` ...).
|
|
1383
|
+
|
|
1384
|
+
## Playing from a program
|
|
1385
|
+
|
|
1386
|
+
The mahjong table also speaks to programs (games 0.10.0): an AI assistant
|
|
1387
|
+
through `asterism mcp`, a script, the host's own AI players. The screens'
|
|
1388
|
+
view (short keys, tiles as bytes) stays as it is; next to it:
|
|
1389
|
+
|
|
1390
|
+
| Method | Arguments | Returns |
|
|
1391
|
+
|---|---|---|
|
|
1392
|
+
| `describe` | none | a text: how to play from a program, what `look` returns, the tiles' names (read it first) |
|
|
1393
|
+
| `look` | token | the table as the member's seat sees it, in words: `status`, `you`, `seats` (name, kind human / cpu / ai / empty, wind, score, riichi, `river`, `riichi_index`, `called_indexes`, `melds`, `hand_count`), `round` (wind, number, honba, riichi sticks, dealer, wall left, turn, phase), `dora_indicators`, `last_discard`, `hand`, `drawn`, `moves`, `help`, `deadline_ms`, `result`, `final`, `rules`; in the lobby `ai_players` |
|
|
1394
|
+
| `choose` | token, sv, n | plays the move numbered `n` of the `moves` of the look whose `sv` it gives: `"ok"`, `"stale"` (look again), `"no such move"`, or why not as `act` |
|
|
1395
|
+
| `points` | token | what the hand would score: `now` (the win offered now) and `waits` (for each wait of a tenpai hand, after each discard that leaves it tenpai on the seat's turn: `ron` and `tsumo` with han, fu, points, yaku, or `no_yaku`) |
|
|
1396
|
+
|
|
1397
|
+
- `moves` lists what the seat may do now, numbered from 0: `{"n", "type",
|
|
1398
|
+
"tile"/"tiles", "text"}` with the types `discard`, `riichi` (a discard
|
|
1399
|
+
that declares it), `tsumo`, `ron`, `pon`, `chi`, `open_kan`,
|
|
1400
|
+
`closed_kan`, `added_kan`, `pass`, `nine_kinds`, `next`. Empty when the
|
|
1401
|
+
seat has nothing to decide: look again a moment later.
|
|
1402
|
+
- `help` is the help of assist level 3 in words: after each discard that
|
|
1403
|
+
leaves the hand tenpai, its waits as `{tile => unseen}`; otherwise the
|
|
1404
|
+
shanten after each discard and the tiles that bring it down.
|
|
1405
|
+
- A seat sees what its screen shows and no more: its own hand, the rivers,
|
|
1406
|
+
the melds, the dora indicators, the scores. `points` leaves the ura dora
|
|
1407
|
+
out (nobody sees them before a win) and does not check furiten.
|
|
1408
|
+
- Every method of both tables has its arguments' names and a text in the
|
|
1409
|
+
meta (asterism's `docs/protocol.md` 5, with an asterism after 0.7.1):
|
|
1410
|
+
`asterism objects` prints them and `asterism mcp` makes them the tools'
|
|
1411
|
+
inputs and descriptions. With asterism 0.7.1 the meta has the numbers
|
|
1412
|
+
only, as before.
|
|
1413
|
+
|
|
1414
|
+
An AI assistant sits at a table like this:
|
|
1415
|
+
|
|
1416
|
+
```
|
|
1417
|
+
claude mcp add asterism -- asterism mcp --router tcp/192.0.2.2:7447 --allow 'games/mahjong1.*' --app game
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
(`--app game`: the host then follows the assistant as it follows a table
|
|
1421
|
+
app, "Who is there"; without it the assistant is away after
|
|
1422
|
+
`away_seconds` without a call, and an away seat gets two seconds for a
|
|
1423
|
+
move.) The table's time for a move (`turn_seconds`, 5 + 20 s of bank) is
|
|
1424
|
+
short for an assistant that thinks: a table for them can take a longer
|
|
1425
|
+
one (`MAHJONG_TURN_SECONDS`, or the owner's local rule item 10).
|
|
1426
|
+
|
|
1427
|
+
## AI players
|
|
1428
|
+
|
|
1429
|
+
A table's owner puts an AI player into an empty seat as it puts a CPU
|
|
1430
|
+
there (games 0.10.0): `cpu(token, seat, "claude")` or `"jev"`, the names
|
|
1431
|
+
the lobby view offers (`"ak"`; `look`'s `ai_players`). On one seat that
|
|
1432
|
+
holds a CPU or another AI player the same call turns it into the one
|
|
1433
|
+
asked for (games 0.12.1; `true` turns an AI player back into a CPU, and
|
|
1434
|
+
a refused one leaves the seat as it was). The table apps give each seat
|
|
1435
|
+
of the lobby a button that goes round empty, CPU, Claude, Jev (the kinds
|
|
1436
|
+
offered), and show the seat by its name (`Claude`, `Jev`, numbered when there are several) with
|
|
1437
|
+
`[AI]`; the view's `"ai"` gives each seat's kind (`""` for the others),
|
|
1438
|
+
`"sk"` keeps 2 for them (an old screen shows a CPU named Claude).
|
|
1439
|
+
|
|
1440
|
+
| Kind | What answers | Model (setting) |
|
|
1441
|
+
|---|---|---|
|
|
1442
|
+
| `claude` | Claude through the Messages API (the `anthropic` gem): the instructions (`describe`) as the cached system prompt, the seat's `look` as JSON, a structured answer `{"n": <move>}` | `claude-sonnet-5-5` (`ASTERISM_GAMES_CLAUDE_MODEL`), effort `low` (`ASTERISM_GAMES_CLAUDE_EFFORT`) |
|
|
1443
|
+
| `jev` | Jev through TypeSafe's own API (`POST https://api.typesafe.ai/v1/systemone`, games 0.11.1): a decision model asked one choice question, the moves as its answers (keys `"0"`, `"1"`, ... with their texts), the `look` as its state; the answer's `usage` is what is counted | `jev-latest` (`ASTERISM_GAMES_JEV_MODEL`; a versioned id such as `jev-1.13.0` keeps one model) |
|
|
1444
|
+
|
|
1445
|
+
- **How a decision goes.** For each decision of an AI seat the table asks
|
|
1446
|
+
the host (the seat's `look`, exactly what a human in that seat sees); the
|
|
1447
|
+
host asks the API on a thread of its own, never holding the game's lock,
|
|
1448
|
+
and hands the number back. A decision with one move is not asked.
|
|
1449
|
+
- **Time.** An AI seat has its own time: `ai_turn_seconds` (30 s) for a
|
|
1450
|
+
move, `ai_call_seconds` (15 s) for its answer to a discard it may call
|
|
1451
|
+
(`MAHJONG_AI_TURN_SECONDS`, `MAHJONG_AI_CALL_SECONDS`). The humans keep
|
|
1452
|
+
theirs. When the answer does not come in time, or fails (an error, a
|
|
1453
|
+
refusal, a move it was not offered), the CPU plays that move and the AI
|
|
1454
|
+
goes on with the next.
|
|
1455
|
+
- **Money.** What each decision costs is counted from the tokens the API
|
|
1456
|
+
reports, per day (the host's local date), in `state_dir/ai_usage.json`
|
|
1457
|
+
(shared by the rooms' hosts and the permanent tables; counts and money
|
|
1458
|
+
only). When today's total reaches `ASTERISM_GAMES_AI_DAILY_USD` (3.0) no
|
|
1459
|
+
table offers an AI player any more and the seated ones become CPUs; a
|
|
1460
|
+
game asks at most `ASTERISM_GAMES_AI_CALLS_PER_GAME` (400) decisions,
|
|
1461
|
+
then its AI seats become CPUs. `bin/asterism-games status` prints the
|
|
1462
|
+
day's decisions, tokens and money per kind; the host's `@status` has the
|
|
1463
|
+
totals (`ai_today`).
|
|
1464
|
+
- **A limit per kind** (games 0.15.0). `ASTERISM_GAMES_AI_DAILY_USD_CLAUDE`
|
|
1465
|
+
and `ASTERISM_GAMES_AI_DAILY_USD_JEV` (the kind's name in capitals) give
|
|
1466
|
+
a kind a daily limit of its own. A kind with one stops when its own money
|
|
1467
|
+
of the day reaches it: no table offers it any more and its seats become
|
|
1468
|
+
CPUs, while the other kinds go on. A kind without one follows
|
|
1469
|
+
`ASTERISM_GAMES_AI_DAILY_USD` as before: it stops when the day's total of
|
|
1470
|
+
every kind reaches it. Without any of them everything is as before
|
|
1471
|
+
0.15.0. A value that is not a number of dollars (0 or more) is ignored,
|
|
1472
|
+
with a line in the log. The log says the limits at the start
|
|
1473
|
+
(`games: AI players: claude, jev (at most $10.0 a day for claude, $2.0
|
|
1474
|
+
for jev, 400 decisions a game)`) and once for each kind that stops
|
|
1475
|
+
(`today's limit for claude ($10.0) is reached`); a new day lets it play
|
|
1476
|
+
again. `ai_today` on `@status` then has `kinds` (README "Status").
|
|
1477
|
+
- **Prices.** USD per million tokens, settings with the published prices
|
|
1478
|
+
as defaults: Claude Sonnet 5.5 `ASTERISM_GAMES_CLAUDE_USD_IN` 2.0, `_OUT`
|
|
1479
|
+
10.0, `_CACHE_WRITE` 2.5 (5-minute cache), `_CACHE_READ` 0.10 (checked
|
|
1480
|
+
2026-10-17); Jev `ASTERISM_GAMES_JEV_USD_IN` 0.042, `_OUT` 0 (TypeSafe
|
|
1481
|
+
charges input tokens only; checked 2026-10-18). Change them when the
|
|
1482
|
+
prices change.
|
|
1483
|
+
- **Keys.** `ANTHROPIC_API_KEY` for Claude, `TYPESAFE_API_KEY` for Jev
|
|
1484
|
+
(console.typesafe.ai, Keys), from the environment (on the VPS:
|
|
1485
|
+
`deploy/vps/.env`, mode 600, docs/install.md). Without a kind's key the
|
|
1486
|
+
kind is not offered at all (no button). The keys go into no log, no
|
|
1487
|
+
state file, no status and no error text (an API's error is logged as its
|
|
1488
|
+
HTTP status or class only).
|
|
1489
|
+
- `ASTERISM_GAMES_AI_THREADS` (4): decisions asked at once.
|
|
1490
|
+
`ASTERISM_GAMES_AI_FAKE=1` seats fakes under both names, for a LAN
|
|
1491
|
+
without keys and for tests (no API is called; `_FAKE_DELAY_MS`,
|
|
1492
|
+
`_FAKE_SLOW_EVERY`, `_FAKE_FAIL_EVERY` make every n-th decision late or
|
|
1493
|
+
failed).
|
|
1494
|
+
- **Writing another decider.** `decide(request, timeout:)` returning
|
|
1495
|
+
`AI::Decision.new(n:, tokens:, ms:)` (lib/asterism_games/ai.rb); the
|
|
1496
|
+
request is `{"game", "table", "seat", "instructions", "view"}`.
|
|
1497
|
+
- **Which game seats which kinds** (games 0.17.0): `AI::GAME_KINDS` in
|
|
1498
|
+
lib/asterism_games/ai.rb is the one place that says it. Mahjong seats
|
|
1499
|
+
`claude` and `jev`; othello seats `jev` only, whatever other keys the
|
|
1500
|
+
host has (its `"ak"` is `["jev"]` with Jev's key, else absent, and
|
|
1501
|
+
`cpu(token, seat, "claude")` is `"no ai"`). Othello's Jev uses the same
|
|
1502
|
+
key (`TYPESAFE_API_KEY`), the same decider, the same money of the day
|
|
1503
|
+
and the same `ASTERISM_GAMES_AI_CALLS_PER_GAME` as mahjong's; nothing is
|
|
1504
|
+
set for it alone. See "The othello table" for its view and its time.
|
|
1505
|
+
|
|
1506
|
+
## The othello table
|
|
1507
|
+
|
|
1508
|
+
The smallest game on the framework (`lib/asterism_games/othello/`): the
|
|
1509
|
+
rules and the CPU (`board.rb`), the table (`table.rb`) and its object
|
|
1510
|
+
(`remote.rb`), about 600 lines. The screens: fmruby-core's
|
|
1511
|
+
`flash/app/game/othello/othello.app.rb` ("Othello (table)", a window; the
|
|
1512
|
+
boards, the browser build and the sim) and `asterism-games play othello`
|
|
1513
|
+
(CRuby, "Playing in a terminal"). The
|
|
1514
|
+
game between two machines without a host is fmruby-core's
|
|
1515
|
+
`flash/app/test/othello.app.rb` ("Othello (direct)").
|
|
1516
|
+
|
|
1517
|
+
- Two seats and the spectators. The first member who sits is the owner:
|
|
1518
|
+
the owner may put the CPU in the empty seat and starts once both seats
|
|
1519
|
+
are taken (one of them a member). The colours are drawn at the start:
|
|
1520
|
+
seat 0 is black and moves first.
|
|
1521
|
+
- Standard othello on 8x8: a move must turn at least one disc, every line
|
|
1522
|
+
it closes is turned, a side with no legal move passes by itself (`"--"`
|
|
1523
|
+
in the history), and when neither side can move the discs decide (a draw
|
|
1524
|
+
when they are equal). A player may resign.
|
|
1525
|
+
- After the game the board and the result stay (`"st" => "over"`) until
|
|
1526
|
+
someone sits or stands (back to the lobby) or the owner starts again.
|
|
1527
|
+
- A member who leaves during a game hands its seat to the CPU.
|
|
1528
|
+
- **Jev** (games 0.17.0, "AI players"): when the host has Jev's key the
|
|
1529
|
+
owner may put Jev in the empty seat, or turn the CPU there into Jev,
|
|
1530
|
+
with `cpu(token, seat, "jev")` (`true` turns it back into the CPU, `false`
|
|
1531
|
+
takes it out). Jev is asked each move with more than one legal square:
|
|
1532
|
+
the request's `"game"` is `"othello"`, `"instructions"` the rules and how
|
|
1533
|
+
to read the view, `"view"` `{"you" => "B" / "W", "board" => the eight
|
|
1534
|
+
rows from row 1 (columns a-h), "discs" => {"B", "W"}, "n", "last",
|
|
1535
|
+
"moves" => [{"n", "square", "turns", "text"}]}`; the answer is a move's
|
|
1536
|
+
`n`. One out of range, one that does not come in `ai_turn_seconds` (30 s)
|
|
1537
|
+
or a failure is played by the CPU, and the game goes on. When the day's
|
|
1538
|
+
money is spent, or the game has asked `ASTERISM_GAMES_AI_CALLS_PER_GAME`
|
|
1539
|
+
decisions, Jev's seat becomes the CPU. The table apps show Jev by its
|
|
1540
|
+
name with `[AI]` and say when it is thinking; `dl` counts its time down.
|
|
1541
|
+
- Nothing in othello is hidden, so every member sees the same view: the
|
|
1542
|
+
table's view (the one the host puts on the state key) and `me`, the
|
|
1543
|
+
member's seat.
|
|
1544
|
+
|
|
1545
|
+
| Method | Arguments | Returns |
|
|
1546
|
+
|---|---|---|
|
|
1547
|
+
| `join` | name | `{"token", "seat" (-1: none), "owner"}`, or `{"error" => "full"}` / `"spectators"` |
|
|
1548
|
+
| `mine` | none | the same for a node that joined, or nil |
|
|
1549
|
+
| `sit` | token, seat | `"ok"` or why not; seat -1: the free seat (else the CPU's). Not during a game |
|
|
1550
|
+
| `stand` | token | `"ok"` or why not |
|
|
1551
|
+
| `cpu` | token, seat, on | the owner: the CPU into an empty seat (on) or out of it; seat -1: every seat. on `"jev"` (one of `ak`): Jev instead of the CPU, or the CPU there into Jev; `"no ai"` when it is not offered |
|
|
1552
|
+
| `start` | token | the owner, both seats taken |
|
|
1553
|
+
| `move` | token, n, square | `"ok"`, or why not (`"stale"`: n is not the view's `n`, `"illegal"`, `"not your turn"`, `"not seated"`, `"not playing"`); square as `"d3"` |
|
|
1554
|
+
| `resign` | token | `"ok"` or why not |
|
|
1555
|
+
| `view` | token | the table's view and `me`, or `{"error" => "token"}` |
|
|
1556
|
+
| `table` | none | the table's view |
|
|
1557
|
+
| `leave` | token | `true` / `false` |
|
|
1558
|
+
|
|
1559
|
+
The view: `st` (`wait` / `play` / `over`), `nm` the seats' names, `sk`
|
|
1560
|
+
(0 empty, 1 member, 2 CPU), `aw` (bits: a seated member is away), `ow` the
|
|
1561
|
+
owner's seat, `mb` the members (`[name, seat, flags]`), `ts` the seconds
|
|
1562
|
+
for a move, `ai` the seats' AI players (`["", "jev"]`; only when there is
|
|
1563
|
+
one; `sk` keeps 2 for them, so an old screen shows a CPU named Jev), `ak`
|
|
1564
|
+
the AI players the owner may seat (out of a game, only when the host
|
|
1565
|
+
offers some); with a game `bd` (64 characters of `.` `B` `W`, a1 first, row
|
|
1566
|
+
by row), `tn` the seat to move (-1 none), `n` the moves so far (passes
|
|
1567
|
+
too), `last`, `lg` the legal squares of the side to move (`"d3c4f5e6"`),
|
|
1568
|
+
`cnt` the discs `[black, white]`, `dl` the ms left of a member's move (with
|
|
1569
|
+
a time per move) or of Jev's; after it `res` (`{"w" => the seat that won, -1 a draw,
|
|
1570
|
+
"why" => "count" / "resign", "b", "wc"}`). At most about 900 B with 16
|
|
1571
|
+
members of 32-byte names (`rake test` prints it); a game between two
|
|
1572
|
+
players about 330 B.
|
|
1573
|
+
|
|
1574
|
+
| Key | Environment | Default | |
|
|
1575
|
+
|---|---|---|---|
|
|
1576
|
+
| `turn_seconds` | `OTHELLO_TURN_SECONDS` | 0 | a member's time for a move (0: none); over 0, when it runs out the CPU plays that move |
|
|
1577
|
+
| `cpu_ms` | `OTHELLO_CPU_MS` | 700 | the CPU's pause before it plays |
|
|
1578
|
+
| `away_seconds` | `OTHELLO_AWAY_SECONDS` | 10 | as for mahjong: away at once when its node leaves, or after this long without a call ("Who is there") |
|
|
1579
|
+
| `spectators` | `OTHELLO_SPECTATORS` | 14 | members without a seat at most (in the lobby the free seats count besides) |
|
|
1580
|
+
| `ai_turn_seconds` | `OTHELLO_AI_TURN_SECONDS` | 30 | Jev's time for a move; then the CPU plays it |
|
|
1581
|
+
|
|
1582
|
+
The CPU is small: the square with the best weight (corners first, the
|
|
1583
|
+
squares next to a corner last), then the most discs turned.
|
|
1584
|
+
|
|
1585
|
+
### In a terminal: `asterism-games play othello`
|
|
1586
|
+
|
|
1587
|
+
(`bin/othello-tty` until games 0.17.0; it is still there and runs this,
|
|
1588
|
+
with the same options.) CRuby and the asterism gem only (no Family
|
|
1589
|
+
mruby). The options are every client's ("Playing in a terminal"). It joins the table,
|
|
1590
|
+
draws the board as text at every change (it takes the state the host puts
|
|
1591
|
+
and asks its view; X black, O white, `*` the squares it may play) and on a
|
|
1592
|
+
terminal reads keys ("Keys": the arrows move a cursor, Enter plays), else
|
|
1593
|
+
commands: `sit`, `stand`, `cpu`, `ai [KIND]` (Jev in the other seat, when
|
|
1594
|
+
the host offers it), `start`, a square (`d3`), `resign`, `auto` (play the
|
|
1595
|
+
first legal square for you), `quit`. With keys the owner's `A` puts Jev in
|
|
1596
|
+
the other seat (empty or the CPU's) and turns it back into the CPU the
|
|
1597
|
+
next time (`[A] Jev`, `[A] back to the CPU`; not shown when the host
|
|
1598
|
+
offers no AI player); the seat shows `Jev (AI)` and the first line
|
|
1599
|
+
`Jev is thinking, 28 s left` on its moves. When the table's
|
|
1600
|
+
host leaves the network (Asterism's `on_leave`) it says the connection is
|
|
1601
|
+
lost and stops asking until the host is back (`on_join`; every 30 s
|
|
1602
|
+
meanwhile, should the word not come). Without `--table` it looks for a
|
|
1603
|
+
table when a node joins.
|
|
1604
|
+
|
|
1605
|
+
```
|
|
1606
|
+
# a home network (a plain zenohd)
|
|
1607
|
+
asterism-games play othello --router tcp/127.0.0.1:7447 --name Alice
|
|
1608
|
+
# the permanent tables of a console (a row's .tar with the role games; the node is its name)
|
|
1609
|
+
asterism-games play othello --credentials alice.tar --table games/games/othello1
|
|
1610
|
+
# start a game against the CPU at once
|
|
1611
|
+
asterism-games play othello --name Alice --commands "sit,cpu,start"
|
|
1612
|
+
```
|
|
1613
|
+
|
|
1614
|
+
```
|
|
1615
|
+
games/games/othello1 move 2, last d3
|
|
1616
|
+
a b c d e f g h
|
|
1617
|
+
1 . . . . . . . .
|
|
1618
|
+
2 . . . . . . . .
|
|
1619
|
+
3 . . * X * . . .
|
|
1620
|
+
4 . . . X X . . .
|
|
1621
|
+
5 . . * X O . . .
|
|
1622
|
+
6 . . . . . . . .
|
|
1623
|
+
7 . . . . . . . .
|
|
1624
|
+
8 . . . . . . . .
|
|
1625
|
+
X black CPU (CPU) 4
|
|
1626
|
+
O white Alice (you, owner) 1 <
|
|
1627
|
+
your move: a square (* marks them), or resign
|
|
1628
|
+
```
|
|
1629
|
+
|
|
1630
|
+
On a terminal, with keys:
|
|
1631
|
+
|
|
1632
|
+
```
|
|
1633
|
+
3 . .[*]X * . . .
|
|
1634
|
+
...
|
|
1635
|
+
your move: [arrows] move [Enter] play (* marks the squares) [:] type a command [Q] quit
|
|
1636
|
+
```
|
|
1637
|
+
|
|
1638
|
+
## The drawing board
|
|
1639
|
+
|
|
1640
|
+
A game without a winner (`lib/asterism_games/board/`, about 360 lines):
|
|
1641
|
+
one picture that every member of the board draws on. It shows the other
|
|
1642
|
+
half of the framework: a state that is handed out as it changes, rather
|
|
1643
|
+
than a view that is read. The screen is fmruby-core's
|
|
1644
|
+
`flash/app/game/board/board.app.rb` ("Board" / "お絵かき", a window; the
|
|
1645
|
+
boards, the browser build and the sim).
|
|
1646
|
+
|
|
1647
|
+
- The picture is strokes in board coordinates (640x360): a colour (one of
|
|
1648
|
+
16; the screen's palette), a width step (one of 4: 1, 2, 4 and 8 board
|
|
1649
|
+
pixels on the screen) and points. A screen sends a stroke while it is
|
|
1650
|
+
drawn (`line`, then `more`), so the others see it grow.
|
|
1651
|
+
- The first member to join is the owner (the next one in the order of
|
|
1652
|
+
joining when it goes), who alone may clear the board. Each member may
|
|
1653
|
+
take back its own last stroke still on the board (`undo`).
|
|
1654
|
+
- The host saves the picture (`state_dir/board1.json`, at most once a
|
|
1655
|
+
second while somebody draws, and when it stops) and has it again when it
|
|
1656
|
+
starts. A room's board goes when the room closes, like a room's tables.
|
|
1657
|
+
|
|
1658
|
+
| Method | Arguments | Returns |
|
|
1659
|
+
|---|---|---|
|
|
1660
|
+
| `join` | name | `{"token", "owner"}`, or `{"error" => "full"}` |
|
|
1661
|
+
| `line` | token, c, w, pts | a new stroke: `{"id" => id}`, or why not (`"colour"`, `"width"`, `"points"`); pts `[x, y, ...]`, 1-256 points |
|
|
1662
|
+
| `more` | token, id, pts | more points of the caller's newest stroke: `"ok"`, or `"not yours"`, `"long"` (past 1024 points: the screen starts a new stroke), `"points"` |
|
|
1663
|
+
| `undo` | token | `"ok"` or `"nothing"` |
|
|
1664
|
+
| `clear` | token | `"ok"` or `"not owner"` |
|
|
1665
|
+
| `picture` | id, at | a page of the picture from stroke `id` (0: the first) and its point `at`: `{"v", "n", "p" => [[id, c, w, pts, k], ...], "nx" => [id, at] of the next page, or nil}`; k 1: the page goes on with a stroke the page before began, and its points start with that stroke's point before `at`. At most 400 points a page (2.2 KB) |
|
|
1666
|
+
| `view` | token | the board's view, `own` (the caller is the owner) and `mine` (its strokes) |
|
|
1667
|
+
| `table` | none | the board's view (what the state key has) |
|
|
1668
|
+
| `leave` | token | `true` / `false` |
|
|
1669
|
+
|
|
1670
|
+
The view (the state key): `st` (`draw`), `n` the strokes, `np` the
|
|
1671
|
+
points, `last` the newest stroke's id, `mb` the members (`[name, flags]`,
|
|
1672
|
+
1 away, 2 owner), `lim` `[640, 360, 16, 4, 256, 1024, 1000, 40000]` (the
|
|
1673
|
+
size, colours, widths, points a message, points a stroke, strokes, points).
|
|
1674
|
+
|
|
1675
|
+
**The strokes key.** Every change goes out on
|
|
1676
|
+
`asterism/<node>/<app>/<board>/strokes` (`Game#tell`) as one message, in
|
|
1677
|
+
the order the host took them:
|
|
1678
|
+
|
|
1679
|
+
| Message | |
|
|
1680
|
+
|---|---|
|
|
1681
|
+
| `{"o" => "a", "id", "c", "w", "p" => [x, y, ...], "k", "v"}` | points of stroke `id`: `k` 0 starts it, `k` 1 goes on with it (`p` then starts with the stroke's last point so far, so each message draws as a line of its own) |
|
|
1682
|
+
| `{"o" => "u", "id", "v"}` | stroke `id` taken back |
|
|
1683
|
+
| `{"o" => "c", "v"}` | the board cleared |
|
|
1684
|
+
|
|
1685
|
+
`v` is the board's version when it was told. A screen draws the messages
|
|
1686
|
+
as they come and keeps the strokes, to draw the place of a stroke taken
|
|
1687
|
+
back again; it reads the whole picture (`picture`, in pages) when it joins
|
|
1688
|
+
and after the connection or the host comes back.
|
|
1689
|
+
|
|
1690
|
+
**Recording and playing back** (the console's recordings, its docs
|
|
1691
|
+
`v4.md` and `rooms.md`): record the Asterism key
|
|
1692
|
+
`asterism/<host>/games/board1/strokes`, and a playback to the network puts
|
|
1693
|
+
the same messages on the same key: the screens draw the board being drawn
|
|
1694
|
+
again. A screen cannot tell a played message from a real one, so the
|
|
1695
|
+
picture it shows may then differ from the board's own (the host does not
|
|
1696
|
+
see the playback); the messages played carry the older versions, so a
|
|
1697
|
+
screen says a recording is playing (`v` older than the last state it saw)
|
|
1698
|
+
and `R` reads the board's own picture again. A console records keys on the
|
|
1699
|
+
registered side only (the boards' own certificates and the browsers with
|
|
1700
|
+
a pass, the permanent tables): it cannot subscribe to or put into a room.
|
|
1701
|
+
|
|
1702
|
+
**Limits.** A message is at most 256 points: with 3-byte coordinates and
|
|
1703
|
+
the largest id and version, 1.6 KB with its key (`rake test` prints it),
|
|
1704
|
+
well under the 4096 bytes a board takes. A stroke is at most 1024 points.
|
|
1705
|
+
The board keeps at most 1000 strokes and 40000 points and drops the oldest
|
|
1706
|
+
strokes past either (nothing is told: a screen keeps the same limits and
|
|
1707
|
+
drops them too; they leave its screen when that place is drawn again).
|
|
1708
|
+
Dropping rather than refusing keeps the board drawable for as long as
|
|
1709
|
+
people draw; the point limit is what a screen can keep (4 bytes a point,
|
|
1710
|
+
about 60 % of a window app's 1.5 MB VM at the limit). 500 strokes of 60
|
|
1711
|
+
points are 240 KB saved and add about 3 MB to the host's RSS.
|
|
1712
|
+
|
|
1713
|
+
| Key | Environment | Default | |
|
|
1714
|
+
|---|---|---|---|
|
|
1715
|
+
| `away_seconds` | `BOARD_AWAY_SECONDS` | 10 | as for mahjong ("Who is there") |
|
|
1716
|
+
|
|
1717
|
+
## riichi_engine
|
|
1718
|
+
|
|
1719
|
+
The engine runs the rounds (wall, deal, draws, discards, calls, riichi, wins,
|
|
1720
|
+
yaku, points) and the CPUs. The host uses a fork of 0.1.0 with its errors
|
|
1721
|
+
fixed: the `fmrb` branch of
|
|
1722
|
+
[kishima/riichi_engine](https://github.com/kishima/riichi_engine), a fork of
|
|
1723
|
+
[riichi_engine](https://github.com/HenrikStephensen/riichi_engine)
|
|
1724
|
+
(`Gemfile` takes it as a git source, so the lockfile pins a revision). Each fix is a branch of its own
|
|
1725
|
+
(`fix/*`, `feat/*`), written to be sent upstream; `fmrb` merges them (version
|
|
1726
|
+
`0.1.1.fmrb6`, not published). The host has no corrections of its own around
|
|
1727
|
+
the engine any more.
|
|
1728
|
+
|
|
1729
|
+
What 0.1.0 got wrong, and what this host did about it before the fork:
|
|
1730
|
+
|
|
1731
|
+
| | In 0.1.0 | Fork branch | The host's correction (removed) |
|
|
1732
|
+
|---|---|---|---|
|
|
1733
|
+
| 1 | `evaluate_win` counts dora (and uradora) on the indicators | `fix/dora-indicators` | evaluated with the dora tiles |
|
|
1734
|
+
| 2 | no houtei: the phase is already `round_end` (and a houtei-only hand is not offered the ron) | `fix/houtei` | evaluated in the phase of the win |
|
|
1735
|
+
| 3 | a seat in riichi gets no discard, its turn cannot go on | `fix/riichi-discard` | offered the drawn tile |
|
|
1736
|
+
| 4 | ippatsu is not cleared by the player's own next discard | `fix/ippatsu` | cleared it |
|
|
1737
|
+
| 5 | a red five is swapped or lost in a discard, chi, pon or kan | `fix/red-five` | put the exact tiles first, recorded the kan's tiles |
|
|
1738
|
+
| 6 | a seat that could ron cannot pon instead | `fix/call-own-options` | left only the chosen action pending |
|
|
1739
|
+
| 7 | ties between rons by seat number (no head bump) | `fix/ron-turn-order` | decided by turn order; now `resolve_pending_action(..., from_seat:)` |
|
|
1740
|
+
| 8 | seeds are read up to the first non-base-36 character | `fix/wall-seed` | used only base-36 seeds |
|
|
1741
|
+
| 9 | the normal/hard CPU keeps the shape with the least acceptance | `fix/cpu-discard-ukeire` | its own CPU (`Engine::CpuAi`); now the engine's |
|
|
1742
|
+
| 10 | `game_type` `"tonpuu"` (the documented name) never ends | `fix/tonpuu-game-type` | (not used) |
|
|
1743
|
+
| 11 | `RiichiEngine.loader.eager_load` fails | `fix/zeitwerk-eager-load` | (not used) |
|
|
1744
|
+
| 12 | no tanyao with a red five | `fix/tanyao-red-five` | none (found by the comparison below) |
|
|
1745
|
+
| 13 | a triplet completed by ron on a shanpon wait has the concealed fu | `fix/shanpon-ron-fu` | none (comparison) |
|
|
1746
|
+
| 14 | only the first wait of the winning tile is scored (pinfu, higher fu missed) | `fix/machi-interpretation` | none (comparison) |
|
|
1747
|
+
| 15 | four identical sequences are iipeiko, not ryanpeiko | `fix/ryanpeiko-four-sequences` | none (comparison) |
|
|
1748
|
+
| 16 | `double_ron` and `triple_ron_draw` have no effect (one ron always wins, by head bump) | `fix/multiple-ron` | none (Step 2 needs them) |
|
|
1749
|
+
| 17 | several yakuman in one hand always score as one (a new key, `multiple_yakuman`) | `feat/multiple-yakuman` | none |
|
|
1750
|
+
| 18 | the shanten counts the concealed tiles as the whole hand (wrong with melds), and slowly | `fix/shanten-melds` | its own count of the regular form (`Engine.shanten`); now the engine's |
|
|
1751
|
+
| 19 | a kan does not shorten the live wall | `fix/kan-live-wall` | none |
|
|
1752
|
+
| 20 | the CPU judges riichi on the hand before the discard (it never declares riichi) | `fix/cpu-riichi` | none |
|
|
1753
|
+
| 21 | every discard ends the first go-around for all four (no nine kinds, double riichi or chiihou for a child) | `fix/first-turn-flags` | none |
|
|
1754
|
+
| 22 | an unknown `game_type` never ends | `fix/unknown-game-type` | none |
|
|
1755
|
+
| 23 | a kan's ura dora indicator is not counted (the ura indicators stay as dealt) | `fix/kan-uradora` | none |
|
|
1756
|
+
| 24 | a seat that lets a winning tile go by with no ron offered (no yaku on it) is not furiten for the go-around | `fix/dojun-furiten-no-yaku` | none |
|
|
1757
|
+
| 25 | the normal/hard CPU calls without a yaku in sight (it looks at the shanten alone), and a chi with several pairs is counted without taking the tiles out | `fix/cpu-yaku-calls` | none |
|
|
1758
|
+
|
|
1759
|
+
New keys in 0.1.1.fmrb3, each off by default (the engine plays as 0.1.0
|
|
1760
|
+
without them; the host turns them on, "Rules"): `suufon_renda`,
|
|
1761
|
+
`suukaikan`, `suucha_riichi` (`feat/abortive-draws`), `nagashi_mangan`
|
|
1762
|
+
(`feat/nagashi-mangan`), `pao` (`feat/pao`), `minkan_dora_after_discard`
|
|
1763
|
+
(`feat/kan-dora-after-discard`), `double_yakuman` used at last
|
|
1764
|
+
(`feat/double-yakuman`), `tenpai_yame` (`feat/agari-yame`). New keys in
|
|
1765
|
+
0.1.1.fmrb4, likewise: `kuikae` (`feat/kuikae`; on by default, the host
|
|
1766
|
+
turns it off), `riichi_ankan` (`feat/riichi-ankan`),
|
|
1767
|
+
`kokushi_ankan_chankan` (`feat/kokushi-ankan-chankan`), `pao_honba`
|
|
1768
|
+
(`feat/pao-honba`), `tenhou_best_tile` (`feat/tenhou-best-tile`),
|
|
1769
|
+
`nan_iri` and `sudden_death` (`feat/sudden-death`). New in 0.1.1.fmrb6:
|
|
1770
|
+
`riichi_void_on_ron` (`feat/riichi-void-on-ron`; off by default, the host
|
|
1771
|
+
turns it on).
|
|
1772
|
+
|
|
1773
|
+
The fork compares its win evaluation with the Python package `mahjong`
|
|
1774
|
+
(PyPI) on 30 000 random winning hands and one or more hands per yaku
|
|
1775
|
+
(`spec/compat/`): the yaku, han, fu and payments agree except where a rule
|
|
1776
|
+
differs (two yakuman in one hand score as one yakuman in riichi_engine, as
|
|
1777
|
+
two in the Python package; with `multiple_yakuman` they agree).
|
|
1778
|
+
|
|
1779
|
+
Exhaustive draws in CPU-only hanchan: with 0.1.0, 341 of 452 rounds (75 %)
|
|
1780
|
+
with the engine's own CPU, 127 of 496 (26 %) with the host's corrected CPU;
|
|
1781
|
+
with the fork's own CPU, 85 of 488 (17 %) at normal and 97 of 496 (20 %) at
|
|
1782
|
+
hard over 40 hanchan each, and 72 of 282 (26 %) in the 20 normal hanchan of
|
|
1783
|
+
one `rake test` (0.1.1.fmrb2). With 0.1.1.fmrb3 (its CPU now declares
|
|
1784
|
+
riichi and calls less), over 40 hanchan each: 15 % at normal (68 of 449
|
|
1785
|
+
rounds), 14 % at hard (57 of 419), 33 % at easy.
|
|
1786
|
+
|
|
1787
|
+
Still as in 0.1.0: tobi ends the game below 0 points, not at 0 (the
|
|
1788
|
+
fork's documents now say so, `fix/tobi-doc`); without `sudden_death` the
|
|
1789
|
+
west round never ends without a player above the threshold (the host ends
|
|
1790
|
+
the game after west 4); `tsumo_agari` and `ron_agari` leave the points to
|
|
1791
|
+
the host (the engine's API says so). Without `kuikae` set to false a seat
|
|
1792
|
+
may discard the tile it just called, and without `riichi_void_on_ron` a
|
|
1793
|
+
riichi declared on a tile that is ronned keeps its stick on the table. A
|
|
1794
|
+
CPU decision takes about 2 ms of CRuby time at the median (about 0.4 s at
|
|
1795
|
+
most, the first time a suit pattern with many tiles is counted).
|
|
1796
|
+
|
|
1797
|
+
## Tests
|
|
1798
|
+
|
|
1799
|
+
```
|
|
1800
|
+
bundle exec rake test # MJ_GAMES=20 CPU hanchan, MJ_HUMAN_GAMES=1 four-player hanchan
|
|
1801
|
+
MJ_GAMES=4 bundle exec rake test # quicker
|
|
1802
|
+
bundle exec rake test:gem # what the gem holds only: lib/ alone, mahjong never loaded, no sqlite3
|
|
1803
|
+
gem build asterism-games.gemspec # the gem (its files: test/test_gem.rb checks them)
|
|
1804
|
+
ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 bundle exec rake test # also through a router
|
|
1805
|
+
```
|
|
1806
|
+
|
|
1807
|
+
- `test_play.rb`: `asterism-games play` (in `rake test:gem` too): the
|
|
1808
|
+
options and `ASTERISM_ROUTER` / `ASTERISM_CREDENTIALS`, `--help` (the
|
|
1809
|
+
host's too: it prints and starts nothing), the tiles in kanji (the
|
|
1810
|
+
default), as emoji and MPSZ both ways, the widths (🀄 two columns), rows that keep their columns
|
|
1811
|
+
(padded by width, or each part put at its column on a terminal) with the
|
|
1812
|
+
letters under their tiles, reading discards (letters, `6s`, `0p`, the red
|
|
1813
|
+
five as `5p`), declarations and calls (choosing by number, tile or
|
|
1814
|
+
letter), the countdown rewritten in place on a terminal (only the time
|
|
1815
|
+
line, between ESC 7 and ESC 8), and the screens (a round in kanji with
|
|
1816
|
+
its two-row hand and a river that goes on below, emoji and MPSZ; the lobby, a result,
|
|
1817
|
+
the end, a call, a spectator). With `ASTERISM_TEST_ROUTER`, a host in a
|
|
1818
|
+
process of its own and three clients at once (kanji by default, emoji,
|
|
1819
|
+
MPSZ) that play a
|
|
1820
|
+
tonpuu game against three CPUs each to the end from scripted input
|
|
1821
|
+
(letters, tiles, calls, passes, OKs; `PLAY_TRANSCRIPT_DIR` keeps what
|
|
1822
|
+
they showed).
|
|
1823
|
+
- `test_play_commands.rb`: `--commands "sit,cpu,start"` against a table
|
|
1824
|
+
in this process: they wait for the lobby when a game is going on as
|
|
1825
|
+
the client joins (and then sit, fill and start), the owner's wait for
|
|
1826
|
+
the owner, and run at once in a fresh lobby.
|
|
1827
|
+
- `test_mahjong_games.rb`: MJ_GAMES hanchan by four CPUs in MJ_PROCS
|
|
1828
|
+
processes (default 8) and MJ_HUMAN_GAMES hanchan by four members playing
|
|
1829
|
+
through `act` (wins when offered, calls now and then, passes, timeouts,
|
|
1830
|
+
and now and then something not offered, refused): no exception, the points
|
|
1831
|
+
add up after every round, after every step no seat's view has another
|
|
1832
|
+
seat's hand, the help comes only as each seat's level says (level 1 only
|
|
1833
|
+
with a riichi offer, uk only at level 3 and never with wt), and every
|
|
1834
|
+
view fits in a board's message (the four seats ask for the help at
|
|
1835
|
+
levels 1, 2, 3 and 0); and the worst-case views (a result, a riichi turn
|
|
1836
|
+
with the riichi help at its largest).
|
|
1837
|
+
- `test_mahjong_assist.rb`: the help by level (which views get `wt` and
|
|
1838
|
+
`uk`, the call with and without the level), the shanten against the
|
|
1839
|
+
engine's for closed hands and against the wait judge with melds, the
|
|
1840
|
+
acceptance, and the largest acceptance view.
|
|
1841
|
+
- `test_mahjong_rules.rb`: the lobby (join, sit, stand, the owner, CPUs,
|
|
1842
|
+
start, a spectator, away and back, back to the lobby after a game), tokens,
|
|
1843
|
+
refusals (no token, a wrong token, an action not offered, a stale state),
|
|
1844
|
+
the timeout, saving and reading back mid-round and in the lobby, the
|
|
1845
|
+
deadline across a restart, calls (every player answers, one deadline for
|
|
1846
|
+
all, ron over pon), double ron (turn order, honba and sticks to the first,
|
|
1847
|
+
off: head bump), three rons (a draw; off: three winners), multiple
|
|
1848
|
+
yakuman on and off, the host's rule defaults, the riichi help (the waits
|
|
1849
|
+
after each riichi discard and what is left of them, only in the seat's
|
|
1850
|
+
own view), and the engine fixes the host relies on.
|
|
1851
|
+
- `test_mahjong_local_rules.rb`: the local rules (the host's values to
|
|
1852
|
+
start from, the owner only, the lobby only, the values offered, each item
|
|
1853
|
+
reaching the engine, the time to discard, saved and kept for the next
|
|
1854
|
+
game; a tonpuu game chosen in the lobby ends in the east round or the
|
|
1855
|
+
south round after it, open
|
|
1856
|
+
tanyao off offers no ron on an open tanyao-only hand, the red fives in
|
|
1857
|
+
the wall), the spectators (over a whole game the four hands and no
|
|
1858
|
+
offer, help or sign of a call in a spectator's own view, no hand in the
|
|
1859
|
+
put, `table` or a seat's view; with a delay they see the game and the
|
|
1860
|
+
hands as they were that long ago while the players do not wait; none by
|
|
1861
|
+
default; how many), the CPUs' level and the spectators' delay chosen in
|
|
1862
|
+
the lobby (reaching the CPUs, saved, the delay setting in minutes) and
|
|
1863
|
+
the han of each yaku in a result; the items of
|
|
1864
|
+
0.1.1.fmrb3 (the three abortive draws as one item, over a setting with
|
|
1865
|
+
only some of them) and the extra round item (both keys, its value by the
|
|
1866
|
+
game chosen, over a setting with only one of them).
|
|
1867
|
+
- `test_mahjong_round_ends.rb`: the rules of 0.1.1.fmrb3, fmrb4 and fmrb6
|
|
1868
|
+
the host turns on: every host key reaching the engine, a ron on the
|
|
1869
|
+
riichi tile giving the 1000 back (a pon of it letting the riichi stand), nagashi mangan paid in place
|
|
1870
|
+
of the no-ten payments (and not when turned off), pao recorded at the pon
|
|
1871
|
+
and paid (all of it at a tsumo, half with the discarder at a ron, the
|
|
1872
|
+
honba of a ron by the seat liable), no swap-calling after a chi (not
|
|
1873
|
+
offered, refused), the west round ended by sudden death (the reason in
|
|
1874
|
+
the view), a tonpuu game going south below 30,000 (not with the extra
|
|
1875
|
+
round turned off), a double
|
|
1876
|
+
yakuman's -2 and points (one yakuman when turned off), four winds ending
|
|
1877
|
+
the round by name (the dealer keeps the seat, one more honba; none when
|
|
1878
|
+
turned off), why a game ended in the view, the riichi sticks left at the
|
|
1879
|
+
end going to the top (equal points by seat), and a table saved before
|
|
1880
|
+
these rules read back and played on.
|
|
1881
|
+
- `test_mahjong_time.rb`: the time ("Time"): 5+20 by default and from the
|
|
1882
|
+
settings, a move beyond its 5 s taking the rest off the bank, the move
|
|
1883
|
+
played for when the bank is used up (then 5 s a move), the bank filled
|
|
1884
|
+
again at every round, an away
|
|
1885
|
+
player's short wait leaving the bank, the calls keeping their time, the
|
|
1886
|
+
owner's choice (a bank, a fixed time, only the lobby's choices) reaching the
|
|
1887
|
+
table, the banks saved, and a table saved before the bank read back.
|
|
1888
|
+
- `test_mahjong_result_ok.rb`: the start of a round and the result
|
|
1889
|
+
("Result"): the pause after the deal (no clock, no CPU or AI move, every
|
|
1890
|
+
round, saved), the result waiting for every human's OK (an away one not
|
|
1891
|
+
counted, an old screen's next as its OK), moving on by itself after the
|
|
1892
|
+
count and 10 s (the count by the yaku lines, a double ron, a draw), the
|
|
1893
|
+
settings, and a table without humans as before.
|
|
1894
|
+
- `test_mahjong_records.rb`: the game records ("Game records"): a game's
|
|
1895
|
+
record whole (every round replayed from its deal by its moves: each
|
|
1896
|
+
discard and call from the hand that holds it, the wins' hands, no tile
|
|
1897
|
+
more than four times, each seat's points from round to round by the
|
|
1898
|
+
result and its riichi stick, the final scores), going on across a
|
|
1899
|
+
restart of the host mid-round, the 20 newest kept (the numbers go on),
|
|
1900
|
+
without a database a closed room's records gone with it; with SQLite
|
|
1901
|
+
(0.14.0) the records read by the next host, the 1000 newest kept (and
|
|
1902
|
+
the pages walking them), the JSON records of 0.13.0 taken in once (the
|
|
1903
|
+
same games in the list and the same records), a closed room's games kept
|
|
1904
|
+
in the database while no other room, permanent or LAN table gives them
|
|
1905
|
+
out (the stats count them), the stats equal to what the records count
|
|
1906
|
+
another way (wins from the tsumo and ron moves, deal-ins from the ron
|
|
1907
|
+
moves, riichi, ranks, rank points; a person by name, CPU, Claude, Jev),
|
|
1908
|
+
the stats in pages; every answer of `records` and
|
|
1909
|
+
`record` under 3800 B (twenty games of long names in pages, a long round
|
|
1910
|
+
in parts), the remote's calls, the last result on `@status` (and not in
|
|
1911
|
+
the rooms' counts), and the declarations in the view (`cl`). The CPU
|
|
1912
|
+
hanchan of `test_mahjong_games.rb` check each game's record the same way.
|
|
1913
|
+
- `test_mahjong_call_privacy.rb`: the calls on a discard keep their secret:
|
|
1914
|
+
each seat sees its own choices only, and a second seat that may call (or
|
|
1915
|
+
answers) changes no other seat's view, the spectator's, the put or the
|
|
1916
|
+
public view (two tables compared key by key); the versions after the
|
|
1917
|
+
calls, the passes naming nobody, and over whole four-member hanchan no
|
|
1918
|
+
`dl` but a candidate's own bit or the "others" bits.
|
|
1919
|
+
- `test_mahjong_room_identity.rb`: who joins in a room: the seats and the
|
|
1920
|
+
host's names join, nobody else (join and mine), nothing checked outside a
|
|
1921
|
+
room, join and mine without a secret, a seat silent for long is a new
|
|
1922
|
+
page, the room's ID reaching the tables.
|
|
1923
|
+
- `test_othello.rb`: the othello rules (the legal moves, the discs turned in
|
|
1924
|
+
every direction, a side that cannot move passing by itself, the end and
|
|
1925
|
+
the winner by the discs, a history replayed), the CPU playing only legal
|
|
1926
|
+
moves to the end, a game against the CPU, the seats and the owner, the
|
|
1927
|
+
spectators' limit, no time limit by default and the CPU playing a move
|
|
1928
|
+
whose time ran out, a game saved and resumed with the same tokens and the
|
|
1929
|
+
time left, the exposed methods and the largest view.
|
|
1930
|
+
- `test_board.rb`: the drawing board: a stroke and more points on the
|
|
1931
|
+
strokes key (a part going on from the last point), what is refused, a
|
|
1932
|
+
stroke's point limit, taking back one's own last stroke, only the owner
|
|
1933
|
+
clearing and the owner passing on, the oldest strokes dropped past the
|
|
1934
|
+
stroke and point limits (the newest staying), the largest message and
|
|
1935
|
+
the largest page of the picture, the picture in pages giving every point
|
|
1936
|
+
once, saved and resumed (owner, ids, tokens), writes held to one a
|
|
1937
|
+
second, the host putting the strokes before the state and counting them
|
|
1938
|
+
in the status, and a host started again having the picture.
|
|
1939
|
+
- `test_rooms.rb`: the rooms' supervisor with stand-in host processes: which
|
|
1940
|
+
rooms get a host (the app, closed, ended), the child's settings (the
|
|
1941
|
+
slot's certificate, the namespace), a new room on the slot starting from
|
|
1942
|
+
nothing (and a slot that moved while the supervisor was down), the same
|
|
1943
|
+
room keeping its tables across a restart, an unreadable file changing
|
|
1944
|
+
nothing, `max` and a stopped host started again, the status (counts
|
|
1945
|
+
only) and the settings from the environment; which tables a room gets
|
|
1946
|
+
(each game's screen and first table by default, a room opening the
|
|
1947
|
+
mahjong screen gets mahjong tables only and one opening the othello
|
|
1948
|
+
screen othello tables only, the given tables split by game, table names
|
|
1949
|
+
in the environment finding their game).
|
|
1950
|
+
- `test_presence.rb`: who is there ("Who is there"): a member's node that
|
|
1951
|
+
leaves is away at once and back at once, a call or a join brings it back,
|
|
1952
|
+
without the network's word a member is away after `away_ms` as before,
|
|
1953
|
+
the time to forget counts from when the node left (a seated member
|
|
1954
|
+
stays), the saved form keeps no presence, a room's seat gone for
|
|
1955
|
+
`seat_reuse_ms` is a new page when its node comes back, an away mahjong
|
|
1956
|
+
seat, and the host telling every table.
|
|
1957
|
+
- `test_mahjong_owner_handover.rb`: the owner away ("Players that drop
|
|
1958
|
+
out"): the role passes after `owner_handover_ms` to the seated member who
|
|
1959
|
+
sat first, else by join order, stays with nobody there, is not taken
|
|
1960
|
+
back, also during a game, 0 turns it off; a table left alone: a game whose
|
|
1961
|
+
humans are all away for `abandon_ms` goes back to the lobby without a
|
|
1962
|
+
record, a member who left stops counting at once, one human back in time
|
|
1963
|
+
keeps the game, a result is abandoned too, a table of CPUs plays on, 0
|
|
1964
|
+
turns it off.
|
|
1965
|
+
- `test_network.rb`: the same refusals through a router, with the host in a
|
|
1966
|
+
child process, and the caller from Asterism (a call that brings `from:`
|
|
1967
|
+
is refused, the numbers of arguments are counted) (skipped without
|
|
1968
|
+
`ASTERISM_TEST_ROUTER`).
|
|
1969
|
+
- `test_framework.rb`: the framework without a game: its files require
|
|
1970
|
+
nothing of a game, loading it loads only its own files (no riichi_engine),
|
|
1971
|
+
and `test/framework/test_tally.rb` runs in a process of its own with a
|
|
1972
|
+
small game of its own (`test/framework/tally.rb`) and nothing else: the
|
|
1973
|
+
exposed methods, the settings from the game, members and tokens through
|
|
1974
|
+
the Remote with `from:`, saving and another host going on from the saved
|
|
1975
|
+
state, the status file, a room and the permanent tables of that game
|
|
1976
|
+
(who joins, `GET /status`, the room's tables removed when it closes) and,
|
|
1977
|
+
with `ASTERISM_TEST_ROUTER`, the host over a router (calls, the state
|
|
1978
|
+
put, the profile, and another node killed: away at once, back when its
|
|
1979
|
+
node is there again). Also the mahjong game's registration and its exposed
|
|
1980
|
+
methods as before, and that nothing of mahjong is in `lib/`.
|
|
1981
|
+
- `test_gem.rb`: the gem (`asterism-games.gemspec`): its files are `lib/`,
|
|
1982
|
+
the two commands and the texts, never `app/`, `deploy/`, `config/`, the
|
|
1983
|
+
tests or anything like a key; its version is `VERSION`'s and it depends on
|
|
1984
|
+
asterism only. Under `rake test:gem` (with `test_framework.rb`,
|
|
1985
|
+
`test_othello.rb`, `test_board.rb` and `test_record_book.rb`, `lib/`
|
|
1986
|
+
alone on the load path, `test/no_sqlite3/` making `require "sqlite3"`
|
|
1987
|
+
fail) the games are othello and the board and nothing of mahjong is
|
|
1988
|
+
loaded.
|
|
1989
|
+
- `test_record_book.rb`: the records' book of the framework (`RecordBook`):
|
|
1990
|
+
the JSON form (the newest kept in the store, read back, the stats' rows),
|
|
1991
|
+
and without the sqlite3 gem a host takes the JSON form; with SQLite the
|
|
1992
|
+
newest kept of a table (another table apart, the seats going with their
|
|
1993
|
+
games, ids never reused), a room's games given out only in that room and
|
|
1994
|
+
kept when it closes (the rooms' table counted together), the stats by
|
|
1995
|
+
time and kind of game, four processes ending 100 games at once into one
|
|
1996
|
+
database (none lost, `integrity_check` ok, the copy whole) and a restore
|
|
1997
|
+
from the copy alone.
|
|
1998
|
+
|
|
1999
|
+
`bin/mahjong-bots` fills a table with players over the network (to try a
|
|
2000
|
+
second table, or the host under load): `bundle exec bin/mahjong-bots
|
|
2001
|
+
--table games/games/mahjong2`. In a room: `--credentials room-01.tar
|
|
2002
|
+
--namespace room/01 --room <room ID> --table games/games/mahjong1`. `--rules 0=1,1=0` sets
|
|
2003
|
+
local rules before the start (a tonpuu game without open tanyao),
|
|
2004
|
+
`--start-ms` waits in the lobby (for spectators), `--result-ms` looks at
|
|
2005
|
+
each result that long.
|
|
2006
|
+
|
|
2007
|
+
## License
|
|
2008
|
+
|
|
2009
|
+
MIT (LICENSE).
|