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", PromoteDark: "#7f1919", 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", HintInfo: "#67ce33", }; 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; let piece_id = null; // Get movements from source. switch(selection.source) { case 0: { 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); } } } break; case 1: { 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); } break; case 2: { let select_alt = INTERFACE.selection_has_alt(INTERFACE_DATA.select); if(select_alt !== null) { movements = GAME_DATA.movement_tiles_alt(select_alt); } } break; } 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); } else if(hx == 1 && hy == 3) { INTERFACE_DATA.hover = new INTERFACE.Selection(2, 0, 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; switch(event.button) { // Main button case 0: { if(INTERFACE_DATA.hover !== null) { INTERFACE_DATA.clicked = INTERFACE_DATA.hover; if(INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) { INTERFACE_DATA.clicked = null; } else { // Check if operation can be performed on new tile. // Otherwise, switch selection. let result = 0; // 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))) { if(INTERFACE_DATA.select !== null) { let tile_state = INTERFACE_DATA.Game.board_state[INTERFACE_DATA.hover.tile][1]; result = +(tile_state == INTERFACE.TileStatus.Valid || tile_state == INTERFACE.TileStatus.Threat); if(INTERFACE_DATA.select.source == 1) { 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) { result = 0; } else { play_player = pool_selected; } } } } } // Alt move selection. else if(INTERFACE_DATA.hover.source == 2) { if(INTERFACE_DATA.select !== null) { if(INTERFACE_DATA.select.source == 0) { let alt_piece = INTERFACE.selection_has_alt(INTERFACE_DATA.select); if(alt_piece !== null) { INTERFACE_DATA.alt_mode = !INTERFACE_DATA.alt_mode; } } } result = 2; } // Handle player action. switch(result) { case 1: { 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: { // Handle new selection. INTERFACE_DATA.select = null; INTERFACE_DATA.alt_mode = false; switch(INTERFACE_DATA.hover.source) { // Select tile on board. case 0: { if(GAME_DATA.board.tiles[INTERFACE_DATA.hover.tile].piece !== null) { INTERFACE_DATA.select = INTERFACE_DATA.hover; } } break; // Select tile in pools. case 1: { 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; } } break; } } } // Clear selection if no tile is hovered. else { INTERFACE_DATA.select = null; INTERFACE_DATA.alt_mode = false; } } 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) { let promote_color = INTERFACE.Color.Promote; console.log("ic " + is_check); if(is_check) { promote_color = INTERFACE.Color.PromoteDark; } GAME_ASSET.Image.Promote.draw(ctx, 1.5 * gui_scale, [0, 0], promote_color); } 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 ); // Alt draw.alt( basis_x + (14 * radius), basis_y - (6 * gui_scale), ); } // 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)) { ctx.fillStyle = INTERFACE.Color.HintInfo; 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; background_color = INTERFACE.Color.HintSelect; } 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(); } } alt(x, y, mirror=false) { let radius = INTERFACE.Radius * this.scale; let is_hover = INTERFACE.Ui.tile_is_hover(2, 0); let is_select = INTERFACE.Ui.tile_is_select(2, 0); let gui_x = x + (1.5 * radius); let gui_y = y + (2 * this.scale); this.ctx.save(); this.ctx.translate(gui_x, gui_y); // Get background color. let background_color = null; let border_color = INTERFACE.Color.Promote; if(is_select) { border_color = INTERFACE.Color.HintSelectLight; } let alt_piece = INTERFACE.selection_has_alt(INTERFACE_DATA.hover); if(alt_piece !== null) { 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(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 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); } GAME_ASSET.Image["Promote"].draw(this.ctx, 1.25 * this.scale, [0, 0], INTERFACE.Color.Promote); 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_DATA.config = 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; 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; 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; } 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;