A Watten card-game trainer (Tyrolean / 33-card variant) written in Rust with a WebAssembly frontend. The bots use a memoized legal-completion search to pick their cards and to estimate per-card win rates for the human player.
- 33-card Watten deck (four suits × Seven..Ace plus the Weli)
Card,Suit,Rank,GameResult,Player,GameStatecard_strengthenforcing Rechte > Striker rank > trump > lead > rest- Seeing-player must-follow-trump rule (
GameState::allowed_indices) GameState::play_round/play_round_logged(non-interactive)start_round_interactive+human_play+advance_bots(interactive)raise_round(team)/concede_round(team)for "raising" and "geh"- Two move evaluators selectable via
set_evaluator:Evaluator::Search(default): memoized enumeration of all legal trick completions from the current state. Sub-second per round.Evaluator::Database: legacy 120⁴ brute-force, kept as a benchmark fallback.cargo fullexercises it.
You need:
- Rust (stable) + the
wasm32-unknown-unknowntarget (rustup target add wasm32-unknown-unknown) wasm-pack(cargo install wasm-pack)- Node.js 20+ (CI builds on Node 20)
Then, from the repository root:
make devOpen http://localhost:5173 in your browser. The Vite dev server hot-reloads both the React frontend and the Rust → WASM bundle.
Equivalent without make:
npm startcargo test # fast Rust unit + integration tests
cargo full # also runs the heavy database population testsFrontend unit / integration tests (Vitest + Testing Library, builds wasm first):
cd frontend
npm install
npm test # Vitest
npm run test:watch # Vitest in watch modeEnd-to-end browser tests (Playwright on Chromium, Firefox and WebKit):
cd frontend
npx playwright install # first time only
npm run test:e2eThe repo is linted and formatted by a layered toolchain, all gated in CI:
| Area | Tools |
|---|---|
| Rust | rustfmt, clippy (with pedantic), cargo-deny (advisories/licenses/bans/sources), cargo-machete (unused deps) |
| Front | ESLint (typescript-eslint type-checked, react, react-hooks, react-refresh, jsx-a11y), Prettier, tsc --noEmit |
| Repo | .editorconfig, lefthook git hooks |
Run everything the way CI does, from the repository root:
make lint # all linters (Rust + JS), no changes made
make fmt # auto-format Rust (cargo fmt) + JS (prettier)
make audit # cargo deny check + cargo macheteThe lint config lives in Cargo.toml ([lints]), deny.toml,
frontend/eslint.config.js, and frontend/.prettierrc.json. The Rust
supply-chain tools install with:
cargo install cargo-deny cargo-machete # or download prebuilt binarieslefthook runs the fast checks before each commit
(rustfmt, clippy, ESLint, Prettier) and the test suite before each push. It
installs automatically via the root npm install (prepare script); to wire
it up manually:
make hooks # npx lefthook installHook behaviour is defined in lefthook.yml.
From the repository root:
npm run coverage # Rust (cargo-llvm-cov) + JS (Vitest v8)
npm run coverage:rust # Rust only — HTML in coverage-rust/html
npm run coverage:js # JS only — HTML in frontend/coveragecargo llvm-cov requires the llvm-tools-preview rustup component and the
cargo-llvm-cov cargo subcommand:
rustup component add llvm-tools-preview
cargo install cargo-llvm-covPushes to main trigger
.github/workflows/gh-pages.yml, which
builds the Vite bundle with VITE_BASE set to the repo path and deploys it
to GitHub Pages. Enable Pages in repository settings → Pages → Source:
GitHub Actions once, and subsequent pushes deploy automatically. The
deployed URL is reported in each workflow run.
cargo runPlays a game in the terminal. Set verbose = true on GameState to see
narration (already enabled by the CLI binary).
You will need wasm-pack and
Node.js. From the repository root:
npm startThis installs JS dependencies, runs wasm-pack build --target web, and starts
Vite. Open http://localhost:5173. The UI shows your hand with a per-card
win-rate estimate (wins / total over all legal completions); opponent hand
sizes; the current trick; the running scores; and Raise (+1) and Concede
round buttons.
To build a production bundle and preview it:
cd frontend
npm run build
npm run serveTo run the Playwright UI tests (Firefox only):
cd frontend
npm run test:ui- Trump (Schlag) is the suit of the top card of the dealer's pile (the first card dealt to the dealer).
- Striker rank (Weisen) is the rank of the top card of the forehand's pile (the first card dealt to the player after the dealer).
- Card strength: Rechte (the trump-suit + striker-rank card) > Weli > any
Striker > any trump > lead suit > rest. First-played wins ties between
equal-strength cards (
card_strengthin src/game.rs). - Seeing players (dealer + forehand) must play a trump or striker when trump
is led if they have one (enforced by
GameState::allowed_indices).
- The Critical cards (Maxi/Belli/Spitz) of Bavarian Watten are not modelled — this is the Tyrolean/generic 33-card variant.
- Winning score defaults to 15 (
game::WINNING_POINTS). Not yet runtime- configurable from the UI.