Skip to content

Instantly share code, notes, and snippets.

@shricodev
Last active May 25, 2025 13:34
Show Gist options
  • Save shricodev/44bfa16082ade763e84162fc88bede99 to your computer and use it in GitHub Desktop.
Save shricodev/44bfa16082ade763e84162fc88bede99 to your computer and use it in GitHub Desktop.
Blog - Chess (Gemini 2.5 Pro)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chess Game</title>
<style>
:root {
--board-bg: #f0d9b5;
--light-square: #f0d9b5;
--dark-square: #b58863;
--highlight-selected: rgba(80, 145, 255, 0.5);
--highlight-possible: rgba(130, 180, 90, 0.7);
--highlight-last-move: rgba(204, 204, 0, 0.4);
--piece-font-size: 36px;
--ui-bg: #f0f0f0;
--button-bg: #4caf50;
--button-text: white;
}
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
background-color: #333;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
#game-container {
display: flex;
flex-wrap: wrap; /* Allow UI panel to wrap on smaller screens */
gap: 20px;
align-items: flex-start;
}
#board-container {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
width: 480px; /* 8 * 60px */
height: 480px; /* 8 * 60px */
border: 2px solid #333;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
.square {
width: 60px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
font-size: var(--piece-font-size);
cursor: pointer;
position: relative; /* For highlight overlays */
}
.square.light {
background-color: var(--light-square);
}
.square.dark {
background-color: var(--dark-square);
}
.square .piece {
user-select: none; /* Prevent text selection */
}
.square.selected::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--highlight-selected);
z-index: 1;
}
.square.possible-move::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: var(--highlight-possible);
transform: translate(-50%, -50%);
z-index: 2;
}
.square.possible-capture::after {
/* Differentiate capture visually */
background-color: rgba(255, 80, 80, 0.6); /* Reddish for captures */
/* Or a ring instead of a dot */
/* border: 3px solid var(--highlight-possible);
background-color: transparent;
width: 50px; height: 50px; */
}
.square.last-move-from::before,
.square.last-move-to::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--highlight-last-move);
z-index: 0;
}
#ui-panel {
width: 250px;
background-color: var(--ui-bg);
padding: 15px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
gap: 15px;
}
#timers {
display: flex;
justify-content: space-between;
font-size: 1.1em;
font-weight: bold;
}
#timers .active-timer {
color: var(--button-bg);
text-decoration: underline;
}
#move-log-container {
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
height: 200px; /* Fixed height for scroll */
overflow-y: auto;
}
#move-log-container h3 {
margin-top: 0;
}
#move-log {
list-style-type: none;
padding-left: 0;
margin: 0;
font-size: 0.9em;
}
#move-log li {
padding: 2px 0;
display: flex;
}
#move-log li span.move-number {
min-width: 25px;
color: #555;
}
#move-log li span.move-white {
min-width: 60px; /* Adjust as needed */
}
#move-log li span.move-black {
min-width: 60px; /* Adjust as needed */
}
#game-status {
font-size: 1.2em;
font-weight: bold;
color: #d32f2f; /* Red for emphasis */
min-height: 1.5em; /* Prevent layout shift */
text-align: center;
}
button {
background-color: var(--button-bg);
color: var(--button-text);
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.2s;
}
button:hover {
background-color: #3e8e41;
}
#promotion-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border: 2px solid #555;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
gap: 10px;
}
#promotion-modal button {
font-size: var(--piece-font-size);
padding: 5px;
min-width: 50px;
background: #eee;
color: #333;
}
#promotion-modal button:hover {
background: #ddd;
}
/* Responsive adjustments */
@media (max-width: 800px) {
/* Approximate breakpoint for board + ui panel */
body {
align-items: center; /* Center content vertically */
flex-direction: column;
}
#game-container {
flex-direction: column;
align-items: center;
}
#ui-panel {
width: 90%; /* Take more width on smaller screens */
max-width: 480px; /* Max width same as board */
}
#board-container {
width: 90vw; /* Responsive board width */
height: 90vw; /* Responsive board height */
max-width: 480px;
max-height: 480px;
}
.square {
/* Size will be determined by grid fractional units */
font-size: calc(
var(--piece-font-size) * (90vw / 480px)
); /* Scale font with board */
}
}
@media (max-width: 400px) {
.square {
font-size: calc(
var(--piece-font-size) * 0.7
); /* Further scale down font on very small screens */
}
#timers {
font-size: 1em;
}
}
</style>
</head>
<body>
<div id="game-container">
<div id="board-container"></div>
<div id="ui-panel">
<div id="timers">
<div id="white-timer">White: 5:00</div>
<div id="black-timer">Black: 5:00</div>
</div>
<div id="move-log-container">
<h3>Move Log</h3>
<ol id="move-log"></ol>
</div>
<div id="game-status"></div>
<button id="restart-button">Restart Game</button>
<button id="flip-board-button">Flip Board</button>
</div>
</div>
<div id="promotion-modal" style="display: none">
<!-- Promotion options will be added by JS
<!-- --> -->
<p>Promote pawn to:</p>
<button data-piece="Q"></button>
<button data-piece="R"></button>
<button data-piece="B"></button>
<button data-piece="N"></button>
</div>
<script>
const boardContainer = document.getElementById("board-container");
const whiteTimerDisplay = document.getElementById("white-timer");
const blackTimerDisplay = document.getElementById("black-timer");
const moveLogDisplay = document.getElementById("move-log");
const gameStatusDisplay = document.getElementById("game-status");
const restartButton = document.getElementById("restart-button");
const flipBoardButton = document.getElementById("flip-board-button");
const promotionModal = document.getElementById("promotion-modal");
const PIECE_UNICODE = {
P: "♙",
R: "♖",
N: "♘",
B: "♗",
Q: "♕",
K: "♔",
p: "♟",
r: "♜",
n: "♞",
b: "♝",
q: "♛",
k: "♚",
};
const INITIAL_BOARD_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"; // Standard starting position
// FEN represents board state, castling rights, en passant target, halfmove clock, fullmove number
// We will use a simplified model internally focusing on piece positions.
let gameState = {};
function getPieceColor(pieceChar) {
if (!pieceChar) return null;
return pieceChar === pieceChar.toUpperCase() ? "white" : "black";
}
function getPieceType(pieceChar) {
if (!pieceChar) return null;
const lower = pieceChar.toLowerCase();
if (lower === "p") return "pawn";
if (lower === "r") return "rook";
if (lower === "n") return "knight";
if (lower === "b") return "bishop";
if (lower === "q") return "queen";
if (lower === "k") return "king";
return null;
}
function initGame() {
gameState = {
board: Array(8)
.fill(null)
.map(() => Array(8).fill(null)), // { piece: 'P', hasMoved: false }
currentPlayer: "white",
isGameOver: false,
gameStatusMessage: "",
selectedSquare: null, // { r, c }
possibleMoves: [], // array of { toR, toC, ... }
lastMove: null, // { fromR, fromC, toR, toC }
enPassantTarget: null, // { r, c } for the square a pawn can move TO for en passant
castlingRights: {
white: { K: true, Q: true }, // Kingside (short), Queenside (long)
black: { K: true, Q: true },
},
moveHistory: [], // { white: 'e4', black: 'e5' }
timers: {
white: 300, // 5 minutes in seconds
black: 300,
intervalId: null,
initialTime: 300,
},
boardFlipped: false,
promotionChoiceCallback: null,
halfMoveClock: 0, // For 50-move rule (not fully implemented for draw detection)
fullMoveNumber: 1,
};
setupBoardFromFen(INITIAL_BOARD_FEN + " w KQkq - 0 1");
renderBoard();
updateTimerDisplay();
startTimers();
gameStatusDisplay.textContent = "White's turn";
moveLogDisplay.innerHTML = ""; // Clear move log
}
function setupBoardFromFen(fen) {
const parts = fen.split(" ");
const piecePlacement = parts[0];
let r = 0,
c = 0;
for (const char of piecePlacement) {
if (char === "/") {
r++;
c = 0;
} else if (/\d/.test(char)) {
c += parseInt(char);
} else {
gameState.board[r][c] = { piece: char, hasMoved: false };
// Logic to set hasMoved based on FEN could be added if FEN implies it
// For simplicity, Rooks and Kings start with hasMoved = false for castling.
// If FEN indicates castling not possible, hasMoved should be true or castlingRights adjusted.
c++;
}
}
gameState.currentPlayer = parts[1] === "w" ? "white" : "black";
const castlingFen = parts[2];
gameState.castlingRights.white.K = castlingFen.includes("K");
gameState.castlingRights.white.Q = castlingFen.includes("Q");
gameState.castlingRights.black.K = castlingFen.includes("k");
gameState.castlingRights.black.Q = castlingFen.includes("q");
if (parts[3] !== "-") {
const epFile = parts[3][0].charCodeAt(0) - "a".charCodeAt(0);
const epRank = 8 - parseInt(parts[3][1]);
gameState.enPassantTarget = { r: epRank, c: epFile };
} else {
gameState.enPassantTarget = null;
}
gameState.halfMoveClock = parseInt(parts[4]);
gameState.fullMoveNumber = parseInt(parts[5]);
// A more robust FEN parser would pre-set hasMoved for rooks/kings if castling rights are lost
// For now, rely on castlingRights primarily. hasMoved updates during gameplay.
if (!gameState.castlingRights.white.K) {
// If no K-side castling for white
if (gameState.board[7][4] && gameState.board[7][4].piece === "K")
gameState.board[7][4].hasMoved = true;
if (gameState.board[7][7] && gameState.board[7][7].piece === "R")
gameState.board[7][7].hasMoved = true;
}
if (!gameState.castlingRights.white.Q) {
if (gameState.board[7][4] && gameState.board[7][4].piece === "K")
gameState.board[7][4].hasMoved = true;
if (gameState.board[7][0] && gameState.board[7][0].piece === "R")
gameState.board[7][0].hasMoved = true;
}
// Similarly for black
if (!gameState.castlingRights.black.K) {
if (gameState.board[0][4] && gameState.board[0][4].piece === "k")
gameState.board[0][4].hasMoved = true;
if (gameState.board[0][7] && gameState.board[0][7].piece === "r")
gameState.board[0][7].hasMoved = true;
}
if (!gameState.castlingRights.black.Q) {
if (gameState.board[0][4] && gameState.board[0][4].piece === "k")
gameState.board[0][4].hasMoved = true;
if (gameState.board[0][0] && gameState.board[0][0].piece === "r")
gameState.board[0][0].hasMoved = true;
}
}
function createBoardHTML() {
boardContainer.innerHTML = ""; // Clear existing board
for (let r_disp = 0; r_disp < 8; r_disp++) {
for (let c_disp = 0; c_disp < 8; c_disp++) {
const square = document.createElement("div");
square.classList.add("square");
square.classList.add(
(r_disp + c_disp) % 2 === 0 ? "light" : "dark",
);
// Store visual coordinates. Mapping to internal will happen in click handler.
square.dataset.r_disp = r_disp;
square.dataset.c_disp = c_disp;
square.addEventListener("click", () => {
let r_internal, c_internal;
if (gameState.boardFlipped) {
r_internal = 7 - r_disp;
c_internal = 7 - c_disp;
} else {
r_internal = r_disp;
c_internal = c_disp;
}
handleSquareClick(r_internal, c_internal);
});
boardContainer.appendChild(square);
}
}
}
function renderBoard() {
if (boardContainer.children.length === 0) {
createBoardHTML(); // Create squares if they don't exist
}
const squares = boardContainer.children;
for (let r_disp = 0; r_disp < 8; r_disp++) {
for (let c_disp = 0; c_disp < 8; c_disp++) {
const squareElement = squares[r_disp * 8 + c_disp];
let r_internal, c_internal;
if (gameState.boardFlipped) {
r_internal = 7 - r_disp;
c_internal = 7 - c_disp;
} else {
r_internal = r_disp;
c_internal = c_disp;
}
const pieceData = gameState.board[r_internal][c_internal];
squareElement.innerHTML = pieceData
? `<span class="piece">${PIECE_UNICODE[pieceData.piece]}</span>`
: "";
// Clear previous highlights
squareElement.classList.remove(
"selected",
"possible-move",
"possible-capture",
"last-move-from",
"last-move-to",
);
// Highlight selected square
if (
gameState.selectedSquare &&
gameState.selectedSquare.r === r_internal &&
gameState.selectedSquare.c === c_internal
) {
squareElement.classList.add("selected");
}
// Highlight last move
if (gameState.lastMove) {
if (
gameState.lastMove.fromR === r_internal &&
gameState.lastMove.fromC === c_internal
) {
squareElement.classList.add("last-move-from");
}
if (
gameState.lastMove.toR === r_internal &&
gameState.lastMove.toC === c_internal
) {
squareElement.classList.add("last-move-to");
}
}
}
}
// Highlight possible moves (after clearing, as selected square is also a possible source)
if (gameState.selectedSquare) {
gameState.possibleMoves.forEach((move) => {
let r_disp_target, c_disp_target;
if (gameState.boardFlipped) {
r_disp_target = 7 - move.toR;
c_disp_target = 7 - move.toC;
} else {
r_disp_target = move.toR;
c_disp_target = move.toC;
}
const targetSquareElement =
squares[r_disp_target * 8 + c_disp_target];
if (targetSquareElement) {
targetSquareElement.classList.add("possible-move");
if (gameState.board[move.toR][move.toC] || move.isEnPassant) {
// If target has a piece or is en passant
targetSquareElement.classList.add("possible-capture");
}
}
});
}
}
function handleSquareClick(r, c) {
if (gameState.isGameOver) return;
const pieceData = gameState.board[r][c];
if (gameState.selectedSquare) {
// A piece is already selected
const { r: fromR, c: fromC } = gameState.selectedSquare;
const legalMove = gameState.possibleMoves.find(
(m) => m.toR === r && m.toC === c,
);
if (legalMove) {
// Check for pawn promotion
const movingPiece = gameState.board[fromR][fromC].piece;
if (getPieceType(movingPiece) === "pawn" && (r === 0 || r === 7)) {
promptForPromotion((promotionPieceType) => {
makeMove(fromR, fromC, r, c, promotionPieceType);
});
} else {
makeMove(fromR, fromC, r, c);
}
} else {
// Clicked on an invalid square or another piece
gameState.selectedSquare = null;
gameState.possibleMoves = [];
if (
pieceData &&
getPieceColor(pieceData.piece) === gameState.currentPlayer
) {
// Selected another piece of the current player
gameState.selectedSquare = { r, c };
gameState.possibleMoves = getLegalMovesForPiece(r, c);
}
}
} else {
// No piece selected
if (
pieceData &&
getPieceColor(pieceData.piece) === gameState.currentPlayer
) {
gameState.selectedSquare = { r, c };
gameState.possibleMoves = getLegalMovesForPiece(r, c);
}
}
renderBoard();
}
function makeMove(fromR, fromC, toR, toC, promotionPiece = null) {
if (gameState.isGameOver) return;
const movingPieceData = JSON.parse(
JSON.stringify(gameState.board[fromR][fromC]),
); // Deep copy
const capturedPieceData = gameState.board[toR][toC]
? JSON.parse(JSON.stringify(gameState.board[toR][toC]))
: null;
let specialMoveInfo = ""; // For notation, e.g. en passant, castling
// Update halfMoveClock: reset on pawn move or capture, otherwise increment
if (
getPieceType(movingPieceData.piece) === "pawn" ||
capturedPieceData
) {
gameState.halfMoveClock = 0;
} else {
gameState.halfMoveClock++;
}
// Handle en passant capture
let isEnPassantCapture = false;
if (
getPieceType(movingPieceData.piece) === "pawn" &&
gameState.enPassantTarget &&
toR === gameState.enPassantTarget.r &&
toC === gameState.enPassantTarget.c
) {
const capturedPawnR =
gameState.currentPlayer === "white" ? toR + 1 : toR - 1;
gameState.board[capturedPawnR][toC] = null; // Remove captured pawn
isEnPassantCapture = true;
specialMoveInfo = " e.p.";
}
// Update board
gameState.board[toR][toC] = movingPieceData;
gameState.board[fromR][fromC] = null;
movingPieceData.hasMoved = true;
// Handle promotion
if (promotionPiece) {
gameState.board[toR][toC].piece =
gameState.currentPlayer === "white"
? promotionPiece.toUpperCase()
: promotionPiece.toLowerCase();
specialMoveInfo = "=" + promotionPiece.toUpperCase();
}
// Handle castling
let castlingSide = null;
if (getPieceType(movingPieceData.piece) === "king") {
if (Math.abs(fromC - toC) === 2) {
// King moved 2 squares: castling
let rookFromC, rookToC;
if (toC === 6) {
// Kingside (O-O)
rookFromC = 7;
rookToC = 5;
castlingSide = "K";
} else {
// Queenside (O-O-O) (toC === 2)
rookFromC = 0;
rookToC = 3;
castlingSide = "Q";
}
const rook = gameState.board[fromR][rookFromC];
gameState.board[fromR][rookToC] = rook;
gameState.board[fromR][rookFromC] = null;
if (rook) rook.hasMoved = true;
}
}
// Update castling rights
if (movingPieceData.piece === "K")
gameState.castlingRights.white = { K: false, Q: false };
if (movingPieceData.piece === "k")
gameState.castlingRights.black = { K: false, Q: false };
if (movingPieceData.piece === "R") {
if (fromR === 7 && fromC === 0)
gameState.castlingRights.white.Q = false;
if (fromR === 7 && fromC === 7)
gameState.castlingRights.white.K = false;
}
if (movingPieceData.piece === "r") {
if (fromR === 0 && fromC === 0)
gameState.castlingRights.black.Q = false;
if (fromR === 0 && fromC === 7)
gameState.castlingRights.black.K = false;
}
// If a rook is captured, castling rights might be lost
if (capturedPieceData) {
if (toR === 7 && toC === 0 && capturedPieceData.piece === "R")
gameState.castlingRights.white.Q = false;
if (toR === 7 && toC === 7 && capturedPieceData.piece === "R")
gameState.castlingRights.white.K = false;
if (toR === 0 && toC === 0 && capturedPieceData.piece === "r")
gameState.castlingRights.black.Q = false;
if (toR === 0 && toC === 7 && capturedPieceData.piece === "r")
gameState.castlingRights.black.K = false;
}
// Set new en passant target if pawn moved two squares
if (
getPieceType(movingPieceData.piece) === "pawn" &&
Math.abs(fromR - toR) === 2
) {
gameState.enPassantTarget = { r: (fromR + toR) / 2, c: fromC };
} else {
gameState.enPassantTarget = null;
}
// Store last move for highlighting
gameState.lastMove = { fromR, fromC, toR, toC };
// Switch player
const prevPlayer = gameState.currentPlayer;
gameState.currentPlayer =
gameState.currentPlayer === "white" ? "black" : "white";
if (prevPlayer === "black") {
// If black just moved, increment fullmove number
gameState.fullMoveNumber++;
}
// Clear selection
gameState.selectedSquare = null;
gameState.possibleMoves = [];
// Generate notation (must be done BEFORE check/checkmate status for current player)
const notation = generateSAN(
movingPieceData,
fromR,
fromC,
toR,
toC,
capturedPieceData,
promotionPiece,
castlingSide,
isEnPassantCapture,
);
// Check for check, checkmate, stalemate
const opponentColor = gameState.currentPlayer;
const legalMovesForOpponent = getAllLegalMoves(opponentColor);
const kingInCheck = isKingInCheck(opponentColor, gameState.board);
let finalNotation = notation;
if (kingInCheck) {
if (legalMovesForOpponent.length === 0) {
gameState.isGameOver = true;
gameState.gameStatusMessage = `Checkmate! ${prevPlayer.charAt(0).toUpperCase() + prevPlayer.slice(1)} wins.`;
finalNotation += "#";
} else {
gameState.gameStatusMessage = `${opponentColor.charAt(0).toUpperCase() + opponentColor.slice(1)} is in Check!`;
finalNotation += "+";
}
} else {
if (legalMovesForOpponent.length === 0) {
gameState.isGameOver = true;
gameState.gameStatusMessage = "Stalemate! It's a draw.";
} else {
gameState.gameStatusMessage = `${opponentColor.charAt(0).toUpperCase() + opponentColor.slice(1)}'s turn`;
}
}
// 50-move rule (basic check)
if (gameState.halfMoveClock >= 100) {
// 50 moves by each player
gameState.isGameOver = true;
gameState.gameStatusMessage = "Draw by 50-move rule.";
}
// Threefold repetition (not implemented - requires storing board states)
// Insufficient material (not implemented)
addMoveToLog(finalNotation, prevPlayer);
if (gameState.isGameOver) {
stopTimers();
} else {
switchTimers();
}
gameStatusDisplay.textContent = gameState.gameStatusMessage;
renderBoard(); // Re-render to show new state and remove highlights
}
function toAlgebraic(r, c) {
return String.fromCharCode("a".charCodeAt(0) + c) + (8 - r);
}
function generateSAN(
pieceData,
fromR,
fromC,
toR,
toC,
capturedPiece,
promotionPieceType,
castlingSide,
isEnPassant,
) {
const pieceType = getPieceType(pieceData.piece);
let san = "";
if (castlingSide === "K") return "O-O";
if (castlingSide === "Q") return "O-O-O";
if (pieceType !== "pawn") {
san += pieceData.piece.toUpperCase();
}
// Disambiguation (simplified: only if needed for same piece type)
// Full disambiguation is complex, requires checking all other pieces of same type
// For now, we will skip full disambiguation, but basic file/rank can be added if needed.
// This minimal version will use file of departure for pawn captures.
if (pieceType === "pawn" && (capturedPiece || isEnPassant)) {
san += String.fromCharCode("a".charCodeAt(0) + fromC);
}
if (capturedPiece || isEnPassant) {
san += "x";
}
san += toAlgebraic(toR, toC);
if (promotionPieceType) {
san += "=" + promotionPieceType.toUpperCase();
}
// Check and checkmate symbols (+, #) are added *after* this function,
// once the state of the opponent's king is known.
return san;
}
function addMoveToLog(notation, player) {
if (player === "white") {
const li = document.createElement("li");
const moveNumSpan = document.createElement("span");
moveNumSpan.className = "move-number";
moveNumSpan.textContent = gameState.fullMoveNumber + ".";
const whiteMoveSpan = document.createElement("span");
whiteMoveSpan.className = "move-white";
whiteMoveSpan.textContent = notation;
li.appendChild(moveNumSpan);
li.appendChild(whiteMoveSpan);
gameState.moveHistory.push(li); // Store the li element itself
moveLogDisplay.appendChild(li);
} else {
// Black's move
const lastLi =
gameState.moveHistory[gameState.moveHistory.length - 1];
if (lastLi) {
const blackMoveSpan = document.createElement("span");
blackMoveSpan.className = "move-black";
blackMoveSpan.textContent = notation;
lastLi.appendChild(blackMoveSpan);
}
}
moveLogDisplay.scrollTop = moveLogDisplay.scrollHeight; // Auto-scroll
}
function promptForPromotion(callback) {
gameState.promotionChoiceCallback = callback;
promotionModal.style.display = "flex";
// Set correct piece characters for current player
const buttons = promotionModal.querySelectorAll("button[data-piece]");
buttons.forEach((button) => {
const pieceType = button.dataset.piece;
button.textContent =
PIECE_UNICODE[
gameState.currentPlayer === "white"
? pieceType.toUpperCase()
: pieceType.toLowerCase()
];
});
}
promotionModal
.querySelectorAll("button[data-piece]")
.forEach((button) => {
button.addEventListener("click", () => {
if (gameState.promotionChoiceCallback) {
gameState.promotionChoiceCallback(button.dataset.piece);
}
promotionModal.style.display = "none";
gameState.promotionChoiceCallback = null;
});
});
// --- Legal Move Generation ---
function getLegalMovesForPiece(r, c) {
const piece = gameState.board[r][c];
if (!piece) return [];
const pseudoLegalMoves = getPseudoLegalMoves(
r,
c,
piece.piece,
gameState.board,
gameState.castlingRights,
gameState.enPassantTarget,
piece.hasMoved,
);
const legalMoves = [];
for (const move of pseudoLegalMoves) {
// Simulate the move
const tempBoard = JSON.parse(JSON.stringify(gameState.board)); // Deep clone
const movingPieceTemp = JSON.parse(
JSON.stringify(tempBoard[move.fromR][move.fromC]),
);
tempBoard[move.toR][move.toC] = movingPieceTemp;
tempBoard[move.fromR][move.fromC] = null;
// Handle en passant capture in simulation
if (move.isEnPassant) {
const capturedPawnR =
getPieceColor(movingPieceTemp.piece) === "white"
? move.toR + 1
: move.toR - 1;
tempBoard[capturedPawnR][move.toC] = null;
}
// Handle castling in simulation
if (move.isCastling) {
let rookFromC, rookToC;
if (move.castlingSide === "K") {
rookFromC = 7;
rookToC = 5;
} else {
rookFromC = 0;
rookToC = 3;
}
tempBoard[move.fromR][rookToC] = tempBoard[move.fromR][rookFromC];
tempBoard[move.fromR][rookFromC] = null;
}
if (!isKingInCheck(getPieceColor(piece.piece), tempBoard)) {
legalMoves.push(move);
}
}
return legalMoves;
}
function getAllLegalMoves(playerColor) {
let allMoves = [];
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const pieceData = gameState.board[r][c];
if (pieceData && getPieceColor(pieceData.piece) === playerColor) {
const moves = getLegalMovesForPiece(r, c);
allMoves.push(...moves);
}
}
}
return allMoves;
}
function getPseudoLegalMoves(
r,
c,
pieceChar,
board,
castlingRights,
enPassantTarget,
hasMoved,
) {
const moves = [];
const color = getPieceColor(pieceChar);
const type = getPieceType(pieceChar);
const addMove = (toR, toC, options = {}) => {
if (toR >= 0 && toR < 8 && toC >= 0 && toC < 8) {
const targetPiece = board[toR][toC];
if (!targetPiece || getPieceColor(targetPiece.piece) !== color) {
// Can move to empty or capture opponent
moves.push({
fromR: r,
fromC: c,
toR,
toC,
piece: pieceChar,
...options,
});
}
return !targetPiece; // Return true if square was empty (can continue sliding)
}
return false; // Off board
};
const addCaptureOnly = (toR, toC, options = {}) => {
// For pawn captures
if (toR >= 0 && toR < 8 && toC >= 0 && toC < 8) {
const targetPiece = board[toR][toC];
if (targetPiece && getPieceColor(targetPiece.piece) !== color) {
moves.push({
fromR: r,
fromC: c,
toR,
toC,
piece: pieceChar,
...options,
});
} else if (options.isEnPassant) {
// En passant target square is empty but represents a capture
moves.push({
fromR: r,
fromC: c,
toR,
toC,
piece: pieceChar,
...options,
});
}
}
};
if (type === "pawn") {
const dir = color === "white" ? -1 : 1;
const startRow = color === "white" ? 6 : 1;
// Forward 1 square
if (r + dir >= 0 && r + dir < 8 && !board[r + dir][c]) {
addMove(r + dir, c);
// Forward 2 squares (from start)
if (r === startRow && !board[r + 2 * dir][c]) {
addMove(r + 2 * dir, c);
}
}
// Captures
addCaptureOnly(r + dir, c - 1);
addCaptureOnly(r + dir, c + 1);
// En Passant
if (enPassantTarget && enPassantTarget.r === r + dir) {
if (enPassantTarget.c === c - 1)
addCaptureOnly(r + dir, c - 1, { isEnPassant: true });
if (enPassantTarget.c === c + 1)
addCaptureOnly(r + dir, c + 1, { isEnPassant: true });
}
}
if (type === "knight") {
const knightMoves = [
[-2, -1],
[-2, 1],
[-1, -2],
[-1, 2],
[1, -2],
[1, 2],
[2, -1],
[2, 1],
];
knightMoves.forEach(([dr, dc]) => addMove(r + dr, c + dc));
}
const slide = (dr, dc) => {
for (let i = 1; i < 8; i++) {
if (!addMove(r + i * dr, c + i * dc)) break;
}
};
if (type === "rook" || type === "queen") {
slide(1, 0);
slide(-1, 0);
slide(0, 1);
slide(0, -1);
}
if (type === "bishop" || type === "queen") {
slide(1, 1);
slide(1, -1);
slide(-1, 1);
slide(-1, -1);
}
if (type === "king") {
const kingMoves = [
[-1, -1],
[-1, 0],
[-1, 1],
[0, -1],
[0, 1],
[1, -1],
[1, 0],
[1, 1],
];
kingMoves.forEach(([dr, dc]) => addMove(r + dr, c + dc));
// Castling
if (!hasMoved) {
// Kingside
if (
castlingRights[color].K &&
!board[r][c + 1] &&
!board[r][c + 2] &&
!isSquareAttacked(
r,
c,
color === "white" ? "black" : "white",
board,
) &&
!isSquareAttacked(
r,
c + 1,
color === "white" ? "black" : "white",
board,
) &&
!isSquareAttacked(
r,
c + 2,
color === "white" ? "black" : "white",
board,
) &&
board[r][c + 3] &&
getPieceType(board[r][c + 3].piece) === "rook" &&
!board[r][c + 3].hasMoved
) {
addMove(r, c + 2, { isCastling: true, castlingSide: "K" });
}
// Queenside
if (
castlingRights[color].Q &&
!board[r][c - 1] &&
!board[r][c - 2] &&
!board[r][c - 3] &&
!isSquareAttacked(
r,
c,
color === "white" ? "black" : "white",
board,
) &&
!isSquareAttacked(
r,
c - 1,
color === "white" ? "black" : "white",
board,
) &&
!isSquareAttacked(
r,
c - 2,
color === "white" ? "black" : "white",
board,
) &&
board[r][c - 4] &&
getPieceType(board[r][c - 4].piece) === "rook" &&
!board[r][c - 4].hasMoved
) {
addMove(r, c - 2, { isCastling: true, castlingSide: "Q" });
}
}
}
return moves;
}
function isSquareAttacked(r, c, attackerColor, currentBoard) {
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
const pieceData = currentBoard[i][j];
if (pieceData && getPieceColor(pieceData.piece) === attackerColor) {
// Pass dummy castling rights and en passant target, as they don't affect attack maps for non-king pieces
// Pass hasMoved as pieceData.hasMoved - relevant for king potentially.
const pseudoMoves = getPseudoLegalMoves(
i,
j,
pieceData.piece,
currentBoard,
{},
null,
pieceData.hasMoved,
);
for (const move of pseudoMoves) {
// For pawns, attack squares are different from move squares (except captures)
// The getPseudoLegalMoves already generates captures to the correct squares.
// A pawn at i,j attacks diagonally forward squares.
if (getPieceType(pieceData.piece) === "pawn") {
const dir = attackerColor === "white" ? -1 : 1;
if (
(i + dir === r && j + 1 === c) ||
(i + dir === r && j - 1 === c)
) {
return true;
}
} else if (move.toR === r && move.toC === c) {
return true;
}
}
}
}
}
return false;
}
function findKing(kingColor, currentBoard) {
const kingChar = kingColor === "white" ? "K" : "k";
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
if (currentBoard[r][c] && currentBoard[r][c].piece === kingChar) {
return { r, c };
}
}
}
return null; // Should not happen in a valid game
}
function isKingInCheck(kingColor, currentBoard) {
const kingPos = findKing(kingColor, currentBoard);
if (!kingPos) return false; // Should not happen
const attackerColor = kingColor === "white" ? "black" : "white";
return isSquareAttacked(
kingPos.r,
kingPos.c,
attackerColor,
currentBoard,
);
}
// --- Timers ---
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
}
function updateTimerDisplay() {
whiteTimerDisplay.textContent = `White: ${formatTime(gameState.timers.white)}`;
blackTimerDisplay.textContent = `Black: ${formatTime(gameState.timers.black)}`;
whiteTimerDisplay.classList.remove("active-timer");
blackTimerDisplay.classList.remove("active-timer");
if (gameState.currentPlayer === "white" && !gameState.isGameOver) {
whiteTimerDisplay.classList.add("active-timer");
} else if (
gameState.currentPlayer === "black" &&
!gameState.isGameOver
) {
blackTimerDisplay.classList.add("active-timer");
}
}
function startTimers() {
stopTimers(); // Clear any existing timer
gameState.timers.intervalId = setInterval(() => {
if (gameState.isGameOver) {
stopTimers();
return;
}
if (gameState.currentPlayer === "white") {
gameState.timers.white--;
if (gameState.timers.white <= 0) handleTimeout("white");
} else {
gameState.timers.black--;
if (gameState.timers.black <= 0) handleTimeout("black");
}
updateTimerDisplay();
}, 1000);
}
function stopTimers() {
if (gameState.timers.intervalId) {
clearInterval(gameState.timers.intervalId);
gameState.timers.intervalId = null;
}
}
function switchTimers() {
// The timer logic simply decrements the current player's time.
// updateTimerDisplay handles showing which is active.
// No specific switch action needed beyond changing gameState.currentPlayer.
updateTimerDisplay();
}
function handleTimeout(playerColor) {
stopTimers();
gameState.isGameOver = true;
const winner = playerColor === "white" ? "Black" : "White";
gameState.gameStatusMessage = `Time out! ${winner} wins.`;
gameStatusDisplay.textContent = gameState.gameStatusMessage;
updateTimerDisplay(); // Final update
}
// --- Event Listeners ---
restartButton.addEventListener("click", () => {
stopTimers();
initGame();
});
flipBoardButton.addEventListener("click", () => {
gameState.boardFlipped = !gameState.boardFlipped;
// If a piece is selected, its possible moves are calculated based on internal coords.
// Re-rendering will map them to the new visual coords.
renderBoard();
});
// --- Initialize Game ---
initGame();
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chess Game</title>
<style>
:root {
--board-bg: #f0d9b5;
--light-square: #f0d9b5;
--dark-square: #b58863;
--highlight-selected: rgba(80, 145, 255, 0.5);
--highlight-possible: rgba(130, 180, 90, 0.7);
--highlight-last-move: rgba(204, 204, 0, 0.4);
--piece-font-size: 36px; /* Base size for 60px squares */
--ui-bg: #f0f0f0;
--button-bg: #4caf50;
--button-text: white;
}
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
background-color: #333;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
#game-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
}
#board-container {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
width: 480px;
height: 480px;
border: 2px solid #333;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
.square {
/* Width/Height will be set by grid system based on board-container size */
display: flex;
justify-content: center;
align-items: center;
font-size: var(--piece-font-size);
cursor: pointer;
position: relative;
}
.square.light {
background-color: var(--light-square);
}
.square.dark {
background-color: var(--dark-square);
}
.square .piece {
user-select: none;
transform: Sscale(
1
); /* Helps with rendering consistency in some browsers */
}
.square.selected::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--highlight-selected);
z-index: 1;
}
.square.possible-move::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 30%;
height: 30%; /* Relative to square size */
border-radius: 50%;
background-color: var(--highlight-possible);
transform: translate(-50%, -50%);
z-index: 2;
}
.square.possible-capture::after {
background-color: rgba(255, 80, 80, 0.6);
}
.square.last-move-from::before,
.square.last-move-to::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--highlight-last-move);
z-index: 0;
}
#ui-panel {
width: 250px;
background-color: var(--ui-bg);
padding: 15px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
gap: 15px;
}
#timers {
display: flex;
justify-content: space-between;
font-size: 1.1em;
font-weight: bold;
}
#timers .active-timer {
color: var(--button-bg);
text-decoration: underline;
}
#move-log-container {
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
height: 200px;
overflow-y: auto;
}
#move-log-container h3 {
margin-top: 0;
}
#move-log {
list-style-type: none;
padding-left: 0;
margin: 0;
font-size: 0.9em;
}
#move-log li {
padding: 2px 0;
display: flex;
}
#move-log li span.move-number {
min-width: 25px;
color: #555;
}
#move-log li span.move-white {
min-width: 60px;
}
#move-log li span.move-black {
min-width: 60px;
}
#game-status {
font-size: 1.2em;
font-weight: bold;
color: #d32f2f;
min-height: 1.5em;
text-align: center;
}
button {
background-color: var(--button-bg);
color: var(--button-text);
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.2s;
}
button:hover {
background-color: #3e8e41;
}
#promotion-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border: 2px solid #555;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
flex-direction: column; /* Stack label and buttons vertically */
align-items: center; /* Center items */
gap: 10px;
}
#promotion-modal p {
margin: 0 0 10px 0; /* Add some margin below the text */
font-weight: bold;
}
#promotion-modal div.promo-buttons {
/* Container for buttons */
display: flex;
gap: 10px;
}
#promotion-modal button {
font-size: var(--piece-font-size);
padding: 5px;
min-width: 50px;
background: #eee;
color: #333;
}
#promotion-modal button:hover {
background: #ddd;
}
/* Responsive adjustments */
@media (max-width: 800px) {
body {
align-items: center;
flex-direction: column;
}
#game-container {
flex-direction: column;
align-items: center;
}
#ui-panel {
width: 90%;
max-width: 480px;
}
#board-container {
width: 90vw;
height: 90vw;
max-width: 480px;
max-height: 480px;
}
/* Adjust piece font size based on board container width */
:root {
/* Redefine for this media query context */
--piece-font-size-dynamic: calc(
min(90vw, 480px) / 8 * 0.6
); /* (Square Size) * (Scaling Factor) */
}
.square {
font-size: var(--piece-font-size-dynamic);
}
#promotion-modal button {
font-size: var(--piece-font-size-dynamic);
}
}
@media (max-width: 400px) {
:root {
--piece-font-size-dynamic: calc(min(90vw, 480px) / 8 * 0.5);
}
.square {
font-size: var(--piece-font-size-dynamic);
}
#promotion-modal button {
font-size: var(--piece-font-size-dynamic);
}
#timers {
font-size: 1em;
}
}
</style>
</head>
<body>
<div id="game-container">
<div id="board-container"></div>
<div id="ui-panel">
<div id="timers">
<div id="white-timer">White: 5:00</div>
<div id="black-timer">Black: 5:00</div>
</div>
<div id="move-log-container">
<h3>Move Log</h3>
<ol id="move-log"></ol>
</div>
<div id="game-status"></div>
<button id="restart-button">Restart Game</button>
<button id="flip-board-button">Flip Board</button>
</div>
</div>
<div id="promotion-modal" style="display: none">
<p>Promote pawn to:</p>
<div class="promo-buttons">
<button data-piece="Q"></button>
<button data-piece="R"></button>
<button data-piece="B"></button>
<button data-piece="N"></button>
</div>
</div>
<script>
const boardContainer = document.getElementById("board-container");
const whiteTimerDisplay = document.getElementById("white-timer");
const blackTimerDisplay = document.getElementById("black-timer");
const moveLogDisplay = document.getElementById("move-log");
const gameStatusDisplay = document.getElementById("game-status");
const restartButton = document.getElementById("restart-button");
const flipBoardButton = document.getElementById("flip-board-button");
const promotionModal = document.getElementById("promotion-modal");
const PIECE_UNICODE = {
P: "♙",
R: "♖",
N: "♘",
B: "♗",
Q: "♕",
K: "♔",
p: "♟",
r: "♜",
n: "♞",
b: "♝",
q: "♛",
k: "♚",
};
const INITIAL_BOARD_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
let gameState = {};
function getPieceColor(pieceChar) {
if (!pieceChar) return null;
return pieceChar === pieceChar.toUpperCase() ? "white" : "black";
}
function getPieceType(pieceChar) {
if (!pieceChar) return null;
const lower = pieceChar.toLowerCase();
if (lower === "p") return "pawn";
if (lower === "r") return "rook";
if (lower === "n") return "knight";
if (lower === "b") return "bishop";
if (lower === "q") return "queen";
if (lower === "k") return "king";
return null;
}
function initGame() {
gameState = {
board: Array(8)
.fill(null)
.map(() => Array(8).fill(null)),
currentPlayer: "white",
isGameOver: false,
gameStatusMessage: "",
selectedSquare: null,
possibleMoves: [],
lastMove: null,
enPassantTarget: null,
castlingRights: {
white: { K: true, Q: true },
black: { K: true, Q: true },
},
moveHistory: [],
timers: {
white: 300,
black: 300,
intervalId: null,
initialTime: 300,
},
boardFlipped: false,
promotionChoiceCallback: null,
halfMoveClock: 0,
fullMoveNumber: 1,
};
setupBoardFromFen(INITIAL_BOARD_FEN + " w KQkq - 0 1");
renderBoard();
updateTimerDisplay();
if (gameState.timers.intervalId)
clearInterval(gameState.timers.intervalId); // Clear before starting
startTimers();
gameStatusDisplay.textContent = "White's turn";
moveLogDisplay.innerHTML = "";
}
function setupBoardFromFen(fen) {
const parts = fen.split(" ");
const piecePlacement = parts[0];
let r = 0,
c = 0;
for (const char of piecePlacement) {
if (char === "/") {
r++;
c = 0;
} else if (/\d/.test(char)) {
c += parseInt(char);
} else {
gameState.board[r][c] = { piece: char, hasMoved: false };
c++;
}
}
gameState.currentPlayer = parts[1] === "w" ? "white" : "black";
const castlingFen = parts[2];
gameState.castlingRights.white.K = castlingFen.includes("K");
gameState.castlingRights.white.Q = castlingFen.includes("Q");
gameState.castlingRights.black.K = castlingFen.includes("k");
gameState.castlingRights.black.Q = castlingFen.includes("q");
if (parts[3] !== "-") {
const epFile = parts[3][0].charCodeAt(0) - "a".charCodeAt(0);
const epRank = 8 - parseInt(parts[3][1]);
gameState.enPassantTarget = { r: epRank, c: epFile };
} else {
gameState.enPassantTarget = null;
}
gameState.halfMoveClock = parseInt(parts[4]);
gameState.fullMoveNumber = parseInt(parts[5]);
if (!gameState.castlingRights.white.K) {
if (gameState.board[7][4] && gameState.board[7][4].piece === "K")
gameState.board[7][4].hasMoved = true;
if (gameState.board[7][7] && gameState.board[7][7].piece === "R")
gameState.board[7][7].hasMoved = true;
}
if (!gameState.castlingRights.white.Q) {
if (gameState.board[7][4] && gameState.board[7][4].piece === "K")
gameState.board[7][4].hasMoved = true;
if (gameState.board[7][0] && gameState.board[7][0].piece === "R")
gameState.board[7][0].hasMoved = true;
}
if (!gameState.castlingRights.black.K) {
if (gameState.board[0][4] && gameState.board[0][4].piece === "k")
gameState.board[0][4].hasMoved = true;
if (gameState.board[0][7] && gameState.board[0][7].piece === "r")
gameState.board[0][7].hasMoved = true;
}
if (!gameState.castlingRights.black.Q) {
if (gameState.board[0][4] && gameState.board[0][4].piece === "k")
gameState.board[0][4].hasMoved = true;
if (gameState.board[0][0] && gameState.board[0][0].piece === "r")
gameState.board[0][0].hasMoved = true;
}
}
function createBoardHTML() {
boardContainer.innerHTML = "";
for (let r_disp = 0; r_disp < 8; r_disp++) {
for (let c_disp = 0; c_disp < 8; c_disp++) {
const square = document.createElement("div");
square.classList.add("square");
square.classList.add(
(r_disp + c_disp) % 2 === 0 ? "light" : "dark",
);
square.dataset.r_disp = r_disp; // Store visual coords
square.dataset.c_disp = c_disp;
square.addEventListener("click", () => {
let r_internal, c_internal;
// Get current visual coords from dataset as r_disp/c_disp might be stale from closure
const clicked_r_disp = parseInt(square.dataset.r_disp);
const clicked_c_disp = parseInt(square.dataset.c_disp);
if (gameState.boardFlipped) {
r_internal = 7 - clicked_r_disp;
c_internal = 7 - clicked_c_disp;
} else {
r_internal = clicked_r_disp;
c_internal = clicked_c_disp;
}
handleSquareClick(r_internal, c_internal);
});
boardContainer.appendChild(square);
}
}
}
function renderBoard() {
if (boardContainer.children.length === 0) {
createBoardHTML();
}
const squares = boardContainer.children;
for (let r_disp_iter = 0; r_disp_iter < 8; r_disp_iter++) {
for (let c_disp_iter = 0; c_disp_iter < 8; c_disp_iter++) {
// The square element's position in the `squares` HTMLCollection
// is fixed once created. So, its visual (display) row/col is fixed.
const squareElement = squares[r_disp_iter * 8 + c_disp_iter];
let r_internal, c_internal;
// Determine internal board coordinates based on whether board is flipped
if (gameState.boardFlipped) {
r_internal = 7 - r_disp_iter;
c_internal = 7 - c_disp_iter;
} else {
r_internal = r_disp_iter;
c_internal = c_disp_iter;
}
const pieceData = gameState.board[r_internal][c_internal];
squareElement.innerHTML = pieceData
? `<span class="piece">${PIECE_UNICODE[pieceData.piece]}</span>`
: "";
squareElement.classList.remove(
"selected",
"possible-move",
"possible-capture",
"last-move-from",
"last-move-to",
);
if (
gameState.selectedSquare &&
gameState.selectedSquare.r === r_internal &&
gameState.selectedSquare.c === c_internal
) {
squareElement.classList.add("selected");
}
if (gameState.lastMove) {
if (
gameState.lastMove.fromR === r_internal &&
gameState.lastMove.fromC === c_internal
) {
squareElement.classList.add("last-move-from");
}
if (
gameState.lastMove.toR === r_internal &&
gameState.lastMove.toC === c_internal
) {
squareElement.classList.add("last-move-to");
}
}
}
}
if (gameState.selectedSquare) {
gameState.possibleMoves.forEach((move) => {
let r_disp_target, c_disp_target;
// Map the internal target coordinates of the move to display coordinates
if (gameState.boardFlipped) {
r_disp_target = 7 - move.toR;
c_disp_target = 7 - move.toC;
} else {
r_disp_target = move.toR;
c_disp_target = move.toC;
}
const targetSquareElement =
squares[r_disp_target * 8 + c_disp_target];
if (targetSquareElement) {
targetSquareElement.classList.add("possible-move");
if (gameState.board[move.toR][move.toC] || move.isEnPassant) {
targetSquareElement.classList.add("possible-capture");
}
}
});
}
}
function handleSquareClick(r, c) {
if (gameState.isGameOver) return;
const pieceData = gameState.board[r][c];
if (gameState.selectedSquare) {
const { r: fromR, c: fromC } = gameState.selectedSquare;
const legalMove = gameState.possibleMoves.find(
(m) => m.toR === r && m.toC === c,
);
if (legalMove) {
const movingPiece = gameState.board[fromR][fromC].piece;
if (getPieceType(movingPiece) === "pawn" && (r === 0 || r === 7)) {
promptForPromotion((promotionPieceType) => {
makeMove(fromR, fromC, r, c, promotionPieceType);
});
} else {
makeMove(fromR, fromC, r, c);
}
} else {
gameState.selectedSquare = null;
gameState.possibleMoves = [];
if (
pieceData &&
getPieceColor(pieceData.piece) === gameState.currentPlayer
) {
gameState.selectedSquare = { r, c };
gameState.possibleMoves = getLegalMovesForPiece(r, c);
}
}
} else {
if (
pieceData &&
getPieceColor(pieceData.piece) === gameState.currentPlayer
) {
gameState.selectedSquare = { r, c };
gameState.possibleMoves = getLegalMovesForPiece(r, c);
}
}
renderBoard();
}
function makeMove(fromR, fromC, toR, toC, promotionPiece = null) {
if (gameState.isGameOver) return;
const movingPieceData = JSON.parse(
JSON.stringify(gameState.board[fromR][fromC]),
);
const capturedPieceData = gameState.board[toR][toC]
? JSON.parse(JSON.stringify(gameState.board[toR][toC]))
: null;
if (
getPieceType(movingPieceData.piece) === "pawn" ||
capturedPieceData
) {
gameState.halfMoveClock = 0;
} else {
gameState.halfMoveClock++;
}
let isEnPassantCapture = false;
if (
getPieceType(movingPieceData.piece) === "pawn" &&
gameState.enPassantTarget &&
toR === gameState.enPassantTarget.r &&
toC === gameState.enPassantTarget.c
) {
const capturedPawnR =
gameState.currentPlayer === "white" ? toR + 1 : toR - 1;
gameState.board[capturedPawnR][toC] = null;
isEnPassantCapture = true;
}
gameState.board[toR][toC] = movingPieceData;
gameState.board[fromR][fromC] = null;
movingPieceData.hasMoved = true;
if (promotionPiece) {
gameState.board[toR][toC].piece =
gameState.currentPlayer === "white"
? promotionPiece.toUpperCase()
: promotionPiece.toLowerCase();
}
let castlingSide = null;
if (getPieceType(movingPieceData.piece) === "king") {
if (Math.abs(fromC - toC) === 2) {
let rookFromC, rookToC;
if (toC === 6) {
rookFromC = 7;
rookToC = 5;
castlingSide = "K";
} else {
rookFromC = 0;
rookToC = 3;
castlingSide = "Q";
}
const rook = gameState.board[fromR][rookFromC];
gameState.board[fromR][rookToC] = rook;
gameState.board[fromR][rookFromC] = null;
if (rook) rook.hasMoved = true;
}
}
if (movingPieceData.piece === "K")
gameState.castlingRights.white = { K: false, Q: false };
if (movingPieceData.piece === "k")
gameState.castlingRights.black = { K: false, Q: false };
if (movingPieceData.piece === "R") {
if (fromR === 7 && fromC === 0)
gameState.castlingRights.white.Q = false;
if (fromR === 7 && fromC === 7)
gameState.castlingRights.white.K = false;
}
if (movingPieceData.piece === "r") {
if (fromR === 0 && fromC === 0)
gameState.castlingRights.black.Q = false;
if (fromR === 0 && fromC === 7)
gameState.castlingRights.black.K = false;
}
if (capturedPieceData) {
if (toR === 7 && toC === 0 && capturedPieceData.piece === "R")
gameState.castlingRights.white.Q = false;
if (toR === 7 && toC === 7 && capturedPieceData.piece === "R")
gameState.castlingRights.white.K = false;
if (toR === 0 && toC === 0 && capturedPieceData.piece === "r")
gameState.castlingRights.black.Q = false;
if (toR === 0 && toC === 7 && capturedPieceData.piece === "r")
gameState.castlingRights.black.K = false;
}
if (
getPieceType(movingPieceData.piece) === "pawn" &&
Math.abs(fromR - toR) === 2
) {
gameState.enPassantTarget = { r: (fromR + toR) / 2, c: fromC };
} else {
gameState.enPassantTarget = null;
}
gameState.lastMove = { fromR, fromC, toR, toC };
const prevPlayer = gameState.currentPlayer;
gameState.currentPlayer =
gameState.currentPlayer === "white" ? "black" : "white";
if (prevPlayer === "black") {
gameState.fullMoveNumber++;
}
gameState.selectedSquare = null;
gameState.possibleMoves = [];
const notation = generateSAN(
movingPieceData,
fromR,
fromC,
toR,
toC,
capturedPieceData,
promotionPiece,
castlingSide,
isEnPassantCapture,
);
const opponentColor = gameState.currentPlayer;
const legalMovesForOpponent = getAllLegalMoves(opponentColor); // Uses updated board
const kingInCheck = isKingInCheck(opponentColor, gameState.board); // Uses updated board
let finalNotation = notation;
if (kingInCheck) {
if (legalMovesForOpponent.length === 0) {
gameState.isGameOver = true;
gameState.gameStatusMessage = `Checkmate! ${prevPlayer.charAt(0).toUpperCase() + prevPlayer.slice(1)} wins.`;
finalNotation += "#";
} else {
gameState.gameStatusMessage = `${opponentColor.charAt(0).toUpperCase() + opponentColor.slice(1)} is in Check!`;
finalNotation += "+";
}
} else {
if (legalMovesForOpponent.length === 0) {
gameState.isGameOver = true;
gameState.gameStatusMessage = "Stalemate! It's a draw.";
// Could add notation for stalemate (e.g., 1/2-1/2) but not standard in move itself
} else {
gameState.gameStatusMessage = `${opponentColor.charAt(0).toUpperCase() + opponentColor.slice(1)}'s turn`;
}
}
if (gameState.halfMoveClock >= 100) {
gameState.isGameOver = true;
gameState.gameStatusMessage = "Draw by 50-move rule.";
}
addMoveToLog(finalNotation, prevPlayer);
if (gameState.isGameOver) {
stopTimers();
} else {
switchTimers(); // This effectively means the next timer tick will use the new current player
}
gameStatusDisplay.textContent = gameState.gameStatusMessage;
renderBoard();
}
function toAlgebraic(r, c) {
return String.fromCharCode("a".charCodeAt(0) + c) + (8 - r);
}
function generateSAN(
pieceData,
fromR,
fromC,
toR,
toC,
capturedPiece,
promotionPieceType,
castlingSide,
isEnPassant,
) {
const pieceType = getPieceType(pieceData.piece);
let san = "";
if (castlingSide === "K") return "O-O";
if (castlingSide === "Q") return "O-O-O";
if (pieceType !== "pawn") {
san += pieceData.piece.toUpperCase();
}
// Simplified disambiguation: for now, only pawn captures need departure file
if (pieceType === "pawn" && (capturedPiece || isEnPassant)) {
san += String.fromCharCode("a".charCodeAt(0) + fromC);
}
// TODO: Add full disambiguation if needed by checking other pieces of the same type
// that can move to the same square.
if (capturedPiece || isEnPassant) {
san += "x";
}
san += toAlgebraic(toR, toC);
if (promotionPieceType) {
san += "=" + promotionPieceType.toUpperCase();
}
return san;
}
function addMoveToLog(notation, player) {
if (player === "white") {
const li = document.createElement("li");
const moveNumSpan = document.createElement("span");
moveNumSpan.className = "move-number";
moveNumSpan.textContent = gameState.fullMoveNumber + ".";
const whiteMoveSpan = document.createElement("span");
whiteMoveSpan.className = "move-white";
whiteMoveSpan.textContent = notation;
li.appendChild(moveNumSpan);
li.appendChild(whiteMoveSpan);
gameState.moveHistory.push(li);
moveLogDisplay.appendChild(li);
} else {
const lastLi =
gameState.moveHistory[gameState.moveHistory.length - 1];
if (lastLi) {
const blackMoveSpan = document.createElement("span");
blackMoveSpan.className = "move-black";
blackMoveSpan.textContent = notation;
lastLi.appendChild(blackMoveSpan);
}
}
moveLogDisplay.scrollTop = moveLogDisplay.scrollHeight;
}
function promptForPromotion(callback) {
gameState.promotionChoiceCallback = callback;
promotionModal.style.display = "flex";
const buttons = promotionModal.querySelectorAll("button[data-piece]");
buttons.forEach((button) => {
const pieceType = button.dataset.piece;
button.textContent =
PIECE_UNICODE[
gameState.currentPlayer === "white"
? pieceType.toUpperCase()
: pieceType.toLowerCase()
];
});
}
promotionModal
.querySelectorAll("div.promo-buttons button[data-piece]")
.forEach((button) => {
button.addEventListener("click", () => {
if (gameState.promotionChoiceCallback) {
gameState.promotionChoiceCallback(button.dataset.piece);
}
promotionModal.style.display = "none";
gameState.promotionChoiceCallback = null;
});
});
function getLegalMovesForPiece(r, c) {
const piece = gameState.board[r][c];
if (!piece) return [];
const pseudoLegalMoves = getPseudoLegalMoves(
r,
c,
piece.piece,
gameState.board,
gameState.castlingRights,
gameState.enPassantTarget,
piece.hasMoved,
);
const legalMoves = [];
const pieceColor = getPieceColor(piece.piece);
for (const move of pseudoLegalMoves) {
const tempBoard = JSON.parse(JSON.stringify(gameState.board));
const movingPieceTemp = JSON.parse(
JSON.stringify(tempBoard[move.fromR][move.fromC]),
);
tempBoard[move.toR][move.toC] = movingPieceTemp;
tempBoard[move.fromR][move.fromC] = null;
if (move.isEnPassant) {
const capturedPawnR =
pieceColor === "white" ? move.toR + 1 : move.toR - 1;
tempBoard[capturedPawnR][move.toC] = null;
}
if (move.isCastling) {
let rookFromC, rookToC;
if (move.castlingSide === "K") {
rookFromC = 7;
rookToC = 5;
} else {
rookFromC = 0;
rookToC = 3;
}
tempBoard[move.fromR][rookToC] = tempBoard[move.fromR][rookFromC];
tempBoard[move.fromR][rookFromC] = null;
}
if (!isKingInCheck(pieceColor, tempBoard)) {
legalMoves.push(move);
}
}
return legalMoves;
}
function getAllLegalMoves(playerColor) {
let allMoves = [];
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const pieceData = gameState.board[r][c];
if (pieceData && getPieceColor(pieceData.piece) === playerColor) {
const moves = getLegalMovesForPiece(r, c);
allMoves.push(...moves);
}
}
}
return allMoves;
}
function getPseudoLegalMoves(
r,
c,
pieceChar,
board,
castlingRights,
enPassantTarget,
pieceHasMoved,
) {
const moves = [];
const color = getPieceColor(pieceChar);
const type = getPieceType(pieceChar);
const addMove = (toR, toC, options = {}) => {
if (toR >= 0 && toR < 8 && toC >= 0 && toC < 8) {
const targetPieceOnBoard = board[toR][toC];
if (
!targetPieceOnBoard ||
getPieceColor(targetPieceOnBoard.piece) !== color
) {
moves.push({
fromR: r,
fromC: c,
toR,
toC,
piece: pieceChar,
...options,
});
}
return !targetPieceOnBoard;
}
return false;
};
const addCaptureOnly = (toR, toC, options = {}) => {
if (toR >= 0 && toR < 8 && toC >= 0 && toC < 8) {
const targetPieceOnBoard = board[toR][toC];
if (
targetPieceOnBoard &&
getPieceColor(targetPieceOnBoard.piece) !== color
) {
moves.push({
fromR: r,
fromC: c,
toR,
toC,
piece: pieceChar,
...options,
});
} else if (options.isEnPassant) {
moves.push({
fromR: r,
fromC: c,
toR,
toC,
piece: pieceChar,
...options,
});
}
}
};
if (type === "pawn") {
const dir = color === "white" ? -1 : 1;
const startRow = color === "white" ? 6 : 1;
if (r + dir >= 0 && r + dir < 8 && !board[r + dir][c]) {
addMove(r + dir, c);
if (r === startRow && !board[r + 2 * dir][c]) {
addMove(r + 2 * dir, c);
}
}
addCaptureOnly(r + dir, c - 1);
addCaptureOnly(r + dir, c + 1);
if (enPassantTarget && enPassantTarget.r === r + dir) {
if (enPassantTarget.c === c - 1)
addCaptureOnly(r + dir, c - 1, { isEnPassant: true });
if (enPassantTarget.c === c + 1)
addCaptureOnly(r + dir, c + 1, { isEnPassant: true });
}
}
if (type === "knight") {
const knightMoves = [
[-2, -1],
[-2, 1],
[-1, -2],
[-1, 2],
[1, -2],
[1, 2],
[2, -1],
[2, 1],
];
knightMoves.forEach(([dr, dc]) => addMove(r + dr, c + dc));
}
const slide = (dr, dc) => {
for (let i = 1; i < 8; i++) {
if (!addMove(r + i * dr, c + i * dc)) break;
}
};
if (type === "rook" || type === "queen") {
slide(1, 0);
slide(-1, 0);
slide(0, 1);
slide(0, -1);
}
if (type === "bishop" || type === "queen") {
slide(1, 1);
slide(1, -1);
slide(-1, 1);
slide(-1, -1);
}
if (type === "king") {
const kingMoves = [
[-1, -1],
[-1, 0],
[-1, 1],
[0, -1],
[0, 1],
[1, -1],
[1, 0],
[1, 1],
];
kingMoves.forEach(([dr, dc]) => addMove(r + dr, c + dc));
if (!pieceHasMoved) {
// Use the passed hasMoved status
const opponentColor = color === "white" ? "black" : "white";
// Kingside
if (
castlingRights[color].K &&
!board[r][c + 1] &&
!board[r][c + 2] &&
board[r][c + 3] &&
getPieceType(board[r][c + 3].piece) === "rook" &&
!board[r][c + 3].hasMoved &&
!isSquareAttacked(r, c, opponentColor, board) && // King's current square
!isSquareAttacked(r, c + 1, opponentColor, board) && // Transit square
!isSquareAttacked(r, c + 2, opponentColor, board) // Final square
) {
addMove(r, c + 2, { isCastling: true, castlingSide: "K" });
}
// Queenside
if (
castlingRights[color].Q &&
!board[r][c - 1] &&
!board[r][c - 2] &&
!board[r][c - 3] &&
board[r][c - 4] &&
getPieceType(board[r][c - 4].piece) === "rook" &&
!board[r][c - 4].hasMoved &&
!isSquareAttacked(r, c, opponentColor, board) && // King's current square
!isSquareAttacked(r, c - 1, opponentColor, board) && // Transit square
!isSquareAttacked(r, c - 2, opponentColor, board) // Final square
) {
addMove(r, c - 2, { isCastling: true, castlingSide: "Q" });
}
}
}
return moves;
}
// Revised isSquareAttacked function
function isSquareAttacked(
r_target,
c_target,
attackerColor,
currentBoard,
) {
for (let r_attacker = 0; r_attacker < 8; r_attacker++) {
for (let c_attacker = 0; c_attacker < 8; c_attacker++) {
const pieceData = currentBoard[r_attacker][c_attacker];
if (pieceData && getPieceColor(pieceData.piece) === attackerColor) {
const pieceType = getPieceType(pieceData.piece);
if (pieceType === "pawn") {
const pawnAttackDir = attackerColor === "white" ? -1 : 1;
if (
(r_attacker + pawnAttackDir === r_target &&
c_attacker + 1 === c_target) ||
(r_attacker + pawnAttackDir === r_target &&
c_attacker - 1 === c_target)
) {
return true;
}
} else {
// For non-pawn pieces (King, Queen, Rook, Bishop, Knight)
// Generate pseudo-legal moves for this attacking piece
// For kings, hasMoved status is pieceData.hasMoved. Castling rights/EP target are irrelevant for attack calc.
const pseudoMoves = getPseudoLegalMoves(
r_attacker,
c_attacker,
pieceData.piece,
currentBoard,
{},
null,
pieceData.hasMoved,
);
for (const move of pseudoMoves) {
if (move.toR === r_target && move.toC === c_target) {
return true;
}
}
}
}
}
}
return false;
}
function findKing(kingColor, currentBoard) {
const kingChar = kingColor === "white" ? "K" : "k";
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
if (currentBoard[r][c] && currentBoard[r][c].piece === kingChar) {
return { r, c };
}
}
}
return null;
}
function isKingInCheck(kingColor, currentBoard) {
const kingPos = findKing(kingColor, currentBoard);
if (!kingPos) return false;
const attackerColor = kingColor === "white" ? "black" : "white";
return isSquareAttacked(
kingPos.r,
kingPos.c,
attackerColor,
currentBoard,
);
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
}
function updateTimerDisplay() {
whiteTimerDisplay.textContent = `White: ${formatTime(gameState.timers.white)}`;
blackTimerDisplay.textContent = `Black: ${formatTime(gameState.timers.black)}`;
whiteTimerDisplay.classList.remove("active-timer");
blackTimerDisplay.classList.remove("active-timer");
if (!gameState.isGameOver) {
// Only show active if game is ongoing
if (gameState.currentPlayer === "white") {
whiteTimerDisplay.classList.add("active-timer");
} else {
blackTimerDisplay.classList.add("active-timer");
}
}
}
function startTimers() {
stopTimers();
gameState.timers.intervalId = setInterval(() => {
if (gameState.isGameOver) {
stopTimers();
return;
}
if (gameState.currentPlayer === "white") {
gameState.timers.white--;
if (gameState.timers.white <= 0) {
gameState.timers.white = 0; // Prevent negative display
handleTimeout("white");
}
} else {
gameState.timers.black--;
if (gameState.timers.black <= 0) {
gameState.timers.black = 0; // Prevent negative display
handleTimeout("black");
}
}
updateTimerDisplay();
}, 1000);
}
function stopTimers() {
if (gameState.timers.intervalId) {
clearInterval(gameState.timers.intervalId);
gameState.timers.intervalId = null;
}
}
function switchTimers() {
updateTimerDisplay(); // Active timer highlighting is handled by updateTimerDisplay
}
function handleTimeout(playerColor) {
stopTimers();
gameState.isGameOver = true;
const winner = playerColor === "white" ? "Black" : "White";
gameState.gameStatusMessage = `Time out! ${winner} wins.`;
gameStatusDisplay.textContent = gameState.gameStatusMessage;
updateTimerDisplay();
}
restartButton.addEventListener("click", () => {
stopTimers();
initGame();
});
flipBoardButton.addEventListener("click", () => {
gameState.boardFlipped = !gameState.boardFlipped;
renderBoard();
});
initGame();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment