/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // --- ChessBoard Class --- var ChessBoard = Container.expand(function () { var self = Container.call(this); // 8x8 array of ChessPiece or null self.squares = []; for (var y = 0; y < 8; y++) { self.squares[y] = []; for (var x = 0; x < 8; x++) { self.squares[y][x] = null; } } // Draw board squares self.squareNodes = []; for (var y = 0; y < 8; y++) { self.squareNodes[y] = []; for (var x = 0; x < 8; x++) { var isLight = (x + y) % 2 === 0; // Use classic chessboard colors: light = #f0d9b5, dark = #b58863 var color = isLight ? 0xf0d9b5 : 0xb58863; var assetId = 'board_' + (isLight ? 'light' : 'dark'); var node = LK.getAsset(assetId, { anchorX: 0, anchorY: 0, x: boardOriginX + x * squareSize, y: boardOriginY + y * squareSize, width: squareSize, height: squareSize, color: color, shape: 'box' }); self.addChild(node); self.squareNodes[y][x] = node; } } // Highlight squares (for moves) self.highlightNodes = []; for (var i = 0; i < 64; i++) { var highlightId = 'highlight_' + i; // Use a circle (ellipse) and center it in the square var node = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, width: Math.floor(squareSize * 0.45), height: Math.floor(squareSize * 0.45), x: -1000, y: -1000 }); // Hide initially node.alpha = 0.25; self.addChild(node); self.highlightNodes[i] = node; } // Place pieces in starting position self.reset = function () { // Remove all pieces for (var y = 0; y < 8; y++) { for (var x = 0; x < 8; x++) { if (self.squares[y][x]) { self.squares[y][x].destroy(); self.squares[y][x] = null; } } } // Place pawns for (var x = 0; x < 8; x++) { var wp = new ChessPiece(); wp.init('pawn', 'white', x, 6); wp.moveTo(x, 6, false); self.addChild(wp); self.squares[6][x] = wp; var bp = new ChessPiece(); bp.init('pawn', 'black', x, 1); bp.moveTo(x, 1, false); self.addChild(bp); self.squares[1][x] = bp; } // Place other pieces var order = ['rook', 'knight', 'bishop', 'queen', 'king', 'bishop', 'knight', 'rook']; for (var x = 0; x < 8; x++) { var w = new ChessPiece(); w.init(order[x], 'white', x, 7); w.moveTo(x, 7, false); self.addChild(w); self.squares[7][x] = w; var b = new ChessPiece(); b.init(order[x], 'black', x, 0); b.moveTo(x, 0, false); self.addChild(b); self.squares[0][x] = b; } }; // Remove all highlights self.clearHighlights = function () { for (var i = 0; i < 64; i++) { self.highlightNodes[i].x = -1000; self.highlightNodes[i].y = -1000; } }; // Highlight given squares (array of {x, y}) self.showHighlights = function (squares) { self.clearHighlights(); for (var i = 0; i < squares.length; i++) { var idx = squares[i].y * 8 + squares[i].x; var node = self.highlightNodes[idx]; // Center the highlight circle in the square node.x = boardOriginX + squares[i].x * squareSize + squareSize / 2; node.y = boardOriginY + squares[i].y * squareSize + squareSize / 2; } }; // Get piece at board position self.getPiece = function (x, y) { if (x < 0 || x > 7 || y < 0 || y > 7) return null; return self.squares[y][x]; }; // Set piece at board position self.setPiece = function (x, y, piece) { self.squares[y][x] = piece; }; // Remove piece at board position self.removePiece = function (x, y) { var p = self.squares[y][x]; if (p) { p.destroy(); self.squares[y][x] = null; } }; return self; }); // --- ChessPiece Class --- var ChessPiece = Container.expand(function () { var self = Container.call(this); // Properties self.type = null; // 'pawn', 'rook', 'knight', 'bishop', 'queen', 'king' self.color = null; // 'white' or 'black' self.boardX = 0; // 0-7 self.boardY = 0; // 0-7 self.hasMoved = false; // For castling/en passant self.promoted = false; // For pawn promotion // Asset self.pieceAsset = null; // Set piece type and color, and create asset self.init = function (type, color, boardX, boardY) { self.type = type; self.color = color; self.boardX = boardX; self.boardY = boardY; self.hasMoved = false; self.promoted = false; // Remove old asset if any if (self.pieceAsset) { self.removeChild(self.pieceAsset); } // Use classic piece colors: white = #fff, black = #222 var colorMap = { 'white': { 'pawn': 0xffffff, 'rook': 0xffffff, 'knight': 0xffffff, 'bishop': 0xffffff, 'queen': 0xffffff, 'king': 0xffffff }, 'black': { 'pawn': 0x222222, 'rook': 0x222222, 'knight': 0x222222, 'bishop': 0x222222, 'queen': 0x222222, 'king': 0x222222 } }; var shape = self.type === 'pawn' || self.type === 'bishop' ? 'ellipse' : 'box'; var assetId = self.color + '_' + self.type; var assetColor = colorMap[self.color][self.type]; var size = self.type === 'king' ? Math.floor(pieceSize * 1.25) : pieceSize; self.pieceAsset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, width: size, height: size, color: assetColor, shape: shape }); // Add a crown for promoted pawns if (self.promoted && self.type === 'queen') { // Overlay a small yellow ellipse as a "crown" var crown = LK.getAsset('crown_' + self.color, { anchorX: 0.5, anchorY: 0.5, y: -size * 0.3 }); self.addChild(crown); } }; // Move piece to board position (with optional animation) self.moveTo = function (bx, by, animate) { self.boardX = bx; self.boardY = by; var targetX = boardOriginX + bx * squareSize + squareSize / 2; var targetY = boardOriginY + by * squareSize + squareSize / 2; if (animate) { tween(self, { x: targetX, y: targetY }, { duration: 180, easing: tween.cubicOut }); } else { self.x = targetX; self.y = targetY; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222222 }); /**** * Game Code ****/ // --- Board and Piece Sizes --- // We need tween for piece movement animations // Board squares // White pieces // Black pieces // Crown for promoted pawn (queen) var boardSize = Math.min(2048, 2048); // Square board, fit width var squareSize = Math.floor(boardSize / 8); var pieceSize = Math.floor(squareSize * 0.8); var boardOriginX = Math.floor((2048 - squareSize * 8) / 2); var boardOriginY = Math.floor((2732 - squareSize * 8) / 2); // --- Game State --- var board = new ChessBoard(); game.addChild(board); board.reset(); var currentTurn = 'white'; // 'white' or 'black' var selectedPiece = null; var legalMoves = []; // Array of {x, y, special} var lastMove = null; // {from: {x,y}, to: {x,y}, piece, captured, special} var enPassantTarget = null; // {x, y} or null var halfmoveClock = 0; // For 50-move rule var fullmoveNumber = 1; var gameEnded = false; // --- GUI: Turn Display --- var turnText = new Text2('White to move', { size: 90, fill: "#fff" }); turnText.anchor.set(0.5, 0); LK.gui.top.addChild(turnText); // --- GUI: Timers --- // Each player starts with 10 minutes (600 seconds) var whiteTime = 600; var blackTime = 600; var whiteTimerText = new Text2('10:00', { size: 90, fill: "#fff" }); // Place white timer near bottom left, above white's side, but not in the bottom left 100x100 reserved area whiteTimerText.anchor.set(0, 1); // left aligned, bottom whiteTimerText.x = 320; // moved even further right whiteTimerText.y = LK.gui.height - 40; LK.gui.bottomLeft.addChild(whiteTimerText); var blackTimerText = new Text2('10:00', { size: 90, fill: "#fff" }); // Place black timer near top right, above black's side, but not in the top right 100x100 reserved area blackTimerText.anchor.set(1, 0); // right aligned, top blackTimerText.x = LK.gui.width - 40; // moved even further right (inward) blackTimerText.y = 0; LK.gui.topRight.addChild(blackTimerText); // Timer state var whiteTimerInterval = null; var blackTimerInterval = null; // Helper to format seconds as MM:SS function formatTime(secs) { var m = Math.floor(secs / 60); var s = Math.floor(secs % 60); return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s; } // Start/stop timer helpers function startWhiteTimer() { if (whiteTimerInterval) LK.clearInterval(whiteTimerInterval); whiteTimerInterval = LK.setInterval(function () { if (gameEnded) return; whiteTime--; if (whiteTime < 0) whiteTime = 0; whiteTimerText.setText(formatTime(whiteTime)); if (whiteTime === 0) { gameEnded = true; LK.effects.flashScreen(0xff0000, 800); LK.showYouWin('Black wins on time!'); stopAllTimers(); } }, 1000); } function startBlackTimer() { if (blackTimerInterval) LK.clearInterval(blackTimerInterval); blackTimerInterval = LK.setInterval(function () { if (gameEnded) return; blackTime--; if (blackTime < 0) blackTime = 0; blackTimerText.setText(formatTime(blackTime)); if (blackTime === 0) { gameEnded = true; LK.effects.flashScreen(0xff0000, 800); LK.showYouWin('White wins on time!'); stopAllTimers(); } }, 1000); } function stopWhiteTimer() { if (whiteTimerInterval) { LK.clearInterval(whiteTimerInterval); whiteTimerInterval = null; } } function stopBlackTimer() { if (blackTimerInterval) { LK.clearInterval(blackTimerInterval); blackTimerInterval = null; } } function stopAllTimers() { stopWhiteTimer(); stopBlackTimer(); } // Start white's timer at game start startWhiteTimer(); whiteTimerText.setText(formatTime(whiteTime)); blackTimerText.setText(formatTime(blackTime)); // --- GUI: Promotion Popup --- var promotionPopup = null; // --- Helper Functions --- // Convert screen (x, y) to board (bx, by) function screenToBoard(x, y) { var bx = Math.floor((x - boardOriginX) / squareSize); var by = Math.floor((y - boardOriginY) / squareSize); if (bx < 0 || bx > 7 || by < 0 || by > 7) return null; return { x: bx, y: by }; } // Convert board (bx, by) to screen (x, y) (center of square) function boardToScreen(bx, by) { return { x: boardOriginX + bx * squareSize + squareSize / 2, y: boardOriginY + by * squareSize + squareSize / 2 }; } // Is (x, y) on the board? function isOnBoard(x, y) { return x >= 0 && x < 8 && y >= 0 && y < 8; } // Get all legal moves for a piece at (x, y) function getLegalMoves(piece, x, y, ignoreCheck) { var moves = []; if (!piece) return moves; var color = piece.color; var opp = color === 'white' ? 'black' : 'white'; // Directions for sliding pieces var dirs = { 'rook': [[1, 0], [-1, 0], [0, 1], [0, -1]], 'bishop': [[1, 1], [1, -1], [-1, 1], [-1, -1]], 'queen': [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]] }; // Pawn moves if (piece.type === 'pawn') { var dir = color === 'white' ? -1 : 1; // Forward if (isOnBoard(x, y + dir) && !board.getPiece(x, y + dir)) { moves.push({ x: x, y: y + dir }); // Double move from start if (!piece.hasMoved && isOnBoard(x, y + 2 * dir) && !board.getPiece(x, y + 2 * dir)) { moves.push({ x: x, y: y + 2 * dir, special: 'double' }); } } // Captures for (var dx = -1; dx <= 1; dx += 2) { var tx = x + dx, ty = y + dir; if (isOnBoard(tx, ty)) { var target = board.getPiece(tx, ty); if (target && target.color === opp) { moves.push({ x: tx, y: ty }); } } } // En passant if (enPassantTarget) { if (Math.abs(enPassantTarget.x - x) === 1 && enPassantTarget.y === y + dir) { moves.push({ x: enPassantTarget.x, y: enPassantTarget.y, special: 'enpassant' }); } } } // Knight moves else if (piece.type === 'knight') { var knightDeltas = [[1, 2], [2, 1], [-1, 2], [-2, 1], [1, -2], [2, -1], [-1, -2], [-2, -1]]; for (var i = 0; i < knightDeltas.length; i++) { var dx = knightDeltas[i][0], dy = knightDeltas[i][1]; var tx = x + dx, ty = y + dy; if (isOnBoard(tx, ty)) { var target = board.getPiece(tx, ty); if (!target || target.color === opp) { moves.push({ x: tx, y: ty }); } } } } // King moves (including castling) else if (piece.type === 'king') { // Prevent infinite recursion: when ignoreCheck is true, do NOT generate king moves if (ignoreCheck) { // Do not generate king moves when ignoreCheck is true (used for attack detection) // This prevents infinite recursion between isInCheck and getLegalMoves } else { // Find the other king's position var otherKingPos = null; for (var yy = 0; yy < 8; yy++) { for (var xx = 0; xx < 8; xx++) { var p = board.getPiece(xx, yy); if (p && p.type === 'king' && p.color !== color) { otherKingPos = { x: xx, y: yy }; break; } } if (otherKingPos) break; } for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { if (dx === 0 && dy === 0) continue; var tx = x + dx, ty = y + dy; if (isOnBoard(tx, ty)) { // Prevent moving within 3 squares horizontally or vertically of the other king var allow = true; if (otherKingPos) { if (Math.abs(tx - otherKingPos.x) <= 3 && ty === otherKingPos.y) allow = false; if (Math.abs(ty - otherKingPos.y) <= 3 && tx === otherKingPos.x) allow = false; if (tx === otherKingPos.x && ty === otherKingPos.y) allow = false; } var target = board.getPiece(tx, ty); if ((!target || target.color === opp) && allow) { moves.push({ x: tx, y: ty }); } } } } // Castling if (!piece.hasMoved && !isInCheck(color)) { // Kingside if (canCastle(color, 'kingside')) { // Check that castling does not violate the king distance rule var castleTx = x + 2; var castleTy = y; var allowCastle = true; if (otherKingPos) { if (Math.abs(castleTx - otherKingPos.x) <= 3 && castleTy === otherKingPos.y) allowCastle = false; if (Math.abs(castleTy - otherKingPos.y) <= 3 && castleTx === otherKingPos.x) allowCastle = false; if (castleTx === otherKingPos.x && castleTy === otherKingPos.y) allowCastle = false; } if (allowCastle) { moves.push({ x: castleTx, y: castleTy, special: 'castle_kingside' }); } } // Queenside if (canCastle(color, 'queenside')) { var castleTx = x - 2; var castleTy = y; var allowCastle = true; if (otherKingPos) { if (Math.abs(castleTx - otherKingPos.x) <= 3 && castleTy === otherKingPos.y) allowCastle = false; if (Math.abs(castleTy - otherKingPos.y) <= 3 && castleTx === otherKingPos.x) allowCastle = false; if (castleTx === otherKingPos.x && castleTy === otherKingPos.y) allowCastle = false; } if (allowCastle) { moves.push({ x: castleTx, y: castleTy, special: 'castle_queenside' }); } } } } } // Sliding pieces else if (piece.type === 'rook' || piece.type === 'bishop' || piece.type === 'queen') { var directions = dirs[piece.type]; for (var d = 0; d < directions.length; d++) { var dx = directions[d][0], dy = directions[d][1]; var tx = x + dx, ty = y + dy; while (isOnBoard(tx, ty)) { var target = board.getPiece(tx, ty); if (!target) { moves.push({ x: tx, y: ty }); } else { if (target.color === opp) { moves.push({ x: tx, y: ty }); } break; } tx += dx; ty += dy; } } } // Remove moves that leave king in check (unless ignoreCheck) if (!ignoreCheck) { var filtered = []; for (var i = 0; i < moves.length; i++) { var m = moves[i]; if (!wouldLeaveKingInCheck(piece, x, y, m.x, m.y, m.special)) { filtered.push(m); } } return filtered; } return moves; } // Would moving piece at (x, y) to (tx, ty) leave own king in check? function wouldLeaveKingInCheck(piece, x, y, tx, ty, special) { // Simulate move var origPiece = board.getPiece(tx, ty); var origEnPassant = enPassantTarget; var origHasMoved = piece.hasMoved; var origFrom = board.squares[y][x]; var origTo = board.squares[ty][tx]; // Remove captured piece if (special === 'enpassant') { var dir = piece.color === 'white' ? -1 : 1; var capY = ty - dir; var capPiece = board.getPiece(tx, capY); board.squares[capY][tx] = null; } board.squares[y][x] = null; board.squares[ty][tx] = piece; // For castling, move rook as well var rookMoved = null, rookFrom = null, rookTo = null; if (special === 'castle_kingside') { var row = piece.color === 'white' ? 7 : 0; rookFrom = board.getPiece(7, row); rookTo = board.getPiece(5, row); board.squares[row][7] = null; board.squares[row][5] = rookFrom; } if (special === 'castle_queenside') { var row = piece.color === 'white' ? 7 : 0; rookFrom = board.getPiece(0, row); rookTo = board.getPiece(3, row); board.squares[row][0] = null; board.squares[row][3] = rookFrom; } var inCheck = isInCheck(piece.color); // Undo move board.squares[y][x] = origFrom; board.squares[ty][tx] = origTo; if (special === 'enpassant') { var dir = piece.color === 'white' ? -1 : 1; var capY = ty - dir; board.squares[capY][tx] = origPiece; } if (special === 'castle_kingside') { var row = piece.color === 'white' ? 7 : 0; board.squares[row][7] = rookFrom; board.squares[row][5] = null; } if (special === 'castle_queenside') { var row = piece.color === 'white' ? 7 : 0; board.squares[row][0] = rookFrom; board.squares[row][3] = null; } piece.hasMoved = origHasMoved; enPassantTarget = origEnPassant; return inCheck; } // Is color's king in check? function isInCheck(color) { var kingPos = findKing(color); if (!kingPos) return false; var opp = color === 'white' ? 'black' : 'white'; for (var y = 0; y < 8; y++) { for (var x = 0; x < 8; x++) { var p = board.getPiece(x, y); if (p && p.color === opp) { var moves = getLegalMoves(p, x, y, true); for (var i = 0; i < moves.length; i++) { if (moves[i].x === kingPos.x && moves[i].y === kingPos.y) { return true; } } } } } return false; } // Find king of color function findKing(color) { for (var y = 0; y < 8; y++) { for (var x = 0; x < 8; x++) { var p = board.getPiece(x, y); if (p && p.type === 'king' && p.color === color) { return { x: x, y: y }; } } } return null; } // Can color castle on side ('kingside' or 'queenside')? function canCastle(color, side) { var row = color === 'white' ? 7 : 0; var king = board.getPiece(4, row); if (!king || king.hasMoved) return false; if (side === 'kingside') { var rook = board.getPiece(7, row); if (!rook || rook.type !== 'rook' || rook.hasMoved) return false; // Squares between must be empty if (board.getPiece(5, row) || board.getPiece(6, row)) return false; // Squares king passes through must not be attacked if (isAttacked(5, row, color) || isAttacked(6, row, color)) return false; return true; } if (side === 'queenside') { var rook = board.getPiece(0, row); if (!rook || rook.type !== 'rook' || rook.hasMoved) return false; if (board.getPiece(1, row) || board.getPiece(2, row) || board.getPiece(3, row)) return false; if (isAttacked(2, row, color) || isAttacked(3, row, color)) return false; return true; } return false; } // Is square (x, y) attacked by opponent of color? function isAttacked(x, y, color) { var opp = color === 'white' ? 'black' : 'white'; for (var yy = 0; yy < 8; yy++) { for (var xx = 0; xx < 8; xx++) { var p = board.getPiece(xx, yy); if (p && p.color === opp) { var moves = getLegalMoves(p, xx, yy, true); for (var i = 0; i < moves.length; i++) { if (moves[i].x === x && moves[i].y === y) { return true; } } } } } return false; } // Get all legal moves for color function getAllLegalMoves(color) { var moves = []; for (var y = 0; y < 8; y++) { for (var x = 0; x < 8; x++) { var p = board.getPiece(x, y); if (p && p.color === color) { var pieceMoves = getLegalMoves(p, x, y, false); for (var i = 0; i < pieceMoves.length; i++) { moves.push({ from: { x: x, y: y }, to: { x: pieceMoves[i].x, y: pieceMoves[i].y }, special: pieceMoves[i].special }); } } } } return moves; } // Is the game over? Returns {over: true/false, result: 'checkmate'/'stalemate'/'draw', winner: 'white'/'black'/null} function checkGameEnd() { var moves = getAllLegalMoves(currentTurn); if (moves.length === 0) { if (isInCheck(currentTurn)) { return { over: true, result: 'checkmate', winner: currentTurn === 'white' ? 'black' : 'white' }; } else { return { over: true, result: 'stalemate', winner: null }; } } // 50-move rule if (halfmoveClock >= 100) { return { over: true, result: 'draw', winner: null }; } // Insufficient material (only kings) var onlyKings = true; for (var y = 0; y < 8; y++) { for (var x = 0; x < 8; x++) { var p = board.getPiece(x, y); if (p && p.type !== 'king') onlyKings = false; } } if (onlyKings) { return { over: true, result: 'draw', winner: null }; } return { over: false }; } // --- Promotion Popup --- function showPromotionPopup(piece, toX, toY, onSelect) { if (promotionPopup) { promotionPopup.destroy(); promotionPopup = null; } promotionPopup = new Container(); var popupW = squareSize * 4; var popupH = squareSize * 1.2; var popupX = boardOriginX + (toX - 1) * squareSize; var popupY = boardOriginY + toY * squareSize - popupH / 2; var bg = LK.getAsset('promotion_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0 }); bg.alpha = 0.95; promotionPopup.addChild(bg); var types = ['queen', 'rook', 'bishop', 'knight']; for (var i = 0; i < types.length; i++) { var t = types[i]; var tempPiece = new ChessPiece(); tempPiece.init(t, piece.color, 0, 0); tempPiece.x = squareSize * (i + 0.5); tempPiece.y = popupH / 2; tempPiece.pieceAsset.width = pieceSize * 0.9; tempPiece.pieceAsset.height = pieceSize * 0.9; (function (type) { tempPiece.down = function (x, y, obj) { if (promotionPopup) { onSelect(type); promotionPopup.destroy(); promotionPopup = null; } }; })(t); promotionPopup.addChild(tempPiece); } promotionPopup.x = popupX; promotionPopup.y = popupY; game.addChild(promotionPopup); } // --- Tap-to-select and tap-to-move --- // No dragging variables needed // --- Event Handlers --- // Handle touch/mouse down (tap) game.down = function (x, y, obj) { if (gameEnded) return; if (promotionPopup) return; // Block input during promotion var pos = screenToBoard(x, y); if (!pos) return; var p = board.getPiece(pos.x, pos.y); // If a piece is already selected and the tap is on a legal move, move there if (selectedPiece && legalMoves.length > 0) { for (var i = 0; i < legalMoves.length; i++) { var m = legalMoves[i]; if (m.x === pos.x && m.y === pos.y) { // Move piece doMove(selectedPiece, selectedPiece.boardX, selectedPiece.boardY, m.x, m.y, m.special); board.clearHighlights(); selectedPiece = null; legalMoves = []; return; } } } // If tap is on a piece of current turn, select it and show moves if (p && p.color === currentTurn) { selectedPiece = p; legalMoves = getLegalMoves(p, pos.x, pos.y, false); board.showHighlights(legalMoves); return; } // If tap is on empty square or not a legal move, clear selection board.clearHighlights(); selectedPiece = null; legalMoves = []; }; // No dragging, so move and up handlers are not needed game.move = function (x, y, obj) {}; game.up = function (x, y, obj) {}; // --- Move Execution --- function doMove(piece, fromX, fromY, toX, toY, special) { var opp = piece.color === 'white' ? 'black' : 'white'; var captured = null; var isPawnMove = piece.type === 'pawn'; var isCapture = false; // Handle en passant if (special === 'enpassant') { var dir = piece.color === 'white' ? -1 : 1; var capY = toY - dir; captured = board.getPiece(toX, capY); board.removePiece(toX, capY); isCapture = true; } // Handle castling if (special === 'castle_kingside') { var row = piece.color === 'white' ? 7 : 0; var rook = board.getPiece(7, row); board.setPiece(5, row, rook); board.setPiece(7, row, null); rook.moveTo(5, row, true); rook.hasMoved = true; } if (special === 'castle_queenside') { var row = piece.color === 'white' ? 7 : 0; var rook = board.getPiece(0, row); board.setPiece(3, row, rook); board.setPiece(0, row, null); rook.moveTo(3, row, true); rook.hasMoved = true; } // Remove captured piece var target = board.getPiece(toX, toY); if (target && target.color !== piece.color) { captured = target; board.removePiece(toX, toY); isCapture = true; } // Move piece board.setPiece(fromX, fromY, null); board.setPiece(toX, toY, piece); piece.moveTo(toX, toY, true); piece.hasMoved = true; // Play move sound LK.getSound('tas').play(); // Pawn promotion if (piece.type === 'pawn' && (toY === 0 || toY === 7)) { showPromotionPopup(piece, toX, toY, function (newType) { piece.type = newType; piece.promoted = newType === 'queen'; piece.init(newType, piece.color, toX, toY); endTurn(piece, fromX, fromY, toX, toY, special, captured, isPawnMove, isCapture); }); return; } endTurn(piece, fromX, fromY, toX, toY, special, captured, isPawnMove, isCapture); } function endTurn(piece, fromX, fromY, toX, toY, special, captured, isPawnMove, isCapture) { // Update en passant if (piece.type === 'pawn' && Math.abs(toY - fromY) === 2) { enPassantTarget = { x: toX, y: (fromY + toY) / 2 }; } else { enPassantTarget = null; } // Update halfmove clock if (isPawnMove || isCapture) { halfmoveClock = 0; } else { halfmoveClock++; } // Update fullmove number if (currentTurn === 'black') { fullmoveNumber++; } // Save last move lastMove = { from: { x: fromX, y: fromY }, to: { x: toX, y: toY }, piece: piece, captured: captured, special: special }; // Switch turn if (currentTurn === 'white') { stopWhiteTimer(); startBlackTimer(); currentTurn = 'black'; } else { stopBlackTimer(); startWhiteTimer(); currentTurn = 'white'; } turnText.setText((currentTurn === 'white' ? 'White' : 'Black') + ' to move'); // Check for game end var end = checkGameEnd(); if (end.over) { gameEnded = true; stopAllTimers(); if (end.result === 'checkmate') { LK.effects.flashScreen(0x00ff00, 800); if (end.winner === 'white') { LK.showYouWin('White wins by checkmate!'); } else if (end.winner === 'black') { LK.showYouWin('Black wins by checkmate!'); } } else if (end.result === 'stalemate') { LK.effects.flashScreen(0xffff00, 800); LK.showYouWin('Draw by stalemate'); } else if (end.result === 'draw') { LK.effects.flashScreen(0xffff00, 800); LK.showYouWin('Draw'); } return; } // --- AI Move for Black --- if (currentTurn === 'black' && !gameEnded) { // --- AI: If an important piece is threatened, try to move it away --- // Define important pieces var importantTypes = { queen: true, rook: true, bishop: true, knight: true }; // Find all black important pieces that are threatened var threatenedPieces = []; for (var y = 0; y < 8; y++) { for (var x = 0; x < 8; x++) { var p = board.getPiece(x, y); if (p && p.color === 'black' && importantTypes[p.type]) { if (isAttacked(x, y, 'black')) { threatenedPieces.push({ piece: p, x: x, y: y }); } } } } // If any important piece is threatened, try to move it to safety var aiMoves = getAllLegalMoves('black'); if (threatenedPieces.length > 0) { // Try to find a move that moves a threatened important piece to a safe square var safeMoves = []; for (var i = 0; i < threatenedPieces.length; i++) { var tp = threatenedPieces[i]; var moves = getLegalMoves(tp.piece, tp.x, tp.y, false); for (var j = 0; j < moves.length; j++) { var m = moves[j]; // Simulate the move var origX = tp.piece.boardX, origY = tp.piece.boardY; var origTo = board.getPiece(m.x, m.y); board.squares[tp.y][tp.x] = null; board.squares[m.y][m.x] = tp.piece; tp.piece.boardX = m.x; tp.piece.boardY = m.y; var stillAttacked = isAttacked(m.x, m.y, 'black'); // Undo board.squares[tp.y][tp.x] = tp.piece; board.squares[m.y][m.x] = origTo; tp.piece.boardX = origX; tp.piece.boardY = origY; if (!stillAttacked) { // Find the corresponding move in aiMoves for (var k = 0; k < aiMoves.length; k++) { var am = aiMoves[k]; if (am.from.x === tp.x && am.from.y === tp.y && am.to.x === m.x && am.to.y === m.y) { safeMoves.push(am); } } } } } // If there are safe moves, pick one randomly and play it if (safeMoves.length > 0) { var chosen = safeMoves[Math.floor(Math.random() * safeMoves.length)]; var piece = board.getPiece(chosen.from.x, chosen.from.y); var delay = 400; if (chosen.special === 'castle_kingside' || chosen.special === 'castle_queenside') delay = 900;else if (piece && piece.type === 'pawn' && (chosen.to.y === 0 || chosen.to.y === 7)) delay = 900;else if (board.getPiece(chosen.to.x, chosen.to.y)) delay = 700;else delay = 350; LK.setTimeout(function () { if (piece) { doMove(piece, chosen.from.x, chosen.from.y, chosen.to.x, chosen.to.y, chosen.special); } }, delay); return; } // If no safe move, fallback to normal AI below } // --- AI tries to mimic player's last move style --- // We'll try to find a move for black that is similar in type to the player's last move. // If not possible, fallback to best move as before. // Helper: get move type string var getMoveType = function getMoveType(move) { if (!move) return ''; if (move.special === 'castle_kingside' || move.special === 'castle_queenside') return 'castle'; if (move.special === 'enpassant') return 'enpassant'; if (move.piece && move.piece.type === 'pawn' && (move.to.y === 0 || move.to.y === 7)) return 'promotion'; if (move.captured) return 'capture'; if (move.piece && move.piece.type === 'pawn') return 'pawn'; if (move.piece && move.piece.type === 'knight') return 'knight'; if (move.piece && move.piece.type === 'bishop') return 'bishop'; if (move.piece && move.piece.type === 'rook') return 'rook'; if (move.piece && move.piece.type === 'queen') return 'queen'; if (move.piece && move.piece.type === 'king') return 'king'; return ''; }; // Helper: get move distance (for delay) var getMoveDistance = function getMoveDistance(move) { if (!move) return 1; var dx = Math.abs(move.from.x - move.to.x); var dy = Math.abs(move.from.y - move.to.y); return Math.max(dx, dy); }; // Get all legal moves for black // aiMoves already defined above if (aiMoves.length > 0) { // Use a simple minimax (depth 2) with material evaluation var evaluateBoard = function evaluateBoard() { var values = { pawn: 1, knight: 3, bishop: 3, rook: 5, queen: 9, king: 0 }; var score = 0; for (var y = 0; y < 8; y++) { for (var x = 0; x < 8; x++) { var p = board.getPiece(x, y); if (p) { var val = values[p.type] || 0; score += p.color === 'white' ? val : -val; } } } return score; }; var simulateMove = function simulateMove(move) { var piece = board.getPiece(move.from.x, move.from.y); var target = board.getPiece(move.to.x, move.to.y); var fromX = move.from.x, fromY = move.from.y, toX = move.to.x, toY = move.to.y; var special = move.special; var origHasMoved = piece.hasMoved; var origEnPassant = enPassantTarget; var captured = null; // Handle en passant if (special === 'enpassant') { var dir = piece.color === 'white' ? -1 : 1; var capY = toY - dir; captured = board.getPiece(toX, capY); board.squares[capY][toX] = null; } // Handle castling var rook = null, rookFrom = null, rookTo = null; if (special === 'castle_kingside') { var row = piece.color === 'white' ? 7 : 0; rook = board.getPiece(7, row); board.squares[row][5] = rook; board.squares[row][7] = null; } if (special === 'castle_queenside') { var row = piece.color === 'white' ? 7 : 0; rook = board.getPiece(0, row); board.squares[row][3] = rook; board.squares[row][0] = null; } // Remove captured piece if (target && target.color !== piece.color) { board.squares[toY][toX] = null; } // Move piece board.squares[fromY][fromX] = null; board.squares[toY][toX] = piece; piece.hasMoved = true; // Pawn promotion (always promote to queen for AI) var promoted = false; var oldType = piece.type; if (piece.type === 'pawn' && (toY === 0 || toY === 7)) { piece.type = 'queen'; promoted = true; } return function undo() { if (promoted) piece.type = oldType; // Undo move board.squares[fromY][fromX] = piece; board.squares[toY][toX] = target; piece.hasMoved = origHasMoved; // Undo en passant if (special === 'enpassant') { var dir = piece.color === 'white' ? -1 : 1; var capY = toY - dir; board.squares[capY][toX] = captured; } // Undo castling if (special === 'castle_kingside') { var row = piece.color === 'white' ? 7 : 0; board.squares[row][7] = board.squares[row][5]; board.squares[row][5] = null; } if (special === 'castle_queenside') { var row = piece.color === 'white' ? 7 : 0; board.squares[row][0] = board.squares[row][3]; board.squares[row][3] = null; } enPassantTarget = origEnPassant; }; }; // Try to mimic player's last move var mimicMove = null; var mimicType = getMoveType(lastMove); var mimicPieceType = lastMove && lastMove.piece ? lastMove.piece.type : null; var mimicCapture = lastMove && lastMove.captured ? true : false; var mimicSpecial = lastMove ? lastMove.special : null; // Find all black moves that match the last move's type var mimicCandidates = []; for (var i = 0; i < aiMoves.length; i++) { var move = aiMoves[i]; var piece = board.getPiece(move.from.x, move.from.y); if (!piece) continue; // Try to match special moves if (mimicSpecial && move.special === mimicSpecial) { mimicCandidates.push(move); continue; } // Try to match captures if (mimicCapture) { var target = board.getPiece(move.to.x, move.to.y); if (target && target.color === 'white') { mimicCandidates.push(move); continue; } } // Try to match piece type if (mimicPieceType && piece.type === mimicPieceType) { mimicCandidates.push(move); continue; } // Try to match move type if (getMoveType({ piece: piece, to: move.to, special: move.special, captured: board.getPiece(move.to.x, move.to.y) }) === mimicType) { mimicCandidates.push(move); continue; } } // If we have mimic candidates, pick the best among them using minimax var candidateMoves = mimicCandidates.length > 0 ? mimicCandidates : aiMoves; // Minimax, depth 2 (AI: black, Player: white) var bestScore = null; var bestMoves = []; for (var i = 0; i < candidateMoves.length; i++) { var move = candidateMoves[i]; var undo = simulateMove(move); // After black's move, get all white replies var whiteMoves = getAllLegalMoves('white'); var worstScore = null; if (whiteMoves.length === 0) { // If white has no moves, checkmate or stalemate if (isInCheck('white')) { // Black wins worstScore = -9999; } else { // Draw worstScore = 0; } } else { for (var j = 0; j < whiteMoves.length; j++) { var wmove = whiteMoves[j]; var wundo = simulateMove(wmove); var score = evaluateBoard(); if (worstScore === null || score > worstScore) { worstScore = score; } wundo(); } } undo(); // AI wants to minimize the score (since black is negative) if (bestScore === null || worstScore < bestScore) { bestScore = worstScore; bestMoves = [move]; } else if (worstScore === bestScore) { bestMoves.push(move); } } // Pick randomly among best moves var chosen = bestMoves[Math.floor(Math.random() * bestMoves.length)]; var piece = board.getPiece(chosen.from.x, chosen.from.y); // Add a delay based on move complexity (longer for captures, promotions, castling, or long moves) var delay = 400; if (chosen.special === 'castle_kingside' || chosen.special === 'castle_queenside') delay = 900;else if (piece && piece.type === 'pawn' && (chosen.to.y === 0 || chosen.to.y === 7)) delay = 900;else if (board.getPiece(chosen.to.x, chosen.to.y)) delay = 700;else delay = 350 + 80 * getMoveDistance(chosen); LK.setTimeout(function () { if (piece) { doMove(piece, chosen.from.x, chosen.from.y, chosen.to.x, chosen.to.y, chosen.special); } }, delay); } } } // --- Game Update --- game.update = function () { // No per-frame logic needed for chess }; // --- Reset on Game Over --- LK.on('gameover', function () { // Reset everything board.reset(); currentTurn = 'white'; selectedPiece = null; legalMoves = []; lastMove = null; enPassantTarget = null; halfmoveClock = 0; fullmoveNumber = 1; gameEnded = false; turnText.setText('White to move'); if (promotionPopup) { promotionPopup.destroy(); promotionPopup = null; } // Reset timers stopAllTimers(); whiteTime = 600; blackTime = 600; whiteTimerText.setText(formatTime(whiteTime)); blackTimerText.setText(formatTime(blackTime)); startWhiteTimer(); });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// --- ChessBoard Class ---
var ChessBoard = Container.expand(function () {
var self = Container.call(this);
// 8x8 array of ChessPiece or null
self.squares = [];
for (var y = 0; y < 8; y++) {
self.squares[y] = [];
for (var x = 0; x < 8; x++) {
self.squares[y][x] = null;
}
}
// Draw board squares
self.squareNodes = [];
for (var y = 0; y < 8; y++) {
self.squareNodes[y] = [];
for (var x = 0; x < 8; x++) {
var isLight = (x + y) % 2 === 0;
// Use classic chessboard colors: light = #f0d9b5, dark = #b58863
var color = isLight ? 0xf0d9b5 : 0xb58863;
var assetId = 'board_' + (isLight ? 'light' : 'dark');
var node = LK.getAsset(assetId, {
anchorX: 0,
anchorY: 0,
x: boardOriginX + x * squareSize,
y: boardOriginY + y * squareSize,
width: squareSize,
height: squareSize,
color: color,
shape: 'box'
});
self.addChild(node);
self.squareNodes[y][x] = node;
}
}
// Highlight squares (for moves)
self.highlightNodes = [];
for (var i = 0; i < 64; i++) {
var highlightId = 'highlight_' + i;
// Use a circle (ellipse) and center it in the square
var node = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: Math.floor(squareSize * 0.45),
height: Math.floor(squareSize * 0.45),
x: -1000,
y: -1000
}); // Hide initially
node.alpha = 0.25;
self.addChild(node);
self.highlightNodes[i] = node;
}
// Place pieces in starting position
self.reset = function () {
// Remove all pieces
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
if (self.squares[y][x]) {
self.squares[y][x].destroy();
self.squares[y][x] = null;
}
}
}
// Place pawns
for (var x = 0; x < 8; x++) {
var wp = new ChessPiece();
wp.init('pawn', 'white', x, 6);
wp.moveTo(x, 6, false);
self.addChild(wp);
self.squares[6][x] = wp;
var bp = new ChessPiece();
bp.init('pawn', 'black', x, 1);
bp.moveTo(x, 1, false);
self.addChild(bp);
self.squares[1][x] = bp;
}
// Place other pieces
var order = ['rook', 'knight', 'bishop', 'queen', 'king', 'bishop', 'knight', 'rook'];
for (var x = 0; x < 8; x++) {
var w = new ChessPiece();
w.init(order[x], 'white', x, 7);
w.moveTo(x, 7, false);
self.addChild(w);
self.squares[7][x] = w;
var b = new ChessPiece();
b.init(order[x], 'black', x, 0);
b.moveTo(x, 0, false);
self.addChild(b);
self.squares[0][x] = b;
}
};
// Remove all highlights
self.clearHighlights = function () {
for (var i = 0; i < 64; i++) {
self.highlightNodes[i].x = -1000;
self.highlightNodes[i].y = -1000;
}
};
// Highlight given squares (array of {x, y})
self.showHighlights = function (squares) {
self.clearHighlights();
for (var i = 0; i < squares.length; i++) {
var idx = squares[i].y * 8 + squares[i].x;
var node = self.highlightNodes[idx];
// Center the highlight circle in the square
node.x = boardOriginX + squares[i].x * squareSize + squareSize / 2;
node.y = boardOriginY + squares[i].y * squareSize + squareSize / 2;
}
};
// Get piece at board position
self.getPiece = function (x, y) {
if (x < 0 || x > 7 || y < 0 || y > 7) return null;
return self.squares[y][x];
};
// Set piece at board position
self.setPiece = function (x, y, piece) {
self.squares[y][x] = piece;
};
// Remove piece at board position
self.removePiece = function (x, y) {
var p = self.squares[y][x];
if (p) {
p.destroy();
self.squares[y][x] = null;
}
};
return self;
});
// --- ChessPiece Class ---
var ChessPiece = Container.expand(function () {
var self = Container.call(this);
// Properties
self.type = null; // 'pawn', 'rook', 'knight', 'bishop', 'queen', 'king'
self.color = null; // 'white' or 'black'
self.boardX = 0; // 0-7
self.boardY = 0; // 0-7
self.hasMoved = false; // For castling/en passant
self.promoted = false; // For pawn promotion
// Asset
self.pieceAsset = null;
// Set piece type and color, and create asset
self.init = function (type, color, boardX, boardY) {
self.type = type;
self.color = color;
self.boardX = boardX;
self.boardY = boardY;
self.hasMoved = false;
self.promoted = false;
// Remove old asset if any
if (self.pieceAsset) {
self.removeChild(self.pieceAsset);
}
// Use classic piece colors: white = #fff, black = #222
var colorMap = {
'white': {
'pawn': 0xffffff,
'rook': 0xffffff,
'knight': 0xffffff,
'bishop': 0xffffff,
'queen': 0xffffff,
'king': 0xffffff
},
'black': {
'pawn': 0x222222,
'rook': 0x222222,
'knight': 0x222222,
'bishop': 0x222222,
'queen': 0x222222,
'king': 0x222222
}
};
var shape = self.type === 'pawn' || self.type === 'bishop' ? 'ellipse' : 'box';
var assetId = self.color + '_' + self.type;
var assetColor = colorMap[self.color][self.type];
var size = self.type === 'king' ? Math.floor(pieceSize * 1.25) : pieceSize;
self.pieceAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
width: size,
height: size,
color: assetColor,
shape: shape
});
// Add a crown for promoted pawns
if (self.promoted && self.type === 'queen') {
// Overlay a small yellow ellipse as a "crown"
var crown = LK.getAsset('crown_' + self.color, {
anchorX: 0.5,
anchorY: 0.5,
y: -size * 0.3
});
self.addChild(crown);
}
};
// Move piece to board position (with optional animation)
self.moveTo = function (bx, by, animate) {
self.boardX = bx;
self.boardY = by;
var targetX = boardOriginX + bx * squareSize + squareSize / 2;
var targetY = boardOriginY + by * squareSize + squareSize / 2;
if (animate) {
tween(self, {
x: targetX,
y: targetY
}, {
duration: 180,
easing: tween.cubicOut
});
} else {
self.x = targetX;
self.y = targetY;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// --- Board and Piece Sizes ---
// We need tween for piece movement animations
// Board squares
// White pieces
// Black pieces
// Crown for promoted pawn (queen)
var boardSize = Math.min(2048, 2048); // Square board, fit width
var squareSize = Math.floor(boardSize / 8);
var pieceSize = Math.floor(squareSize * 0.8);
var boardOriginX = Math.floor((2048 - squareSize * 8) / 2);
var boardOriginY = Math.floor((2732 - squareSize * 8) / 2);
// --- Game State ---
var board = new ChessBoard();
game.addChild(board);
board.reset();
var currentTurn = 'white'; // 'white' or 'black'
var selectedPiece = null;
var legalMoves = []; // Array of {x, y, special}
var lastMove = null; // {from: {x,y}, to: {x,y}, piece, captured, special}
var enPassantTarget = null; // {x, y} or null
var halfmoveClock = 0; // For 50-move rule
var fullmoveNumber = 1;
var gameEnded = false;
// --- GUI: Turn Display ---
var turnText = new Text2('White to move', {
size: 90,
fill: "#fff"
});
turnText.anchor.set(0.5, 0);
LK.gui.top.addChild(turnText);
// --- GUI: Timers ---
// Each player starts with 10 minutes (600 seconds)
var whiteTime = 600;
var blackTime = 600;
var whiteTimerText = new Text2('10:00', {
size: 90,
fill: "#fff"
});
// Place white timer near bottom left, above white's side, but not in the bottom left 100x100 reserved area
whiteTimerText.anchor.set(0, 1); // left aligned, bottom
whiteTimerText.x = 320; // moved even further right
whiteTimerText.y = LK.gui.height - 40;
LK.gui.bottomLeft.addChild(whiteTimerText);
var blackTimerText = new Text2('10:00', {
size: 90,
fill: "#fff"
});
// Place black timer near top right, above black's side, but not in the top right 100x100 reserved area
blackTimerText.anchor.set(1, 0); // right aligned, top
blackTimerText.x = LK.gui.width - 40; // moved even further right (inward)
blackTimerText.y = 0;
LK.gui.topRight.addChild(blackTimerText);
// Timer state
var whiteTimerInterval = null;
var blackTimerInterval = null;
// Helper to format seconds as MM:SS
function formatTime(secs) {
var m = Math.floor(secs / 60);
var s = Math.floor(secs % 60);
return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
}
// Start/stop timer helpers
function startWhiteTimer() {
if (whiteTimerInterval) LK.clearInterval(whiteTimerInterval);
whiteTimerInterval = LK.setInterval(function () {
if (gameEnded) return;
whiteTime--;
if (whiteTime < 0) whiteTime = 0;
whiteTimerText.setText(formatTime(whiteTime));
if (whiteTime === 0) {
gameEnded = true;
LK.effects.flashScreen(0xff0000, 800);
LK.showYouWin('Black wins on time!');
stopAllTimers();
}
}, 1000);
}
function startBlackTimer() {
if (blackTimerInterval) LK.clearInterval(blackTimerInterval);
blackTimerInterval = LK.setInterval(function () {
if (gameEnded) return;
blackTime--;
if (blackTime < 0) blackTime = 0;
blackTimerText.setText(formatTime(blackTime));
if (blackTime === 0) {
gameEnded = true;
LK.effects.flashScreen(0xff0000, 800);
LK.showYouWin('White wins on time!');
stopAllTimers();
}
}, 1000);
}
function stopWhiteTimer() {
if (whiteTimerInterval) {
LK.clearInterval(whiteTimerInterval);
whiteTimerInterval = null;
}
}
function stopBlackTimer() {
if (blackTimerInterval) {
LK.clearInterval(blackTimerInterval);
blackTimerInterval = null;
}
}
function stopAllTimers() {
stopWhiteTimer();
stopBlackTimer();
}
// Start white's timer at game start
startWhiteTimer();
whiteTimerText.setText(formatTime(whiteTime));
blackTimerText.setText(formatTime(blackTime));
// --- GUI: Promotion Popup ---
var promotionPopup = null;
// --- Helper Functions ---
// Convert screen (x, y) to board (bx, by)
function screenToBoard(x, y) {
var bx = Math.floor((x - boardOriginX) / squareSize);
var by = Math.floor((y - boardOriginY) / squareSize);
if (bx < 0 || bx > 7 || by < 0 || by > 7) return null;
return {
x: bx,
y: by
};
}
// Convert board (bx, by) to screen (x, y) (center of square)
function boardToScreen(bx, by) {
return {
x: boardOriginX + bx * squareSize + squareSize / 2,
y: boardOriginY + by * squareSize + squareSize / 2
};
}
// Is (x, y) on the board?
function isOnBoard(x, y) {
return x >= 0 && x < 8 && y >= 0 && y < 8;
}
// Get all legal moves for a piece at (x, y)
function getLegalMoves(piece, x, y, ignoreCheck) {
var moves = [];
if (!piece) return moves;
var color = piece.color;
var opp = color === 'white' ? 'black' : 'white';
// Directions for sliding pieces
var dirs = {
'rook': [[1, 0], [-1, 0], [0, 1], [0, -1]],
'bishop': [[1, 1], [1, -1], [-1, 1], [-1, -1]],
'queen': [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]]
};
// Pawn moves
if (piece.type === 'pawn') {
var dir = color === 'white' ? -1 : 1;
// Forward
if (isOnBoard(x, y + dir) && !board.getPiece(x, y + dir)) {
moves.push({
x: x,
y: y + dir
});
// Double move from start
if (!piece.hasMoved && isOnBoard(x, y + 2 * dir) && !board.getPiece(x, y + 2 * dir)) {
moves.push({
x: x,
y: y + 2 * dir,
special: 'double'
});
}
}
// Captures
for (var dx = -1; dx <= 1; dx += 2) {
var tx = x + dx,
ty = y + dir;
if (isOnBoard(tx, ty)) {
var target = board.getPiece(tx, ty);
if (target && target.color === opp) {
moves.push({
x: tx,
y: ty
});
}
}
}
// En passant
if (enPassantTarget) {
if (Math.abs(enPassantTarget.x - x) === 1 && enPassantTarget.y === y + dir) {
moves.push({
x: enPassantTarget.x,
y: enPassantTarget.y,
special: 'enpassant'
});
}
}
}
// Knight moves
else if (piece.type === 'knight') {
var knightDeltas = [[1, 2], [2, 1], [-1, 2], [-2, 1], [1, -2], [2, -1], [-1, -2], [-2, -1]];
for (var i = 0; i < knightDeltas.length; i++) {
var dx = knightDeltas[i][0],
dy = knightDeltas[i][1];
var tx = x + dx,
ty = y + dy;
if (isOnBoard(tx, ty)) {
var target = board.getPiece(tx, ty);
if (!target || target.color === opp) {
moves.push({
x: tx,
y: ty
});
}
}
}
}
// King moves (including castling)
else if (piece.type === 'king') {
// Prevent infinite recursion: when ignoreCheck is true, do NOT generate king moves
if (ignoreCheck) {
// Do not generate king moves when ignoreCheck is true (used for attack detection)
// This prevents infinite recursion between isInCheck and getLegalMoves
} else {
// Find the other king's position
var otherKingPos = null;
for (var yy = 0; yy < 8; yy++) {
for (var xx = 0; xx < 8; xx++) {
var p = board.getPiece(xx, yy);
if (p && p.type === 'king' && p.color !== color) {
otherKingPos = {
x: xx,
y: yy
};
break;
}
}
if (otherKingPos) break;
}
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
if (dx === 0 && dy === 0) continue;
var tx = x + dx,
ty = y + dy;
if (isOnBoard(tx, ty)) {
// Prevent moving within 3 squares horizontally or vertically of the other king
var allow = true;
if (otherKingPos) {
if (Math.abs(tx - otherKingPos.x) <= 3 && ty === otherKingPos.y) allow = false;
if (Math.abs(ty - otherKingPos.y) <= 3 && tx === otherKingPos.x) allow = false;
if (tx === otherKingPos.x && ty === otherKingPos.y) allow = false;
}
var target = board.getPiece(tx, ty);
if ((!target || target.color === opp) && allow) {
moves.push({
x: tx,
y: ty
});
}
}
}
}
// Castling
if (!piece.hasMoved && !isInCheck(color)) {
// Kingside
if (canCastle(color, 'kingside')) {
// Check that castling does not violate the king distance rule
var castleTx = x + 2;
var castleTy = y;
var allowCastle = true;
if (otherKingPos) {
if (Math.abs(castleTx - otherKingPos.x) <= 3 && castleTy === otherKingPos.y) allowCastle = false;
if (Math.abs(castleTy - otherKingPos.y) <= 3 && castleTx === otherKingPos.x) allowCastle = false;
if (castleTx === otherKingPos.x && castleTy === otherKingPos.y) allowCastle = false;
}
if (allowCastle) {
moves.push({
x: castleTx,
y: castleTy,
special: 'castle_kingside'
});
}
}
// Queenside
if (canCastle(color, 'queenside')) {
var castleTx = x - 2;
var castleTy = y;
var allowCastle = true;
if (otherKingPos) {
if (Math.abs(castleTx - otherKingPos.x) <= 3 && castleTy === otherKingPos.y) allowCastle = false;
if (Math.abs(castleTy - otherKingPos.y) <= 3 && castleTx === otherKingPos.x) allowCastle = false;
if (castleTx === otherKingPos.x && castleTy === otherKingPos.y) allowCastle = false;
}
if (allowCastle) {
moves.push({
x: castleTx,
y: castleTy,
special: 'castle_queenside'
});
}
}
}
}
}
// Sliding pieces
else if (piece.type === 'rook' || piece.type === 'bishop' || piece.type === 'queen') {
var directions = dirs[piece.type];
for (var d = 0; d < directions.length; d++) {
var dx = directions[d][0],
dy = directions[d][1];
var tx = x + dx,
ty = y + dy;
while (isOnBoard(tx, ty)) {
var target = board.getPiece(tx, ty);
if (!target) {
moves.push({
x: tx,
y: ty
});
} else {
if (target.color === opp) {
moves.push({
x: tx,
y: ty
});
}
break;
}
tx += dx;
ty += dy;
}
}
}
// Remove moves that leave king in check (unless ignoreCheck)
if (!ignoreCheck) {
var filtered = [];
for (var i = 0; i < moves.length; i++) {
var m = moves[i];
if (!wouldLeaveKingInCheck(piece, x, y, m.x, m.y, m.special)) {
filtered.push(m);
}
}
return filtered;
}
return moves;
}
// Would moving piece at (x, y) to (tx, ty) leave own king in check?
function wouldLeaveKingInCheck(piece, x, y, tx, ty, special) {
// Simulate move
var origPiece = board.getPiece(tx, ty);
var origEnPassant = enPassantTarget;
var origHasMoved = piece.hasMoved;
var origFrom = board.squares[y][x];
var origTo = board.squares[ty][tx];
// Remove captured piece
if (special === 'enpassant') {
var dir = piece.color === 'white' ? -1 : 1;
var capY = ty - dir;
var capPiece = board.getPiece(tx, capY);
board.squares[capY][tx] = null;
}
board.squares[y][x] = null;
board.squares[ty][tx] = piece;
// For castling, move rook as well
var rookMoved = null,
rookFrom = null,
rookTo = null;
if (special === 'castle_kingside') {
var row = piece.color === 'white' ? 7 : 0;
rookFrom = board.getPiece(7, row);
rookTo = board.getPiece(5, row);
board.squares[row][7] = null;
board.squares[row][5] = rookFrom;
}
if (special === 'castle_queenside') {
var row = piece.color === 'white' ? 7 : 0;
rookFrom = board.getPiece(0, row);
rookTo = board.getPiece(3, row);
board.squares[row][0] = null;
board.squares[row][3] = rookFrom;
}
var inCheck = isInCheck(piece.color);
// Undo move
board.squares[y][x] = origFrom;
board.squares[ty][tx] = origTo;
if (special === 'enpassant') {
var dir = piece.color === 'white' ? -1 : 1;
var capY = ty - dir;
board.squares[capY][tx] = origPiece;
}
if (special === 'castle_kingside') {
var row = piece.color === 'white' ? 7 : 0;
board.squares[row][7] = rookFrom;
board.squares[row][5] = null;
}
if (special === 'castle_queenside') {
var row = piece.color === 'white' ? 7 : 0;
board.squares[row][0] = rookFrom;
board.squares[row][3] = null;
}
piece.hasMoved = origHasMoved;
enPassantTarget = origEnPassant;
return inCheck;
}
// Is color's king in check?
function isInCheck(color) {
var kingPos = findKing(color);
if (!kingPos) return false;
var opp = color === 'white' ? 'black' : 'white';
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
var p = board.getPiece(x, y);
if (p && p.color === opp) {
var moves = getLegalMoves(p, x, y, true);
for (var i = 0; i < moves.length; i++) {
if (moves[i].x === kingPos.x && moves[i].y === kingPos.y) {
return true;
}
}
}
}
}
return false;
}
// Find king of color
function findKing(color) {
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
var p = board.getPiece(x, y);
if (p && p.type === 'king' && p.color === color) {
return {
x: x,
y: y
};
}
}
}
return null;
}
// Can color castle on side ('kingside' or 'queenside')?
function canCastle(color, side) {
var row = color === 'white' ? 7 : 0;
var king = board.getPiece(4, row);
if (!king || king.hasMoved) return false;
if (side === 'kingside') {
var rook = board.getPiece(7, row);
if (!rook || rook.type !== 'rook' || rook.hasMoved) return false;
// Squares between must be empty
if (board.getPiece(5, row) || board.getPiece(6, row)) return false;
// Squares king passes through must not be attacked
if (isAttacked(5, row, color) || isAttacked(6, row, color)) return false;
return true;
}
if (side === 'queenside') {
var rook = board.getPiece(0, row);
if (!rook || rook.type !== 'rook' || rook.hasMoved) return false;
if (board.getPiece(1, row) || board.getPiece(2, row) || board.getPiece(3, row)) return false;
if (isAttacked(2, row, color) || isAttacked(3, row, color)) return false;
return true;
}
return false;
}
// Is square (x, y) attacked by opponent of color?
function isAttacked(x, y, color) {
var opp = color === 'white' ? 'black' : 'white';
for (var yy = 0; yy < 8; yy++) {
for (var xx = 0; xx < 8; xx++) {
var p = board.getPiece(xx, yy);
if (p && p.color === opp) {
var moves = getLegalMoves(p, xx, yy, true);
for (var i = 0; i < moves.length; i++) {
if (moves[i].x === x && moves[i].y === y) {
return true;
}
}
}
}
}
return false;
}
// Get all legal moves for color
function getAllLegalMoves(color) {
var moves = [];
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
var p = board.getPiece(x, y);
if (p && p.color === color) {
var pieceMoves = getLegalMoves(p, x, y, false);
for (var i = 0; i < pieceMoves.length; i++) {
moves.push({
from: {
x: x,
y: y
},
to: {
x: pieceMoves[i].x,
y: pieceMoves[i].y
},
special: pieceMoves[i].special
});
}
}
}
}
return moves;
}
// Is the game over? Returns {over: true/false, result: 'checkmate'/'stalemate'/'draw', winner: 'white'/'black'/null}
function checkGameEnd() {
var moves = getAllLegalMoves(currentTurn);
if (moves.length === 0) {
if (isInCheck(currentTurn)) {
return {
over: true,
result: 'checkmate',
winner: currentTurn === 'white' ? 'black' : 'white'
};
} else {
return {
over: true,
result: 'stalemate',
winner: null
};
}
}
// 50-move rule
if (halfmoveClock >= 100) {
return {
over: true,
result: 'draw',
winner: null
};
}
// Insufficient material (only kings)
var onlyKings = true;
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
var p = board.getPiece(x, y);
if (p && p.type !== 'king') onlyKings = false;
}
}
if (onlyKings) {
return {
over: true,
result: 'draw',
winner: null
};
}
return {
over: false
};
}
// --- Promotion Popup ---
function showPromotionPopup(piece, toX, toY, onSelect) {
if (promotionPopup) {
promotionPopup.destroy();
promotionPopup = null;
}
promotionPopup = new Container();
var popupW = squareSize * 4;
var popupH = squareSize * 1.2;
var popupX = boardOriginX + (toX - 1) * squareSize;
var popupY = boardOriginY + toY * squareSize - popupH / 2;
var bg = LK.getAsset('promotion_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
bg.alpha = 0.95;
promotionPopup.addChild(bg);
var types = ['queen', 'rook', 'bishop', 'knight'];
for (var i = 0; i < types.length; i++) {
var t = types[i];
var tempPiece = new ChessPiece();
tempPiece.init(t, piece.color, 0, 0);
tempPiece.x = squareSize * (i + 0.5);
tempPiece.y = popupH / 2;
tempPiece.pieceAsset.width = pieceSize * 0.9;
tempPiece.pieceAsset.height = pieceSize * 0.9;
(function (type) {
tempPiece.down = function (x, y, obj) {
if (promotionPopup) {
onSelect(type);
promotionPopup.destroy();
promotionPopup = null;
}
};
})(t);
promotionPopup.addChild(tempPiece);
}
promotionPopup.x = popupX;
promotionPopup.y = popupY;
game.addChild(promotionPopup);
}
// --- Tap-to-select and tap-to-move ---
// No dragging variables needed
// --- Event Handlers ---
// Handle touch/mouse down (tap)
game.down = function (x, y, obj) {
if (gameEnded) return;
if (promotionPopup) return; // Block input during promotion
var pos = screenToBoard(x, y);
if (!pos) return;
var p = board.getPiece(pos.x, pos.y);
// If a piece is already selected and the tap is on a legal move, move there
if (selectedPiece && legalMoves.length > 0) {
for (var i = 0; i < legalMoves.length; i++) {
var m = legalMoves[i];
if (m.x === pos.x && m.y === pos.y) {
// Move piece
doMove(selectedPiece, selectedPiece.boardX, selectedPiece.boardY, m.x, m.y, m.special);
board.clearHighlights();
selectedPiece = null;
legalMoves = [];
return;
}
}
}
// If tap is on a piece of current turn, select it and show moves
if (p && p.color === currentTurn) {
selectedPiece = p;
legalMoves = getLegalMoves(p, pos.x, pos.y, false);
board.showHighlights(legalMoves);
return;
}
// If tap is on empty square or not a legal move, clear selection
board.clearHighlights();
selectedPiece = null;
legalMoves = [];
};
// No dragging, so move and up handlers are not needed
game.move = function (x, y, obj) {};
game.up = function (x, y, obj) {};
// --- Move Execution ---
function doMove(piece, fromX, fromY, toX, toY, special) {
var opp = piece.color === 'white' ? 'black' : 'white';
var captured = null;
var isPawnMove = piece.type === 'pawn';
var isCapture = false;
// Handle en passant
if (special === 'enpassant') {
var dir = piece.color === 'white' ? -1 : 1;
var capY = toY - dir;
captured = board.getPiece(toX, capY);
board.removePiece(toX, capY);
isCapture = true;
}
// Handle castling
if (special === 'castle_kingside') {
var row = piece.color === 'white' ? 7 : 0;
var rook = board.getPiece(7, row);
board.setPiece(5, row, rook);
board.setPiece(7, row, null);
rook.moveTo(5, row, true);
rook.hasMoved = true;
}
if (special === 'castle_queenside') {
var row = piece.color === 'white' ? 7 : 0;
var rook = board.getPiece(0, row);
board.setPiece(3, row, rook);
board.setPiece(0, row, null);
rook.moveTo(3, row, true);
rook.hasMoved = true;
}
// Remove captured piece
var target = board.getPiece(toX, toY);
if (target && target.color !== piece.color) {
captured = target;
board.removePiece(toX, toY);
isCapture = true;
}
// Move piece
board.setPiece(fromX, fromY, null);
board.setPiece(toX, toY, piece);
piece.moveTo(toX, toY, true);
piece.hasMoved = true;
// Play move sound
LK.getSound('tas').play();
// Pawn promotion
if (piece.type === 'pawn' && (toY === 0 || toY === 7)) {
showPromotionPopup(piece, toX, toY, function (newType) {
piece.type = newType;
piece.promoted = newType === 'queen';
piece.init(newType, piece.color, toX, toY);
endTurn(piece, fromX, fromY, toX, toY, special, captured, isPawnMove, isCapture);
});
return;
}
endTurn(piece, fromX, fromY, toX, toY, special, captured, isPawnMove, isCapture);
}
function endTurn(piece, fromX, fromY, toX, toY, special, captured, isPawnMove, isCapture) {
// Update en passant
if (piece.type === 'pawn' && Math.abs(toY - fromY) === 2) {
enPassantTarget = {
x: toX,
y: (fromY + toY) / 2
};
} else {
enPassantTarget = null;
}
// Update halfmove clock
if (isPawnMove || isCapture) {
halfmoveClock = 0;
} else {
halfmoveClock++;
}
// Update fullmove number
if (currentTurn === 'black') {
fullmoveNumber++;
}
// Save last move
lastMove = {
from: {
x: fromX,
y: fromY
},
to: {
x: toX,
y: toY
},
piece: piece,
captured: captured,
special: special
};
// Switch turn
if (currentTurn === 'white') {
stopWhiteTimer();
startBlackTimer();
currentTurn = 'black';
} else {
stopBlackTimer();
startWhiteTimer();
currentTurn = 'white';
}
turnText.setText((currentTurn === 'white' ? 'White' : 'Black') + ' to move');
// Check for game end
var end = checkGameEnd();
if (end.over) {
gameEnded = true;
stopAllTimers();
if (end.result === 'checkmate') {
LK.effects.flashScreen(0x00ff00, 800);
if (end.winner === 'white') {
LK.showYouWin('White wins by checkmate!');
} else if (end.winner === 'black') {
LK.showYouWin('Black wins by checkmate!');
}
} else if (end.result === 'stalemate') {
LK.effects.flashScreen(0xffff00, 800);
LK.showYouWin('Draw by stalemate');
} else if (end.result === 'draw') {
LK.effects.flashScreen(0xffff00, 800);
LK.showYouWin('Draw');
}
return;
}
// --- AI Move for Black ---
if (currentTurn === 'black' && !gameEnded) {
// --- AI: If an important piece is threatened, try to move it away ---
// Define important pieces
var importantTypes = {
queen: true,
rook: true,
bishop: true,
knight: true
};
// Find all black important pieces that are threatened
var threatenedPieces = [];
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
var p = board.getPiece(x, y);
if (p && p.color === 'black' && importantTypes[p.type]) {
if (isAttacked(x, y, 'black')) {
threatenedPieces.push({
piece: p,
x: x,
y: y
});
}
}
}
}
// If any important piece is threatened, try to move it to safety
var aiMoves = getAllLegalMoves('black');
if (threatenedPieces.length > 0) {
// Try to find a move that moves a threatened important piece to a safe square
var safeMoves = [];
for (var i = 0; i < threatenedPieces.length; i++) {
var tp = threatenedPieces[i];
var moves = getLegalMoves(tp.piece, tp.x, tp.y, false);
for (var j = 0; j < moves.length; j++) {
var m = moves[j];
// Simulate the move
var origX = tp.piece.boardX,
origY = tp.piece.boardY;
var origTo = board.getPiece(m.x, m.y);
board.squares[tp.y][tp.x] = null;
board.squares[m.y][m.x] = tp.piece;
tp.piece.boardX = m.x;
tp.piece.boardY = m.y;
var stillAttacked = isAttacked(m.x, m.y, 'black');
// Undo
board.squares[tp.y][tp.x] = tp.piece;
board.squares[m.y][m.x] = origTo;
tp.piece.boardX = origX;
tp.piece.boardY = origY;
if (!stillAttacked) {
// Find the corresponding move in aiMoves
for (var k = 0; k < aiMoves.length; k++) {
var am = aiMoves[k];
if (am.from.x === tp.x && am.from.y === tp.y && am.to.x === m.x && am.to.y === m.y) {
safeMoves.push(am);
}
}
}
}
}
// If there are safe moves, pick one randomly and play it
if (safeMoves.length > 0) {
var chosen = safeMoves[Math.floor(Math.random() * safeMoves.length)];
var piece = board.getPiece(chosen.from.x, chosen.from.y);
var delay = 400;
if (chosen.special === 'castle_kingside' || chosen.special === 'castle_queenside') delay = 900;else if (piece && piece.type === 'pawn' && (chosen.to.y === 0 || chosen.to.y === 7)) delay = 900;else if (board.getPiece(chosen.to.x, chosen.to.y)) delay = 700;else delay = 350;
LK.setTimeout(function () {
if (piece) {
doMove(piece, chosen.from.x, chosen.from.y, chosen.to.x, chosen.to.y, chosen.special);
}
}, delay);
return;
}
// If no safe move, fallback to normal AI below
}
// --- AI tries to mimic player's last move style ---
// We'll try to find a move for black that is similar in type to the player's last move.
// If not possible, fallback to best move as before.
// Helper: get move type string
var getMoveType = function getMoveType(move) {
if (!move) return '';
if (move.special === 'castle_kingside' || move.special === 'castle_queenside') return 'castle';
if (move.special === 'enpassant') return 'enpassant';
if (move.piece && move.piece.type === 'pawn' && (move.to.y === 0 || move.to.y === 7)) return 'promotion';
if (move.captured) return 'capture';
if (move.piece && move.piece.type === 'pawn') return 'pawn';
if (move.piece && move.piece.type === 'knight') return 'knight';
if (move.piece && move.piece.type === 'bishop') return 'bishop';
if (move.piece && move.piece.type === 'rook') return 'rook';
if (move.piece && move.piece.type === 'queen') return 'queen';
if (move.piece && move.piece.type === 'king') return 'king';
return '';
}; // Helper: get move distance (for delay)
var getMoveDistance = function getMoveDistance(move) {
if (!move) return 1;
var dx = Math.abs(move.from.x - move.to.x);
var dy = Math.abs(move.from.y - move.to.y);
return Math.max(dx, dy);
}; // Get all legal moves for black
// aiMoves already defined above
if (aiMoves.length > 0) {
// Use a simple minimax (depth 2) with material evaluation
var evaluateBoard = function evaluateBoard() {
var values = {
pawn: 1,
knight: 3,
bishop: 3,
rook: 5,
queen: 9,
king: 0
};
var score = 0;
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
var p = board.getPiece(x, y);
if (p) {
var val = values[p.type] || 0;
score += p.color === 'white' ? val : -val;
}
}
}
return score;
};
var simulateMove = function simulateMove(move) {
var piece = board.getPiece(move.from.x, move.from.y);
var target = board.getPiece(move.to.x, move.to.y);
var fromX = move.from.x,
fromY = move.from.y,
toX = move.to.x,
toY = move.to.y;
var special = move.special;
var origHasMoved = piece.hasMoved;
var origEnPassant = enPassantTarget;
var captured = null;
// Handle en passant
if (special === 'enpassant') {
var dir = piece.color === 'white' ? -1 : 1;
var capY = toY - dir;
captured = board.getPiece(toX, capY);
board.squares[capY][toX] = null;
}
// Handle castling
var rook = null,
rookFrom = null,
rookTo = null;
if (special === 'castle_kingside') {
var row = piece.color === 'white' ? 7 : 0;
rook = board.getPiece(7, row);
board.squares[row][5] = rook;
board.squares[row][7] = null;
}
if (special === 'castle_queenside') {
var row = piece.color === 'white' ? 7 : 0;
rook = board.getPiece(0, row);
board.squares[row][3] = rook;
board.squares[row][0] = null;
}
// Remove captured piece
if (target && target.color !== piece.color) {
board.squares[toY][toX] = null;
}
// Move piece
board.squares[fromY][fromX] = null;
board.squares[toY][toX] = piece;
piece.hasMoved = true;
// Pawn promotion (always promote to queen for AI)
var promoted = false;
var oldType = piece.type;
if (piece.type === 'pawn' && (toY === 0 || toY === 7)) {
piece.type = 'queen';
promoted = true;
}
return function undo() {
if (promoted) piece.type = oldType;
// Undo move
board.squares[fromY][fromX] = piece;
board.squares[toY][toX] = target;
piece.hasMoved = origHasMoved;
// Undo en passant
if (special === 'enpassant') {
var dir = piece.color === 'white' ? -1 : 1;
var capY = toY - dir;
board.squares[capY][toX] = captured;
}
// Undo castling
if (special === 'castle_kingside') {
var row = piece.color === 'white' ? 7 : 0;
board.squares[row][7] = board.squares[row][5];
board.squares[row][5] = null;
}
if (special === 'castle_queenside') {
var row = piece.color === 'white' ? 7 : 0;
board.squares[row][0] = board.squares[row][3];
board.squares[row][3] = null;
}
enPassantTarget = origEnPassant;
};
};
// Try to mimic player's last move
var mimicMove = null;
var mimicType = getMoveType(lastMove);
var mimicPieceType = lastMove && lastMove.piece ? lastMove.piece.type : null;
var mimicCapture = lastMove && lastMove.captured ? true : false;
var mimicSpecial = lastMove ? lastMove.special : null;
// Find all black moves that match the last move's type
var mimicCandidates = [];
for (var i = 0; i < aiMoves.length; i++) {
var move = aiMoves[i];
var piece = board.getPiece(move.from.x, move.from.y);
if (!piece) continue;
// Try to match special moves
if (mimicSpecial && move.special === mimicSpecial) {
mimicCandidates.push(move);
continue;
}
// Try to match captures
if (mimicCapture) {
var target = board.getPiece(move.to.x, move.to.y);
if (target && target.color === 'white') {
mimicCandidates.push(move);
continue;
}
}
// Try to match piece type
if (mimicPieceType && piece.type === mimicPieceType) {
mimicCandidates.push(move);
continue;
}
// Try to match move type
if (getMoveType({
piece: piece,
to: move.to,
special: move.special,
captured: board.getPiece(move.to.x, move.to.y)
}) === mimicType) {
mimicCandidates.push(move);
continue;
}
}
// If we have mimic candidates, pick the best among them using minimax
var candidateMoves = mimicCandidates.length > 0 ? mimicCandidates : aiMoves;
// Minimax, depth 2 (AI: black, Player: white)
var bestScore = null;
var bestMoves = [];
for (var i = 0; i < candidateMoves.length; i++) {
var move = candidateMoves[i];
var undo = simulateMove(move);
// After black's move, get all white replies
var whiteMoves = getAllLegalMoves('white');
var worstScore = null;
if (whiteMoves.length === 0) {
// If white has no moves, checkmate or stalemate
if (isInCheck('white')) {
// Black wins
worstScore = -9999;
} else {
// Draw
worstScore = 0;
}
} else {
for (var j = 0; j < whiteMoves.length; j++) {
var wmove = whiteMoves[j];
var wundo = simulateMove(wmove);
var score = evaluateBoard();
if (worstScore === null || score > worstScore) {
worstScore = score;
}
wundo();
}
}
undo();
// AI wants to minimize the score (since black is negative)
if (bestScore === null || worstScore < bestScore) {
bestScore = worstScore;
bestMoves = [move];
} else if (worstScore === bestScore) {
bestMoves.push(move);
}
}
// Pick randomly among best moves
var chosen = bestMoves[Math.floor(Math.random() * bestMoves.length)];
var piece = board.getPiece(chosen.from.x, chosen.from.y);
// Add a delay based on move complexity (longer for captures, promotions, castling, or long moves)
var delay = 400;
if (chosen.special === 'castle_kingside' || chosen.special === 'castle_queenside') delay = 900;else if (piece && piece.type === 'pawn' && (chosen.to.y === 0 || chosen.to.y === 7)) delay = 900;else if (board.getPiece(chosen.to.x, chosen.to.y)) delay = 700;else delay = 350 + 80 * getMoveDistance(chosen);
LK.setTimeout(function () {
if (piece) {
doMove(piece, chosen.from.x, chosen.from.y, chosen.to.x, chosen.to.y, chosen.special);
}
}, delay);
}
}
}
// --- Game Update ---
game.update = function () {
// No per-frame logic needed for chess
};
// --- Reset on Game Over ---
LK.on('gameover', function () {
// Reset everything
board.reset();
currentTurn = 'white';
selectedPiece = null;
legalMoves = [];
lastMove = null;
enPassantTarget = null;
halfmoveClock = 0;
fullmoveNumber = 1;
gameEnded = false;
turnText.setText('White to move');
if (promotionPopup) {
promotionPopup.destroy();
promotionPopup = null;
}
// Reset timers
stopAllTimers();
whiteTime = 600;
blackTime = 600;
whiteTimerText.setText(formatTime(whiteTime));
blackTimerText.setText(formatTime(blackTime));
startWhiteTimer();
});