dzura/www/js/game.js
2024-08-17 18:50:23 -07:00

768 lines
23 KiB
JavaScript

const GAME = { };
let GAME_DATA = null;
GAME.Board = class {
constructor() {
this.tiles = [ ]; for(let i = 0; i < 61; ++i) { this.tiles.push(new GAME.Tile(i)); }
this.pieces = [ ]; for(let i = 0; i < GAME.Const.Count.Pieces; ++i) { this.pieces.push(null); }
this.columns = [ ]; for(let i = 0; i < 9; ++i) { this.columns.push(new GAME.Column()); }
this.init();
}
init() {
this.pieces = [ ]; for(let i = 0; i < GAME.Const.Count.Pieces; ++i) { this.pieces.push(null); }
// Describe Dawn layout
let layout = [
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(0, 1) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(1, 1) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(2, 2) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(3, 2) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(4, 3) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(5, 3) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(6, 4) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(7, 4) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(8, 5) },
{ piece:GAME.Const.PieceId.Lance, hex:new MATH.Vec2(0, 0) },
{ piece:GAME.Const.PieceId.Lance, hex:new MATH.Vec2(8, 4) },
{ piece:GAME.Const.PieceId.Knight, hex:new MATH.Vec2(1, 0) },
{ piece:GAME.Const.PieceId.Knight, hex:new MATH.Vec2(7, 3) },
{ piece:GAME.Const.PieceId.Castle, hex:new MATH.Vec2(2, 0) },
{ piece:GAME.Const.PieceId.Castle, hex:new MATH.Vec2(6, 2) },
{ piece:GAME.Const.PieceId.Tower, hex:new MATH.Vec2(3, 0) },
{ piece:GAME.Const.PieceId.Tower, hex:new MATH.Vec2(5, 1) },
{ piece:GAME.Const.PieceId.Dragon, hex:new MATH.Vec2(4, 2) },
{ piece:GAME.Const.PieceId.Behemoth, hex:new MATH.Vec2(4, 1) },
{ piece:GAME.Const.PieceId.Omen, hex:new MATH.Vec2(4, 0) },
];
// Add Dawn pieces
for(let lay of layout) {
this.set_piece(
lay.piece,
GAME.Const.Player.Dawn,
HEX.hex_to_tile(lay.hex)
);
}
// Add Dusk pieces
for(let lay of layout) {
this.set_piece(
lay.piece,
GAME.Const.Player.Dusk,
HEX.hex_to_tile(new MATH.Vec2(8 - lay.hex.x, 8 - lay.hex.y))
);
}
}
set_piece(piece, player, tile) {
let index = 0;
while(this.pieces[index] !== null) { index++; }
let game_piece = new GAME.Piece(piece, player);
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); }
}
};
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 = [false, false];
this.checking = false;
this.hex = HEX.tile_to_hex(index);
}
reset() {
this.threaten = [false, false];
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) {
this.source = source;
this.from = from;
this.to = to;
}
};
GAME.GamePiece = class {
constructor(name, moves, promote_moves) {
this.name = name;
this.moves = moves;
this.pmoves = promote_moves;
}
};
GAME.PieceMovement = class {
constructor() {
this.direction = 0;
this.stride = 0;
}
add(direction) {
this.direction |= 1 << direction;
return this;
}
add_stride(direction) {
this.direction |= 1 << direction;
this.stride |= 1 << direction;
return this;
}
rotate() {
let copy = new GAME.PieceMovement();
copy.direction = BITWISE.rotate_blocks(this.direction);
copy.stride = BITWISE.rotate_blocks(this.stride);
return copy;
}
};
GAME.Piece = class {
constructor(piece, player) {
this.piece = piece;
this.player = player;
this.promoted = false;
this.tile = 0;
this.blocking = 0;
}
moves() {
let moves = null;
if(this.promoted) { moves = GAME.Const.Piece[this.piece].pmoves; }
else { moves = GAME.Const.Piece[this.piece].moves; }
if(this.player == GAME.Const.Player.Dusk) { moves = moves.rotate(); }
return moves;
}
reset() {
this.blocking = 0;
}
};
GAME.Game = class {
constructor() {
this.turn = 0;
this.board = new GAME.Board();
this.pools = [
new GAME.Pool(),
new GAME.Pool(),
];
this.state = {
check:false,
checkmate:false,
};
this.update_board();
}
update_board() {
// Reset tiles
this.board.reset();
this.state.check = false;
this.state.checkmate = false;
// Determine threaten, check, and blocking for each piece
for(let piece of this.board.pieces) {
if(piece !== null) {
let hex = this.board.tiles[piece.tile].hex;
// Check if column has militia.
if(piece.piece == GAME.Const.PieceId.Militia) {
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] = true;
}
if(movement.check) {
this.board.tiles[piece.tile].checking = true;
this.board.tiles[movement.tile].checking = true;
this.state.check = true;
}
if(movement.block != 0) {
this.board.pieces[this.board.tiles[movement.tile].piece].blocking = movement.block;
}
}
}
}
// Count moves available to next turn player to determine checkmate.
if(this.state.check) {
let moves = 0;
// Search for valid board moves.
for(let piece of this.board.pieces) {
if(piece !== null && piece.player == (this.turn & 1)) {
for(let move of this.movement_tiles(piece, piece.tile)) {
if(move.valid) { moves += 1; }
}
}
}
// Search for valid pool placements.
for(let i = 0; i < this.pools[0].length; ++i) {
for(let move of this.placement_tiles(i, (this.turn & 1))) {
if(move.valid) { moves += 1; }
}
}
if(moves == 0) { this.state.checkmate = true; }
}
}
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.
//
let player = this.turn & 1;
// Move piece on board.
if(play.source == 0) {
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 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 && !HEX.is_valid(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 && !HEX.is_valid(hex)) {
piece.promoted = true;
}
}
// Place piece from pool.
else {
this.board.set_piece(
play.from,
player,
play.to
);
this.pools[this.turn & 1].pieces[play.from] -= 1;
}
this.turn++;
// Recalculate new board state.
this.update_board();
// If check, detect checkmate.
}
movement_tiles(piece, tile) {
let tiles = [ ];
let moves = piece.moves();
let hex = this.board.tiles[tile].hex;
let directions = moves.direction;
let permitted_moves = directions;
let block_directions = piece.blocking;
if(block_directions != 0) {
block_directions |= BITWISE.rotate_blocks(piece.blocking);
//if(piece.piece != GAME.Const.PieceId.Omen) {
// permitted_moves &= 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 = (moves.stride & mask) != 0;
// Get initial status in direction.
let valid = (permitted_moves & mask) != 0;
let pieces_blocking = 0;
let move_hex = hex.copy();
// Check tiles in direction up to movement limit.
let max_dist = (stride)? 8 : 1;
for(let dist = 1; dist <= max_dist; ++dist) {
move_hex.add(direction);
let threat = valid;
let check = false;
let block = 0;
let swap = false;
let tile_occupied = false;
if(HEX.is_valid(move_hex)) {
let tile_id = HEX.hex_to_tile(move_hex);
let tile_data = this.board.tiles[tile_id];
let result = valid;
// Prevent moves that do not uncheck the King.
if(piece.player == (this.turn & 1)
&& this.state.check && !tile_data.checking
&& piece.piece != GAME.Const.PieceId.Omen) {
result = false;
}
// King may not move onto threatened tile.
if(piece.piece == GAME.Const.PieceId.Omen && tile_data.threaten[+(!piece.player)]) {
result = false;
}
// Handle occupied tile.
if(tile_data.piece !== null) {
let target = this.board.pieces[tile_data.piece];
tile_occupied = true;
// Target piece is ally.
if(target.player == piece.player) {
// Move is only valid if pieces are swappable.
if(this.movement_swappable(piece, target, mask, dist, 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.Omen) {
if(valid) {
check = true;
block = mask;
// Apply check to previous moves.
for(let idist = 1; idist < dist; idist++) {
tiles[tiles.length - idist].check = true;
}
}
if(pieces_blocking == 1) {
// Apply blocking to last .
for(let idist = 1; idist < dist; idist++) {
if(GAME_DATA.board.tiles[tiles[tiles.length - idist].tile].piece !== null) {
tiles[tiles.length - idist].block = mask;
}
}
}
}
pieces_blocking += 1;
valid = false;
}
}
// Handle blocking restrictions.
if(block_directions != 0) {
if(piece.piece == GAME.Const.PieceId.Omen) {
result = result && ((mask & block_directions) == 0 || tile_occupied);
} else {
result = result && ((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, range, tile) {
if(piece.piece == target.piece && piece.promoted == target.promoted) {
return false;
}
mask = BITWISE.rotate_blocks(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.Omen
&& (this.board.tiles[tile].threaten[+(!target.player)] || piece.blocking != 0)) {
return false;
}
return ((moves.direction & mask) != 0 && (range == 1 || (moves.stride & mask) != 0));
}
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;
if(player == (this.turn & 1) && this.state.check && !tile.checking) {
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);
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;
}
}
tiles.push(new GAME.MovementTile(i, valid, false, false));
}
return tiles;
}
};
GAME.Const = {
Player: {
Dawn: 0,
Dusk: 1,
},
Source: {
Board: 0,
Pool: 1,
},
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),
new MATH.Vec2(1, 3),
new MATH.Vec2(2, 3),
new MATH.Vec2(3, 2),
new MATH.Vec2(3, 1),
new MATH.Vec2(2, -1),
new MATH.Vec2(1, -2),
new MATH.Vec2(-1, -3),
new MATH.Vec2(-2, -3),
new MATH.Vec2(-3, -2),
new MATH.Vec2(-3, -1),
new MATH.Vec2(-2, 1),
new MATH.Vec2(-1, 2),
],
MoveStatus: {
Valid: 0,
Invalid: 1,
Check: 2,
},
PieceId: {
Militia: 0,
Lance: 1,
Knight: 2,
Tower: 3,
Castle: 4,
Dragon: 5,
Behemoth: 6,
Omen: 7,
},
Count: {
Pieces:40,
},
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(5),
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5),
),
new GAME.GamePiece(
"Knight",
new GAME.PieceMovement()
.add(12)
.add(13)
.add(14)
.add(15)
.add(16)
.add(17)
.add(18)
.add(19)
.add(20)
.add(21)
.add(22)
.add(23),
new GAME.PieceMovement()
.add(1)
.add(3)
.add(5)
.add(12)
.add(13)
.add(14)
.add(15)
.add(16)
.add(17)
.add(18)
.add(19)
.add(20)
.add(21)
.add(22)
.add(23),
),
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_stride(7)
.add_stride(10),
),
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(6)
.add(7)
.add(8)
.add(9)
.add(10)
.add(11)
),
new GAME.GamePiece(
"Omen",
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 % 24].copy();
direction.mul(Math.ceil((direction_id + 1) / 24));
return direction;
},
};
GAME.init = () => {
GAME_DATA = new GAME.Game();
};