From d0802541f425fd769b2e2bdfc75558b5a2d1d5f7 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 | 91 ++++++++++++++++++++++++++---------------- client_cli/src/main.rs | 59 ++++++++++++++++++++++++++- store/src/dice.rs | 82 +++++++++++++++++++++++++------------ store/src/game.rs | 66 +++++++++++++++--------------- store/src/lib.rs | 1 + 5 files changed, 202 insertions(+), 97 deletions(-) diff --git a/client_cli/src/app.rs b/client_cli/src/app.rs index 4bfcdee..65f93a4 100644 --- a/client_cli/src/app.rs +++ b/client_cli/src/app.rs @@ -1,39 +1,57 @@ use pretty_assertions::assert_eq; -use store::{CheckerMove, GameEvent, GameState, PlayerId}; +use store::{CheckerMove, Dice, DiceRoller, GameEvent, GameState, PlayerId}; + +#[derive(Debug, Default)] +pub struct AppArgs { + pub seed: Option, +} + +// Application Game +#[derive(Debug, Default)] +pub struct Game { + pub state: GameState, + pub dice_roller: DiceRoller, + first_move: Option, + player_id: Option, +} + +impl Game { + // Constructs a new instance of [`App`]. + pub fn new(seed: Option) -> Self { + let mut state = GameState::default(); + // local : player + let player_id: Option = state.init_player("myself"); + state.init_player("adversary"); + Self { + state, + dice_roller: DiceRoller::new(seed), + first_move: None, + player_id, + } + } +} // Application. #[derive(Debug, Default)] pub struct App { // should the application exit? pub should_quit: bool, - pub game: GameState, - first_move: Option, - player_id: Option, + pub game: Game, } impl App { // Constructs a new instance of [`App`]. - pub fn new() -> Self { - // Self::default() - - let mut state = GameState::default(); - // local : player - let player_id: Option = state.init_player("myself"); - state.init_player("adversary"); - println!("player_id ? {:?}", player_id); + pub fn new(args: AppArgs) -> Self { Self { - game: state, + game: Game::new(args.seed.map(|s| s as u64)), should_quit: false, - first_move: None, - player_id, } } fn get_my_player(&mut self) {} - // Constructs a new instance of [`App`]. pub fn start(&mut self) { - self.game = GameState::new(); + self.game.state = GameState::new(); } pub fn input(&mut self, input: &str) { @@ -52,17 +70,19 @@ impl App { } fn roll_dice(&mut self) { - if self.player_id.is_none() { + if self.game.player_id.is_none() { println!("player_id not set "); return; } - self.game.consume(&GameEvent::Roll { - player_id: self.player_id.unwrap(), + let dice = self.game.dice_roller.roll(); + self.game.state.consume(&GameEvent::RollResult { + player_id: self.game.player_id.unwrap(), + dice, }); } fn add_move(&mut self, input: &str) { - if self.player_id.is_none() { + if self.game.player_id.is_none() { println!("player_id not set "); return; } @@ -73,20 +93,20 @@ impl App { if positions.len() == 2 && positions[0] != 0 && positions[1] != 0 { let checker_move = CheckerMove::new(positions[0], positions[1]); if checker_move.is_ok() { - if self.first_move.is_some() { + if self.game.first_move.is_some() { let move_event = GameEvent::Move { - player_id: self.player_id.unwrap(), - moves: (self.first_move.unwrap(), checker_move.unwrap()), + player_id: self.game.player_id.unwrap(), + moves: (self.game.first_move.unwrap(), checker_move.unwrap()), }; - if !self.game.validate(&move_event) { + if !self.game.state.validate(&move_event) { println!("Move invalid"); - self.first_move = None; + self.game.first_move = None; return; } - self.game.consume(&move_event); - self.first_move = None; + self.game.state.consume(&move_event); + self.game.first_move = None; } else { - self.first_move = Some(checker_move.unwrap()); + self.game.first_move = Some(checker_move.unwrap()); } return; } @@ -96,9 +116,9 @@ impl App { pub fn display(&mut self) -> String { let mut output = "-------------------------------".to_owned(); - output = output + "\nRolled dice : " + &self.game.dices.to_display_string(); + output = output + "\nRolled dice : " + &self.game.state.dice.to_display_string(); output = output + "\n-------------------------------"; - output = output + "\n" + &self.game.board.to_display_grid(9); + output = output + "\n" + &self.game.state.board.to_display_grid(9); output } } @@ -144,7 +164,7 @@ Rolled dice : 0 & 0 #[test] fn test_move() { let expected = "------------------------------- -Rolled dice : 0 & 0 +Rolled dice : 2 & 3 ------------------------------- 13 14 15 16 17 18 19 20 21 22 23 24 @@ -167,13 +187,14 @@ Rolled dice : 0 & 0 | | | O | | | | O | | | | O | - | | | O O O | + | | | O O O | ---------------------------------------------------------------- 12 11 10 9 8 7 6 5 4 3 2 1 "; - let mut app = App::new(); + let mut app = App::new(AppArgs { seed: Some(1327) }); + app.input("roll"); + app.input("1 3"); 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..1406297 100644 --- a/store/src/dice.rs +++ b/store/src/dice.rs @@ -1,34 +1,57 @@ use crate::Error; use rand::distributions::{Distribution, Uniform}; +use rand::{rngs::StdRng, SeedableRng}; use serde::{Deserialize, Serialize}; -/// Represents the two dices +#[derive(Debug)] +pub struct DiceRoller { + rng: StdRng, +} + +impl Default for DiceRoller { + fn default() -> Self { + Self::new(None) + } +} + +impl DiceRoller { + pub 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 +84,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..0475677 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 @@ -205,7 +205,7 @@ impl GameState { return false; } } - Roll { player_id } => { + Roll { player_id } | RollResult { player_id, dice: _ } => { // Check player exists if !self.players.contains_key(player_id) { return false; @@ -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; @@ -372,6 +372,16 @@ impl GameState { match valid_event { BeginGame { goes_first } => { self.active_player_id = *goes_first; + // if self.who_plays().is_none() { + // let active_color = match self.dice.coin() { + // false => Color::Black, + // true => Color::White, + // }; + // let color_player_id = self.player_id_by_color(active_color); + // if color_player_id.is_some() { + // self.active_player_id = *color_player_id.unwrap(); + // } + // } self.stage = Stage::InGame; self.turn_stage = TurnStage::RollDice; } @@ -397,8 +407,9 @@ impl GameState { PlayerDisconnected { player_id } => { self.players.remove(player_id); } - Roll { player_id: _ } => { - self.roll(); + Roll { player_id: _ } => {} + RollResult { player_id: _, dice } => { + self.dice = *dice; self.turn_stage = TurnStage::MarkPoints; } Mark { player_id, points } => { @@ -461,6 +472,10 @@ pub enum GameEvent { Roll { player_id: PlayerId, }, + RollResult { + player_id: PlayerId, + dice: Dice, + }, Mark { player_id: PlayerId, points: u8, @@ -471,23 +486,6 @@ pub enum GameEvent { }, } -impl Roll for GameState { - fn roll(&mut self) -> &mut Self { - self.dices = self.dices.roll(); - if self.who_plays().is_none() { - let active_color = match self.dices.coin() { - false => Color::Black, - true => Color::White, - }; - let color_player_id = self.player_id_by_color(active_color); - if color_player_id.is_some() { - self.active_player_id = *color_player_id.unwrap(); - } - } - self - } -} - impl Move for GameState { fn move_checker(&mut self, player: &Player, dice: u8, from: usize) -> Result<&mut Self, Error> { // check if move is permitted @@ -504,7 +502,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 +528,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 +587,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)); } diff --git a/store/src/lib.rs b/store/src/lib.rs index 12f17b4..9eed587 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -11,3 +11,4 @@ mod board; pub use board::CheckerMove; mod dice; +pub use dice::{Dice, DiceRoller};