dzura/www/js/game.js
2024-12-27 19:56:45 -08:00

1026 lines
33 KiB
JavaScript

const GAME = { };
let GAME_DATA = null;
GAME.Board = class {
constructor(config=null) {
this.tiles = [ ]; for(let i = 0; i < 61; ++i) { this.tiles.push(new GAME.Tile(i)); }
this.columns = [ ]; for(let i = 0; i < 9; ++i) { this.columns.push(new GAME.Column()); }
this.pieces = [ ];
if(config !== null) {
for(let i = 0; i < config.count_pieces(); ++i) { this.pieces.push(null); }
for(let lay of config.layout.pieces) {
this.set_piece(
lay.piece,
lay.player,
lay.promote,
HEX.hex_to_tile(lay.hex)
);
}
}
}
clone() {
let board = new GAME.Board();
for(let i = 0; i < this.tiles.length; ++i) { board.tiles[i] = this.tiles[i].clone(); }
for(let i = 0; i < this.pieces.length; ++i) { board.pieces.push(null); }
for(let i = 0; i < this.pieces.length; ++i) {
if(this.pieces[i] !== null) {
board.pieces[i] = this.pieces[i].clone();
} else {
board.pieces[i] = null;
}
}
return board;
}
set_piece(piece, player, promote, tile) {
let index = 0;
while(index < this.pieces.length && this.pieces[index] !== null) { index++; }
if(index == this.pieces.length) { this.pieces.push(null); }
let game_piece = new GAME.Piece(piece, player, promote);
game_piece.tile = tile;
this.tiles[tile].piece = index;
this.pieces[index] = game_piece;
return index;
}
reset() {
for(let i = 0; i < this.tiles.length; ++i) { this.tiles[i].reset(); }
for(let i = 0; i < this.columns.length; ++i) { this.columns[i].reset(); }
for(let i = 0; i < this.pieces.length; ++i) { if(this.pieces[i] !== null) { this.pieces[i].reset(); } }
}
};
GAME.Pool = class {
constructor() {
this.pieces = [ ]; for(let i = 0; i < 7; ++i) { this.pieces.push(0); }
}
clone() {
let pool = new GAME.Pool();
for(let i = 0; i < pool.pieces.length; ++i) { pool.pieces[i] = this.pieces[i]; }
return pool;
}
};
GAME.Column = class {
constructor() {
this.militia = [false, false];
this.extent = [0, 8];
}
reset() {
this.militia = [false, false];
this.extent = [0, 8];
}
};
GAME.Tile = class {
constructor(index) {
this.piece = null;
this.threaten = [0, 0];
this.checking = false;
this.hex = HEX.tile_to_hex(index);
}
clone() {
let tile = new GAME.Tile(0);
tile.piece = this.piece;
tile.hex = this.hex;
return tile;
}
reset() {
this.threaten = [0, 0];
this.checking = false;
}
};
GAME.MovementTile = class {
constructor(tile, valid, threat, check, block) {
this.tile = tile;
this.valid = valid;
this.threat = threat;
this.check = check;
this.block = block;
}
};
GAME.Play = class {
constructor(source, from, to, player=-1) {
this.source = source;
this.from = from;
this.to = to;
this.player = player;
}
};
GAME.GamePiece = class {
constructor(name, moves, promote_moves=null) {
this.name = name;
this.moves = moves;
this.pmoves = promote_moves;
}
};
GAME.PieceMovement = class {
constructor() {
this.direction = 0;
this.stride = 0;
this.alt = 0;
}
add(direction) {
this.direction |= 1 << direction;
return this;
}
add_stride(direction, mode=3) {
this.direction |= 1 << direction;
this.stride |= mode << (direction * 2);
return this;
}
add_alt(id) {
this.alt = id;
return this;
}
rotate() {
let copy = new GAME.PieceMovement();
copy.direction = BITWISE.rotate_blocks6(this.direction);
copy.stride = BITWISE.rotate_blocks12(this.stride);
copy.alt = this.alt;
return copy;
}
};
GAME.Piece = class {
constructor(piece, player, promote=false) {
this.piece = piece;
this.player = player;
this.promoted = promote;
this.tile = 0;
this.blocking = 0;
}
clone() {
let piece = new GAME.Piece(this.piece, this.player);
piece.promoted = this.promoted;
piece.tile = this.tile;
return piece;
}
moves() {
let def = GAME.Const.Piece[this.piece];
let moves = null;
if(this.promoted) { moves = def.pmoves; }
else { moves = def.moves; }
if(this.player == GAME.Const.Player.Dusk) { moves = moves.rotate(); }
return moves;
}
has_promotion() {
return !this.promoted && GAME.Const.Piece[this.piece].pmoves !== null;
}
reset() {
this.blocking = 0;
}
};
GAME.Game = class {
constructor(config) {
this.config = config;
this.turn = 0;
this.board = new GAME.Board(this.config);
this.pools = [
new GAME.Pool(),
new GAME.Pool(),
];
for(let i = 0; i < 7; ++i) {
this.pools[0].pieces[i] = this.config.pool[i];
}
for(let i = 0; i < 7; ++i) {
this.pools[1].pieces[i] = this.config.pool[i + 7];
}
this.state = {
code:0,
check:false, //[false, false],
};
this.update_board();
}
clone() {
let game = new GAME.Game(this.config);
game.turn = this.turn;
game.board = this.board.clone();
game.pools = [ this.pools[0].clone(), this.pools[1].clone() ];
game.update_board();
return game;
}
update_board() {
// Reset tiles
this.board.reset();
this.state.check = 0;
this.state.checkmate = false;
let player = (this.turn + this.config.rules.reverse) & 1;
// Determine threaten, check, and blocking for each piece
let checking_pieces = 0;
for(let piece of this.board.pieces) {
if(piece !== null) {
let hex = this.board.tiles[piece.tile].hex;
let is_checking = false;
// Check if column has militia.
if(piece.piece == GAME.Const.PieceId.Militia && !piece.promoted) {
this.board.columns[hex.x].militia[piece.player] = true;
}
// Check furthest piece in column.
if(piece.player == 0) { this.board.columns[hex.x].extent[0] = Math.max(hex.y, this.board.columns[hex.x].extent[0]); }
else { this.board.columns[hex.x].extent[1] = Math.min(hex.y, this.board.columns[hex.x].extent[1]); }
// Get threatened tiles.
for(let movement of this.movement_tiles(piece, piece.tile)) {
if(movement.threat) {
this.board.tiles[movement.tile].threaten[piece.player] += 1;
}
if(movement.valid && movement.check != 0) {
is_checking = true;
this.board.tiles[movement.tile].checking = true;
this.state.check |= movement.check;
}
if(movement.block != 0) {
this.board.pieces[this.board.tiles[movement.tile].piece].blocking = movement.block;
}
}
if(is_checking) {
this.board.tiles[piece.tile].checking = true;
checking_pieces++;
}
}
}
this.state.check += checking_pieces;
// Count moves available to next turn player to determine checkmate.
let moves = 0;
// Search for valid board moves.
for(let piece of this.board.pieces) {
if(piece !== null && piece.player == player) {
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; }
}
}
}
// Search for valid pool placements.
for(let i = 0; i < this.pools[player].pieces.length; ++i) {
if(this.pools[player].pieces[i] > 0) {
for(let move of this.placement_tiles(i, player)) {
if(move.valid) { moves += 1; }
}
}
}
if(moves == 0) {
this.state.code = GAME.Const.State.Checkmate;
}
}
process(play) {
if(this.state.code != 0) { return false; }
let player = 0;
if(play.player == -1) {
player = (this.turn + this.config.rules.reverse) & 1;
} else {
player = play.player;
}
// Move piece on board.
switch(play.source) {
case 0:
case 2: {
let piece_id = this.board.tiles[play.from].piece;
let piece = this.board.pieces[piece_id];
piece.tile = play.to;
this.board.tiles[play.from].piece = null;
let moves = piece.moves();
let target_id = this.board.tiles[play.to].piece;
if(target_id !== null) {
let target = this.board.pieces[target_id];
// Swap piece with moving piece.
if(target.player == piece.player) {
target.tile = play.from;
this.board.tiles[play.from].piece = target_id;
// Check if swap is promoted.
let hex = HEX.tile_to_hex(target.tile);
hex.y -= MATH.sign_branch(target.player);
if(!target.promoted && target.has_promotion() && !HEX.is_valid_board(hex)) {
target.promoted = true;
}
}
// Add captured piece to pool and destroy.
else {
this.pools[piece.player].pieces[target.piece] += 1;
this.board.pieces[target_id] = null;
}
}
this.board.tiles[play.to].piece = piece_id;
// Check if piece is promoted.
let hex = HEX.tile_to_hex(piece.tile);
hex.y -= MATH.sign_branch(piece.player);
if(!piece.promoted && piece.has_promotion() && !HEX.is_valid_board(hex)) {
piece.promoted = true;
}
// Handle alt moves.
if(play.source == 2) {
switch(moves.alt) {
case 1: {
piece.promoted = false;
} break;
}
}
this.turn++;
} break;
// Place piece from pool.
case 1: {
this.board.set_piece(
play.from,
player,
false,
play.to
);
this.pools[player].pieces[play.from] -= 1;
this.turn++;
} break;
case 0xF: {
this.state.code = GAME.Const.State.Resign;
} break;
}
// Recalculate new board state.
this.update_board();
}
play_is_valid(play) {
let player = 0;
if(play.player == -1) {
player = (this.turn + this.config.rules.reverse) & 1;
} else {
player = play.player;
}
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 || !this.config.rules.turn) {
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();
let hex = this.board.tiles[tile].hex;
let directions = moves.direction;
let block_directions = piece.blocking | BITWISE.rotate_blocks6(piece.blocking);
// Check directions of movement.
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 = 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; }
// Get initial status in direction.
let valid = this.state.code == 0;
let pieces_blocking = 0;
let move_hex = hex.copy();
// Check tiles in direction up to movement limit.
let max_dist = 1;
switch(stride) {
case 1: max_dist = 2; break;
case 3: max_dist = 8; break;
}
for(let dist = 1; dist <= max_dist; ++dist) {
move_hex.add(direction);
let threat = valid;
let check = 0;
let block = 0;
let swap = false;
let tile_occupied = false;
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 result = valid;
// Prevent moves that do not uncheck King.
let check_direct = (this.state.check & 0x040) != 0;
let check_count = this.state.check & 0x3F;
if(piece.player == ((this.turn + this.config.rules.reverse) & 1) && this.state.check != 0) {
if(piece.piece != GAME.Const.PieceId.Heart) {
if(tile_data.checking) {
if(target_id !== null) {
if(check_count > 1) {
result = false;
}
} else if(check_direct != 0 || check_count > 1) {
result = false;
}
} else {
result = false;
}
}
}
// King may not move onto threatened tile.
if(piece.piece == GAME.Const.PieceId.Heart && tile_data.threaten[+(!piece.player)] > 0) {
result = false;
}
// Handle occupied tile.
if(target_id !== null) {
let target = this.board.pieces[target_id];
tile_occupied = true;
// Target piece is ally.
if(target.player == piece.player) {
pieces_blocking += 2;
// Move is only valid if pieces are swappable.
if(dist == 1 && this.movement_swappable(piece, target, mask, tile)) {
swap = true;
} else {
result = false;
}
valid = false;
}
// Target piece is opposing.
else {
// Check if target piece is opposing king.
if(target.piece == GAME.Const.PieceId.Heart) {
if(valid) {
if(dist == 1) { check = GAME.Const.Check.Direct; }
else { check = GAME.Const.Check.Stride; }
if(stride > 0) { block = dir_mask; }
// Apply check to previous moves.
for(let idist = 1; idist < dist; idist++) {
tiles[tiles.length - idist].check = GAME.Const.Check.Stride;
}
}
if(pieces_blocking == 1 && stride > 0) {
// Apply blocking to last piece tile.
for(let idist = 1; idist < dist; idist++) {
if(this.board.tiles[tiles[tiles.length - idist].tile].piece !== null) {
tiles[tiles.length - idist].block = dir_mask;
break;
}
}
}
}
pieces_blocking += 1;
valid = false;
}
}
// Handle blocking restrictions.
if(block_directions != 0) {
if(piece.piece == GAME.Const.PieceId.Heart) {
result = result && ((dir_mask & block_directions) == 0 || tile_occupied);
} else {
result = result && ((dir_mask & block_directions) != 0 || swap);
}
}
tiles.push(new GAME.MovementTile(tile_id, result, threat, check, block));
} else { break; }
}
directions &= ~mask;
}
return tiles;
}
movement_swappable(piece, target, mask, tile) {
if(piece.piece == target.piece && piece.promoted == target.promoted) {
return false;
}
mask = BITWISE.rotate_blocks6(mask);
let moves = target.moves();
// King cannot swap onto tile that is threatened.
// This case should also cover blocking.
if(target.piece == GAME.Const.PieceId.Heart
&& (this.board.tiles[tile].threaten[+(!target.player)] > 0 || piece.blocking != 0)) {
return false;
}
return (moves.direction & 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_blocks6(piece.blocking);
let moves = piece.moves();
switch(moves.alt) {
// Drop Once
case 1: {
// 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, {check:false, extent:false})) {
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 tile_hex = hex.copy();
for(let dist = 1; dist <= 9; ++dist) {
tile_hex.add(direction);
if(HEX.is_valid_board(tile_hex)) {
let tile_id = HEX.hex_to_tile(tile_hex);
if(this.placable_tile(piece, tile_id, {check:false, extent:false})) {
tiles.push(new GAME.MovementTile(tile_id, true, false, 0, 0));
}
} else { break; }
}
directions &= ~mask;
}
}
} break;
// Cone Drop
case 2: {
// Check all tiles if not blocking.
if(block_directions == 0) {
for(let i = 0; i < GAME_DATA.board.tiles.length; ++i) {
let tile_hex = this.board.tiles[i].hex;
let valid = true;
if(piece.player == 0) {
if(tile_hex.x >= hex.x) {
valid = tile_hex.y <= hex.y;
} else {
valid = tile_hex.y <= hex.y - (hex.x - tile_hex.x);
}
} else {
if(tile_hex.x >= hex.x) {
valid = tile_hex.y >= hex.y + (tile_hex.x - hex.x);
} else {
valid = tile_hex.y >= hex.y;
}
}
if(valid && this.placable_tile(piece, i, {check:false, extent:false})) {
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 tile_hex = hex.copy();
for(let dist = 1; dist <= 9; ++dist) {
tile_hex.add(direction);
if(HEX.is_valid_board(tile_hex)) {
let tile_id = HEX.hex_to_tile(tile_hex);
let valid = true;
if(piece.player == 0) {
if(tile_hex.x >= hex.x) {
valid = tile_hex.y <= hex.y;
} else {
valid = tile_hex.y <= hex.y - (hex.x - tile_hex.x);
}
} else {
if(tile_hex.x >= hex.x) {
valid = tile_hex.y >= hex.y + (tile_hex.x - hex.x);
} else {
valid = tile_hex.y >= hex.y;
}
}
if(this.placable_tile(piece, tile_id, {check:false, extent:false})) {
tiles.push(new GAME.MovementTile(tile_id, true, false, 0, 0));
}
} else { break; }
}
directions &= ~mask;
}
}
} break;
}
}
return tiles;
}
placement_tiles(piece_id, player) {
let tiles = [ ];
if(this.state.code != 0) { return 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) {
if(this.placable_tile(piece, i)) {
tiles.push(new GAME.MovementTile(i, true, false, false));
}
}
return tiles;
}
placable_tile(piece, tile_id, params={}) {
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 && this.state.code == 0) {
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 + this.config.rules.reverse) & 1) && this.state.check != 0) {
if(check_direct == 0 && check_count == 1) {
position_valid = tile.checking;
} else {
position_valid = false;
}
}
// Check off-sides.
if(params.extent !== false) {
if(piece.player == 0) {
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[0]);
}
}
// Check militia stacking.
if(params.stack !== false) {
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);
if(params.check !== false) {
for(let movement of movements) {
if(movement.check) {
checking = true;
break;
}
}
}
// Piece must have movements and not put king in check.
if(position_valid && !checking) {
valid = true;
}
}
return valid;
}
};
GAME.Const = {
Player: {
Dawn: 0,
Dusk: 1,
},
Source: {
Board: 0,
Pool: 1,
},
State: {
Normal: 0,
Checkmate: 1,
Resign: 2,
Default: 3,
},
Direction: [
new MATH.Vec2(0, 1),
new MATH.Vec2(1, 1),
new MATH.Vec2(1, 0),
new MATH.Vec2(0, -1),
new MATH.Vec2(-1, -1),
new MATH.Vec2(-1, 0),
new MATH.Vec2(1, 2),
new MATH.Vec2(2, 1),
new MATH.Vec2(1, -1),
new MATH.Vec2(-1, -2),
new MATH.Vec2(-2, -1),
new MATH.Vec2(-1, 1),
],
Check: {
Direct: 0x0040,
Stride: 0x0080,
},
PieceId: {
Militia: 0,
Lance: 1,
Knight: 2,
Tower: 3,
Castle: 4,
Dragon: 5,
Behemoth: 6,
Heart: 7,
},
Piece: [
new GAME.GamePiece(
"Militia",
new GAME.PieceMovement()
.add(0)
.add(1)
.add(5),
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5),
),
new GAME.GamePiece(
"Lance",
new GAME.PieceMovement()
.add_stride(0)
.add(1)
.add(3)
.add(5),
new GAME.PieceMovement()
.add(0)
.add_stride(1, 1)
.add_stride(2, 1)
.add(3)
.add_stride(4, 1)
.add_stride(5, 1),
),
new GAME.GamePiece(
"Knight",
new GAME.PieceMovement()
.add(6)
.add(8)
.add(9)
.add(11)
.add(13)
.add(14)
.add(16)
.add(17),
new GAME.PieceMovement()
.add(6)
.add(8)
.add(9)
.add(11)
.add(13)
.add(14)
.add(16)
.add(17)
.add_alt(1),
),
new GAME.GamePiece(
"Tower",
new GAME.PieceMovement()
.add(0)
.add(1)
.add(3)
.add(5)
.add(6)
.add(11),
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(6)
.add(8)
.add(9)
.add(11),
),
new GAME.GamePiece(
"Castle",
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
.add(4)
.add(5)
.add(7)
.add(10),
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(7)
.add(10)
.add_alt(2),
),
new GAME.GamePiece(
"Dragon",
new GAME.PieceMovement()
.add_stride(6)
.add_stride(7)
.add_stride(8)
.add_stride(9)
.add_stride(10)
.add_stride(11),
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add_stride(6)
.add_stride(7)
.add_stride(8)
.add_stride(9)
.add_stride(10)
.add_stride(11),
),
new GAME.GamePiece(
"Behemoth",
new GAME.PieceMovement()
.add_stride(0)
.add_stride(1)
.add_stride(2)
.add_stride(3)
.add_stride(4)
.add_stride(5),
new GAME.PieceMovement()
.add_stride(0)
.add_stride(1)
.add_stride(2)
.add_stride(3)
.add_stride(4)
.add_stride(5)
.add(12)
.add(13)
.add(14)
.add(15)
.add(16)
.add(17)
),
new GAME.GamePiece(
"Heart",
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(7)
.add(10),
),
],
get_direction(direction_id) {
let direction = GAME.Const.Direction[direction_id % GAME.Const.Direction.length].copy();
direction.mul(Math.ceil((direction_id + 1) / GAME.Const.Direction.length));
return direction;
},
};
GAME.init = (config) => {
GAME_DATA = new GAME.Game(config);
};