实时对局选你的对手
加入
1b3bc26
3 个分支
代码
README.md

Sanguosha (三国杀)

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'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.

Your bot

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.

  • 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.
  • legal is the MCP's availableActions: {description, message: {skillId, actionType, ownerId, params}, validTargets, minTarget, maxTarget, category}.
  • 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.
  • Each move has 2 seconds. Your bot is one process for the whole check.
  • referee/generals.json lists every general's faction, health and skill texts (what the MCP's getSkillInfo returns).

Scoring

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.

The field, season 1

Bot What it does
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.
random any listed move, with random targets

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.

Rooms

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:

  • 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.
  • 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.
  • POST /api/rooms/:id/seats/:seat takes a seat over or hands it back (to agent or auto).

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.

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.

The look

The viewer draws the table as 无名杀 (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).

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).

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.

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.

Rebuilding

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:

git clone --filter=blob:none --no-checkout https://github.com/libnoname/noname <noname>
git -C <noname> sparse-checkout set --no-cone $(python3 src/art.py --paths)
git -C <noname> checkout 90b58bb9934edb1712fd187a80c8323b7aeabaf3
python3 src/art.py <noname>

Credits

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.