From 36cc9f7543c7bb63e45c68d7e4981ec128e2d81f Mon Sep 17 00:00:00 2001 From: yukirij Date: Sat, 12 Oct 2024 11:18:58 -0700 Subject: [PATCH] Fix bugs in server-side move validation. --- game/src/game/checkstate.rs | 2 +- game/src/history/mod.rs | 2 + game/src/lib.rs | 378 +++++++++++++++++++++--------------- game/src/piece/mod.rs | 4 +- game/src/util/binary.rs | 7 +- www/js/game.js | 12 +- 6 files changed, 234 insertions(+), 171 deletions(-) diff --git a/game/src/game/checkstate.rs b/game/src/game/checkstate.rs index 8b80bec..373bd93 100644 --- a/game/src/game/checkstate.rs +++ b/game/src/game/checkstate.rs @@ -12,7 +12,7 @@ impl CheckState { pub fn immediate(mut self) -> Self { - self.data |= 0x80; + self.data |= 0x80 * (self.data > 0) as u8; self } diff --git a/game/src/history/mod.rs b/game/src/history/mod.rs index f3157f3..7b1c8fa 100644 --- a/game/src/history/mod.rs +++ b/game/src/history/mod.rs @@ -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, diff --git a/game/src/lib.rs b/game/src/lib.rs index 47dcbc7..f49e924 100644 --- a/game/src/lib.rs +++ b/game/src/lib.rs @@ -50,21 +50,21 @@ impl Game { { *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 } + 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 + { + 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::::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 { - let mut plays = Vec::::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 { - 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 @@ -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; } } } diff --git a/game/src/piece/mod.rs b/game/src/piece/mod.rs index b98d586..ef5979b 100644 --- a/game/src/piece/mod.rs +++ b/game/src/piece/mod.rs @@ -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)) diff --git a/game/src/util/binary.rs b/game/src/util/binary.rs index a1604e0..44d3e33 100644 --- a/game/src/util/binary.rs +++ b/game/src/util/binary.rs @@ -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) diff --git a/www/js/game.js b/www/js/game.js index 670a95a..132bd5e 100644 --- a/www/js/game.js +++ b/www/js/game.js @@ -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; }