纯前端 Web 斗地主对局应用:完全自研的拆牌手数 DP + 记牌推断 + 残局求解引擎 跑在 Web Worker 里,零第三方依赖、零构建、零下载,可直接托管在 GitHub Pages。
中文说明见 README_CN.md。 在线试玩:https://justa-cai.github.io/doudizhu/ · 源码:https://github.com/justa-cai/doudizhu
- Self-written engine — the core is a NOIP-2015-style minimum hands solver:
an outer DFS enumerates straights / pair-straights / planes (with wings), and an
inner memoized DP solves the leftover
{singles, pairs, trios, bombs}exactly. On top of that sit card counting (infer what opponents can still hold from public plays), teammate cooperation and endgame finishing. No DouZero, no RLCard, no ONNX, no model weights. - Zero dependencies — no build step, no CDN, no image/font assets. Playing cards are drawn procedurally on Canvas (rounded rects + text + suit glyphs).
- Honest stats — the side panel shows, for every move, the real hands-before →
hands-after, the decision time in the worker, and truthfully derived flags
(
bomb/endgame/card-counted). Moves that a low difficulty level chose instead of the engine's first pick are explicitly marked sub-optimal — the application never pretends to be smarter than it was. - Six levels — difficulty is two honest knobs, not a fake number: cognitive weakening (how many wing layouts it enumerates, whether it counts cards, whether farmers cover each other, whether it solves the endgame) plus root sampling (softmax temperature + blunder rate).
- Classic rules — 54 cards, 3 players, landlord gets the 3 bottom cards. 14 legal play shapes. Bomb / rocket double the multiplier; spring and anti-spring double it too. Full settlement (+/− 2×mult for the landlord, ∓1×mult for each farmer).
| Level | Name | Wing layouts | Card counting | Team play | Endgame | Sampling |
|---|---|---|---|---|---|---|
| 1 | 入门 | 1 | off | off | off | temp 800, blunder 35% |
| 2 | 初级 | 1 | off | off | off | temp 400, blunder 15% |
| 3 | 中级 | 2 | off | on | off | temp 150, blunder 3% |
| 4 | 高级 | 2 | on | on | off | temp 40 |
| 5 | 大师 | 2 | on | on | on | — |
| 6 | 宗师 | 3 | on | on | on | — |
Two hard rules bypass all weakening: if a hand can be finished in one move it
is played, and the leader may never pass (the latter is a rule of the game,
not a difficulty setting). Sampling carries a loss cap, so a weak level
"plays weakly" but never "plays as if broken". All of this lives in
js/difficulty.js, the single source of truth read by both the main thread and
the engine Worker.
View layer (Canvas table / side panel / menus, ES modules)
│
Rules kernel rules.js (move legality, beats(), minHands(), the state machine)
Difficulty layer difficulty.js (six levels + sampling, single source of truth)
│
Engine bridge bridge.js (seq-matched async facade, stale replies dropped)
│
engine.worker.js → engine.js (Web Worker: move generation + scoring)
The one structural difference from the sibling 04.Gomoku: everything is an
ES module, and rules.js is imported by both the main thread and the Worker
(via new Worker(url, { type: 'module' })). AI moves are submitted through the
very same rules.play() path as the human's — "there is exactly one
implementation of the rules" is a structural guarantee here, not a convention.
- Minimum hands (minHands) — the primary scoring axis. Outer DFS enumerates
every straight / pair-straight / plane the hand admits (plane wings are chosen
greedily as the smallest available cards, trying both single and pair wings);
the remainder is solved exactly by a memoized DP over
(singles, pairs, trios, bombs). The DP also models three-with-one, three-with-two, four-with-two and every sensible split (4→3+1, 4→2+2, 3→2+1 …), and treats the two jokers as either one rocket or two singles. A node budget of 20 000 keeps the worst case bounded; a typical 17-card hand finishes in well under a millisecond. - Card counting — from the public plays it derives which high cards the opponents can still hold, and rewards a move when nobody can beat it (i.e. it wins the lead back).
- Teammate cooperation — a farmer will not cover its partner's lead, and pays a large penalty for letting a hand through when an opponent is nearly out.
- Endgame — the finishing move is forced (
score ≥ WIN_BASE), and leading a lone card is discouraged when an opponent is down to one card.
Card encoding: id 0..51 → rank = id >> 2 (3…2), suit = id & 3;
52 = small joker, 53 = big joker. Rank indices 0..14 are
3 4 5 6 7 8 9 10 J Q K A 2 小王 大王; chains are limited to 0..11 (3…A).
A hand is Int8Array(15).
python3 server.py # → http://127.0.0.1:6330/(Any static file server works. Opening index.html via file:// won't —
the ES module Worker requires HTTP.)
Push to a GitHub Pages repo; .nojekyll is included. No headers, no
cross-origin isolation, no build pipeline needed — the engine is an ordinary
ES module Worker and does not use SharedArrayBuffer.
The strongest open-source Dou Dizhu AIs are DouZero (ICML 2021, Kwai) and
RLCard (Texas A&M). Both are Python/PyTorch projects; running them in a
browser would mean exporting to ONNX and shipping onnxruntime-web plus model
weights — several megabytes that directly conflict with the "pure static, open
and play" goal of this series. So this project writes its own algorithm in plain
JS. If DouZero were ever wired in, only the implementation behind
js/ai/bridge.js would change — rules.js and the entire UI would not move a
line. The full research record is in NOTICE.md and specs/prd.md.
| 02.chess | 03.chess_master | 04.Gomoku | This project | |
|---|---|---|---|---|
| Engine | Pikafish WASM (external, GPL) | Stockfish 18 WASM (external, GPL) | self-written JS (MIT) | self-written JS (MIT) |
| Engine size | 5 MB + 49 MB weights | 7.0 MB | ~50 KB source | ~100 KB source |
| Rules | chess.js (third-party) | chess.js (third-party) | self-written | self-written rules.js |
| Rendering | hand-drawn DOM | chessground (third-party) | self-written Canvas | self-written Canvas |
| Search | external engine | external engine | depth-limited α-β | no fixed-depth search (imperfect information) |
| License | GPL-3.0 | GPL-3.0 | MIT | MIT |
Dou Dizhu is an imperfect-information game: the opponents' hands are hidden, so a fixed-depth search cannot be made accurate and is deliberately not used. The analogue of "search depth" here is the minHands DP plus the cognitive switches above.
MIT — see LICENSE and NOTICE.md. All code in this repository is original; algorithm background references are listed in NOTICE.md.