| @@ -0,0 +1,63 @@ |
| 1 | +# Sanguosha (三国杀) |
| 2 | + |
| 3 | +Five 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` is the seat's view as the MCP's `getSnapshot` returns it: `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 move the upstream's heuristic AI would play (the MCP's `recommendedAction`), `game`, which changes when a new game starts, and `refused`, the moves the engine has already refused at this ask. |
| 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 | + |
| 17 | +On ten seeds your bot takes each of the five seats in turn while four bots of one kind from the field fill the others: a hundred games. 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 same seeds and bots play the same games: the engine runs on a virtual clock and shuffles from the seed. |
| 18 | + |
| 19 | +## The field, season 1 |
| 20 | + |
| 21 | +| Bot | What it does | |
| 22 | +|---|---| |
| 23 | +| `upstream` | wmzy/sanguosha's own AI: the heuristic scorer behind its MCP server's `recommendedAction` (`src/ai-mcp/heuristics.ts`), with the targets and cards it leaves open filled in. It also plays every `auto` seat in a room. | |
| 24 | +| `random` | any listed move, with random targets | |
| 25 | + |
| 26 | +The starter plays the recommended move and scores 0.375 (主公 0.25, 忠臣 0.4, 反贼 0.8, 内奸 0.05); the `upstream` bot in a route's place scores 0.369 and `random` 0.263. |
| 27 | + |
| 28 | +## Rooms |
| 29 | + |
| 30 | +A 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: |
| 31 | + |
| 32 | +- `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 upstream AI moves for you. |
| 33 | +- `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. |
| 34 | +- `POST /api/rooms/:id/seats/:seat` takes a seat over or hands it back (to `agent` or `auto`). |
| 35 | + |
| 36 | +Everyone also sees the public messages: `start` (with the table as a spectator sees it), `events` (the upstream engine's own view events), `moved` (a seat answered), `control` (a seat changed hands) and `end`, with 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. |
| 37 | + |
| 38 | +The 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. |
| 39 | + |
| 40 | +## The look |
| 41 | + |
| 42 | +The 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`). |
| 43 | + |
| 44 | +Under 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). |
| 45 | + |
| 46 | +The 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. |
| 47 | + |
| 48 | +**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. |
| 49 | + |
| 50 | +## Rebuilding |
| 51 | + |
| 52 | +`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: |
| 53 | + |
| 54 | +``` |
| 55 | +git clone --filter=blob:none --no-checkout https://github.com/libnoname/noname <noname> |
| 56 | +git -C <noname> sparse-checkout set --no-cone $(python3 src/art.py --paths) |
| 57 | +git -C <noname> checkout 90b58bb9934edb1712fd187a80c8323b7aeabaf3 |
| 58 | +python3 src/art.py <noname> |
| 59 | +``` |
| 60 | + |
| 61 | +## Credits |
| 62 | + |
| 63 | +The 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. |