dzura/game/src/lib.rs
2024-10-11 18:38:12 -07:00

714 lines
24 KiB
Rust

#![allow(dead_code)]
pub mod constant; use constant::*;
pub mod util; use util::Hex;
pub mod piece; use piece::Piece;
pub mod board; use board::Board;
pub mod history; use history::*;
pub mod config; use config::GameConfig;
mod game; use game::*;
#[derive(Clone, Copy)]
pub enum GameStatus {
Normal,
Check(CheckState),
Checkmate,
Resign,
}
pub type Pool = [u8; 7];
#[derive(Clone)]
pub struct Game {
pub config:GameConfig,
status:GameStatus,
pub turn:u16,
pub board:Board,
pub pool:[Pool; 2],
pub history:Vec<Play>,
}
impl Game {
pub fn new() -> Self
{
Self {
config:GameConfig { },
status:GameStatus::Normal,
turn:0,
board:Board::new(),
pool:[Pool::default(); 2],
history:Vec::new(),
}
}
pub fn init(&mut self)
{
*self = Self::new();
self.board.init();
}
pub fn apply_history(&mut self, history:&Vec<Play>) -> Result<(),()>
{
self.init();
for play in history {
self.process(play).ok();
}
Ok(())
}
pub fn update_board(&mut self)
{
let mut player_moves = 0usize;
let current_player = (self.turn & 1) as u8;
/*
** Reset board meta data.
*/
self.status = GameStatus::Normal;
// Reset columns
for column in &mut self.board.columns {
column.extent = [0, 9];
column.militia = [false; 2];
}
// Reset tiles
for tile in &mut self.board.tiles {
tile.check = false;
tile.threat = [false; 2];
}
// Reset pieces
for piece in &mut self.board.pieces {
if let Some(piece) = piece {
piece.blocking = 0;
}
}
/*
** Fill board meta data; count piece moves.
*/
for piece in &self.board.pieces.clone() {
if let Some(piece) = piece {
let moves = self.get_moves_data(&piece);
let alt_moves = self.get_alts_data(&piece);
/*
** Mark threats, checks, and extents.
*/
for info in &moves {
let hex = Hex::from_tile(info.play.to);
/*
** Mark tile as threatened.
*/
self.board.tiles[info.play.to as usize].threat[piece.player as usize] = true;
/*
** Apply checks.
*/
if info.check.is_check() {
if info.check.is_immediate() {
if let GameStatus::Check(data) = &mut self.status {
data.apply(info.check);
} else {
self.status = GameStatus::Check(info.check);
}
// Mark checking piece's tile.
self.board.tiles[info.play.from as usize].check = true;
}
// Mark tile on checking path.
self.board.tiles[info.play.to as usize].check = true;
}
/*
** Apply column extent.
*/
self.board.columns[hex.x as usize].extent[piece.player as usize] = hex.y as u8;
/*
** Mark column as having unpromoted militia, if present.
*/
if piece.class == PIECE_MILITIA && !piece.promoted {
self.board.columns[hex.x as usize].militia[piece.player as usize] = true;
}
/*
** Count moves for current player.
*/
if piece.player == current_player {
player_moves += moves.len() + alt_moves.len();
}
/*
** Apply blocking to piece on tile, if present.
*/
if info.blocking != 0 {
if let Some(piece_id) = self.board.tiles[info.play.to as usize].piece {
if let Some(piece) = &mut self.board.pieces[piece_id as usize] {
piece.blocking = info.blocking;
}
}
}
}
}
}
/*
** Determine if game is in checkmate.
*/
if player_moves == 0 {
self.status = GameStatus::Checkmate;
}
}
pub fn process(&mut self, play:&Play) -> Result<(),()>
{
let player = (self.turn & 1) as u8;
if self.play_is_valid(play) {
// Move piece on board.
if match play.source {
0 | 2 => {
if let Some(pid) = self.board.tiles[play.from as usize].piece {
if let Some(mut piece) = self.board.pieces[pid as usize] {
let mut swap = false;
if let Some(tid) = self.board.tiles[play.to as usize].piece {
if let Some(target) = &mut self.board.pieces[tid as usize] {
// Check for piece swap.
if piece.player == target.player {
swap = true;
target.tile = play.from;
// Check for target promotion.
let hex = Hex::from_tile(play.from);
if !target.promoted && target.has_promotion() && Hex::is_back(hex.x, hex.y, target.player) {
target.promoted = true;
}
}
// Add captured piece to pool.
else {
self.pool[piece.player as usize][target.class as usize] += 1;
}
}
// Destroy piece if captured.
if !swap { self.board.pieces[tid as usize] = None; }
}
// Set tile/piece associations.
if swap {
self.board.tiles[play.from as usize].piece = self.board.tiles[play.to as usize].piece;
} else {
self.board.tiles[play.from as usize].piece = None;
}
self.board.tiles[play.to as usize].piece = Some(pid);
piece.tile = play.to;
self.board.pieces[pid as usize] = Some(piece);
// Check for piece promotion.
let hex = Hex::from_tile(play.to);
if !piece.promoted && piece.has_promotion() && Hex::is_back(hex.x, hex.y, piece.player) {
if let Some(piece) = &mut self.board.pieces[pid as usize] {
piece.promoted = true;
}
}
self.turn += 1;
true
} else { false }
} else { false }
}
// Place piece from pool.
1 => {
if self.pool[player as usize][play.from as usize] > 0 && self.board.tiles[play.to as usize].piece.is_none() {
self.pool[player as usize][play.from as usize] -= 1;
let piece = Piece::new(play.from, player);
self.board.set_piece(piece, play.to);
self.turn += 1;
true
} else { false }
}
// Player retired.
0xF => {
self.status = GameStatus::Resign;
true
}
_ => false,
} {
self.history.push(*play);
Ok(())
} else { Err(()) }
} else { Err(()) }
}
pub fn play_is_valid(&self, play:&Play) -> bool
// Returns whether a play may be made by the current player.
//
{
//println!("play_is_valid {} {} {}", play.source, play.from, play.to);
let mut valid = false;
let player = (self.turn & 1) as u8;
match play.source {
0 => {
if let Some(piece_id) = self.board.tiles[play.from as usize].piece {
if let Some(piece) = &self.board.pieces[piece_id as usize] {
//println!("piece {} {}", piece.class, piece.player);
if piece.player == player {
for p in self.get_moves(piece) {
if p.to == play.to {
valid = true;
break;
}
}
}
}
}
valid
}
1 => {
if (play.from as usize) < self.pool[player as usize].len() {
if self.pool[player as usize][play.from as usize] > 0 {
for p in self.get_drops(&Piece::new(play.from, player)) {
if p.to == play.to {
valid = true;
break;
}
}
}
}
valid
}
2 => {
if let Some(piece_id) = self.board.tiles[play.from as usize].piece {
if let Some(piece) = &self.board.pieces[piece_id as usize] {
if piece.player == player {
for p in self.get_alts(piece) {
if p.to == play.to {
valid = true;
break;
}
}
}
}
}
valid
}
0xF => { true }
_ => false,
}
}
fn get_moves_data(&self, piece:&Piece) -> Vec<PlayInfo>
{
let mut plays = Vec::<PlayInfo>::new();
let moves = piece.moves();
/*
** Get permitted move directions.
*/
let mut directions = moves.direction;
let blocking_directions = piece.blocking | util::rotate6(piece.blocking);
if blocking_directions != 0 {
if piece.class == PIECE_HEART {
directions &= !blocking_directions;
} else {
directions &= blocking_directions;
}
}
//println!("dir {:b}", directions);
//println!("blk {:b}", blocking_directions);
/*
** Handle movable directions.
*/
while directions != 0 {
let mask_original = util::lsb(directions);
let mut multiplier = 1;
let mut mask = mask_original;
/*
** Shift second tile jumps to standard direction masks.
*/
if (mask & 0xFFF) == 0 {
mask >>= 12;
multiplier = 2;
}
let direction = util::ffs(mask);
//println!(" - dir {}", direction);
/*
** Extract stride value:
** 00 - 1 Tile
** 01 - 2 Tiles
** 10 - Reserved
** 11 - Unlimited
*/
let stride_offset: u32 = direction * 2;
let stride = if direction < 12 {
match ((3 << stride_offset) & moves.stride) >> stride_offset {
0 => 1,
1 => 2,
_ => 8,
}
} else { 1 };
let (mut rx, mut ry) = DIRECTIONS[direction as usize];
rx *= multiplier;
ry *= multiplier;
let mut block_count = 0;
/*
** Step along direction up to max stride.
*/
let mut current_hex = Hex::from_tile(piece.tile);
for stride_dist in 0..stride {
if Hex::is_valid(current_hex.x + rx, current_hex.y + ry) {
current_hex = Hex::from_hex(current_hex.x + rx, current_hex.y + ry);
let mut valid = block_count == 0;
let tile = self.board.tiles[current_hex.tile as usize];
let mut checkstate = CheckState::new();
/*
** If in check, move must break check.
** King may only move to non-threatened tiles.
*/
if piece.class == PIECE_HEART {
valid = valid && !tile.threat[(piece.player == 0) as usize];
} else {
valid = valid && match self.status {
GameStatus::Check(state) => {
state.count() == 1 && tile.check
}
GameStatus::Normal => true,
_ => false,
};
}
/*
** If tile is occupied, piece must be opposing or swappable.
*/
if let Some(target_id) = tile.piece {
if let Some(target) = &self.board.pieces[target_id as usize] {
if target.player == piece.player {
/*
** Move is valid if piece can swap.
*/
valid = valid && stride_dist == 0 && self.can_swap(piece, mask_original, current_hex.tile);
block_count += 2;
} else {
/*
** Handle capturing of king.
*/
if target.class == PIECE_HEART {
if stride_dist == 0 {
checkstate = checkstate.direct();
} else {
checkstate = checkstate.stride();
}
match block_count {
0 => {
// Mark plays in direction as check.
for i in 1..stride_dist {
let index = plays.len() - i;
plays[index].check = checkstate;
}
}
1 => {
// Mark last piece as blocking.
let index = plays.len() - 1;
plays[index].blocking = mask;
}
_ => { }
}
}
block_count += 1;
}
}
}
if valid {
plays.push(PlayInfo {
play:Play { source:0, from:piece.tile, to:current_hex.tile, },
check:checkstate,
blocking:0,
});
}
}
}
directions &= !mask_original;
}
plays
}
pub fn get_moves(&self, piece:&Piece) -> Vec<Play>
{
let mut plays = Vec::new();
for info in self.get_moves_data(piece) {
plays.push(info.play);
}
plays
}
fn get_alts_data(&self, piece:&Piece) -> Vec<PlayInfo>
{
let mut plays = Vec::<PlayInfo>::new();
let piece_moves = piece.moves();
let subject_tiles = if piece.blocking == 0 {
(0..61u8).collect()
} else {
let mut tiles = Vec::new();
let mut directions = piece.blocking | util::rotate6(piece.blocking);
while directions != 0 {
let mask = util::lsb(directions);
let direction = util::ffs(mask);
let (rx, ry) = DIRECTIONS[direction as usize];
let mut current_hex = Hex::from_tile(piece.tile);
for _ in 0..8 {
if Hex::is_valid(current_hex.x + rx, current_hex.y + ry) {
current_hex = Hex::from_hex(current_hex.x + rx, current_hex.y + ry);
if self.board.tiles[current_hex.tile as usize].piece.is_none() {
tiles.push(current_hex.tile);
} else { break }
} else { break }
}
directions &= !mask;
}
tiles
};
if let Some(alt_mode) = piece_moves.alt {
match alt_mode {
// Knight
1 => {
for tile_id in subject_tiles {
if self.can_drop(piece, tile_id, flags::IGNORE_CHECK) {
plays.push(PlayInfo {
play: Play::from_alt(piece.tile, tile_id),
check: CheckState::new(),
blocking: 0,
});
}
}
}
// Caslte
2 => {
for tile_id in subject_tiles {
let hex = Hex::from_tile(piece.tile);
let tile_hex = Hex::from_tile(tile_id);
let in_rear_cone = if piece.player == 0 {
if tile_hex.x >= hex.x {
tile_hex.y <= hex.y
} else {
tile_hex.y <= hex.y - (hex.x - tile_hex.x)
}
} else {
if tile_hex.x >= hex.x {
tile_hex.y >= hex.y + (tile_hex.x - hex.x)
} else {
tile_hex.y >= hex.y
}
};
if in_rear_cone && self.can_drop(piece, tile_id, flags::IGNORE_CHECK | flags::IGNORE_EXTENT) {
plays.push(PlayInfo {
play: Play::from_alt(piece.tile, tile_id),
check: CheckState::new(),
blocking: 0,
});
}
}
}
_ => { }
}
}
plays
}
pub fn get_alts(&self, piece:&Piece) -> Vec<Play>
{
let mut plays = Vec::new();
for info in self.get_alts_data(piece) {
plays.push(info.play);
}
plays
}
pub fn get_drops(&self, piece:&Piece) -> Vec<Play>
{
let mut piece = piece.clone();
let mut moves = Vec::new();
for tile_id in 0..self.board.tiles.len() {
let tile = &self.board.tiles[tile_id];
if tile.piece.is_none() {
piece.tile = tile_id as u8;
if self.can_drop(&piece, tile_id as u8, flags::NONE) {
moves.push(Play::from_drop(piece.class, tile_id as u8));
}
}
}
moves
}
fn can_swap(&self, piece:&Piece, mask:u32, tile_id:u8) -> bool
{
let mut valid = false;
if let Some(target_id) = self.board.tiles[tile_id as usize].piece {
let target_id = target_id as usize;
if let Some(target) = self.board.pieces[target_id as usize] {
valid = true;
/*
** Target must be same color.
*/
valid = valid && piece.player == target.player;
/*
** Target must not be same piece.
*/
valid = valid && !(piece.class == target.class && piece.promoted == target.promoted);
/*
** King may not swap onto a contested tile.
*/
if target.class == PIECE_HEART
&& self.board.tiles[piece.tile as usize].threat[(piece.player == 0) as usize]
{
valid = false;
}
/*
** Target must have movement in reverse direction.
*/
let moves = target.moves().rotate().direction;
valid = valid && (mask & moves) != 0;
}
}
valid
}
fn can_drop(&self, piece:&Piece, tile_id:u8, flags:u32) -> bool
{
let hex = Hex::from_tile(tile_id);
let tile = &self.board.tiles[tile_id as usize];
/*
** Tile must not be occupied.
*/
let mut valid = tile.piece.is_none();
/*
** If in check, a piece may only be dropped if check
** is due to a multi-tile move from a single piece.
*/
valid = valid && match self.status {
GameStatus::Check(state) => {
!state.is_direct() && state.count() == 1 && tile.check
}
GameStatus::Normal => true,
_ => false,
};
/*
** A piece may not be dropped behind the first opposing piece in a column.
*/
if (flags & flags::IGNORE_EXTENT as u32) == 0 {
valid = valid && if piece.player == 0 {
hex.y > self.board.columns[hex.x as usize].extent[1] as i8
} else {
hex.y < self.board.columns[hex.x as usize].extent[0] as i8
};
}
/*
** Militia may not be dropped in a column with another militia present.
*/
if (flags & flags::IGNORE_STACKING as u32) == 0 {
if piece.class == PIECE_MILITIA
&& self.board.columns[hex.x as usize].militia[piece.player as usize]
{
valid = false;
}
}
/*
** A piece must be able to move from its drop position.
** A piece may not be dropped onto a position that puts the opposing king in check.
*/
let piece_moves = self.get_moves(piece);
if piece_moves.len() > 0 {
if (flags & flags::IGNORE_CHECK as u32) == 0 {
for mv in piece_moves {
if let Some(target_id) = self.board.tiles[mv.to as usize].piece {
if let Some(target) = self.board.pieces[target_id as usize] {
if target.class == PIECE_HEART && target.player != piece.player {
valid = false;
break;
}
}
}
}
}
} else {
valid = false;
}
valid
}
pub fn is_complete(&self) -> bool
{
match self.status {
GameStatus::Checkmate | GameStatus::Resign => true,
_ => false,
}
}
}