//! # Play a TricTrac Game use crate::board::{Board, CheckerMove, Field}; use crate::dice::Dice; use crate::player::{Color, Player, PlayerId}; use log::error; use std::cmp; // use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::{fmt, str}; use base64::{engine::general_purpose, Engine as _}; /// The different stages a game can be in. (not to be confused with the entire "GameState") #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Stage { PreGame, InGame, Ended, } /// The different stages a game turn can be in. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum TurnStage { RollDice, RollWaiting, MarkPoints, Move, } /// Represents a TricTrac game #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GameState { pub stage: Stage, pub turn_stage: TurnStage, pub board: Board, pub active_player_id: PlayerId, pub players: HashMap, pub history: Vec, /// last dice pair rolled pub dice: Dice, /// true if player needs to roll first roll_first: bool, } // implement Display trait impl fmt::Display for GameState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut s = String::new(); s.push_str(&format!( "Stage: {:?} / {:?}\n", self.stage, self.turn_stage )); 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) } } impl Default for GameState { fn default() -> Self { Self { stage: Stage::PreGame, turn_stage: TurnStage::RollDice, board: Board::default(), active_player_id: 0, players: HashMap::new(), history: Vec::new(), dice: Dice::default(), roll_first: true, } } } impl GameState { /// Create a new default game pub fn new() -> Self { GameState::default() } // ------------------------------------------------------------------------- // accessors // ------------------------------------------------------------------------- /// Calculate game state id : pub fn to_string_id(&self) -> String { // Pieces placement -> 77 bits (24 + 23 + 30 max) let mut pos_bits = self.board.to_gnupg_pos_id(); // active player -> 1 bit // white : 0 (false) // black : 1 (true) pos_bits.push( self.who_plays() .map(|player| { if player.color == Color::Black { '1' } else { '0' } }) .unwrap_or('0'), // White by default ); // step -> 2 bits let step_bits = match self.turn_stage { TurnStage::RollWaiting => "00", TurnStage::RollDice => "01", TurnStage::MarkPoints => "10", TurnStage::Move => "11", }; pos_bits.push_str(step_bits); // dice roll -> 6 bits let dice_bits = self.dice.to_bits_string(); pos_bits.push_str(&dice_bits); // points 10bits x2 joueurs = 20bits let white_bits = self.get_white_player().unwrap().to_bits_string(); let black_bits = self.get_black_player().unwrap().to_bits_string(); pos_bits.push_str(&white_bits); pos_bits.push_str(&black_bits); pos_bits = format!("{:0>108}", pos_bits); // println!("{}", pos_bits); let pos_u8 = pos_bits .as_bytes() .chunks(6) .map(|chunk| str::from_utf8(chunk).unwrap()) .map(|chunk| u8::from_str_radix(chunk, 2).unwrap()) .collect::>(); general_purpose::STANDARD.encode(pos_u8) } pub fn who_plays(&self) -> Option<&Player> { self.players.get(&self.active_player_id) } pub fn get_white_player(&self) -> Option<&Player> { self.players .iter() .filter(|(_id, player)| player.color == Color::White) .map(|(_id, player)| player) .next() } pub fn get_black_player(&self) -> Option<&Player> { self.players .iter() .filter(|(_id, player)| player.color == Color::Black) .map(|(_id, player)| player) .next() } pub fn player_id_by_color(&self, color: Color) -> Option<&PlayerId> { self.players .iter() .filter(|(_id, player)| player.color == color) .map(|(id, _player)| id) .next() } pub fn player_id(&self, player: &Player) -> Option<&PlayerId> { self.players .iter() .filter(|(_id, candidate)| player.color == candidate.color) .map(|(id, _candidate)| id) .next() } pub fn player_color_by_id(&self, player_id: &PlayerId) -> Option { self.players .iter() .filter(|(id, _)| *id == player_id) .map(|(_, player)| player.color) .next() } // ---------------------------------------------------------------------------------- // Rules checks // ---------------------------------------------------------------------------------- /// Determines whether an event is valid considering the current GameState pub fn validate(&self, event: &GameEvent) -> bool { use GameEvent::*; match event { BeginGame { goes_first } => { // Check that the player supposed to go first exists if !self.players.contains_key(goes_first) { return false; } // Check that the game hasn't started yet. (we don't want to double start a game) if self.stage != Stage::PreGame { return false; } } EndGame { reason } => { if let EndGameReason::PlayerWon { winner: _ } = reason { // Check that the game has started before someone wins it if self.stage != Stage::InGame { return false; } } } PlayerJoined { player_id, name: _ } => { // Check that there isn't another player with the same id if self.players.contains_key(player_id) { return false; } } PlayerDisconnected { player_id } => { // Check player exists if !self.players.contains_key(player_id) { return false; } } Roll { player_id } | RollResult { player_id, dice: _ } => { // Check player exists if !self.players.contains_key(player_id) { return false; } // Check player is currently the one making their move if self.active_player_id != *player_id { return false; } } Mark { player_id, points: _, } => { // Check player exists if !self.players.contains_key(player_id) { return false; } // Check player is currently the one making their move if self.active_player_id != *player_id { return false; } } Move { player_id, moves } => { // Check player exists if !self.players.contains_key(player_id) { error!("Player {} unknown", player_id); return false; } // Check player is currently the one making their move if self.active_player_id != *player_id { error!("Player not active : {}", self.active_player_id); return false; } let color = &self.players[player_id].color; // Check moves possibles on the board if !self.moves_possible(color, moves) { return false; } // Check moves conforms to the dice if !self.moves_follows_dices(color, moves) { return false; } // Check move is allowed by the rules (to desactivate when playing with schools) if !self.moves_allowed(color, moves) { return false; } } } // We couldn't find anything wrong with the event so it must be good true } fn moves_possible(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { // Check move is physically possible if !self.board.move_possible(color, &moves.0) { return false; } // Chained_move : "Tout d'une" if let Ok(chained_move) = moves.0.chain(moves.1) { if !self.board.move_possible(color, &chained_move) { return false; } } else if !self.board.move_possible(color, &moves.1) { return false; } true } fn get_move_compatible_dices(&self, color: &Color, cmove: &CheckerMove) -> Vec { let (dice1, dice2) = self.dice.values; let mut move_dices = Vec::new(); if cmove.get_to() == 0 { // Exits let min_dist = match color { Color::White => 25 - cmove.get_from(), Color::Black => cmove.get_from(), }; if dice1 as usize >= min_dist { move_dices.push(dice1); } if dice2 as usize >= min_dist { move_dices.push(dice2); } } else { let dist = (cmove.get_to() as i8 - cmove.get_from() as i8).unsigned_abs(); if dice1 == dist { move_dices.push(dice1); } if dice2 == dist { move_dices.push(dice2); } } move_dices } fn moves_follows_dices(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { // Prise de coin par puissance if self.is_move_by_puissance(color, moves) { return true; } let (dice1, dice2) = self.dice.values; let (move1, move2): &(CheckerMove, CheckerMove) = moves; let move1_dices = self.get_move_compatible_dices(color, move1); if move1_dices.is_empty() { return false; } let move2_dices = self.get_move_compatible_dices(color, move2); if move2_dices.is_empty() { return false; } if move1_dices.len() == 1 && move2_dices.len() == 1 && move1_dices[0] == move2_dices[0] && dice1 != dice2 { return false; } // no rule was broken true } fn moves_allowed(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { // ------- corner rules ---------- let corner_field: Field = self.board.get_color_corner(color); let (corner_count, _color) = self.board.get_field_checkers(corner_field).unwrap(); let (from0, to0, from1, to1) = ( moves.0.get_from(), moves.0.get_to(), moves.1.get_from(), moves.1.get_to(), ); // 2 checkers must go at the same time on an empty corner if (to0 == corner_field || to1 == corner_field) && (to0 != to1) && corner_count == 0 { return false; } // the last 2 checkers of a corner must leave at the same time if (from0 == corner_field || from1 == corner_field) && (from0 != from1) && corner_count == 2 { return false; } if self.is_move_by_puissance(color, moves) && self.can_take_corner_by_effect(color) { return false; } // check exit rules let exit_moves_count = if moves.0.get_to() == 0 && moves.1.get_to() == 0 { 2 } else if moves.0.get_to() == 0 || moves.1.get_to() == 0 { 1 } else { 0 }; if exit_moves_count > 0 { // toutes les dames doivent être dans le jan de retour let has_outsiders = !self .board .get_color_fields(*color) .iter() .filter(|(field, _count)| { (*color == Color::White && *field < 19) || (*color == Color::Black && *field > 6) }) .collect::>() .is_empty(); if has_outsiders { return false; } // les sorties directes sont autorisées let direct_exit_moves = self.get_direct_exit_moves(color); if direct_exit_moves.contains(&moves.0) && direct_exit_moves.contains(&moves.1) { return true; } // détermination du nombre de sorties par excédant let mut excedant_count = exit_moves_count; if direct_exit_moves.contains(&moves.0) && moves.0.get_to() == 0 { excedant_count -= 1; } if direct_exit_moves.contains(&moves.1) && moves.1.get_to() == 0 { excedant_count -= 1; } // on ne peut pas sortir une dame en excédant si on peut en jouer une en défaillant // - on ne doit pas jouer le premier nombre de manière à ce que le second soit excédant (si possible) // - si on ne peut pas jouer un nombre défaillant parce que la place d'arrivée est prise // par une dame adverse, il n'est pas possible de jouer ce nombre en excédant } // --- remplir cadran si possible ---- // --- conserver cadran rempli si possible ---- // --- interdit de jouer dans cadran que l'adversaire peut encore remplir ---- // no rule was broken true } fn get_direct_exit_moves(&self, color: &Color) -> Vec { let mut moves = Vec::new(); let (dice1, dice2) = self.dice.values; // sorties directes simples let (field1_candidate, field2_candidate) = if color == &Color::White { (25 - dice1 as usize, 25 - dice2 as usize) } else { (dice1 as usize, dice2 as usize) }; let (count1, col1) = self.board.get_field_checkers(field1_candidate).unwrap(); let (count2, col2) = self.board.get_field_checkers(field2_candidate).unwrap(); if count1 > 0 { moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); } if dice2 != dice1 { if count2 > 0 { moves.push(CheckerMove::new(field2_candidate, 0).unwrap()); } } else if count1 > 1 { // doublet et deux dames disponibles moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); } // sortie directe tout d'une let fieldall_candidate = if color == &Color::White { 25 - dice1 - dice2 } else { dice1 + dice2 } as usize; let (countall, _col) = self.board.get_field_checkers(fieldall_candidate).unwrap(); if countall > 0 { if col1.is_none() || col1 == Some(color) { moves.push(CheckerMove::new(fieldall_candidate, field1_candidate).unwrap()); moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); } if col2.is_none() || col2 == Some(color) { moves.push(CheckerMove::new(fieldall_candidate, field2_candidate).unwrap()); moves.push(CheckerMove::new(field2_candidate, 0).unwrap()); } } moves } fn is_move_by_puissance(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { let (dice1, dice2) = self.dice.values; let (move1, move2): &(CheckerMove, CheckerMove) = moves; let dist1 = (move1.get_to() as i8 - move1.get_from() as i8).unsigned_abs(); let dist2 = (move2.get_to() as i8 - move2.get_from() as i8).unsigned_abs(); // Both corners must be empty let (count1, _color) = self.board.get_field_checkers(12).unwrap(); let (count2, _color2) = self.board.get_field_checkers(13).unwrap(); if count1 > 0 || count2 > 0 { return false; } move1.get_to() == move2.get_to() && move1.get_to() == self.board.get_color_corner(color) && ((*color == Color::White && cmp::min(dist1, dist2) == cmp::min(dice1, dice2) - 1 && cmp::max(dist1, dist2) == cmp::max(dice1, dice2) - 1) || (*color == Color::Black && cmp::min(dist1, dist2) == cmp::min(dice1, dice2) + 1 && cmp::max(dist1, dist2) == cmp::max(dice1, dice2) + 1)) } fn can_take_corner_by_effect(&self, color: &Color) -> bool { // return false if corner already taken let corner_field: Field = self.board.get_color_corner(color); let (count, _col) = self.board.get_field_checkers(corner_field).unwrap(); if count > 0 { return false; } let (dice1, dice2) = self.dice.values; let (field1, field2) = match color { Color::White => (12 - dice1, 12 - dice2), Color::Black => (13 + dice1, 13 + dice2), }; let res1 = self.board.get_field_checkers(field1.into()); let res2 = self.board.get_field_checkers(field2.into()); if res1.is_err() || res2.is_err() { return false; } let (count1, opt_color1) = res1.unwrap(); let (count2, opt_color2) = res2.unwrap(); count1 > 0 && count2 > 0 && opt_color1 == Some(color) && opt_color2 == Some(color) } // ---------------------------------------------------------------------------------- // State updates // ---------------------------------------------------------------------------------- pub fn init_player(&mut self, player_name: &str) -> Option { if self.players.len() > 2 { println!("more than two players"); return None; } let player_id = self.players.len() + 1; println!("player_id {}", player_id); let color = if player_id == 1 { Color::White } else { Color::Black }; let player = Player::new(player_name.into(), color); self.players.insert(player_id as PlayerId, player); Some(player_id as PlayerId) } fn add_player(&mut self, player_id: PlayerId, player: Player) { self.players.insert(player_id, player); } pub fn switch_active_player(&mut self) { let other_player_id = self .players .iter() .filter(|(id, _player)| **id != self.active_player_id) .map(|(id, _player)| *id) .next(); self.active_player_id = other_player_id.unwrap_or(0); } /// Consumes an event, modifying the GameState and adding the event to its history /// NOTE: consume assumes the event to have already been validated and will accept *any* event passed to it pub fn consume(&mut self, valid_event: &GameEvent) { use GameEvent::*; 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; } EndGame { reason: _ } => self.stage = Stage::Ended, PlayerJoined { player_id, name } => { let color = if !self.players.is_empty() { Color::White } else { Color::Black }; self.players.insert( *player_id, Player { name: name.to_string(), color, holes: 0, points: 0, can_bredouille: true, can_big_bredouille: true, }, ); } PlayerDisconnected { player_id } => { self.players.remove(player_id); } Roll { player_id: _ } => { self.turn_stage = TurnStage::RollWaiting; } RollResult { player_id: _, dice } => { self.dice = *dice; self.turn_stage = TurnStage::MarkPoints; } Mark { player_id, points } => { self.mark_points(*player_id, *points); if self.stage != Stage::Ended { self.turn_stage = TurnStage::Move; } } Move { player_id, moves } => { let player = self.players.get(player_id).unwrap(); self.board.move_checker(&player.color, moves.0).unwrap(); self.board.move_checker(&player.color, moves.1).unwrap(); self.active_player_id = *self.players.keys().find(|id| *id != player_id).unwrap(); self.turn_stage = TurnStage::RollDice; } } self.history.push(valid_event.clone()); } /// Determines if someone has won the game pub fn determine_winner(&self) -> Option { None } fn mark_points(&mut self, player_id: PlayerId, points: u8) { self.players.get_mut(&player_id).map(|p| { p.points += points; p }); } } /// The reasons why a game could end #[derive(Debug, Clone, Copy, Serialize, PartialEq, Deserialize)] pub enum EndGameReason { // In tic tac toe it doesn't make sense to keep playing when one of the players disconnect. // Note that it might make sense to keep playing in some other game (like Team Fight Tactics for instance). PlayerLeft { player_id: PlayerId }, PlayerWon { winner: PlayerId }, } /// An event that progresses the GameState forward #[derive(Debug, Clone, Serialize, PartialEq, Deserialize)] pub enum GameEvent { BeginGame { goes_first: PlayerId, }, EndGame { reason: EndGameReason, }, PlayerJoined { player_id: PlayerId, name: String, }, PlayerDisconnected { player_id: PlayerId, }, Roll { player_id: PlayerId, }, RollResult { player_id: PlayerId, dice: Dice, }, Mark { player_id: PlayerId, points: u8, }, Move { player_id: PlayerId, moves: (CheckerMove, CheckerMove), }, } #[cfg(test)] mod tests { use super::*; #[test] fn to_string_id() { let mut state = GameState::default(); state.add_player(1, Player::new("player1".into(), Color::White)); state.add_player(2, Player::new("player2".into(), Color::Black)); let string_id = state.to_string_id(); // println!("string_id : {}", string_id); assert!(string_id == "Dz8+AAAAAT8/MAAAAAQAADAD"); } #[test] fn moves_possible() { let mut state = GameState::default(); let player1 = Player::new("player1".into(), Color::White); let player_id = 1; state.add_player(player_id, player1); state.add_player(2, Player::new("player2".into(), Color::Black)); state.consume(&GameEvent::BeginGame { goes_first: player_id, }); // Chained moves let moves = ( CheckerMove::new(1, 5).unwrap(), CheckerMove::new(5, 9).unwrap(), ); assert!(state.moves_possible(&Color::White, &moves)); // not chained moves let moves = ( CheckerMove::new(1, 5).unwrap(), CheckerMove::new(6, 9).unwrap(), ); assert!(!state.moves_possible(&Color::White, &moves)); // black moves let moves = ( CheckerMove::new(24, 20).unwrap(), CheckerMove::new(20, 19).unwrap(), ); assert!(state.moves_possible(&Color::Black, &moves)); } #[test] fn moves_follow_dices() { let mut state = GameState::default(); let player1 = Player::new("player1".into(), Color::White); let player_id = 1; state.add_player(player_id, player1); state.add_player(2, Player::new("player2".into(), Color::Black)); state.consume(&GameEvent::BeginGame { goes_first: player_id, }); state.consume(&GameEvent::Roll { player_id }); let dice = state.dice.values; let moves = ( 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 + 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)); } #[test] fn can_take_corner_by_effect() { let mut state = GameState::default(); let player1 = Player::new("player1".into(), Color::White); let player_id = 1; state.add_player(player_id, player1); state.add_player(2, Player::new("player2".into(), Color::Black)); state.consume(&GameEvent::BeginGame { goes_first: player_id, }); state.consume(&GameEvent::Roll { player_id }); state.board.set_positions([ 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, ]); state.dice.values = (4, 4); assert!(state.can_take_corner_by_effect(&Color::White)); state.dice.values = (5, 5); assert!(!state.can_take_corner_by_effect(&Color::White)); state.board.set_positions([ 10, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, ]); state.dice.values = (4, 4); assert!(!state.can_take_corner_by_effect(&Color::White)); state.board.set_positions([ 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, ]); state.dice.values = (1, 1); assert!(state.can_take_corner_by_effect(&Color::Black)); } #[test] fn prise_en_puissance() { let mut state = GameState::default(); let player1 = Player::new("player1".into(), Color::White); let player_id = 1; state.add_player(player_id, player1); state.add_player(2, Player::new("player2".into(), Color::Black)); state.consume(&GameEvent::BeginGame { goes_first: player_id, }); state.consume(&GameEvent::Roll { player_id }); // prise par puissance ok state.board.set_positions([ 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, ]); state.dice.values = (5, 5); let moves = ( CheckerMove::new(8, 12).unwrap(), CheckerMove::new(8, 12).unwrap(), ); assert!(state.is_move_by_puissance(&Color::White, &moves)); assert!(state.moves_follows_dices(&Color::White, &moves)); assert!(state.moves_allowed(&Color::White, &moves)); // opponent corner must be empty state.board.set_positions([ 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, ]); assert!(!state.is_move_by_puissance(&Color::White, &moves)); assert!(!state.moves_follows_dices(&Color::White, &moves)); // Si on a la possibilité de prendre son coin à la fois par effet, c'est à dire naturellement, et aussi par puissance, on doit le prendre par effet state.board.set_positions([ 5, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, ]); assert!(!state.moves_allowed(&Color::White, &moves)); // on a déjà pris son coin : on ne peux plus y deplacer des dames par puissance state.board.set_positions([ 8, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, ]); assert!(!state.is_move_by_puissance(&Color::White, &moves)); assert!(!state.moves_follows_dices(&Color::White, &moves)); } #[test] fn exit() { let mut state = GameState::default(); let player1 = Player::new("player1".into(), Color::White); let player_id = 1; state.add_player(player_id, player1); state.add_player(2, Player::new("player2".into(), Color::Black)); state.consume(&GameEvent::BeginGame { goes_first: player_id, }); state.consume(&GameEvent::Roll { player_id }); // exit ok state.board.set_positions([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, ]); state.dice.values = (5, 5); let moves = ( CheckerMove::new(20, 0).unwrap(), CheckerMove::new(20, 0).unwrap(), ); assert!(state.moves_possible(&Color::White, &moves)); assert!(state.moves_follows_dices(&Color::White, &moves)); assert!(state.moves_allowed(&Color::White, &moves)); // toutes les dames doivent être dans le jan de retour state.board.set_positions([ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, ]); state.dice.values = (5, 5); let moves = ( CheckerMove::new(20, 0).unwrap(), CheckerMove::new(20, 0).unwrap(), ); assert!(!state.moves_allowed(&Color::White, &moves)); // on ne peut pas sortir une dame avec un nombre excédant si on peut en jouer une avec un nombre défaillant state.board.set_positions([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 0, 2, 0, ]); state.dice.values = (5, 5); let moves = ( CheckerMove::new(20, 0).unwrap(), CheckerMove::new(23, 0).unwrap(), ); assert!(!state.moves_allowed(&Color::White, &moves)); } }