From 86df37b872633bdded11c315e657ac9c5eccbf29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:22:04 +0000 Subject: [PATCH 1/4] Initial plan From 16d46413f7a8df94e4b8840b6ca428589483f1ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:43:09 +0000 Subject: [PATCH 2/4] Phase 1-3: Remove difficulty system, add chess timers, and enhance AI Co-authored-by: CodeKunalTomar <111980003+CodeKunalTomar@users.noreply.github.com> --- Connect-4.css | 90 +++++++++++++++-------------- Connect-4.js | 128 +++++++++++++++++++++++++++++++++--------- index.html | 35 ++++-------- index.js | 146 +++++++++++++++++++++++++++++++++++++++++------- test-timer.html | 35 ++++++++++++ 5 files changed, 323 insertions(+), 111 deletions(-) create mode 100644 test-timer.html diff --git a/Connect-4.css b/Connect-4.css index 78b960b..8a160f2 100644 --- a/Connect-4.css +++ b/Connect-4.css @@ -55,62 +55,62 @@ h2 { border-radius: 8px; } -.dif-options { - clear: both; - overflow: hidden; - margin: 20px -7px 0; +/* Timer Panel */ +.timer-panel { + display: flex; + flex-direction: column; + gap: 10px; + padding: 15px; } -.dif-options div { - float: left; - width: 20%; +.timer { + padding: 15px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 8px; + font-family: "Doppio One", monospace; + text-align: center; + border: 2px solid transparent; + transition: all 0.3s ease; } -.dif-options input { - display: none; +.timer.active { + border-color: #4CAF50; + box-shadow: 0 0 15px rgba(76, 175, 80, 0.5); + background-color: rgba(76, 175, 80, 0.2); } -.dif-options input:checked+label { - color: #fff; - background-color: #593f6b; - border-color: #fff; - cursor: default; +.timer.warning { + border-color: #f44336; + background-color: rgba(244, 67, 54, 0.2); + animation: pulse 1s infinite; } -.dif-options label { - display: block; - margin: 0 auto; - width: 24px; - height: 24px; - background-color: #666; - border: solid 2px #ccc; - border-radius: 8px; - color: #999; - text-align: center; - line-height: 24px; - cursor: pointer; +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } } -.freeze .dif-options input:not(:checked)+label { - font-size: 0; - margin: 7px auto; - width: 10px; - height: 10px; - border-radius: 4px; - color: transparent; - line-height: 10px; - cursor: default; - transition: .2s +.timer-label { + font-size: 14px; + color: #aaa; + margin-bottom: 5px; } -.start { - margin-top: 20px; +.timer-value { + font-size: 32px; + color: #fff; + font-weight: bold; } -.start button { +/* Start Panel */ +.start-panel { + padding: 12px; +} + +.start-button { display: block; width: 100%; - padding: 2px 12px 4px; + padding: 8px 12px; font-family: inherit; font-size: 24px; border: solid 2px #ccc; @@ -118,13 +118,19 @@ h2 { background-color: #593f6b; color: #fff; cursor: pointer; + transition: all 0.2s; +} + +.start-button:hover { + background-color: #6d4d82; + transform: scale(1.02); } -.start button:focus { +.start-button:focus { outline: none; } -.freeze .start { +.start-panel.freeze .start-button { display: none; } diff --git a/Connect-4.js b/Connect-4.js index 4f0dad1..a9ee1f5 100644 --- a/Connect-4.js +++ b/Connect-4.js @@ -15,6 +15,18 @@ const MAX_TT_SIZE = 1000000; // Max entries in transposition table const BOARD_HEIGHT = TOTAL_ROWS + 1; // Extra row for overflow detection const BOARD_WIDTH = TOTAL_COLUMNS; +// Opening book - prioritize center column +const OPENING_BOOK = { + '': 3, // First move - always play center column +}; + +// Column ordering for move ordering (center columns first for better alpha-beta pruning) +const COLUMN_ORDER = [3, 2, 4, 1, 5, 0, 6]; + +// Position evaluation weights +const CENTER_COLUMN_WEIGHT = 3; +const CENTER_ADJACENT_WEIGHT = 2; + // Initialize Zobrist hashing table (random 64-bit values for each position and player) const zobristTable = []; function initZobrist() { @@ -239,6 +251,53 @@ GameState.prototype.getPlayerForChipAt = function(col, row) { return player; } +// Evaluate position heuristically for non-terminal positions +GameState.prototype.evaluatePosition = function(player) { + let score = 0; + + // Center control - pieces in center columns are more valuable + for (let row = 0; row < this.bitboard.heights[3]; row++) { + if (this.board[3][row] === player) { + score += CENTER_COLUMN_WEIGHT; + } else if (this.board[3][row] !== undefined) { + score -= CENTER_COLUMN_WEIGHT; + } + } + + // Adjacent to center also valuable + for (let col of [2, 4]) { + for (let row = 0; row < this.bitboard.heights[col]; row++) { + if (this.board[col][row] === player) { + score += CENTER_ADJACENT_WEIGHT; + } else if (this.board[col][row] !== undefined) { + score -= CENTER_ADJACENT_WEIGHT; + } + } + } + + // Normalize score to be within minimax range + return score * 0.1; +} + +// Get a simple board state hash for opening book lookup +function getBoardStateKey(gameState) { + let key = ''; + let moveCount = 0; + for (let col = 0; col < TOTAL_COLUMNS; col++) { + moveCount += gameState.board[col].length; + } + + // Only use opening book for first few moves + if (moveCount > 2) return null; + + for (let col = 0; col < TOTAL_COLUMNS; col++) { + for (let row = 0; row < gameState.board[col].length; row++) { + key += col + '' + gameState.board[col][row]; + } + } + return key; +} + // listen for messages from the main thread self.addEventListener('message', function(e) { switch(e.data.messageType) { @@ -285,27 +344,41 @@ function makeComputerMove(maxDepth) { let isWinImminent = false; let isLossImminent = false; - for (let depth = 0; depth <= maxDepth; depth++) { - const origin = new GameState(currentGameState); - const isTopLevel = (depth === maxDepth); - - // Alpha-beta search with initial bounds - const tentativeCol = think(origin, 2, depth, isTopLevel, -Infinity, Infinity); - - if (origin.score === HUMAN_WIN_SCORE) { - // AI realizes it can lose, thinks all moves suck now, keep move picked at previous depth - // this solves the "apathy" problem - isLossImminent = true; - break; - } else if (origin.score === COMPUTER_WIN_SCORE) { - // AI knows how to win, no need to think deeper, use this move - // this solves the "cocky" problem - col = tentativeCol; - isWinImminent = true; - break; - } else { - // go with this move, for now at least - col = tentativeCol; + // Check opening book first + const boardKey = getBoardStateKey(currentGameState); + if (boardKey !== null && OPENING_BOOK.hasOwnProperty(boardKey)) { + col = OPENING_BOOK[boardKey]; + // Verify move is valid + if (currentGameState.bitboard.heights[col] >= TOTAL_ROWS) { + // Opening book move is invalid, fall through to regular search + col = undefined; + } + } + + if (col === undefined) { + // Use iterative deepening with fixed high depth + for (let depth = 0; depth <= maxDepth; depth++) { + const origin = new GameState(currentGameState); + const isTopLevel = (depth === maxDepth); + + // Alpha-beta search with initial bounds + const tentativeCol = think(origin, 2, depth, isTopLevel, -Infinity, Infinity); + + if (origin.score === HUMAN_WIN_SCORE) { + // AI realizes it can lose, thinks all moves suck now, keep move picked at previous depth + // this solves the "apathy" problem + isLossImminent = true; + break; + } else if (origin.score === COMPUTER_WIN_SCORE) { + // AI knows how to win, no need to think deeper, use this move + // this solves the "cocky" problem + col = tentativeCol; + isWinImminent = true; + break; + } else { + // go with this move, for now at least + col = tentativeCol; + } } } @@ -350,13 +423,14 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) { } } - let col; let scoreSet = false; const childNodes = []; let bestMove = -1; - // consider each column as a potential move - for (col = 0; col < TOTAL_COLUMNS; col++) { + // Use column ordering for better alpha-beta pruning (center columns first) + for (let colIdx = 0; colIdx < COLUMN_ORDER.length; colIdx++) { + const col = COLUMN_ORDER[colIdx]; + if(isTopLevel) { self.postMessage({ messageType: 'progress', @@ -376,6 +450,10 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) { // no game stopping win and there are still recursions to make, think deeper const nextPlayer = (player === 1) ? 2 : 1; think(childNode, nextPlayer, recursionsRemaining - 1, false, alpha, beta); + } else if (!childNode.isWin() && recursionsRemaining === 0) { + // At leaf node, apply heuristic evaluation + const heuristicScore = childNode.evaluatePosition(2); // Evaluate for computer + childNode.score = heuristicScore; } if (!scoreSet) { @@ -433,7 +511,7 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) { // For non-top level, just return the best move (may have been pruned) if (isTopLevel) { const candidates = []; - for (col = 0; col < TOTAL_COLUMNS; col++) { + for (let col = 0; col < TOTAL_COLUMNS; col++) { if (childNodes[col] !== undefined && childNodes[col].score === node.score) { candidates.push(col); } diff --git a/index.html b/index.html index 56f6d48..f33cf02 100644 --- a/index.html +++ b/index.html @@ -11,34 +11,19 @@