trictrac/store/src/board.rs

411 lines
12 KiB
Rust
Raw Normal View History

2024-01-09 17:58:10 +01:00
use crate::player::{Color, Player};
2023-10-07 20:46:24 +02:00
use crate::Error;
use serde::{Deserialize, Serialize};
2024-02-18 18:40:45 +01:00
use std::cmp;
2024-01-12 17:02:18 +01:00
use std::fmt;
2023-10-07 20:46:24 +02:00
2024-01-29 21:42:40 +01:00
/// field (aka 'point') position on the board (from 0 to 24, 0 being 'outside')
2024-01-27 20:22:20 +01:00
pub type Field = usize;
#[derive(Debug, Copy, Clone, Serialize, PartialEq, Deserialize)]
pub struct CheckerMove {
from: Field,
to: Field,
}
2024-02-18 18:40:45 +01:00
fn transpose(matrix: Vec<Vec<String>>) -> Vec<Vec<String>> {
let num_cols = matrix.first().unwrap().len();
let mut row_iters: Vec<_> = matrix.into_iter().map(Vec::into_iter).collect();
let mut out: Vec<Vec<_>> = (0..num_cols).map(|_| Vec::new()).collect();
for out_row in out.iter_mut() {
for it in row_iters.iter_mut() {
out_row.push(it.next().unwrap());
}
}
out
}
2024-01-27 20:22:20 +01:00
impl CheckerMove {
pub fn new(from: Field, to: Field) -> Result<Self, Error> {
2024-01-29 21:42:40 +01:00
// check if the field is on the board
// we allow 0 for 'to', which represents the exit of a checker
2024-01-31 15:39:02 +01:00
if from < 1 || 24 < from || 24 < to {
2024-01-27 20:22:20 +01:00
return Err(Error::FieldInvalid);
}
2024-01-29 21:42:40 +01:00
// check that the destination is after the origin field
if to < from && to != 0 {
return Err(Error::MoveInvalid);
}
2024-01-31 15:39:02 +01:00
Ok(Self { from, to })
}
// Construct the move resulting of two successive moves
pub fn chain(self, cmove: Self) -> Result<Self, Error> {
if self.to != cmove.from {
return Err(Error::MoveInvalid);
}
Ok(Self {
from: self.from,
to: cmove.to,
})
2024-01-27 20:22:20 +01:00
}
2024-01-29 21:42:40 +01:00
pub fn get_from(&self) -> Field {
self.from
}
2024-01-27 20:22:20 +01:00
pub fn get_to(&self) -> Field {
self.to
}
}
2023-11-05 17:14:58 +01:00
/// Represents the Tric Trac board
2024-01-09 17:58:10 +01:00
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2024-01-12 17:02:18 +01:00
pub struct Board {
2024-01-27 20:22:20 +01:00
positions: [i8; 24],
2023-10-07 20:46:24 +02:00
}
2024-01-09 17:58:10 +01:00
impl Default for Board {
fn default() -> Self {
Board {
2024-01-27 20:22:20 +01:00
positions: [
2024-01-09 17:58:10 +01:00
15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15,
],
}
}
2023-10-07 20:46:24 +02:00
}
2024-01-12 17:02:18 +01:00
// implement Display trait
impl fmt::Display for Board {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = String::new();
2024-01-27 20:22:20 +01:00
s.push_str(&format!("{:?}", self.positions));
2024-01-12 17:02:18 +01:00
write!(f, "{}", s)
}
}
2023-10-07 20:46:24 +02:00
impl Board {
/// Create a new board
pub fn new() -> Self {
Board::default()
}
2024-01-25 21:33:46 +01:00
// maybe todo : operate on bits (cf. https://github.com/bungogood/bkgm/blob/a2fb3f395243bcb0bc9f146df73413f73f5ea1e0/src/position.rs#L217)
2024-01-20 21:40:06 +01:00
pub fn to_gnupg_pos_id(&self) -> String {
2024-01-12 17:02:18 +01:00
// Pieces placement -> 77bits (24 + 23 + 30 max)
// inspired by https://www.gnu.org/software/gnubg/manual/html_node/A-technical-description-of-the-Position-ID.html
// - white positions
2024-01-27 20:22:20 +01:00
let white_board = self.positions.clone();
2024-01-20 21:40:06 +01:00
let mut pos_bits = white_board.iter().fold(vec![], |acc, nb| {
let mut new_acc = acc.clone();
if *nb > 0 {
2024-01-12 17:02:18 +01:00
// add as many `true` as there are pieces on the arrow
2024-01-20 21:40:06 +01:00
new_acc.append(&mut vec!['1'; *nb as usize]);
2024-01-12 17:02:18 +01:00
}
2024-01-20 21:40:06 +01:00
new_acc.push('0'); // arrow separator
new_acc
2024-01-12 17:02:18 +01:00
});
// - black positions
2024-01-27 20:22:20 +01:00
let mut black_board = self.positions.clone();
2024-01-12 17:02:18 +01:00
black_board.reverse();
2024-01-20 21:40:06 +01:00
let mut pos_black_bits = black_board.iter().fold(vec![], |acc, nb| {
let mut new_acc = acc.clone();
if *nb < 0 {
2024-01-12 17:02:18 +01:00
// add as many `true` as there are pieces on the arrow
2024-01-20 21:40:06 +01:00
new_acc.append(&mut vec!['1'; (0 - *nb) as usize]);
2024-01-12 17:02:18 +01:00
}
2024-01-20 21:40:06 +01:00
new_acc.push('0'); // arrow separator
new_acc
2024-01-12 17:02:18 +01:00
});
2024-01-20 21:40:06 +01:00
pos_bits.append(&mut pos_black_bits);
2024-01-12 17:02:18 +01:00
// fill with 0 bits until 77
2024-01-20 21:40:06 +01:00
pos_bits.resize(77, '0');
pos_bits.iter().collect::<String>()
2024-01-12 17:02:18 +01:00
}
2024-02-18 18:40:45 +01:00
/// format positions to a grid of symbols
pub fn to_display_grid(&self, col_size: usize) -> String {
// convert numbers to columns of chars
let mut columns: Vec<Vec<String>> = self
.positions
.iter()
.map(|count| {
let char = if *count > 0 { "O" } else { "X" };
let men_count = count.abs();
let mut cells = vec!["".to_owned(); col_size];
cells[0..(cmp::min(men_count, col_size as i8) as usize)].fill(char.to_owned());
if men_count as usize > col_size {
cells[col_size - 1] = men_count.to_string();
}
cells
})
.collect();
// upper columns (13 to 24)
let upper_positions: Vec<Vec<String>> = columns.split_off(12).into_iter().collect();
// lower columns (12 to 1)
let mut lower_positions: Vec<Vec<String>> = columns
.into_iter()
.map(|mut col| {
col.reverse();
col
})
.collect();
lower_positions.reverse();
// display board columns
let upper: Vec<String> = transpose(upper_positions)
.into_iter()
.map(|cells| {
cells
.into_iter()
.map(|cell| format!("{:>5}", cell))
.collect::<Vec<String>>()
.join("")
})
.collect();
let lower: Vec<String> = transpose(lower_positions)
.into_iter()
.map(|cells| {
cells
.into_iter()
.map(|cell| format!("{:>5}", cell))
.collect::<Vec<String>>()
.join("")
})
.collect();
let mut output = "
13 14 15 16 17 18 19 20 21 22 23 24
----------------------------------------------------------------\n"
.to_owned();
for mut line in upper {
// add middle bar
line.replace_range(30..30, "| |");
output = output + " |" + &line + " |\n";
}
output = output + " |----------------------------- | | ------------------------------|\n";
for mut line in lower {
// add middle bar
line.replace_range(30..30, "| |");
output = output + " |" + &line + " |\n";
}
output = output
+ " ----------------------------------------------------------------
12 11 10 9 8 7 6 5 4 3 2 1 \n";
output
}
2023-10-07 20:46:24 +02:00
/// Set checkers for a player on a field
///
/// This method adds the amount of checkers for a player on a field. The field is numbered from
2024-01-09 17:58:10 +01:00
/// 1 to 24, starting from the first field of each player in the home board, the most far away
/// field for each player is number 24.
2023-10-07 20:46:24 +02:00
///
/// If the field is blocked for the player, an error is returned. If the field is not blocked,
/// but there is already one checker from the other player on the field, that checker is hit and
/// moved to the bar.
2024-01-27 20:22:20 +01:00
pub fn set(&mut self, color: &Color, field: Field, amount: i8) -> Result<(), Error> {
2024-01-09 17:58:10 +01:00
if field > 24 {
2023-10-07 20:46:24 +02:00
return Err(Error::FieldInvalid);
}
2024-01-29 21:42:40 +01:00
// the exit : no checker added to the board
if field == 0 {
2024-01-31 15:39:02 +01:00
return Ok(());
2024-01-29 21:42:40 +01:00
}
2024-01-27 20:22:20 +01:00
if self.blocked(color, field)? {
2023-10-07 20:46:24 +02:00
return Err(Error::FieldBlocked);
}
2024-01-27 20:22:20 +01:00
match color {
2023-10-28 15:12:04 +02:00
Color::White => {
2024-01-27 20:22:20 +01:00
let new = self.positions[field - 1] + amount;
2023-10-07 20:46:24 +02:00
if new < 0 {
return Err(Error::MoveInvalid);
}
2024-01-27 20:22:20 +01:00
self.positions[field - 1] = new;
2023-10-07 20:46:24 +02:00
Ok(())
}
2023-10-28 15:12:04 +02:00
Color::Black => {
2024-01-27 20:22:20 +01:00
let new = self.positions[24 - field] - amount;
2024-01-09 17:58:10 +01:00
if new > 0 {
2023-10-07 20:46:24 +02:00
return Err(Error::MoveInvalid);
}
2024-01-27 20:22:20 +01:00
self.positions[24 - field] = new;
2023-10-07 20:46:24 +02:00
Ok(())
}
}
}
/// Check if a field is blocked for a player
2024-01-27 20:22:20 +01:00
pub fn blocked(&self, color: &Color, field: Field) -> Result<bool, Error> {
2024-01-29 21:42:40 +01:00
if 24 < field {
2023-10-07 20:46:24 +02:00
return Err(Error::FieldInvalid);
}
2024-01-29 21:42:40 +01:00
// the exit is never 'blocked'
if field == 0 {
2024-01-31 15:39:02 +01:00
return Ok(false);
2024-01-29 21:42:40 +01:00
}
2024-01-27 20:22:20 +01:00
// the square is blocked on the opponent rest corner or if there are opponent's men on the square
match color {
2023-10-28 15:12:04 +02:00
Color::White => {
2024-01-27 20:22:20 +01:00
if field == 13 || self.positions[field - 1] < 0 {
2023-10-07 20:46:24 +02:00
Ok(true)
} else {
Ok(false)
}
}
2023-10-28 15:12:04 +02:00
Color::Black => {
2024-01-27 20:22:20 +01:00
if field == 12 || self.positions[23 - field] > 1 {
2023-10-07 20:46:24 +02:00
Ok(true)
} else {
Ok(false)
}
}
}
}
2024-01-27 20:22:20 +01:00
2024-01-29 21:42:40 +01:00
pub fn get_field_checkers(&self, field: Field) -> Result<(u8, Option<&Color>), Error> {
2024-01-27 20:22:20 +01:00
if field < 1 || field > 24 {
return Err(Error::FieldInvalid);
}
2024-01-29 21:42:40 +01:00
let checkers_count = self.positions[field - 1];
2024-01-27 20:22:20 +01:00
let color = if checkers_count < 0 {
Some(&Color::Black)
} else if checkers_count > 0 {
Some(&Color::White)
} else {
None
};
2024-01-29 21:42:40 +01:00
Ok((checkers_count.abs() as u8, color))
}
pub fn get_checkers_color(&self, field: Field) -> Result<Option<&Color>, Error> {
self.get_field_checkers(field).map(|(count, color)| color)
}
// Get the corner field for the color
pub fn get_color_corner(&self, color: &Color) -> Field {
2024-01-31 15:39:02 +01:00
if color == &Color::White {
12
} else {
13
}
2024-01-27 20:22:20 +01:00
}
2024-01-31 15:39:02 +01:00
pub fn move_possible(&self, color: &Color, cmove: &CheckerMove) -> bool {
2024-01-27 20:22:20 +01:00
let blocked = self.blocked(color, cmove.to).unwrap_or(true);
// Check if there is a player's checker on the 'from' square
let has_checker = self.get_checkers_color(cmove.from).unwrap_or(None) == Some(color);
has_checker && !blocked
}
pub fn move_checker(&mut self, color: &Color, cmove: CheckerMove) -> Result<(), Error> {
self.remove_checker(color, cmove.from)?;
self.add_checker(color, cmove.to)?;
Ok(())
}
pub fn remove_checker(&mut self, color: &Color, field: Field) -> Result<(), Error> {
let checker_color = self.get_checkers_color(field)?;
if Some(color) != checker_color {
return Err(Error::FieldInvalid);
}
self.positions[field] -= 1;
Ok(())
}
pub fn add_checker(&mut self, color: &Color, field: Field) -> Result<(), Error> {
let checker_color = self.get_checkers_color(field)?;
// error if the case contains the other color
if None != checker_color && Some(color) != checker_color {
return Err(Error::FieldInvalid);
}
self.positions[field] += 1;
Ok(())
}
2023-10-07 20:46:24 +02:00
}
/// Trait to move checkers
pub trait Move {
/// Move a checker
2024-01-27 20:22:20 +01:00
fn move_checker(&mut self, player: &Player, dice: u8, from: Field) -> Result<&mut Self, Error>
2023-10-07 20:46:24 +02:00
where
Self: Sized;
/// Move permitted
2023-10-29 20:48:53 +01:00
fn move_permitted(&mut self, player: &Player, dice: u8) -> Result<&mut Self, Error>
2023-10-07 20:46:24 +02:00
where
Self: Sized;
}
// Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_board() {
assert_eq!(Board::new(), Board::default());
}
#[test]
2024-01-27 18:53:48 +01:00
fn blocked_outofrange() -> Result<(), Error> {
2023-10-07 20:46:24 +02:00
let board = Board::new();
2024-01-31 15:39:02 +01:00
assert!(!board.blocked(&Color::White, 0).is_err());
assert!(board.blocked(&Color::White, 28).is_err());
2023-10-07 20:46:24 +02:00
Ok(())
}
#[test]
2024-01-27 18:53:48 +01:00
fn blocked_otherplayer() -> Result<(), Error> {
2023-10-07 20:46:24 +02:00
let board = Board::new();
2024-01-31 15:39:02 +01:00
assert!(board.blocked(&Color::White, 24)?);
2023-10-07 20:46:24 +02:00
Ok(())
}
#[test]
2024-01-27 18:53:48 +01:00
fn blocked_notblocked() -> Result<(), Error> {
let board = Board::new();
2024-01-31 15:39:02 +01:00
assert!(!board.blocked(&Color::White, 6)?);
2023-10-07 20:46:24 +02:00
Ok(())
}
#[test]
fn set_field_blocked() {
let mut board = Board::new();
2024-01-31 15:39:02 +01:00
assert!(board.set(&Color::White, 24, 2).is_err());
2023-10-07 20:46:24 +02:00
}
#[test]
fn set_wrong_field1() {
let mut board = Board::new();
2024-01-31 15:39:02 +01:00
assert!(board.set(&Color::White, 50, 2).is_err());
2023-10-07 20:46:24 +02:00
}
#[test]
fn set_wrong_amount0() {
let mut board = Board::new();
2024-01-31 15:39:02 +01:00
assert!(board.set(&Color::White, 23, -3).is_err());
2023-10-07 20:46:24 +02:00
}
#[test]
fn set_wrong_amount1() {
let mut board = Board::new();
2024-01-27 18:53:48 +01:00
let player = Player::new("".into(), Color::White);
2024-01-31 15:39:02 +01:00
assert!(board.set(&Color::White, 23, -3).is_err());
2023-10-07 20:46:24 +02:00
}
}