Fix bugs in server-side move validation.

This commit is contained in:
yukirij 2024-10-12 11:18:58 -07:00
parent d0bad35995
commit 36cc9f7543
6 changed files with 234 additions and 171 deletions

View File

@ -12,7 +12,7 @@ impl CheckState {
pub fn immediate(mut self) -> Self
{
self.data |= 0x80;
self.data |= 0x80 * (self.data > 0) as u8;
self
}

View File

@ -46,6 +46,8 @@ impl Play {
#[derive(Clone, Copy)]
pub struct PlayInfo {
pub valid:bool,
pub threat:bool,
pub play:Play,
pub check:CheckState,
pub blocking:u32,

View File

@ -50,21 +50,21 @@ impl Game {
{
*self = Self::new();
self.board.init();
self.update_board();
}
pub fn apply_history(&mut self, history:&Vec<Play>) -> Result<(),()>
{
self.init();
for play in history {
if self.process(play).is_err() { break }
if self.process(play).is_err() { break; }
}
Ok(())
}
pub fn update_board(&mut self)
{
let mut player_moves = 0usize;
let current_player = (self.turn & 1) as u8;
if self.is_complete() { return; }
/*
** Reset board meta data.
@ -73,7 +73,7 @@ impl Game {
// Reset columns
for column in &mut self.board.columns {
column.extent = [0, 9];
column.extent = [0, 8];
column.militia = [false; 2];
}
@ -95,65 +95,64 @@ impl Game {
*/
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);
let piece_hex = Hex::from_tile(piece.tile);
/*
** Mark threats, checks, and extents.
** Apply column extent.
*/
for info in &moves {
let hex = Hex::from_tile(info.play.to);
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.
*/
self.board.tiles[info.play.to as usize].threat[piece.player as usize] = true;
if info.valid || info.threat {
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);
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 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;
}
// 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;
/*
** 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;
}
}
}
}
@ -164,21 +163,48 @@ impl Game {
/*
** Determine if game is in checkmate.
*/
if player_moves == 0 {
if self.get_plays().len() == 0 {
self.status = GameStatus::Checkmate;
}
}
pub fn get_plays(&mut self) -> Vec<Play>
{
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 {
plays.append(&mut self.get_drops(&Piece::new(i, current_player)));
}
plays
}
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 {
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] {
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;
if let Some(tid) = self.board.tiles[play.to as usize].piece {
@ -206,54 +232,51 @@ impl Game {
if !swap { self.board.pieces[tid as usize] = None; }
}
// Set tile/piece associations.
/*
** 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(pid);
self.board.tiles[play.to as usize].piece = Some(piece_id);
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;
}
piece.promoted = true;
}
self.board.pieces[piece_id as usize] = Some(piece);
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.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 }
self.turn += 1;
}
// Player retired.
0xF => {
self.status = GameStatus::Resign;
true
}
_ => false,
} {
self.history.push(*play);
Ok(())
} else { Err(()) }
_ => { }
}
self.history.push(*play);
self.update_board();
Ok(())
} else { Err(()) }
}
@ -261,7 +284,7 @@ impl Game {
// Returns whether a play may be made by the current player.
//
{
//println!("play_is_valid {} {} {}", play.source, play.from, play.to);
if self.is_complete() { return false; }
let mut valid = false;
let player = (self.turn & 1) as u8;
@ -270,13 +293,13 @@ impl Game {
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;
for p in self.get_moves_data(piece) {
if p.play.to == play.to {
if p.valid {
valid = true;
break;
}
}
}
}
@ -284,6 +307,7 @@ impl Game {
}
valid
}
1 => {
if (play.from as usize) < self.pool[player as usize].len() {
if self.pool[player as usize][play.from as usize] > 0 {
@ -297,11 +321,13 @@ impl Game {
}
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) {
let plays = self.get_alts(piece);
for p in plays {
if p.to == play.to {
valid = true;
break;
@ -312,7 +338,8 @@ impl Game {
}
valid
}
0xF => { true }
0xF => true,
_ => false,
}
}
@ -321,13 +348,16 @@ impl Game {
{
let mut plays = Vec::<PlayInfo>::new();
let current_player = (self.turn & 1) as u8;
let moves = piece.moves();
/*
** Get permitted move directions.
*/
let mut directions = moves.direction;
let blocking_directions = piece.blocking | util::rotate6(piece.blocking);
let mut blocking_directions = piece.blocking;
blocking_directions |= blocking_directions << 12;
if blocking_directions != 0 {
if piece.class == PIECE_HEART {
directions &= !blocking_directions;
@ -336,30 +366,24 @@ impl Game {
}
}
//println!("dir {:b}", directions);
//println!("blk {:b}", 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;
let mut mask = mask_original;
/*
** 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);
//println!(" - dir {}", direction);
/*
** Extract stride value:
** 00 - 1 Tile
@ -368,7 +392,7 @@ impl Game {
** 11 - Unlimited
*/
let stride_offset: u32 = direction * 2;
let stride = if direction < 12 {
let stride = if direction_original < 12 {
match ((3 << stride_offset) & moves.stride) >> stride_offset {
0 => 1,
1 => 2,
@ -380,35 +404,39 @@ impl Game {
rx *= multiplier;
ry *= multiplier;
let mut block_count = 0;
/*
** 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 mut valid = block_count == 0;
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 in check, move must break check.
** If player 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];
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 piece.player == current_player {
valid &= match self.status {
GameStatus::Check(state) => {
state.count() == 1 && tile.check
}
GameStatus::Normal => true,
_ => false,
};
}
}
/*
@ -416,35 +444,60 @@ impl Game {
*/
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;
} else {
}
/*
** Target is opposing army.
*/
else {
/*
** Handle capturing of king.
** Find checking of king and blocking pieces.
*/
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 {
/*
** 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;
plays[index].blocking = mask | util::rotate6(mask);
}
_ => { }
}
@ -455,13 +508,13 @@ impl Game {
}
}
if valid {
plays.push(PlayInfo {
play:Play { source:0, from:piece.tile, to:current_hex.tile, },
check:checkstate,
blocking:0,
});
}
plays.push(PlayInfo {
valid,
threat,
play:Play { source:0, from:piece.tile, to:current_hex.tile, },
check:checkstate.immediate(),
blocking:blocking,
});
}
}
@ -475,15 +528,20 @@ impl Game {
{
let mut plays = Vec::new();
for info in self.get_moves_data(piece) {
plays.push(info.play);
if info.valid {
plays.push(info.play);
}
}
plays
}
fn get_alts_data(&self, piece:&Piece) -> Vec<PlayInfo>
{
let mut plays = Vec::<PlayInfo>::new();
let mut plays = Vec::new();
/*
** Get allowed target tiles.
*/
let piece_moves = piece.moves();
let subject_tiles = if piece.blocking == 0 {
(0..61u8).collect()
@ -514,7 +572,9 @@ impl Game {
tiles
};
/*
** Filter valid tiles from allowed.
*/
if let Some(alt_mode) = piece_moves.alt {
match alt_mode {
// Knight
@ -522,6 +582,8 @@ impl Game {
for tile_id in subject_tiles {
if self.can_drop(piece, tile_id, flags::IGNORE_CHECK) {
plays.push(PlayInfo {
valid: true,
threat: false,
play: Play::from_alt(piece.tile, tile_id),
check: CheckState::new(),
blocking: 0,
@ -533,25 +595,27 @@ impl Game {
// Caslte
2 => {
for tile_id in subject_tiles {
let hex = Hex::from_tile(piece.tile);
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 >= hex.x {
tile_hex.y <= hex.y
if tile_hex.x >= piece_hex.x {
tile_hex.y <= piece_hex.y
} else {
tile_hex.y <= hex.y - (hex.x - tile_hex.x)
tile_hex.y <= piece_hex.y - (piece_hex.x - tile_hex.x)
}
} else {
if tile_hex.x >= hex.x {
tile_hex.y >= hex.y + (tile_hex.x - hex.x)
if tile_hex.x >= piece_hex.x {
tile_hex.y >= piece_hex.y + (tile_hex.x - piece_hex.x)
} else {
tile_hex.y >= hex.y
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,
@ -569,11 +633,7 @@ impl Game {
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
self.get_alts_data(piece).iter().map(|info| info.play).collect()
}
pub fn get_drops(&self, piece:&Piece) -> Vec<Play>
@ -601,12 +661,12 @@ impl Game {
/*
** Target must be same color.
*/
valid = valid && piece.player == target.player;
valid &= piece.player == target.player;
/*
** Target must not be same piece.
*/
valid = valid && !(piece.class == target.class && piece.promoted == target.promoted);
valid &= !(piece.class == target.class && piece.promoted == target.promoted);
/*
** King may not swap onto a contested tile.
@ -621,7 +681,7 @@ impl Game {
** Target must have movement in reverse direction.
*/
let moves = target.moves().rotate().direction;
valid = valid && (mask & moves) != 0;
valid &= (mask & moves) != 0;
}
}
valid
@ -632,16 +692,18 @@ impl Game {
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 if check
** is due to a multi-tile move from a single piece.
** If in check, a piece may only be dropped onto a tile blocking check.
*/
valid = valid && match self.status {
valid &= match self.status {
GameStatus::Check(state) => {
!state.is_direct() && state.count() == 1 && tile.check
}
@ -653,10 +715,10 @@ impl Game {
** 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
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
hex.y >= self.board.columns[hex.x as usize].extent[0] as i8
};
}
@ -675,17 +737,13 @@ impl Game {
** 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);
let piece_moves = self.get_moves_data(&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;
}
}
if mv.valid && mv.check.is_check() {
valid = false;
break;
}
}
}

View File

@ -53,7 +53,7 @@ impl MoveSet {
Self {
direction:rotate6(self.direction),
stride:rotate12(self.stride),
alt:None,
alt:self.alt,
}
}
}
@ -76,7 +76,7 @@ pub const PIECES :[PieceClass; PIECES_COUNT] = [
PieceClass {
name: "Lance",
moves: MoveSet::new()
.add(bit(1) | bit(5))
.add(bit(1) | bit(3) | bit(5))
.add_stride(bit(0), 3),
pmoves: MoveSet::new()
.add(bit(0) | bit(3))

View File

@ -9,14 +9,17 @@ pub const fn rotate6(x:u32) -> u32
const R1 :u32 = 0x0000_003F;
const R2 :u32 = 0x0000_0FC0;
const R3 :u32 = 0x0003_F000;
const R4 :u32 = 0x00FC_0000;
let a = (x & R1) << 3;
let b = (x & R2) << 3;
let c = (x & R3) << 3;
let d = (x & R4) << 3;
(a & R1) | ((a >> 6) & R1)
| (b & R2) | ((b >> 6) & R2)
| (c & R3) | ((c >> 6) & R3)
| (d & R4) | ((d >> 6) & R4)
}
pub const fn rotate12(x:u32) -> u32
@ -24,8 +27,8 @@ pub const fn rotate12(x:u32) -> u32
const R1 :u32 = 0x0000_0FFF;
const R2 :u32 = 0x00FF_F000;
let a = (x & R1) << 3;
let b = (x & R2) << 3;
let a = (x & R1) << 6;
let b = (x & R2) << 6;
(a & R1) | ((a >> 12) & R1)
| (b & R2) | ((b >> 12) & R2)

View File

@ -122,7 +122,7 @@ GAME.Tile = class {
this.piece = null;
this.threaten = [0, 0];
this.checking = 0;
this.checking = false;
this.hex = HEX.tile_to_hex(index);
}
@ -136,7 +136,7 @@ GAME.Tile = class {
reset() {
this.threaten = [0, 0];
this.checking = 0;
this.checking = false;
}
};
@ -294,7 +294,7 @@ GAME.Game = class {
}
if(movement.valid && movement.check != 0) {
is_checking = true;
this.board.tiles[movement.tile].checking |= movement.check;
this.board.tiles[movement.tile].checking = true;
this.state.check |= movement.check;
}
if(movement.block != 0) {
@ -303,7 +303,7 @@ GAME.Game = class {
}
if(is_checking) {
this.board.tiles[piece.tile].checking = 1;
this.board.tiles[piece.tile].checking = true;
checking_pieces++;
}
}
@ -522,7 +522,7 @@ GAME.Game = class {
let check_count = this.state.check & 0x3F;
if(piece.player == (this.turn & 1) && this.state.check != 0) {
if(piece.piece != GAME.Const.PieceId.Heart) {
if(tile_data.checking != 0) {
if(tile_data.checking) {
if(target_id !== null) {
if(check_count > 1) {
result = false;
@ -776,7 +776,7 @@ GAME.Game = class {
let check_count = this.state.check & 0x3F;
if(piece.player == (this.turn & 1) && this.state.check != 0) {
if(check_direct == 0 && check_count == 1) {
position_valid = tile.checking != 0;
position_valid = tile.checking;
} else {
position_valid = false;
}