Files
checkers/src/logic/board.rs
2026-08-12 22:17:38 +02:00

393 lines
12 KiB
Rust

use crate::logic::{
field::Field,
pawn::{Pawn, PawnState},
player::Player,
utils::{DeltaPosition, Position},
};
use std::ops::{Index, IndexMut};
use log::{debug, info, warn};
pub struct Board {
board: Vec<Field>,
pub size: usize,
}
impl Board {
/// Board is always a square. Size parameter is a side of this square. Defaults to 8
pub fn new(player1: &mut Player, player2: &mut Player, size: Option<usize>) -> Self {
let size = size.unwrap_or(8);
info!("Creating new board of size: {}", size);
player1.end_row = Some(size - 1);
player1.vertical_direction = Some(crate::logic::utils::DeltaPosition { row: 1, column: 0 });
player2.end_row = Some(0);
player2.vertical_direction =
Some(crate::logic::utils::DeltaPosition { row: -1, column: 0 });
let p1_ref: &Player = &*player1;
let p2_ref: &Player = &*player2;
let board: Vec<Field> = (0..size)
.flat_map(|row| {
(0..size).map(move |column| {
let is_row_even: bool = row % 2 == 0;
let is_column_even: bool = column % 2 == 0;
let is_valid_place_for_a_pawn: bool = is_row_even ^ is_column_even;
let position: Position = Position { row, column };
let current_player: Option<&Player> = if row < 3 {
Some(p1_ref)
} else if row >= size - 3 {
Some(p2_ref)
} else {
None
};
if let Some(player) = current_player
&& is_valid_place_for_a_pawn
{
debug!("Placing new pawn at: {}, owned by: {}", position, player);
Field {
position,
pawn: Some(Pawn::new(player)),
}
} else {
debug!("Not placing any pawn at: {}", position);
Field {
position,
pawn: None,
}
}
})
})
.collect();
Board { board, size }
}
/// Checks if move from position to position is valid.
/// It returns true if you want to move pawn from position to position where there is no pawn
///
/// Doesn't check turn
///
/// Not meant for capturing
fn is_move_valid(&self, from: &Position, to: &Position) -> bool {
if let Some(_) = &self[from.row][from.column].pawn {
self[to.row][to.column].pawn.is_none()
} else {
false
}
}
fn is_player_owner_of_the_pawn(&self, player: &Player, pawn: &Pawn) -> bool {
player.id == pawn.owner
}
fn is_movable(&self, pos: Position, player: &Player) -> bool {
if !pos.is_in_range(self.size) {
debug!(
"Tried checking if pos: {} is movable, but it's out of bounds!",
pos
);
return false;
}
let Some(pawn) = &self[pos].pawn else {
return false;
};
let Some(direction) = player.vertical_direction else {
debug!(
"Tried checking if player: {} can move his pawn, but their direction is not yet set!",
player.id
);
return false;
};
let left_up: DeltaPosition = DeltaPosition { row: 0, column: -1 } + direction;
let right_up: DeltaPosition = DeltaPosition { row: 0, column: 1 } + direction;
match pawn.state {
PawnState::Man => {
if let Some(left_pos) = pos.checked_add(&left_up)
&& left_pos.is_in_range(self.size)
&& self[left_pos].pawn.is_none()
{
return true;
}
if let Some(right_pos) = pos.checked_add(&right_up)
&& right_pos.is_in_range(self.size)
&& self[right_pos].pawn.is_none()
{
return true;
}
false
}
PawnState::Dame => {
todo!();
}
PawnState::Captured => false,
}
}
fn get_player_pawns_positions(&self, player: &Player) -> Vec<Position> {
self.board
.iter()
.filter_map(|field| {
if let Some(pawn) = &field.pawn
&& pawn.owner == player.id
{
Some(field.position)
} else {
None
}
})
.collect()
}
/// Returns true if move was successful
pub fn move_pawn(&mut self, player: &Player, from: Position, to: Position) -> bool {
if !self.is_move_valid(&from, &to) {
warn!("Tried invalid move: {} -> {}", from, to);
return false;
}
if !self.is_player_owner_of_the_pawn(&player, self[from].pawn.as_ref().unwrap()) {
warn!("Player {} tried moving a pawn which he doesn own!", player);
debug!("Tried moving from {} to {}", from, to);
return false;
}
let Some(end_row) = player.end_row else {
warn!(
"Tried moving pawn of player {}, who wasn't initialized by the board!",
player.id
);
return false;
};
// we can safely unwrap, because if there's not pawn function returns false earlier
let mut mowing_pawn: Pawn = self[from].pawn.take().unwrap();
if to.row == end_row {
mowing_pawn.state = super::pawn::PawnState::Dame;
}
self[to.row][to.column].pawn = Some(mowing_pawn);
true
}
}
impl Index<usize> for Board {
type Output = [Field];
fn index<'a>(&'a self, i: usize) -> &'a [Field] {
let start = i * &self.size;
let end: usize = start + &self.size;
&self.board[start..end]
}
}
impl Index<Position> for Board {
type Output = Field;
fn index(&self, index: Position) -> &Self::Output {
&self[index.row][index.column]
}
}
impl IndexMut<usize> for Board {
fn index_mut<'a>(&'a mut self, i: usize) -> &'a mut [Field] {
let start = i * &self.size;
let end: usize = start + &self.size;
&mut self.board[start..end]
}
}
impl IndexMut<Position> for Board {
fn index_mut(&mut self, index: Position) -> &mut Self::Output {
&mut self[index.row][index.column]
}
}
#[cfg(test)]
mod tests {
use crate::logic::pawn::PawnState;
use super::*;
fn init_logger() {
let _ = env_logger::builder().is_test(true).try_init();
}
#[test]
fn new_board_test() {
init_logger();
let mut player1: Player = Player::new("Morbius".to_string());
let mut player2: Player = Player::new("Milo".to_string());
let test_board: Board = Board::new(&mut player1, &mut player2, None);
let size: usize = test_board.size;
for row in 0..size {
for column in 0..size {
assert_eq!(test_board[row][column].position.row, row);
assert_eq!(test_board[row][column].position.column, column);
}
}
for row in 0..size {
for column in 0..size {
let is_row_even: bool = row % 2 == 0;
let is_column_even: bool = column % 2 == 0;
let is_valid_place_for_a_pawn: bool =
(is_row_even ^ is_column_even) && (row < 3 || row >= size - 3);
if is_valid_place_for_a_pawn {
assert!(test_board[row][column].pawn.is_some());
// we unwrap, because some value has to be here, if it's not then something is wrong
// beside we check for that in the assert above
assert!(test_board[row][column].pawn.as_ref().unwrap().state == PawnState::Man);
if row < 3 {
assert_eq!(
test_board[row][column].pawn.as_ref().unwrap().owner,
player1.id
)
} else if row >= size - 3 {
assert_eq!(
test_board[row][column].pawn.as_ref().unwrap().owner,
player2.id
)
}
} else {
assert!(test_board[row][column].pawn.is_none());
}
}
}
}
#[test]
fn test_overloaded_operators() {
init_logger();
let mut player1: Player = Player::new("Morbius".to_string());
let mut player2: Player = Player::new("Milo".to_string());
let test_board: Board = Board::new(&mut player1, &mut player2, None);
let row1 = 0;
let column1 = 0;
let position1: Position = Position {
row: row1,
column: column1,
};
let row2 = row1;
let column2 = 1;
let position2: Position = Position {
row: row2,
column: column2,
};
assert_eq!(test_board[row1][column1], test_board[position1]);
assert_eq!(test_board[row2][column2], test_board[position2]);
assert!(test_board[position1].pawn.is_none());
assert!(test_board[position2].pawn.is_some());
}
#[test]
fn test_moving_pawn() {
init_logger();
let mut player1: Player = Player::new("Morbius".to_string());
let mut player2: Player = Player::new("Milo".to_string());
let mut test_board: Board = Board::new(&mut player1, &mut player2, None);
// we cant move pawn into another pawn; this should fail
assert!(!test_board.move_pawn(
&player1,
Position { row: 0, column: 1 },
Position { row: 1, column: 0 },
));
// player can't move pawn that he doesn't own; this should fail
assert!(!test_board.move_pawn(
&player2,
Position { row: 2, column: 1 },
Position { row: 3, column: 0 },
));
// walid move
assert!(test_board.move_pawn(
&player1,
Position { row: 2, column: 1 },
Position { row: 3, column: 0 }
));
assert!(test_board[3][0].pawn.is_some());
assert_eq!(test_board[3][0].pawn.as_ref().unwrap().owner, player1.id);
assert!(test_board[2][1].pawn.is_none());
}
#[test]
fn test_getting_player_pawns_positions() {
init_logger();
let mut p1: Player = Player::new("Morbius".to_string());
let mut p2: Player = Player::new("Milo".to_string());
let test_board: Board = Board::new(&mut p1, &mut p2, None);
let expected_positions: Vec<Position> = (0..test_board.size)
.into_iter()
.flat_map(|row| {
(0..test_board.size).filter_map(move |column| {
let is_row_even: bool = row % 2 == 0;
let is_column_even: bool = column % 2 == 0;
let is_valid_place_for_a_pawn: bool = (is_row_even ^ is_column_even) && row < 3;
if is_valid_place_for_a_pawn {
Some(Position { row, column })
} else {
None
}
})
})
.collect();
assert_eq!(
expected_positions,
test_board.get_player_pawns_positions(&p1)
);
}
#[test]
fn test_is_movable() {
init_logger();
let mut p1: Player = Player::new("Morbius".to_string());
let mut p2: Player = Player::new("Milo".to_string());
let test_board: Board = Board::new(&mut p1, &mut p2, None);
assert!(test_board.is_movable(Position { row: 2, column: 1 }, &p1));
assert!(!test_board.is_movable(
Position {
row: 10,
column: 12
},
&p1
));
assert!(!test_board.is_movable(Position { row: 0, column: 0 }, &p1));
}
}