yahtzee-engine 0.1.0

Yahtzee rules, scoring, and bots: a fast heuristic and an exact optimal expected-value solver
Documentation
//! An interactive terminal game against the heuristic bot.
//!
//! ```sh
//! cargo run --release --example play
//! ```
//!
//! Commands at the prompt: `k 446` holds those dice and rerolls the
//! rest (`k` alone rerolls everything), `s 3` scores the third listed
//! category, and `q` resigns.

use std::io::Write as _;
use yahtzee_engine::{Category, Game, HeuristicBot, Keep, Phase, Strategy, TurnAction};

fn show_card(game: &Game, seat: usize, name: &str) {
    let card = game.scorecard(seat);
    println!("--- {name} ---");
    for category in Category::ALL {
        match card.get(category) {
            Some(value) => println!("  {category:16} {value:3}"),
            None => println!("  {category:16}   -"),
        }
    }
    println!(
        "  upper {}/63 (+{})  yahtzee bonuses {}  total {}",
        card.upper_subtotal(),
        card.upper_bonus(),
        card.yahtzee_bonuses(),
        card.total()
    );
}

fn prompt(line: &mut String) -> Option<()> {
    print!("> ");
    std::io::stdout().flush().ok()?;
    line.clear();
    (std::io::stdin().read_line(line).ok()? > 0).then_some(())
}

fn human_action(game: &Game) -> Option<TurnAction> {
    let view = game.view();
    let legal: Vec<Category> = view.legal_categories().iter().collect();
    println!("dice {}  ({} roll(s) left)", view.dice(), view.rolls_left());
    for (i, category) in legal.iter().enumerate() {
        let value = view.state().apply(*category, view.dice());
        let value = value.expect("legal").value;
        println!("  s {:2}  {category:16} {value:3}", i + 1);
    }
    let mut line = String::new();
    loop {
        prompt(&mut line)?;
        let input = line.trim();
        if input == "q" {
            return None;
        }
        if let Some(rest) = input.strip_prefix('s')
            && let Ok(i) = rest.trim().parse::<usize>()
            && let Some(&category) = legal.get(i.wrapping_sub(1))
        {
            return Some(TurnAction::Score(category));
        }
        if view.rolls_left() > 0
            && let Some(rest) = input.strip_prefix('k')
            && let Ok(keep) = rest.trim().parse::<Keep>()
            && view.dice().contains(keep)
        {
            return Some(TurnAction::Reroll(keep));
        }
        println!("try `k 446`, `s 3`, or `q`");
    }
}

fn main() {
    let mut rng = rand::rng();
    let mut bot = HeuristicBot::new();
    let mut game = Game::new(2);
    println!("You are seat 0; the heuristic bot is seat 1.");
    while !game.is_over() {
        match game.phase() {
            Phase::AwaitingRoll => {
                let dice = game.roll_with(&mut rng).expect("a roll is due");
                if game.seat() == 1 {
                    println!("bot rolls {dice}");
                }
            }
            Phase::AwaitingAction if game.seat() == 0 => {
                let Some(action) = human_action(&game) else {
                    println!("resigned");
                    return;
                };
                if game.act(action).is_err() {
                    println!("illegal move; try again");
                }
            }
            Phase::AwaitingAction => {
                let action = if game.rolls_left() > 0 {
                    bot.choose_action(&game.view())
                } else {
                    TurnAction::Score(bot.choose_category(&game.view()))
                };
                match action {
                    TurnAction::Reroll(keep) => println!("bot keeps [{keep}]"),
                    TurnAction::Score(category) => println!("bot scores {category}"),
                }
                game.act(action).expect("the bot plays legally");
            }
            Phase::Finished => unreachable!(),
        }
    }
    show_card(&game, 0, "you");
    show_card(&game, 1, bot.name());
    let totals = game.totals();
    println!("final: you {} - {} bot", totals[0], totals[1]);
}