Game-theoretic agents for Liar's Deck, a social-deduction bluffing card game — from a deterministic rules engine up to trained agents you can play against in the browser. Two-player tables are solved with counterfactual regret minimization (CFR); three- and four-player tables use population-based reinforcement learning (PSRO) — hence "agent", not "RL", in the name.
▶ Live demo: https://liarsdeck-demo-319509297648.us-west1.run.app
Choose a 2-, 3-, or 4-seat table and either play one seat against the trained agents or spectate an all-agent game with pause/step/speed controls. An optional Learning mode compares each decision you commit with the deployed policy's action distribution at that exact information state — shown only after you act, so your choice stays honest. The demo is a single-visitor showcase: no accounts, no data collection, sessions are in-memory and expire. The first visit after an idle period may take ~10–15 s to load (the free-tier backend scales to zero).
| Seats | Policy | Notes |
|---|---|---|
| 2 | Whole-game CFR on the full information-set lattice | Near-exact equilibrium play |
| 3 | 16-member PSRO population | 0.517 mixed-lineup win rate vs 1/3 chance; qualified under pre-registered gates |
| 4 | 12-member PSRO population | Uniform-sigma release; +0.077 over chance, BR-probe edge +0.254 |
Multiway games that reach heads-up switch the surviving agents to the projected 2p CFR strategy (a measured, pre-registered improvement), and served PSRO policies suppress a class of dominated mixed plays — both disclosed in the demo's UI. Opponents sample one population member per seat per game, so styles vary between games but stay consistent within one.
The evolution of the project — engine, scripted baselines, PPO/NFSP/league
training, CFR solving and acceleration, PSRO populations, and the web
demo's hardening and launch — is documented as dated engineering reports
under reports/.
| Path | Contents |
|---|---|
liarsdeck/ |
Rules engine, PettingZoo AEC env, encoding, baselines, tournament, CFR, and the FastAPI game service (liarsdeck/service/) |
train/ |
PPO self-play, league/NFSP/PSRO training, probes |
web/ |
React + TypeScript SPA (Vite), Vitest unit tests, Playwright e2e matrix |
scripts/ |
Training/evaluation CLIs, release packaging, deploy tooling, soak/benchmark harnesses |
tests/ |
Engine, encoding, agent, and service test suites |
reports/ |
Dated experiment reports, release inventory, runbook, closeouts |
Backend + tests:
python3.12 -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -m pytest # engine + service suitesWeb demo (dev mode, Vite proxying to the service):
.venv/bin/uvicorn --factory liarsdeck.service.api:create_app --port 7860
cd web && npm ci && npm run devBrowser test matrix:
cd web && npm run build && npx playwright testOr the exact production container (multi-stage build; startup validates every model artifact against the checksummed release inventory and refuses to serve on any mismatch):
docker build -t liarsdeck-demo .
docker run --rm -p 7860:7860 liarsdeck-demoThe released agents were trained on a MacBook Pro (M3 Pro); wall-clock numbers below are from that machine.
1. Solve the 2p CFR policy. The whole game factors into a lattice of 72
round subgames solved by backward induction; this is the compute-heavy step
at roughly 9 hours of cumulative solver time (it parallelizes across
subgames via --workers, and --resume continues an interrupted solve):
.venv/bin/python scripts/solve_lattice.py \
--checkpoint reports/cfr-lattice-dcfr-sweep.npz --workers 82. Train the 3p/4p PSRO populations. Each iteration trains a
best-response probe against the current population and adds it as a member.
The released populations bootstrap from seed checkpoints produced by earlier
league/self-play training (--seed-member NAME=PATH; training good seeds
from scratch with train/ppo_league.py adds hours). The PSRO loops
themselves are fast on this laptop: the released 4p run (8 iterations →
12 members) took ~40 minutes on Apple-silicon MPS, and the 3p run
(→ 16 members) under 10 minutes on CPU:
.venv/bin/python train/psro.py --num-players 4 --iterations 8 \
--meta-solver uniform --psro-dir reports/psro-4p --json reports/psro-4p.json
.venv/bin/python train/psro.py --num-players 3 --iterations 8 \
--meta-solver uniform --psro-dir reports/psro-3p --json reports/psro-3p.json3. Package and validate the release. Re-pins every artifact hash into the inventory/registry and regenerates the Learning golden corpus; the service tests fail loudly on any mismatch, and the container will refuse to serve an inconsistent bundle:
.venv/bin/python scripts/build_web_release_inventory.py
.venv/bin/python scripts/build_policy_registry.py
.venv/bin/python scripts/build_learning_golden.py
.venv/bin/python -m pytest tests/serviceTwo expectations to set: retrained results are not bit-identical (PSRO
is stochastic — you get a different population and release digest, by
design), and strength claims need re-evaluation — the released policies
passed pre-registered gates (mixed-lineup win rates, best-response probes,
behavioral probes; see scripts/evaluate_*.py and the dated reports) that
a new population should be re-run through before trusting its labels.
https://liarsdeck-demo-319509297648.us-west1.run.app/
The demo runs on Google Cloud Run (single instance, scale-to-zero,
free-tier configuration). scripts/deploy_cloud_run.py builds the image,
tags it with the release digest, pushes it to Artifact Registry, and applies
the full pinned service configuration; it also manages rollbacks and the
keep-warm scheduler job. Operations are documented in
reports/phase6-runbook.md.
.venv/bin/python -m pip install -e ".[train]"
.venv/bin/python scripts/fuzz_random_games.py --games 100000 # engine fuzz gate
.venv/bin/python scripts/tournament.py random honest paranoid heuristic --games 10000
.venv/bin/python train/ppo_selfplay.py --updates 200 --batch-games 128
.venv/bin/python scripts/train_cfr.py --help # CFR solver
.venv/bin/python scripts/benchmark_serving_policies.py --report /dev/stdout