From 30eaa81d1b320d17acd6742a9b7c8fc8cc8c58aa Mon Sep 17 00:00:00 2001 From: yukirij Date: Fri, 11 Oct 2024 18:38:12 -0700 Subject: [PATCH] Implement server-side move validation. --- game/src/board/mod.rs | 4 +- game/src/config/mod.rs | 4 + game/src/{consts.rs => constant.rs} | 25 +- game/src/game/checkstate.rs | 68 ++ game/src/game/mod.rs | 173 +---- game/src/history/mod.rs | 36 ++ game/src/lib.rs | 717 ++++++++++++++++++++- game/src/piece/mod.rs | 186 +++--- game/src/util/binary.rs | 30 + game/src/util/hex.rs | 26 +- server/src/app/mod.rs | 8 + server/src/main.rs | 43 +- server/src/manager/data.rs | 37 +- server/src/protocol/code.rs | 1 + server/src/protocol/mod.rs | 2 + server/src/protocol/packet/game_message.rs | 30 +- server/src/protocol/packet/mod.rs | 2 + server/src/protocol/packet/test_result.rs | 28 + www/js/const.js | 2 + www/js/game.js | 56 +- www/js/interface.js | 46 ++ www/js/system.js | 14 + 22 files changed, 1248 insertions(+), 290 deletions(-) create mode 100644 game/src/config/mod.rs rename game/src/{consts.rs => constant.rs} (50%) create mode 100644 game/src/game/checkstate.rs create mode 100644 server/src/protocol/packet/test_result.rs diff --git a/game/src/board/mod.rs b/game/src/board/mod.rs index 769318f..0f982b0 100644 --- a/game/src/board/mod.rs +++ b/game/src/board/mod.rs @@ -1,5 +1,5 @@ use crate::{ - consts::*, + constant::*, piece::*, util::Hex, }; @@ -87,7 +87,7 @@ impl Board { (Piece::new(PIECE_DRAGON, PLAYER_DAWN), Hex::from_hex(4, 2)), (Piece::new(PIECE_BEHEMOTH, PLAYER_DAWN), Hex::from_hex(4, 1)), - (Piece::new(PIECE_SOURCE, PLAYER_DAWN), Hex::from_hex(4, 0)), + (Piece::new(PIECE_HEART, PLAYER_DAWN), Hex::from_hex(4, 0)), ]; for (piece, hex) in &layout { diff --git a/game/src/config/mod.rs b/game/src/config/mod.rs new file mode 100644 index 0000000..905ab7e --- /dev/null +++ b/game/src/config/mod.rs @@ -0,0 +1,4 @@ +#[derive(Clone)] +pub struct GameConfig { + +} diff --git a/game/src/consts.rs b/game/src/constant.rs similarity index 50% rename from game/src/consts.rs rename to game/src/constant.rs index fa3d059..54e37c9 100644 --- a/game/src/consts.rs +++ b/game/src/constant.rs @@ -11,4 +11,27 @@ pub const PIECE_TOWER :u8 = 3; pub const PIECE_CASTLE :u8 = 4; pub const PIECE_DRAGON :u8 = 5; pub const PIECE_BEHEMOTH :u8 = 6; -pub const PIECE_SOURCE :u8 = 7; +pub const PIECE_HEART :u8 = 7; + +pub mod flags { + pub const NONE :u32 = 0x00; + pub const IGNORE_EXTENT :u32 = 0x01; + pub const IGNORE_CHECK :u32 = 0x02; + pub const IGNORE_STACKING :u32 = 0x04; +} + +pub const DIRECTIONS :&[(i8, i8)] = &[ + (0, 1), + (1, 1), + (1, 0), + (0, -1), + (-1, -1), + (-1, 0), + + (1, 2), + (2, 1), + (1, -1), + (-1, -2), + (-2, -1), + (-1, 1), +]; diff --git a/game/src/game/checkstate.rs b/game/src/game/checkstate.rs new file mode 100644 index 0000000..8b80bec --- /dev/null +++ b/game/src/game/checkstate.rs @@ -0,0 +1,68 @@ +#[derive(Clone, Copy)] +pub struct CheckState { + data:u8, +} +impl CheckState { + pub fn new() -> Self + { + Self { + data:0, + } + } + + pub fn immediate(mut self) -> Self + { + self.data |= 0x80; + self + } + + pub fn direct(mut self) -> Self + { + self.data += 1; + self.data |= 0x40; + self + } + + pub fn stride(mut self) -> Self + { + self.data += 1; + self.data |= 0x20; + self + } + + pub fn apply(&mut self, other:Self) + { + self.data += other.data & 0x1F; + self.data |= other.data & 0xC0; + } + + pub fn reset(&mut self) + { + self.data = 0; + } + + pub fn is_check(&self) -> bool + { + self.data != 0 + } + + pub fn is_immediate(&self) -> bool + { + (self.data & 0x80) != 0 + } + + pub fn is_direct(&self) -> bool + { + (self.data & 0x40) != 0 + } + + pub fn is_stride(&self) -> bool + { + (self.data & 0x20) != 0 + } + + pub fn count(&self) -> usize + { + (self.data & 0x1F) as usize + } +} diff --git a/game/src/game/mod.rs b/game/src/game/mod.rs index 9dff684..d86d389 100644 --- a/game/src/game/mod.rs +++ b/game/src/game/mod.rs @@ -1,172 +1 @@ -use crate::{ - //consts::*, - board::Board, - history::Play, - piece::Piece, util::Hex, -}; - -#[derive(Clone, Copy)] -enum GameStatus { - Normal, - Check(u32), - Checkmate, - Resign, -} - -pub type Pool = [u8; 7]; - -#[derive(Clone)] -pub struct Game { - status:GameStatus, - - pub turn:u16, - pub board:Board, - pub pool:[Pool; 2], - - pub history:Vec, -} -impl Game { - pub fn new() -> Self - { - Self { - 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 process(&mut self, play:&Play) -> Result<(),()> - { - let player = (self.turn & 1) as u8; - - match self.status { - GameStatus::Checkmate | GameStatus::Resign => { return Err(()); } - _ => { } - } - - let valid = true; - - // - // TODO: - // - Check for piece promotion. - // - Validate plays against move sets and specific rules. - // - Determine game state (check, checkmate). - // - - if valid { - // 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(()) } - } - - fn get_moves(_piece:&Piece) -> Vec - { - Vec::new() - } - - pub fn is_complete(&self) -> bool - { - match self.status { - GameStatus::Checkmate | GameStatus::Resign => true, - _ => false, - } - } -} +mod checkstate; pub use checkstate::CheckState; diff --git a/game/src/history/mod.rs b/game/src/history/mod.rs index 65885d6..f3157f3 100644 --- a/game/src/history/mod.rs +++ b/game/src/history/mod.rs @@ -1,3 +1,5 @@ +use crate::CheckState; + #[derive(Clone, Copy)] pub struct Play { pub source:u8, @@ -13,4 +15,38 @@ impl Play { to:0, } } + + pub fn from_move(from:u8, to:u8) -> Self + { + Self { + source:0, + from, + to, + } + } + + pub fn from_alt(from:u8, to:u8) -> Self + { + Self { + source:2, + from, + to, + } + } + + pub fn from_drop(piece:u8, tile:u8) -> Self + { + Self { + source:1, + from:piece, + to:tile, + } + } +} + +#[derive(Clone, Copy)] +pub struct PlayInfo { + pub play:Play, + pub check:CheckState, + pub blocking:u32, } diff --git a/game/src/lib.rs b/game/src/lib.rs index f574ce0..728352e 100644 --- a/game/src/lib.rs +++ b/game/src/lib.rs @@ -1,8 +1,713 @@ #![allow(dead_code)] -pub mod consts; -pub mod util; -pub mod piece; -pub mod board; -pub mod history; -pub mod game; pub use game::Game; +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, + } + } +} diff --git a/game/src/piece/mod.rs b/game/src/piece/mod.rs index 65c1349..b98d586 100644 --- a/game/src/piece/mod.rs +++ b/game/src/piece/mod.rs @@ -1,19 +1,59 @@ use crate::{ - consts::*, - util::bit, + constant::*, + util::*, }; #[derive(Clone, Copy)] pub struct MoveSet { pub direction:u32, pub stride:u32, + pub alt:Option, } impl MoveSet { - pub fn rotate(&self) -> Self + pub const fn new() -> Self { Self { direction:0, stride:0, + alt:None, + } + } + + pub const fn add(mut self, bits:u32) -> Self + { + self.direction |= bits; + self + } + + pub const fn add_stride(mut self, bits:u32, mode:u32) -> Self + { + self.direction |= bits; + + let mut bits_set = bits; + while bits_set != 0 { + let mask = lsb(bits_set); + let index = ffs(mask); + + self.stride |= (mode & 3) << (index * 2); + + bits_set &= !mask; + } + + self + } + + pub const fn add_alt(mut self, mode:u32) -> Self + { + self.alt = Some(mode); + self + } + + pub fn rotate(&self) -> Self + { + Self { + direction:rotate6(self.direction), + stride:rotate12(self.stride), + alt:None, } } } @@ -28,119 +68,113 @@ pub struct PieceClass { pub const PIECES :[PieceClass; PIECES_COUNT] = [ PieceClass { name: "Militia", - moves: MoveSet { - direction:bit(0) | bit(1) | bit(5), - stride:0, - }, - pmoves: MoveSet{ - direction:bit(0) | bit(1) | bit(2) | bit(4) | bit(5), - stride:0, - }, + moves: MoveSet::new() + .add(bit(0) | bit(1) | bit(5)), + pmoves: MoveSet::new() + .add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5)), }, PieceClass { name: "Lance", - moves: MoveSet { - direction:1, - stride:0, - }, - pmoves: MoveSet { - direction:1, - stride:0, - }, + moves: MoveSet::new() + .add(bit(1) | bit(5)) + .add_stride(bit(0), 3), + pmoves: MoveSet::new() + .add(bit(0) | bit(3)) + .add_stride(bit(1) | bit(2) | bit(4) | bit(5), 1), }, PieceClass { name: "Knight", - moves: MoveSet { - direction:bit(3) | bit(6) | bit(11) | bit(13) | bit(17), - stride:0, - }, - pmoves: MoveSet{ - direction:0, - stride:0, - }, + moves: MoveSet::new() + .add(bit(6) | bit(8) | bit(9) | bit(11) | bit(13) | bit(14) | bit(16) | bit(17)), + pmoves: MoveSet::new() + .add(bit(6) | bit(8) | bit(9) | bit(11) | bit(13) | bit(14) | bit(16) | bit(17)) + .add_alt(1), }, PieceClass { name: "Tower", - moves: MoveSet { - direction:1, - stride:0, - }, - pmoves: MoveSet { - direction:1, - stride:0, - }, + moves: MoveSet::new() + .add(bit(0) | bit(1) | bit(3) | bit(5) | bit(6) | bit(11)), + pmoves: MoveSet::new() + .add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5) | bit(6) | bit(8) | bit(9) | bit(11)), }, PieceClass { name: "Castle", - moves: MoveSet { - direction:1, - stride:0, - }, - pmoves: MoveSet { - direction:1, - stride:0, - }, + moves: MoveSet::new() + .add(bit(0) | bit(1) | bit(2) | bit(4) | bit(5) | bit(7) | bit(10)), + pmoves: MoveSet::new() + .add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5) | bit(7) | bit(10)) + .add_alt(2), }, PieceClass { name: "Dragon", - moves: MoveSet { - direction:1, - stride:0, - }, - pmoves: MoveSet { - direction:1, - stride:0, - }, + moves: MoveSet::new() + .add_stride(bit(6) | bit(7) | bit(8) | bit(9) | bit(10) | bit(11), 3), + pmoves: MoveSet::new() + .add_stride(bit(6) | bit(7) | bit(8) | bit(9) | bit(10) | bit(11), 3) + .add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5)), }, PieceClass { name: "Behemoth", - moves: MoveSet { - direction:1, - stride:0, - }, - pmoves: MoveSet { - direction:1, - stride:0, - }, + moves: MoveSet::new() + .add_stride(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5), 3), + pmoves: MoveSet::new() + .add_stride(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5), 3) + .add(bit(12) | bit(13) | bit(14) | bit(15) | bit(16) | bit(17)), }, PieceClass { - name: "Source", - moves: MoveSet { - direction:0, - stride:0, - }, - pmoves: MoveSet { - direction:0, - stride:0, - }, + name: "Heart", + moves: MoveSet::new() + .add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5) | bit(7) | bit(10)), + pmoves: MoveSet::new(), }, ]; #[derive(Clone, Copy)] pub struct Piece { pub class:u8, - pub promoted:bool, pub player:u8, + pub promoted:bool, pub tile:u8, + pub blocking:u32, } impl Piece { pub fn new(class:u8, player:u8) -> Self { Self { class, - promoted:false, player, + promoted:false, tile:0, + blocking:0, } } - pub fn new_at(class:u8, player:u8, tile:u8) -> Self + pub fn at(mut self, tile:u8) -> Self { - Self { - class, - promoted:false, - player, - tile, + self.tile = tile; + self + } + + pub fn promote(mut self, promoted:bool) -> Self + { + self.promoted = promoted; + self + } + + pub fn moves(&self) -> MoveSet + { + let class = &crate::piece::PIECES[self.class as usize]; + + let moves = if self.promoted { + class.pmoves + } else { + class.moves + }; + + if self.player == 0 { + moves + } else { + moves.rotate() } } diff --git a/game/src/util/binary.rs b/game/src/util/binary.rs index 8bd2511..a1604e0 100644 --- a/game/src/util/binary.rs +++ b/game/src/util/binary.rs @@ -1,2 +1,32 @@ pub const fn bit(v:u32) -> u32 { 1u32 << v } pub const fn mask(b:u32, s:u32) -> u32 { ((1u32 << b) - 1) << s } + +pub const fn lsb(x:u32) -> u32 { x & x.wrapping_neg() } +pub const fn ffs(x:u32) -> u32 { 31 - x.leading_zeros() } + +pub const fn rotate6(x:u32) -> u32 +{ + const R1 :u32 = 0x0000_003F; + const R2 :u32 = 0x0000_0FC0; + const R3 :u32 = 0x0003_F000; + + let a = (x & R1) << 3; + let b = (x & R2) << 3; + let c = (x & R3) << 3; + + (a & R1) | ((a >> 6) & R1) + | (b & R2) | ((b >> 6) & R2) + | (c & R3) | ((c >> 6) & R3) +} + +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; + + (a & R1) | ((a >> 12) & R1) + | (b & R2) | ((b >> 12) & R2) +} diff --git a/game/src/util/hex.rs b/game/src/util/hex.rs index ca1683f..a99b0ef 100644 --- a/game/src/util/hex.rs +++ b/game/src/util/hex.rs @@ -1,9 +1,9 @@ -const ROWS :[u8; 10] = [ 0, 5, 11, 18, 26, 35, 43, 50, 56, 61 ]; +const ROWS :[i8; 10] = [ 0, 5, 11, 18, 26, 35, 43, 50, 56, 61 ]; #[derive(Clone, Copy)] pub struct Hex { - pub x:u8, - pub y:u8, + pub x:i8, + pub y:i8, pub tile:u8, } impl Hex { @@ -16,7 +16,7 @@ impl Hex { } } - pub fn from_hex(x:u8, y:u8) -> Self + pub fn from_hex(x:i8, y:i8) -> Self { let x = x as i32; let y = y as i32; @@ -24,8 +24,8 @@ impl Hex { let b = (x > 4) as i32 * ((x - 4) + ((x - 5) * (x - 4))); let tile = a - b + y; Self { - x:x as u8, - y:y as u8, + x:x as i8, + y:y as i8, tile:tile as u8, } } @@ -33,16 +33,16 @@ impl Hex { pub fn from_tile(tile:u8) -> Self { let mut x = 0i32; - while tile >= ROWS[x as usize + 1] { x += 1; } - let y = tile - ROWS[x as usize] + ((x > 4) as i32 * (x - 4)) as u8; + while tile as i8 >= ROWS[x as usize + 1] { x += 1; } + let y = tile as i8 - ROWS[x as usize] + ((x > 4) as i32 * (x - 4)) as i8; Self { - x:x as u8, + x:x as i8, y, tile, } } - /*pub fn is_valid(x:i8, y:i8) -> bool + pub fn is_valid(x:i8, y:i8) -> bool { const COLUMNS :[(i8, i8); 9] = [ (0, 4), @@ -61,11 +61,11 @@ impl Hex { let (min, max) = COLUMNS[x as usize]; y >= min && y <= max } else { false } - }*/ + } - pub fn is_back(x:u8, y:u8, player:u8) -> bool + pub fn is_back(x:i8, y:i8, player:u8) -> bool { - const COLUMNS :[[u8; 2]; 9] = [ + const COLUMNS :[[i8; 2]; 9] = [ [0, 4], [0, 5], [0, 6], diff --git a/server/src/app/mod.rs b/server/src/app/mod.rs index f3c5141..dc2bd97 100644 --- a/server/src/app/mod.rs +++ b/server/src/app/mod.rs @@ -217,6 +217,12 @@ impl App { )).await.ok(); } + QRPacketData::TestResult(response) => { + socket.send(Message::Binary( + encode_response(CODE_TEST_RESULT, response.encode()) + )).await.ok(); + } + _ => { } } } @@ -239,6 +245,8 @@ impl App { session.p_dusk.connections.len() > 0, session.connections.len() as u32, ), + test: false, + expected: false, }), )); } diff --git a/server/src/main.rs b/server/src/main.rs index 0b9d10a..f62b241 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -37,6 +37,9 @@ async fn service_http(mut request:hyper::Request, args:Ht println!("Serving: {}", request.uri().path()); + /* + ** Get client language. + */ let mut language_code = 0; if let Some(languages) = request.headers().get(ACCEPT_LANGUAGE) { if let Ok(languages) = languages.to_str() { @@ -55,6 +58,9 @@ async fn service_http(mut request:hyper::Request, args:Ht } } + /* + ** Upgrade to websocket, if reqeusted. + */ if hyper_tungstenite::is_upgrade_request(&request) { if let Ok((response, websocket)) = hyper_tungstenite::upgrade(&mut request, None) { tokio::task::spawn(async move { @@ -71,7 +77,12 @@ async fn service_http(mut request:hyper::Request, args:Ht .body(Full::new(Bytes::new())) .unwrap()) } - } else { + } + + /* + ** Otherwise, serve cached assets. + */ + else { match args.cache.fetch(request.uri().path()) { Some(data) => { let mut output = data.data; @@ -137,16 +148,20 @@ async fn main() { println!("Server Version: {}", config::VERSION); - // Initialize application data. - let app; - if let Ok(result) = App::init() { - app = result; + /* + ** Initialize application data. + */ + let app = if let Ok(result) = App::init() { + result } else { println!("fatal: failed to initialize server."); return; - } + }; - // Initialize central bus and data serivce. + + /* + ** Initialize central bus and data serivce. + */ let b_main :Bus = Bus::new_as(bus::Mode::Transmitter); match b_main.connect() { Ok(bus) => { @@ -160,7 +175,10 @@ async fn main() } } - // Load image assets + + /* + ** Load image assets. + */ let mut js_asset_data = String::from("const GAME_ASSET = { Image: {"); let asset_path = Path::new("www/asset/"); for name in [ @@ -182,9 +200,12 @@ async fn main() } js_asset_data += "} };"; - // Initialize HTTPS service. match b_main.connect() { Ok(bus) => { + + /* + ** Cache source files. + */ let cache = WebCache::new(); cache.cache("text/html", "/.html", &[ WebCache::file("www/.html"), @@ -242,6 +263,10 @@ async fn main() } } + + /* + ** Initialize network services. + */ let mut tcp_server = TcpServer::new(); match tcp_server.bind("127.0.0.1:38611").await { Ok(_) => { diff --git a/server/src/manager/data.rs b/server/src/manager/data.rs index cd1f0cf..64edcfd 100644 --- a/server/src/manager/data.rs +++ b/server/src/manager/data.rs @@ -566,6 +566,8 @@ pub async fn thread_system(mut app:App, bus:Bus) cid, QRPacketData::GameMessage(PacketGameMessage { data: GameMessageData::Resign, + test: false, + expected: false, }), )); } @@ -628,6 +630,7 @@ pub async fn thread_system(mut app:App, bus:Bus) println!("Request: Game Message"); let mut packets = Vec::::new(); + let mut response = QRPacketData::None; if let Some(sid) = session_id { if let Some(session) = app.sessions.get_mut(&sid) { @@ -637,7 +640,37 @@ pub async fn thread_system(mut app:App, bus:Bus) | GameMessageData::PlayDrop(turn, from, to) | GameMessageData::PlayAlt(turn, from, to) => { - if !session.game.is_complete() { + if request.test { + let play = Play { + source: match request.data { + GameMessageData::PlayMove(..) => 0, + GameMessageData::PlayDrop(..) => 1, + GameMessageData::PlayAlt(..) => 2, + _ => 0, + }, + from, to, + }; + + let text = format!("PLAY {} {} {}", play.source, play.from, play.to); + let result = if session.game.play_is_valid(&play) { + response = QRPacketData::TestResult(PacketTestResult::new( + request.expected, + &text, + )); + request.expected + } else { + response = QRPacketData::TestResult(PacketTestResult::new( + !request.expected, + &text, + )); + !request.expected + }; + if result { + println!("OK {} {}", request.expected, text); + } else { + println!("NO {} {}", request.expected, text); + } + } else if !session.game.is_complete() { if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0) || (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) { if turn == session.game.turn { @@ -780,7 +813,7 @@ pub async fn thread_system(mut app:App, bus:Bus) } // Updates already sent; nothing to do here. - Some(QRPacket::new(qr.id, QRPacketData::None)) + Some(QRPacket::new(qr.id, response)) } } } diff --git a/server/src/protocol/code.rs b/server/src/protocol/code.rs index 1b01df5..238c114 100644 --- a/server/src/protocol/code.rs +++ b/server/src/protocol/code.rs @@ -48,6 +48,7 @@ pub const CODE_USER_LIST :u16 = 0x0100; //pub const CODE_USER_AWAIT_GET :u16 = 0x0110; //pub const CODE_USER_AWAIT_SET :u16 = 0x0111; +pub const CODE_TEST_RESULT :u16 = 0xFFFF; /* ** Game Messages diff --git a/server/src/protocol/mod.rs b/server/src/protocol/mod.rs index 4a84150..76ea29c 100644 --- a/server/src/protocol/mod.rs +++ b/server/src/protocol/mod.rs @@ -51,6 +51,8 @@ pub enum QRPacketData { QChallengeList, RChallengeList(PacketChallengeListResponse), + + TestResult(PacketTestResult), } #[derive(Clone)] diff --git a/server/src/protocol/packet/game_message.rs b/server/src/protocol/packet/game_message.rs index 002c32d..94f96ed 100644 --- a/server/src/protocol/packet/game_message.rs +++ b/server/src/protocol/packet/game_message.rs @@ -27,18 +27,26 @@ pub enum GameMessageData { #[derive(Clone)] pub struct PacketGameMessage { pub data:GameMessageData, + pub test:bool, + pub expected:bool, } impl PacketGameMessage { pub fn new() -> Self { Self { data:GameMessageData::Error, + test:false, + expected:false, } } pub fn with(data:GameMessageData) -> Self { - Self { data } + Self { + data, + test:false, + expected:false, + } } } impl Packet for PacketGameMessage { @@ -79,15 +87,20 @@ impl Packet for PacketGameMessage { ), _ => GameMessageData::Error, - } - } + }; - Ok(result) + result.test = (data & (1<<63)) != 0; + result.expected = (data & (1<<62)) != 0; + + Ok(result) + } else { + Err(()) + } } fn encode(&self) -> Vec { - pack_u64(match self.data { + let mut output = match self.data { GameMessageData::PlayMove(turn, from, to) => { GMSG_PLAY_MOVE as u64 | ((turn as u64) << 8) @@ -129,6 +142,11 @@ impl Packet for PacketGameMessage { } _ => { 0 } - }) + }; + + output |= (self.test as u64) << 63; + output |= (self.expected as u64) << 62; + + pack_u64(output) } } diff --git a/server/src/protocol/packet/mod.rs b/server/src/protocol/packet/mod.rs index 50ea1fa..be588a8 100644 --- a/server/src/protocol/packet/mod.rs +++ b/server/src/protocol/packet/mod.rs @@ -22,6 +22,8 @@ mod challenge_list; pub use challenge_list::*; mod user_list; pub use user_list::*; +mod test_result; pub use test_result::*; + mod prelude { pub trait Packet { type Data; diff --git a/server/src/protocol/packet/test_result.rs b/server/src/protocol/packet/test_result.rs new file mode 100644 index 0000000..1105160 --- /dev/null +++ b/server/src/protocol/packet/test_result.rs @@ -0,0 +1,28 @@ +use crate::util::pack::{pack_u8, pack_u16}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketTestResult { + pub result:bool, + pub text:String, +} +impl PacketTestResult { + pub fn new(result:bool, text:&str) -> Self + { + Self { result, text:text.to_string() } + } +} +impl Packet for PacketTestResult { + type Data = Self; + + fn encode(&self) -> Vec + { + let bytes = self.text.as_bytes().to_vec(); + [ + pack_u8(self.result as u8), + pack_u16(bytes.len() as u16), + bytes, + ].concat() + } +} diff --git a/www/js/const.js b/www/js/const.js index 858108b..da28d8f 100644 --- a/www/js/const.js +++ b/www/js/const.js @@ -54,6 +54,8 @@ const OpCode = { ChallengeList :0x0062, UserList :0x0100, + + TestResult :0xFFFF, }; const GameState = { diff --git a/www/js/game.js b/www/js/game.js index ba3ffa0..670a95a 100644 --- a/www/js/game.js +++ b/www/js/game.js @@ -419,6 +419,56 @@ GAME.Game = class { this.update_board(); } + play_is_valid(play) { + let player = this.turn & 1; + + switch(play.source) { + case 0: { + let piece_id = this.board.tiles[play.from].piece; + if(piece_id !== null) { + let piece = this.board.pieces[piece_id]; + if(piece.player == player) { + let moves = this.movement_tiles(piece, play.from); + for(let move of moves) { + if(move.tile == play.to && move.valid) { + return true; + } + } + } + } + } break; + + case 1: { + if(play.from >= 0 && play.from <= 6) { + if(this.pools[player].pieces[play.from] > 0) { + let moves = this.placement_tiles(play.from, player); + for(let move of moves) { + if(move.tile == play.to && move.valid) { + return true; + } + } + } + } + } break; + + case 2: { + let piece_id = this.board.tiles[play.from].piece; + if(piece_id !== null) { + let piece = this.board.pieces[piece_id]; + if(piece.player == player) { + let moves = this.movement_tiles_alt(piece); + for(let move of moves) { + if(move.tile == play.to && move.valid) { + return true; + } + } + } + } + } break; + } + return false; + } + movement_tiles(piece, tile) { let tiles = [ ]; let moves = piece.moves(); @@ -449,7 +499,7 @@ GAME.Game = class { let max_dist = 1; switch(stride) { case 1: max_dist = 2; break; - case 3: max_dist = 9; break; + case 3: max_dist = 8; break; } for(let dist = 1; dist <= max_dist; ++dist) { move_hex.add(direction); @@ -735,9 +785,9 @@ GAME.Game = class { // Check off-sides. if(params.extent !== false) { if(piece.player == 0) { - position_valid = position_valid && (hex.y <= this.board.columns[hex.x].extent[+(!piece.player)]); + position_valid = position_valid && (hex.y <= this.board.columns[hex.x].extent[1]); } else { - position_valid = position_valid && (hex.y >= this.board.columns[hex.x].extent[+(!piece.player)]); + position_valid = position_valid && (hex.y >= this.board.columns[hex.x].extent[0]); } } diff --git a/www/js/interface.js b/www/js/interface.js index 8f678fb..633ee09 100644 --- a/www/js/interface.js +++ b/www/js/interface.js @@ -214,6 +214,7 @@ const INTERFACE = { click(event) { console.log("CLICK"); switch(event.button) { + // Main button case 0: { console.log("A"); @@ -1829,6 +1830,51 @@ const INTERFACE = { setTimeout(INTERFACE.reaction_generate, 50); } } + }, + + run_test(source=0, from=0, to=0, all=true) + { + let next_source = source; + let next_from = from; + let next_to = to + 1; + + if(next_to == 61) { + next_to = 0; + next_from += 1; + } + if(next_from == 61 || (next_source == 1 && next_from == 7)) { + next_from = 0; + next_source += 1; + } + + let msg = GAME_DATA.turn | (from << 16) | (to << 22); + + let high = msg >> 24; + let low = (msg << 8) & 0xFFFF_FFFF; + + switch(source) { + case 0: low |= GameMessage.Move; break; + case 1: low |= GameMessage.Drop; break; + case 2: low |= GameMessage.Alt; break; + } + + // Mark test and expected value + let expect = GAME_DATA.play_is_valid(new GAME.Play(source, from, to)); + + high |= 1 << 31; + high |= (+expect) << 30; + + console.log("Test Expect: " + expect); + + MESSAGE_COMPOSE([ + PACK.u16(OpCode.GameMessage), + PACK.u32(high), + PACK.u32(low), + ]); + + if(all && next_source < 3) { + setTimeout(INTERFACE.run_test, 100, next_source, next_from, next_to); + } } }; diff --git a/www/js/system.js b/www/js/system.js index e49fc76..09bcdaf 100644 --- a/www/js/system.js +++ b/www/js/system.js @@ -470,6 +470,20 @@ function MESSAGE(event) { } } break; + case OpCode.TestResult: { + result = UNPACK.u8(bytes, index); + index = result.index; + let test_result = result.data; + + result = UNPACK.string(bytes, index, UNPACK.u16); + index = result.index; + let text = result.data; + + if(test_result == 0) { + console.log("BD " + text); + } + } break; + default: console.log("RECV Undefined " + code); return;