const size = 5; const target = 4; const empty = ""; const directions = [ [-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1], ]; const boardElement = document.querySelector("#board"); const messageElement = document.querySelector("#message"); const turnBadge = document.querySelector("#turnBadge"); const resetButton = document.querySelector("#resetButton"); const xCapturesElement = document.querySelector("#xCaptures"); const oCapturesElement = document.querySelector("#oCaptures"); const moveList = document.querySelector("#moveList"); let board; let currentPlayer; let captures; let gameOver; let moveNumber; let winningCells; function createBoard() { return Array.from({ length: size }, () => Array(size).fill(empty)); } function setupGame() { board = createBoard(); currentPlayer = "X"; captures = { X: 0, O: 0 }; gameOver = false; moveNumber = 0; winningCells = []; moveList.innerHTML = ""; render(); setMessage("Trap enemy marks between your marks. First to four wins."); } function render() { boardElement.innerHTML = ""; boardElement.style.setProperty("--size", size); for (let row = 0; row < size; row += 1) { for (let col = 0; col < size; col += 1) { const cell = document.createElement("button"); cell.className = "cell"; cell.type = "button"; cell.role = "gridcell"; cell.dataset.row = row; cell.dataset.col = col; cell.setAttribute("aria-label", cellLabel(row, col)); cell.disabled = gameOver || board[row][col] !== empty; if (winningCells.some(([winRow, winCol]) => winRow === row && winCol === col)) { cell.classList.add("winning"); } if (board[row][col]) { const mark = document.createElement("span"); mark.className = `mark mark-${board[row][col].toLowerCase()}`; mark.textContent = board[row][col]; cell.append(mark); } cell.addEventListener("click", () => playMove(row, col)); boardElement.append(cell); } } turnBadge.textContent = gameOver ? "Game over" : `${currentPlayer} to play`; turnBadge.className = `turn-badge player-${currentPlayer.toLowerCase()}`; xCapturesElement.textContent = captures.X; oCapturesElement.textContent = captures.O; } function cellLabel(row, col) { const mark = board[row][col]; const position = `row ${row + 1}, column ${col + 1}`; return mark ? `${mark} at ${position}` : `Empty cell at ${position}`; } function playMove(row, col) { if (gameOver || board[row][col] !== empty) { return; } moveNumber += 1; board[row][col] = currentPlayer; const captured = captureFrom(row, col, currentPlayer); captures[currentPlayer] += captured.length; const win = findWin(currentPlayer); addMove(row, col, captured.length); if (win.length > 0) { winningCells = win; gameOver = true; setMessage(`${currentPlayer} wins with four in a row.`); render(); return; } if (isDraw()) { gameOver = true; setMessage("Draw. The board is full."); render(); return; } currentPlayer = opponentOf(currentPlayer); setMessage(captured.length > 0 ? `${opponentOf(currentPlayer)} captured ${captured.length}. ${currentPlayer} to play.` : `${currentPlayer} to play.` ); render(); flashCapturedCells(captured); } function captureFrom(row, col, player) { const opponent = opponentOf(player); const captured = []; for (const [rowStep, colStep] of directions) { const candidates = []; let nextRow = row + rowStep; let nextCol = col + colStep; while (isInside(nextRow, nextCol) && board[nextRow][nextCol] === opponent) { candidates.push([nextRow, nextCol]); nextRow += rowStep; nextCol += colStep; } if (candidates.length > 0 && isInside(nextRow, nextCol) && board[nextRow][nextCol] === player) { captured.push(...candidates); } } for (const [captureRow, captureCol] of captured) { board[captureRow][captureCol] = empty; } return captured; } function findWin(player) { const axes = [ [0, 1], [1, 0], [1, 1], [1, -1], ]; for (let row = 0; row < size; row += 1) { for (let col = 0; col < size; col += 1) { if (board[row][col] !== player) { continue; } for (const [rowStep, colStep] of axes) { const cells = [[row, col]]; for (let index = 1; index < target; index += 1) { const nextRow = row + rowStep * index; const nextCol = col + colStep * index; if (!isInside(nextRow, nextCol) || board[nextRow][nextCol] !== player) { break; } cells.push([nextRow, nextCol]); } if (cells.length === target) { return cells; } } } } return []; } function isDraw() { return board.every((row) => row.every((cell) => cell !== empty)); } function opponentOf(player) { return player === "X" ? "O" : "X"; } function isInside(row, col) { return row >= 0 && row < size && col >= 0 && col < size; } function addMove(row, col, captureCount) { const item = document.createElement("li"); const captureText = captureCount === 0 ? "" : `, captured ${captureCount}`; item.textContent = `${moveNumber}. ${currentPlayer} to ${row + 1},${col + 1}${captureText}`; moveList.prepend(item); } function setMessage(text) { messageElement.textContent = text; } function flashCapturedCells(captured) { for (const [row, col] of captured) { const cell = boardElement.querySelector(`[data-row="${row}"][data-col="${col}"]`); if (!cell) { continue; } cell.classList.add("captured"); window.setTimeout(() => cell.classList.remove("captured"), 380); } } resetButton.addEventListener("click", setupGame); setupGame();