/****
* 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
****/
// Crown for promoted pawn (queen)
// Black pieces
// White pieces
// Board squares
// We need tween for piece movement animations
// --- Board and Piece Sizes ---
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 to any square adjacent (1 square away) to the other king
var allow = true;
if (otherKingPos) {
// If the target square is adjacent (including diagonals) to the other king, disallow
if (Math.abs(tx - otherKingPos.x) <= 1 && Math.abs(ty - otherKingPos.y) <= 1) 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
****/
// Crown for promoted pawn (queen)
// Black pieces
// White pieces
// Board squares
// We need tween for piece movement animations
// --- Board and Piece Sizes ---
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 to any square adjacent (1 square away) to the other king
var allow = true;
if (otherKingPos) {
// If the target square is adjacent (including diagonals) to the other king, disallow
if (Math.abs(tx - otherKingPos.x) <= 1 && Math.abs(ty - otherKingPos.y) <= 1) 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();
});