#![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, } 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) -> 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 { let mut plays = Vec::::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 { 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 { let mut plays = Vec::::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 { 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 { 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, } } }