1328 lines
47 KiB
JavaScript
1328 lines
47 KiB
JavaScript
let INTERFACE_DATA = null;
|
|
|
|
const INTERFACE = {
|
|
Mode: {
|
|
Local: 0,
|
|
Player: 1,
|
|
Review: 2,
|
|
},
|
|
|
|
Color: {
|
|
Background: "#101010",
|
|
Text: "#c0c0c0",
|
|
|
|
TileBorder: "#606060",
|
|
TileLight: "#383838",
|
|
TileMedium: "#242424",
|
|
TileDark: "#101010",
|
|
|
|
Dawn: "#ffe082",
|
|
DawnMedium: "#fca03f",
|
|
DawnDark: "#ff6d00",
|
|
|
|
Dusk: "#f6a1bd",
|
|
DuskMedium: "#e84a79",
|
|
DuskDark: "#c51162",
|
|
|
|
HintHover: "#71a1e8",
|
|
HintSelect: "#4a148c",
|
|
HintValid: "#1a237e",
|
|
HintValidDark: "#121859",
|
|
HintThreat: "#054719",
|
|
HintThreatDark: "#023311",
|
|
HintOpponent: "#49136b",
|
|
HintOpponentDark: "#2a0b3f",
|
|
HintInvalid: "#b71c1c",
|
|
HintInvalidDark: "#3f0808",
|
|
HintPlay: "#004966",
|
|
HintCheck: "#C62828",
|
|
},
|
|
|
|
TileStatus: {
|
|
Valid: 1,
|
|
Threat: 2,
|
|
Invalid: 3,
|
|
Opponent: 4,
|
|
Play: 5,
|
|
Check: 6,
|
|
},
|
|
|
|
Selection: class {
|
|
constructor(source, tile, hex) {
|
|
this.source = source;
|
|
this.tile = tile;
|
|
this.hex = hex;
|
|
}
|
|
},
|
|
|
|
resolve_board() {
|
|
for(let i = 0; i < INTERFACE_DATA.board_state.length; ++i) {
|
|
INTERFACE_DATA.board_state[i] = [0, 0];
|
|
}
|
|
|
|
if(INTERFACE_DATA.select !== null) { INTERFACE.resolve_piece(INTERFACE_DATA.select, 1); }
|
|
if(INTERFACE_DATA.hover !== null) { INTERFACE.resolve_piece(INTERFACE_DATA.hover, 0); }
|
|
},
|
|
|
|
resolve_piece(selection, zone) {
|
|
// Determine piece movement hints.
|
|
|
|
let movements = null;
|
|
let player = 0;
|
|
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];
|
|
player = piece.player;
|
|
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);
|
|
}
|
|
|
|
if(movements !== null) {
|
|
// Generate hint for each potential movement.
|
|
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(GAME_DATA.board.tiles[movement.tile].threaten[+(!player)] > 0) {
|
|
INTERFACE_DATA.board_state[movement.tile][zone] = INTERFACE.TileStatus.Threat;
|
|
} else {
|
|
INTERFACE_DATA.board_state[movement.tile][zone] = INTERFACE.TileStatus.Valid;
|
|
}
|
|
}
|
|
else {
|
|
INTERFACE_DATA.board_state[movement.tile][zone] = INTERFACE.TileStatus.Opponent;
|
|
}
|
|
} else {
|
|
INTERFACE_DATA.board_state[movement.tile][zone] = INTERFACE.TileStatus.Invalid;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
hover(event) {
|
|
let initial_hover = INTERFACE_DATA.hover;
|
|
|
|
let apothem = INTERFACE_DATA.Ui.scale;
|
|
let radius = INTERFACE.Radius * apothem;
|
|
let halfradius = radius / 2;
|
|
let grid_offset_x = 1.5 * radius;
|
|
|
|
let hex_slope = 0.25 / 0.75;
|
|
|
|
INTERFACE_DATA.hover = null;
|
|
|
|
// Handle board area
|
|
if(event.offsetY >= INTERFACE_DATA.Ui.margin && event.offsetY < INTERFACE_DATA.Ui.margin + INTERFACE_DATA.Ui.area.y) {
|
|
if(event.offsetX >= INTERFACE_DATA.Ui.offset.x && event.offsetX < INTERFACE_DATA.Ui.offset.x + INTERFACE_DATA.Ui.board_width) {
|
|
|
|
let basis_x = INTERFACE_DATA.Ui.offset.x + halfradius;
|
|
let basis_y = INTERFACE_DATA.Ui.offset.y + (14 * apothem);
|
|
|
|
let x = (event.offsetX - basis_x) / grid_offset_x;
|
|
let y = -(event.offsetY - basis_y) / apothem;
|
|
|
|
let kx = Math.floor(x);
|
|
let ky = Math.floor(y);
|
|
|
|
let apo_offset = Math.abs(MATH.mod(y + (kx & 1), 2.0) - 1);
|
|
let rad_offset = MATH.mod(x, 1);
|
|
let rad_slope = 1 - (hex_slope * apo_offset);
|
|
|
|
let hx = kx + (rad_offset > rad_slope);
|
|
let hy = Math.floor((ky + hx) / 2.0);
|
|
|
|
if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate == 1) {
|
|
hx = 8 - hx;
|
|
hy = 8 - hy;
|
|
}
|
|
|
|
let hex = new MATH.Vec2(hx, hy);
|
|
if(HEX.is_valid_board(hex)) {
|
|
let tile = HEX.hex_to_tile(hex);
|
|
INTERFACE_DATA.hover = new INTERFACE.Selection(0, tile, hex);
|
|
}
|
|
}
|
|
|
|
// Handle pool area
|
|
else if(event.offsetX >= INTERFACE_DATA.Ui.pool_offset && event.offsetX < INTERFACE_DATA.Ui.offset.x + INTERFACE_DATA.Ui.area.x) {
|
|
|
|
let basis_x = INTERFACE_DATA.Ui.pool_offset + halfradius;
|
|
let basis_y = INTERFACE_DATA.Ui.offset.y + (3 * apothem);
|
|
|
|
let x = (event.offsetX - basis_x) / grid_offset_x;
|
|
let y = (event.offsetY - basis_y) / apothem;
|
|
|
|
let kx = Math.floor(x);
|
|
let ky = Math.floor(y);
|
|
|
|
let apo_offset = Math.abs(MATH.mod(y + (kx & 1), 2.0) - 1);
|
|
let rad_offset = MATH.mod(x, 1);
|
|
let rad_slope = 1 - (hex_slope * apo_offset);
|
|
|
|
let hx = kx + (rad_offset > rad_slope);
|
|
let hy = Math.floor((ky + hx) / 2.0);
|
|
|
|
let tile_set = 7;
|
|
if(hy > 3) {
|
|
hy -= 4;
|
|
tile_set = 0;
|
|
}
|
|
|
|
let hex = new MATH.Vec2(hx, hy);
|
|
if(HEX.is_valid_pool(hex)) {
|
|
let tx = (2 * (hx > 0)) + (2 * (hx > 1));
|
|
let tile = tile_set + tx + hy;
|
|
INTERFACE_DATA.hover = new INTERFACE.Selection(1, tile, hex);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(initial_hover != INTERFACE_DATA.hover) { INTERFACE.draw(); }
|
|
},
|
|
|
|
unhover() {
|
|
let redraw = (INTERFACE_DATA.hover !== null);
|
|
INTERFACE_DATA.hover = null;
|
|
if(redraw) { INTERFACE.draw(); }
|
|
},
|
|
|
|
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;
|
|
} 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);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Handle new selection.
|
|
else {
|
|
INTERFACE_DATA.select = null;
|
|
|
|
// 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; }
|
|
|
|
let pool_piece = INTERFACE_DATA.hover.tile % 7;
|
|
|
|
if(GAME_DATA.pools[pool_player].pieces[pool_piece] > 0) {
|
|
INTERFACE_DATA.select = INTERFACE_DATA.hover;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear selection if no tile is hovered.
|
|
else {
|
|
INTERFACE_DATA.select = null;
|
|
}
|
|
|
|
if(initial_select !== INTERFACE_DATA.select) {
|
|
INTERFACE.draw();
|
|
}
|
|
},
|
|
|
|
release() {
|
|
if(INTERFACE_DATA.hover !== null && !INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) {
|
|
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);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
},
|
|
|
|
resize() {
|
|
let width = INTERFACE_DATA.canvas.width = INTERFACE_DATA.canvas.clientWidth;
|
|
let height = INTERFACE_DATA.canvas.height = INTERFACE_DATA.canvas.clientHeight;
|
|
|
|
let margin = INTERFACE_DATA.Ui.margin = Math.floor(Math.min(width, height) / 100);
|
|
|
|
let gui_width = width - (margin * 2);
|
|
let gui_height = height - (margin * 2);
|
|
|
|
if(gui_width < gui_height * INTERFACE.Ratio) {
|
|
gui_height = Math.floor(gui_width / INTERFACE.Ratio);
|
|
} else {
|
|
gui_width = Math.floor(gui_height * INTERFACE.Ratio);
|
|
}
|
|
|
|
let gui_scale = gui_height * INTERFACE.Scale;
|
|
|
|
INTERFACE_DATA.Ui.area.x = gui_width;
|
|
INTERFACE_DATA.Ui.area.y = gui_height;
|
|
INTERFACE_DATA.Ui.scale = gui_scale;
|
|
|
|
INTERFACE_DATA.Ui.offset.x = (width - gui_width) / 2;
|
|
INTERFACE_DATA.Ui.offset.y = (height - gui_height) / 2;
|
|
|
|
INTERFACE_DATA.Ui.board_width = Math.ceil(INTERFACE.BoardWidth * gui_scale);
|
|
INTERFACE_DATA.Ui.pool_offset = INTERFACE_DATA.Ui.offset.x + Math.floor(INTERFACE.PoolOffset * gui_scale);
|
|
},
|
|
|
|
draw() {
|
|
if(INTERFACE_DATA === null) return;
|
|
|
|
INTERFACE.resize();
|
|
INTERFACE.resolve_board();
|
|
|
|
INTERFACE.render();
|
|
},
|
|
|
|
render() {
|
|
let canvas = INTERFACE_DATA.canvas;
|
|
let ctx = INTERFACE_DATA.context;
|
|
|
|
let width = canvas.width;
|
|
let height = canvas.height;
|
|
|
|
let gui_margin = INTERFACE_DATA.Ui.margin;
|
|
let gui_offset = INTERFACE_DATA.Ui.offset;
|
|
let gui_scale = INTERFACE_DATA.Ui.scale;
|
|
|
|
let play = null;
|
|
if(INTERFACE_DATA.replay_turn > 0) {
|
|
play = INTERFACE_DATA.history[INTERFACE_DATA.replay_turn - 1];
|
|
}
|
|
|
|
|
|
// Draw background
|
|
ctx.clearRect(0, 0, width, height);
|
|
ctx.fillStyle = INTERFACE.Color.Background;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
|
|
// Draw tiles
|
|
let radius = INTERFACE.Radius * gui_scale;
|
|
let basis_x = gui_offset.x + radius;
|
|
let basis_y = gui_offset.y + (13 * gui_scale);
|
|
let icon_radius = 0.69 * radius;
|
|
|
|
ctx.lineWidth = Math.min(gui_scale * 0.06, 3);
|
|
|
|
let draw = new INTERFACE.Draw(ctx, gui_scale);
|
|
|
|
for(let i = 0; i < GAME_DATA.board.tiles.length; ++i) {
|
|
let tile = GAME_DATA.board.tiles[i];
|
|
|
|
let is_hover = INTERFACE.Ui.tile_is_hover(0, i);
|
|
let is_select = INTERFACE.Ui.tile_is_select(0, i);
|
|
|
|
let tile_state = INTERFACE_DATA.board_state[i][1];
|
|
let hover_state = INTERFACE_DATA.board_state[i][0];
|
|
|
|
let draw_piece = true;
|
|
if(INTERFACE_DATA.Animate.play !== null) {
|
|
let play = INTERFACE_DATA.Animate.play;
|
|
draw_piece = draw_piece && !(play.source == 0 && (play.from == i || play.to == i));
|
|
draw_piece = draw_piece && !(play.source == 1 && play.to == i);
|
|
}
|
|
|
|
let coord = HEX.tile_to_hex(i);
|
|
if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate == 1) {
|
|
coord.x = 8 - coord.x;
|
|
coord.y = 8 - coord.y;
|
|
}
|
|
|
|
let gui_x = basis_x + (1.5 * radius * coord.x);
|
|
let gui_y = basis_y - (2 * gui_scale * coord.y) + (gui_scale * coord.x);
|
|
|
|
ctx.save();
|
|
ctx.translate(gui_x, gui_y);
|
|
|
|
let piece = null;
|
|
if(tile.piece !== null) { piece = GAME_DATA.board.pieces[tile.piece]; }
|
|
|
|
// Draw border
|
|
ctx.fillStyle = INTERFACE.Color.TileBorder;
|
|
if(draw_piece && tile.piece !== null) {
|
|
if(GAME_DATA.board.pieces[tile.piece].player == GAME.Const.Player.Dawn) { ctx.fillStyle = INTERFACE.Color.DawnDark; }
|
|
else { ctx.fillStyle = INTERFACE.Color.DuskDark; }
|
|
}
|
|
if(is_hover) {
|
|
ctx.fillStyle = INTERFACE.Color.HintHover;
|
|
}
|
|
ctx.beginPath();
|
|
draw.hex();
|
|
ctx.fill();
|
|
|
|
// Draw background.
|
|
// Select indicator color or default to tile color.
|
|
switch(MATH.mod(coord.x + coord.y, 3)) {
|
|
case 0: ctx.fillStyle = INTERFACE.Color.TileMedium; break;
|
|
case 1: ctx.fillStyle = INTERFACE.Color.TileLight; break;
|
|
case 2: ctx.fillStyle = INTERFACE.Color.TileDark; break;
|
|
}
|
|
if(GAME_DATA.turn > 0 && play.source < 2 && (play.to == i || (play.source == 0 && play.from == i))) {
|
|
ctx.fillStyle = INTERFACE.Color.HintPlay;
|
|
} else if(GAME_DATA.state.check != 0 && piece !== null && piece.piece == GAME.Const.PieceId.Omen && piece.player == (GAME_DATA.turn & 1)) {
|
|
ctx.fillStyle = INTERFACE.Color.HintCheck;
|
|
}
|
|
switch(hover_state) {
|
|
case INTERFACE.TileStatus.Valid: ctx.fillStyle = INTERFACE.Color.HintValidDark; break;
|
|
case INTERFACE.TileStatus.Threat: ctx.fillStyle = INTERFACE.Color.HintThreatDark; break;
|
|
case INTERFACE.TileStatus.Invalid: ctx.fillStyle = INTERFACE.Color.HintInvalidDark; break;
|
|
case INTERFACE.TileStatus.Opponent: ctx.fillStyle = INTERFACE.Color.HintOpponentDark; break;
|
|
}
|
|
switch(tile_state) {
|
|
case INTERFACE.TileStatus.Valid: ctx.fillStyle = INTERFACE.Color.HintValid; break;
|
|
case INTERFACE.TileStatus.Threat: ctx.fillStyle = INTERFACE.Color.HintThreat; break;
|
|
case INTERFACE.TileStatus.Invalid: ctx.fillStyle = INTERFACE.Color.HintInvalid; break;
|
|
case INTERFACE.TileStatus.Opponent: ctx.fillStyle = INTERFACE.Color.HintOpponent; break;
|
|
}
|
|
if(is_select) {
|
|
ctx.fillStyle = INTERFACE.Color.HintSelect;
|
|
}
|
|
ctx.beginPath();
|
|
draw.hex(0.94);
|
|
ctx.fill();
|
|
|
|
// Draw tile content
|
|
if(draw_piece && piece !== null) {
|
|
// Draw border hints
|
|
if(!is_hover) { draw.hints(piece); }
|
|
|
|
// Draw piece icon
|
|
if(INTERFACE_DATA.mirror && (piece.player ^ (INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate) != 0) {
|
|
ctx.rotate(Math.PI);
|
|
}
|
|
if(piece.promoted) { ctx.drawImage(GAME_ASSET.Image.Promote, -icon_radius, -icon_radius, icon_radius * 2., icon_radius * 2.); }
|
|
ctx.drawImage(GAME_ASSET.Image.Piece[piece.piece][piece.player], -icon_radius, -icon_radius, icon_radius * 2., icon_radius * 2.);
|
|
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
|
|
ctx.font = Math.ceil(gui_scale / 2) + "px sans-serif";
|
|
|
|
let player_identity = GAME.Const.Player.Dawn;
|
|
let player_color = INTERFACE.Color.Dawn;
|
|
let opponent_color = INTERFACE.Color.Dusk;
|
|
|
|
if(INTERFACE_DATA.player == 1 || (INTERFACE_DATA.player == 2 && INTERFACE_DATA.rotate == 1)) {
|
|
player_identity = GAME.Const.Player.Dusk;
|
|
player_color = INTERFACE.Color.Dusk;
|
|
opponent_color = INTERFACE.Color.Dawn;
|
|
}
|
|
|
|
// Player pool
|
|
draw.pool(
|
|
basis_x + (14 * radius),
|
|
basis_y - gui_scale,
|
|
0,
|
|
player_identity,
|
|
);
|
|
|
|
// Opponent pool
|
|
draw.pool(
|
|
basis_x + (14 * radius),
|
|
basis_y - (9 * gui_scale),
|
|
7,
|
|
player_identity ^ 1,
|
|
);
|
|
|
|
// Draw informational text
|
|
|
|
let handle_pos = [
|
|
new MATH.Vec2(
|
|
basis_x + (radius * 12),
|
|
basis_y - (11 * gui_scale)
|
|
),
|
|
new MATH.Vec2(
|
|
basis_x + (radius * 12),
|
|
basis_y + (3 * gui_scale)
|
|
),
|
|
];
|
|
|
|
// Player handles
|
|
ctx.font = Math.ceil(gui_scale / 1.3) + "px sans-serif";
|
|
|
|
if(INTERFACE_DATA.handles[0] !== null) {
|
|
let pos = handle_pos[(1 ^ INTERFACE_DATA.player ^ INTERFACE_DATA.rotate) & 1];
|
|
|
|
ctx.fillStyle = INTERFACE.Color.Dawn;
|
|
ctx.textBaseline = "middle";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText(INTERFACE_DATA.handles[0], pos.x, pos.y);
|
|
}
|
|
|
|
if(INTERFACE_DATA.handles[1] !== null) {
|
|
let pos = handle_pos[(INTERFACE_DATA.player ^ INTERFACE_DATA.rotate) & 1];
|
|
|
|
ctx.fillStyle = INTERFACE.Color.Dusk;
|
|
ctx.textBaseline = "middle";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText(INTERFACE_DATA.handles[1], pos.x, pos.y);
|
|
}
|
|
|
|
// Tile information
|
|
ctx.font = Math.ceil(gui_scale / 2) + "px sans-serif";
|
|
|
|
if(INTERFACE_DATA.hover !== null) {
|
|
let text = "";
|
|
if(INTERFACE_DATA.hover.source == 0) {
|
|
text = INTERFACE.Ui.hex_to_alnum(INTERFACE_DATA.hover.hex);
|
|
let piece_id = GAME_DATA.board.tiles[INTERFACE_DATA.hover.tile].piece;
|
|
if(piece_id !== null) {
|
|
let piece = GAME_DATA.board.pieces[piece_id];
|
|
text += " " + LANG(GAME.Const.Piece[piece.piece].name);
|
|
if(piece.promoted) { text += "+"; }
|
|
}
|
|
} else {
|
|
text = " " + GAME.Const.Piece[INTERFACE_DATA.hover.tile % 7].name;
|
|
}
|
|
|
|
ctx.fillStyle = INTERFACE.Color.Text;
|
|
ctx.textBaseline = "top";
|
|
ctx.textAlign = "left";
|
|
ctx.fillText(text, gui_margin, gui_margin);
|
|
}
|
|
|
|
// Number of moves
|
|
ctx.fillStyle = INTERFACE.Color.Text;
|
|
ctx.textBaseline = "top";
|
|
ctx.textAlign = "right";
|
|
ctx.fillText(GAME_DATA.turn, width - gui_margin, gui_margin);
|
|
|
|
// Game state message
|
|
let message = null;
|
|
ctx.fillStyle = INTERFACE.Color.Text;
|
|
|
|
if(INTERFACE_DATA.auto_mode !== null) {
|
|
switch(INTERFACE_DATA.auto_mode) {
|
|
case 0: message = LANG("auto") + " " + LANG("dawn"); break;
|
|
case 1: message = LANG("auto") + " " + LANG("dusk"); break;
|
|
}
|
|
}
|
|
|
|
if(GAME_DATA.state.code == GAME.Const.State.Resign) {
|
|
ctx.fillStyle = INTERFACE.Color.HintCheck;
|
|
message = LANG("resign");
|
|
} else if(GAME_DATA.state.check != 0) {
|
|
ctx.fillStyle = INTERFACE.Color.HintCheck;
|
|
if(GAME_DATA.state.checkmate) {
|
|
message = LANG("checkmate");
|
|
} else {
|
|
message = LANG("check");
|
|
}
|
|
}
|
|
|
|
if(message !== null) {
|
|
ctx.textBaseline = "bottom";
|
|
ctx.textAlign = "left";
|
|
ctx.fillText(message, gui_margin, height - gui_margin);
|
|
}
|
|
|
|
if(INTERFACE_DATA.Animate.play !== null) {
|
|
let time = Math.min(1 - (INTERFACE_DATA.Animate.time - Date.now()) / 1000, 1);
|
|
time = time * time;
|
|
let play = INTERFACE_DATA.Animate.play;
|
|
|
|
let piece = INTERFACE_DATA.Animate.piece;
|
|
let target = INTERFACE_DATA.Animate.target;
|
|
|
|
// Get to and from coordinates.
|
|
let coord_to = HEX.tile_to_hex(play.to);
|
|
if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate == 1) {
|
|
coord_to.x = 8 - coord_to.x;
|
|
coord_to.y = 8 - coord_to.y;
|
|
}
|
|
let to_x = basis_x + (1.5 * radius * coord_to.x);
|
|
let to_y = basis_y - (2 * gui_scale * coord_to.y) + (gui_scale * coord_to.x);
|
|
|
|
let from_x = 0;
|
|
let from_y = 0;
|
|
switch(play.source) {
|
|
// Lerp between board positions.
|
|
case 0: {
|
|
let coord_from = HEX.tile_to_hex(play.from);
|
|
if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate == 1) {
|
|
coord_from.x = 8 - coord_from.x;
|
|
coord_from.y = 8 - coord_from.y;
|
|
}
|
|
from_x = basis_x + (1.5 * radius * coord_from.x);
|
|
from_y = basis_y - (2 * gui_scale * coord_from.y) + (gui_scale * coord_from.x);
|
|
} break;
|
|
|
|
// Lerp between pool and board positions.
|
|
case 1: {
|
|
from_x = to_x;
|
|
from_y = to_y - (gui_scale / 1.5);
|
|
|
|
switch(piece.player) {
|
|
case 0: ctx.fillStyle = INTERFACE.Color.DawnMedium; break;
|
|
case 1: ctx.fillStyle = INTERFACE.Color.DuskMedium; break;
|
|
}
|
|
//ctx.fillStyle = INTERFACE.Color.AnimateShadow;
|
|
|
|
ctx.save();
|
|
ctx.translate(to_x, to_y);
|
|
ctx.beginPath();
|
|
draw.hex(MATH.lerp(1.2, 0.94, time));
|
|
ctx.fill();
|
|
ctx.restore();
|
|
} break;
|
|
}
|
|
|
|
// Get animation coordinates.
|
|
let x = MATH.lerp(from_x, to_x, time);
|
|
let y = MATH.lerp(from_y, to_y, time);
|
|
|
|
// Draw target piece.
|
|
if(target !== null) {
|
|
if(target.player == piece.player) {
|
|
let x = MATH.lerp(to_x, from_x, time);
|
|
let y = MATH.lerp(to_y, from_y, time);
|
|
draw.animation_piece(target, x, y);
|
|
} else {
|
|
draw.animation_piece(target, to_x, to_y);
|
|
}
|
|
}
|
|
|
|
// Draw moving piece.
|
|
draw.animation_piece(piece, x, y);
|
|
|
|
if(Date.now() >= INTERFACE_DATA.Animate.time) {
|
|
INTERFACE_DATA.Animate.play = null;
|
|
INTERFACE_DATA.Animate.time = 0;
|
|
}
|
|
|
|
setTimeout(INTERFACE.render, 1000 / 30);
|
|
}
|
|
},
|
|
|
|
Draw: class {
|
|
constructor(ctx, scale) {
|
|
this.ctx = ctx;
|
|
this.scale = scale;
|
|
}
|
|
|
|
hex(scale=1) {
|
|
scale *= INTERFACE.TileScale * this.scale;
|
|
this.ctx.moveTo(INTERFACE.HexVertex[5].x * scale, INTERFACE.HexVertex[5].y * scale);
|
|
for(let i = 0; i < INTERFACE.HexVertex.length; ++i) {
|
|
this.ctx.lineTo(INTERFACE.HexVertex[i].x * scale, INTERFACE.HexVertex[i].y * scale);
|
|
}
|
|
}
|
|
|
|
hints(piece) {
|
|
let scale = INTERFACE.TileScale * this.scale;
|
|
|
|
let movement = piece.moves();
|
|
if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate) {
|
|
movement = movement.rotate();
|
|
}
|
|
let moves = movement.direction;
|
|
|
|
for(let mask = BITWISE.lsb(moves); moves > 0; mask = BITWISE.lsb(moves)) {
|
|
let move = BITWISE.ffs(mask);
|
|
let nmove = move % 12;
|
|
|
|
this.ctx.strokeStyle = (piece.player == GAME.Const.Player.Dawn)? INTERFACE.Color.Dawn : INTERFACE.Color.Dusk;
|
|
this.ctx.beginPath();
|
|
|
|
// draw edge marker
|
|
if(nmove < 6) {
|
|
let fr = INTERFACE.HexVertex[nmove];
|
|
let to = INTERFACE.HexVertex[(nmove + 1) % 6];
|
|
|
|
let dx = (to.x - fr.x) / 3.0;
|
|
let dy = (to.y - fr.y) / 3.0;
|
|
|
|
let fqx = fr.x + dx;
|
|
let fqy = fr.y + dy;
|
|
|
|
let tqx = fr.x + (2 * dx);
|
|
let tqy = fr.y + (2 * dy);
|
|
|
|
this.ctx.moveTo(fqx * scale, fqy * scale);
|
|
this.ctx.lineTo(tqx * scale, tqy * scale);
|
|
this.ctx.stroke();
|
|
}
|
|
|
|
// draw vertex marker
|
|
else {
|
|
let fr = INTERFACE.HexVertex[nmove % 6];
|
|
let mid = INTERFACE.HexVertex[(nmove + 1) % 6];
|
|
let to = INTERFACE.HexVertex[(nmove + 2) % 6];
|
|
|
|
let dx1 = (mid.x - fr.x) / 3.0;
|
|
let dy1 = (mid.y - fr.y) / 3.0;
|
|
|
|
let dx2 = (to.x - mid.x) / 3.0;
|
|
let dy2 = (to.y - mid.y) / 3.0;
|
|
|
|
let fqx = mid.x - dx1;
|
|
let fqy = mid.y - dy1;
|
|
|
|
let tqx = mid.x + dx2;
|
|
let tqy = mid.y + dy2;
|
|
|
|
this.ctx.moveTo(fqx * scale, fqy * scale);
|
|
this.ctx.lineTo(mid.x * scale, mid.y * scale);
|
|
this.ctx.lineTo(tqx * scale, tqy * scale);
|
|
this.ctx.stroke();
|
|
}
|
|
moves &= ~mask;
|
|
}
|
|
}
|
|
|
|
animation_piece(piece, x, y) {
|
|
let radius = INTERFACE.Radius * this.scale;
|
|
let icon_radius = 0.69 * radius;
|
|
|
|
this.ctx.save();
|
|
this.ctx.translate(x, y);
|
|
|
|
// Draw border
|
|
if(piece.player == GAME.Const.Player.Dawn) { this.ctx.fillStyle = INTERFACE.Color.DawnDark; }
|
|
else { this.ctx.fillStyle = INTERFACE.Color.DuskDark; }
|
|
|
|
this.ctx.beginPath();
|
|
this.hex();
|
|
this.ctx.fill();
|
|
|
|
// Draw background.
|
|
this.ctx.fillStyle = INTERFACE.Color.HintPlay;
|
|
this.ctx.beginPath();
|
|
this.hex(0.94);
|
|
this.ctx.fill();
|
|
|
|
// Draw tile content
|
|
if(piece !== null) {
|
|
// Draw border hints
|
|
this.hints(piece);
|
|
|
|
// Draw piece icon
|
|
if(INTERFACE_DATA.mirror && (piece.player ^ (INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate) != 0) {
|
|
this.ctx.rotate(Math.PI);
|
|
}
|
|
if(piece.promoted) { this.ctx.drawImage(GAME_ASSET.Image.Promote, -icon_radius, -icon_radius, icon_radius * 2., icon_radius * 2.); }
|
|
this.ctx.drawImage(GAME_ASSET.Image.Piece[piece.piece][piece.player], -icon_radius, -icon_radius, icon_radius * 2., icon_radius * 2.);
|
|
|
|
}
|
|
|
|
this.ctx.restore();
|
|
}
|
|
|
|
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 ix = +(i > 1) + (i > 4);
|
|
let iy = i - ((i > 1) * 3) - ((i > 4) * 2) + (0.5 * (ix == 1));
|
|
|
|
let gui_x = x + (1.5 * radius * ix);
|
|
let gui_y = y + (2 * this.scale * iy);
|
|
|
|
let player_color = INTERFACE.Color.Dawn;
|
|
if(player == 1) { player_color = INTERFACE.Color.Dusk; }
|
|
|
|
this.ctx.save();
|
|
this.ctx.translate(gui_x, gui_y);
|
|
|
|
// 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))) {
|
|
if(is_hover) { this.ctx.fillStyle = INTERFACE.Color.HintHover; }
|
|
else { this.ctx.fillStyle = player_color; }
|
|
this.ctx.beginPath();
|
|
this.hex();
|
|
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; }
|
|
this.ctx.beginPath();
|
|
this.hex(0.94);
|
|
this.ctx.fill();
|
|
|
|
// Draw image
|
|
this.ctx.drawImage(GAME_ASSET.Image.Piece[i][player], -icon_radius * 0.55, -icon_radius * 0.8, icon_radius * 1.6, icon_radius * 1.6);
|
|
|
|
// Draw count
|
|
this.ctx.fillStyle = player_color;
|
|
this.ctx.textBaseline = "middle";
|
|
this.ctx.textAlign = "left";
|
|
this.ctx.fillText(GAME_DATA.pools[player].pieces[i], -0.6 * radius, 0);
|
|
|
|
this.ctx.restore();
|
|
}
|
|
}
|
|
},
|
|
|
|
Ui: {
|
|
tile_is_hover(source, tile) {
|
|
return INTERFACE_DATA.hover !== null
|
|
&& INTERFACE_DATA.hover.source == source
|
|
&& INTERFACE_DATA.hover.tile == tile;
|
|
},
|
|
|
|
tile_is_select(source, tile) {
|
|
return INTERFACE_DATA.select !== null
|
|
&& INTERFACE_DATA.select.source == source
|
|
&& INTERFACE_DATA.select.tile == tile;
|
|
},
|
|
|
|
hex_to_alnum(hex) {
|
|
return String.fromCharCode(65 + hex.x) + (hex.y + 1);
|
|
},
|
|
|
|
match_select(a, b) {
|
|
if(a !== null && b !== null) {
|
|
return (
|
|
a.source == b.source
|
|
&& a.tile == b.tile
|
|
&& a.hex.x == b.hex.x
|
|
&& a.hex.y == b.hex.y
|
|
);
|
|
}
|
|
return false;
|
|
},
|
|
},
|
|
|
|
init(token, mode) {
|
|
GAME.init();
|
|
|
|
let player = 2;
|
|
if(mode == INTERFACE.Mode.Local) { player = 0; }
|
|
|
|
let history = [ ];
|
|
let dawn = null;
|
|
let dusk = null;
|
|
|
|
INTERFACE_DATA = {
|
|
mode: mode,
|
|
token: token,
|
|
|
|
canvas: document.getElementById("game"),
|
|
context: null,
|
|
|
|
player: player,
|
|
rotate: 0,
|
|
mirror: false,
|
|
|
|
hover: null,
|
|
select: null,
|
|
|
|
handles: [dawn, dusk],
|
|
board_state: [ ],
|
|
resign:false,
|
|
resign_warn:false,
|
|
|
|
history: history,
|
|
|
|
replay_turn: 0,
|
|
replay_auto: false,
|
|
auto_mode: null,
|
|
|
|
Ui: {
|
|
scale: 0,
|
|
margin: 0,
|
|
offset: new MATH.Vec2(),
|
|
area: new MATH.Vec2(),
|
|
|
|
board_width: 0,
|
|
pool_offset: 0,
|
|
},
|
|
|
|
Animate: {
|
|
play: null,
|
|
piece: null,
|
|
target: null,
|
|
time: 0,
|
|
},
|
|
};
|
|
for(let i = 0; i < 61; ++i) { INTERFACE_DATA.board_state.push([0, 0]); }
|
|
|
|
let canvas = INTERFACE_DATA.canvas;
|
|
if(canvas !== undefined) {
|
|
INTERFACE_DATA.context = canvas.getContext("2d");
|
|
|
|
canvas.addEventListener("mousemove", INTERFACE.hover);
|
|
canvas.addEventListener("mouseout", INTERFACE.unhover);
|
|
canvas.addEventListener("mousedown", INTERFACE.click);
|
|
canvas.addEventListener("mouseup", INTERFACE.release);
|
|
window.addEventListener("resize", INTERFACE.draw);
|
|
|
|
switch(INTERFACE_DATA.mode) {
|
|
case INTERFACE.Mode.Local: {
|
|
INTERFACE.draw();
|
|
} break;
|
|
}
|
|
} else {
|
|
LOAD(SCENES.Browse);
|
|
}
|
|
},
|
|
|
|
uninit() {
|
|
if(INTERFACE_DATA !== null) {
|
|
if(INTERFACE_DATA.mode != INTERFACE.Mode.Local) {
|
|
MESSAGE_COMPOSE([
|
|
PACK.u16(OpCode.SessionLeave),
|
|
]);
|
|
}
|
|
|
|
MAIN.removeChild(INTERFACE_DATA.canvas);
|
|
INTERFACE_DATA = null;
|
|
}
|
|
},
|
|
|
|
reset() {
|
|
GAME.init();
|
|
INTERFACE_DATA.player = 0;
|
|
INTERFACE_DATA.rotate = 0;
|
|
INTERFACE_DATA.auto_mode = null;
|
|
INTERFACE.draw();
|
|
},
|
|
|
|
message(code, data) {
|
|
if(data === null) { return; }
|
|
|
|
switch(code) {
|
|
|
|
case OpCode.SessionView: {
|
|
if(data.status != Status.Ok) {
|
|
LOAD(SCENES.Browse);
|
|
}
|
|
|
|
switch(INTERFACE_DATA.mode) {
|
|
case INTERFACE.Mode.Review:
|
|
case INTERFACE.Mode.Player: {
|
|
MESSAGE_COMPOSE([
|
|
PACK.u16(OpCode.GameState),
|
|
INTERFACE_DATA.token,
|
|
]);
|
|
} break;
|
|
}
|
|
} break;
|
|
|
|
case OpCode.GameState: {
|
|
if(INTERFACE_DATA.mode == INTERFACE.Mode.Player) {
|
|
INTERFACE_DATA.player = data.player;
|
|
}
|
|
|
|
INTERFACE_DATA.history = data.history;
|
|
let turn = INTERFACE_DATA.history.length;
|
|
|
|
if(INTERFACE_DATA.history.length > 0) {
|
|
if(INTERFACE_DATA.history[INTERFACE_DATA.history.length-1].source == 2) {
|
|
turn = 0;
|
|
}
|
|
}
|
|
|
|
if(data.dawn.length > 0) { INTERFACE_DATA.handles[0] = data.dawn; }
|
|
if(data.dusk.length > 0) { INTERFACE_DATA.handles[1] = data.dusk; }
|
|
|
|
if(INTERFACE_DATA.mode == INTERFACE.Mode.Review) {
|
|
document.getElementById("indicator-turn").innerText = INTERFACE_DATA.replay_turn + " / " + INTERFACE_DATA.history.length;
|
|
document.getElementById("turn-slider").setAttribute("max", INTERFACE_DATA.history.length);
|
|
}
|
|
|
|
INTERFACE.replay_jump(turn);
|
|
} break;
|
|
|
|
case OpCode.GamePlay: {
|
|
if(data.status == Status.Ok && data.turn == INTERFACE_DATA.history.length) {
|
|
INTERFACE.history_push(data.play, true);
|
|
}
|
|
} break;
|
|
}
|
|
},
|
|
|
|
process(play) {
|
|
// Send message to server for online game.
|
|
switch(INTERFACE_DATA.mode) {
|
|
// Apply action and change turn for local game.
|
|
case INTERFACE.Mode.Local: {
|
|
INTERFACE.history_push(play, true);
|
|
|
|
INTERFACE_DATA.player = +(!INTERFACE_DATA.player);
|
|
INTERFACE_DATA.rotate = +(!INTERFACE_DATA.rotate);
|
|
|
|
INTERFACE.draw();
|
|
|
|
if(INTERFACE_DATA.auto_mode !== null && INTERFACE_DATA.auto_mode == (GAME_DATA.turn & 1)) {
|
|
setTimeout(INTERFACE.auto_play, 1000);
|
|
}
|
|
} break;
|
|
|
|
// Send action to server for validation.
|
|
case INTERFACE.Mode.Player: {
|
|
let move_data = play.source | (play.from << 1) | (play.to << 7);
|
|
MESSAGE_COMPOSE([
|
|
PACK.u16(OpCode.GamePlay),
|
|
PACK.u16(0),
|
|
PACK.u16(GAME_DATA.turn),
|
|
PACK.u16(move_data),
|
|
]);
|
|
} break;
|
|
}
|
|
},
|
|
|
|
rotate() {
|
|
INTERFACE_DATA.rotate ^= 1;
|
|
INTERFACE.draw();
|
|
},
|
|
|
|
mirror() {
|
|
INTERFACE_DATA.mirror = !INTERFACE_DATA.mirror;
|
|
INTERFACE.draw();
|
|
},
|
|
|
|
resign() {
|
|
if(INTERFACE_DATA.mode == INTERFACE.Mode.Player) {
|
|
let button_resign = document.getElementById("button-resign");
|
|
|
|
if(INTERFACE_DATA.resign_warn) {
|
|
INTERFACE.resign_reset();
|
|
MESSAGE_COMPOSE([
|
|
PACK.u16(OpCode.SessionResign),
|
|
INTERFACE_DATA.token,
|
|
]);
|
|
} else {
|
|
INTERFACE_DATA.resign_warn = true;
|
|
button_resign.innerText = "Confirm?";
|
|
button_resign.setAttribute("class", "warn");
|
|
setTimeout(INTERFACE.resign_reset, 3_000);
|
|
}
|
|
}
|
|
},
|
|
|
|
resign_reset() {
|
|
if(INTERFACE_DATA.resign_warn) {
|
|
INTERFACE_DATA.resign_warn = false;
|
|
|
|
// Reset resign button
|
|
let button_resign = document.getElementById("button-resign");
|
|
button_resign.innerText = "Resign";
|
|
button_resign.removeAttribute("class");
|
|
}
|
|
},
|
|
|
|
history_push(play, animate=false) {
|
|
INTERFACE_DATA.history.push(play);
|
|
if(INTERFACE_DATA.mode == INTERFACE.Mode.Review) {
|
|
document.getElementById("indicator-turn").innerText = INTERFACE_DATA.replay_turn + " / " + INTERFACE_DATA.history.length;
|
|
document.getElementById("turn-slider").setAttribute("max", INTERFACE_DATA.history.length);
|
|
}
|
|
if(INTERFACE_DATA.replay_turn == INTERFACE_DATA.history.length - 1) {
|
|
INTERFACE.replay_next(animate);
|
|
}
|
|
},
|
|
|
|
replay_jump(turn, animate=false) {
|
|
turn = +turn;
|
|
|
|
if(turn >= 0 && turn <= INTERFACE_DATA.history.length) {
|
|
if(turn < INTERFACE_DATA.replay_turn) {
|
|
INTERFACE_DATA.replay_turn = 0;
|
|
GAME.init();
|
|
}
|
|
|
|
let play = null;
|
|
let piece = null;
|
|
let target = null;
|
|
while(INTERFACE_DATA.replay_turn < turn) {
|
|
if(INTERFACE_DATA.history[INTERFACE_DATA.replay_turn].source < 2) {
|
|
play = INTERFACE_DATA.history[INTERFACE_DATA.replay_turn];
|
|
|
|
if(play.source == 0) {
|
|
let piece_id = GAME_DATA.board.tiles[play.from].piece;
|
|
if(piece_id !== null) { piece = GAME_DATA.board.pieces[piece_id].clone(); }
|
|
|
|
piece_id = GAME_DATA.board.tiles[play.to].piece;
|
|
if(piece_id !== null) { target = GAME_DATA.board.pieces[piece_id].clone(); }
|
|
else { target = null; }
|
|
}
|
|
|
|
GAME_DATA.process(play);
|
|
|
|
if(play.source == 1) {
|
|
let piece_id = GAME_DATA.board.tiles[play.to].piece;
|
|
if(piece_id !== null) { piece = GAME_DATA.board.pieces[piece_id].clone(); }
|
|
target = null;
|
|
}
|
|
} else {
|
|
GAME_DATA.process(play);
|
|
}
|
|
INTERFACE_DATA.replay_turn++;
|
|
}
|
|
if(animate) {
|
|
INTERFACE_DATA.Animate.time = Date.now() + 500;
|
|
INTERFACE_DATA.Animate.play = play;
|
|
INTERFACE_DATA.Animate.piece = piece;
|
|
INTERFACE_DATA.Animate.target = target;
|
|
} else {
|
|
INTERFACE_DATA.Animate.play = null;
|
|
}
|
|
INTERFACE_DATA.replay_turn = turn;
|
|
|
|
if(INTERFACE_DATA.mode == INTERFACE.Mode.Review) {
|
|
document.getElementById("indicator-turn").innerText = INTERFACE_DATA.replay_turn + " / " + INTERFACE_DATA.history.length;
|
|
document.getElementById("turn-slider").value = INTERFACE_DATA.replay_turn;
|
|
}
|
|
|
|
INTERFACE.draw();
|
|
}
|
|
},
|
|
replay_first() {
|
|
INTERFACE.replay_jump(0);
|
|
},
|
|
replay_last(animate=false) {
|
|
INTERFACE.replay_jump(INTERFACE_DATA.history.length, animate);
|
|
},
|
|
replay_prev(animate=false) {
|
|
INTERFACE.replay_jump(INTERFACE_DATA.replay_turn - 1, animate);
|
|
},
|
|
replay_next(animate=false) {
|
|
INTERFACE.replay_jump(INTERFACE_DATA.replay_turn + 1, animate);
|
|
},
|
|
replay_toggle_auto() {
|
|
INTERFACE_DATA.replay_auto = !INTERFACE_DATA.replay_auto;
|
|
if(INTERFACE_DATA.replay_auto) {
|
|
setTimeout(INTERFACE.replay_auto, 1000);
|
|
}
|
|
},
|
|
replay_auto() {
|
|
if(INTERFACE_DATA.replay_auto) {
|
|
INTERFACE.replay_jump(INTERFACE_DATA.replay_turn + 1, true);
|
|
setTimeout(INTERFACE.replay_auto, 1100);
|
|
}
|
|
},
|
|
replay_off() {
|
|
INTERFACE_DATA.replay_auto = false;
|
|
},
|
|
|
|
|
|
auto() {
|
|
if(INTERFACE_DATA.auto_mode === null) {
|
|
INTERFACE_DATA.auto_mode = (GAME_DATA.turn & 1) ^ INTERFACE_DATA.rotate ^ 1;
|
|
setTimeout(INTERFACE.auto_play, 500);
|
|
} else {
|
|
INTERFACE_DATA.auto_mode = null;
|
|
}
|
|
INTERFACE.draw();
|
|
},
|
|
|
|
auto_play() {
|
|
if(INTERFACE_DATA.auto_mode !== (GAME_DATA.turn & 1) && !GAME_DATA.state.checkmate) { return; }
|
|
|
|
function state_score(state, player) {
|
|
let score = 0;
|
|
let opponent = player ^ 1;
|
|
let turn = (state.turn & 1);
|
|
|
|
for(let i = 0; i < state.board.tiles.length; ++i) {
|
|
let tile = state.board.tiles[i];
|
|
score += Math.floor((tile.threaten[player] - tile.threaten[opponent]) / 2);
|
|
}
|
|
|
|
for(let i = 0; i < state.board.pieces.length; ++i) {
|
|
let piece_id = state.board.pieces[i];
|
|
if(piece_id !== null) {
|
|
let piece = state.board.pieces[i];
|
|
let tile = state.board.tiles[piece.tile];
|
|
let hex = HEX.tile_to_hex(tile);
|
|
|
|
let piece_score = 3 + (4 * (piece.piece + piece.promoted));
|
|
|
|
if(piece.player == player) {
|
|
if(tile.threaten[opponent] == 0) { score += piece_score; }
|
|
else { score -= piece_score; }
|
|
if(hex.y == state.board.columns[hex.x].extent[player]) {
|
|
score += piece_score * tile.threaten[player];
|
|
}
|
|
} else {
|
|
score -= piece_score - 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
for(let i = 0; i < state.pools[player].pieces.length; ++i) {
|
|
score += 3 * (2 + i) * state.pools[player].pieces[i];
|
|
}
|
|
for(let i = 0; i < state.pools[opponent].pieces.length; ++i) {
|
|
score -= 2 * (1 + i) * state.pools[opponent].pieces[i];
|
|
}
|
|
|
|
for(let i = 0; i < state.board.columns.length; ++i) {
|
|
let column = state.board.columns[i];
|
|
let extent_score = 0;
|
|
if(player == 0) {
|
|
extent_score += column.extent[player];
|
|
extent_score -= 8 - column.extent[opponent];
|
|
} else {
|
|
extent_score += 8 - column.extent[player];
|
|
extent_score -= column.extent[opponent];
|
|
}
|
|
score += Math.floor(extent_score / 3);
|
|
}
|
|
|
|
if(state.state.check != 0) {
|
|
if(turn == player) { score -= 20; }
|
|
else { score += 1; }
|
|
}
|
|
if(state.state.checkmate) {
|
|
if(turn == player) { score -= 1000; }
|
|
else { score += 1000; }
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
function determine_play(state, search_player, depth) {
|
|
let moves = [ ];
|
|
let player = state.turn & 1;
|
|
|
|
// Get available placement moves.
|
|
for(let p = 0; p < 8; ++p) {
|
|
if(state.pools[player].pieces[p] > 0) {
|
|
for(let move of state.placement_tiles(p, player)) {
|
|
if(move.valid) {
|
|
moves.push({
|
|
score: 0,
|
|
play: new GAME.Play(1, p, move.tile),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get available piece moves.
|
|
for(let i = 0; i < state.board.pieces.length; ++i) {
|
|
let piece = state.board.pieces[i];
|
|
if(piece !== null) {
|
|
if(piece.player == player) {
|
|
for(let move of state.movement_tiles(piece, piece.tile)) {
|
|
if(move.valid) {
|
|
moves.push({
|
|
score: 0,
|
|
play: new GAME.Play(0, piece.tile, move.tile),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get move scores.
|
|
for(let i = 0; i < moves.length; ++i) {
|
|
let st = state.clone();
|
|
st.process(moves[i].play);
|
|
moves[i].score = state_score(st, player);
|
|
}
|
|
|
|
// Select move.
|
|
if(moves.length > 0) {
|
|
moves.sort((a, b) => { return b.score - a.score });
|
|
|
|
if(depth == 0) {
|
|
// Get move scores for search player.
|
|
for(let i = 0; i < moves.length; ++i) {
|
|
let st = state.clone();
|
|
st.process(moves[i].play);
|
|
moves[i].score = state_score(st, search_player);
|
|
}
|
|
} else {
|
|
for(let i = 0; i < moves.length && i < Math.ceil(Math.log2(moves.length)); ++i) {
|
|
let st = state.clone();
|
|
st.process(moves[i].play);
|
|
|
|
let result = determine_play(st, search_player, depth - 1);
|
|
if(result !== null) {
|
|
moves[i].score = result.score;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Select random from ties.
|
|
let selection = 0;
|
|
for(let i = 1; i < moves.length; ++i) {
|
|
if(moves[i].score + 5 >= moves[i-1].score) {
|
|
selection++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return moves[Math.floor(Math.random() * selection)];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
let result = determine_play(GAME_DATA, GAME_DATA.turn & 1, 1);
|
|
if(result !== null) {
|
|
INTERFACE.process(result.play);
|
|
}
|
|
},
|
|
};
|
|
|
|
INTERFACE.TileScale = 0.9;
|
|
|
|
INTERFACE.Radius = 2.0 / Math.sqrt(3.0);
|
|
INTERFACE.HalfRadius = 1.0 / Math.sqrt(3.0);
|
|
INTERFACE.Scale = 1 / 18;
|
|
INTERFACE.Ratio = (19 * INTERFACE.Radius) * INTERFACE.Scale;
|
|
INTERFACE.HexVertex = [
|
|
// top face
|
|
new MATH.Vec2(-INTERFACE.HalfRadius, -1),
|
|
// top-right face
|
|
new MATH.Vec2(INTERFACE.HalfRadius, -1),
|
|
// bottom-right face
|
|
new MATH.Vec2(INTERFACE.Radius, 0),
|
|
// bottom face
|
|
new MATH.Vec2(INTERFACE.HalfRadius, 1),
|
|
// bottom-left face
|
|
new MATH.Vec2(-INTERFACE.HalfRadius, 1),
|
|
// top-left face
|
|
new MATH.Vec2(-INTERFACE.Radius, 0),
|
|
];
|
|
|
|
INTERFACE.BoardWidth = INTERFACE.Radius * 14;
|
|
INTERFACE.PoolOffset = INTERFACE.BoardWidth;
|