let INTERFACE_DATA = null; const INTERFACE = { Mode: { Local: 0, Player: 1, Review: 2, }, Color: { Background: "#101010", Text: "#c0c0c0", TextDark: "#848484", 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; 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) { 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() { if(INTERFACE_DATA.hover !== null) { if(INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) { INTERFACE_DATA.select = null; INTERFACE_DATA.alt_mode = false; } else { // Check if operation can be performed on new tile. // Otherwise, switch selection. let result = 0; if(INTERFACE_DATA.select !== null) { // Play selection. if(INTERFACE_DATA.hover.source == 0 && INTERFACE_DATA.player == (GAME_DATA.turn & 1)) { let tile_state = INTERFACE_DATA.board_state[INTERFACE_DATA.hover.tile][1]; result = +(tile_state == INTERFACE.TileStatus.Valid || tile_state == INTERFACE.TileStatus.Threat); } // Alt move selection. else if(INTERFACE_DATA.select.source == 0 && INTERFACE_DATA.hover.source == 1) { let alt_piece = INTERFACE.selection_has_alt(INTERFACE_DATA.select); if(alt_piece !== null) { 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: { let play = new GAME.Play( INTERFACE_DATA.select.source, INTERFACE_DATA.select.tile, INTERFACE_DATA.hover.tile, INTERFACE_DATA.alt_mode ); INTERFACE.process(play); INTERFACE_DATA.select = null; INTERFACE_DATA.alt_mode = false; } break; case 0: { // 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; } 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_DATA.alt_mode ); INTERFACE.process(play); INTERFACE_DATA.select = null; INTERFACE_DATA.alt_mode = false; } } }, 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) / 96); 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.Source && 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.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.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); } // 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 < 4; ++i) { let x = basis_x + (1.5 * radius * i); let y = basis_y + (gui_scale * (1.15 + 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); } ctx.textAlign = "right"; for(let i = 0; i < 5; ++i) { let x = basis_x - (0.85 * radius); let y = basis_y - (gui_scale * (0.9 + (2 * i))); ctx.fillText(numbers[i], x, y); } for(let i = 0; i < 4; ++i) { let x = basis_x + (1.5 * radius * (i + 0.45)); let y = basis_y - (gui_scale * (9.9 + i)); ctx.fillText(numbers[i + 5], x, y); } 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 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 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; } } 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; } else { background_color = INTERFACE.Color.HintValid; } } } // Draw border let turn_indicator = player == (GAME_DATA.turn & 1) && (INTERFACE_DATA.player == player || INTERFACE_DATA.player == 2); if(is_hover || background_color !== null || turn_indicator) { if(is_hover) { this.ctx.fillStyle = INTERFACE.Color.HintHover; } else { this.ctx.fillStyle = player_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(); // 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, alt_mode: false, 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) { play = INTERFACE_DATA.history[INTERFACE_DATA.replay_turn]; if(play.source < 2) { 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); play = null; } INTERFACE_DATA.replay_turn++; } if(animate && play !== null) { 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); } }, 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; }, }; 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;