dzura/www/js/interface.js
2025-02-03 17:01:24 -08:00

2171 lines
80 KiB
JavaScript

let INTERFACE_DATA = null;
function INTERFACE_STYLE(id) {
switch(id) {
case 0: return {
Background: "#101010",
Text: "#c0c0c0",
TextDark: "#848484",
TileBorder: "#606060",
TileLight: "#383838",
TileMedium: "#242424",
TileDark: "#101010",
Promote: "#a52121",
Dawn: "#ffe082",
DawnShade: "#a59154",
DawnMedium: "#fca03f",
DawnDark: "#ff6d00",
DawnDarkest: "#4c3422",
Dusk: "#f6a1bd",
DuskShade: "#a56b7f",
DuskMedium: "#e84a79",
DuskDark: "#c51162",
DuskDarkest: "#4c2235",
HintHover: "#b8f1fc",
HintSelect: "#4a148c",
HintSelectLight: "#cda6fc",
HintValid: "#1d268c",
HintValidLight: "#6e9de5",
HintValidTint: "#3786fc",
HintValidDark: "#151c66",
HintThreat: "#054719",
HintThreatDark: "#023311",
HintOpponent: "#49136b",
HintOpponentDark: "#2a0b3f",
HintInvalid: "#660d0d",
HintInvalidLight: "#fc5050",
HintInvalidTint: "#fc3737",
HintInvalidDark: "#4c0909",
HintPlay: "#307c7f",
HintCheck: "#C62828",
};
case 1: return {
Background: "rgba(0,0,0,0)",
Text: "#222222",
TextDark: "#444444",
TileBorder: "#444444",
TileLight: "#ffffff",
TileMedium: "#ffffff",
TileDark: "#ffffff",
Promote: "#888888",
Dawn: "#ff6d00",
DawnShade: "#ffffff",
DawnMedium: "#ffffff",
DawnDark: "#444444",
DawnDarkest: "#ffffff",
Dusk: "#c51162",
DuskShade: "#ffffff",
DuskMedium: "#ffffff",
DuskDark: "#444444",
DuskDarkest: "#ffffff",
HintHover: "#222222",
HintSelect: "#ffffff",
HintSelectLight: "#444444",
HintValid: "#AAAAAA",
HintValidLight: "#444444",
HintValidTint: "#444444",
HintValidDark: "#DDDDDD",
HintThreat: "#ffffff",
HintThreatDark: "#000000",
HintOpponent: "#ffffff",
HintOpponentDark: "#000000",
HintInvalid: "#ffffff",
HintInvalidLight: "#444444",
HintInvalidTint: "#444444",
HintInvalidDark: "#DDDDDD",
HintPlay: "#ffffff",
HintCheck: "#ffffff",
};
}
}
const INTERFACE = {
Mode: {
Local: 0,
Player: 1,
Review: 2,
},
Config: {
DisablePool: false,
DisableHint: false,
DisableNumbers: false,
DisableText: false,
DisableInvalid: false,
},
Color: INTERFACE_STYLE(0),
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.Game.board_state.length; ++i) {
INTERFACE_DATA.Game.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;
if(piece.moves().alt && INTERFACE_DATA.alt_mode) {
movements = GAME_DATA.movement_tiles_alt(piece);
} else {
movements = GAME_DATA.movement_tiles(piece, selection.tile);
}
}
} else {
player = Math.floor(selection.tile / 7);
player ^= INTERFACE_DATA.player & 1;
if(INTERFACE_DATA.player == 2) { player ^= INTERFACE_DATA.rotate; }
let select_alt = INTERFACE.selection_has_alt(INTERFACE_DATA.select);
if(select_alt !== null) {
movements = GAME_DATA.movement_tiles_alt(select_alt);
} else {
movements = GAME_DATA.placement_tiles(selection.tile % 7, player);
}
}
if(movements !== null) {
// 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) {
INTERFACE_DATA.Game.board_state[movement.tile][zone] = INTERFACE.TileStatus.Valid;
/*if(GAME_DATA.board.tiles[movement.tile].threaten[+(!player)] > 0) {
INTERFACE_DATA.Game.board_state[movement.tile][zone] = INTERFACE.TileStatus.Threat;
} else {
INTERFACE_DATA.Game.board_state[movement.tile][zone] = INTERFACE.TileStatus.Valid;
}*/
}
else {
INTERFACE_DATA.Game.board_state[movement.tile][zone] = INTERFACE.TileStatus.Opponent;
}
} else {
INTERFACE_DATA.Game.board_state[movement.tile][zone] = INTERFACE.TileStatus.Invalid;
}
}
}
},
hover(event) {
let initial_hover = INTERFACE_DATA.hover;
let apothem = INTERFACE_DATA.Render.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;
let mouse_x = event.offsetX * (window.devicePixelRatio || 1);
let mouse_y = event.offsetY * (window.devicePixelRatio || 1);
// Handle board area
if(mouse_y >= INTERFACE_DATA.Render.margin.t && mouse_y < INTERFACE_DATA.Render.margin.l + INTERFACE_DATA.Render.area.y) {
if(mouse_x >= INTERFACE_DATA.Render.offset.x && mouse_x < INTERFACE_DATA.Render.offset.x + INTERFACE_DATA.Render.board_width) {
let basis_x = INTERFACE_DATA.Render.offset.x + halfradius;
let basis_y = INTERFACE_DATA.Render.offset.y + (14 * apothem);
let x = (mouse_x - basis_x) / grid_offset_x;
let y = -(mouse_y - 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(hx, hy)) {
let tile = HEX.hex_to_tile(hx, hy);
INTERFACE_DATA.hover = new INTERFACE.Selection(0, tile, hex);
}
}
// Handle pool area
else if(!INTERFACE.Config.DisablePool && mouse_x >= INTERFACE_DATA.Render.pool_offset && mouse_x < INTERFACE_DATA.Render.offset.x + INTERFACE_DATA.Render.area.x) {
let basis_x = INTERFACE_DATA.Render.pool_offset + halfradius;
let basis_y = INTERFACE_DATA.Render.offset.y + (3 * apothem);
let x = (mouse_x - basis_x) / grid_offset_x;
let y = (mouse_y - 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(hx, hy)) {
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.game_step(); }
},
unhover() {
let redraw = (INTERFACE_DATA.hover !== null);
INTERFACE_DATA.hover = null;
if(redraw) { INTERFACE.game_step(); }
},
click(event) {
let play_player = -1;
console.log("CLICK");
switch(event.button) {
// Main button
case 0: {
console.log("A");
if(INTERFACE_DATA.hover !== null) {
console.log("B");
INTERFACE_DATA.clicked = INTERFACE_DATA.hover;
if(INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) {
INTERFACE_DATA.clicked = null;
} else {
console.log("C");
// Check if operation can be performed on new tile.
// Otherwise, switch selection.
let result = 0;
if(INTERFACE_DATA.select !== null) {
console.log("Select not null");
// Play selection.
if(INTERFACE_DATA.hover.source == 0 && (INTERFACE_DATA.mode == INTERFACE.Mode.Local || (INTERFACE_DATA.player == ((GAME_DATA.turn + GAME_DATA.config.rules.reverse) & 1) || !GAME_DATA.config.rules.turn))) {
console.log("D1");
let tile_state = INTERFACE_DATA.Game.board_state[INTERFACE_DATA.hover.tile][1];
result = +(tile_state == INTERFACE.TileStatus.Valid || tile_state == INTERFACE.TileStatus.Threat);
console.log("RES " + result);
if(INTERFACE_DATA.select.source == 1) {
console.log("SRC1");
let pool_selected = +(INTERFACE_DATA.select.tile >= 7);
pool_selected ^= (INTERFACE_DATA.player & 1);
if(INTERFACE_DATA.mode == INTERFACE.Mode.Local) {
pool_selected ^= INTERFACE_DATA.rotate;
}
if(((GAME_DATA.turn + GAME_DATA.config.rules.reverse) & 1) != pool_selected) {
if(GAME_DATA.config.rules.turn) {
console.log("NOT POOL");
result = 0;
} else {
play_player = pool_selected;
}
}
}
}
// Alt move selection.
else if(INTERFACE_DATA.select.source == 0 && INTERFACE_DATA.hover.source == 1) {
console.log("D2");
let alt_piece = INTERFACE.selection_has_alt(INTERFACE_DATA.select);
if(alt_piece !== null) {
console.log("HAS ALT");
let pool_player = Math.floor(INTERFACE_DATA.hover.tile / 7);
pool_player ^= INTERFACE_DATA.player & 1;
if(INTERFACE_DATA.player == 2) { pool_player ^= INTERFACE_DATA.rotate; }
if((INTERFACE_DATA.hover.tile % 7) == alt_piece.piece && alt_piece.player == pool_player) {
INTERFACE_DATA.alt_mode = !INTERFACE_DATA.alt_mode;
result = 2;
}
}
}
}
// Handle player action.
switch(result) {
case 1: {
console.log("ACT1");
let source = INTERFACE_DATA.select.source;
if(source == 0 && INTERFACE_DATA.alt_mode) {
source = 2;
}
let play = new GAME.Play(
source,
INTERFACE_DATA.select.tile,
INTERFACE_DATA.hover.tile,
INTERFACE_DATA.alt_mode
);
play.player = play_player;
INTERFACE.process(play);
INTERFACE_DATA.select = null;
INTERFACE_DATA.alt_mode = false;
} break;
case 0: {
console.log("ACT2");
// Handle new selection.
INTERFACE_DATA.select = null;
INTERFACE_DATA.alt_mode = false;
// Select tile on board.
if(INTERFACE_DATA.hover.source == 0) {
if(GAME_DATA.board.tiles[INTERFACE_DATA.hover.tile].piece !== null) {
INTERFACE_DATA.select = INTERFACE_DATA.hover;
}
}
// Select tile 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;
}
}
} break;
}
}
}
// Clear selection if no tile is hovered.
else {
INTERFACE_DATA.select = null;
}
} break;
default: {
if(!event.ctrlKey) {
if(!event.altKey) {
INTERFACE_DATA.select = null;
} else {
INTERFACE_DATA.alt_mode = !INTERFACE_DATA.alt_mode;
}
}
} break;
}
INTERFACE.game_step();
if(event.preventDefault !== undefined && !event.ctrlKey) {
event.preventDefault();
}
},
click_touch(event) {
let rect = event.target.getBoundingClientRect();
let touch = event.touches[0] || event.changedTouches[0];
let x = touch.clientX - rect.left;
let y = touch.clientY - rect.top;
INTERFACE.hover({ offsetX:x, offsetY:y });
INTERFACE.click({ button:0 });
event.preventDefault();
},
contextmenu(event) {
if(!event.ctrlKey) {
if(!event.altKey) {
INTERFACE_DATA.select = null;
}
event.preventDefault();
return false;
} else {
return true;
}
},
release(event) {
if(event.button == 0) {
if(INTERFACE_DATA.hover !== null
&& !INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.clicked)
){
if(INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) {
INTERFACE_DATA.select = null;
INTERFACE_DATA.alt_mode = false;
INTERFACE.game_step();
} else {
INTERFACE.click({button:0});
}
}
INTERFACE_DATA.clicked = null;
}
},
release_touch(event) {
let rect = event.target.getBoundingClientRect();
let touch = event.touches[0] || event.changedTouches[0];
let x = touch.clientX - rect.left;
let y = touch.clientY - rect.top;
INTERFACE.hover({ offsetX:x, offsetY:y });
INTERFACE.release({ button:0 });
event.preventDefault();
},
resize() {
let width = INTERFACE_DATA.canvas.width = INTERFACE_DATA.canvas.clientWidth * (window.devicePixelRatio || 1);
let height = INTERFACE_DATA.canvas.height = INTERFACE_DATA.canvas.clientHeight * (window.devicePixelRatio || 1);
INTERFACE_DATA.Render.margin.t = Math.floor(Math.min(width, height) / 96);
INTERFACE_DATA.Render.margin.r = INTERFACE_DATA.Render.margin.t;
if(!INTERFACE.Config.DisableNumbers) {
INTERFACE_DATA.Render.margin.l = 1.75 * INTERFACE_DATA.Render.margin.t;
INTERFACE_DATA.Render.margin.b = 3 * INTERFACE_DATA.Render.margin.t;
} else {
INTERFACE_DATA.Render.margin.l = INTERFACE_DATA.Render.margin.t;
INTERFACE_DATA.Render.margin.b = INTERFACE_DATA.Render.margin.t;
}
let margin = INTERFACE_DATA.Render.margin;
let gui_width = width - (margin.l + margin.r);
let gui_height = height - (margin.t + margin.b);
if(!INTERFACE.Config.DisablePool) {
if(gui_width < gui_height * INTERFACE.Ratio) {
gui_height = Math.floor(gui_width / INTERFACE.Ratio);
} else {
gui_width = Math.floor(gui_height * INTERFACE.Ratio);
}
} else {
if(gui_width < gui_height * INTERFACE.BoardRatio) {
gui_height = Math.floor(gui_width / INTERFACE.BoardRatio);
} else {
gui_width = Math.floor(gui_height * INTERFACE.BoardRatio);
}
}
let gui_scale = gui_height * INTERFACE.Scale;
if(INTERFACE.Config.DisablePool) {
gui_scale = gui_height / 18;
}
INTERFACE_DATA.Render.area.x = gui_width;
INTERFACE_DATA.Render.area.y = gui_height;
INTERFACE_DATA.Render.scale = gui_scale;
INTERFACE_DATA.Render.offset.x = (INTERFACE_DATA.Render.margin.l - INTERFACE_DATA.Render.margin.r) + (width - gui_width) / 2;
INTERFACE_DATA.Render.offset.y = (INTERFACE_DATA.Render.margin.t - INTERFACE_DATA.Render.margin.b) + (height - gui_height) / 2;
INTERFACE_DATA.Render.board_width = Math.ceil(INTERFACE.BoardWidth * gui_scale);
INTERFACE_DATA.Render.pool_offset = INTERFACE_DATA.Render.offset.x + Math.floor(INTERFACE.PoolOffset * gui_scale);
},
game_step() {
if(INTERFACE_DATA === null) return;
if(GAME_DATA.turn == 0) {
INTERFACE_DATA.Ui.request_undo = 0;
}
switch(INTERFACE_DATA.mode) {
case INTERFACE.Mode.Player: {
let b_resign = document.getElementById("button-resign");
if(GAME_DATA.state.code == GAME.Const.State.Normal && ((GAME_DATA.turn + GAME_DATA.config.rules.reverse) & 1) == INTERFACE_DATA.player) {
b_resign.removeAttribute("disabled");
} else {
b_resign.setAttribute("disabled", "");
}
let b_undo = document.getElementById("button-undo");
if(GAME_DATA.turn == 0 || GAME_DATA.state.code != 0 || INTERFACE_DATA.Ui.request_undo == 1) {
b_undo.setAttribute("disabled", "");
b_undo.removeAttribute("class");
} else {
b_undo.removeAttribute("disabled");
if(INTERFACE_DATA.Ui.request_undo == 2) {
b_undo.innerText = "Undo?";
b_undo.setAttribute("class", "warn");
} else {
b_undo.innerText = "Undo";
b_undo.removeAttribute("class");
}
}
} break;
case INTERFACE.Mode.Review: {
document.getElementById("indicator-turn").innerText = INTERFACE_DATA.Replay.turn + " / " + INTERFACE_DATA.Game.history.length;
document.getElementById("turn-slider").setAttribute("max", INTERFACE_DATA.Game.history.length);
INTERFACE.replay_update_auto();
} break;
}
INTERFACE.step();
},
step() {
if(INTERFACE_DATA === null) return;
INTERFACE.resolve_board();
if(INTERFACE_DATA.Timeout.draw === null) {
INTERFACE.draw();
}
},
draw() {
INTERFACE.resize();
let canvas = INTERFACE_DATA.canvas;
let ctx = INTERFACE_DATA.context;
let width = canvas.width;
let height = canvas.height;
let gui_margin = INTERFACE_DATA.Render.margin;
let gui_offset = INTERFACE_DATA.Render.offset;
let gui_scale = INTERFACE_DATA.Render.scale;
let play = null;
if(INTERFACE_DATA.Replay.turn > 0) {
play = INTERFACE_DATA.Game.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 particles
for(let p = 0; p < INTERFACE_DATA.Animation.particles.length; ++p) {
let particle = INTERFACE_DATA.Animation.particles[p];
ctx.save();
if(particle !== null) {
if(particle.time > 0) {
let name = GAME_EMOJI[particle.index];
let color = INTERFACE.Color[GAME_EMOJI_COLOR[particle.color]];
ctx.translate((width / 10) + particle.position[0], height - particle.position[1]);
ctx.rotate(particle.rotation);
if(particle.time < 1) {
ctx.globalAlpha = Math.max(0, particle.time);
}
GAME_ASSET.Image[name].draw(ctx, particle.scale * gui_scale, [0, 0], color);
particle.position[0] += particle.velocity[0];
particle.position[1] += particle.velocity[1];
particle.rotation += particle.angular;
particle.time -= 30. / 1000;
} else {
INTERFACE_DATA.Animation.particles[p] = null;
}
}
ctx.restore();
}
let temp = INTERFACE_DATA.Animation.particles;
INTERFACE_DATA.Animation.particles = [ ];
for(particle of temp) {
if(particle !== null) {
INTERFACE_DATA.Animation.particles.push(particle);
}
}
// Draw tiles
let radius = INTERFACE.Radius * gui_scale;
let basis_x = gui_offset.x + radius;
let basis_y = gui_offset.y + (13 * gui_scale);
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.Game.board_state[i][1];
let hover_state = INTERFACE_DATA.Game.board_state[i][0];
let piece = null;
let draw_piece = false;
if(tile.piece !== null) {
piece = GAME_DATA.board.pieces[tile.piece];
if(piece.piece < 8) {
draw_piece = true;
}
}
if(INTERFACE_DATA.Animation.piece !== null) {
let play = INTERFACE_DATA.Animation.piece.play;
draw_piece = draw_piece && !((play.source == 0 || play.source == 2) && (play.from == i || play.to == i));
draw_piece = draw_piece && !(play.source == 1 && play.to == i);
}
let show_hints = !(is_hover || is_select) && piece !== null && draw_piece;
let [cx, cy] = HEX.tile_to_hex(i);
if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate == 1) {
cx = 8 - cx;
cy = 8 - cy;
}
let gui_x = basis_x + (1.5 * radius * cx);
let gui_y = basis_y - (2 * gui_scale * cy) + (gui_scale * cx);
ctx.save();
ctx.translate(gui_x, gui_y);
let is_play = null;
if(GAME_DATA.turn > 0 && play.source != 0xF && (play.to == i || ((play.source == 0 || play.source == 2) && play.from == i))) {
let piece_id = GAME_DATA.board.tiles[play.to].piece;
is_play = GAME_DATA.board.pieces[piece_id].player;
}
let is_check = (GAME_DATA.state.check != 0 || GAME_DATA.state.code == GAME.Const.State.Checkmate)
&& piece !== null
&& piece.piece == GAME.Const.PieceId.Heart
&& tile.checking;
let background_color = null;
let border_color = null;
let background_scale = 0.94;
// Get background color
if(is_check) {
background_color = INTERFACE.Color.HintCheck;
}
switch(hover_state) {
case INTERFACE.TileStatus.Valid: background_color = INTERFACE.Color.HintValidDark; break;
case INTERFACE.TileStatus.Threat: background_color = INTERFACE.Color.HintThreatDark; break;
case INTERFACE.TileStatus.Invalid: if(!INTERFACE.Config.DisableInvalid) { background_color = INTERFACE.Color.HintInvalidDark; } break;
case INTERFACE.TileStatus.Opponent: background_color = INTERFACE.Color.HintOpponentDark; break;
}
switch(tile_state) {
case INTERFACE.TileStatus.Valid: background_color = INTERFACE.Color.HintValid; break;
case INTERFACE.TileStatus.Threat: background_color = INTERFACE.Color.HintThreat; break;
case INTERFACE.TileStatus.Invalid: if(!INTERFACE.Config.DisableInvalid) { background_color = INTERFACE.Color.HintInvalid; } break;
case INTERFACE.TileStatus.Opponent: background_color = INTERFACE.Color.HintOpponent; break;
}
if(is_select) {
background_color = INTERFACE.Color.HintSelect;
}
// Get border color
if(draw_piece && tile.piece !== null) {
if(GAME_DATA.board.pieces[tile.piece].player == GAME.Const.Player.Dawn) {
border_color = INTERFACE.Color.DawnDark;
} else {
border_color = INTERFACE.Color.DuskDark;
}
}
if(is_hover) {
border_color = INTERFACE.Color.HintHover;
} else if(background_color == null) {
if(INTERFACE_DATA.select === null && is_play !== null) {
if(is_play == GAME.Const.Player.Dawn) { border_color = INTERFACE.Color.DawnDark; }
else { border_color = INTERFACE.Color.DuskDark; }
background_scale = 0.9;
}
}
// Get default colors
if(background_color === null) {
if(INTERFACE_DATA.select === null && is_play !== null) {
if(is_play === GAME.Const.Player.Dawn) {
background_color = INTERFACE.Color.DawnDarkest;
} else {
background_color = INTERFACE.Color.DuskDarkest;
}
} else {
switch(MATH.mod(cx + cy, 3)) {
case 0: background_color = INTERFACE.Color.TileMedium; break;
case 1: background_color = INTERFACE.Color.TileLight; break;
case 2: background_color = INTERFACE.Color.TileDark; break;
}
}
}
if(!is_hover) {
switch(background_color) {
case INTERFACE.Color.HintValid: {
border_color = INTERFACE.Color.HintValidLight;
show_hints = false;
} break;
case INTERFACE.Color.HintValidDark: {
border_color = INTERFACE.Color.HintValidTint;
show_hints = false;
} break;
case INTERFACE.Color.HintInvalid: {
border_color = INTERFACE.Color.HintInvalidLight;
show_hints = false;
} break;
case INTERFACE.Color.HintInvalidDark: {
border_color = INTERFACE.Color.HintInvalidTint;
show_hints = false;
} break;
case INTERFACE.Color.HintSelect: {
border_color = INTERFACE.Color.HintSelectLight;
}
}
}
if(border_color === null) {
border_color = INTERFACE.Color.TileBorder;
}
// Draw border
ctx.fillStyle = border_color;
ctx.beginPath();
draw.hex();
ctx.fill();
// Draw border hints
if(show_hints) { draw.hints(piece); }
// Draw background
if(background_scale < 0.94) {
ctx.fillStyle = border_color;
ctx.beginPath();
draw.hex(0.94);
ctx.fill();
}
ctx.fillStyle = background_color;
ctx.beginPath();
draw.hex(background_scale);
ctx.fill();
// Draw tile content
if(draw_piece && piece !== null) {
let piece_def = GAME.Const.Piece[piece.piece];
let piece_color = (piece.player == GAME.Const.Player.Dawn)? INTERFACE.Color.Dawn : INTERFACE.Color.Dusk;
if(GAME_ASSET.Image[piece_def.name] !== undefined) {
// Draw piece icon
if(INTERFACE_DATA.mirror && (piece.player ^ (INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate) != 0) {
ctx.rotate(Math.PI);
}
if(piece.promoted) {
GAME_ASSET.Image.Promote.draw(ctx, 1.5 * gui_scale, [0, 0], INTERFACE.Color.Promote);
}
GAME_ASSET.Image[piece_def.name].draw(ctx, 1.5 * gui_scale, [0, 0], piece_color);
}
}
ctx.restore();
}
ctx.font = Math.ceil(gui_scale / 2) + "px sans-serif";
let player_identity = GAME.Const.Player.Dawn;
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;
}
if(!INTERFACE.Config.DisablePool) {
// 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,
INTERFACE_DATA.mode == INTERFACE.Mode.Local && INTERFACE_DATA.mirror
);
}
// Draw informational text
let handle_pos = [
new MATH.Vec2(
basis_x + (radius * 12),
basis_y - (11.5 * gui_scale)
),
new MATH.Vec2(
basis_x + (radius * 12),
basis_y + (3.5 * gui_scale)
),
];
// Player handles
ctx.font = Math.ceil(gui_scale / 1.3) + "px sans-serif";
if(INTERFACE_DATA.Session.Client.Dawn.handle !== null) {
let pos = handle_pos[(1 ^ INTERFACE_DATA.player ^ INTERFACE_DATA.rotate) & 1];
if(INTERFACE_DATA.Session.Client.Dawn.online) {
ctx.fillStyle = INTERFACE.Color.Dawn;
} else {
ctx.fillStyle = INTERFACE.Color.DawnShade;
}
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillText(INTERFACE_DATA.Session.Client.Dawn.handle, pos.x, pos.y);
}
if(INTERFACE_DATA.Session.Client.Dusk.handle !== null) {
let pos = handle_pos[(INTERFACE_DATA.player ^ INTERFACE_DATA.rotate) & 1];
if(INTERFACE_DATA.Session.Client.Dusk.online) {
ctx.fillStyle = INTERFACE.Color.Dusk;
} else {
ctx.fillStyle = INTERFACE.Color.DuskShade;
}
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillText(INTERFACE_DATA.Session.Client.Dusk.handle, pos.x, pos.y);
}
// Tile information
ctx.font = Math.ceil(gui_scale / 2) + "px sans-serif";
if(!INTERFACE.Config.DisableText) {
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.t, gui_margin.t);
}
// Number of moves
ctx.fillStyle = INTERFACE.Color.Text;
ctx.textBaseline = "top";
ctx.textAlign = "right";
ctx.fillText(GAME_DATA.turn, width - gui_margin.t, gui_margin.t);
// Number of spectators
if(INTERFACE_DATA.mode == INTERFACE.Mode.Player || INTERFACE_DATA.mode == INTERFACE.Mode.Review) {
ctx.fillStyle = INTERFACE.Color.Text;
ctx.textBaseline = "bottom";
ctx.textAlign = "right";
ctx.fillText("⚇" + INTERFACE_DATA.Session.Client.Spectators.count, width - gui_margin.t, height - gui_margin.t);
}
// Game state message
let message = null;
ctx.fillStyle = INTERFACE.Color.Text;
switch(INTERFACE_DATA.Game.auto) {
case 1: message = LANG("cpu") + " " + LANG("dawn"); break;
case 2: message = LANG("cpu") + " " + LANG("dusk"); break;
case 3: message = LANG("cpu"); break;
}
if(GAME_DATA.config.rules.par != 0) {
if(GAME_DATA.turn >= (GAME_DATA.config.rules.par * 2)) {
message = "Par " + GAME_DATA.config.rules.par;
}
}
switch(GAME_DATA.state.code) {
case GAME.Const.State.Resign: {
ctx.fillStyle = INTERFACE.Color.HintCheck;
message = LANG("resign");
} break;
case GAME.Const.State.Checkmate: {
ctx.fillStyle = INTERFACE.Color.HintCheck;
message = LANG("checkmate");
} break;
default: {
if(GAME_DATA.state.check != 0) {
ctx.fillStyle = INTERFACE.Color.HintCheck;
message = LANG("check");
}
}
}
if(message !== null) {
ctx.textBaseline = "bottom";
ctx.textAlign = "left";
ctx.fillText(message, gui_margin.t, height - gui_margin.t);
}
}
if(!INTERFACE.Config.DisableNumbers) {
// Draw tile numbers
let letters = [ "A", "B", "C", "D", "E", "F", "G", "H", "I"];
let numbers = [ "1", "2", "3", "4", "5", "6", "7", "8", "9"];
if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate != 0) {
letters.reverse();
numbers.reverse();
}
ctx.fillStyle = INTERFACE.Color.TextDark;
ctx.textBaseline = "top";
ctx.textAlign = "center";
for(let i = 0; i < 5; ++i) {
let x = basis_x + (1.5 * radius * i);
let y = basis_y + (gui_scale * (1.1 + i));
ctx.fillText(letters[i], x, y);
}
for(let i = 0; i < 4; ++i) {
let x = basis_x + (1.5 * radius * (5 + i));
let y = basis_y + (gui_scale * (4.15 - i));
ctx.fillText(letters[i + 5], x, y);
}
let number_offset_x = -1.15 * radius;
let number_offset_y = -0.75 * gui_scale;
ctx.textAlign = "center";
for(let i = 0; i < 5; ++i) {
let y = basis_y - (gui_scale * 2 * i);
ctx.save();
ctx.translate(basis_x + number_offset_x, y + number_offset_y);
ctx.rotate(-Math.PI / 3);
ctx.fillText(numbers[i], 0, 0);
ctx.restore();
}
for(let i = 0; i < 4; ++i) {
let x = basis_x + (1.5 * radius * (1 + i));
let y = basis_y - (gui_scale * (9 + i));
ctx.save();
ctx.translate(x + number_offset_x, y + number_offset_y);
ctx.rotate(-Math.PI / 3);
ctx.fillText(numbers[i + 5], 0, 0);
ctx.restore();
}
}
if(INTERFACE_DATA.Animation.piece !== null) {
let time = Math.min(1 - (INTERFACE_DATA.Animation.piece.time - Date.now()) / 1000, 1);
time = time * time;
let play = INTERFACE_DATA.Animation.piece.play;
let piece = INTERFACE_DATA.Animation.piece.piece;
let target = INTERFACE_DATA.Animation.piece.target;
// Get to and from coordinates.
let [cdx, cdy] = HEX.tile_to_hex(play.to);
if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate == 1) {
cdx = 8 - cdx;
cdy = 8 - cdy;
}
let to_x = basis_x + (1.5 * radius * cdx);
let to_y = basis_y - (2 * gui_scale * cdy) + (gui_scale * cdx);
let from_x = 0;
let from_y = 0;
switch(play.source) {
// Lerp between board positions.
case 0:
case 2: {
let [cfx, cfy] = HEX.tile_to_hex(play.from);
if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate == 1) {
cfx = 8 - cfx;
cfy = 8 - cfy;
}
from_x = basis_x + (1.5 * radius * cfx);
from_y = basis_y - (2 * gui_scale * cfy) + (gui_scale * cfx);
} 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.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.Animation.piece.time) {
INTERFACE_DATA.Animation.piece = null;
}
}
if(INTERFACE_DATA.Animation.piece !== null
|| INTERFACE_DATA.Animation.particles.length > 0)
{
INTERFACE_DATA.Timeout.draw = setTimeout(INTERFACE.draw, 1000 / 30);
} else {
if(INTERFACE_DATA.Timeout.draw !== null) {
INTERFACE_DATA.Timeout.draw = null;
setTimeout(INTERFACE.draw, 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) {
if(INTERFACE.Config.DisableHint) { return; }
let scale = 0.92 * this.scale;
let half_scale = scale * 0.5;
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.fillStyle = (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 ifr = INTERFACE.HexVertex[(nmove + 5) % 6];
let ito = INTERFACE.HexVertex[(nmove + 2) % 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(ifr.x * half_scale, ifr.y * half_scale);
this.ctx.lineTo(fqx * scale, fqy * scale);
this.ctx.lineTo(tqx * scale, tqy * scale);
this.ctx.lineTo(ito.x * half_scale, ito.y * half_scale);
this.ctx.lineTo(ifr.x * half_scale, ifr.y * half_scale);
this.ctx.fill();
}
// 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 ifr = INTERFACE.HexVertex[(nmove + 5) % 6];
let ito = INTERFACE.HexVertex[(nmove + 3) % 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(ifr.x * half_scale, ifr.y * half_scale);
this.ctx.lineTo(fqx * scale, fqy * scale);
this.ctx.lineTo(mid.x * scale, mid.y * scale);
this.ctx.lineTo(tqx * scale, tqy * scale);
this.ctx.lineTo(ito.x * half_scale, ito.y * half_scale);
this.ctx.lineTo(ifr.x * half_scale, ifr.y * half_scale);
this.ctx.fill();
}
moves &= ~mask;
}
}
animation_piece(piece, x, y) {
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 hints
if(piece !== null) {
this.hints(piece);
}
// Draw inside 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(0.94);
this.ctx.fill();
// Draw background.
if(piece.player == GAME.Const.Player.Dawn) { this.ctx.fillStyle = INTERFACE.Color.DawnDarkest; }
else { this.ctx.fillStyle = INTERFACE.Color.DuskDarkest; }
this.ctx.beginPath();
this.hex(0.9);
this.ctx.fill();
// Draw tile content
if(piece !== null) {
let piece_def = GAME.Const.Piece[piece.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) {
GAME_ASSET.Image.Promote.draw(this.ctx, 1.5 * this.scale, [0, 0], INTERFACE.Color.Promote);
}
if(GAME_ASSET.Image[piece_def.name] !== undefined) {
let piece_color = (piece.player == GAME.Const.Player.Dawn)? INTERFACE.Color.Dawn : INTERFACE.Color.Dusk;
GAME_ASSET.Image[piece_def.name].draw(this.ctx, 1.5 * this.scale, [0, 0], piece_color);
}
}
this.ctx.restore();
}
pool(x, y, basis, player, mirror=false) {
let radius = INTERFACE.Radius * this.scale;
for(let i = 0; i < 7; ++i) {
let piece_def = GAME.Const.Piece[i];
let tile_id = i + basis;
let is_hover = INTERFACE.Ui.tile_is_hover(1, tile_id);
let is_select = INTERFACE.Ui.tile_is_select(1, tile_id);
let ix = +(i > 1) + (i > 4);
let iy = i - ((i > 1) * 3) - ((i > 4) * 2) + (0.5 * (ix == 1));
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);
// Get background color.
let background_color = null;
let border_color = player_color;
if(is_select) {
border_color = INTERFACE.Color.HintSelectLight;
}
let alt_piece = INTERFACE.selection_has_alt(INTERFACE_DATA.hover);
if(alt_piece !== null) {
if(alt_piece.piece == i && alt_piece.player == player) {
background_color = INTERFACE.Color.HintValidDark;
border_color = INTERFACE.Color.HintValidTint;
}
}
alt_piece = INTERFACE.selection_has_alt(INTERFACE_DATA.select);
if(is_select) { background_color = INTERFACE.Color.HintSelect; }
else if(alt_piece !== null) {
if(alt_piece.piece == i && alt_piece.player == player) {
if(INTERFACE_DATA.alt_mode) {
background_color = INTERFACE.Color.HintSelect;
border_color = INTERFACE.Color.HintSelectLight;
} else {
background_color = INTERFACE.Color.HintValid;
border_color = INTERFACE.Color.HintValidLight;
}
}
}
if(is_hover) { border_color = INTERFACE.Color.HintHover; }
// Draw border
let turn_indicator = !GAME_DATA.config.rules.turn || (player == ((GAME_DATA.turn + GAME_DATA.config.rules.reverse) & 1) && (INTERFACE_DATA.player == player || INTERFACE_DATA.player == 2));
if(is_hover || background_color !== null || turn_indicator) {
this.ctx.fillStyle = border_color;
this.ctx.beginPath();
this.hex();
this.ctx.fill();
}
if(background_color === null) { background_color = INTERFACE.Color.TileDark; }
this.ctx.fillStyle = background_color;
this.ctx.beginPath();
this.hex(0.94);
this.ctx.fill();
if(mirror) {
this.ctx.rotate(Math.PI);
}
if(GAME_ASSET.Image[piece_def.name] !== undefined) {
let piece_color = (player == GAME.Const.Player.Dawn)? INTERFACE.Color.Dawn : INTERFACE.Color.Dusk;
GAME_ASSET.Image[piece_def.name].draw(this.ctx, radius, [0.2 * radius, 0], piece_color);
}
// 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, config, mode, params={}) {
GAME.init(config);
let player = 2;
INTERFACE_DATA = {
canvas: document.getElementById("game"),
context: null,
player: player,
rotate: params.rotate !== undefined? params.rotate : 0,
mirror: false,
hover: null,
select: null,
clicked: null,
alt_mode: false,
mode: mode,
Session: {
token: token,
Client: {
Dawn: {
handle: null,
online: false,
},
Dusk: {
handle: null,
online: false,
},
Spectators: {
count: 0,
},
},
},
Game: {
board_state: [ ],
history: [ ],
history_begin: [ ],
auto: false,
},
Ui: {
request_undo:0,
resign_warn:false,
},
Timeout: {
draw: null,
},
Replay: {
turn: 0,
auto: false,
},
Render: {
scale: 0,
margin: { t:0, l:0, r:0, b:0 },
offset: new MATH.Vec2(),
area: new MATH.Vec2(),
board_width: 0,
pool_offset: 0,
},
Animation: {
piece: null,
// play: null,
// piece: null,
// target: null,
// time: 0,
particles: [ ],
queue: [ ],
},
};
for(let i = 0; i < 61; ++i) { INTERFACE_DATA.Game.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("touchstart", INTERFACE.click_touch);
canvas.addEventListener("mouseup", INTERFACE.release);
canvas.addEventListener("touchend", INTERFACE.release_touch);
canvas.addEventListener("contextmenu", INTERFACE.contextmenu);
canvas.addEventListener("selectstart", () => false);
window.addEventListener("resize", INTERFACE.step);
switch(INTERFACE_DATA.mode) {
case INTERFACE.Mode.Local: {
INTERFACE.game_step();
} break;
}
} else {
LOAD(SCENES.Browse);
}
switch(INTERFACE_DATA.mode) {
case INTERFACE.Mode.Review:
case INTERFACE.Mode.Player: {
MESSAGE_COMPOSE([
PACK.u16(OpCode.GameState),
INTERFACE_DATA.Session.token,
]);
} break;
}
},
load(config) {
GAME.init(config);
INTERFACE.reset();
},
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() {
INTERFACE_DATA.Game.auto = (GAME_DATA.config.rules.cpu)? 2 : false;
INTERFACE_DATA.Game.history = [ ];
for(let i = 0; i < INTERFACE_DATA.Game.history_begin.length; ++i) {
INTERFACE_DATA.Game.history.push(INTERFACE_DATA.Game.history_begin[i]);
}
INTERFACE_DATA.Replay.turn = INTERFACE_DATA.Game.history.length + 1;
INTERFACE.replay_jump(INTERFACE_DATA.Game.history.length, false);
},
undo() {
switch(INTERFACE_DATA.mode) {
case INTERFACE.Mode.Local: {
let back = 1;
if(INTERFACE_DATA.Game.auto != 0 && INTERFACE_DATA.rotate != INTERFACE_DATA.Game.auto) {
back = 2;
}
for(let i = 0; i < back; ++i) {
if(INTERFACE_DATA.Game.history.length > 0) {
INTERFACE_DATA.Replay.turn = INTERFACE_DATA.Game.history.length + 1;
INTERFACE_DATA.Game.history.pop();
}
}
INTERFACE.replay_jump(INTERFACE_DATA.Game.history.length, false);
} break;
case INTERFACE.Mode.Player: {
let high = 0;
let low = GameMessage.Undo | (GAME_DATA.turn << 9);
MESSAGE_COMPOSE([
PACK.u16(OpCode.GameMessage),
PACK.u32(high),
PACK.u32(low),
]);
INTERFACE_DATA.Ui.request_undo = 1;
INTERFACE.game_step();
} break;
}
},
message(msg) {
if(msg === null) { return; }
let code = msg.code;
let data = msg.data;
switch(code) {
case OpCode.GameState: {
INTERFACE_DATA.player = data.player;
if(INTERFACE_DATA.rotate >= 2) {
INTERFACE_DATA.rotate = (INTERFACE_DATA.rotate & 1) ^ (data.player & 1);
}
if(INTERFACE_DATA.mode == INTERFACE.Mode.Player) {
if(data.undo > 0) {
if((1 << INTERFACE_DATA.player) == data.undo) {
INTERFACE_DATA.Ui.request_undo = 1;
} else {
INTERFACE_DATA.Ui.request_undo = 2;
}
}
}
INTERFACE_DATA.Game.history = data.history;
let turn = INTERFACE_DATA.Game.history.length;
INTERFACE_DATA.Session.Client.Dawn.online = data.dawn_online;
INTERFACE_DATA.Session.Client.Dusk.online = data.dusk_online;
INTERFACE_DATA.Session.Client.Spectators.count = data.spectators;
if(INTERFACE_DATA.Game.history.length > 0) {
if(INTERFACE_DATA.Replay.turn == 0) {
//if(INTERFACE_DATA.Game.history[INTERFACE_DATA.Game.history.length-1].source == 2) {
// turn = 0;
//}
} else {
turn = INTERFACE_DATA.Replay.turn;
}
}
if(data.dawn.length > 0) { INTERFACE_DATA.Session.Client.Dawn.handle = data.dawn; }
if(data.dusk.length > 0) { INTERFACE_DATA.Session.Client.Dusk.handle = data.dusk; }
if(INTERFACE_DATA.mode == INTERFACE.Mode.Review) {
document.getElementById("indicator-turn").innerText = INTERFACE_DATA.Replay.turn + " / " + INTERFACE_DATA.Game.history.length;
document.getElementById("turn-slider").setAttribute("max", INTERFACE_DATA.Game.history.length);
}
INTERFACE.replay_jump(turn);
} break;
case OpCode.GameMessage: {
switch(data.code) {
case GameMessage.Error: break;
case GameMessage.Move: {
if(data.turn == INTERFACE_DATA.Game.history.length) {
INTERFACE.history_push(new GAME.Play(0, data.from, data.to), true);
}
} break;
case GameMessage.Drop: {
if(data.turn == INTERFACE_DATA.Game.history.length) {
INTERFACE.history_push(new GAME.Play(1, data.piece, data.to), true);
}
} break;
case GameMessage.Alt: {
if(data.turn == INTERFACE_DATA.Game.history.length) {
INTERFACE.history_push(new GAME.Play(2, data.from, data.to), true);
}
} break;
case GameMessage.Online: {
INTERFACE_DATA.Session.Client.Dawn.online = data.dawn;
INTERFACE_DATA.Session.Client.Dusk.online = data.dusk;
INTERFACE_DATA.Session.Client.Spectators.count = data.spectators;
INTERFACE.redraw();
} break;
case GameMessage.Undo: {
if(data.state == 0) {
// Request undo
if(data.turn == INTERFACE_DATA.Game.history.length) {
INTERFACE_DATA.Ui.request_undo = 2;
INTERFACE.game_step();
}
} else {
// Perform undo
INTERFACE_DATA.Ui.request_undo = 0;
while(data.turn < INTERFACE_DATA.Game.history.length) {
INTERFACE_DATA.Game.history.pop();
}
INTERFACE.replay_last(false);
}
} break;
case GameMessage.Resign: {
GAME_DATA.state.code = GAME.Const.State.Resign;
INTERFACE.game_step();
} break;
case GameMessage.Reaction: {
INTERFACE_DATA.Animation.queue.push(data.index);
INTERFACE.reaction_generate();
} break;
}
} break;
}
},
redraw() {
if(INTERFACE_DATA !== null) {
if(INTERFACE_DATA.Timeout.draw === null) {
INTERFACE.draw();
}
}
},
process(play) {
let valid = true;
console.log("PRC " + play.source);
switch(play.source) {
case 0:
case 2: {
let piece_id = GAME_DATA.board.tiles[play.from].piece;
let piece = GAME_DATA.board.pieces[piece_id];
valid = piece.player == ((GAME_DATA.turn + GAME_DATA.config.rules.reverse) & 1) || !GAME_DATA.config.rules.turn;
} break;
case 1: {
play.from %= 7;
valid = true;
} break;
}
console.log("PRC V=" + valid);
if(valid) {
// 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);
if((INTERFACE_DATA.Game.auto & (1 << ((GAME_DATA.turn + GAME_DATA.config.rules.reverse) & 1))) != 0) {
setTimeout(INTERFACE.auto_play, 1000);
}
} break;
// Send action to server for validation.
case INTERFACE.Mode.Player: {
let msg = GAME_DATA.turn | (play.from << 16) | (play.to << 22);
let high = msg >> 24;
let low = (msg << 8) & 0xFFFF_FFFF;
switch(play.source) {
case 0: low |= GameMessage.Move; break;
case 1: low |= GameMessage.Drop; break;
case 2: low |= GameMessage.Alt; break;
}
MESSAGE_COMPOSE([
PACK.u16(OpCode.GameMessage),
PACK.u32(high),
PACK.u32(low),
]);
} break;
}
}
},
rotate() {
INTERFACE_DATA.rotate ^= 1;
INTERFACE.game_step();
},
mirror() {
INTERFACE_DATA.mirror = !INTERFACE_DATA.mirror;
INTERFACE.game_step();
},
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.Session.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.Ui.request_undo = 0;
INTERFACE_DATA.Game.history.push(play);
if(INTERFACE_DATA.Replay.turn == INTERFACE_DATA.Game.history.length - 1) {
INTERFACE.replay_next(animate);
} else {
INTERFACE.game_step();
}
},
replay_jump(turn, animate=false) {
turn = +turn;
if(turn >= 0 && turn <= INTERFACE_DATA.Game.history.length) {
if(turn <= INTERFACE_DATA.Replay.turn) {
INTERFACE_DATA.Replay.turn = 0;
GAME.init(GAME_DATA.config);
}
let play = null;
let piece = null;
let target = null;
while(INTERFACE_DATA.Replay.turn < turn) {
play = INTERFACE_DATA.Game.history[INTERFACE_DATA.Replay.turn];
switch(play.source) {
case 0:
case 1:
case 2: {
if(play.source == 0 || play.source == 2) {
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;
}
} break;
default: {
GAME_DATA.process(play);
play = null;
}
}
INTERFACE_DATA.Replay.turn++;
}
if(animate && play !== null) {
INTERFACE_DATA.Animation.piece = {
time: Date.now() + 500,
play: play,
piece: piece,
target: target,
};
} else {
INTERFACE_DATA.Animation.piece = null;
}
INTERFACE_DATA.Replay.turn = turn;
if(INTERFACE_DATA.mode == INTERFACE.Mode.Review) {
document.getElementById("indicator-turn").innerText = INTERFACE_DATA.Replay.turn + " / " + INTERFACE_DATA.Game.history.length;
document.getElementById("turn-slider").value = INTERFACE_DATA.Replay.turn;
}
INTERFACE.game_step();
}
},
replay_first() {
INTERFACE.replay_jump(0);
},
replay_last(animate=false) {
INTERFACE.replay_jump(INTERFACE_DATA.Game.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);
}
INTERFACE.replay_update_auto();
},
replay_auto() {
if(INTERFACE_DATA.Replay.auto) {
if(INTERFACE_DATA.Replay.turn < INTERFACE_DATA.Game.history.length) {
INTERFACE.replay_jump(INTERFACE_DATA.Replay.turn + 1, true);
setTimeout(INTERFACE.replay_auto, 1100);
} else {
INTERFACE_DATA.Replay.auto = false;
}
}
INTERFACE.replay_update_auto();
},
replay_off() {
INTERFACE_DATA.Replay.auto = false;
INTERFACE.replay_update_auto();
},
replay_update_auto() {
let b_auto = document.getElementById("button-auto");
if(b_auto !== null) {
if(INTERFACE_DATA.Replay.auto) {
b_auto.setAttribute("class", "active");
} else {
b_auto.removeAttribute("class");
}
}
},
auto() {
let bit = 1 << (INTERFACE_DATA.rotate ^ 1);
if((INTERFACE_DATA.Game.auto & bit) == 0) {
INTERFACE_DATA.Game.auto |= bit;
setTimeout(INTERFACE.auto_play, 500);
} else {
INTERFACE_DATA.Game.auto &= ~bit;
}
INTERFACE.game_step();
},
auto_play() {
let bit = 1 << (GAME_DATA.turn & 1);
if((INTERFACE_DATA.Game.auto & bit) == 0 || GAME_DATA.state.checkmate) { return; }
function state_score(state, player) {
let score = 0;
let opponent = player ^ 1;
let turn = (state.turn & 1);
if(state.state.code != 0) {
if(turn == player) { score = -100000; }
else { score = 100000; }
} else {
// Increase score for check, decrease for checked.
if(state.state.check != 0) {
if(turn == player) { score -= 4; }
else { score += 2; }
}
// Increase score for each threatening tile, decrease for threatened tile.
for(let i = 0; i < state.board.tiles.length; ++i) {
let tile = state.board.tiles[i];
score += tile.threaten[player] - tile.threaten[opponent];
}
// Increase score for pieces on board.
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 [hx, hy] = 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(hy == state.board.columns[hx].extent[player]) {
score += piece_score * tile.threaten[player];
}
} else {
score -= piece_score - 2;
}
}
}
// Increase for player pool, decrease for opponent pool.
for(let i = 0; i < state.pools[player].pieces.length; ++i) {
score += 2;
}
for(let i = 0; i < state.pools[opponent].pieces.length; ++i) {
score -= 3;
}
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);
}
}
return score;
}
function determine_play(state, search_player, depth) {
let moves = [ ];
let player = state.turn & 1;
let st = state.clone();
// 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,
true_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),
});
}
}
}
}
}
// Select move.
if(moves.length > 0) {
// Determine move scores.
for(let mv = 0; mv < moves.length; ++mv) {
st.from(state);
st.process(moves[mv].play);
moves[mv].score = state_score(st, player);
moves[mv].true_score = state_score(st, search_player);
if(depth > 0) {
let result = determine_play(st, search_player, depth - 1);
if(result !== null) {
moves[mv].score = result.true_score;
moves[mv].true_score = result.true_score;
}
}
}
moves.sort((a, b) => { return b.score - a.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);
}
},
selection_has_alt(selection) {
if(selection !== null) {
if(selection.source == 0) {
let piece_id = GAME_DATA.board.tiles[selection.tile].piece;
if(piece_id !== null) {
let piece = GAME_DATA.board.pieces[piece_id];
if(piece.moves().alt) {
return piece;
}
}
}
}
return null;
},
react()
{
if(INTERFACE_DATA.mode == INTERFACE.Mode.Review) {
let high = 0;
let low = GameMessage.Reaction;
MESSAGE_COMPOSE([
PACK.u16(OpCode.GameMessage),
PACK.u32(high),
PACK.u32(low),
]);
}
},
reaction_generate()
{
if(INTERFACE_DATA !== null) {
if(INTERFACE_DATA.Animation.queue.length > 0) {
let index = INTERFACE_DATA.Animation.queue.pop();
INTERFACE_DATA.Animation.particles.push({
index:index,
color:0,
scale:1.75 + (Math.random() * 0.25),
position:[0, 0],
rotation:0,
velocity:[1 - (Math.random() * 2), 2 + (Math.random() * 4)],
angular:0.01 - (Math.random() * 0.02),
time:1.5 + (Math.random() * 1.5),
});
if(INTERFACE_DATA.Timeout.draw === null) {
INTERFACE_DATA.Timeout.draw = setTimeout(INTERFACE.draw, 1);
}
setTimeout(INTERFACE.reaction_generate, 50);
}
}
},
run_test(source=0, from=0, to=0, all=false)
{
let next_source = source;
let next_from = from;
let next_to = to + 1;
if(next_to == 61) {
next_to = 0;
next_from += 1;
}
if(next_from == 61 || (next_source == 1 && next_from == 7)) {
next_from = 0;
next_source += 1;
}
let msg = GAME_DATA.turn | (from << 16) | (to << 22);
let high = msg >> 24;
let low = (msg << 8) & 0xFFFF_FFFF;
switch(source) {
case 0: low |= GameMessage.Move; break;
case 1: low |= GameMessage.Drop; break;
case 2: low |= GameMessage.Alt; break;
}
// Mark test and expected value
let expect = GAME_DATA.play_is_valid(new GAME.Play(source, from, to));
high |= 1 << 31;
high |= (+expect) << 30;
console.log("Test Expect: " + expect);
MESSAGE_COMPOSE([
PACK.u16(OpCode.GameMessage),
PACK.u32(high),
PACK.u32(low),
]);
if(all && next_source < 3) {
setTimeout(INTERFACE.run_test, 100, next_source, next_from, next_to, true);
}
}
};
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.BoardRatio = (14 * INTERFACE.Radius) * INTERFACE.Scale;
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;