use crate::bitfield::BitField;
use crate::board::geometry::BoardGeometry;
use crate::board::state::BoardState;
use crate::collections::piecemap::PieceMap;
use crate::collections::tileset::TileSet;
use crate::error::PlayInvalid::{BlockedByPiece, GameOver, MoveOntoBlockedTile, MoveThroughBlockedTile, NoCommonAxis, NoPiece, OutOfBounds, TooFar, WrongPlayer};
use crate::error::{BoardError, PlayInvalid};
use crate::game::state::GameState;
use crate::game::GameOutcome::{Draw, Win};
use crate::game::GameStatus::{Ongoing, Over};
use crate::game::WinReason::{AllCaptured, Enclosed, ExitFort, KingCaptured, KingEscaped};
use crate::game::{DrawReason, GameOutcome, WinReason};
use crate::pieces::PieceType::{King, Soldier};
use crate::pieces::Side::{Attacker, Defender};
use crate::pieces::{Piece, PieceSet, PlacedPiece, Side, KING};
use crate::play::{Play, PlayEffects, PlayRecord, ValidPlay, ValidPlayIterator};
use crate::rules::EnclosureWinRules::WithoutEdgeAccess;
use crate::rules::KingAttack::{Anvil, Armed, Hammer};
use crate::rules::{KingStrength, RepetitionRule, Ruleset, ShieldwallRules};
use crate::tiles::Axis::{Horizontal, Vertical};
use crate::tiles::{Axis, AxisOffset, Coords, RowColOffset, Tile};
use crate::utils::UniqueStack;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Default)]
pub struct Enclosure<B: BitField> {
pub occupied: TileSet<B>,
pub unoccupied: TileSet<B>,
pub boundary: TileSet<B>,
}
impl<B: BitField> Enclosure<B> {
pub fn contains(&self, tile: &Tile) -> bool {
self.occupied.contains(*tile) || self.unoccupied.contains(*tile)
}
}
pub struct DoPlayResult<B: BoardState> {
pub new_state: GameState<B>,
pub record: PlayRecord<B>
}
impl<B: BoardState> From<DoPlayResult<B>> for (GameState<B>, PlayRecord<B>) {
fn from(result: DoPlayResult<B>) -> (GameState<B>, PlayRecord<B>) {
(result.new_state, result.record)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GameLogic<B: BoardState> {
pub rules: Ruleset,
pub board_geo: BoardGeometry<B>
}
impl<B: BoardState> GameLogic<B> {
pub fn new(rules: Ruleset, board_length: u8) -> Self {
Self { rules, board_geo: BoardGeometry::new(board_length) }
}
pub fn special_tile_hostile(&self, tile: Tile, piece: Piece) -> bool {
(self.rules.hostile_tiles.throne.contains(piece) && tile == self.board_geo.special_tiles.throne)
|| (self.rules.hostile_tiles.corners.contains(piece)
&& self.board_geo.special_tiles.corners.contains(tile))
|| (self.rules.hostile_tiles.edge.contains(piece)
&& !self.board_geo.tile_in_bounds(tile))
}
pub fn tile_hostile(&self, tile: Tile, piece: Piece, board: &B) -> bool {
if let Some(other_piece) = board.get_piece(tile) {
(other_piece.side != piece.side) && (
other_piece.piece_type != King
|| self.rules.king_attack == Armed
|| self.rules.king_attack == Anvil
)
} else {
self.special_tile_hostile(tile, piece)
}
}
pub fn coords_hostile(
&self,
coords: Coords,
piece: Piece,
board: &B
) -> bool {
if self.board_geo.coords_in_bounds(coords) {
self.tile_hostile(Tile::new(coords.row as u8, coords.col as u8), piece, board)
} else {
self.rules.hostile_tiles.edge.contains(piece)
}
}
pub fn can_occupy_or_pass(
&self,
play: Play,
piece: Piece,
state: &GameState<B>
) -> (bool, bool) {
let validity = self.validate_play_for_side(play, piece.side, state);
let can_occupy = validity.is_ok();
let can_pass = match validity {
Ok(_) => true,
Err(MoveOntoBlockedTile) => {
if play.to() == self.board_geo.special_tiles.throne {
self.rules.passable_tiles.throne.contains(piece)
&& !state.board.tile_occupied(self.board_geo.special_tiles.throne)
} else {
false
}
},
_ => {
false
}
};
(can_occupy, can_pass)
}
pub fn validate_play_for_side(
&self,
play: Play,
side: Side,
state: &GameState<B>
) -> Result<ValidPlay, PlayInvalid> {
if state.status != Ongoing {
return Err(GameOver)
}
let from = play.from;
let to = play.to();
let maybe_piece = state.board.get_piece(from);
match maybe_piece {
None => Err(NoPiece),
Some(piece) => {
if piece.side != side {
return Err(WrongPlayer);
}
if !(self.board_geo.tile_in_bounds(from) && self.board_geo.tile_in_bounds(to)) {
return Err(OutOfBounds)
}
if (from.row != to.row) && (from.col != to.col) {
return Err(NoCommonAxis)
}
if state.board.tile_occupied(to) {
return Err(BlockedByPiece)
}
let between = self.board_geo.tiles_between(from, to);
if between.iter().any(|t| state.board.tile_occupied(*t)) {
return Err(BlockedByPiece)
}
if !self.rules.occupiable_tiles.corners.contains(piece) &&
self.board_geo.special_tiles.corners.contains(to) {
return Err(MoveOntoBlockedTile)
}
if !self.rules.occupiable_tiles.throne.contains(piece)
&& (to == self.board_geo.special_tiles.throne) {
return Err(MoveOntoBlockedTile)
}
if !self.rules.passable_tiles.throne.contains(piece)
&& between.contains(&self.board_geo.special_tiles.throne) {
return Err(MoveThroughBlockedTile)
}
if self.rules.slow_pieces.contains(piece) && play.distance() > 1 {
return Err(TooFar)
}
Ok(ValidPlay { play })
}
}
}
pub fn validate_play(&self, play: Play, state: &GameState<B>)
-> Result<ValidPlay, PlayInvalid> {
self.validate_play_for_side(play, state.side_to_play, state)
}
pub fn king_beside_throne(&self, board: &B) -> bool {
if let Some(k) = board.get_king() {
self.board_geo.neighbors(self.board_geo.special_tiles.throne).contains(&k)
} else {
false
}
}
pub fn king_on_throne(&self, board: &B) -> bool {
board.get_king() == Some(self.board_geo.special_tiles.throne)
}
pub fn king_is_strong(&self, board: &B) -> bool {
match self.rules.king_strength {
KingStrength::Strong => true,
KingStrength::Weak => false,
KingStrength::StrongByThrone => {
self.king_beside_throne(board) || self.king_on_throne(board)
}
}
}
pub fn coords_occupiable(&self, coords: Coords, piece: Piece) -> bool {
if !self.board_geo.coords_in_bounds(coords) {
return false
}
let t = Tile::new(coords.row as u8, coords.col as u8);
if self.board_geo.special_tiles.throne == t
&& !self.rules.occupiable_tiles.throne.contains(piece) {
return false
}
if !self.rules.occupiable_tiles.corners.contains(piece)
&& self.board_geo.special_tiles.corners.contains(t) {
return false
}
true
}
fn row_col_enclosed(
&self,
row: i8,
col: i8,
enclosed_piece_types: PieceSet,
enclosing_piece_types: PieceSet,
enclosure: &mut Enclosure<B::BitField>,
board: &B,
) -> Option<bool> {
let coords = Coords { row, col };
if let Ok(tile) = self.board_geo.coords_to_tile(coords) {
if let Some(p) = board.get_piece(tile) {
if enclosed_piece_types.contains(p) {
enclosure.occupied.insert(tile);
Some(true)
} else if enclosing_piece_types.contains(p) {
enclosure.boundary.insert(tile);
Some(false)
} else {
None
}
} else {
enclosure.unoccupied.insert(tile);
Some(true)
}
} else {
Some(false)
}
}
pub fn find_enclosure(
&self,
tile: Tile,
enclosed_pieces: PieceSet,
enclosing_pieces: PieceSet,
abort_on_edge: bool,
abort_on_corner: bool,
board: &B,
) -> Option<Enclosure<B::BitField>> {
let Coords { row, col } = Coords::from(tile);
let mut enclosure = Enclosure::default();
if !self.row_col_enclosed(
row, col,
enclosed_pieces, enclosing_pieces,
&mut enclosure,
board
)? {
return None
}
let mut stack: UniqueStack<(i8, i8, i8, i8)> = UniqueStack::default();
stack.push((col, col, row, 1));
stack.push((col, col, row - 1, -1));
while let Some((mut c1, c2, r, dr)) = stack.pop() {
let mut c = c1;
if self.row_col_enclosed(r, c, enclosed_pieces, enclosing_pieces, &mut enclosure, board)? {
while self.row_col_enclosed(
r,
c - 1,
enclosed_pieces,
enclosing_pieces,
&mut enclosure,
board
)? {
let t= Tile::new(r as u8, (c - 1) as u8);
if (abort_on_edge && self.board_geo.tile_at_edge(t))
|| (abort_on_corner && self.board_geo.special_tiles.corners.contains(t)) {
return None
}
c -= 1
}
if c < c1 {
stack.push((c, c1 - 1, r - dr, -dr))
}
}
while c1 <= c2 {
while self.row_col_enclosed(
r,
c1,
enclosed_pieces,
enclosing_pieces,
&mut enclosure,
board
)? {
let t= Tile::new(r as u8, c1 as u8);
if abort_on_edge && self.board_geo.tile_at_edge(t) {
return None
}
if abort_on_corner && self.board_geo.special_tiles.corners.contains(t) {
return None
}
c1 += 1
}
if c1 > c {
stack.push((c, c1 - 1, r + dr, dr));
}
if c1 - 1 > c2 {
stack.push((c2 + 1, c1 - 1, r - dr, -dr))
}
c1 += 1;
while (c1 < c2) && !self.row_col_enclosed(
r,
c1,
enclosed_pieces,
enclosing_pieces,
&mut enclosure,
board
)? {
c1 += 1
}
c = c1
}
}
Some(enclosure)
}
pub fn enclosure_secure(
&self,
encl: &Enclosure<B::BitField>,
inside_safe: bool,
outside_safe: bool,
board: &B
) -> bool {
if inside_safe && outside_safe {
return true
}
for t in &encl.boundary {
let piece = board.get_piece(t)
.expect("Boundary should not include empty tiles.");
let hostile_soldier = Piece::new(Soldier, piece.side.other());
'axisloop: for axis in [Vertical, Horizontal] {
for d in [-1, 1] {
let n_coords = Play::new(t, AxisOffset::new(axis, d)).to_coords();
if let Ok(n_tile) = self.board_geo.coords_to_tile(n_coords) {
let is_inside = encl.contains(&n_tile);
if (inside_safe && is_inside) || (outside_safe && !is_inside) {
if !self.special_tile_hostile(n_tile, piece) {
continue 'axisloop;
}
}
if (!self.tile_hostile(n_tile, piece, board)) && (
board.tile_occupied(n_tile)
|| !self.coords_occupiable(n_coords, hostile_soldier)
) {
continue 'axisloop;
}
} else {
if !self.rules.hostile_tiles.edge.contains(piece) {
continue 'axisloop;
}
}
}
return false
}
}
true
}
fn dir_sw_search(
&self,
play: Play,
sw_rule: ShieldwallRules,
axis: Axis,
away_from_edge: i8,
dir: i8,
state: &GameState<B>
) -> Option<B::PieceMap> {
let mut t = play.to();
let mut wall = B::PieceMap::default();
loop {
let step = Play::new(t, AxisOffset::new(axis, dir));
t = step.to();
if !self.board_geo.tile_in_bounds(t) {
return None
}
if !(
state.board.tile_occupied(t)
|| sw_rule.corners_may_close
&& self.board_geo.special_tiles.corners.contains(t)
) {
return None
}
let piece_opt = state.board.get_piece(t);
if piece_opt.is_none() {
return if wall.occupied().count() < 2 { None } else { Some(wall) };
}
let piece = piece_opt.expect("Tile should be occupied.");
if piece.side == state.side_to_play.other() {
let pin = Play::new(t, AxisOffset::new(axis.other(), away_from_edge)).to();
if let Some(p) = state.board.get_piece(pin) {
if p.side == state.side_to_play {
wall.set(t, piece);
} else {
return None
}
} else {
return None
}
}
if (piece.side == state.side_to_play) ||
(self.board_geo.special_tiles.corners.contains(t) && sw_rule.corners_may_close) {
return if wall.occupied().count() < 2 { None } else { Some(wall) };
}
}
}
pub fn detect_shieldwall(
&self,
valid_play: ValidPlay,
state: &GameState<B>
) -> Option<B::PieceMap> {
let play = valid_play.play;
let sw_rule = self.rules.shieldwall?;
let to = play.to();
let (axis, away_from_edge) = if to.row == 0 {
(Horizontal, 1i8)
} else if to.row == self.board_geo.side_len - 1 {
(Horizontal, -1)
} else if to.col == 0 {
(Vertical, 1)
} else if to.col == self.board_geo.side_len - 1 {
(Vertical, -1)
} else {
return None
};
let mut wall = self.dir_sw_search(play, sw_rule, axis, away_from_edge, -1, state);
if wall.is_none() {
wall = self.dir_sw_search(play, sw_rule, axis, away_from_edge, 1, state);
}
if let Some(w) = wall {
if w.occupied().count() < 2 {
return None
}
Some(w.without_pieces(sw_rule.captures))
} else {
None
}
}
pub fn detect_exit_fort(&self, board: &B) -> bool {
if let Some(king_tile) = board.get_king() {
if !self.board_geo.tile_at_edge(king_tile) {
return false
}
if let Some(encl) = self.find_enclosure(
king_tile,
PieceSet::from(King),
PieceSet::from(Defender),
false,
true,
board,
) {
if !self.board_geo.neighbors(king_tile).iter().any(|t| !board.tile_occupied(*t)) {
return false
}
if !self.enclosure_secure(&encl, true, false, board) {
return false
}
true
} else {
false
}
} else {
false
}
}
pub fn get_captures(
&self,
valid_play: ValidPlay,
moving_piece: Piece,
state: &GameState<B>
) -> B::PieceMap {
let mut captures = B::PieceMap::default();
let to = valid_play.play.to();
if moving_piece.piece_type != King
|| self.rules.king_attack == Armed
|| self.rules.king_attack == Hammer {
for n in self.board_geo.neighbors(to) {
if let Some(other_piece) = state.board.get_piece(n) {
if other_piece.side == moving_piece.side {
continue
}
if other_piece.piece_type == King
&& self.king_beside_throne(&state.board)
&& self.rules.king_strength == KingStrength::StrongByThrone
&& (self.rules.occupiable_tiles.throne.is_empty()
|| self.rules.occupiable_tiles.throne.is_king_only())
&& self.board_geo.neighbors(n).iter().all(|t|
t == &self.board_geo.special_tiles.throne
|| self.tile_hostile(*t, other_piece, &state.board)
) {
captures.set(n, other_piece);
continue
}
let signed_to_row = to.row as i8;
let signed_to_col = to.col as i8;
let signed_n_row = n.row as i8;
let signed_n_col = n.col as i8;
let signed_far_row = signed_to_row + ((signed_n_row - signed_to_row) * 2);
let signed_far_col = signed_to_col + ((signed_n_col - signed_to_col) * 2);
let far_coords = Coords { row: signed_far_row, col: signed_far_col };
if self.coords_hostile(far_coords, other_piece, &state.board) {
if (other_piece.piece_type == King) && self.king_is_strong(&state.board) {
let n_coords = Coords::from(n);
let perp_hostile= if to.row == n.row {
self.coords_hostile(
n_coords + RowColOffset::new(1, 0),
other_piece,
&state.board,
) && self.coords_hostile(
n_coords + RowColOffset::new(-1, 0),
other_piece,
&state.board,
)
} else {
self.coords_hostile(
n_coords + RowColOffset::new(0, 1),
other_piece,
&state.board,
) && self.coords_hostile(
n_coords + RowColOffset::new(0, -1),
other_piece,
&state.board,
)
};
if !perp_hostile {
continue
}
}
captures.set(n, other_piece);
} else if self.rules.linnaean_capture && state.side_to_play == Attacker {
if let Some(_pp) = self.detect_linnaean_capture(
n,
other_piece,
far_coords,
state
) {
captures.set(n, other_piece);
}
}
}
}
}
if let Some(walled) = self.detect_shieldwall(valid_play, state) {
captures.extend(&walled);
}
captures
}
pub fn get_game_outcome(
&self,
valid_play: ValidPlay,
moving_piece: Piece,
state: &GameState<B>,
) -> Option<GameOutcome> {
if state.board.count_pieces_of_side(state.side_to_play.other()) == 0 {
return Some(Win(AllCaptured, state.side_to_play))
}
if state.side_to_play == Attacker {
if let Some(k) = state.board.get_king() {
if let Some(encl_win) = self.rules.enclosure_win {
if let Some(encl) = self.find_enclosure(
k,
PieceSet::from(Defender),
PieceSet::from(Attacker),
encl_win == WithoutEdgeAccess,
true,
&state.board
) {
if encl.occupied.count() == state.board.count_pieces_of_side(Defender) as u32
&& self.enclosure_secure(&encl, false, true, &state.board) {
return Some(Win(Enclosed, Attacker))
}
}
}
} else {
return Some(Win(KingCaptured, Attacker))
}
} else {
if moving_piece.piece_type == King && (
(self.rules.edge_escape && self.board_geo.tile_at_edge(valid_play.play.to()))
|| (!self.rules.edge_escape && self.board_geo.special_tiles.corners.contains(valid_play.play.to()))
) {
return Some(Win(KingEscaped, Defender))
}
if self.rules.exit_fort && self.detect_exit_fort(&state.board) {
return Some(Win(ExitFort, Defender))
}
}
if let Some(RepetitionRule { n_repetitions, is_loss }) = self.rules.repetition_rule {
if state.repetitions.get_repetitions(state.side_to_play) >= n_repetitions {
return if is_loss {
Some(Win(WinReason::Repetition, state.side_to_play.other()))
} else {
Some(Draw(DrawReason::Repetition))
}
}
}
if !self.side_can_play(state.side_to_play.other(), state) {
if self.rules.draw_on_no_plays {
return Some(Draw(DrawReason::NoPlays))
} else {
return Some(Win(WinReason::NoPlays, state.side_to_play))
}
}
None
}
pub fn do_valid_play(
&self,
valid_play: ValidPlay,
mut state: GameState<B>
) -> DoPlayResult<B> {
let play = valid_play.play;
let moving_piece = state.board.move_piece(play.from, play.to());
let captures = self.get_captures(valid_play, moving_piece, &state);
state.board.remove_placed_pieces(&captures);
state.repetitions.track_play(state.side_to_play, play, !captures.is_empty());
if captures.is_empty() {
state.plays_since_capture += 1;
}
let game_outcome = self.get_game_outcome(valid_play, moving_piece, &state);
state.turn += 1;
let game_status = match game_outcome {
Some(game_outcome) => Over(game_outcome),
None => Ongoing
};
let outcome = PlayEffects { captures, game_outcome };
let record = PlayRecord {
side: state.side_to_play, play,
effects: outcome
};
state.side_to_play = state.side_to_play.other();
state.status = game_status;
DoPlayResult { new_state: state, record }
}
pub fn do_play(
&self,
play: Play,
state: GameState<B>
) -> Result<DoPlayResult<B>, PlayInvalid> {
let valid_play = self.validate_play(play, &state)?;
Ok(self.do_valid_play(valid_play, state))
}
pub fn side_can_play(&self, side: Side, state: &GameState<B>) -> bool {
for tile in state.board.occupied_by_side(side) {
if self.iter_plays(tile, state)
.expect("Tile must not be empty.")
.next().is_some() {
return true
}
}
false
}
pub fn iter_plays<'logic, 'state>(
&'logic self,
tile: Tile,
state: &'state GameState<B>
) -> Result<ValidPlayIterator<'logic, 'state, B>, BoardError> {
ValidPlayIterator::new(self, state, tile)
}
fn detect_linnaean_capture(
&self,
tile: Tile,
other_piece: Piece,
far_coords: Coords,
state: &GameState<B>
) -> Option<PlacedPiece> {
if let Ok(far_tile) = self.board_geo.coords_to_tile(far_coords) {
if far_tile == self.board_geo.special_tiles.throne
&& state.board.is_king(far_tile)
&& self.board_geo.neighbors(far_tile).iter()
.filter(|t|
self.tile_hostile(**t, KING, &state.board)
).count() == 3 {
return Some(PlacedPiece { tile, piece: other_piece})
}
}
None
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::board::state::BoardState;
use crate::error::PlayInvalid;
use crate::error::PlayInvalid::{BlockedByPiece, MoveOntoBlockedTile, MoveThroughBlockedTile, NoPiece, OutOfBounds, TooFar};
use crate::game::logic::GameLogic;
use crate::game::state::GameState;
use crate::game::Game;
use crate::game::GameOutcome::Win;
use crate::game::GameStatus::{Ongoing, Over};
use crate::game::WinReason::{KingCaptured, KingEscaped, Repetition};
use crate::pieces::PieceType::{King, Soldier};
use crate::pieces::Side::{Attacker, Defender};
use crate::pieces::{Piece, PieceSet, PlacedPiece, KING};
use crate::play::{Play, ValidPlay};
use crate::preset::{boards, rules};
use crate::rules::{HostilityRules, PassRules, Ruleset, ShieldwallRules};
use crate::tiles::Tile;
use crate::{basic_piecemap, tileset};
use crate::utils::check_tile_vec;
use std::str::FromStr;
use crate::aliases::{HugeBasicBoardState, LargeBasicBoardState, MediumBasicBoardState, MediumBasicGameState, SmallBasicBoardState, SmallBasicGameState};
use crate::collections::piecemap::PieceMap;
const TEST_RULES: Ruleset = Ruleset {
slow_pieces: PieceSet::from_piece_type(King),
passable_tiles: PassRules {
throne: PieceSet::none(),
..rules::BRANDUBH.passable_tiles
},
..rules::BRANDUBH
};
fn assert_valid_play<B: BoardState>(logic: GameLogic<B>, play: Play, state: &GameState<B>) {
assert_eq!(logic.validate_play(play, state), Ok(ValidPlay { play }));
}
fn assert_invalid_play<B: BoardState>(
logic: GameLogic<B>,
play: Play,
state: &GameState<B>,
reason: PlayInvalid
) {
assert_eq!(logic.validate_play(play, state), Err(reason));
}
fn generic_test_play_validity<B: BoardState>() {
let logic = GameLogic::new(rules::BRANDUBH, 7);
let mut state: GameState<B> = GameState::new(boards::BRANDUBH, logic.rules.starting_side)
.expect("Could not initiate game state.");
assert_valid_play(
logic,
Play::from_tiles(Tile::new(3, 1), Tile::new(4, 1)).unwrap(),
&state
);
assert_invalid_play(
logic,
Play::from_tiles(Tile::new(0, 3), Tile::new(0, 0)).unwrap(),
&state,
MoveOntoBlockedTile
);
assert_invalid_play(
logic,
Play::from_tiles(Tile::new(1, 1), Tile::new(2, 1)).unwrap(),
&state,
NoPiece
);
assert_invalid_play(
logic,
Play::from_tiles(Tile::new(0, 3), Tile::new(0, 7)).unwrap(),
&state,
OutOfBounds
);
assert_invalid_play(
logic,
Play::from_tiles(Tile::new(0, 3), Tile::new(2, 3)).unwrap(),
&state,
BlockedByPiece
);
state = logic.do_play(
Play::from_tiles(
Tile::new(3, 1),
Tile::new(4, 1)
).unwrap(),
state
).unwrap().new_state;
let play = Play::from_tiles(
Tile::new(3, 3),
Tile::new(3, 2)
).unwrap();
assert_invalid_play(logic, play, &state, BlockedByPiece);
state.board.move_piece(Tile::new(3, 2), Tile::new(4, 2));
state.board.move_piece(Tile::new(3, 3), Tile::new(3, 2));
assert_invalid_play(
logic,
Play::from_tiles(Tile::new(2, 3), Tile::new(3, 3)).unwrap(),
&state,
MoveOntoBlockedTile
);
assert_valid_play(
logic,
Play::from_tiles(Tile::new(3, 2), Tile::new(3, 3)).unwrap(),
&state
);
let logic = GameLogic::new(TEST_RULES, 7);
let mut state: GameState<B> = GameState::new("7/5Tt/2T4/2t2t1/Tt4T/2t4/2T2K1", Defender)
.expect("Could not initiate game state.");
assert_invalid_play(
logic,
Play::from_tiles(Tile::new(6, 5), Tile::new(6, 3)).unwrap(),
&state,
TooFar
);
assert_valid_play(
logic,
Play::from_tiles(Tile::new(6, 5), Tile::new(6, 4)).unwrap(),
&state
);
state.side_to_play = Attacker;
assert_invalid_play(
logic,
Play::from_tiles(Tile::new(3, 2), Tile::new(3, 4)).unwrap(),
&state,
MoveThroughBlockedTile
);
}
#[test]
fn test_play_validity() {
generic_test_play_validity::<SmallBasicBoardState>();
generic_test_play_validity::<MediumBasicBoardState>();
generic_test_play_validity::<LargeBasicBoardState>();
generic_test_play_validity::<HugeBasicBoardState>();
}
fn generic_test_play_outcome<T: BoardState>() {
let proto: (GameLogic<T>, GameState<T>) = (
GameLogic::new(TEST_RULES, 7),
GameState::new("4t2/5Tt/2T4/2t2t1/Tt4T/2t4/2T2K1", TEST_RULES.starting_side).unwrap()
);
let (logic, mut state) = proto.clone();
let vp = logic.validate_play(
Play::from_tiles(Tile::new(0, 4), Tile::new(6, 4)).unwrap(),
&state
).unwrap();
let piece = state.board.move_piece(vp.play.from, vp.play.to());
assert_eq!(
logic.get_captures(vp, piece, &state).into_iter().collect::<HashSet<_>>(),
basic_piecemap!(u64,
PlacedPiece { tile: Tile::new(6, 5), piece: Piece::defender(King) }
).into_iter().collect::<HashSet<_>>()
);
state.board.move_piece(vp.play.to(), vp.play.from);
assert_eq!(logic.do_play(vp.play, state).unwrap().new_state.status, Over(Win(KingCaptured, Attacker)));
let (logic, mut state) = proto.clone();
state.side_to_play = Defender;
let vp = ValidPlay { play: Play::from_tiles(Tile::new(4, 6), Tile::new(4, 2)).unwrap() };
let piece = state.board.move_piece(vp.play.from, vp.play.to());
assert_eq!(
logic.get_captures(vp, piece, &state),
basic_piecemap!(u64,
PlacedPiece::new(Tile::new(4, 1), Piece::new(Soldier, Attacker)),
PlacedPiece::new(Tile::new(3, 2), Piece::new(Soldier, Attacker)),
PlacedPiece::new(Tile::new(5, 2), Piece::new(Soldier, Attacker))
).into_iter().collect()
);
state.board.move_piece(vp.play.to(), vp.play.from);
assert_eq!(logic.do_valid_play(vp, state).new_state.status, Ongoing);
let (logic, mut state) = proto.clone();
state.side_to_play = Defender;
let vp = logic.validate_play(
Play::from_tiles(Tile::new(6, 5), Tile::new(6, 6)).unwrap(),
&state
).unwrap();
let piece = state.board.move_piece(vp.play.from, vp.play.to());
assert!(logic.get_captures(vp, piece, &state).is_empty());
state.board.move_piece(vp.play.to(), vp.play.from);
assert_eq!(logic.do_valid_play(vp, state).new_state.status, Over(Win(KingEscaped, Defender)));
let (logic, mut state) = proto.clone();
state.side_to_play = Defender;
let vp = logic.validate_play(
Play::from_tiles(Tile::new(6, 5), Tile::new(5, 5)).unwrap(),
&state
).unwrap();
let piece = state.board.move_piece(vp.play.from, vp.play.to());
assert!(logic.get_captures(vp, piece, &state).is_empty());
state.board.move_piece(vp.play.to(), vp.play.from);
assert_eq!(logic.do_valid_play(vp, state).new_state.status, Ongoing);
}
#[test]
fn test_play_outcome() {
generic_test_play_outcome::<SmallBasicBoardState>();
generic_test_play_outcome::<MediumBasicBoardState>();
generic_test_play_outcome::<LargeBasicBoardState>();
generic_test_play_outcome::<HugeBasicBoardState>();
}
#[test]
fn test_shieldwalls() {
let no_corner_rules = Ruleset{
shieldwall: Some(ShieldwallRules{
corners_may_close: false,
captures: PieceSet::from(Soldier)
}),
..rules::COPENHAGEN
};
let king_capture_rules = Ruleset{
shieldwall: Some(ShieldwallRules{
corners_may_close: false,
captures: PieceSet::all()
}),
..rules::COPENHAGEN
};
let corner_sw = "9/9/9/9/6t2/7tT/7tT/7tT/9";
let regular_sw = "9/9/9/6t2/7tT/7tT/7tT/8t/9";
let regular_sw_king = "9/9/9/6t2/7tT/7tK/7tT/8t/9";
let no_sw_gap = "9/9/9/6t2/7tT/8T/7tT/8t/9";
let no_sw_friend = "9/9/9/6t2/7tT/6tTT/7tT/8t/9";
let no_sw_small = "9/9/9/6t2/7tT/8t/9/9/9";
let cm = ValidPlay { play: Play::from_tiles(
Tile::new(4, 6),
Tile::new(4, 8)
).unwrap() };
let m = ValidPlay { play: Play::from_tiles(
Tile::new(3, 6),
Tile::new(3, 8)
).unwrap() };
let n = ValidPlay { play: Play::from_tiles(
Tile::new(3, 6),
Tile::new(3, 7)
).unwrap() };
let corner_logic: GameLogic<MediumBasicBoardState> = GameLogic::new(rules::COPENHAGEN, 9);
let corner_state: GameState<MediumBasicBoardState> = GameState::new(corner_sw, Attacker).unwrap();
assert_eq!(corner_logic.detect_shieldwall(n, &corner_state), None);
assert_eq!(
corner_logic.detect_shieldwall(cm, &corner_state).unwrap().occupied(),
tileset!(
Tile::new(5, 8),
Tile::new(6, 8),
Tile::new(7, 8)
)
);
let no_corner_logic: GameLogic<MediumBasicBoardState> = GameLogic::new(no_corner_rules, 9);
assert_eq!(no_corner_logic.detect_shieldwall(m, &corner_state), None);
let regular_logic: GameLogic<MediumBasicBoardState> = GameLogic::new(no_corner_rules, 9);
let regular_state: GameState<MediumBasicBoardState> = GameState::new(regular_sw, Attacker).unwrap();
assert_eq!(
regular_logic.detect_shieldwall(m, ®ular_state).unwrap().occupied(),
tileset!(
Tile::new(4, 8),
Tile::new(5, 8),
Tile::new(6, 8)
)
);
let king_state: GameState<MediumBasicBoardState> = GameState::new(regular_sw_king, Attacker).unwrap();
assert_eq!(
regular_logic.detect_shieldwall(m, &king_state).unwrap().occupied(),
tileset!(
Tile::new(4, 8),
Tile::new(6, 8)
)
);
let king_cap_logic: GameLogic<MediumBasicBoardState> = GameLogic::new(king_capture_rules, 9);
assert_eq!(
king_cap_logic.detect_shieldwall(m, &king_state).unwrap().occupied(),
tileset!(
Tile::new(4, 8),
Tile::new(5, 8),
Tile::new(6, 8)
)
);
let gap_state: GameState<MediumBasicBoardState> = GameState::new(no_sw_gap, Attacker).unwrap();
assert_eq!(regular_logic.detect_shieldwall(m, &gap_state), None);
let friend_state: GameState<MediumBasicBoardState> = GameState::new(no_sw_friend, Attacker).unwrap();
assert_eq!(regular_logic.detect_shieldwall(m, &friend_state), None);
let small_state: GameState<MediumBasicBoardState> = GameState::new(no_sw_small, Attacker).unwrap();
assert_eq!(regular_logic.detect_shieldwall(m, &small_state), None);
}
#[test]
fn test_encl_secure() {
let setup_1 = "7/2ttt2/1t1K1t1/2ttt2/7";
let setup_2 = "7/1tttt2/1t1K1t1/2tttt1/7";
let setup_3 = "2t1t2/1t1t1t1/1t1K1t1/2ttt2/7";
let setup_4 = "2t2t1/1t3t1/1t1K1t1/2ttt2/7";
let safe_corners = Ruleset {
hostile_tiles: HostilityRules {
corners: PieceSet::none(),
edge: PieceSet::none(),
throne: PieceSet::none()
},
..rules::COPENHAGEN
};
let candidates = [
(&setup_1, false, true, true, rules::COPENHAGEN),
(&setup_1, false, false, false, rules::COPENHAGEN),
(&setup_2, false, true, true, rules::COPENHAGEN),
(&setup_2, true, false, true, rules::COPENHAGEN),
(&setup_3, false, true, false, rules::COPENHAGEN),
(&setup_4, false, true, false, rules::COPENHAGEN),
(&setup_4, false, true, true, safe_corners),
(&setup_4, true, false, true, rules::COPENHAGEN),
];
for (string, inside_safe, outside_safe, is_secure, rules) in candidates {
let logic: GameLogic<SmallBasicBoardState> = GameLogic::new(rules, 7);
let state: GameState<SmallBasicBoardState> = GameState::new(string, rules.starting_side).unwrap();
let encl_opt = logic.find_enclosure(
Tile::new(2, 3),
PieceSet::from(King),
PieceSet::from(Piece::new(Soldier, Attacker)),
false, false,
&state.board,
);
assert!(encl_opt.is_some());
let encl = encl_opt.unwrap();
assert_eq!(logic.enclosure_secure(&encl, inside_safe, outside_safe, &state.board), is_secure)
}
}
#[test]
fn test_exit_forts() {
let exit_fort_flat = "9/9/8t/7tT/7T1/6tT1/7TK/7tT/9";
let exit_fort_bulge = "9/9/9/9/9/5TTTT/5T2K/6TTT/9";
let no_fort_enemy = "9/9/9/8T/7Tt/7T1/7TK/8T/9";
let no_fort_unfree = "9/9/9/8T/7TT/7TT/7TK/8T/9";
let no_fort_gap = "9/9/9/8T/9/4t2T1/7TK/8T/9";
let no_fort_vuln = "9/9/9/9/9/6TTT/5T2K/6TTT/9";
for s in [exit_fort_flat, exit_fort_bulge] {
let logic: GameLogic<MediumBasicBoardState> = GameLogic::new(rules::COPENHAGEN, 9);
let state: GameState<MediumBasicBoardState> = GameState::new(s, logic.rules.starting_side).unwrap();
assert!(logic.detect_exit_fort(&state.board));
}
for s in [no_fort_enemy, no_fort_unfree, no_fort_gap, no_fort_vuln] {
let logic: GameLogic<MediumBasicBoardState> = GameLogic::new(rules::COPENHAGEN, 9);
let state: GameState<MediumBasicBoardState> = GameState::new(s, logic.rules.starting_side).unwrap();
assert!(!logic.detect_exit_fort(&state.board));
}
}
#[test]
fn test_enclosures() {
let full_enclosure = "2ttt2/1t1K1t1/2tttt1/7/7/7/7";
let encl_with_edge = "2t1t2/1t1K1t1/2tttt1/7/7/7/7";
let encl_with_corner = "5t1/4tK1/4ttt/7/7/7/7";
let encl_with_soldier = "2ttt2/1t1KTt1/2tttt1/7/7/7/7";
let encl_edge_2 = "1t2t2/1t1K1t1/2tttt1/7/7/7/7";
let state = SmallBasicBoardState::from_str(full_enclosure).unwrap();
let game_logic: GameLogic<SmallBasicBoardState> = GameLogic::new(rules::BRANDUBH, state.side_len());
let encl_res = game_logic.find_enclosure(
Tile::new(1, 3),
PieceSet::from(King),
PieceSet::from(Soldier),
true,
true,
&state
);
assert!(encl_res.is_some());
let encl = encl_res.unwrap();
check_tile_vec(encl.occupied.into_iter().collect(), vec![Tile::new(1, 3)]);
check_tile_vec(
encl.unoccupied.into_iter().collect(),
vec![Tile::new(1, 2), Tile::new(1, 4)]
);
check_tile_vec(
encl.boundary.into_iter().collect(),
vec![
Tile::new(0, 2), Tile::new(0, 3), Tile::new(0, 4),
Tile::new(1, 1), Tile::new(1, 5),
Tile::new(2, 2), Tile::new(2, 3), Tile::new(2, 4)
]
);
let state = SmallBasicBoardState::from_str(encl_with_edge).unwrap();
let game_logic: GameLogic<SmallBasicBoardState> = GameLogic::new(rules::BRANDUBH, state.side_len());
let encl_res = game_logic.find_enclosure(
Tile::new(1, 3),
PieceSet::from(King),
PieceSet::from(Soldier),
true,
true,
&state
);
assert!(encl_res.is_none());
let encl_res = game_logic.find_enclosure(
Tile::new(1, 3),
PieceSet::from(King),
PieceSet::from(Soldier),
false,
true,
&state
);
assert!(encl_res.is_some());
let encl = encl_res.unwrap();
check_tile_vec(encl.occupied.into_iter().collect(), vec![Tile::new(1, 3)]);
check_tile_vec(
encl.unoccupied.into_iter().collect(),
vec![Tile::new(0, 3), Tile::new(1, 2), Tile::new(1, 4)]
);
check_tile_vec(
encl.boundary.into_iter().collect(),
vec![
Tile::new(0, 2), Tile::new(0, 4),
Tile::new(1, 1), Tile::new(1, 5),
Tile::new(2, 2), Tile::new(2, 3), Tile::new(2, 4)
]
);
let state = SmallBasicBoardState::from_str(encl_with_corner).unwrap();
let game_logic: GameLogic<SmallBasicBoardState> = GameLogic::new(rules::BRANDUBH, state.side_len());
let encl_res = game_logic.find_enclosure(
Tile::new(1, 3),
PieceSet::from(King),
PieceSet::from(Soldier),
false,
true,
&state
);
assert!(encl_res.is_none());
let encl_res = game_logic.find_enclosure(
Tile::new(1, 5),
PieceSet::from(King),
PieceSet::from(Soldier),
false,
false,
&state
);
assert!(encl_res.is_some());
let encl = encl_res.unwrap();
check_tile_vec(encl.occupied.into_iter().collect(), vec![Tile::new(1, 5)]);
check_tile_vec(
encl.unoccupied.into_iter().collect(),
vec![Tile::new(0, 6), Tile::new(1, 6)]
);
check_tile_vec(
encl.boundary.into_iter().collect(),
vec![
Tile::new(0, 5), Tile::new(1, 4),
Tile::new(2, 5), Tile::new(2, 6)
]
);
let state = SmallBasicBoardState::from_str(encl_with_soldier).unwrap();
let game_logic: GameLogic<SmallBasicBoardState> = GameLogic::new(rules::BRANDUBH, state.side_len());
let encl_res = game_logic.find_enclosure(
Tile::new(1, 3),
PieceSet::from(King),
PieceSet::from(Piece::new(Soldier, Attacker)),
true,
true,
&state
);
assert!(encl_res.is_none());
let encl_res = game_logic.find_enclosure(
Tile::new(1, 3),
PieceSet::from(vec![Piece::new(King, Defender), Piece::new(Soldier, Defender)]),
PieceSet::from(Piece::new(Soldier, Attacker)),
true,
true,
&state
);
assert!(encl_res.is_some());
let encl = encl_res.unwrap();
check_tile_vec(
encl.occupied.into_iter().collect(),
vec![Tile::new(1, 3), Tile::new(1, 4)]);
check_tile_vec(
encl.unoccupied.into_iter().collect(),
vec![Tile::new(1, 2)]
);
check_tile_vec(
encl.boundary.into_iter().collect(),
vec![
Tile::new(0, 2), Tile::new(0, 3), Tile::new(0, 4),
Tile::new(1, 1), Tile::new(1, 5),
Tile::new(2, 2), Tile::new(2, 3), Tile::new(2, 4)
]
);
let state = SmallBasicBoardState::from_str(encl_edge_2).unwrap();
let game_logic: GameLogic<SmallBasicBoardState> = GameLogic::new(rules::BRANDUBH, state.side_len());
let encl_res = game_logic.find_enclosure(
Tile::new(1, 3),
PieceSet::from(King),
PieceSet::from(Piece::new(Soldier, Attacker)),
false,
false,
&state
);
assert!(encl_res.is_some());
}
#[test]
fn test_can_play() {
let logic = GameLogic::new(rules::BRANDUBH, 7);
let state: GameState<SmallBasicBoardState> = GameState::new(
"2tt3/1tTKt2/2tt3/7/7/7/7",
logic.rules.starting_side
).unwrap();
assert!(logic.side_can_play(Attacker, &state));
assert!(!logic.side_can_play(Defender, &state));
let state: GameState<SmallBasicBoardState> = GameState::new(
"2tKt2/3t3/7/7/7/7/7",
logic.rules.starting_side
).unwrap();
assert!(logic.side_can_play(Attacker, &state));
assert!(!logic.side_can_play(Defender, &state));
}
#[test]
fn test_repetitions() {
let mut game: Game<SmallBasicBoardState> = Game::new(
rules::BRANDUBH,
boards::BRANDUBH
).unwrap();
for _ in 0..3 {
game.do_play(Play::from_str("d6-f6").unwrap()).unwrap();
game.do_play(Play::from_str("d5-f5").unwrap()).unwrap();
game.do_play(Play::from_str("f6-d6").unwrap()).unwrap();
game.do_play(Play::from_str("f5-d5").unwrap()).unwrap();
}
assert_eq!(game.state.status, Ongoing);
game.do_play(Play::from_str("d6-f6").unwrap()).unwrap();
assert_eq!(game.state.status, Over(Win(Repetition, Defender)));
}
#[test]
fn test_strong_king_capture() {
let logic = GameLogic::new(rules::BRANDUBH, 7);
let king = PlacedPiece { tile: Tile::new(3, 4), piece: KING };
let (_, record) = logic.do_play(
Play::from_tiles(Tile::new(3, 6), Tile::new(3, 5)).unwrap(),
SmallBasicGameState::new("1T5/7/4t2/4K1t/4t2/7/7", Attacker).unwrap()
).unwrap().into();
assert!(record.effects.captures.king.contains(king.tile));
assert_eq!(record.effects.captures.occupied().count(), 1);
assert_eq!(record.effects.game_outcome, Some(Win(KingCaptured, Attacker)));
let (_, record) = logic.do_play(
Play::from_tiles(Tile::new(1, 4), Tile::new(2, 4)).unwrap(),
SmallBasicGameState::new("1T5/4t2/7/4Kt1/4t2/7/7", Attacker).unwrap()
).unwrap().into();
assert!(record.effects.captures.king.contains(king.tile));
assert_eq!(record.effects.captures.occupied().count(), 1);
assert_eq!(record.effects.game_outcome, Some(Win(KingCaptured, Attacker)));
let (_, record) = logic.do_play(
Play::from_tiles(Tile::new(3, 6), Tile::new(3, 5)).unwrap(),
SmallBasicGameState::new("1T5/7/7/4K1t/4t2/7/7", Attacker).unwrap()
).unwrap().into();
assert!(record.effects.captures.is_empty());
assert_eq!(record.effects.game_outcome, None);
let (_, record) = logic.do_play(
Play::from_tiles(Tile::new(1, 4), Tile::new(2, 4)).unwrap(),
SmallBasicGameState::new("1T5/4t2/7/4K2/4t2/7/7", Attacker).unwrap()
).unwrap().into();
assert!(record.effects.captures.is_empty());
assert_eq!(record.effects.game_outcome, None);
}
#[test]
fn test_linnaean_capture() {
let logic = GameLogic::new(rules::TABLUT, 9);
let state = MediumBasicGameState::new(
"tT7/9/9/4t4/t2TKt3/4t4/9/9/9",
Attacker
).unwrap();
let (_, r) = logic.do_play(
Play::from_tiles(
Tile::new(4, 0),
Tile::new(4, 2)
).expect("Invalid play."),
state
).expect("Invalid play").into();
assert_eq!(r.effects.captures.occupied(), tileset!(Tile::new(4, 3)));
}
}