asterism-games 0.18.0 → 0.20.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8555b735e1a25aff6167fe382f450dd367c4c6d81d13497c932265cf3a61a5f4
4
- data.tar.gz: 5185c71e72d56963cebb26d480bf647763dd51156a0285db1104f6a9f600ae22
3
+ metadata.gz: 0a35db1346246e5d20505026e701fef357536cc75fa53f22bf47878398fb01c3
4
+ data.tar.gz: 79c4c46763e2104c3e4d03ba521327e9ceb5fa77fc74521cec6041eb3ca48b89
5
5
  SHA512:
6
- metadata.gz: 48a9fc1196c4f3fb5b2b2b8cf8c9f3c4f0682a4b1beee018ec0518c2c131068c5113ccb309d008f97a8ce479dd2ba994a2e268b5bcd50869bca5ee692144a030
7
- data.tar.gz: 8227908a95b02825a6e53ba1c1d75d9008f33b802fca25c82b41c03b0194b3d58e3f7c317ab3e6df7f770140b9a34410467ec3c0ea0a2be189ddc56ffdabca5b
6
+ metadata.gz: 2b31fdedf12d610dca0c8cbd4f1ae060b5b42eeab54dee46d62f0ced96d8895468b48e84db2eae2904489448329a19ef5888cf9cb2a514684c2610d4e0937d87
7
+ data.tar.gz: 9908df82204210223cecd09b651da9d12e3a27ee832433d0e57b74d4d895a7a9753c83c20b75cadfeff88ef94eb91f6564290be193b1e501a37ce209124b67d3
data/CHANGELOG.md CHANGED
@@ -3,6 +3,115 @@
3
3
  Versions before 0.11.1 are noted in the README where each feature is
4
4
  described ("games 0.10.0" and the like).
5
5
 
6
+ ## 0.20.0
7
+
8
+ - **Watch games** (README "Watch games"): the owner of a mahjong table
9
+ may start a game of CPUs and AI players only, and watch it with the
10
+ others at the table (and a commentary, `commentate mahjong`). The owner
11
+ need not be seated: a member that joins a table where nobody is seated
12
+ and there is no owner is the owner (`join`'s `"owner"`, the new `"mo"`
13
+ of the member's own view); once somebody sits the first seated is the
14
+ owner as before, so a game with people seated goes as it did. The new
15
+ method `watch(token)` starts it when the four seats are CPUs or AI
16
+ players, nobody is seated and at most one seat is Claude's (Jev may take
17
+ all four; `"human"`, `"ai limit"`, `"seats"`, `"not owner"` otherwise).
18
+ The lobby view's `"wc"` says whether it may start (1), is over the AI
19
+ limit (2) or not (0); views during it have `"wm" => 1` and the host's
20
+ `@status` `"watch" => true`. Nobody is seated, so the table's
21
+ abandoning does not apply; instead the game stops (unrecorded) when
22
+ every member has left. One game, then the lobby with the same seats.
23
+ The record is kept with `"watch" => true` (also on `records`' entries)
24
+ and counts in the stats for the CPU and the AI kinds only, never for a
25
+ person. The AI players' daily limits work as before.
26
+ - **The game's AI decisions are each seat's own**: `ASTERISM_GAMES_AI_CALLS_PER_GAME`
27
+ (400) is now how many decisions each AI seat may ask in a game (mahjong
28
+ and othello, every game, watch games or not); a seat past it becomes a
29
+ CPU alone. Before, the seats of a game shared it, so a game with three
30
+ or four AI seats stopped them all part way through a tonpuu. A game's
31
+ most is now 400 times its AI seats; the daily limits are as before.
32
+ - `play mahjong`: `W` (and the command `watch`) starts a watch game, for
33
+ the owner with or without a seat; `A` then a seat's number turns that
34
+ seat into a CPU, Claude, Jev and back (the kinds the lobby offers); the
35
+ owner's commands (`cpu`, `ai`, `watch`, `--commands`) work for an owner
36
+ without a seat; the lobby marks the owner among those watching and says
37
+ the AI limit when the seats are over it; a watch game is headed
38
+ `観戦対局`.
39
+ - Family mruby's mahjong app (fmruby-core) has a `観戦で始める[W]` button
40
+ for the owner when the host says it may start (`"wc"`); with an older
41
+ host it is not shown.
42
+
43
+ ## 0.19.1
44
+
45
+ - The commentary tells where to get VOICEVOX when it finds no engine:
46
+ the terminal says it is a free app installed and started apart from the
47
+ gem (with its home page), the page's bar says the same in Japanese, and
48
+ `commentate --help` and the README's "The voice" say it up front.
49
+
50
+ ## 0.19.0
51
+
52
+ - **Commentary** (README "Commentary"): `asterism-games commentate
53
+ mahjong` watches a table from its spectate key (0.18.0) and shows it in
54
+ the browser: a small web server on `127.0.0.1` (HTTP and Server-Sent
55
+ Events by hand on `socket`, no new gem; port 7470 or the next free one),
56
+ a page that draws the table in a canvas with the hand-drawn tile sheets
57
+ of Family mruby's mahjong app, and commentary in Japanese made by rules
58
+ from what changed between two views (a round's start, the oorasu,
59
+ riichi and its waits, calls, a dora or red five discarded, a new dora,
60
+ the wall at 10, a tenpai, a win with its yaku and points, a draw, the
61
+ lead changing hands, the end), shown as captions and read by the
62
+ browser's voice (Web Speech API) in a queue that drops the old small
63
+ lines first. Space pauses, M silences, H hides the hands; `--public`
64
+ keeps the hands from the page altogether; `--json` gives the lines on
65
+ standard output; `--table`, `--namespace` (a room's tables), `--port`,
66
+ `--no-open`. It only reads (never joins or calls a table) and goes on
67
+ when the host leaves and comes back.
68
+ - **The voice** (README "Commentary", "The voice"): the commentary speaks
69
+ with VOICEVOX on this machine (`--voicevox URL`, `VOICEVOX_URL`, default
70
+ `http://127.0.0.1:50021`), by default as ずんだもん (`--speaker`,
71
+ `--style`; the numbers looked up by name in `/speakers`). The fixed
72
+ phrases (riichi, the calls, ron and tsumo, the rounds, 満貫 to 役満, ...)
73
+ are WAV files made by the user's own engine from the gem's list
74
+ (`commentate/mahjong_voice.yml`) into
75
+ `$XDG_CACHE_HOME/asterism-games/voices/<speaker>-<style>/`, named by the
76
+ phrasing's digest (`asterism-games commentate voices --make`, and the
77
+ missing ones at the start; `voices --list` lists the characters). The
78
+ rest of each line is synthesised as it comes and served from the
79
+ command's web server, kept in a bounded cache. The page says it all in
80
+ one queue (the fixed phrase, then the line; captions as the voice
81
+ starts; the old small lines dropped first) and shows the credit
82
+ `VOICEVOX:ずんだもん`. ずんだもん's lines are in her tone (〜なのだ),
83
+ kept as a tone of the phrasings (`MahjongPhrases::TONES`). Without an
84
+ engine the browser's voice reads as before. The engine gets the speech
85
+ only (サンマン, 30フ 2ハン), with the words it misreads written out
86
+ (流局, 起家, 跳満: `MahjongPhrases::SPEECH_READ`). No sound is in the gem;
87
+ `net/http` only, no new gem.
88
+ - **The AI's one-liners** (README "Commentary", "The AI's one-liners"):
89
+ at a riichi, a win, a draw, the lead changing hands, the oorasu, the end
90
+ and after a quiet minute, Claude (`claude-haiku-5-5` by default,
91
+ `--ai-model`) adds a short line after the fixed commentary, in the
92
+ voice's tone, marked AI on the page. At most one call every 20 s (8 s
93
+ for a scene bigger than the last one asked about: a win after a riichi),
94
+ one at a time, only while a page is open. A quiet minute is about one
95
+ topic the command picks (the lead, a dama tenpai with the hands shown,
96
+ the dora, the wall, the last discard). Thinking off, effort low,
97
+ `max_tokens` 200; a cut answer is dropped with its stop reason in the
98
+ log. The fixed part of the ask (the role,
99
+ the rules in short, the tone, the length, whether the hands may be
100
+ told) is cached; the table now and the lines that just came follow.
101
+ With `--public` or a page's H the AI gets no hand. An answer whose round
102
+ or phase is over, or that comes after 15 s, is dropped; on the page an
103
+ AI's line comes after the fixed ones and goes first when the voice
104
+ falls behind. The money is counted from the tokens (cache writes and
105
+ reads too) in `$XDG_STATE_HOME/asterism-games/commentary_ai_usage.json`
106
+ (`AI::Budget`), shown in the bar, and capped at `--ai-daily-usd`
107
+ (default 3 USD a day). `ANTHROPIC_API_KEY`; the anthropic gem stays
108
+ outside the gem's dependencies (loaded only with a key);
109
+ `ASTERISM_GAMES_AI_FAKE=1` tries it without one. `--ai off` turns it off.
110
+ - The gem holds the page (HTML, JS, CSS) and the two tile sheets
111
+ `tiles_hand.bmp` and `tiles_river.bmp`, copied byte for byte from
112
+ fmruby-core; in the gem they are MIT (`TILES_LICENSE.txt`). The gem's
113
+ test checks they are there and unchanged.
114
+
6
115
  ## 0.18.0
7
116
 
8
117
  - **Spectate** (README "Spectate"): the host puts a table's spectate view
data/README.md CHANGED
@@ -60,9 +60,10 @@ the console ("Permanent tables"; the server's setup: docs/install.md).
60
60
  | Path | What |
61
61
  |---|---|
62
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` |
63
+ | `bin/asterism-games` | the host (loads the framework and the games); `status`; `play <game>`, a table in this terminal ("Playing in a terminal"); `commentate <game>`, a table in the browser with commentary ("Commentary"); `help` |
64
64
  | `bin/mahjong-bots` | players without screens, for trying a mahjong table (one process each; not in the gem) |
65
65
  | `lib/asterism_games/play.rb`, `lib/asterism_games/play/` | the terminal clients ("Playing in a terminal"): the options, `othello.rb`, `mahjong.rb` with `mahjong_text.rb` (tiles as text, widths, reading the input) and `mahjong_screen.rb` (the screen); they need neither the framework nor riichi_engine |
66
+ | `lib/asterism_games/commentate.rb`, `lib/asterism_games/commentate/` | `asterism-games commentate mahjong` ("Commentary"): the events and their words (`mahjong_events.rb`, `mahjong_phrases.rb`), the hub and the runner (`mahjong.rb`), the web server (`server.rb`), the voice (`voicevox.rb`, VOICEVOX's HTTP; `voice.rb`, the fixed phrases' files and the lines' sound; `mahjong_voice.yml`, the fixed phrases), the page and the tile sheets (`assets/`) |
66
67
  | `lib/asterism_games.rb` | the framework, without any game |
67
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") |
68
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` |
@@ -89,7 +90,9 @@ the console ("Permanent tables"; the server's setup: docs/install.md).
89
90
  game host of your own: the framework, the two sample games that need
90
91
  nothing else (othello, the drawing board), `bin/asterism-games` (with
91
92
  `play`, the terminal clients of othello and mahjong: a mahjong table needs
92
- riichi_engine, its client does not). It depends on
93
+ riichi_engine, its client does not; and `commentate`, a mahjong table watched
94
+ in the browser with commentary, with the tile sheets of Family mruby's
95
+ mahjong app). It depends on
93
96
  the asterism gem only (0.7.1 or later in
94
97
  0.x; what came after 0.7.1, such as `@status` and `on_app_join`, is used
95
98
  when there). Not in it: mahjong (`app/`, it needs the riichi_engine fork
@@ -473,7 +476,8 @@ asterism/games/games/@status
473
476
 
474
477
  `last` is the four in rank order as `[name, rank, score]`, by the names
475
478
  the table shows its members anyway (a table app shows them to whoever
476
- joins); it is there once a game has ended. The rooms' status file
479
+ joins); it is there once a game has ended. During a watch game the
480
+ table's entry has `"watch" => true` (games 0.20.0). The rooms' status file
477
481
  (`GET /status` of the rooms' host) stays counts only.
478
482
 
479
483
  `ais` counts the AI players' seats (they are counted in `cpus` too), and
@@ -698,6 +702,7 @@ connects again after 3 s; while a client of a router only reconnects
698
702
  | `stand` | token | `"ok"` or why not (lobby only) |
699
703
  | `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) |
700
704
  | `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 |
705
+ | `watch` | token | the owner: a watch game ("Watch games", games 0.20.0): the four seats CPUs or AI players, nobody seated, Claude in one seat at most; `"ok"` or why not (`"not owner"`, `"not waiting"`, `"seats"`, `"human"`, `"ai limit"`) |
701
706
  | `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"`) |
702
707
  | `view` | token, assist | the member's view (below), or `{"error" => "token"}`; assist: the help its screen shows (Assist, 0-3) |
703
708
  | `act` | token, sv, type, tiles | `"ok"`, or why not: `"token"`, `"not seated"`, `"stale"` (sv is not the current one), `"not offered"`, `"not playing"` |
@@ -709,8 +714,9 @@ connects again after 3 s; while a client of a router only reconnects
709
714
 
710
715
  - People join a table and wait in its lobby (`"st" => "wait"`): a member
711
716
  sits in a seat or watches. The first member who sat is the owner (when the
712
- owner stands or leaves, the member seated longest takes over). The owner
713
- fills empty seats with CPUs and starts. At the end of a game the owner's
717
+ owner stands or leaves, the member seated longest takes over). While
718
+ nobody is seated the owner may be a member without a seat (games 0.20.0,
719
+ "Watch games"). The owner fills empty seats with CPUs and starts. At the end of a game the owner's
714
720
  next (or `over_seconds`) brings the table back to the lobby with the same
715
721
  seats, for another game.
716
722
  - The node that calls is the caller in the call's key (`from:`, filled
@@ -970,6 +976,47 @@ going for everyone (the other away seats are played for as before). A
970
976
  table of CPUs only (`humans: 0`, the permanent spectator tables) is never
971
977
  abandoned.
972
978
 
979
+ ### Watch games
980
+
981
+ A game of CPUs and AI players only, which the people at the table watch
982
+ (games 0.20.0): the owner fills the four seats and starts it without
983
+ sitting down, and a commentary (`commentate mahjong`, "Commentary") can
984
+ follow it like any other table.
985
+
986
+ - **The owner without a seat.** A member that joins a table where nobody
987
+ is seated and nobody is the owner is the owner (`join`'s `"owner"`, the
988
+ member's own view's `"mo"`). When somebody sits, the first to sit is the
989
+ owner as before: whenever a member is seated the role is where it always
990
+ was (the seated member who sat first, "Players that drop out"). An owner
991
+ who stands up with nobody else seated keeps the role; an owner who
992
+ leaves with nobody seated hands it to the member who joined first, of
993
+ those there. An older table app, which knows only `"ow"` (the owner's
994
+ seat), shows an owner without a seat as no owner, as it showed a lobby
995
+ without one before.
996
+ - **Starting.** `watch(token)`, the owner, in the lobby: the four seats
997
+ are CPUs or AI players (`cpu`), no member is seated, and at most one seat
998
+ is Claude's (`WATCH_AI_MAX`: Claude 1, Jev 4). The lobby view's `"wc"`
999
+ tells the screens whether it may start (1), or the seats are all CPUs
1000
+ and AI players but over the AI limit (2), or not (0); a host before
1001
+ 0.20.0 has no `"wc"`, and the screens offer no watch game then. The
1002
+ seats are drawn as for `start`. The AI players' money is as for any
1003
+ game ("AI players"): a kind past its day's limit becomes a CPU.
1004
+ - **During it.** Nobody is seated, so nobody is played for and the
1005
+ table's abandoning does not apply ("Players that drop out"); the results
1006
+ move on by themselves as at a table of CPUs (`result_seconds`), and the
1007
+ end of the game after `over_seconds`. The members watch with the four
1008
+ hands ("Spectators", the spectator delay too). Views have `"wm" => 1`
1009
+ and the host's `@status` has `"watch" => true` for the table ("Status").
1010
+ When every member has left the table (`leave`, or forgotten after being
1011
+ away six times `away_seconds`) the game stops: back to the lobby, an
1012
+ unfinished game not recorded. The log says `watch game started (...)`
1013
+ and `watch game stopped: nobody at the table (not recorded)`.
1014
+ - **One game.** At the end the table goes back to the lobby with the same
1015
+ seats; the owner starts another with `watch` again.
1016
+ - **Records.** The game is recorded as any other with `"watch" => true`
1017
+ (in `records`' entries and in `record(id)`). Its seats count in the
1018
+ stats for their kinds (CPU, Claude, Jev), never for a person ("Stats").
1019
+
973
1020
  ### Views
974
1021
 
975
1022
  A Hash (MessagePack); tiles are one byte each in binary strings (bin).
@@ -983,7 +1030,9 @@ A Hash (MessagePack); tiles are one byte each in binary strings (bin).
983
1030
  | `sk` | the four seats: 0 empty, 1 a member, 2 a CPU (an AI player too) |
984
1031
  | `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") |
985
1032
  | `aw` | the seats whose member is away, as bits |
986
- | `ow` | the owner's seat (-1: nobody seated) |
1033
+ | `ow` | the owner's seat (-1: nobody seated, or an owner without a seat) |
1034
+ | `mo` | the member's own view only: 1 when this member is the owner, seated or not (games 0.20.0, "Watch games") |
1035
+ | `wc`, `wm` | in the lobby, whether a watch game may start: 1 yes, 2 the seats are all CPUs and AI players but over the AI limit, 0 no; during a watch game `wm` 1 ("Watch games", games 0.20.0) |
987
1036
  | `mb` | the members in the order they joined: `[name, seat (-1 none), flags (1 away, 2 owner)]` |
988
1037
  | `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)]` |
989
1038
  | `sc`, `ri` | the scores; riichi as bits |
@@ -1168,7 +1217,7 @@ assistant through `asterism mcp`).
1168
1217
 
1169
1218
  | Method | Arguments | Returns |
1170
1219
  |---|---|---|
1171
- | `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) |
1220
+ | `records` | from (optional, 0) | `{"records" => [...], "total", "next"}`: the games, newest first, from the from-th, as many as fit one message; `next` is the from of the next page (none: the end). Each: `id`, `at` (the host's local time, `"2026-10-18 21:05"`), `game` (`hanchan` / `tonpuu`), `names`, `ranks`, `scores` (in seat order), `watch` (`true` for a watch game, "Watch games"; else absent) |
1172
1221
  | `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]], ...]`) |
1173
1222
  | `record` | id, round | round `round` (from 0) as below, with `id`, `round`, `of` (how many rounds), `part`, `parts` |
1174
1223
  | `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 |
@@ -1328,7 +1377,9 @@ pipe or a file (and the tests) they read lines as before, and
1328
1377
  | `1`..`4` | the lobby: sit there (an empty seat or a CPU's) |
1329
1378
  | `S` | the lobby: stand |
1330
1379
  | `C` | the lobby, the owner: CPUs into the empty seats |
1380
+ | `A`, then `1`..`4` | the lobby, the owner: that seat's next player: an empty seat a CPU, a CPU the first AI player offered, an AI player the next one, the last back a CPU (games 0.20.0; another key gives it up) |
1331
1381
  | Enter | the lobby, the owner: start (four seats taken) |
1382
+ | `W` | the lobby, the owner (seated or not): a watch game, the four seats CPUs or AI players ("Watch games", games 0.20.0) |
1332
1383
 
1333
1384
  | Othello | |
1334
1385
  |---|---|
@@ -1457,12 +1508,297 @@ Typed (in a pipe or a file each line; on a terminal after `:`):
1457
1508
  | `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") |
1458
1509
  | `tonpuu`, `hanchan`, `rule ITEM VALUE` | the owner: the game's length, any local rule ("Local rules") |
1459
1510
  | `start` | the owner: start |
1511
+ | `watch` | the owner, seated or not: a watch game ("Watch games", games 0.20.0); `--commands "cpu,ai 1 claude,watch"` from a client that does not sit |
1460
1512
  | `auto` | the client plays for you: wins when offered, passes calls, discards the drawn tile, OKs the results (again to stop) |
1461
1513
  | `tables`, `view`, `help`, `quit` | |
1462
1514
 
1463
1515
  The host's answers are shown in the table app's words (`今はできない`,
1464
1516
  `卓の主だけができる` ...).
1465
1517
 
1518
+ ## Commentary
1519
+
1520
+ (games 0.19.0) `asterism-games commentate mahjong` watches a mahjong table
1521
+ from outside, the way a sports broadcast does: it reads the table's
1522
+ spectate key ("Spectate", with the four hands), draws the table in a
1523
+ browser with the tiles of Family mruby's mahjong app, and comments on what
1524
+ happens in Japanese, as captions under the table and in a voice: a
1525
+ VOICEVOX character's (ずんだもん by default) when a VOICEVOX engine runs on
1526
+ this machine, else the browser's own (the Web Speech API; "The voice"
1527
+ below). It only reads: it never joins the table or calls it. CRuby and
1528
+ the asterism gem only; it runs on WSL and on macOS.
1529
+
1530
+ ```
1531
+ asterism-games commentate mahjong --router tcp/127.0.0.1:7447 # a host on this machine or the home network
1532
+ asterism-games commentate mahjong --credentials viewer.tar # a console's network (a viewer's or a spectator's .tar)
1533
+ asterism-games commentate mahjong --credentials viewer.tar --namespace room/01 # a room's tables
1534
+ asterism-games commentate mahjong --public # for a stream: the hands never reach the page
1535
+ asterism-games commentate mahjong --json | tee commentary.jsonl # the lines for another tool too
1536
+ ```
1537
+
1538
+ It starts a small web server on `127.0.0.1` (port 7470, the next free one
1539
+ when it is taken), prints the address and opens it in the browser (macOS
1540
+ `open`; WSL `wslview`, else Windows' `start`; elsewhere `xdg-open`).
1541
+
1542
+ | Option | |
1543
+ |---|---|
1544
+ | `--router LOCATOR`, `--credentials FILE` | as `play` (with neither, `ASTERISM_ROUTER` / `ASTERISM_CREDENTIALS`, all or nothing; with nothing, `tcp/127.0.0.1:7447`) |
1545
+ | `--namespace PREFIX` | a room's tables: `room/01` shows `room01-<table>` (the spectate keys are outside the room, so the connection has no namespace) |
1546
+ | `--table TABLE` | the table to show first: `mahjong1`, `games/mahjong1` or `games/spectate/mahjong1`; without it, every table that puts its spectate key is offered and the first one shown |
1547
+ | `--node NODE` | the node ID (default `commentate-<host>-<pid>`, or the `.tar`'s name) |
1548
+ | `--port N` | the page's port (default 7470) |
1549
+ | `--public` | hide the hands: the views sent to the page lose `hand` and `drawn`, and no line tells a hand |
1550
+ | `--no-open` | do not open the browser |
1551
+ | `--json` | each line of commentary as a JSON line on standard output (`at`, `table`, `type`, `prio`, `text`; an AI's line has `type` `ai` and `ai: true`) |
1552
+ | `--voicevox URL` | the VOICEVOX engine (default `VOICEVOX_URL`, else `http://127.0.0.1:50021`) |
1553
+ | `--speaker NAME`, `--style NAME` | the VOICEVOX character and its style (default `ずんだもん`, `ノーマル`) |
1554
+ | `--ai on\|off` | the AI's one-liners (default `on`; they need `ANTHROPIC_API_KEY` and the anthropic gem; "The AI's one-liners") |
1555
+ | `--ai-model MODEL` | the model they ask (default `claude-haiku-5-5`) |
1556
+ | `--ai-daily-usd USD` | what they may cost a day (default 3); then the fixed commentary alone |
1557
+
1558
+ - **The role.** The spectate keys are `asterism/<host>/spectate/<table>/state`.
1559
+ On a console's network a `viewer` (Read `asterism/**`) or a `spectator`
1560
+ (Read `asterism/*/spectate` and `asterism/*/spectate/**`) reads them; the
1561
+ players' role `games` does not. On a plain zenohd anyone does.
1562
+ - **The page.** A bar of states on top (the table, the voice, the delay the
1563
+ host puts on the table, a host that is not there), the table in a
1564
+ canvas (the round, honba, riichi sticks, the wall and the dora
1565
+ indicators; for each seat its wind, name, points, riichi, river with the
1566
+ riichi tile sideways and the called tiles pale, hand and drawn tile,
1567
+ melds; the seat to act framed), the captions below with the time of
1568
+ each, the newest at the bottom. Two seats to a row in a wide window, one
1569
+ in a narrow one (a phone). Keys: **Space** pauses (what comes meanwhile
1570
+ is held and shown when it goes on), **M** turns the voice off and on,
1571
+ **H** hides the hands on that page (backs instead of the tiles, and the
1572
+ lines that tell a hand left out). A table that several hosts put is
1573
+ chosen in the bar.
1574
+ - **The AI's one-liners.** Claude's short lines at the big moments,
1575
+ marked AI, after the fixed ones; the bar shows the model and today's
1576
+ money. "The AI's one-liners" below.
1577
+ - **The voice.** One queue on the page says the lines in turn, each
1578
+ caption coming up as its voice starts; when the voice falls behind, the
1579
+ old small lines are dropped (their captions still come up) and a win or
1580
+ a big moment is said next. With VOICEVOX, a line plays its fixed phrase
1581
+ first (リーチ, ポン, ロン, the round's name ...) and then the rest of it,
1582
+ synthesised meanwhile; without, the browser reads the lines with a
1583
+ Japanese voice when it has one (the bar says which voice, or that it has
1584
+ none). Browsers play sound only after the page has been clicked once:
1585
+ the page says so until then. "The voice" below.
1586
+ - **The commentary.** Made by rules from the difference between one view
1587
+ and the one before (pure functions, `commentate/mahjong_events.rb` and
1588
+ `mahjong_phrases.rb`, so the host can take them over later), each event
1589
+ in two to three phrasings chosen at random, the big moments in strong
1590
+ ones: the first look at the table, a round's start (the dealer and the
1591
+ dora; the game's first; the oorasu, and the oorasu going on), a discard
1592
+ of a dora or a red five, a call (チー, ポン, the kans), a new dora, the
1593
+ wall at 10, riichi with its waits, a tenpai (from the hand, once a round
1594
+ for each seat), a win with its yaku and points (満貫 and over: strong),
1595
+ a draw with who was tenpai, an abortive draw, the lead changing hands,
1596
+ the end with the ranks. The tiles and the yaku are read as a player
1597
+ says them (サンマン, リーチ, タンヤオ). With the hands hidden (`--public`,
1598
+ H) a tenpai is not said and a riichi is said without its waits.
1599
+ - **It does not stop.** A host that leaves (`on_leave`, or no put for 15 s)
1600
+ is shown as gone and comes back with its next put; a new round or a new
1601
+ game goes on as before. The delay of the table is the host's
1602
+ ("Spectators"); the page shows it and adds none.
1603
+ - **The web server.** HTTP by hand on the standard library's `socket`
1604
+ (no gem): the page, its script and style, the two tile sheets and
1605
+ `/events`, a stream of Server-Sent Events (`status`, `state`, `line`)
1606
+ that pushes the table and the commentary. It listens on `127.0.0.1`
1607
+ only, answers GET (and HEAD) on those paths only, and refuses a request
1608
+ whose Host is not `127.0.0.1` or `localhost`. The page loads nothing from
1609
+ elsewhere.
1610
+ - **On a stream (OBS).** Add a Browser Source with the address
1611
+ (`http://127.0.0.1:7470/`), 1280 x 720 or larger. `?obs=1` leaves out the
1612
+ bar and the keys' line and makes the background transparent around the
1613
+ captions; `?hands=0` hides the hands on that page, `?voice=0` silences
1614
+ it, `?table=games/mahjong1` picks the table. Start the command with
1615
+ `--public` when the stream must never show a hand. OBS's browser does not
1616
+ always play the Web Speech voice into the stream: for the voice, capture
1617
+ a browser window and the desktop's sound instead.
1618
+ - **The tiles.** `lib/asterism_games/commentate/assets/tiles_hand.bmp` and
1619
+ `tiles_river.bmp` are the hand-drawn sheets of Family mruby's mahjong app
1620
+ (fmruby-core's `flash/app/game/mahjong/`), copied byte for byte; the
1621
+ browser reads the BMPs as they are and only makes their transparent
1622
+ colour (magenta) transparent when it draws them. In this gem they are
1623
+ under the MIT License like the rest of it, by the author's decision
1624
+ (`TILES_LICENSE.txt` next to them).
1625
+
1626
+ ### The voice
1627
+
1628
+ (games 0.19.0) The commentary speaks with VOICEVOX,
1629
+ a free Japanese speech synthesiser, when its engine runs on this machine;
1630
+ the default character is ずんだもん (style ノーマル). Nothing of its voice is
1631
+ in this gem: the gem holds the words (`commentate/mahjong_voice.yml`) and
1632
+ the code that has your VOICEVOX say them. **VOICEVOX is a separate app
1633
+ you install and start yourself** (https://voicevox.hiroshiba.jp/, free);
1634
+ without it the commentary still runs, with the browser's voice. When it
1635
+ finds no engine, the command says so on the terminal, with where to get
1636
+ VOICEVOX, and so does the page's bar.
1637
+
1638
+ - **Finding the engine.** At its start the command asks
1639
+ `GET /version` of `--voicevox URL` (default `VOICEVOX_URL`, else
1640
+ `http://127.0.0.1:50021`). When nothing answers, the browser's voice
1641
+ reads, as before, the terminal says where to get VOICEVOX, and the
1642
+ page's bar says 「VOICEVOX が見つからない (無料のアプリ VOICEVOX を入れて
1643
+ 起動すると、ずんだもんの声になる): ブラウザの声で読む」.
1644
+ - **The fixed phrases** (リーチ, チー, ポン, カン, ロン, ツモ, the rounds'
1645
+ names, 流局, 途中流局, 満貫 to 役満, オーラス, 逆転, 終局, 対局開始, and
1646
+ 「実況を始めます」 when the page is clicked) are WAV files, played at
1647
+ once. The command makes the missing ones at its start (in the
1648
+ background: a phrase not made yet is skipped), or
1649
+ `asterism-games commentate voices --make` makes them all now (about 30
1650
+ files, half a minute on a CPU engine). They go to
1651
+ `$XDG_CACHE_HOME/asterism-games/voices/<speaker>-<style>/`
1652
+ (`~/.cache/asterism-games/voices/ずんだもん-ノーマル/` by default) with a
1653
+ `CREDITS.txt`; each is named by the digest of its phrasing, so a changed
1654
+ list makes the changed ones again and removes the old. **The directory
1655
+ can be deleted at any time**: it is made again.
1656
+ - **The rest of a line** is synthesised as the line comes (the engine's
1657
+ `/audio_query` and `/synthesis`) while its fixed phrase plays, and
1658
+ served to the page from the command's own web server at a temporary
1659
+ address (`/voice/<digest>.wav`; only lines the command made, never a
1660
+ text a page sends). A line that begins with its fixed phrase's word
1661
+ leaves it out (ロン! then 「CPU1、断么九 …」). The same line is
1662
+ synthesised once while it is kept (200 lines and 64 MB at most, the
1663
+ least recently used dropped first). A line the engine fails on is read
1664
+ by the browser's voice.
1665
+ - **What the engine reads** is always a line's speech, never its caption:
1666
+ the tiles, the yaku and the points as a player says them (サンマン,
1667
+ リーチ, 30フ 2ハン), and the words VOICEVOX would read wrongly written
1668
+ out (流局 リューキョク, 起家 チーチャ, 跳満 ハネマン;
1669
+ `MahjongPhrases::SPEECH_READ`, used for the fixed phrases too). No user
1670
+ dictionary is needed.
1671
+ - **Choosing a character.** `asterism-games commentate voices --list`
1672
+ lists the engine's characters and their styles; `--speaker NAME
1673
+ --style NAME` picks one by name (the engine's numbers are looked up,
1674
+ never assumed; a name that is not there is answered with the ones that
1675
+ are). ずんだもん speaks in her own tone: the lines and the fixed phrases
1676
+ end in 〜なのだ (「リーチなのだ!」「満貫、8000点なのだ!」); any other
1677
+ character speaks plainly. The tones are kept with the phrasings
1678
+ (`MahjongPhrases::TONES`), with a note on how to ask an AI to speak so.
1679
+ - **The page** plays the sound itself (never the PC with `paplay` or
1680
+ `afplay`), so a Browser Source in OBS carries it. **M** cuts the voice
1681
+ (the captions go on). When VOICEVOX speaks, the page shows the credit
1682
+ in its corner: `VOICEVOX:ずんだもん`.
1683
+
1684
+ Running VOICEVOX:
1685
+
1686
+ ```
1687
+ # Windows (and WSL): install the VOICEVOX app on Windows and start it (its engine
1688
+ # listens on 127.0.0.1:50021). From WSL with networkingMode=mirrored (.wslconfig)
1689
+ # 127.0.0.1 reaches it; otherwise pass Windows' address:
1690
+ asterism-games commentate mahjong --voicevox http://$(ip route show default | awk '{print $3}'):50021
1691
+
1692
+ # macOS: install the VOICEVOX app and start it; the default URL reaches it.
1693
+
1694
+ # docker (Linux, WSL, macOS): the engine alone, CPU version
1695
+ docker run -d --name voicevox -p 127.0.0.1:50021:50021 voicevox/voicevox_engine:cpu-0.25.0
1696
+ asterism-games commentate voices --list
1697
+ asterism-games commentate voices --make # optional: the commentary makes them at its start too
1698
+ asterism-games commentate mahjong --speaker 四国めたん --style ノーマル
1699
+ ```
1700
+
1701
+ The engine takes a gigabyte or two of memory; one line of commentary
1702
+ takes it about a second or two on a laptop's CPU.
1703
+
1704
+ **VOICEVOX's terms, in short** (read the current terms of VOICEVOX and of
1705
+ your character before you publish anything; they can change):
1706
+
1707
+ - The software and the voices it makes may be used free of charge,
1708
+ commercially or not, **with a credit** naming the character, such as
1709
+ `VOICEVOX:ずんだもん`. Each character has terms of its own on top of
1710
+ VOICEVOX's (ずんだもん's: a credit where it can be found, in a video's
1711
+ description or on screen; no use against public order, for political or
1712
+ religious campaigns, for deception, and the like).
1713
+ - **When you stream or upload the commentary, put the credit in the
1714
+ description too** (the page shows it in its corner).
1715
+ - Whoever you give generated voices to must follow the same terms, so
1716
+ they cannot be mixed into an MIT package. That is why this gem ships no
1717
+ sound: each user makes the files with their own VOICEVOX, and they stay
1718
+ on that machine.
1719
+
1720
+ ### The AI's one-liners
1721
+
1722
+ (games 0.19.0) At the big moments of a table Claude adds a short line of
1723
+ its own after the fixed commentary, in the same voice and tone (ずんだもん's
1724
+ 〜なのだ with her voice), marked **AI** in the captions:
1725
+ 「この待ち、山にまだ残ってるのだ。勝負はここからなのだ!」. It is also a
1726
+ small demonstration of an AI watching a table on the network and telling
1727
+ what it sees.
1728
+
1729
+ ```
1730
+ export ANTHROPIC_API_KEY=sk-ant-... # your own key
1731
+ gem install anthropic # not a dependency of this gem
1732
+ asterism-games commentate mahjong # Claude Haiku, at most 3 USD a day
1733
+ asterism-games commentate mahjong --ai-model claude-sonnet-5-5 --ai-daily-usd 1
1734
+ asterism-games commentate mahjong --ai off # the fixed commentary alone
1735
+ ```
1736
+
1737
+ - **The key and the gem.** The key is `ANTHROPIC_API_KEY`; the anthropic
1738
+ gem is loaded only when it is set, and is not a dependency of this gem.
1739
+ Without a key the page's bar says 「AI: off (キーなし)」; with a key and
1740
+ no gem, 「AI: off (anthropic の gem なし)」 and the command says to
1741
+ `gem install anthropic`. `--ai off` turns them off.
1742
+ - **The model.** `claude-haiku-5-5` by default (`--ai-model`), asked with
1743
+ thinking off at effort `low` and `max_tokens` 200 (two short sentences
1744
+ with room; an answer cut there is dropped and the log says its stop
1745
+ reason), one try with no retries, 8 s at most. Every call's line in the
1746
+ log has its seconds, stop reason, tokens and money.
1747
+ - **When.** A riichi, a win (a mangan or more counts as bigger), a draw or
1748
+ an abortive draw, the lead changing hands, the last round (オーラス),
1749
+ the end; and a quiet minute in the middle of a round, about one topic
1750
+ the command picks in turn (the leader's lead, a seat in tenpai without
1751
+ riichi when the hands are shown, the dora, the wall left, the last
1752
+ discard; never the same twice running). One call at a
1753
+ time and **at most one every 20 s**, or every 8 s for a scene bigger
1754
+ than the last one asked about (a win after a riichi, the end after a
1755
+ win; the ranks: the end, a big win and the lead changing hands, a win
1756
+ and the oorasu, a draw, a riichi, a quiet minute). A scene that comes
1757
+ sooner waits (the biggest of those waiting, for 15 s at most) and is let
1758
+ go when its round or phase is over. Calls are made only while a page is
1759
+ open (or with `--json`): nobody watching, nothing is spent. A tonpuu of
1760
+ four CPUs gets about two a round, and a line for its wins.
1761
+ - **What it is told.** A fixed part, the same to the byte every time and
1762
+ cached (prompt caching: the calls after the first read it at a tenth of
1763
+ the price): the commentator's role, mahjong's rules in short, the tone,
1764
+ the length (40 characters, one or two sentences, the line only: no
1765
+ preamble, quotes or emoji) and whether it may speak of the hands. Then
1766
+ the table now (the round, honba, riichi sticks, the wall, the dora; each
1767
+ seat's name, wind, points, riichi, melds and last discards; with the
1768
+ hands shown, each hand and its waits; a win's yaku and points, the
1769
+ ranks at the end), the scene, the fixed lines that just came, and its
1770
+ own last lines (not to repeat itself).
1771
+ - **The hands hidden.** With `--public`, and while a page has **H** on,
1772
+ the AI is not told the hands (nor the waits, nor who is tenpai): it
1773
+ speaks from what everyone can see. A line made while the hands were
1774
+ shown is not shown on a page that hides them.
1775
+ - **Late lines are dropped.** An answer that comes when its round or
1776
+ phase is over, or more than 15 s after it was asked, is not said. On
1777
+ the page an AI's line waits behind the fixed lines (its scene's own come
1778
+ first; a round's start waits behind it), and is dropped when a new scene
1779
+ comes before it is said (a riichi, a win, a draw), first when the voice
1780
+ falls behind, and after 40 s in the queue.
1781
+ - **The voice.** The caption is the line as it came; the voice reads it
1782
+ through the same readings as the fixed lines (`SPEECH_READ` and the
1783
+ tiles: 三萬 サンマン, 流局 リューキョク).
1784
+ - **What it costs.** Counted from the tokens the API reports (input,
1785
+ output, the cache's writes and reads) at the model's published price
1786
+ (Haiku 5.5: 0.10 USD a million input tokens, 0.50 output, 0.125 a cache
1787
+ write, 0.01 a cache read), in
1788
+ `$XDG_STATE_HOME/asterism-games/commentary_ai_usage.json`
1789
+ (`~/.local/state/...` by default; counts and money only, no words). The
1790
+ bar shows today's money and the limit; when today's money reaches
1791
+ `--ai-daily-usd` (default 3 USD) no call is made and the fixed
1792
+ commentary goes on alone; the next day (local date) starts at 0. A
1793
+ call costs about a hundredth of a cent with Haiku (some 1,000 cached
1794
+ tokens, a few hundred new ones, a line out), so a tonpuu costs well
1795
+ under a cent.
1796
+ - **Failures** (no answer in time, an error, a refusal) are one line in
1797
+ the command's output and nothing is said; the key and the words are
1798
+ never written out. `ASTERISM_GAMES_AI_FAKE=1` puts a fake in Claude's
1799
+ place (no API is called; its money is counted apart, in
1800
+ `commentary_ai_usage.fake.json`), to try the page without a key.
1801
+
1466
1802
  ## Playing from a program
1467
1803
 
1468
1804
  The mahjong table also speaks to programs (games 0.10.0): an AI assistant
@@ -1538,9 +1874,12 @@ offered), and show the seat by its name (`Claude`, `Jev`, numbered when there ar
1538
1874
  reports, per day (the host's local date), in `state_dir/ai_usage.json`
1539
1875
  (shared by the rooms' hosts and the permanent tables; counts and money
1540
1876
  only). When today's total reaches `ASTERISM_GAMES_AI_DAILY_USD` (3.0) no
1541
- table offers an AI player any more and the seated ones become CPUs; a
1542
- game asks at most `ASTERISM_GAMES_AI_CALLS_PER_GAME` (400) decisions,
1543
- then its AI seats become CPUs. `bin/asterism-games status` prints the
1877
+ table offers an AI player any more and the seated ones become CPUs; each
1878
+ AI seat asks at most `ASTERISM_GAMES_AI_CALLS_PER_GAME` (400) decisions
1879
+ in a game, then that seat becomes a CPU (games 0.20.0: the count is each
1880
+ seat's own, so a game's most is 400 times its AI seats; before, the
1881
+ seats of a game shared the 400 and every AI seat stopped together). A
1882
+ tonpuu takes about 150-200 decisions a seat, a hanchan about twice that. `bin/asterism-games status` prints the
1544
1883
  day's decisions, tokens and money per kind; the host's `@status` has the
1545
1884
  totals (`ai_today`).
1546
1885
  - **A limit per kind** (games 0.15.0). `ASTERISM_GAMES_AI_DAILY_USD_CLAUDE`
@@ -1553,7 +1892,7 @@ offered), and show the seat by its name (`Claude`, `Jev`, numbered when there ar
1553
1892
  0.15.0. A value that is not a number of dollars (0 or more) is ignored,
1554
1893
  with a line in the log. The log says the limits at the start
1555
1894
  (`games: AI players: claude, jev (at most $10.0 a day for claude, $2.0
1556
- for jev, 400 decisions a game)`) and once for each kind that stops
1895
+ for jev, 400 decisions a seat in a game)`) and once for each kind that stops
1557
1896
  (`today's limit for claude ($10.0) is reached`); a new day lets it play
1558
1897
  again. `ai_today` on `@status` then has `kinds` (README "Status").
1559
1898
  - **Prices.** USD per million tokens, settings with the published prices
@@ -1617,8 +1956,9 @@ game between two machines without a host is fmruby-core's
1617
1956
  "moves" => [{"n", "square", "turns", "text"}]}`; the answer is a move's
1618
1957
  `n`. One out of range, one that does not come in `ai_turn_seconds` (30 s)
1619
1958
  or a failure is played by the CPU, and the game goes on. When the day's
1620
- money is spent, or the game has asked `ASTERISM_GAMES_AI_CALLS_PER_GAME`
1621
- decisions, Jev's seat becomes the CPU. The table apps show Jev by its
1959
+ money is spent, or the seat has asked `ASTERISM_GAMES_AI_CALLS_PER_GAME`
1960
+ decisions in the game (each seat its own count, games 0.20.0), Jev's
1961
+ seat becomes the CPU. The table apps show Jev by its
1622
1962
  name with `[AI]` and say when it is thinking; `dl` counts its time down.
1623
1963
  - Nothing in othello is hidden, so every member sees the same view: the
1624
1964
  table's view (the one the host puts on the state key) and `me`, the
@@ -1901,6 +2241,34 @@ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 bundle exec rake test # also through a
1901
2241
  tonpuu game against three CPUs each to the end from scripted input
1902
2242
  (letters, tiles, calls, passes, OKs; `PLAY_TRANSCRIPT_DIR` keeps what
1903
2243
  they showed).
2244
+ - `test_commentate.rb`: `asterism-games commentate` (in `rake test:gem`
2245
+ too): the waits of a hand (seven pairs, thirteen orphans, with melds),
2246
+ the dora of an indicator, each event from views made by hand (a round's
2247
+ start, the oorasu, riichi with its waits, calls and an added kan, a dora
2248
+ and a red five discarded, the wall, a new dora, a tenpai once a round, a
2249
+ big win and the lead changing hands, a draw, an abortive draw, the end)
2250
+ and their words, a recorded tonpuu of four CPUs
2251
+ (`test/fixtures/commentate_tonpuu.json.gz`) through the events and
2252
+ phrases, nothing of a hand with the hands hidden, the hub (the pages'
2253
+ frames, `--public`, `--json`, a page that opens late, a host that leaves
2254
+ and comes back), the web server (the page, the sheets byte for byte,
2255
+ 404 / 405 / a foreign Host refused, 127.0.0.1 only, the events stream, a
2256
+ port taken), the options and `--help`; with the table there, a CPU
2257
+ round played here through the hub.
2258
+ - `test_commentate_voice.rb`: the commentary's voice (in `rake test:gem`
2259
+ too), against a small VOICEVOX engine started in the test: a character
2260
+ and a style by name (and the names offered when one is not there), the
2261
+ synthesis, no engine (the browser's voice, nothing made), the fixed
2262
+ phrases made once and named by their digest, made again when the list
2263
+ changes (the old files removed), the cache directory, every clip a line
2264
+ asks for in the list in both tones, a line synthesised once and kept
2265
+ (the least recently used dropped, made again when asked), a failing
2266
+ engine, the words a fixed phrase said left out of the line, ずんだもん's
2267
+ tone (every phrasing, every line of the recorded game, the clips that
2268
+ tell no hand), the hub's lines with their sound, the web server's
2269
+ `/clip/` and `/voice/` (only what the list and the hub name), the
2270
+ missing phrases made at the start, the options and `voices --list |
2271
+ --make`.
1904
2272
  - `test_spectate.rb`: the spectate view (README "Spectate"; its
1905
2273
  framework part in `rake test:gem` too): nothing by default (othello, the
1906
2274
  board), the keys of the permanent tables and of a room, what the rooms
@@ -2094,4 +2462,7 @@ each result that long.
2094
2462
 
2095
2463
  ## License
2096
2464
 
2097
- MIT (LICENSE).
2465
+ MIT (LICENSE). The tile sheets of the commentary page
2466
+ (`lib/asterism_games/commentate/assets/tiles_*.bmp`, the hand-drawn tiles
2467
+ of Family mruby's mahjong app) are under the MIT License in this gem too
2468
+ (`TILES_LICENSE.txt`).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.18.0
1
+ 0.20.0