Add alt moves for Castle and Knight; change Lance promotion moves to two tiles on sides.

This commit is contained in:
yukirij 2024-09-03 17:21:56 -07:00
parent efe46428f7
commit 24e5e9d609
2 changed files with 310 additions and 183 deletions

View File

@ -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",

View File

@ -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;
let pool_piece = INTERFACE_DATA.hover.tile % 7;
if(GAME_DATA.pools[pool_player].pieces[pool_piece] > 0) {
INTERFACE_DATA.select = INTERFACE_DATA.hover;
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;
}
}
},
@ -785,11 +826,10 @@ const INTERFACE = {
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));
@ -803,8 +843,31 @@ 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;