From 24e5e9d6093ab1e4f628972cd04ae44a2a4b3911 Mon Sep 17 00:00:00 2001 From: yukirij Date: Tue, 3 Sep 2024 17:21:56 -0700 Subject: [PATCH] Add alt moves for Castle and Knight; change Lance promotion moves to two tiles on sides. --- www/js/game.js | 327 +++++++++++++++++++++++++------------------- www/js/interface.js | 166 ++++++++++++++++------ 2 files changed, 310 insertions(+), 183 deletions(-) diff --git a/www/js/game.js b/www/js/game.js index 5918aae..a08dd6d 100644 --- a/www/js/game.js +++ b/www/js/game.js @@ -151,10 +151,11 @@ GAME.MovementTile = class { }; GAME.Play = class { - constructor(source, from, to) { + constructor(source, from, to, alt=false) { this.source = source; this.from = from; this.to = to; + this.alt = alt; } }; @@ -170,6 +171,7 @@ GAME.PieceMovement = class { constructor() { this.direction = 0; this.stride = 0; + this.alt = false; } add(direction) { @@ -177,9 +179,14 @@ GAME.PieceMovement = class { return this; } - add_stride(direction) { + add_stride(direction, mode=3) { this.direction |= 1 << direction; - this.stride |= 1 << direction; + this.stride |= mode << (direction * 2); + return this; + } + + add_alt() { + this.alt = true; return this; } @@ -187,6 +194,7 @@ GAME.PieceMovement = class { let copy = new GAME.PieceMovement(); copy.direction = BITWISE.rotate_blocks(this.direction); copy.stride = BITWISE.rotate_blocks(this.stride); + copy.alt = this.alt; return copy; } }; @@ -314,6 +322,10 @@ GAME.Game = class { for(let move of this.movement_tiles(piece, piece.tile)) { if(move.valid) { moves += 1; } } + + for(let move of this.movement_tiles_alt(piece, piece.tile)) { + if(move.valid) { moves += 1; } + } } } @@ -329,17 +341,6 @@ GAME.Game = class { } process(play) { - // Check if swapped piece. - // Check if piece should be promoted. - // Check if swapped piece should be promoted. - // Add piece to pool if taken. - // Move pieces. - - // TODO - // - Validate move. - // - Improve data safety validation. - // - if(this.state.code != 0) { return false; } let player = this.turn & 1; @@ -384,6 +385,16 @@ GAME.Game = class { if(!piece.promoted && piece.has_promotion() && !HEX.is_valid_board(hex)) { piece.promoted = true; } + + // Handle alt moves. + if(play.alt) { + switch(piece.piece) { + case GAME.Const.PieceId.Knight: { + piece.promoted = false; + } break; + } + } + this.turn++; } break; @@ -408,7 +419,7 @@ GAME.Game = class { this.update_board(); } - movement_tiles(piece, tile, check_drop=false) { + movement_tiles(piece, tile) { let tiles = [ ]; let moves = piece.moves(); let hex = this.board.tiles[tile].hex; @@ -420,7 +431,10 @@ GAME.Game = class { for(let mask = BITWISE.lsb(directions); directions > 0; mask = BITWISE.lsb(directions)) { let direction_id = BITWISE.ffs(mask); let direction = GAME.Const.get_direction(direction_id); - let stride = (moves.stride & mask) != 0; + let stride = 0; + if(direction_id < 12) { + stride = (moves.stride & (3 << (direction_id * 2))) >> (direction_id * 2); + } let dir_mask = mask; if((dir_mask & 0xFFF) == 0) { dir_mask >>= 12; } @@ -432,7 +446,11 @@ GAME.Game = class { let move_hex = hex.copy(); // Check tiles in direction up to movement limit. - let max_dist = (stride)? 8 : 1; + let max_dist = 1; + switch(stride) { + case 1: max_dist = 2; break; + case 3: max_dist = 9; break; + } for(let dist = 1; dist <= max_dist; ++dist) { move_hex.add(direction); @@ -539,73 +557,6 @@ GAME.Game = class { directions &= ~mask; } - // Handle drop rules for promoted castle - if(!check_drop && piece.piece == GAME.Const.PieceId.Castle && piece.promoted) { - let move_hex = hex.copy(); - let mask_back = 1 << 3; - if(piece.player == GAME.Const.Player.Dusk) { - mask_back = BITWISE.rotate_blocks(mask_back); - } - let mask_column = mask_back | BITWISE.rotate_blocks(mask_back); - - let direction = GAME.Const.get_direction(BITWISE.ffs(mask_back)); - move_hex.add(direction); - - let status = (block_directions == 0 || (mask_back & block_directions) != 0); - - // Check if backward tile meets placement rules. - for(let i = 0; i < 9; ++i) { - move_hex.add(direction); - - if(HEX.is_valid_board(move_hex)) { - let valid = status; - - let tile_id = HEX.hex_to_tile(move_hex); - let tile_data = this.board.tiles[tile_id]; - let target_id = tile_data.piece; - - // Check tile is empty. - if(target_id !== null) { - valid = false; - - // Prevent change in blocking status - if((mask_column & block_directions) != 0) { - status = false; - } - } - - // Prevent placement that does not uncheck King. - let check_direct = (this.state.check & GAME.Const.Check.Direct) != 0; - let check_count = this.state.check & 0x3F; - if(piece.player == (this.turn & 1) && this.state.check != 0) { - if(check_direct == 0 && check_count == 1) { - valid = valid && tile.checking != 0; - } else { - valid = false; - } - } - - // Check off-sides. - if(piece.player == 0) { - valid = valid && (move_hex.y <= this.board.columns[move_hex.x].extent[+(!piece.player)]); - } else { - valid = valid && (move_hex.y >= this.board.columns[move_hex.x].extent[+(!piece.player)]); - } - - // Check if position puts king in check. - let movements = this.movement_tiles(piece, tile_id, true); - for(let movement of movements) { - if(movement.check) { - valid = false; - break; - } - } - - tiles.push(new GAME.MovementTile(tile_id, valid, false, 0, 0)); - } else { break; } - } - } - return tiles; } @@ -626,64 +577,162 @@ GAME.Game = class { return ((moves.direction & mask) != 0 && (range == 1 || (moves.stride & mask) != 0)); } + movement_tiles_alt(piece) { + let tiles = [ ]; + + if(piece.promoted) { + let hex = this.board.tiles[piece.tile].hex; + let block_directions = piece.blocking | BITWISE.rotate_blocks(piece.blocking); + + switch(piece.piece) { + case GAME.Const.PieceId.Knight: { + + // Check all tiles if not blocking. + if(block_directions == 0) { + for(let i = 0; i < GAME_DATA.board.tiles.length; ++i) { + if(this.placable_tile(piece, i)) { + tiles.push(new GAME.MovementTile(i, true, false, 0, 0)); + } + } + } + + // Check tiles in blocking directions if blocking. + else { + let directions = block_directions; + for(let mask = BITWISE.lsb(directions); directions > 0; mask = BITWISE.lsb(directions)) { + let direction_id = BITWISE.ffs(mask); + let direction = GAME.Const.get_direction(direction_id); + + let move_hex = hex.copy(); + + for(let dist = 1; dist <= 9; ++dist) { + move_hex.add(direction); + + if(HEX.is_valid_board(move_hex)) { + let tile_id = HEX.hex_to_tile(move_hex); + let tile_data = this.board.tiles[tile_id]; + let target_id = tile_data.piece; + + if(target_id !== null) { break; } + + if(this.placable_tile(piece, tile_id)) { + tiles.push(new GAME.MovementTile(tile_id, true, false, 0, 0)); + } + } else { break; } + } + + directions &= ~mask; + } + } + } break; + + case GAME.Const.PieceId.Castle: { + let mask_back = 1 << 3; + if(piece.player == GAME.Const.Player.Dusk) { + mask_back = BITWISE.rotate_blocks(mask_back); + } + let mask_column = mask_back | BITWISE.rotate_blocks(mask_back); + + let direction = GAME.Const.get_direction(BITWISE.ffs(mask_back)); + let move_hex = hex.copy(); + move_hex.add(direction); + + let status = (block_directions == 0 || (mask_back & block_directions) != 0); + + // Check if backward tile meets placement rules. + for(let i = 0; i < 9; ++i) { + move_hex.add(direction); + + if(HEX.is_valid_board(move_hex)) { + let tile_id = HEX.hex_to_tile(move_hex); + let tile_data = this.board.tiles[tile_id]; + let target_id = tile_data.piece; + + let valid = status && this.placable_tile(piece, tile_id); + + if(target_id !== null) { + if((mask_column & block_directions) != 0) { + status = false; + } + } + + if(valid) { + tiles.push(new GAME.MovementTile(tile_id, true, false, 0, 0)); + } + } else { break; } + } + } break; + } + } + + return tiles; + } + placement_tiles(piece_id, player) { let tiles = [ ]; let piece = new GAME.Piece(piece_id, player); // Get tiles onto which piece may be placed. for(let i = 0; i < this.board.tiles.length; ++i) { - let hex = HEX.tile_to_hex(i); - let tile = this.board.tiles[i]; - let valid = false; - - // Check if tile is occupied. - if(tile.piece === null) { - let position_valid = true; - - // Prevent placement that does not uncheck King. - let check_direct = (this.state.check & GAME.Const.Check.Direct) != 0; - let check_count = this.state.check & 0x3F; - if(player == (this.turn & 1) && this.state.check != 0) { - if(check_direct == 0 && check_count == 1) { - position_valid = tile.checking != 0; - } else { - position_valid = false; - } - } - - // Check off-sides. - if(piece.player == 0) { - position_valid = position_valid && (hex.y <= this.board.columns[hex.x].extent[+(!player)]); - } else { - position_valid = position_valid && (hex.y >= this.board.columns[hex.x].extent[+(!player)]); - } - - // Check militia stacking. - if(piece_id == GAME.Const.PieceId.Militia && this.board.columns[hex.x].militia[player]) { - position_valid = false; - } - - // Check if position puts king in check. - let checking = false; - let movements = this.movement_tiles(piece, i, true); - for(let movement of movements) { - if(movement.check) { - checking = true; - break; - } - } - - // Piece must have movements and not put king in check. - if(position_valid && movements.length > 0 && !checking) { - valid = true; - } + if(this.placable_tile(piece, i)) { + tiles.push(new GAME.MovementTile(i, true, false, false)); } - - tiles.push(new GAME.MovementTile(i, valid, false, false)); } return tiles; } + + placable_tile(piece, tile_id) { + let valid = false; + + let hex = HEX.tile_to_hex(tile_id); + let tile = this.board.tiles[tile_id]; + + // Check if tile is occupied. + if(tile.piece === null) { + let position_valid = true; + + // Prevent placement that does not uncheck King. + let check_direct = (this.state.check & GAME.Const.Check.Direct) != 0; + 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; + } else { + position_valid = false; + } + } + + // Check off-sides. + if(piece.player == 0) { + position_valid = position_valid && (hex.y <= this.board.columns[hex.x].extent[+(!piece.player)]); + } else { + position_valid = position_valid && (hex.y >= this.board.columns[hex.x].extent[+(!piece.player)]); + } + + // Check militia stacking. + if(piece.piece == GAME.Const.PieceId.Militia && this.board.columns[hex.x].militia[piece.player]) { + position_valid = false; + } + + // Check if position puts king in check. + let checking = false; + let movements = this.movement_tiles(piece, tile_id, true); + for(let movement of movements) { + if(movement.check) { + checking = true; + break; + } + } + + // Piece must have movements and not put king in check. + if(position_valid && movements.length > 0 && !checking) { + valid = true; + } + } + + return valid; + } }; GAME.Const = { @@ -763,11 +812,11 @@ GAME.Const = { .add(5), new GAME.PieceMovement() .add(0) - .add(1) - .add(2) + .add_stride(1, 1) + .add_stride(2, 1) .add(3) - .add(4) - .add(5), + .add_stride(4, 1) + .add_stride(5, 1), ), new GAME.GamePiece( "Knight", @@ -781,8 +830,6 @@ GAME.Const = { .add(16) .add(17), new GAME.PieceMovement() - .add(0) - .add(3) .add(6) .add(8) .add(9) @@ -790,7 +837,8 @@ GAME.Const = { .add(13) .add(14) .add(16) - .add(17), + .add(17) + .add_alt(), ), new GAME.GamePiece( "Tower", @@ -831,7 +879,8 @@ GAME.Const = { .add(4) .add(5) .add(7) - .add(10), + .add(10) + .add_alt(), ), new GAME.GamePiece( "Dragon", diff --git a/www/js/interface.js b/www/js/interface.js index 78a5871..8270b8c 100644 --- a/www/js/interface.js +++ b/www/js/interface.js @@ -76,13 +76,23 @@ const INTERFACE = { if(piece_id !== null) { let piece = GAME_DATA.board.pieces[piece_id]; player = piece.player; - movements = GAME_DATA.movement_tiles(piece, selection.tile); + if(piece.moves().alt && INTERFACE_DATA.alt_mode) { + movements = GAME_DATA.movement_tiles_alt(piece); + } else { + movements = GAME_DATA.movement_tiles(piece, selection.tile); + } } } else { player = Math.floor(selection.tile / 7); player ^= INTERFACE_DATA.player & 1; if(INTERFACE_DATA.player == 2) { player ^= INTERFACE_DATA.rotate; } - movements = GAME_DATA.placement_tiles(selection.tile % 7, player); + + let select_alt = INTERFACE.selection_has_alt(INTERFACE_DATA.select); + if(select_alt !== null) { + movements = GAME_DATA.movement_tiles_alt(select_alt); + } else { + movements = GAME_DATA.placement_tiles(selection.tile % 7, player); + } } if(movements !== null) { @@ -90,7 +100,7 @@ const INTERFACE = { for(let movement of movements) { if(movement.valid) { // Show valid/threat hints if piece belongs to player and is player turn. - if(INTERFACE_DATA.player == 2 || player == INTERFACE_DATA.player) { // && (GAME_DATA.turn & 1) == player + if(INTERFACE_DATA.player == 2 || player == INTERFACE_DATA.player) { if(GAME_DATA.board.tiles[movement.tile].threaten[+(!player)] > 0) { INTERFACE_DATA.board_state[movement.tile][zone] = INTERFACE.TileStatus.Threat; } else { @@ -195,50 +205,77 @@ const INTERFACE = { }, click() { - let initial_select = INTERFACE_DATA.select; - if(INTERFACE_DATA.hover !== null) { if(INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) { INTERFACE_DATA.select = null; + INTERFACE_DATA.alt_mode = false; } else { // Check if operation can be performed on new tile. // Otherwise, switch selection. - let is_valid = false; - if(INTERFACE_DATA.select !== null && INTERFACE_DATA.hover.source == 0 && INTERFACE_DATA.player == (GAME_DATA.turn & 1)) { - let tile_state = INTERFACE_DATA.board_state[INTERFACE_DATA.hover.tile][1]; - is_valid = (tile_state == INTERFACE.TileStatus.Valid || tile_state == INTERFACE.TileStatus.Threat); + let result = 0; + if(INTERFACE_DATA.select !== null) { + + // Play selection. + if(INTERFACE_DATA.hover.source == 0 && INTERFACE_DATA.player == (GAME_DATA.turn & 1)) { + let tile_state = INTERFACE_DATA.board_state[INTERFACE_DATA.hover.tile][1]; + result = +(tile_state == INTERFACE.TileStatus.Valid || tile_state == INTERFACE.TileStatus.Threat); + } + + // Alt move selection. + else if(INTERFACE_DATA.select.source == 0 && INTERFACE_DATA.hover.source == 1) { + let alt_piece = INTERFACE.selection_has_alt(INTERFACE_DATA.select); + if(alt_piece !== null) { + let pool_player = Math.floor(INTERFACE_DATA.hover.tile / 7); + pool_player ^= INTERFACE_DATA.player & 1; + if(INTERFACE_DATA.player == 2) { pool_player ^= INTERFACE_DATA.rotate; } + + if((INTERFACE_DATA.hover.tile % 7) == alt_piece.piece && alt_piece.player == pool_player) { + INTERFACE_DATA.alt_mode = !INTERFACE_DATA.alt_mode; + result = 2; + } + } + } } // Handle player action. - if(is_valid) { - let play = new GAME.Play(INTERFACE_DATA.select.source, INTERFACE_DATA.select.tile, INTERFACE_DATA.hover.tile); - INTERFACE.process(play); - INTERFACE_DATA.select = null; - } + switch(result) { + case 1: { + let play = new GAME.Play( + INTERFACE_DATA.select.source, + INTERFACE_DATA.select.tile, + INTERFACE_DATA.hover.tile, + INTERFACE_DATA.alt_mode + ); + INTERFACE.process(play); + INTERFACE_DATA.select = null; + INTERFACE_DATA.alt_mode = false; + } break; - // Handle new selection. - else { - INTERFACE_DATA.select = null; + case 0: { + // Handle new selection. + INTERFACE_DATA.select = null; + INTERFACE_DATA.alt_mode = false; - // Select tile on board. - if(INTERFACE_DATA.hover.source == 0) { - if(GAME_DATA.board.tiles[INTERFACE_DATA.hover.tile].piece !== null) { - INTERFACE_DATA.select = INTERFACE_DATA.hover; + // Select tile on board. + if(INTERFACE_DATA.hover.source == 0) { + if(GAME_DATA.board.tiles[INTERFACE_DATA.hover.tile].piece !== null) { + INTERFACE_DATA.select = INTERFACE_DATA.hover; + } } - } - // Select tile in pools. - else { - let pool_player = Math.floor(INTERFACE_DATA.hover.tile / 7); - pool_player ^= INTERFACE_DATA.player & 1; - if(INTERFACE_DATA.player == 2) { pool_player ^= INTERFACE_DATA.rotate; } + // Select tile in pools. + else { + let pool_player = Math.floor(INTERFACE_DATA.hover.tile / 7); + pool_player ^= INTERFACE_DATA.player & 1; + if(INTERFACE_DATA.player == 2) { pool_player ^= INTERFACE_DATA.rotate; } - let pool_piece = INTERFACE_DATA.hover.tile % 7; - - if(GAME_DATA.pools[pool_player].pieces[pool_piece] > 0) { - INTERFACE_DATA.select = INTERFACE_DATA.hover; + let pool_piece = INTERFACE_DATA.hover.tile % 7; + + if(GAME_DATA.pools[pool_player].pieces[pool_piece] > 0) { + INTERFACE_DATA.select = INTERFACE_DATA.hover; + } } - } + } break; } } } @@ -248,9 +285,7 @@ const INTERFACE = { INTERFACE_DATA.select = null; } - if(initial_select !== INTERFACE_DATA.select) { - INTERFACE.draw(); - } + INTERFACE.draw(); }, release() { @@ -263,9 +298,15 @@ const INTERFACE = { // Handle player action. if(is_valid) { - let play = new GAME.Play(INTERFACE_DATA.select.source, INTERFACE_DATA.select.tile, INTERFACE_DATA.hover.tile); + let play = new GAME.Play( + INTERFACE_DATA.select.source, + INTERFACE_DATA.select.tile, + INTERFACE_DATA.hover.tile, + INTERFACE_DATA.alt_mode + ); INTERFACE.process(play); INTERFACE_DATA.select = null; + INTERFACE_DATA.alt_mode = false; } } }, @@ -784,12 +825,11 @@ const INTERFACE = { pool(x, y, basis, player) { let radius = INTERFACE.Radius * this.scale; let icon_radius = 0.69 * radius; - - for(let i = 0; i < 7; ++i) { - let is_hover = INTERFACE.Ui.tile_is_hover(1, i + basis); - let is_select = INTERFACE.Ui.tile_is_select(1, i + basis); + let tile_id = i + basis; + let is_hover = INTERFACE.Ui.tile_is_hover(1, tile_id); + let is_select = INTERFACE.Ui.tile_is_select(1, tile_id); let ix = +(i > 1) + (i > 4); let iy = i - ((i > 1) * 3) - ((i > 4) * 2) + (0.5 * (ix == 1)); @@ -802,9 +842,32 @@ const INTERFACE = { this.ctx.save(); this.ctx.translate(gui_x, gui_y); + + // Get background color. + let background_color = null; + + let alt_piece = INTERFACE.selection_has_alt(INTERFACE_DATA.hover); + if(alt_piece !== null) { + if(alt_piece.piece == i && alt_piece.player == player) { + background_color = INTERFACE.Color.HintValidDark; + } + } + + alt_piece = INTERFACE.selection_has_alt(INTERFACE_DATA.select); + if(is_select) { background_color = INTERFACE.Color.HintSelect; } + else if(alt_piece !== null) { + if(alt_piece.piece == i && alt_piece.player == player) { + if(INTERFACE_DATA.alt_mode) { + background_color = INTERFACE.Color.HintSelect; + } else { + background_color = INTERFACE.Color.HintValid; + } + } + } // Draw border - if(is_select || is_hover || (INTERFACE_DATA.player == player && INTERFACE_DATA.player == (GAME_DATA.turn & 1)) || (INTERFACE_DATA.player == 2 && player == (GAME_DATA.turn & 1))) { + let turn_indicator = player == (GAME_DATA.turn & 1) && (INTERFACE_DATA.player == player || INTERFACE_DATA.player == 2); + if(background_color !== null || turn_indicator) { if(is_hover) { this.ctx.fillStyle = INTERFACE.Color.HintHover; } else { this.ctx.fillStyle = player_color; } this.ctx.beginPath(); @@ -812,9 +875,8 @@ const INTERFACE = { this.ctx.fill(); } - // Draw background if indicator is present. - if(is_select) { this.ctx.fillStyle = INTERFACE.Color.HintSelect; } - else { this.ctx.fillStyle = INTERFACE.Color.TileDark; } + if(background_color === null) { background_color = INTERFACE.Color.TileDark; } + this.ctx.fillStyle = background_color; this.ctx.beginPath(); this.hex(0.94); this.ctx.fill(); @@ -886,6 +948,7 @@ const INTERFACE = { hover: null, select: null, + alt_mode: false, handles: [dawn, dusk], board_state: [ ], @@ -1338,6 +1401,21 @@ const INTERFACE = { INTERFACE.process(result.play); } }, + + selection_has_alt(selection) { + if(selection !== null) { + if(selection.source == 0) { + let piece_id = GAME_DATA.board.tiles[selection.tile].piece; + if(piece_id !== null) { + let piece = GAME_DATA.board.pieces[piece_id]; + if(piece.moves().alt) { + return piece; + } + } + } + } + return null; + }, }; INTERFACE.TileScale = 0.9;