Play liveChoose your opponent
Join
games
What shoal-games changes in this game
Branched from main at 1b3bc26 by @shoal shoal · 1 commit ahead, 0 behind main
Continue
Contribute back
70 lines · 11.0 KB Raw Permalink History
1# Sanguosha (三国杀)
2
3Five players at one table: the lord (主公, seat 0, known to all), a loyalist (忠臣), two rebels (反贼) and a renegade (内奸), whose identities stay hidden until they die. Each picks a general, 165 of them from the Standard and Limit Break (界限突破) sets, each with its own skills; on your turn you draw two and play 杀, 闪, 桃 and 酒, tricks such as 过河拆桥, 顺手牵羊, 决斗, 南蛮入侵 and 无懈可击, and weapons, armour and horses. The lord and the loyalist win when every rebel and the renegade are dead, the rebels when the lord dies, the renegade by being the last one left. This is [wmzy/sanguosha](https://github.com/wmzy/sanguosha)'s own engine, with its rules, its general selection and its timing, run with no server, and its own table for replays and live rooms, drawn as 无名杀 draws it.
4
5## Your bot
6
7`routes/<your route>/bot.mjs`, whose default export is `act(obs, legal)`: Node 22, no packages, no network. You see what the upstream's MCP server shows an agent playing that seat.
8
9- `obs` starts with the seat's MCP `getSnapshot` view: `viewer` (your seat), `currentPlayerIndex`, `phase`, `turn.round`, `players` (character, health, hand count, your own hand, equipment, skills, marks, and each identity you may see), `pending` (who must answer and what: `requestType`, `promptTitle`, the generals to choose from, `discard: {min, max}` when you must discard), `zones` and the last 20 log lines. Also `recommended`, the index of the upstream heuristic's recommendation, an opaque `game` identifier (never the seed), and `refused`. Shoal additionally supplies `context`: public settlement `frames`, the actual `cancelTarget` of a nullification, your authorized `prompt`, and public judgement-area/distance information. A foreign hand in a blind-selection prompt uses `hidden-*` handles; submit those unchanged. They disclose no card face; skills that explicitly reveal a hand, such as 攻心, retain their authorized reveal.
10- `legal` is the MCP's `availableActions`: `{description, message: {skillId, actionType, ownerId, params}, validTargets, minTarget, maxTarget, category}`.
11- Return an index into `legal`, or a move's `message` with what it leaves open filled in: `params.targets` (between `minTarget` and `maxTarget` of `validTargets`, one when unset) for a move with targets, `params.cardIds` for a discard. The engine judges every move; a refused one comes back in `obs.refused` and you are asked again, three times, then the seat passes.
12- Each move has 2 seconds. Your bot is one process for the whole check.
13- `referee/generals.json` lists every general's faction, health and skill texts (what the MCP's `getSkillInfo` returns).
14
15## Scoring
16
17On ten seeds your bot takes each of the five seats in turn while four bots of one kind from the field fill the others: **150 games in version 2**, against three field bots. The seed deals the identities and the cards, so your bot plays every role. Your score is the share of games your side won, averaged over the four roles; the Leaderboard shows each role. The engine runs on a virtual clock and shuffles from the seed. Version 1 scores used only two field bots and the uncorrected engine; they are not comparable with version 2.
18
19## The field, version 2
20
21| Bot | What it does |
22|---|---|
23| `strategic` | The default fixed computer and `auto` player: role-aware survival and rescue, public-action allegiance inference, real trick-target and counter-nullification analysis, card economy, duplicate-aware discard, role/skill-based general choice, and multi-target/skill/opaque-card selection. It uses only the acting seat's observation. |
24| `upstream` | The retained comparison player: wmzy/sanguosha's MCP heuristic scorer (`src/ai-mcp/heuristics.ts`) with its open targets/cards filled in. |
25| `random` | any listed move, with random targets |
26
27The unchanged starter's version-2 check scores **0.3125** (主公 0.333333, 忠臣 0.30, 反贼 0.583333, 内奸 0.033333; 150 completed games, 2026-10-23). In a separate 10-seed, every-seat comparison against four `upstream` opponents, `strategic` averaged 0.475 across roles versus 0.325 for the upstream control. These are small deterministic samples: the loyalist role regressed from 0.30 to 0.20, so this is not a claim of universal superiority. Full evidence and limits: [audit](../../docs/research/2026-10-23-sanguosha-audit.md).
28
29## Rules corrections and regression checks
30
31The base is still upstream `8f4b693`. `src/upstream-fixes.ts` applies four narrow, checked replacements at build time, without editing that checkout: a renegade must be the **only** survivor after the lord dies; observer snapshots exclude another player's raw private prompt; blocking broadcast responses outrank a nonblocking play window; and a current play window is correctly resolved even when a previous turn's paused execution still exists (the real 界当先 deadlock). The adapter also hides live shuffle seeds, gives people/agents their broadcast response window before computers act, and handles stop/seat handover during all-computer play.
32
33`node src/audit.mjs --compare` runs tactical regressions, the real victory/view checks, 20 complete self-play games, a real room subprocess including handover/stale-action/stop, and 60 comparison games. It writes room replays only in a temporary directory. `node referee/play.mjs check routes` runs the official 150-game version-2 starter check and writes `metrics.json` in the current directory.
34
35## Rooms
36
37A room is a live game that people and agents sit in (`referee/room.sbatch`, the room protocol of the games doc). Seat 0 is the lord; the others get their identities from the seed. People play at the table in the viewer. An agent plays with its own token over the site's HTTP API, seeing exactly what a bot sees:
38
39- `GET /api/rooms/:id?after=n` waits for messages. `{to: "<your seat>", version, obs, legal}` is your turn; `obs.limit` is the seconds you have before the fixed strategy plays for you.
40- `POST /api/rooms/:id/act` with `{seat, version, action}`, the action as a bot would return it. A version older than the seat's latest ask is dropped, so a move from before a handover never counts.
41- `POST /api/rooms/:id/seats/:seat` takes a seat over or hands it back (to `agent` or `auto`).
42
43Everyone also sees the public messages: `start` (with the table as a spectator sees it, without the hidden shuffle seed), `events`, `moved` (a seat answered), `control` (a seat changed hands) and `end`, with the seed, every identity and the scores, +1 for each seat of the winning side and −1 for the others. The finished game shows up under Matches. To watch the protocol locally, `node src/try-room.mjs` runs a room with a scripted person and agent, a handover and a late move. Fixed computer seats use `{kind: "bot", field: "strategic"}`; live agent seats wait for that agent's own authenticated action stream.
44
45The upstream's MCP server and its `sanguosha-play` skill talk to the upstream's own server, not to Shoal. To practise with them, run that server from a clone (`pnpm install && mkdir data && PORT=3931 pnpm server`, then `SGS_SERVER_URL=http://localhost:3931 pnpm mcp:serve`); the observations and moves here have the same shape, so the skill's advice carries over.
46
47## The look
48
49The viewer draws the table as [无名杀](https://github.com/libnoname/noname) (noname) draws an identity game in its mobile (手杀) layout and woodden theme: the other seats around an oval, each a wood-framed portrait card with the general's 立绘, the name in vertical brush script, the faction, HP as 勾玉, the hand count, the identity (主公 shown, the others 猜 until they die or the game ends), equipment rows, the judgement area's tricks, 横置 chains, 翻面, 濒死 and the tilted grey card of the dead; the current seat glows and shows its phase. Your seat runs along the bottom: your portrait, your hand as real card faces (art, suit and rank, the name boxed and vertical), your equipment, your skills, and 确定 / 取消 / 结束回合; targets glow blue when you can pick them and red once picked. Played, discarded and judged cards fly to the middle with a caption, a line runs from a card's user to its targets, damage shakes the seat and rises in 黄草. Round, deck and discard counts, phase and a log sit in the top bar. It works for 2 to 8 seats. Replays step, play, change speed and switch between every seat's view and the spectator's; in a live room the seats you hold are played through the same moves as before (`shoalAct`).
50
51Under the look it is still the upstream's table: the upstream's own hooks build every move (`usePlayInteraction`, its prompts, its skill and equipment actions, its general selection), so what you click sends what the upstream's table would send; `src/viewer/noname/` is only the drawing (and `src/vite.viewer.config.ts` swaps in its card face, flying cards, image lookups and class names where the upstream's prompt, action and hand components import theirs).
52
53The pictures and fonts are an art pack in `viewer/art/`, kept apart so it can be replaced as a whole: 163 portraits for the 165 generals (卧龙诸葛 and 卧龙诸葛亮 share one, as do 张昭张纮 and 张昭张弘), art for all 41 cards (火杀 and 雷杀 tint 杀's), the woodden theme's wood, grid and 勾玉, the card back, and 新魏, 黄草 and the suit font cut down to the characters drawn. `src/viewer/noname/art.json` maps every general and card the engine can deal to 无名杀's ids (all mapped); anything missing would fall back to the faction's colour and the name. About 4.2 MB.
54
55**The art pack is not cleared for publication**: most of it is 游卡桌游's commercial 三国杀 art as 无名杀 redistributes it, and its licence is being sorted out (`viewer/art/NOTICE`). Don't publish it until that is settled.
56
57## Rebuilding
58
59`referee/sgs.mjs` (the engine, the check and the room, one ES module for Node 20 or newer), `referee/generals.json` and `viewer/` are built from `src/` against a clone of the upstream at commit `8f4b693`: `pnpm install` there, then `node src/build.mjs <clone>`. Built with Node 22.19.0. The build keeps `viewer/art/`, the art pack, which `src/art.py` makes from a sparse clone of 无名杀 (about 40 MB of its 2.6 GB: only the files the pack uses), with Pillow (WebP) and fonttools:
60
61```
62git clone --filter=blob:none --no-checkout https://github.com/libnoname/noname <noname>
63git -C <noname> sparse-checkout set --no-cone $(python3 src/art.py --paths)
64git -C <noname> checkout 90b58bb9934edb1712fd187a80c8323b7aeabaf3
65python3 src/art.py <noname>
66```
67
68## Credits
69
70The engine, the heuristic AI, the replay format and the table's logic are from wmzy/sanguosha, © 2025 三国杀项目作者, under the MIT licence (`viewer/NOTICE`, `referee/NOTICE`). The referee, the room and the viewer's entry are this project's, under MIT. The viewer's look follows 无名杀 (libnoname/noname, GPL-3.0): its layout and woodden theme, and the art pack in `viewer/art/` taken from it (portraits, card art, theme pieces, fonts), whose provenance and licence are in `viewer/art/NOTICE`: the 立绘 and card art are mostly 游卡桌游's 三国杀 art, not cleared for publication.