714 lines
24 KiB
Rust
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,
|
|
}
|
|
}
|
|
}
|