dzura/www/js/game.js

955 lines
30 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))
);
}
}
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) {
if(this.pieces[i] !== null) {
board.pieces[i] = this.pieces[i].clone();
} else {
board.pieces[i] = null;
}
}
return board;
}
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); }
}
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 = 0;
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 = 0;
}
};
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, alt=false) {
this.source = source;
this.from = from;
this.to = to;
this.alt = alt;
}
};
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 = false;
}
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() {
this.alt = true;
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) {
this.piece = piece;
this.player = player;
this.promoted = false;
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() {
this.turn = 0;
this.board = new GAME.Board();
this.pools = [
new GAME.Pool(),
new GAME.Pool(),
];
this.state = {
code:0,
check:false,
checkmate:false,
};
this.update_board();
}
clone() {
let game = new GAME.Game();
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 & 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 |= movement.check;
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 = 1;
checking_pieces++;
}
}
}
this.state.check += checking_pieces;
// Count moves available to next turn player to determine checkmate.
if(this.state.check != 0) {
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; }
}
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) {
for(let move of this.placement_tiles(i, player)) {
if(move.valid) { moves += 1; }
}
}
if(moves == 0) { this.state.checkmate = true; }
}
}
process(play) {
if(this.state.code != 0) { return false; }
let player = this.turn & 1;
// Move piece on board.
switch(play.source) {
case 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 && 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.alt) {
switch(piece.piece) {
case GAME.Const.PieceId.Knight: {
piece.promoted = false;
} break;
}
}
this.turn++;
} break;
// Place piece from pool.
case 1: {
this.board.set_piece(
play.from,
player,
play.to
);
this.pools[this.turn & 1].pieces[play.from] -= 1;
this.turn++;
} break;
// Play retired.
case 2: {
this.state.code = 2;
} break;
}
// Recalculate new board state.
this.update_board();
}
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 = true;
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 = 9; 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 & 1) && this.state.check != 0) {
if(piece.piece != GAME.Const.PieceId.Omen) {
if(tile_data.checking != 0) {
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.Omen && 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(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) {
if(dist == 1) { check = GAME.Const.Check.Direct; }
else { check = GAME.Const.Check.Stride; }
if(stride) { 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) {
// Apply blocking to last .
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.Omen) {
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, range, 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.Omen
&& (this.board.tiles[tile].threaten[+(!target.player)] > 0 || piece.blocking != 0)) {
return false;
}
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_blocks6(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_blocks6(mask_back);
}
let mask_column = mask_back | BITWISE.rotate_blocks6(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) {
if(this.placable_tile(piece, i)) {
tiles.push(new GAME.MovementTile(i, true, 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 = {
Player: {
Dawn: 0,
Dusk: 1,
},
Source: {
Board: 0,
Pool: 1,
},
State: {
Ongoing: 0,
Complete: 1,
Resign: 2,
},
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,
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(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(),
),
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(),
),
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(
"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 % GAME.Const.Direction.length].copy();
direction.mul(Math.ceil((direction_id + 1) / GAME.Const.Direction.length));
return direction;
},
};
GAME.init = () => {
GAME_DATA = new GAME.Game();
};