refact: remove unwrap() & panic!

This commit is contained in:
Henri Bourcereau 2026-03-01 12:47:42 +01:00
parent 953b5f451a
commit ad157e1626
4 changed files with 177 additions and 52 deletions

View file

@ -55,8 +55,12 @@ impl BoardGameBoard for TrictracBoard {
fn play(&mut self, mv: Self::Move) -> Result<(), PlayError> {
self.check_can_play(mv)?;
self.0.consume(&mv.to_event(&self.0).unwrap());
Ok(())
if let Some(evt) = mv.to_event(&self.0) {
self.0.consume(&evt);
Ok(())
} else {
Err(PlayError::UnavailableMove)
}
}
fn outcome(&self) -> Option<Outcome> {
@ -159,9 +163,9 @@ impl InternalIterator for TrictracAvailableMovesIterator<'_> {
where
F: FnMut(Self::Item) -> ControlFlow<R>,
{
get_valid_actions(&self.board.0)
.unwrap()
.into_iter()
.try_for_each(f)
match get_valid_actions(&self.board.0) {
Ok(actions) => actions.into_iter().try_for_each(f),
Err(_) => ControlFlow::Continue(()),
}
}
}

View file

@ -4,6 +4,7 @@ use crate::dice::Dice;
use crate::game_rules_moves::MoveRules;
use crate::game_rules_points::{PointsRules, PossibleJans, PossibleJansMethods};
use crate::player::{Color, Player, PlayerId};
// use anyhow::{Context, Result};
use log::{debug, error};
// use itertools::Itertools;
@ -90,11 +91,12 @@ impl fmt::Display for GameState {
self.stage, self.turn_stage
));
s.push_str(&format!("Dice: {:?}\n", self.dice));
let empty_string = String::from("");
s.push_str(&format!(
"Who plays: {}\n",
self.who_plays()
.map(|player| &player.name)
.unwrap_or(&String::from(""))
.unwrap_or_else(|| &empty_string)
));
s.push_str(&format!("Board: {:?}\n", self.board));
// s.push_str(&format!("History: {:?}\n", self.history));
@ -233,20 +235,13 @@ impl GameState {
// points, trous, bredouille, grande bredouille length=4 x2 joueurs = 8
let white_player: Vec<i8> = self
.get_white_player()
.unwrap()
.to_vec()
.iter()
.map(|&x| x as i8)
.collect();
.map(|p| p.to_vec().iter().map(|&x| x as i8).collect())
.unwrap_or(vec![0; 10]);
state.extend(white_player);
let black_player: Vec<i8> = self
.get_black_player()
.unwrap()
.to_vec()
.iter()
.map(|&x| x as i8)
.collect();
// .iter().map(|&x| x as i8) .collect()
.map(|p| p.to_vec().iter().map(|&x| x as i8).collect())
.unwrap_or(vec![0; 10]);
state.extend(black_player);
// ensure state has length state_len
@ -258,7 +253,7 @@ impl GameState {
}
/// Calculate game state id :
pub fn to_string_id(&self) -> String {
pub fn to_string_id_slow(&self) -> String {
// Pieces placement -> 77 bits (24 + 23 + 30 max)
let mut pos_bits = self.board.to_gnupg_pos_id();
@ -293,22 +288,141 @@ impl GameState {
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();
let white_bits = self
.get_white_player()
.map(|p| p.to_bits_string())
.unwrap_or("0000000000".into());
let black_bits = self
.get_black_player()
.map(|p| p.to_bits_string())
.unwrap_or("0000000000".into());
pos_bits.push_str(&white_bits);
pos_bits.push_str(&black_bits);
pos_bits = format!("{pos_bits:0<108}");
// 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::<Vec<u8>>();
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())
.map(|chunk| chunk.iter().fold(0u8, |acc, &b| (acc << 1) | (b - b'0')))
.collect::<Vec<u8>>();
general_purpose::STANDARD.encode(pos_u8)
}
pub fn to_string_id(&self) -> String {
const TOTAL_BITS: usize = 108;
const TOTAL_BYTES: usize = TOTAL_BITS / 6; // 18 bytes
let mut output = Vec::with_capacity(TOTAL_BYTES);
let mut current: u8 = 0;
let mut bit_count: u8 = 0;
// helper to push a single bit
let mut push_bit = |bit: u8, output: &mut Vec<u8>, current: &mut u8, bit_count: &mut u8| {
*current = (*current << 1) | (bit & 1);
*bit_count += 1;
if *bit_count == 6 {
output.push(*current);
*current = 0;
*bit_count = 0;
}
};
// helper to push a string of '0'/'1'
let mut push_bits_str =
|bits: &str, output: &mut Vec<u8>, current: &mut u8, bit_count: &mut u8| {
for b in bits.bytes() {
push_bit(b - b'0', output, current, bit_count);
}
};
// --------------------------------------------------
// 1⃣ Board position bits
// --------------------------------------------------
push_bits_str(
&self.board.to_gnupg_pos_id(),
&mut output,
&mut current,
&mut bit_count,
);
// --------------------------------------------------
// 2⃣ Active player (1 bit)
// --------------------------------------------------
let active_bit = self
.who_plays()
.map(|player| (player.color == Color::Black) as u8)
.unwrap_or(0);
push_bit(active_bit, &mut output, &mut current, &mut bit_count);
// --------------------------------------------------
// 3⃣ Turn stage (3 bits)
// --------------------------------------------------
let stage_bits: u8 = match self.turn_stage {
TurnStage::RollWaiting => 0b000,
TurnStage::RollDice => 0b001,
TurnStage::MarkPoints => 0b010,
TurnStage::HoldOrGoChoice => 0b011,
TurnStage::Move => 0b100,
TurnStage::MarkAdvPoints => 0b101,
};
for i in (0..3).rev() {
push_bit(
(stage_bits >> i) & 1,
&mut output,
&mut current,
&mut bit_count,
);
}
// --------------------------------------------------
// 4⃣ Dice (6 bits)
// --------------------------------------------------
push_bits_str(
&self.dice.to_bits_string(),
&mut output,
&mut current,
&mut bit_count,
);
// --------------------------------------------------
// 5⃣ Players points (10 bits each)
// --------------------------------------------------
let white_bits = self
.get_white_player()
.map(|p| p.to_bits_string())
.unwrap_or_else(|| "0000000000".to_string());
let black_bits = self
.get_black_player()
.map(|p| p.to_bits_string())
.unwrap_or_else(|| "0000000000".to_string());
push_bits_str(&white_bits, &mut output, &mut current, &mut bit_count);
push_bits_str(&black_bits, &mut output, &mut current, &mut bit_count);
// --------------------------------------------------
// 6⃣ Pad remaining bits (if needed)
// --------------------------------------------------
while output.len() < TOTAL_BYTES {
push_bit(0, &mut output, &mut current, &mut bit_count);
}
base64::engine::general_purpose::STANDARD.encode(output)
}
pub fn from_string_id(id: &str) -> Result<Self, String> {
let bytes = general_purpose::STANDARD
.decode(id)
@ -326,7 +440,9 @@ impl GameState {
let board_bits = &bits[0..77];
let board = Board::from_gnupg_pos_id(board_bits)?;
let active_player_bit = bits.chars().nth(77).unwrap();
let Some(active_player_bit) = bits.chars().nth(77) else {
return Err("No bit at 77th position".to_string());
};
let active_player_color = if active_player_bit == '1' {
Color::Black
} else {
@ -621,23 +737,14 @@ impl GameState {
.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) {
pub fn consume(&mut self, valid_event: &GameEvent) -> Result<(), String> {
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;
}
@ -673,14 +780,16 @@ impl GameState {
self.dice = *dice;
self.inc_roll_count(self.active_player_id);
self.turn_stage = TurnStage::MarkPoints;
(self.dice_jans, self.dice_points) = self.get_rollresult_jans(dice);
(self.dice_jans, self.dice_points) = self.get_rollresult_jans(dice)?;
debug!("points from result : {:?}", self.dice_points);
if !self.schools_enabled {
// Schools are not enabled. We mark points automatically
// the points earned by the opponent will be marked on its turn
let new_hole = self.mark_points(self.active_player_id, self.dice_points.0);
if new_hole {
let holes_count = self.get_active_player().unwrap().holes;
let Some(holes_count) = self.get_active_player().map(|p| p.holes) else {
return Err("No active player".into());
};
debug!("new hole -> {holes_count:?}");
if holes_count > 12 {
self.stage = Stage::Ended;
@ -696,7 +805,10 @@ impl GameState {
if self.schools_enabled {
let new_hole = self.mark_points(*player_id, *points);
if new_hole {
if self.get_active_player().unwrap().holes > 12 {
let Some(holes) = self.get_active_player().map(|p| p.holes) else {
return Err("No active player".into());
};
if holes > 12 {
self.stage = Stage::Ended;
} else {
self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints {
@ -716,17 +828,26 @@ impl GameState {
}
Go { player_id: _ } => self.new_pick_up(),
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();
let Some(player) = self.players.get(player_id) else {
return Err("unknown player {player_id}".into());
};
self.board
.move_checker(&player.color, moves.0)
.map_err(|e| e.to_string())?;
self.board
.move_checker(&player.color, moves.1)
.map_err(|e| e.to_string())?;
self.dice_moves = *moves;
self.active_player_id = *self.players.keys().find(|id| *id != player_id).unwrap();
let Some(active_player_id) = self.players.keys().find(|id| *id != player_id) else {
return Err("Can't find player id {id}".into());
};
self.active_player_id = *active_player_id;
self.turn_stage = if self.schools_enabled {
TurnStage::MarkAdvPoints
} else {
// The player has moved, we can mark its opponent's points (which is now the current player)
let new_hole = self.mark_points(self.active_player_id, self.dice_points.1);
if new_hole && self.get_active_player().unwrap().holes > 12 {
if new_hole && self.get_active_player().map(|p| p.holes).unwrap_or(0) > 12 {
self.stage = Stage::Ended;
}
TurnStage::RollDice
@ -735,6 +856,7 @@ impl GameState {
PlayError => {}
}
self.history.push(valid_event.clone());
Ok(())
}
/// Set a new pick up ('relevé') after a player won a hole and choose to 'go',
@ -757,14 +879,16 @@ impl GameState {
self.board = Board::new();
}
fn get_rollresult_jans(&self, dice: &Dice) -> (PossibleJans, (u8, u8)) {
let player = &self.players.get(&self.active_player_id).unwrap();
fn get_rollresult_jans(&self, dice: &Dice) -> Result<(PossibleJans, (u8, u8)), String> {
let Some(player) = &self.players.get(&self.active_player_id) else {
return Err("No active player".into());
};
debug!(
"get rollresult for {:?} {:?} {:?} (roll count {:?})",
player.color, self.board, dice, player.dice_roll_count
);
let points_rules = PointsRules::new(&player.color, &self.board, *dice);
points_rules.get_result_jans(player.dice_roll_count)
Ok(points_rules.get_result_jans(player.dice_roll_count))
}
/// Determines if someone has won the game

View file

@ -71,8 +71,8 @@ impl Player {
}
let points = u8::from_str_radix(&bits[0..4], 2).map_err(|e| e.to_string())?;
let holes = u8::from_str_radix(&bits[4..8], 2).map_err(|e| e.to_string())?;
let can_bredouille = bits.chars().nth(8).unwrap() == '1';
let can_big_bredouille = bits.chars().nth(9).unwrap() == '1';
let can_bredouille = bits.chars().nth(8).ok_or_else(|| "8th bit unreadable")? == '1';
let can_big_bredouille = bits.chars().nth(9).ok_or_else(|| "9th bit unreadable")? == '1';
Ok(Player {
name,

View file

@ -323,10 +323,7 @@ fn white_checker_moves_to_trictrac_action(
let checker1 = board.get_field_checker(&crate::Color::White, from1) as usize;
let mut tmp_board = board.clone();
// should not raise an error for a valid action
let move_res = tmp_board.move_checker(&crate::Color::White, *move1);
if move_res.is_err() {
panic!("error while moving checker {move_res:?}");
}
tmp_board.move_checker(&crate::Color::White, *move1)?;
let checker2 = tmp_board.get_field_checker(&crate::Color::White, from2) as usize;
Ok(TrictracAction::Move {
dice_order,