From 310d6b7afaf2ed5b566a314a503b6d06190426ff Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Mon, 2 Jan 2023 10:33:07 -0800 Subject: [PATCH] random game sampling and basic benchmark --- src/game.rs | 187 +++++++++++++++++++++++++++++++++++++++++++-------- src/main.rs | 21 +++++- src/stack.rs | 4 ++ 3 files changed, 182 insertions(+), 30 deletions(-) diff --git a/src/game.rs b/src/game.rs index 8be4481..e4b4851 100644 --- a/src/game.rs +++ b/src/game.rs @@ -69,7 +69,7 @@ impl Default for Square { } -#[derive(Debug, Default)] +#[derive(Debug, Default, Copy, Clone)] pub struct Game { squares: [Square; 16], dice: EnumMap, @@ -77,12 +77,12 @@ pub struct Game { } impl Game { - fn new() -> Self { + pub fn new() -> Self { Self::default() } // new game with random starting positions - fn new_random() -> Self { + pub fn new_random() -> Self { let mut game = Self::default(); let mut rng = rand::thread_rng(); let mut dice = *&COLORS; @@ -95,20 +95,46 @@ impl Game { game } - fn set_state(&mut self, squares: [Square; 16], dice: EnumMap) { - self.squares = squares; - self.dice = dice; - for (i, square) in self.squares.iter().enumerate() { - if let Square::Camels(stack) = square { - for camel in stack.iter() { - self.camels[*camel] = i - } - } + pub fn set_state(&mut self, camels: &[(Color, usize); 5], dice: &EnumMap) { + for i in 0..16 { + self.squares[i] = match self.squares[i] { + Square::Camels(mut stack) => { + stack.clear(); + Square::Camels(stack) + }, + _ => Square::Camels(Stack::new()) + }; + } + + for square in self.squares { + assert_eq!(square.assume_stack().len(), 0) + } + + self.dice = *dice; + for &(color, sq) in camels { + self.squares[sq].assume_stack_mut().push(color); + self.camels[color] = sq; } } + pub fn get_state(&self) -> ([(Color, usize); 5], EnumMap) { + let mut state = [(Color::Red, 0); 5]; + + let mut j = 0; + for (sq_idx, square) in self.squares.iter().enumerate() { + if let Square::Camels(stack) = square { + for camel in stack.iter() { + state[j] = (*camel, sq_idx); + j += 1; + } + } + } + + (state, self.dice) + } + // returns winner if there is one - fn advance(&mut self, die: Color, roll: usize) -> Option { + pub fn advance(&mut self, die: Color, roll: usize) -> Option { let src_sq = self.camels[die]; let dst_sq = src_sq + roll; if dst_sq >= 16 { @@ -171,7 +197,6 @@ impl Game { } (&mut leg_dice[..]).shuffle(&mut rng); - for color in leg_dice.iter() { let roll = rng.gen_range(1..=3); if let Some(winner) = self.advance(*color, roll) { @@ -189,8 +214,8 @@ impl Game { // we are now guaranteed to be at the start of a new leg, // so we don't need to check the dice state let mut rng = rand::thread_rng(); - let roll_dist = Uniform::from(0..=3); - let mut dice = *&COLORS; // makes a copy of the constant + let roll_dist = Uniform::from(1..=3); + let mut dice = COLORS; // makes a copy of the constant loop { // easiest if we shuffle at the start of the leg @@ -203,34 +228,53 @@ impl Game { } } } + + pub fn project_outcomes(&self, count: usize) -> EnumMap { + let (orig_camels, orig_dice) = self.get_state(); + let mut projection = *self; + + let mut scores: EnumMap = EnumMap::default(); + for i in 0..count { + let winner = projection.finish_game_random(); + scores[winner] += 1; + projection.set_state(&orig_camels, &orig_dice); + } + + scores + } } #[cfg(test)] mod test { use super::*; + use Color::*; #[test] fn test_advance() { - use Color::*; let mut game = Game::new(); // all dice are false (not rolled) to start with assert_eq!(game.dice.values().any(|&v| v), false); - - let mut squares = [Square::Camels(Default::default()); 16]; - let one = squares[0].assume_stack_mut(); - one.push(Blue); - one.push(Yellow); - squares[1].assume_stack_mut().push(Red); - let three = squares[2].assume_stack_mut(); - three.push(Green); - three.push(Purple); + + let camel_state = [ + (Blue, 0), + (Yellow, 0), + (Red, 1), + (Green, 2), + (Purple, 2), + ]; + game.set_state(&camel_state, &Default::default()); + assert_eq!(game.squares[0].assume_stack(), &Stack::from([Blue, Yellow])); + assert_eq!(game.camels[Blue], 0); + assert_eq!(game.camels[Yellow], 0); + assert_eq!(game.squares[1].assume_stack(), &Stack::from([Red])); + assert_eq!(game.camels[Red], 1); + assert_eq!(game.squares[2].assume_stack(), &Stack::from([Green, Purple])); + assert_eq!(game.camels[Green], 2); + assert_eq!(game.camels[Purple], 2); // BY, R, GP - game.set_state(squares, Default::default()); - game.advance(Yellow, 2); - println!("{:?}", game.camels); assert_eq!(game.dice[Yellow], true); assert_eq!(game.camels[Yellow], 2); assert_eq!(game.squares[2].assume_stack(), &Stack::from([Green, Purple, Yellow])); @@ -247,5 +291,90 @@ mod test { // B, _, G, RPY } + #[test] + fn test_new_random() { + for _ in 0..100 { + let game = Game::new_random(); + for (camel, i) in game.camels { + assert!(i < 3); // since we've only rolled the die once for each camel + let stack = game.squares[i].assume_stack(); + assert!(stack[..].contains(&camel)); + } + } + } + #[test] + fn test_finish_leg() { + let mut game = Game::new(); + let camel_state = [ + (Purple, 0), + (Blue, 0), + (Green, 1), + (Red, 1), + (Yellow, 2), + ]; + game.set_state(&camel_state, &Default::default()); + // PB, G, RY + game.advance(Green, 2); + // PB, _, RY, G + game.advance(Purple, 1); + // _, PB, RY, G + + // since this is randomized, we should do it a bunch of times to make sure + for _ in 0..100 { + let mut projection = game; // copy? + assert_eq!(projection.squares[1].assume_stack(), &Stack::from([Purple, Blue])); + projection.finish_leg_random(); + // since we already rolled Green, it can't have moved + assert_eq!(projection.camels[Green], 3); + // likewise purple + assert_eq!(projection.camels[Purple], 1); + // blue, red,and yellow, on the other hand, *must* have moved + assert_ne!(projection.camels[Blue], 1); + assert_ne!(projection.camels[Red], 2); + assert_ne!(projection.camels[Yellow], 2); + } + } + + #[test] + fn test_finish_leg_winner() { + let mut game = Game::new(); + let camel_state = [ + (Green, 13), + (Red, 14), + (Purple, 14), + (Blue, 15), + (Yellow, 15), + ]; + game.set_state(&camel_state, &Default::default()); + + // since there are no tiles involved, and multiple camels are on 15, there must be a winner + assert!(matches!(game.finish_leg_random(), Some(_))); + } + + #[test] + fn test_project_outcomes() { + let mut game = Game::new(); + let camel_state = [ + (Blue, 1), + (Green, 2), + (Yellow, 2), + (Purple, 4), + (Red, 10), + ]; + game.set_state(&camel_state, &Default::default()); + // _, B, GY, _, P, _, _, _, _, _, R + + let scores = game.project_outcomes(10_000); + let mut max = 0; + let mut winner = Blue; // just "anything that's not red" + for (color, score) in scores { + if score > max { + max = score; + winner = color; + } + } + + assert_eq!(winner, Red); + } } diff --git a/src/main.rs b/src/main.rs index 79a05f3..e1ab2d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,26 @@ +use std::time::Instant; + mod stack; mod game; +use game::Game; + fn main() { - println!("Hello, world!"); + let n_games = 10_000_000; + + let start = Instant::now(); + let game = Game::new_random(); + let _scores = game.project_outcomes(n_games); + let end = Instant::now(); + + let elapsed = end.duration_since(start); + let secs = elapsed.as_secs(); + let hundredths = elapsed.subsec_millis() / 10; // technically not accurate but good enough for now + + let rate = (10_000_000 as f64) / elapsed.as_secs_f64(); + + println!("Test completed:"); + println!("{n_games} in {secs}.{hundredths:02} seconds", ); + println!("Games per second: {rate}"); } diff --git a/src/stack.rs b/src/stack.rs index 99f81e6..0cf5816 100644 --- a/src/stack.rs +++ b/src/stack.rs @@ -42,6 +42,10 @@ impl Stack { len: S, } } + + pub fn into_inner(self) -> [T; S] { + self.data + } }