#![allow(dead_code)] pub mod constant; use constant::*; pub mod util; use util::Hex; pub mod piece; use piece::{Piece, PieceClass}; pub mod board; use board::Board; pub mod history; use history::*; pub mod config; use config::GameConfig; mod game; use game::*; mod ranking; pub use ranking::Ranking; #[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, } impl Game { pub fn new() -> Self { Self { config:GameConfig::new(), 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(); self.update_board(); } pub fn apply_history(&mut self, history:&Vec) -> Result<(),()> { self.init(); for play in history { if self.process(play).is_err() { break; } } Ok(()) } pub fn update_board(&mut self) { if self.is_complete() { return; } /* ** Reset board meta data. */ self.status = GameStatus::Normal; // Reset columns for column in &mut self.board.columns { column.extent = [0, 8]; 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 piece_hex = Hex::from_tile(piece.tile); /* ** Apply column extent. */ if piece.player == 0 { self.board.columns[piece_hex.x as usize].extent[0] = self.board.columns[piece_hex.x as usize].extent[0].max(piece_hex.y as u8); } else { self.board.columns[piece_hex.x as usize].extent[1] = self.board.columns[piece_hex.x as usize].extent[1].min(piece_hex.y as u8); } /* ** Mark column as having unpromoted militia, if present. */ if piece.class == PIECE_MILITIA && !piece.promoted { self.board.columns[piece_hex.x as usize].militia[piece.player as usize] = true; } /* ** Mark threats, checks, and blocking. */ for info in &self.get_moves_data(&piece) { /* ** Mark tile as threatened. */ if info.valid || info.threat { self.board.tiles[info.play.to as usize].threat[piece.player as usize] = true; } if info.valid { /* ** 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 blocking to piece on tile, if present. */ if info.blocking != 0 { if let Some(target_id) = self.board.tiles[info.play.to as usize].piece { if let Some(target) = &mut self.board.pieces[target_id as usize] { target.blocking = info.blocking; } } } } } } } /* ** Determine if game is in checkmate. */ if self.get_plays().len() == 0 { self.status = GameStatus::Checkmate; } } pub fn get_plays(&mut self) -> Vec { let mut plays = Vec::new(); let current_player = (self.turn & 1) as u8; /* ** Fill board meta data; count piece moves. */ for piece in &self.board.pieces.clone() { if let Some(piece) = piece { if piece.player == current_player { plays.append(&mut self.get_moves(&piece)); plays.append(&mut self.get_alts(&piece)); } } } /* ** Add drops to player moves. */ for i in 0..6 { if self.pool[current_player as usize][i] > 0 { plays.append(&mut self.get_drops(&Piece::new(i as u8, current_player))); } } plays } pub fn process(&mut self, play:&Play) -> Result<(),()> { let player = (self.turn & 1) as u8; if self.play_is_valid(play) { let mut meta = 0; // Move piece on board. match play.source { 0 | 2 => { if let Some(piece_id) = self.board.tiles[play.from as usize].piece { if let Some(mut piece) = self.board.pieces[piece_id as usize] { let mut swap = false; meta = piece.class | ((piece.promoted as u8) << 3) | (piece.player << 4); 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; } } /* ** Move piece to new tile. */ 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(piece_id); piece.tile = play.to; // 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) { piece.promoted = true; } self.board.pieces[piece_id as usize] = Some(piece); self.turn += 1; } } } // Place piece from pool. 1 => { meta = play.from | (player << 4); 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; } // Player retired. 0xF => { self.status = GameStatus::Resign; } _ => { } } self.history.push(Play { source: play.source, from: play.from, to: play.to, meta, }); self.update_board(); Ok(()) } else { Err(()) } } pub fn play_is_valid(&self, play:&Play) -> bool // Returns whether a play may be made by the current player. // { if self.is_complete() { return false; } 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] { if piece.player == player { for p in self.get_moves_data(piece) { if p.play.to == play.to { if p.valid { 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 { let plays = self.get_alts(piece); for p in plays { if p.to == play.to { valid = true; break; } } } } } valid } 0xF => true, _ => false, } } fn get_moves_data(&self, piece:&Piece) -> Vec { let mut plays = Vec::::new(); let current_player = (self.turn & 1) as u8; let moves = piece.moves(); /* ** Get permitted move directions. */ let mut directions = moves.direction; let mut blocking_directions = piece.blocking; blocking_directions |= blocking_directions << 12; if blocking_directions != 0 { if piece.class == PIECE_HEART { directions &= !blocking_directions; } else { directions &= blocking_directions; } } /* ** Handle movable directions. */ while directions != 0 { let mask_original = util::lsb(directions); let direction_original = util::ffs(mask_original); let mut multiplier = 1; /* ** Shift second tile jumps to standard direction masks. */ let mut mask = mask_original; if (mask & 0xFFF) == 0 { mask >>= 12; multiplier = 2; } let direction = util::ffs(mask); /* ** Extract stride value: ** 00 - 1 Tile ** 01 - 2 Tiles ** 10 - Reserved ** 11 - Unlimited */ let stride_offset: u32 = direction * 2; let stride = if direction_original < 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; /* ** Step along direction up to max stride. */ let mut block_count = 0; 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 tile = self.board.tiles[current_hex.tile as usize]; let mut valid = block_count == 0; let mut checkstate = CheckState::new(); let mut blocking = 0; let mut threat = false; /* ** If player in check, move must break check. ** King may only move to non-threatened tiles. */ if piece.class == PIECE_HEART { valid &= !tile.threat[(piece.player == 0) as usize]; } else { if piece.player == current_player { 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] { /* ** Target is same army. */ 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); if block_count == 0 { threat = true; } block_count += 2; } /* ** Target is opposing army. */ else { /* ** Find checking of king and blocking pieces. */ if target.class == PIECE_HEART { match block_count { 0 => { /* ** Mark plays in direction as checking. */ if stride_dist == 0 { checkstate = checkstate.direct(); } else { checkstate = checkstate.stride(); } for i in 1..=stride_dist { let index = plays.len() - i; plays[index].check = checkstate; } /* ** Apply blocking to king in directions that can be reached by piece. */ if stride_dist < stride - 1 { blocking = mask; if stride_dist > 0 { blocking |= util::rotate6(mask); } } } 1 => { // Mark last piece as blocking. let index = plays.len() - 1; plays[index].blocking = mask | util::rotate6(mask); } _ => { } } } block_count += 1; } } } plays.push(PlayInfo { valid, threat, play:Play { source:0, from:piece.tile, to:current_hex.tile, meta:0 }, check:checkstate.immediate(), blocking:blocking, }); } } directions &= !mask_original; } plays } pub fn get_moves(&self, piece:&Piece) -> Vec { let mut plays = Vec::new(); for info in self.get_moves_data(piece) { if info.valid { plays.push(info.play); } } plays } fn get_alts_data(&self, piece:&Piece) -> Vec { let mut plays = Vec::new(); /* ** Get allowed target tiles. */ 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 }; /* ** Filter valid tiles from allowed. */ 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 | flags::IGNORE_EXTENT) { plays.push(PlayInfo { valid: true, threat: false, play: Play::from_alt(piece.tile, tile_id), check: CheckState::new(), blocking: 0, }); } } } // Castle 2 => { for tile_id in subject_tiles { let piece_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 >= piece_hex.x { tile_hex.y <= piece_hex.y } else { tile_hex.y <= piece_hex.y - (piece_hex.x - tile_hex.x) } } else { if tile_hex.x >= piece_hex.x { tile_hex.y >= piece_hex.y + (tile_hex.x - piece_hex.x) } else { tile_hex.y >= piece_hex.y } }; if in_rear_cone && self.can_drop(piece, tile_id, flags::IGNORE_CHECK | flags::IGNORE_EXTENT) { plays.push(PlayInfo { valid: true, threat: false, play: Play::from_alt(piece.tile, tile_id), check: CheckState::new(), blocking: 0, }); } } } _ => { } } } plays } pub fn get_alts(&self, piece:&Piece) -> Vec { self.get_alts_data(piece).iter().map(|info| info.play).collect() } pub fn get_drops(&self, piece:&Piece) -> Vec { let mut moves = Vec::new(); for tile_id in 0..self.board.tiles.len() { 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 &= piece.player == target.player; /* ** Target must not be same piece. */ 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 &= (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]; let mut piece = piece.clone(); piece.tile = tile_id; /* ** Tile must not be occupied. */ let mut valid = tile.piece.is_none(); /* ** If in check, a piece may only be dropped onto a tile blocking check. */ 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 &= 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_data(&piece); if (flags & flags::IGNORE_CHECK as u32) == 0 { for mv in piece_moves { if mv.valid && mv.check.is_check() { valid = false; break; } } } valid } pub fn is_complete(&self) -> bool { match self.status { GameStatus::Checkmate | GameStatus::Resign => true, _ => false, } } }