Skip to content
justa-caiPublic

About

Dou Dizhu (斗地主) with a self-written pure-JS engine: minimum-hands DP, card counting, endgame solving. Zero deps, zero build.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Dou Dizhu · 斗地主(自研引擎)

纯前端 Web 斗地主对局应用:完全自研的拆牌手数 DP + 记牌推断 + 残局求解引擎 跑在 Web Worker 里,零第三方依赖、零构建、零下载,可直接托管在 GitHub Pages。

中文说明见 README_CN.md。 在线试玩:https://justa-cai.github.io/doudizhu/ · 源码:https://github.com/justa-cai/doudizhu

Highlights

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

Difficulty

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.

Architecture

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.

How the engine thinks

  1. 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.
  2. 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).
  3. 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.
  4. 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).

Run locally

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

Deploy

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.

Research notes (why it is built this way)

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.

Differences from the earlier projects in this series

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.

License

MIT — see LICENSE and NOTICE.md. All code in this repository is original; algorithm background references are listed in NOTICE.md.

About

Dou Dizhu (斗地主) with a self-written pure-JS engine: minimum-hands DP, card counting, endgame solving. Zero deps, zero build.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages