This commit is contained in:
Henri Bourcereau 2024-03-11 20:45:36 +01:00
parent 44c040b414
commit a886526fcf
5 changed files with 205 additions and 97 deletions

View file

@ -1,39 +1,60 @@
use pretty_assertions::assert_eq; 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<u32>,
}
// Application Game
#[derive(Debug, Default)]
pub struct Game {
pub state: GameState,
pub dice_roller: DiceRoller,
first_move: Option<CheckerMove>,
player_id: Option<PlayerId>,
}
impl Game {
// Constructs a new instance of [`App`].
pub fn new(seed: Option<u64>) -> Self {
let mut state = GameState::default();
// local : player
let player_id: Option<PlayerId> = state.init_player("myself");
state.init_player("adversary");
state.consume(&GameEvent::BeginGame {
goes_first: player_id.unwrap(),
});
Self {
state,
dice_roller: DiceRoller::new(seed),
first_move: None,
player_id,
}
}
}
// Application. // Application.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct App { pub struct App {
// should the application exit? // should the application exit?
pub should_quit: bool, pub should_quit: bool,
pub game: GameState, pub game: Game,
first_move: Option<CheckerMove>,
player_id: Option<PlayerId>,
} }
impl App { impl App {
// Constructs a new instance of [`App`]. // Constructs a new instance of [`App`].
pub fn new() -> Self { pub fn new(args: AppArgs) -> Self {
// Self::default()
let mut state = GameState::default();
// local : player
let player_id: Option<PlayerId> = state.init_player("myself");
state.init_player("adversary");
println!("player_id ? {:?}", player_id);
Self { Self {
game: state, game: Game::new(args.seed.map(|s| s as u64)),
should_quit: false, should_quit: false,
first_move: None,
player_id,
} }
} }
fn get_my_player(&mut self) {} fn get_my_player(&mut self) {}
// Constructs a new instance of [`App`].
pub fn start(&mut self) { pub fn start(&mut self) {
self.game = GameState::new(); self.game.state = GameState::new();
} }
pub fn input(&mut self, input: &str) { pub fn input(&mut self, input: &str) {
@ -52,17 +73,19 @@ impl App {
} }
fn roll_dice(&mut self) { fn roll_dice(&mut self) {
if self.player_id.is_none() { if self.game.player_id.is_none() {
println!("player_id not set "); println!("player_id not set ");
return; return;
} }
self.game.consume(&GameEvent::Roll { let dice = self.game.dice_roller.roll();
player_id: self.player_id.unwrap(), self.game.state.consume(&GameEvent::RollResult {
player_id: self.game.player_id.unwrap(),
dice,
}); });
} }
fn add_move(&mut self, input: &str) { 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 "); println!("player_id not set ");
return; return;
} }
@ -73,20 +96,20 @@ impl App {
if positions.len() == 2 && positions[0] != 0 && positions[1] != 0 { if positions.len() == 2 && positions[0] != 0 && positions[1] != 0 {
let checker_move = CheckerMove::new(positions[0], positions[1]); let checker_move = CheckerMove::new(positions[0], positions[1]);
if checker_move.is_ok() { if checker_move.is_ok() {
if self.first_move.is_some() { if self.game.first_move.is_some() {
let move_event = GameEvent::Move { let move_event = GameEvent::Move {
player_id: self.player_id.unwrap(), player_id: self.game.player_id.unwrap(),
moves: (self.first_move.unwrap(), checker_move.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"); println!("Move invalid");
self.first_move = None; self.game.first_move = None;
return; return;
} }
self.game.consume(&move_event); self.game.state.consume(&move_event);
self.first_move = None; self.game.first_move = None;
} else { } else {
self.first_move = Some(checker_move.unwrap()); self.game.first_move = Some(checker_move.unwrap());
} }
return; return;
} }
@ -96,9 +119,9 @@ impl App {
pub fn display(&mut self) -> String { pub fn display(&mut self) -> String {
let mut output = "-------------------------------".to_owned(); 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-------------------------------";
output = output + "\n" + &self.game.board.to_display_grid(9); output = output + "\n" + &self.game.state.board.to_display_grid(9);
output output
} }
} }
@ -144,7 +167,7 @@ Rolled dice : 0 & 0
#[test] #[test]
fn test_move() { fn test_move() {
let expected = "------------------------------- let expected = "-------------------------------
Rolled dice : 0 & 0 Rolled dice : 2 & 3
------------------------------- -------------------------------
13 14 15 16 17 18 19 20 21 22 23 24 13 14 15 16 17 18 19 20 21 22 23 24
@ -167,13 +190,14 @@ Rolled dice : 0 & 0
| | | O | | | | O |
| | | O | | | | O |
| | | O | | | | O |
| | | O O O | | | | O O O |
---------------------------------------------------------------- ----------------------------------------------------------------
12 11 10 9 8 7 6 5 4 3 2 1 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 4");
app.input("1 5");
self::assert_eq!(app.display(), expected); self::assert_eq!(app.display(), expected);
} }
} }

View file

@ -2,12 +2,38 @@
pub mod app; pub mod app;
use anyhow::Result; use anyhow::Result;
use app::App; use app::{App, AppArgs};
use std::io; 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:
<INPUT>
";
fn main() -> Result<()> { 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. // Create an application.
let mut app = App::new(); let mut app = App::new(args);
// Start the main loop. // Start the main loop.
while !app.should_quit { while !app.should_quit {
@ -19,3 +45,32 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
fn parse_args() -> Result<AppArgs, pico_args::Error> {
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<u32, &'static str> {
// s.parse().map_err(|_| "not a number")
// }

View file

@ -1,34 +1,57 @@
use crate::Error; use crate::Error;
use rand::distributions::{Distribution, Uniform}; use rand::distributions::{Distribution, Uniform};
use rand::{rngs::StdRng, SeedableRng};
use serde::{Deserialize, Serialize}; 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<u64>) -> 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)] #[derive(Debug, Clone, Copy, Serialize, PartialEq, Deserialize, Default)]
pub struct Dices { pub struct Dice {
/// The two dice values /// The two dice values
pub values: (u8, u8), 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 { pub fn to_bits_string(self) -> String {
format!("{:0>3b}{:0>3b}", self.values.0, self.values.1) format!("{:0>3b}{:0>3b}", self.values.0, self.values.1)
} }
@ -61,14 +84,21 @@ mod tests {
#[test] #[test]
fn test_roll() { fn test_roll() {
let dices = Dices::default().roll(); let dice = DiceRoller::default().roll();
assert!(dices.values.0 >= 1 && dices.values.0 <= 6); assert!(dice.values.0 >= 1 && dice.values.0 <= 6);
assert!(dices.values.1 >= 1 && dices.values.1 <= 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] #[test]
fn test_to_bits_string() { fn test_to_bits_string() {
let dices = Dices { values: (4, 2) }; let dice = Dice { values: (4, 2) };
assert!(dices.to_bits_string() == "100010"); assert!(dice.to_bits_string() == "100010");
} }
} }

View file

@ -1,6 +1,6 @@
//! # Play a TricTrac Game //! # Play a TricTrac Game
use crate::board::{Board, CheckerMove, Field, Move}; 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::player::{Color, Player, PlayerId};
use crate::Error; use crate::Error;
use log::{error, info}; use log::{error, info};
@ -39,7 +39,7 @@ pub struct GameState {
pub players: HashMap<PlayerId, Player>, pub players: HashMap<PlayerId, Player>,
pub history: Vec<GameEvent>, pub history: Vec<GameEvent>,
/// last dice pair rolled /// last dice pair rolled
pub dices: Dices, pub dice: Dice,
/// true if player needs to roll first /// true if player needs to roll first
roll_first: bool, roll_first: bool,
} }
@ -48,7 +48,7 @@ pub struct GameState {
impl fmt::Display for GameState { impl fmt::Display for GameState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = String::new(); 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!("Who plays: {}\n", self.who_plays().map(|player| &player.name ).unwrap_or("")));
s.push_str(&format!("Board: {:?}\n", self.board)); s.push_str(&format!("Board: {:?}\n", self.board));
write!(f, "{}", s) write!(f, "{}", s)
@ -64,7 +64,7 @@ impl Default for GameState {
active_player_id: 0, active_player_id: 0,
players: HashMap::new(), players: HashMap::new(),
history: Vec::new(), history: Vec::new(),
dices: Dices::default(), dice: Dice::default(),
roll_first: true, roll_first: true,
} }
} }
@ -109,7 +109,7 @@ impl GameState {
pos_bits.push_str(step_bits); pos_bits.push_str(step_bits);
// dice roll -> 6 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); pos_bits.push_str(&dice_bits);
// points 10bits x2 joueurs = 20bits // points 10bits x2 joueurs = 20bits
@ -205,7 +205,7 @@ impl GameState {
return false; return false;
} }
} }
Roll { player_id } => { Roll { player_id } | RollResult { player_id, dice: _ } => {
// Check player exists // Check player exists
if !self.players.contains_key(player_id) { if !self.players.contains_key(player_id) {
return false; return false;
@ -243,7 +243,7 @@ impl GameState {
return false; return false;
} }
// Check moves conforms to the dices // Check moves conforms to the dice
if !self.moves_follows_dices(color, moves) { if !self.moves_follows_dices(color, moves) {
return false; return false;
} }
@ -278,7 +278,7 @@ impl GameState {
} }
fn moves_follows_dices(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { 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 (move1, move2): &(CheckerMove, CheckerMove) = moves.into();
let dist1 = (move1.get_to() - move1.get_from()) as u8; let dist1 = (move1.get_to() - move1.get_from()) as u8;
let dist2 = (move2.get_to() - move2.get_from()) as u8; let dist2 = (move2.get_to() - move2.get_from()) as u8;
@ -372,6 +372,16 @@ impl GameState {
match valid_event { match valid_event {
BeginGame { goes_first } => { BeginGame { goes_first } => {
self.active_player_id = *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.stage = Stage::InGame;
self.turn_stage = TurnStage::RollDice; self.turn_stage = TurnStage::RollDice;
} }
@ -397,8 +407,9 @@ impl GameState {
PlayerDisconnected { player_id } => { PlayerDisconnected { player_id } => {
self.players.remove(player_id); self.players.remove(player_id);
} }
Roll { player_id: _ } => { Roll { player_id: _ } => {}
self.roll(); RollResult { player_id: _, dice } => {
self.dice = *dice;
self.turn_stage = TurnStage::MarkPoints; self.turn_stage = TurnStage::MarkPoints;
} }
Mark { player_id, points } => { Mark { player_id, points } => {
@ -461,6 +472,10 @@ pub enum GameEvent {
Roll { Roll {
player_id: PlayerId, player_id: PlayerId,
}, },
RollResult {
player_id: PlayerId,
dice: Dice,
},
Mark { Mark {
player_id: PlayerId, player_id: PlayerId,
points: u8, 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 { impl Move for GameState {
fn move_checker(&mut self, player: &Player, dice: u8, from: usize) -> Result<&mut Self, Error> { fn move_checker(&mut self, player: &Player, dice: u8, from: usize) -> Result<&mut Self, Error> {
// check if move is permitted // check if move is permitted
@ -504,7 +502,7 @@ impl Move for GameState {
// self.board.set(player, new_position as usize, 1)?; // 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.switch_active_player();
self.roll_first = true; self.roll_first = true;
@ -530,7 +528,7 @@ impl Move for GameState {
} }
// check if dice value has actually been rolled // 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); return Err(Error::DiceInvalid);
} }
@ -589,16 +587,16 @@ mod tests {
goes_first: player_id, goes_first: player_id,
}); });
state.consume(&GameEvent::Roll { player_id }); state.consume(&GameEvent::Roll { player_id });
let dices = state.dices.values; let dice = state.dice.values;
let moves = ( let moves = (
CheckerMove::new(1, (1 + dices.0).into()).unwrap(), CheckerMove::new(1, (1 + dice.0).into()).unwrap(),
CheckerMove::new((1 + dices.0).into(), (1 + dices.0 + dices.1).into()).unwrap(), CheckerMove::new((1 + dice.0).into(), (1 + dice.0 + dice.1).into()).unwrap(),
); );
assert!(state.moves_follows_dices(&Color::White, &moves)); assert!(state.moves_follows_dices(&Color::White, &moves));
let badmoves = ( let badmoves = (
CheckerMove::new(1, (2 + dices.0).into()).unwrap(), CheckerMove::new(1, (2 + dice.0).into()).unwrap(),
CheckerMove::new((1 + dices.0).into(), (1 + dices.0 + dices.1).into()).unwrap(), CheckerMove::new((1 + dice.0).into(), (1 + dice.0 + dice.1).into()).unwrap(),
); );
assert!(!state.moves_follows_dices(&Color::White, &badmoves)); assert!(!state.moves_follows_dices(&Color::White, &badmoves));
} }

View file

@ -11,3 +11,4 @@ mod board;
pub use board::CheckerMove; pub use board::CheckerMove;
mod dice; mod dice;
pub use dice::{Dice, DiceRoller};