yahtzee-engine 0.1.0

Yahtzee rules, scoring, and bots: a fast heuristic and an exact optimal expected-value solver
Documentation
# Yahtzee Engine

[![Crates.io](https://img.shields.io/crates/v/yahtzee-engine)](https://crates.io/crates/yahtzee-engine)
[![Docs.rs](https://docs.rs/yahtzee-engine/badge.svg)](https://docs.rs/yahtzee-engine)
[![Build Status](https://github.com/jdh8/yahtzee-engine/actions/workflows/rust.yml/badge.svg)](https://github.com/jdh8/yahtzee-engine)

Complete [Yahtzee] rules and bots: a multiplayer state machine with the
official forced-joker scoring, a fast heuristic player, and an exact
expectimax solver that knows the best move — and its expected value — in
every position.

The design triangle:

- [`State`]: the compact rules authority.  Which categories are legal,
  what they pay, joker forcing, both bonuses — one implementation shared
  by the scorecard, the game, and the solver, so they can never disagree.
- [`Game`]: the table-level state machine.  Deterministic dice injection
  is the primitive (replays and front ends stay trivial); illegal moves
  are rejected without changing anything.  A [`Strategy`] decides from a
  [`View`], and drivers run whole turns and games.
- [`Solver`]: exact expected values by backward induction over every
  scorecard state.  Under the official rules the empty card is worth
  254.5877 points; the widely cited 254.5896 ([Verhoeff], 1999) uses a
  laxer joker convention that the same engine reproduces when the
  forcing is lifted.

## Bots

- [`HeuristicBot`]: deterministic rules of thumb, microsecond decisions,
  no tables.  Averages 216 points over 10 000 seeded games.
- [`OptimalBot`]: provably perfect solo play backed by the solver.
  Solves lazily by default; `OptimalBot::presolved()` computes the whole
  table up front (seconds in a release build, spread across cores with
  the `parallel` feature).

## Quick start

The rules need no features — inject dice, hold, and score:

```rust
use yahtzee_engine::{Category, Game, TurnAction};

let mut game = Game::new(1);
game.roll("13446".parse()?)?;
game.act(TurnAction::Reroll("44".parse()?))?;
game.roll("24464".parse()?)?;
game.act(TurnAction::Reroll("444".parse()?))?;
game.roll("44446".parse()?)?;
let delta = game.act(TurnAction::Score(Category::FourOfAKind))?;
assert_eq!(delta.expect("scored").value, 22);
# Ok::<(), Box<dyn std::error::Error>>(())
```

Neither does the solver — ask it what any position is worth:

```rust
use yahtzee_engine::{Category, Solver, State};

let mut solver = Solver::new();
// With only Chance open, optimal play is worth exactly 70/3 points.
let state = State::new(!Category::Chance.bit(), 0, false);
assert!((solver.value(state) - 70.0 / 3.0).abs() < 1e-9);
```

With the (default) `rand` feature, roll for real and pit bots against
each other:

```rust
# #[cfg(feature = "rand")]
# fn main() -> Result<(), yahtzee_engine::EngineError> {
use yahtzee_engine::{Game, HeuristicBot, play_game};

let mut game = Game::new(2);
let (mut alice, mut bob) = (HeuristicBot::new(), HeuristicBot::new());
let totals = play_game(&mut game, &mut [&mut alice, &mut bob], &mut rand::rng())?;
println!("final scores: {totals:?}");
# Ok(())
# }
# #[cfg(not(feature = "rand"))]
# fn main() {}
```

Writing your own bot is implementing [`Strategy`]'s two decisions
against a [`View`]; the driver handles all bookkeeping.

## Feature flags

- `rand` (default): dice-rolling helpers (`Game::roll_with`,
  `play_turn`, `play_game`) and the `play`/`arena` examples.  The rules
  and the solver are deterministic; disable it for a dependency-light
  build.
- `parallel`: solves scorecard tiers across the CPU cores via rayon.
  The table is bit-identical to the serial build, it just arrives
  faster.  Off by default.

## Examples

- `solve`: build the full table and print the optimal expected score —
  `cargo run --release --example solve --features parallel`
- `play`: play against the heuristic bot in the terminal —
  `cargo run --release --example play`
- `arena`: bot strength measurements —
  `cargo run --release --example arena -- 10000 --optimal`

## Play in the browser

The [`web/`][web] crate wraps the engine in WebAssembly with the
solver as a live advisor: <https://jdh8.github.io/yahtzee-engine/>.

[Yahtzee]: https://en.wikipedia.org/wiki/Yahtzee
[web]: https://github.com/jdh8/yahtzee-engine/tree/main/web
[Verhoeff]: https://www-set.win.tue.nl/~wstomv/misc/yahtzee/
[`State`]: https://docs.rs/yahtzee-engine/latest/yahtzee_engine/struct.State.html
[`Game`]: https://docs.rs/yahtzee-engine/latest/yahtzee_engine/struct.Game.html
[`Solver`]: https://docs.rs/yahtzee-engine/latest/yahtzee_engine/struct.Solver.html
[`Strategy`]: https://docs.rs/yahtzee-engine/latest/yahtzee_engine/trait.Strategy.html
[`View`]: https://docs.rs/yahtzee-engine/latest/yahtzee_engine/struct.View.html
[`HeuristicBot`]: https://docs.rs/yahtzee-engine/latest/yahtzee_engine/struct.HeuristicBot.html
[`OptimalBot`]: https://docs.rs/yahtzee-engine/latest/yahtzee_engine/struct.OptimalBot.html