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", Promote: "#a52121", Dawn: "#ffe082", DawnShade: "#a59154", DawnMedium: "#fca03f", DawnDark: "#ff6d00", DawnDarkest: "#4c3422", Dusk: "#f6a1bd", DuskShade: "#a56b7f", DuskMedium: "#e84a79", DuskDark: "#c51162", DuskDarkest: "#4c2235", HintHover: "#71a1e8", HintSelect: "#4a148c", HintValid: "#1d268c", HintValidDark: "#151c66", HintThreat: "#054719", HintThreatDark: "#023311", HintOpponent: "#49136b", HintOpponentDark: "#2a0b3f", HintInvalid: "#b21818", HintInvalidDark: "#7f1111", HintPlay: "#307c7f", 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.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; // Handle board area if(event.offsetY >= INTERFACE_DATA.Render.margin.t && event.offsetY < INTERFACE_DATA.Render.margin.l + INTERFACE_DATA.Render.area.y) { if(event.offsetX >= INTERFACE_DATA.Render.offset.x && event.offsetX < 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 = (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.Render.pool_offset && event.offsetX < 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 = (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.step(); } }, unhover() { let redraw = (INTERFACE_DATA.hover !== null); INTERFACE_DATA.hover = null; if(redraw) { INTERFACE.step(); } }, click(event) { 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) { // Play selection. if(INTERFACE_DATA.hover.source == 0 && (INTERFACE_DATA.mode == INTERFACE.Mode.Local || INTERFACE_DATA.player == (GAME_DATA.turn & 1))) { 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 & 1) != pool_selected) { result = 0; } } } // 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: { console.log("D1"); 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 ); INTERFACE.process(play); INTERFACE_DATA.select = null; INTERFACE_DATA.alt_mode = false; } break; case 0: { console.log("D2"); // 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; // Aux button case 1: { INTERFACE_DATA.select = null; } break; } INTERFACE.step(); }, contextmenu() { INTERFACE_DATA.select = null; return false; }, release() { 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.step(); } else { INTERFACE.click({button:0}); } } INTERFACE_DATA.clicked = null; }, resize() { let width = INTERFACE_DATA.canvas.width = INTERFACE_DATA.canvas.clientWidth; let height = INTERFACE_DATA.canvas.height = INTERFACE_DATA.canvas.clientHeight; INTERFACE_DATA.Render.margin.t = Math.floor(Math.min(width, height) / 96); INTERFACE_DATA.Render.margin.l = 1.75 * INTERFACE_DATA.Render.margin.t; INTERFACE_DATA.Render.margin.r = INTERFACE_DATA.Render.margin.t; INTERFACE_DATA.Render.margin.b = 3 * 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(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.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); }, 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 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 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]; } let is_play = null; if(GAME_DATA.turn > 0 && play.source != 0xF && (play.to == i || ((play.source == 0 || play.source == 2) && play.from == i))) { is_play = +!(GAME_DATA.turn & 1); } let is_check = GAME_DATA.state.check != 0 && piece !== null && piece.piece == GAME.Const.PieceId.Heart && piece.player == (GAME_DATA.turn & 1); 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: 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: 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((GAME_DATA.turn & 1) != 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(coord.x + coord.y, 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(border_color === null) { border_color = INTERFACE.Color.TileBorder; } // Draw border ctx.fillStyle = border_color; ctx.beginPath(); draw.hex(); ctx.fill(); // Draw background 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; // Draw border hints if(!is_hover) { draw.hints(piece); } 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; } // 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_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 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; if(INTERFACE_DATA.Game.auto !== null) { switch(INTERFACE_DATA.Game.auto) { 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.code == GAME.Const.State.Checkmate) { message = LANG("checkmate"); } else { message = LANG("check"); } } if(message !== null) { ctx.textBaseline = "bottom"; ctx.textAlign = "left"; ctx.fillText(message, gui_margin.t, height - gui_margin.t); } // 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 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: case 2: { 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.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) { 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. 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 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) { 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 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(); 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, mode) { GAME.init(); let player = 2; let dawn = null; let dusk = null; INTERFACE_DATA = { canvas: document.getElementById("game"), context: null, player: player, 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: null, }, Ui: { request_undo:false, 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("mouseup", INTERFACE.release); canvas.addEventListener("contextmenu", INTERFACE.contextmenu); window.addEventListener("resize", INTERFACE.step); switch(INTERFACE_DATA.mode) { case INTERFACE.Mode.Local: { INTERFACE.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; } }, 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 = null; 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() { INTERFACE_DATA.Game.auto = null; 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); } }, message(code, data) { if(data === null) { return; } switch(code) { case OpCode.GameState: { if(INTERFACE_DATA.mode == INTERFACE.Mode.Player) { INTERFACE_DATA.player = data.player; } 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: { switch(data.client) { case 0: { // Spectator INTERFACE_DATA.Session.Client.Spectators.count = data.state; } break; case 1: { // Dawn INTERFACE_DATA.Session.Client.Dawn.online = (data.state != 0); } break; case 2: { // Dusk INTERFACE_DATA.Session.Client.Dusk.online = (data.state != 0); } break; } INTERFACE.redraw(); } break; case GameMessage.Undo: { switch(data.state) { case 0: { // Request undo if(data.turn == INTERFACE_DATA.Game.history.length) { INTERFACE_DATA.Ui.request_undo = true; } } break; case 1: { // Perform undo INTERFACE.undo(); } break; } } break; case GameMessage.Retire: { GAME_DATA.state.code = GAME.Const.State.Resign; INTERFACE.redraw(); } 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 & 1); } 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); INTERFACE.step(); if(INTERFACE_DATA.Game.auto !== null && INTERFACE_DATA.Game.auto == (GAME_DATA.turn & 1)) { 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.step(); }, mirror() { INTERFACE_DATA.mirror = !INTERFACE_DATA.mirror; INTERFACE.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.Game.history.push(play); 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); } if(INTERFACE_DATA.Replay.turn == INTERFACE_DATA.Game.history.length - 1) { INTERFACE.replay_next(animate); } }, 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(); } 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.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); } }, 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.Game.auto === null) { INTERFACE_DATA.Game.auto = INTERFACE_DATA.rotate ^ 1; setTimeout(INTERFACE.auto_play, 500); } else { INTERFACE_DATA.Game.auto = null; } INTERFACE.step(); }, auto_play() { if(INTERFACE_DATA.Game.auto !== (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); } else { console.log("warn: autoplay move was null."); } }, 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); } } } }; 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;