diff --git a/store/Cargo.toml b/store/Cargo.toml index 935a2a0..a9234ff 100644 --- a/store/Cargo.toml +++ b/store/Cargo.toml @@ -25,9 +25,5 @@ rand = "0.9" serde = { version = "1.0", features = ["derive"] } transpose = "0.2.2" -[[bin]] -name = "random_game" -path = "src/bin/random_game.rs" - [build-dependencies] cxx-build = "1.0" diff --git a/store/src/bin/random_game.rs b/store/src/bin/random_game.rs deleted file mode 100644 index 6da3b9c..0000000 --- a/store/src/bin/random_game.rs +++ /dev/null @@ -1,262 +0,0 @@ -//! Run one or many games of trictrac between two random players. -//! In single-game mode, prints play-by-play like OpenSpiel's `example.cc`. -//! In multi-game mode, runs silently and reports throughput at the end. -//! -//! Usage: -//! cargo run --bin random_game -- [--seed ] [--games ] [--max-steps ] [--verbose] - -use std::borrow::Cow; -use std::env; -use std::time::Instant; - -use trictrac_store::{ - training_common::sample_valid_action, - Dice, DiceRoller, GameEvent, GameState, Stage, TurnStage, -}; - -// ── CLI args ────────────────────────────────────────────────────────────────── - -struct Args { - seed: Option, - games: usize, - max_steps: usize, - verbose: bool, -} - -fn parse_args() -> Args { - let args: Vec = env::args().collect(); - let mut seed = None; - let mut games = 1; - let mut max_steps = 10_000; - let mut verbose = false; - - let mut i = 1; - while i < args.len() { - match args[i].as_str() { - "--seed" => { - i += 1; - seed = args.get(i).and_then(|s| s.parse().ok()); - } - "--games" => { - i += 1; - if let Some(v) = args.get(i).and_then(|s| s.parse().ok()) { - games = v; - } - } - "--max-steps" => { - i += 1; - if let Some(v) = args.get(i).and_then(|s| s.parse().ok()) { - max_steps = v; - } - } - "--verbose" => verbose = true, - _ => {} - } - i += 1; - } - - Args { - seed, - games, - max_steps, - verbose, - } -} - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -fn player_label(id: u64) -> &'static str { - if id == 1 { "White" } else { "Black" } -} - -/// Apply a `Roll` + `RollResult` in one logical step, returning the dice. -/// This collapses the two-step dice phase into a single "chance node" action, -/// matching how the OpenSpiel layer exposes it. -fn apply_dice_roll(state: &mut GameState, roller: &mut DiceRoller) -> Result { - // RollDice → RollWaiting - state - .consume(&GameEvent::Roll { player_id: state.active_player_id }) - .map_err(|e| format!("Roll event failed: {e}"))?; - - // RollWaiting → Move / HoldOrGoChoice (or Stage::Ended if 13th hole) - let dice = roller.roll(); - state - .consume(&GameEvent::RollResult { player_id: state.active_player_id, dice }) - .map_err(|e| format!("RollResult event failed: {e}"))?; - - Ok(dice) -} - -/// Sample a random action and apply it to `state`, handling the Black-mirror -/// transform exactly as `cxxengine.rs::apply_action` does: -/// -/// 1. For Black, build a mirrored view of the state so that `sample_valid_action` -/// and `to_event` always reason from White's perspective. -/// 2. Mirror the resulting event back to the original coordinate frame before -/// calling `state.consume`. -/// -/// Returns the chosen action (in the view's coordinate frame) for display. -fn apply_player_action(state: &mut GameState) -> Result<(), String> { - let needs_mirror = state.active_player_id == 2; - - // Build a White-perspective view: borrowed for White, owned mirror for Black. - let view: Cow = if needs_mirror { - Cow::Owned(state.mirror()) - } else { - Cow::Borrowed(state) - }; - - let action = sample_valid_action(&view) - .ok_or_else(|| format!("no valid action in stage {:?}", state.turn_stage))?; - - let event = action - .to_event(&view) - .ok_or_else(|| format!("could not convert {action:?} to event"))?; - - // Translate the event from the view's frame back to the game's frame. - let event = if needs_mirror { event.get_mirror(false) } else { event }; - - state - .consume(&event) - .map_err(|e| format!("consume({action:?}): {e}"))?; - - Ok(()) -} - -// ── Single game ──────────────────────────────────────────────────────────────── - -/// Run one full game, optionally printing play-by-play. -/// Returns `(steps, truncated)`. -fn run_game(roller: &mut DiceRoller, max_steps: usize, quiet: bool, verbose: bool) -> (usize, bool) { - let mut state = GameState::new_with_players("White", "Black"); - let mut step = 0usize; - - if !quiet { - println!("{state}"); - } - - while state.stage != Stage::Ended { - step += 1; - if step > max_steps { - return (step - 1, true); - } - - match state.turn_stage { - TurnStage::RollDice => { - let player = state.active_player_id; - match apply_dice_roll(&mut state, roller) { - Ok(dice) => { - if !quiet { - println!( - "[step {step:4}] {} rolls: {} & {}", - player_label(player), - dice.values.0, - dice.values.1 - ); - } - } - Err(e) => { - eprintln!("Error during dice roll: {e}"); - eprintln!("State:\n{state}"); - return (step, true); - } - } - } - stage => { - let player = state.active_player_id; - match apply_player_action(&mut state) { - Ok(()) => { - if !quiet { - println!( - "[step {step:4}] {} ({stage:?})", - player_label(player) - ); - if verbose { - println!("{state}"); - } - } - } - Err(e) => { - eprintln!("Error: {e}"); - eprintln!("State:\n{state}"); - return (step, true); - } - } - } - } - } - - if !quiet { - println!("\n=== Game over after {step} steps ===\n"); - println!("{state}"); - - let white = state.players.get(&1); - let black = state.players.get(&2); - - match (white, black) { - (Some(w), Some(b)) => { - println!("White — holes: {:2}, points: {:2}", w.holes, w.points); - println!("Black — holes: {:2}, points: {:2}", b.holes, b.points); - println!(); - - let white_score = w.holes as i32 * 12 + w.points as i32; - let black_score = b.holes as i32 * 12 + b.points as i32; - - if white_score > black_score { - println!("Winner: White (+{})", white_score - black_score); - } else if black_score > white_score { - println!("Winner: Black (+{})", black_score - white_score); - } else { - println!("Draw"); - } - } - _ => eprintln!("Could not read final player scores."), - } - } - - (step, false) -} - -// ── Main ────────────────────────────────────────────────────────────────────── - -fn main() { - let args = parse_args(); - let mut roller = DiceRoller::new(args.seed); - - if args.games == 1 { - println!("=== Trictrac — random game ==="); - if let Some(s) = args.seed { - println!("seed: {s}"); - } - println!(); - run_game(&mut roller, args.max_steps, false, args.verbose); - } else { - println!("=== Trictrac — {} games ===", args.games); - if let Some(s) = args.seed { - println!("seed: {s}"); - } - println!(); - - let mut total_steps = 0u64; - let mut truncated = 0usize; - - let t0 = Instant::now(); - for _ in 0..args.games { - let (steps, trunc) = run_game(&mut roller, args.max_steps, !args.verbose, args.verbose); - total_steps += steps as u64; - if trunc { - truncated += 1; - } - } - let elapsed = t0.elapsed(); - - let secs = elapsed.as_secs_f64(); - println!("Games : {}", args.games); - println!("Truncated : {truncated}"); - println!("Total steps: {total_steps}"); - println!("Avg steps : {:.1}", total_steps as f64 / args.games as f64); - println!("Elapsed : {:.3} s", secs); - println!("Throughput : {:.1} games/s", args.games as f64 / secs); - println!(" {:.0} steps/s", total_steps as f64 / secs); - } -} diff --git a/store/src/game_rules_moves.rs b/store/src/game_rules_moves.rs index 955ab3c..41221f2 100644 --- a/store/src/game_rules_moves.rs +++ b/store/src/game_rules_moves.rs @@ -361,24 +361,17 @@ impl MoveRules { let _ = board_to_check.move_checker(&Color::White, moves.0); let farthest_on_move2 = Self::get_board_exit_farthest(&board_to_check); - // dice normal order - let (is_move1_exedant, is_move2_exedant) = self.move_excedants(moves, true); - let is_not_farthest1 = (is_move1_exedant && moves.0.get_from() != farthest_on_move1) - || (is_move2_exedant && moves.1.get_from() != farthest_on_move2); - - // dice reversed order - let (is_move1_exedant, is_move2_exedant) = self.move_excedants(moves, false); - let is_not_farthest2 = (is_move1_exedant && moves.0.get_from() != farthest_on_move1) - || (is_move2_exedant && moves.1.get_from() != farthest_on_move2); - - if is_not_farthest1 && is_not_farthest2 { + let (is_move1_exedant, is_move2_exedant) = self.move_excedants(moves); + if (is_move1_exedant && moves.0.get_from() != farthest_on_move1) + || (is_move2_exedant && moves.1.get_from() != farthest_on_move2) + { return Err(MoveError::ExitNotFarthest); } Ok(()) } - fn move_excedants(&self, moves: &(CheckerMove, CheckerMove), dice_order: bool) -> (bool, bool) { + fn move_excedants(&self, moves: &(CheckerMove, CheckerMove)) -> (bool, bool) { let move1to = if moves.0.get_to() == 0 { 25 } else { @@ -393,16 +386,20 @@ impl MoveRules { }; let dist2 = move2to - moves.1.get_from(); - let (dice1, dice2) = if dice_order { - self.dice.values - } else { - (self.dice.values.1, self.dice.values.0) - }; + let dist_min = cmp::min(dist1, dist2); + let dist_max = cmp::max(dist1, dist2); - ( - dist1 != 0 && dist1 < dice1 as usize, - dist2 != 0 && dist2 < dice2 as usize, - ) + let dice_min = cmp::min(self.dice.values.0, self.dice.values.1) as usize; + let dice_max = cmp::max(self.dice.values.0, self.dice.values.1) as usize; + + let min_excedant = dist_min != 0 && dist_min < dice_min; + let max_excedant = dist_max != 0 && dist_max < dice_max; + + if dist_min == dist1 { + (min_excedant, max_excedant) + } else { + (max_excedant, min_excedant) + } } fn get_board_exit_farthest(board: &Board) -> Field { @@ -1590,19 +1587,6 @@ mod tests { CheckerMove::new(22, 0).unwrap(), ); assert!(state.check_exit_rules(&moves).is_ok()); - - state.dice.values = (6, 4); - state.board.set_positions( - &crate::Color::White, - [ - -4, -1, -2, -1, 0, 0, 0, -1, 0, 0, 0, 0, -5, -1, 0, 0, 0, 0, 2, 3, 2, 2, 5, 1, - ], - ); - let moves = ( - CheckerMove::new(20, 24).unwrap(), - CheckerMove::new(23, 0).unwrap(), - ); - assert!(state.check_exit_rules(&moves).is_ok()); } #[test]