asterism-games 0.17.1 → 0.19.1

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: 74d2dd1c8e013edfbce9943fa6a725a5173f5acfdaabbc42d93907759fd287b4
4
- data.tar.gz: e8325208b814ef8e5e7a1d98286c828c287977f8698f1ccf049fd8a6917a4a04
3
+ metadata.gz: d10ded9e6550829d2169bb7be24ba662f11d69cec3eaf0fe676923104aa44727
4
+ data.tar.gz: '039b615f91c53f50c6bb10500b4e9516c7893bd213af57d1f7823c2bc67e84f5'
5
5
  SHA512:
6
- metadata.gz: 36982adb8c6655a83e6f5a007647f1fa814ad9719d6d4f92ceebee2e18131c5297b42576c24692ed8e388afa42f8a62b186a264bd1bec7a2d696565f5f03d74c
7
- data.tar.gz: 2102d3e914d962b470008e8ffce3b8d4366243aa5962b0b124f91e5fbdf993cd36e49e97af735c8f861e412465ac3d0af8b93c777ae5c3133e3b4356d1d3d48d
6
+ metadata.gz: c2571c2374914e2f9bb3d299e0482cd3f2f43b9c6ebe7fb4f5423b5c37c121cd5b06c06eb058ee91ecb5b05ddd0d6db22ce89e9b8b06eaf40cd3ab6ac6081f65
7
+ data.tar.gz: 260165d7df98b105ec8070f0c850d80f783b220cf4cf819c3a17de189c0ed7b0ccb78f43dc7ce29fe8f31f08aa4d4793c0eca1a2e600a265e52c61c67833852e
data/CHANGELOG.md CHANGED
@@ -3,6 +3,105 @@
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.19.1
7
+
8
+ - The commentary tells where to get VOICEVOX when it finds no engine:
9
+ the terminal says it is a free app installed and started apart from the
10
+ gem (with its home page), the page's bar says the same in Japanese, and
11
+ `commentate --help` and the README's "The voice" say it up front.
12
+
13
+ ## 0.19.0
14
+
15
+ - **Commentary** (README "Commentary"): `asterism-games commentate
16
+ mahjong` watches a table from its spectate key (0.18.0) and shows it in
17
+ the browser: a small web server on `127.0.0.1` (HTTP and Server-Sent
18
+ Events by hand on `socket`, no new gem; port 7470 or the next free one),
19
+ a page that draws the table in a canvas with the hand-drawn tile sheets
20
+ of Family mruby's mahjong app, and commentary in Japanese made by rules
21
+ from what changed between two views (a round's start, the oorasu,
22
+ riichi and its waits, calls, a dora or red five discarded, a new dora,
23
+ the wall at 10, a tenpai, a win with its yaku and points, a draw, the
24
+ lead changing hands, the end), shown as captions and read by the
25
+ browser's voice (Web Speech API) in a queue that drops the old small
26
+ lines first. Space pauses, M silences, H hides the hands; `--public`
27
+ keeps the hands from the page altogether; `--json` gives the lines on
28
+ standard output; `--table`, `--namespace` (a room's tables), `--port`,
29
+ `--no-open`. It only reads (never joins or calls a table) and goes on
30
+ when the host leaves and comes back.
31
+ - **The voice** (README "Commentary", "The voice"): the commentary speaks
32
+ with VOICEVOX on this machine (`--voicevox URL`, `VOICEVOX_URL`, default
33
+ `http://127.0.0.1:50021`), by default as ずんだもん (`--speaker`,
34
+ `--style`; the numbers looked up by name in `/speakers`). The fixed
35
+ phrases (riichi, the calls, ron and tsumo, the rounds, 満貫 to 役満, ...)
36
+ are WAV files made by the user's own engine from the gem's list
37
+ (`commentate/mahjong_voice.yml`) into
38
+ `$XDG_CACHE_HOME/asterism-games/voices/<speaker>-<style>/`, named by the
39
+ phrasing's digest (`asterism-games commentate voices --make`, and the
40
+ missing ones at the start; `voices --list` lists the characters). The
41
+ rest of each line is synthesised as it comes and served from the
42
+ command's web server, kept in a bounded cache. The page says it all in
43
+ one queue (the fixed phrase, then the line; captions as the voice
44
+ starts; the old small lines dropped first) and shows the credit
45
+ `VOICEVOX:ずんだもん`. ずんだもん's lines are in her tone (〜なのだ),
46
+ kept as a tone of the phrasings (`MahjongPhrases::TONES`). Without an
47
+ engine the browser's voice reads as before. The engine gets the speech
48
+ only (サンマン, 30フ 2ハン), with the words it misreads written out
49
+ (流局, 起家, 跳満: `MahjongPhrases::SPEECH_READ`). No sound is in the gem;
50
+ `net/http` only, no new gem.
51
+ - **The AI's one-liners** (README "Commentary", "The AI's one-liners"):
52
+ at a riichi, a win, a draw, the lead changing hands, the oorasu, the end
53
+ and after a quiet minute, Claude (`claude-haiku-5-5` by default,
54
+ `--ai-model`) adds a short line after the fixed commentary, in the
55
+ voice's tone, marked AI on the page. At most one call every 20 s (8 s
56
+ for a scene bigger than the last one asked about: a win after a riichi),
57
+ one at a time, only while a page is open. A quiet minute is about one
58
+ topic the command picks (the lead, a dama tenpai with the hands shown,
59
+ the dora, the wall, the last discard). Thinking off, effort low,
60
+ `max_tokens` 200; a cut answer is dropped with its stop reason in the
61
+ log. The fixed part of the ask (the role,
62
+ the rules in short, the tone, the length, whether the hands may be
63
+ told) is cached; the table now and the lines that just came follow.
64
+ With `--public` or a page's H the AI gets no hand. An answer whose round
65
+ or phase is over, or that comes after 15 s, is dropped; on the page an
66
+ AI's line comes after the fixed ones and goes first when the voice
67
+ falls behind. The money is counted from the tokens (cache writes and
68
+ reads too) in `$XDG_STATE_HOME/asterism-games/commentary_ai_usage.json`
69
+ (`AI::Budget`), shown in the bar, and capped at `--ai-daily-usd`
70
+ (default 3 USD a day). `ANTHROPIC_API_KEY`; the anthropic gem stays
71
+ outside the gem's dependencies (loaded only with a key);
72
+ `ASTERISM_GAMES_AI_FAKE=1` tries it without one. `--ai off` turns it off.
73
+ - The gem holds the page (HTML, JS, CSS) and the two tile sheets
74
+ `tiles_hand.bmp` and `tiles_river.bmp`, copied byte for byte from
75
+ fmruby-core; in the gem they are MIT (`TILES_LICENSE.txt`). The gem's
76
+ test checks they are there and unchanged.
77
+
78
+ ## 0.18.0
79
+
80
+ - **Spectate** (README "Spectate"): the host puts a table's spectate view
81
+ on `asterism/<node>/spectate/<table>/state` (a room's table as
82
+ `room<slot>-<table>`), a key outside the players' role
83
+ (`asterism/*/games/**`) and outside every room, for a console's
84
+ spectators and viewers and for the asterism gem's command line and MCP
85
+ bridge. The framework's `Game#spectate_view` (nil by default: nothing
86
+ is put) and `Game#spectate_delay_ms`; the host takes the view when it
87
+ puts the state, keeps it for the delay and puts each one in order, and
88
+ puts the last one again every 5 s. A room's host puts its tables' views
89
+ on a second connection of its own (the host's own certificate, no
90
+ namespace), never into the room; with a slot's certificate (a console
91
+ before 0.3.0) nothing. The application `spectate` is on the network (a
92
+ liveliness token) while the host puts.
93
+ - The mahjong table's spectate view: the table in words as `look` gives
94
+ it, with every seat's hand and drawn tile (the hands a spectator member
95
+ sees); never the wall, the ura dora, an indicator not turned yet, a node
96
+ or a token. Held for the table's spectator delay (none by default).
97
+ Othello and the drawing board put nothing.
98
+ - The console's role `games-host` needs two more lines for it (Read and
99
+ write `asterism/${node}/spectate`, `asterism/${node}/spectate/**`); the
100
+ console's docs/rooms.md has them and the `spectator` role. Without them
101
+ the router drops the puts and nothing else changes.
102
+ - The host's last log line counts the spectate puts and their largest
103
+ size apart from the players' messages.
104
+
6
105
  ## 0.17.1
7
106
 
8
107
  - `bin/othello-tty` is gone: `asterism-games play othello` is the same
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
@@ -512,6 +515,84 @@ the host offers: its money of the day (`usd`), the limit it stops at
512
515
  - **Size.** Five tables take about 300 bytes (the largest put is in the
513
516
  log's `largest message` with the others).
514
517
 
518
+ ## Spectate
519
+
520
+ A table can also be watched from outside it, with what the players do not
521
+ see of each other (games 0.18.0): the host puts a game's spectate view on
522
+ a key no player's role reads and no room reaches, for an admin, a
523
+ spectator's row of the console, or an AI assistant that only looks (the
524
+ asterism gem's command line and MCP bridge). The mahjong table puts the
525
+ four hands there; othello and the drawing board put nothing (they hide
526
+ nothing: their state is the whole game).
527
+
528
+ - **The key.** `asterism/<node>/spectate/<table>/state`: the host's node,
529
+ the application `spectate` (not the host's `games`, so the players' role
530
+ `asterism/*/games/**` does not match it), and the table. A room's table
531
+ is `room<slot>-<table>` there (`asterism/games/spectate/room01-mahjong1/state`),
532
+ so the rooms' tables and the permanent ones never meet. The value is
533
+ MessagePack, like the state.
534
+ - **Outside the rooms.** Every key a room's host writes on its own
535
+ connection gets the room's namespace (`room/<slot>/...`), which every
536
+ seat of the room reads whole. So a room's host opens a second connection
537
+ of its own for the spectate keys, with its own certificate and without
538
+ the namespace: the key is then on the registered side, where the seats'
539
+ rules do not reach. It does so only with the host's own certificate (a
540
+ console from 0.3.0 lists the host in `rooms.json`'s `"hosts"`); with the
541
+ slot's certificate of an older console it puts nothing (that
542
+ certificate is the room's). The permanent tables' host and a host of its
543
+ own put on their one connection.
544
+ - **What the mahjong table puts.** The table in words, as `look` gives it
545
+ ("Playing from a program") to a member without a seat, with each seat's
546
+ `hand` and `drawn` tile, the same four hands a spectator member's view
547
+ has (`sh`, `sd`): the seats (name, kind, wind, score, riichi, river,
548
+ melds, hand count, hand, drawn), the round (wind, number, honba, riichi
549
+ sticks, dealer, wall left, turn, phase), the dora indicators turned so
550
+ far, the last discard, the result and the final ranks, the rules, and
551
+ `delay_s`. Never the wall, the ura dora, an indicator not turned yet, a
552
+ member's node or token (`test/test_spectate.rb` looks for them):
553
+
554
+ ```
555
+ games/spectate/mahjong1/state
556
+ {"table":"mahjong1","status":"play","sv":212,
557
+ "seats":[{"seat":0,"name":"CPU1","kind":"cpu","score":21000,"wind":"north","riichi":false,
558
+ "river":["2z","4z","5z","1m",...],"melds":[{"type":"pon","tiles":["7z","7z","7z"],"from":1}],
559
+ "hand_count":10,"hand":["7m","7m","2p","3p","3p","4p","4p","5s","6s","7s"],"drawn":null}, ...],
560
+ "round":{"wind":"east","number":2,"honba":0,"riichi_sticks":1,"dealer":1,"wall_left":19,"turn":3,"phase":"discard"},
561
+ "dora_indicators":["9p"],"last_discard":{...},"rules":{...},"delay_s":0}
562
+ ```
563
+ - **The delay.** The table's spectator delay ("Spectators": the setting
564
+ `spectator_delay_minutes`, or the owner's choice in the lobby; none by
565
+ default). The host takes the view when it puts the state (after a change,
566
+ and every 5 s), keeps each one in order and puts it once it is the delay
567
+ old, each in turn: what is put is what the table was the delay ago,
568
+ never sooner, also across a round's end or when the delay is chosen
569
+ again (a shorter delay lets out what is old enough at once). A host
570
+ started again starts with nothing kept (nothing is put for the delay).
571
+ At most 8 MB is kept per table; past that the oldest is dropped (never
572
+ shown rather than shown early). The last one put is put again every
573
+ 5 s, so a new subscriber has it within that.
574
+ - **Who reads it.** The console's docs/rooms.md, "Watching the tables
575
+ from outside": the host's role `games-host` needs
576
+ `asterism/${node}/spectate` and `asterism/${node}/spectate/**` in "Read
577
+ and write" (without them the router drops the puts, and nothing else
578
+ changes); a role `spectator` (Read `asterism/*/spectate` and
579
+ `asterism/*/spectate/**`, Watch `asterism/**`) reads them with the
580
+ command line, and the `viewer` role (Read `asterism/**`) with the
581
+ command line and the MCP bridge. The players' role `games` and the
582
+ rooms' seats do not reach them.
583
+ - **Looking.** With the asterism gem's command line (asterism 0.10.0's
584
+ `object` commands): `asterism object echo 'games/spectate/*' --credentials
585
+ spectator.tar` (each put as it comes; `-o json` a line each), `asterism
586
+ object get games/spectate/mahjong1 --credentials viewer.tar` (the next
587
+ one). The MCP bridge (`asterism mcp --credentials viewer.tar`):
588
+ `asterism_values` with the pattern `games/spectate/*/state`. The bridge
589
+ subscribes to `asterism/**`, which a `spectator` row may not, so it
590
+ needs the viewer role. The host's application `spectate` is on the
591
+ network (a liveliness token) while it puts, so the values show as an
592
+ application that is there.
593
+ - **Writing a game.** `spectate_view(now)` (nil, the default: nothing) and
594
+ `spectate_delay_ms` (0) of `AsterismGames::Game`.
595
+
515
596
  ## Writing a game
516
597
 
517
598
  A game is two classes on the framework: the table (`AsterismGames::Game`:
@@ -591,6 +672,7 @@ What the framework gives a game:
591
672
  | `changed`, `dirty?`, `unsaved?` | a change to save and put; a change only one member's view shows (`@unsaved = true`) is saved, not put |
592
673
  | `to_saved` / `restore` / `resume_clock`, `SAVE_FORMAT` | the saved form (one JSON file per table, `state_dir/<table>.json`, written to a temporary file and renamed); a state of another format is not read. Times are saved as the time left and resumed with the host's clock |
593
674
  | `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) |
675
+ | `spectate_view(now)`, `spectate_delay_ms` | what the host puts on `asterism/<node>/spectate/<table>/state` for those who watch from outside the table ("Spectate"), held for the delay; nil (the default): nothing. It may hold what the players do not see of each other, never what nobody knows yet |
594
676
  | `summary(now)` | the counts the status shows (`st`, `members`, `humans`, `cpus`, `away`, and the game's own): no names, no nodes, no tokens. `busy` (`bin/asterism-games status --idle`) is `humans - away` |
595
677
  | `settings`, `tables`, `screens`, `DEFAULTS` | the table keys of the settings file and the environment, the tables a host serves by default, the table apps that get the tables in a room, the table's options |
596
678
 
@@ -758,6 +840,10 @@ hand.
758
840
  CPU) or every 20 s (a person) does not change how far behind the
759
841
  spectators are. A host started again starts the delay over (the past is
760
842
  not saved).
843
+ - **From outside the table.** The same four hands, in words and as old
844
+ as the spectators' view, go on `asterism/<node>/spectate/<table>/state`
845
+ for a console's spectators and viewers ("Spectate"), a key the players
846
+ cannot read.
761
847
  - **The default is no delay.** The spectators see the hands as they are:
762
848
  good for watching a game, and for an audience in the same room. A seated
763
849
  player who joins the same table from a second screen sees the others'
@@ -1380,6 +1466,290 @@ Typed (in a pipe or a file each line; on a terminal after `:`):
1380
1466
  The host's answers are shown in the table app's words (`今はできない`,
1381
1467
  `卓の主だけができる` ...).
1382
1468
 
1469
+ ## Commentary
1470
+
1471
+ (games 0.19.0) `asterism-games commentate mahjong` watches a mahjong table
1472
+ from outside, the way a sports broadcast does: it reads the table's
1473
+ spectate key ("Spectate", with the four hands), draws the table in a
1474
+ browser with the tiles of Family mruby's mahjong app, and comments on what
1475
+ happens in Japanese, as captions under the table and in a voice: a
1476
+ VOICEVOX character's (ずんだもん by default) when a VOICEVOX engine runs on
1477
+ this machine, else the browser's own (the Web Speech API; "The voice"
1478
+ below). It only reads: it never joins the table or calls it. CRuby and
1479
+ the asterism gem only; it runs on WSL and on macOS.
1480
+
1481
+ ```
1482
+ asterism-games commentate mahjong --router tcp/127.0.0.1:7447 # a host on this machine or the home network
1483
+ asterism-games commentate mahjong --credentials viewer.tar # a console's network (a viewer's or a spectator's .tar)
1484
+ asterism-games commentate mahjong --credentials viewer.tar --namespace room/01 # a room's tables
1485
+ asterism-games commentate mahjong --public # for a stream: the hands never reach the page
1486
+ asterism-games commentate mahjong --json | tee commentary.jsonl # the lines for another tool too
1487
+ ```
1488
+
1489
+ It starts a small web server on `127.0.0.1` (port 7470, the next free one
1490
+ when it is taken), prints the address and opens it in the browser (macOS
1491
+ `open`; WSL `wslview`, else Windows' `start`; elsewhere `xdg-open`).
1492
+
1493
+ | Option | |
1494
+ |---|---|
1495
+ | `--router LOCATOR`, `--credentials FILE` | as `play` (with neither, `ASTERISM_ROUTER` / `ASTERISM_CREDENTIALS`, all or nothing; with nothing, `tcp/127.0.0.1:7447`) |
1496
+ | `--namespace PREFIX` | a room's tables: `room/01` shows `room01-<table>` (the spectate keys are outside the room, so the connection has no namespace) |
1497
+ | `--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 |
1498
+ | `--node NODE` | the node ID (default `commentate-<host>-<pid>`, or the `.tar`'s name) |
1499
+ | `--port N` | the page's port (default 7470) |
1500
+ | `--public` | hide the hands: the views sent to the page lose `hand` and `drawn`, and no line tells a hand |
1501
+ | `--no-open` | do not open the browser |
1502
+ | `--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`) |
1503
+ | `--voicevox URL` | the VOICEVOX engine (default `VOICEVOX_URL`, else `http://127.0.0.1:50021`) |
1504
+ | `--speaker NAME`, `--style NAME` | the VOICEVOX character and its style (default `ずんだもん`, `ノーマル`) |
1505
+ | `--ai on\|off` | the AI's one-liners (default `on`; they need `ANTHROPIC_API_KEY` and the anthropic gem; "The AI's one-liners") |
1506
+ | `--ai-model MODEL` | the model they ask (default `claude-haiku-5-5`) |
1507
+ | `--ai-daily-usd USD` | what they may cost a day (default 3); then the fixed commentary alone |
1508
+
1509
+ - **The role.** The spectate keys are `asterism/<host>/spectate/<table>/state`.
1510
+ On a console's network a `viewer` (Read `asterism/**`) or a `spectator`
1511
+ (Read `asterism/*/spectate` and `asterism/*/spectate/**`) reads them; the
1512
+ players' role `games` does not. On a plain zenohd anyone does.
1513
+ - **The page.** A bar of states on top (the table, the voice, the delay the
1514
+ host puts on the table, a host that is not there), the table in a
1515
+ canvas (the round, honba, riichi sticks, the wall and the dora
1516
+ indicators; for each seat its wind, name, points, riichi, river with the
1517
+ riichi tile sideways and the called tiles pale, hand and drawn tile,
1518
+ melds; the seat to act framed), the captions below with the time of
1519
+ each, the newest at the bottom. Two seats to a row in a wide window, one
1520
+ in a narrow one (a phone). Keys: **Space** pauses (what comes meanwhile
1521
+ is held and shown when it goes on), **M** turns the voice off and on,
1522
+ **H** hides the hands on that page (backs instead of the tiles, and the
1523
+ lines that tell a hand left out). A table that several hosts put is
1524
+ chosen in the bar.
1525
+ - **The AI's one-liners.** Claude's short lines at the big moments,
1526
+ marked AI, after the fixed ones; the bar shows the model and today's
1527
+ money. "The AI's one-liners" below.
1528
+ - **The voice.** One queue on the page says the lines in turn, each
1529
+ caption coming up as its voice starts; when the voice falls behind, the
1530
+ old small lines are dropped (their captions still come up) and a win or
1531
+ a big moment is said next. With VOICEVOX, a line plays its fixed phrase
1532
+ first (リーチ, ポン, ロン, the round's name ...) and then the rest of it,
1533
+ synthesised meanwhile; without, the browser reads the lines with a
1534
+ Japanese voice when it has one (the bar says which voice, or that it has
1535
+ none). Browsers play sound only after the page has been clicked once:
1536
+ the page says so until then. "The voice" below.
1537
+ - **The commentary.** Made by rules from the difference between one view
1538
+ and the one before (pure functions, `commentate/mahjong_events.rb` and
1539
+ `mahjong_phrases.rb`, so the host can take them over later), each event
1540
+ in two to three phrasings chosen at random, the big moments in strong
1541
+ ones: the first look at the table, a round's start (the dealer and the
1542
+ dora; the game's first; the oorasu, and the oorasu going on), a discard
1543
+ of a dora or a red five, a call (チー, ポン, the kans), a new dora, the
1544
+ wall at 10, riichi with its waits, a tenpai (from the hand, once a round
1545
+ for each seat), a win with its yaku and points (満貫 and over: strong),
1546
+ a draw with who was tenpai, an abortive draw, the lead changing hands,
1547
+ the end with the ranks. The tiles and the yaku are read as a player
1548
+ says them (サンマン, リーチ, タンヤオ). With the hands hidden (`--public`,
1549
+ H) a tenpai is not said and a riichi is said without its waits.
1550
+ - **It does not stop.** A host that leaves (`on_leave`, or no put for 15 s)
1551
+ is shown as gone and comes back with its next put; a new round or a new
1552
+ game goes on as before. The delay of the table is the host's
1553
+ ("Spectators"); the page shows it and adds none.
1554
+ - **The web server.** HTTP by hand on the standard library's `socket`
1555
+ (no gem): the page, its script and style, the two tile sheets and
1556
+ `/events`, a stream of Server-Sent Events (`status`, `state`, `line`)
1557
+ that pushes the table and the commentary. It listens on `127.0.0.1`
1558
+ only, answers GET (and HEAD) on those paths only, and refuses a request
1559
+ whose Host is not `127.0.0.1` or `localhost`. The page loads nothing from
1560
+ elsewhere.
1561
+ - **On a stream (OBS).** Add a Browser Source with the address
1562
+ (`http://127.0.0.1:7470/`), 1280 x 720 or larger. `?obs=1` leaves out the
1563
+ bar and the keys' line and makes the background transparent around the
1564
+ captions; `?hands=0` hides the hands on that page, `?voice=0` silences
1565
+ it, `?table=games/mahjong1` picks the table. Start the command with
1566
+ `--public` when the stream must never show a hand. OBS's browser does not
1567
+ always play the Web Speech voice into the stream: for the voice, capture
1568
+ a browser window and the desktop's sound instead.
1569
+ - **The tiles.** `lib/asterism_games/commentate/assets/tiles_hand.bmp` and
1570
+ `tiles_river.bmp` are the hand-drawn sheets of Family mruby's mahjong app
1571
+ (fmruby-core's `flash/app/game/mahjong/`), copied byte for byte; the
1572
+ browser reads the BMPs as they are and only makes their transparent
1573
+ colour (magenta) transparent when it draws them. In this gem they are
1574
+ under the MIT License like the rest of it, by the author's decision
1575
+ (`TILES_LICENSE.txt` next to them).
1576
+
1577
+ ### The voice
1578
+
1579
+ (games 0.19.0) The commentary speaks with VOICEVOX,
1580
+ a free Japanese speech synthesiser, when its engine runs on this machine;
1581
+ the default character is ずんだもん (style ノーマル). Nothing of its voice is
1582
+ in this gem: the gem holds the words (`commentate/mahjong_voice.yml`) and
1583
+ the code that has your VOICEVOX say them. **VOICEVOX is a separate app
1584
+ you install and start yourself** (https://voicevox.hiroshiba.jp/, free);
1585
+ without it the commentary still runs, with the browser's voice. When it
1586
+ finds no engine, the command says so on the terminal, with where to get
1587
+ VOICEVOX, and so does the page's bar.
1588
+
1589
+ - **Finding the engine.** At its start the command asks
1590
+ `GET /version` of `--voicevox URL` (default `VOICEVOX_URL`, else
1591
+ `http://127.0.0.1:50021`). When nothing answers, the browser's voice
1592
+ reads, as before, the terminal says where to get VOICEVOX, and the
1593
+ page's bar says 「VOICEVOX が見つからない (無料のアプリ VOICEVOX を入れて
1594
+ 起動すると、ずんだもんの声になる): ブラウザの声で読む」.
1595
+ - **The fixed phrases** (リーチ, チー, ポン, カン, ロン, ツモ, the rounds'
1596
+ names, 流局, 途中流局, 満貫 to 役満, オーラス, 逆転, 終局, 対局開始, and
1597
+ 「実況を始めます」 when the page is clicked) are WAV files, played at
1598
+ once. The command makes the missing ones at its start (in the
1599
+ background: a phrase not made yet is skipped), or
1600
+ `asterism-games commentate voices --make` makes them all now (about 30
1601
+ files, half a minute on a CPU engine). They go to
1602
+ `$XDG_CACHE_HOME/asterism-games/voices/<speaker>-<style>/`
1603
+ (`~/.cache/asterism-games/voices/ずんだもん-ノーマル/` by default) with a
1604
+ `CREDITS.txt`; each is named by the digest of its phrasing, so a changed
1605
+ list makes the changed ones again and removes the old. **The directory
1606
+ can be deleted at any time**: it is made again.
1607
+ - **The rest of a line** is synthesised as the line comes (the engine's
1608
+ `/audio_query` and `/synthesis`) while its fixed phrase plays, and
1609
+ served to the page from the command's own web server at a temporary
1610
+ address (`/voice/<digest>.wav`; only lines the command made, never a
1611
+ text a page sends). A line that begins with its fixed phrase's word
1612
+ leaves it out (ロン! then 「CPU1、断么九 …」). The same line is
1613
+ synthesised once while it is kept (200 lines and 64 MB at most, the
1614
+ least recently used dropped first). A line the engine fails on is read
1615
+ by the browser's voice.
1616
+ - **What the engine reads** is always a line's speech, never its caption:
1617
+ the tiles, the yaku and the points as a player says them (サンマン,
1618
+ リーチ, 30フ 2ハン), and the words VOICEVOX would read wrongly written
1619
+ out (流局 リューキョク, 起家 チーチャ, 跳満 ハネマン;
1620
+ `MahjongPhrases::SPEECH_READ`, used for the fixed phrases too). No user
1621
+ dictionary is needed.
1622
+ - **Choosing a character.** `asterism-games commentate voices --list`
1623
+ lists the engine's characters and their styles; `--speaker NAME
1624
+ --style NAME` picks one by name (the engine's numbers are looked up,
1625
+ never assumed; a name that is not there is answered with the ones that
1626
+ are). ずんだもん speaks in her own tone: the lines and the fixed phrases
1627
+ end in 〜なのだ (「リーチなのだ!」「満貫、8000点なのだ!」); any other
1628
+ character speaks plainly. The tones are kept with the phrasings
1629
+ (`MahjongPhrases::TONES`), with a note on how to ask an AI to speak so.
1630
+ - **The page** plays the sound itself (never the PC with `paplay` or
1631
+ `afplay`), so a Browser Source in OBS carries it. **M** cuts the voice
1632
+ (the captions go on). When VOICEVOX speaks, the page shows the credit
1633
+ in its corner: `VOICEVOX:ずんだもん`.
1634
+
1635
+ Running VOICEVOX:
1636
+
1637
+ ```
1638
+ # Windows (and WSL): install the VOICEVOX app on Windows and start it (its engine
1639
+ # listens on 127.0.0.1:50021). From WSL with networkingMode=mirrored (.wslconfig)
1640
+ # 127.0.0.1 reaches it; otherwise pass Windows' address:
1641
+ asterism-games commentate mahjong --voicevox http://$(ip route show default | awk '{print $3}'):50021
1642
+
1643
+ # macOS: install the VOICEVOX app and start it; the default URL reaches it.
1644
+
1645
+ # docker (Linux, WSL, macOS): the engine alone, CPU version
1646
+ docker run -d --name voicevox -p 127.0.0.1:50021:50021 voicevox/voicevox_engine:cpu-0.25.0
1647
+ asterism-games commentate voices --list
1648
+ asterism-games commentate voices --make # optional: the commentary makes them at its start too
1649
+ asterism-games commentate mahjong --speaker 四国めたん --style ノーマル
1650
+ ```
1651
+
1652
+ The engine takes a gigabyte or two of memory; one line of commentary
1653
+ takes it about a second or two on a laptop's CPU.
1654
+
1655
+ **VOICEVOX's terms, in short** (read the current terms of VOICEVOX and of
1656
+ your character before you publish anything; they can change):
1657
+
1658
+ - The software and the voices it makes may be used free of charge,
1659
+ commercially or not, **with a credit** naming the character, such as
1660
+ `VOICEVOX:ずんだもん`. Each character has terms of its own on top of
1661
+ VOICEVOX's (ずんだもん's: a credit where it can be found, in a video's
1662
+ description or on screen; no use against public order, for political or
1663
+ religious campaigns, for deception, and the like).
1664
+ - **When you stream or upload the commentary, put the credit in the
1665
+ description too** (the page shows it in its corner).
1666
+ - Whoever you give generated voices to must follow the same terms, so
1667
+ they cannot be mixed into an MIT package. That is why this gem ships no
1668
+ sound: each user makes the files with their own VOICEVOX, and they stay
1669
+ on that machine.
1670
+
1671
+ ### The AI's one-liners
1672
+
1673
+ (games 0.19.0) At the big moments of a table Claude adds a short line of
1674
+ its own after the fixed commentary, in the same voice and tone (ずんだもん's
1675
+ 〜なのだ with her voice), marked **AI** in the captions:
1676
+ 「この待ち、山にまだ残ってるのだ。勝負はここからなのだ!」. It is also a
1677
+ small demonstration of an AI watching a table on the network and telling
1678
+ what it sees.
1679
+
1680
+ ```
1681
+ export ANTHROPIC_API_KEY=sk-ant-... # your own key
1682
+ gem install anthropic # not a dependency of this gem
1683
+ asterism-games commentate mahjong # Claude Haiku, at most 3 USD a day
1684
+ asterism-games commentate mahjong --ai-model claude-sonnet-5-5 --ai-daily-usd 1
1685
+ asterism-games commentate mahjong --ai off # the fixed commentary alone
1686
+ ```
1687
+
1688
+ - **The key and the gem.** The key is `ANTHROPIC_API_KEY`; the anthropic
1689
+ gem is loaded only when it is set, and is not a dependency of this gem.
1690
+ Without a key the page's bar says 「AI: off (キーなし)」; with a key and
1691
+ no gem, 「AI: off (anthropic の gem なし)」 and the command says to
1692
+ `gem install anthropic`. `--ai off` turns them off.
1693
+ - **The model.** `claude-haiku-5-5` by default (`--ai-model`), asked with
1694
+ thinking off at effort `low` and `max_tokens` 200 (two short sentences
1695
+ with room; an answer cut there is dropped and the log says its stop
1696
+ reason), one try with no retries, 8 s at most. Every call's line in the
1697
+ log has its seconds, stop reason, tokens and money.
1698
+ - **When.** A riichi, a win (a mangan or more counts as bigger), a draw or
1699
+ an abortive draw, the lead changing hands, the last round (オーラス),
1700
+ the end; and a quiet minute in the middle of a round, about one topic
1701
+ the command picks in turn (the leader's lead, a seat in tenpai without
1702
+ riichi when the hands are shown, the dora, the wall left, the last
1703
+ discard; never the same twice running). One call at a
1704
+ time and **at most one every 20 s**, or every 8 s for a scene bigger
1705
+ than the last one asked about (a win after a riichi, the end after a
1706
+ win; the ranks: the end, a big win and the lead changing hands, a win
1707
+ and the oorasu, a draw, a riichi, a quiet minute). A scene that comes
1708
+ sooner waits (the biggest of those waiting, for 15 s at most) and is let
1709
+ go when its round or phase is over. Calls are made only while a page is
1710
+ open (or with `--json`): nobody watching, nothing is spent. A tonpuu of
1711
+ four CPUs gets about two a round, and a line for its wins.
1712
+ - **What it is told.** A fixed part, the same to the byte every time and
1713
+ cached (prompt caching: the calls after the first read it at a tenth of
1714
+ the price): the commentator's role, mahjong's rules in short, the tone,
1715
+ the length (40 characters, one or two sentences, the line only: no
1716
+ preamble, quotes or emoji) and whether it may speak of the hands. Then
1717
+ the table now (the round, honba, riichi sticks, the wall, the dora; each
1718
+ seat's name, wind, points, riichi, melds and last discards; with the
1719
+ hands shown, each hand and its waits; a win's yaku and points, the
1720
+ ranks at the end), the scene, the fixed lines that just came, and its
1721
+ own last lines (not to repeat itself).
1722
+ - **The hands hidden.** With `--public`, and while a page has **H** on,
1723
+ the AI is not told the hands (nor the waits, nor who is tenpai): it
1724
+ speaks from what everyone can see. A line made while the hands were
1725
+ shown is not shown on a page that hides them.
1726
+ - **Late lines are dropped.** An answer that comes when its round or
1727
+ phase is over, or more than 15 s after it was asked, is not said. On
1728
+ the page an AI's line waits behind the fixed lines (its scene's own come
1729
+ first; a round's start waits behind it), and is dropped when a new scene
1730
+ comes before it is said (a riichi, a win, a draw), first when the voice
1731
+ falls behind, and after 40 s in the queue.
1732
+ - **The voice.** The caption is the line as it came; the voice reads it
1733
+ through the same readings as the fixed lines (`SPEECH_READ` and the
1734
+ tiles: 三萬 サンマン, 流局 リューキョク).
1735
+ - **What it costs.** Counted from the tokens the API reports (input,
1736
+ output, the cache's writes and reads) at the model's published price
1737
+ (Haiku 5.5: 0.10 USD a million input tokens, 0.50 output, 0.125 a cache
1738
+ write, 0.01 a cache read), in
1739
+ `$XDG_STATE_HOME/asterism-games/commentary_ai_usage.json`
1740
+ (`~/.local/state/...` by default; counts and money only, no words). The
1741
+ bar shows today's money and the limit; when today's money reaches
1742
+ `--ai-daily-usd` (default 3 USD) no call is made and the fixed
1743
+ commentary goes on alone; the next day (local date) starts at 0. A
1744
+ call costs about a hundredth of a cent with Haiku (some 1,000 cached
1745
+ tokens, a few hundred new ones, a line out), so a tonpuu costs well
1746
+ under a cent.
1747
+ - **Failures** (no answer in time, an error, a refusal) are one line in
1748
+ the command's output and nothing is said; the key and the words are
1749
+ never written out. `ASTERISM_GAMES_AI_FAKE=1` puts a fake in Claude's
1750
+ place (no API is called; its money is counted apart, in
1751
+ `commentary_ai_usage.fake.json`), to try the page without a key.
1752
+
1383
1753
  ## Playing from a program
1384
1754
 
1385
1755
  The mahjong table also speaks to programs (games 0.10.0): an AI assistant
@@ -1818,6 +2188,41 @@ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 bundle exec rake test # also through a
1818
2188
  tonpuu game against three CPUs each to the end from scripted input
1819
2189
  (letters, tiles, calls, passes, OKs; `PLAY_TRANSCRIPT_DIR` keeps what
1820
2190
  they showed).
2191
+ - `test_commentate.rb`: `asterism-games commentate` (in `rake test:gem`
2192
+ too): the waits of a hand (seven pairs, thirteen orphans, with melds),
2193
+ the dora of an indicator, each event from views made by hand (a round's
2194
+ start, the oorasu, riichi with its waits, calls and an added kan, a dora
2195
+ and a red five discarded, the wall, a new dora, a tenpai once a round, a
2196
+ big win and the lead changing hands, a draw, an abortive draw, the end)
2197
+ and their words, a recorded tonpuu of four CPUs
2198
+ (`test/fixtures/commentate_tonpuu.json.gz`) through the events and
2199
+ phrases, nothing of a hand with the hands hidden, the hub (the pages'
2200
+ frames, `--public`, `--json`, a page that opens late, a host that leaves
2201
+ and comes back), the web server (the page, the sheets byte for byte,
2202
+ 404 / 405 / a foreign Host refused, 127.0.0.1 only, the events stream, a
2203
+ port taken), the options and `--help`; with the table there, a CPU
2204
+ round played here through the hub.
2205
+ - `test_commentate_voice.rb`: the commentary's voice (in `rake test:gem`
2206
+ too), against a small VOICEVOX engine started in the test: a character
2207
+ and a style by name (and the names offered when one is not there), the
2208
+ synthesis, no engine (the browser's voice, nothing made), the fixed
2209
+ phrases made once and named by their digest, made again when the list
2210
+ changes (the old files removed), the cache directory, every clip a line
2211
+ asks for in the list in both tones, a line synthesised once and kept
2212
+ (the least recently used dropped, made again when asked), a failing
2213
+ engine, the words a fixed phrase said left out of the line, ずんだもん's
2214
+ tone (every phrasing, every line of the recorded game, the clips that
2215
+ tell no hand), the hub's lines with their sound, the web server's
2216
+ `/clip/` and `/voice/` (only what the list and the hub name), the
2217
+ missing phrases made at the start, the options and `voices --list |
2218
+ --make`.
2219
+ - `test_spectate.rb`: the spectate view (README "Spectate"; its
2220
+ framework part in `rake test:gem` too): nothing by default (othello, the
2221
+ board), the keys of the permanent tables and of a room, what the rooms
2222
+ give their hosts, the mahjong view (the four hands, no token, node, wall
2223
+ or ura dora), a CPU game on the host, the delay (0, and 3 s across a
2224
+ round's end, in order, never sooner), a room's table on the connection
2225
+ outside the room only, and the last put again after 5 s.
1821
2226
  - `test_play_commands.rb`: `--commands "sit,cpu,start"` against a table
1822
2227
  in this process: they wait for the lobby when a game is going on as
1823
2228
  the client joins (and then sit, fill and start), the owner's wait for
@@ -2004,4 +2409,7 @@ each result that long.
2004
2409
 
2005
2410
  ## License
2006
2411
 
2007
- MIT (LICENSE).
2412
+ MIT (LICENSE). The tile sheets of the commentary page
2413
+ (`lib/asterism_games/commentate/assets/tiles_*.bmp`, the hand-drawn tiles
2414
+ of Family mruby's mahjong app) are under the MIT License in this gem too
2415
+ (`TILES_LICENSE.txt`).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.1
1
+ 0.19.1
data/bin/asterism-games CHANGED
@@ -13,6 +13,9 @@
13
13
  # # --idle: exit 0 when nobody sits at a table
14
14
  # bin/asterism-games play <game> [options] # play at a table in this terminal (othello,
15
15
  # # mahjong; README "Playing in a terminal")
16
+ # bin/asterism-games commentate <game> [options]
17
+ # # watch a table in a browser with commentary
18
+ # # (mahjong; README "Commentary")
16
19
  # bin/asterism-games help # this text (also --help, -h)
17
20
  $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
18
21
 
@@ -26,6 +29,8 @@ USAGE = <<~TEXT
26
29
  (--idle: exit 0 when nobody sits at a table)
27
30
  asterism-games play <game> [options] play at a table in this terminal
28
31
  (othello, mahjong; play --help for the options)
32
+ asterism-games commentate <game> [options] watch a table in a browser, with commentary in
33
+ captions and voice (mahjong; commentate --help)
29
34
  asterism-games help this text (also --help, -h)
30
35
  TEXT
31
36
 
@@ -36,6 +41,9 @@ when "help", "--help", "-h"
36
41
  when "play"
37
42
  require "asterism_games/play"
38
43
  exit AsterismGames::Play.main(ARGV.drop(1))
44
+ when "commentate"
45
+ require "asterism_games/commentate"
46
+ exit AsterismGames::Commentate.main(ARGV.drop(1))
39
47
  when "status"
40
48
  nil
41
49
  else