From 0b09a517a2a90078fabaa7986c729458c20b7053 Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Mon, 11 Mar 2024 20:45:36 +0100 Subject: [PATCH] wip rng seed --- client_cli/src/app.rs | 12 +++++-- client_cli/src/main.rs | 59 ++++++++++++++++++++++++++++-- store/src/dice.rs | 81 ++++++++++++++++++++++++++++-------------- store/src/game.rs | 32 ++++++++--------- 4 files changed, 138 insertions(+), 46 deletions(-) diff --git a/client_cli/src/app.rs b/client_cli/src/app.rs index 4bfcdee..bf357fa 100644 --- a/client_cli/src/app.rs +++ b/client_cli/src/app.rs @@ -1,19 +1,26 @@ +use dice::DiceRoller; use pretty_assertions::assert_eq; use store::{CheckerMove, GameEvent, GameState, PlayerId}; +#[derive(Debug, Default)] +pub struct AppArgs { + pub seed: Option, +} + // Application. #[derive(Debug, Default)] pub struct App { // should the application exit? pub should_quit: bool, pub game: GameState, + pub dice_roller: DiceRoller, first_move: Option, player_id: Option, } impl App { // Constructs a new instance of [`App`]. - pub fn new() -> Self { + pub fn new(args: AppArgs) -> Self { // Self::default() let mut state = GameState::default(); @@ -23,6 +30,7 @@ impl App { println!("player_id ? {:?}", player_id); Self { game: state, + dice_roller: DiceRoller::new(args.seed), should_quit: false, first_move: None, player_id, @@ -171,7 +179,7 @@ Rolled dice : 0 & 0 ---------------------------------------------------------------- 12 11 10 9 8 7 6 5 4 3 2 1 "; - let mut app = App::new(); + let mut app = App::new(AppArgs::default()); app.input("1 4"); app.input("1 5"); self::assert_eq!(app.display(), expected); diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 1ed2455..007d2d4 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -2,12 +2,38 @@ pub mod app; use anyhow::Result; -use app::App; +use app::{App, AppArgs}; use std::io; +// see pico-args example at https://github.com/RazrFalcon/pico-args/blob/master/examples/app.rs +const HELP: &str = "\ +Trictrac CLI + +USAGE: + trictrac-cli [OPTIONS] + +FLAGS: + -h, --help Prints help information + +OPTIONS: + --seed SEED Sets the random generator seed + +ARGS: + +"; + fn main() -> Result<()> { + let args = match parse_args() { + Ok(v) => v, + Err(e) => { + eprintln!("Error: {}.", e); + std::process::exit(1); + } + }; + // println!("{:#?}", args); + // Create an application. - let mut app = App::new(); + let mut app = App::new(args); // Start the main loop. while !app.should_quit { @@ -19,3 +45,32 @@ fn main() -> Result<()> { Ok(()) } + +fn parse_args() -> Result { + let mut pargs = pico_args::Arguments::from_env(); + + // Help has a higher priority and should be handled separately. + if pargs.contains(["-h", "--help"]) { + print!("{}", HELP); + std::process::exit(0); + } + + let args = AppArgs { + // Parses an optional value that implements `FromStr`. + seed: pargs.opt_value_from_str("--seed")?, + // Parses an optional value from `&str` using a specified function. + // width: pargs.opt_value_from_fn("--width", parse_width)?.unwrap_or(10), + }; + + // It's up to the caller what to do with the remaining arguments. + let remaining = pargs.finish(); + if !remaining.is_empty() { + eprintln!("Warning: unused arguments left: {:?}.", remaining); + } + + Ok(args) +} + +// fn parse_width(s: &str) -> Result { +// s.parse().map_err(|_| "not a number") +// } diff --git a/store/src/dice.rs b/store/src/dice.rs index e258d1f..e8f3854 100644 --- a/store/src/dice.rs +++ b/store/src/dice.rs @@ -1,34 +1,56 @@ use crate::Error; use rand::distributions::{Distribution, Uniform}; +use rand::{rngs::StdRng, SeedableRng}; use serde::{Deserialize, Serialize}; -/// Represents the two dices +pub struct DiceRoller { + rng: StdRng, +} + +impl Default for DiceRoller { + fn default() -> Self { + Self::new(None) + } +} + +impl DiceRoller { + fn new(opt_seed: Option) -> Self { + Self { + rng: match opt_seed { + None => StdRng::from_rng(rand::thread_rng()).unwrap(), + Some(seed) => SeedableRng::seed_from_u64(seed), + }, + } + } + + /// Roll the dices which generates two random numbers between 1 and 6, replicating a perfect + /// dice. We use the operating system's random number generator. + pub fn roll(&mut self) -> Dice { + let between = Uniform::new_inclusive(1, 6); + + let v = (between.sample(&mut self.rng), between.sample(&mut self.rng)); + + Dice { values: (v.0, v.1) } + } + + // Heads or tails + // pub fn coin(self) -> bool { + // let between = Uniform::new_inclusive(1, 2); + // let mut rng = rand::thread_rng(); + // between.sample(&mut rng) == 1 + // } +} + +/// Represents the two dice /// -/// Trictrac is always played with two dices. +/// Trictrac is always played with two dice. #[derive(Debug, Clone, Copy, Serialize, PartialEq, Deserialize, Default)] -pub struct Dices { +pub struct Dice { /// The two dice values pub values: (u8, u8), } -impl Dices { - /// Roll the dices which generates two random numbers between 1 and 6, replicating a perfect - /// dice. We use the operating system's random number generator. - pub fn roll(self) -> Self { - let between = Uniform::new_inclusive(1, 6); - let mut rng = rand::thread_rng(); - - let v = (between.sample(&mut rng), between.sample(&mut rng)); - - Dices { values: (v.0, v.1) } - } - - /// Heads or tails - pub fn coin(self) -> bool { - let between = Uniform::new_inclusive(1, 2); - let mut rng = rand::thread_rng(); - between.sample(&mut rng) == 1 - } +impl Dice { pub fn to_bits_string(self) -> String { format!("{:0>3b}{:0>3b}", self.values.0, self.values.1) } @@ -61,14 +83,21 @@ mod tests { #[test] fn test_roll() { - let dices = Dices::default().roll(); - assert!(dices.values.0 >= 1 && dices.values.0 <= 6); - assert!(dices.values.1 >= 1 && dices.values.1 <= 6); + let dice = DiceRoller::default().roll(); + assert!(dice.values.0 >= 1 && dice.values.0 <= 6); + assert!(dice.values.1 >= 1 && dice.values.1 <= 6); + } + + #[test] + fn test_seed() { + let dice = DiceRoller::new(Some(123)).roll(); + assert!(dice.values.0 == 3); + assert!(dice.values.1 == 2); } #[test] fn test_to_bits_string() { - let dices = Dices { values: (4, 2) }; - assert!(dices.to_bits_string() == "100010"); + let dice = Dice { values: (4, 2) }; + assert!(dice.to_bits_string() == "100010"); } } diff --git a/store/src/game.rs b/store/src/game.rs index 977648a..de77c10 100644 --- a/store/src/game.rs +++ b/store/src/game.rs @@ -1,6 +1,6 @@ //! # Play a TricTrac Game use crate::board::{Board, CheckerMove, Field, Move}; -use crate::dice::{Dices, Roll}; +use crate::dice::{Dice, DiceRoller, Roll}; use crate::player::{Color, Player, PlayerId}; use crate::Error; use log::{error, info}; @@ -39,7 +39,7 @@ pub struct GameState { pub players: HashMap, pub history: Vec, /// last dice pair rolled - pub dices: Dices, + pub dice: Dice, /// true if player needs to roll first roll_first: bool, } @@ -48,7 +48,7 @@ pub struct GameState { impl fmt::Display for GameState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut s = String::new(); - s.push_str(&format!("Dices: {:?}\n", self.dices)); + s.push_str(&format!("Dice: {:?}\n", self.dice)); // s.push_str(&format!("Who plays: {}\n", self.who_plays().map(|player| &player.name ).unwrap_or(""))); s.push_str(&format!("Board: {:?}\n", self.board)); write!(f, "{}", s) @@ -64,7 +64,7 @@ impl Default for GameState { active_player_id: 0, players: HashMap::new(), history: Vec::new(), - dices: Dices::default(), + dice: Dice::default(), roll_first: true, } } @@ -109,7 +109,7 @@ impl GameState { pos_bits.push_str(step_bits); // dice roll -> 6 bits - let dice_bits = self.dices.to_bits_string(); + let dice_bits = self.dice.to_bits_string(); pos_bits.push_str(&dice_bits); // points 10bits x2 joueurs = 20bits @@ -243,7 +243,7 @@ impl GameState { return false; } - // Check moves conforms to the dices + // Check moves conforms to the dice if !self.moves_follows_dices(color, moves) { return false; } @@ -278,7 +278,7 @@ impl GameState { } fn moves_follows_dices(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { - let (dice1, dice2) = self.dices.values; + let (dice1, dice2) = self.dice.values; let (move1, move2): &(CheckerMove, CheckerMove) = moves.into(); let dist1 = (move1.get_to() - move1.get_from()) as u8; let dist2 = (move2.get_to() - move2.get_from()) as u8; @@ -473,9 +473,9 @@ pub enum GameEvent { impl Roll for GameState { fn roll(&mut self) -> &mut Self { - self.dices = self.dices.roll(); + self.dice = self.dice.roll(); if self.who_plays().is_none() { - let active_color = match self.dices.coin() { + let active_color = match self.dice.coin() { false => Color::Black, true => Color::White, }; @@ -504,7 +504,7 @@ impl Move for GameState { // self.board.set(player, new_position as usize, 1)?; } - // switch to other player if all dices have been consumed + // switch to other player if all dice have been consumed self.switch_active_player(); self.roll_first = true; @@ -530,7 +530,7 @@ impl Move for GameState { } // check if dice value has actually been rolled - if dice != self.dices.values.0 && dice != self.dices.values.1 { + if dice != self.dice.values.0 && dice != self.dice.values.1 { return Err(Error::DiceInvalid); } @@ -589,16 +589,16 @@ mod tests { goes_first: player_id, }); state.consume(&GameEvent::Roll { player_id }); - let dices = state.dices.values; + let dice = state.dice.values; let moves = ( - CheckerMove::new(1, (1 + dices.0).into()).unwrap(), - CheckerMove::new((1 + dices.0).into(), (1 + dices.0 + dices.1).into()).unwrap(), + CheckerMove::new(1, (1 + dice.0).into()).unwrap(), + CheckerMove::new((1 + dice.0).into(), (1 + dice.0 + dice.1).into()).unwrap(), ); assert!(state.moves_follows_dices(&Color::White, &moves)); let badmoves = ( - CheckerMove::new(1, (2 + dices.0).into()).unwrap(), - CheckerMove::new((1 + dices.0).into(), (1 + dices.0 + dices.1).into()).unwrap(), + CheckerMove::new(1, (2 + dice.0).into()).unwrap(), + CheckerMove::new((1 + dice.0).into(), (1 + dice.0 + dice.1).into()).unwrap(), ); assert!(!state.moves_follows_dices(&Color::White, &badmoves)); }