From e001d5b2018f3e101dbe34aba2718f36ba090d2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:06:12 +0000 Subject: [PATCH 1/9] Initial plan From 7479728509d376b1714bb262eb7f782adb589604 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:10:43 +0000 Subject: [PATCH 2/9] Implement all critical AI bug fixes and optimizations Co-authored-by: CodeKunalTomar <111980003+CodeKunalTomar@users.noreply.github.com> --- Connect-4.js | 179 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 133 insertions(+), 46 deletions(-) diff --git a/Connect-4.js b/Connect-4.js index 5239956..3d38a4c 100644 --- a/Connect-4.js +++ b/Connect-4.js @@ -1,8 +1,8 @@ // constants const TOTAL_COLUMNS = 7; const TOTAL_ROWS = 7; -const HUMAN_WIN_SCORE = -4; -const COMPUTER_WIN_SCORE = 4; +const HUMAN_WIN_SCORE = -1000000; +const COMPUTER_WIN_SCORE = 1000000; const NO_WIN_SCORE = 0; // Transposition Table constants @@ -55,6 +55,17 @@ const OPENING_BOOK = { // Block 3-stacks on edges '010111': 0, // Block left edge 3-stack '616161': 6, // Block right edge 3-stack + + // Vertical stacking defense (any column) + '4142': 4, // Block column 4 stack + '414241': 4, // Continue blocking column 4 + '2122': 2, // Block column 2 stack + '5152': 5, // Block column 5 stack + + // Human stacks center - MUST respond + '3132': 3, // Stack center with them + '313231': 3, // Continue center control + '31323132': 3, // Keep stacking center }; const MAX_OPENING_MOVES = 15; // Use opening book for first 15 moves (7-8 ply per side) @@ -81,12 +92,13 @@ const AI_CONFIG = { USE_THREAT_SEARCH: true, // Evaluation weights for near-perfect play - DOUBLE_THREAT_WEIGHT: 5000, - THREAT_WEIGHT: 500, - POTENTIAL_THREAT_WEIGHT: 50, - CENTER_WEIGHT: 100, - ODD_EVEN_WEIGHT: 300, - MOBILITY_WEIGHT: 10, + DOUBLE_THREAT_WEIGHT: 50000, + THREAT_WEIGHT: 5000, + POTENTIAL_THREAT_WEIGHT: 500, + CENTER_WEIGHT: 200, + ODD_EVEN_WEIGHT: 600, + MOBILITY_WEIGHT: 20, + VERTICAL_THREAT_WEIGHT: 1500, }; // ============================================================================ @@ -571,31 +583,30 @@ GameState.prototype.countPotentialLines = function(player) { return lines; } -// Detect threats building on edges (columns 0, 1, 5, 6) -GameState.prototype.detectEdgeThreats = function(player) { +// Detect vertical stacking threats on ANY column +GameState.prototype.detectVerticalThreats = function(player) { let threats = 0; - const edgeCols = [0, 1, 5, 6]; - for (const col of edgeCols) { + for (let col = 0; col < TOTAL_COLUMNS; col++) { const height = this.bitboard.heights[col]; + if (height < 2 || height >= TOTAL_ROWS) continue; - // Check vertical stacking - count consecutive pieces from the top - if (height >= 2) { - let consecutive = 0; - let maxConsecutive = 0; - - for (let row = 0; row < height; row++) { - if (this.board[col][row] === player) { - consecutive++; - maxConsecutive = Math.max(maxConsecutive, consecutive); - } else { - consecutive = 0; - } + // Count consecutive pieces from top of stack + let consecutive = 0; + for (let row = height - 1; row >= 0; row--) { + if (this.board[col][row] === player) { + consecutive++; + } else { + break; // Stop at first non-player piece } - - // Threat if there are 2+ consecutive pieces and room to grow - if (maxConsecutive >= 2 && height < TOTAL_ROWS) { - threats++; + } + + // Threat if 2+ consecutive at top with room to grow + if (consecutive >= 2) { + threats++; + // Extra threat for 3 in a row (one move from winning) + if (consecutive >= 3) { + threats += 2; } } } @@ -608,8 +619,8 @@ GameState.prototype.advancedEvaluate = function(player) { const opponent = player === 1 ? 2 : 1; // Terminal states - if (this.score === COMPUTER_WIN_SCORE) return 100000; - if (this.score === HUMAN_WIN_SCORE) return -100000; + if (this.score === COMPUTER_WIN_SCORE) return COMPUTER_WIN_SCORE; + if (this.score === HUMAN_WIN_SCORE) return HUMAN_WIN_SCORE; if (this.isBoardFull()) return 0; let score = 0; @@ -649,11 +660,11 @@ GameState.prototype.advancedEvaluate = function(player) { score += this.countPotentialLines(2) * AI_CONFIG.MOBILITY_WEIGHT; score -= this.countPotentialLines(1) * AI_CONFIG.MOBILITY_WEIGHT; - // 7. Edge threat detection - const aiEdgeThreats = this.detectEdgeThreats(2); - const humanEdgeThreats = this.detectEdgeThreats(1); - score += aiEdgeThreats * 400; - score -= humanEdgeThreats * 600; // Weight human edge threats higher (defensive) + // 7. Vertical threat detection (ALL columns, not just edges) + const aiVerticalThreats = this.detectVerticalThreats(2); + const humanVerticalThreats = this.detectVerticalThreats(1); + score += aiVerticalThreats * 800; + score -= humanVerticalThreats * 1200; // Weight human threats higher (defensive priority) return score; } @@ -843,18 +854,79 @@ function makePlayer2Move(col) { }); } +// Check for forced moves (instant wins or required blocks) +function getForcedMove(gameState) { + const validMoves = []; + const winningMoves = []; + const blockingMoves = []; + + for (let col = 0; col < TOTAL_COLUMNS; col++) { + if (gameState.bitboard.heights[col] >= TOTAL_ROWS) continue; + validMoves.push(col); + + // Check if AI can win immediately + const winTest = new GameState(gameState); + winTest.makeMove(2, col); + if (winTest.isWin()) { + winningMoves.push(col); + } + + // Check if human would win if they play here + const blockTest = new GameState(gameState); + blockTest.makeMove(1, col); + if (blockTest.isWin()) { + blockingMoves.push(col); + } + } + + // Priority 1: Take immediate win + if (winningMoves.length > 0) { + return { col: winningMoves[0], type: 'win' }; + } + + // Priority 2: Block immediate threat (if only one blocking move) + if (blockingMoves.length === 1) { + return { col: blockingMoves[0], type: 'block' }; + } + + // Priority 3: Multiple threats = losing position, but still must block one + if (blockingMoves.length > 1) { + return { col: blockingMoves[0], type: 'desperate-block' }; + } + + // Priority 4: Only one valid move + if (validMoves.length === 1) { + return { col: validMoves[0], type: 'only-move' }; + } + + return null; // No forced move, use search +} + function makeComputerMove(maxDepth) { let col; let isWinImminent = false; let isLossImminent = false; - // Check opening book first - const boardKey = getBoardStateKey(currentGameState); - if (AI_CONFIG.USE_OPENING_BOOK && boardKey !== null && boardKey in OPENING_BOOK) { - const openingCol = OPENING_BOOK[boardKey]; - // Verify move is valid - if (currentGameState.bitboard.heights[openingCol] < TOTAL_ROWS) { - col = openingCol; + // Check for forced moves FIRST (before opening book) + const forcedMove = getForcedMove(currentGameState); + if (forcedMove) { + col = forcedMove.col; + if (forcedMove.type === 'win') { + isWinImminent = true; + } else if (forcedMove.type === 'desperate-block') { + isLossImminent = true; + } + } + + // Then check opening book (only if no forced move) + if (col === undefined) { + const boardKey = getBoardStateKey(currentGameState); + if (AI_CONFIG.USE_OPENING_BOOK && boardKey !== null && boardKey in OPENING_BOOK) { + const openingCol = OPENING_BOOK[boardKey]; + // Verify move is valid + if (currentGameState.bitboard.heights[openingCol] < TOTAL_ROWS) { + col = openingCol; + } } } @@ -862,7 +934,8 @@ function makeComputerMove(maxDepth) { // Use iterative deepening with aspiration windows and time management const startTime = Date.now(); const maxTime = AI_CONFIG.MAX_TIME; - const actualMaxDepth = Math.min(maxDepth, AI_CONFIG.MAX_DEPTH); + const MIN_SEARCH_DEPTH = 12; + const actualMaxDepth = Math.max(MIN_SEARCH_DEPTH, Math.min(maxDepth, AI_CONFIG.MAX_DEPTH)); let bestMove = 3; // Center as default let bestScore = 0; @@ -922,7 +995,7 @@ function makeComputerMove(maxDepth) { } // Early exit if we have a very strong position - if (Math.abs(bestScore) > 8000) { + if (Math.abs(bestScore) > 50000) { break; } } @@ -1027,7 +1100,14 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) { if (childNode.isWin()) { // Terminal win node + // Properly handle win scores relative to current player score = childNode.score; + // Add depth bonus - prefer faster wins, delay losses + if (score > 0) { + score = score - recursionsRemaining; // Prefer faster AI wins + } else { + score = score + recursionsRemaining; // Prefer slower human wins (delay loss) + } } else if (childNode.isBoardFull()) { // Terminal draw node score = 0; @@ -1140,7 +1220,7 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) { }); } - // For top level, collect all moves tied for best move and randomly pick one + // For top level, collect all moves tied for best move and use deterministic tie-breaking if (isTopLevel) { const candidates = []; for (let col = 0; col < TOTAL_COLUMNS; col++) { @@ -1150,7 +1230,14 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) { candidates.push(col); } } - return candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)] : bestMove; + // Deterministic tie-breaking - prefer center columns + if (candidates.length > 0) { + const preference = [3, 2, 4, 1, 5, 0, 6]; + for (const col of preference) { + if (candidates.includes(col)) return col; + } + } + return bestMove; } return bestMove; From 55cb8c3bdb90b827b20943af3c1feab8c70ebe53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:13:24 +0000 Subject: [PATCH 3/9] Add test cases for center column vertical threats Co-authored-by: CodeKunalTomar <111980003+CodeKunalTomar@users.noreply.github.com> --- ai-tests.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/ai-tests.js b/ai-tests.js index 4e835c9..adf79b2 100644 --- a/ai-tests.js +++ b/ai-tests.js @@ -129,6 +129,40 @@ const AI_TESTS = [ setup: [{ player: 1, col: 3 }], shouldConsider: [2, 3, 4], }, + + // Center Column Vertical Threat Detection (Bug from screenshot) + { + name: "AI blocks column 4 (center-adjacent) vertical stack", + setup: [ + { player: 1, col: 4 }, { player: 2, col: 3 }, + { player: 1, col: 4 }, { player: 2, col: 3 }, + { player: 1, col: 4 }, + ], + expectedMove: 4, + mustBlock: true, + description: "AI must block vertical threat on column 4 (the screenshot scenario)" + }, + { + name: "AI blocks column 3 (center) vertical stack", + setup: [ + { player: 1, col: 3 }, { player: 2, col: 2 }, + { player: 1, col: 3 }, { player: 2, col: 2 }, + { player: 1, col: 3 }, + ], + expectedMove: 3, + mustBlock: true, + description: "AI must block vertical threat on center column" + }, + { + name: "AI blocks column 2 vertical stack", + setup: [ + { player: 1, col: 2 }, { player: 2, col: 3 }, + { player: 1, col: 2 }, { player: 2, col: 3 }, + { player: 1, col: 2 }, + ], + expectedMove: 2, + mustBlock: true + }, ]; // Test runner From fadb94dd58b8cc850ccad9ab3011801692cee5bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:15:12 +0000 Subject: [PATCH 4/9] Move MIN_DEPTH to AI_CONFIG for better organization Co-authored-by: CodeKunalTomar <111980003+CodeKunalTomar@users.noreply.github.com> --- Connect-4.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Connect-4.js b/Connect-4.js index 3d38a4c..05f1a77 100644 --- a/Connect-4.js +++ b/Connect-4.js @@ -81,6 +81,7 @@ const CENTER_ADJACENT_WEIGHT = 2; // ============================================================================ const AI_CONFIG = { MAX_DEPTH: 20, // Maximum search depth (20 ply) + MIN_DEPTH: 12, // Minimum search depth (12 ply) MAX_TIME: 5000, // Max 5 seconds per move TT_SIZE: MAX_TT_SIZE, // Transposition table size USE_OPENING_BOOK: true, @@ -934,8 +935,7 @@ function makeComputerMove(maxDepth) { // Use iterative deepening with aspiration windows and time management const startTime = Date.now(); const maxTime = AI_CONFIG.MAX_TIME; - const MIN_SEARCH_DEPTH = 12; - const actualMaxDepth = Math.max(MIN_SEARCH_DEPTH, Math.min(maxDepth, AI_CONFIG.MAX_DEPTH)); + const actualMaxDepth = Math.max(AI_CONFIG.MIN_DEPTH, Math.min(maxDepth, AI_CONFIG.MAX_DEPTH)); let bestMove = 3; // Center as default let bestScore = 0; From bbbb83e76f2ead4cda4fb0a0b28751cd84ebfb44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:16:56 +0000 Subject: [PATCH 5/9] Fix win score handling to use constants instead of sign check Co-authored-by: CodeKunalTomar <111980003+CodeKunalTomar@users.noreply.github.com> --- Connect-4.js | 4 ++-- ai-tests.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Connect-4.js b/Connect-4.js index 05f1a77..f65fd2f 100644 --- a/Connect-4.js +++ b/Connect-4.js @@ -1103,9 +1103,9 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) { // Properly handle win scores relative to current player score = childNode.score; // Add depth bonus - prefer faster wins, delay losses - if (score > 0) { + if (score === COMPUTER_WIN_SCORE) { score = score - recursionsRemaining; // Prefer faster AI wins - } else { + } else if (score === HUMAN_WIN_SCORE) { score = score + recursionsRemaining; // Prefer slower human wins (delay loss) } } else if (childNode.isBoardFull()) { diff --git a/ai-tests.js b/ai-tests.js index adf79b2..892595d 100644 --- a/ai-tests.js +++ b/ai-tests.js @@ -161,7 +161,8 @@ const AI_TESTS = [ { player: 1, col: 2 }, ], expectedMove: 2, - mustBlock: true + mustBlock: true, + description: "AI must block vertical threat on column 2" }, ]; From 36dfc356278385a973e1c760554a468b0ba3ef67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:19:34 +0000 Subject: [PATCH 6/9] Add helper functions for win score detection with depth bonus support Co-authored-by: CodeKunalTomar <111980003+CodeKunalTomar@users.noreply.github.com> --- Connect-4.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Connect-4.js b/Connect-4.js index f65fd2f..d81acde 100644 --- a/Connect-4.js +++ b/Connect-4.js @@ -325,6 +325,20 @@ GameState.prototype.isWin = function() { return (this.score === HUMAN_WIN_SCORE || this.score === COMPUTER_WIN_SCORE); } +// Helper function to check if a score indicates a win, accounting for depth bonus +function isWinScore(score) { + return score >= COMPUTER_WIN_SCORE - AI_CONFIG.MAX_DEPTH || + score <= HUMAN_WIN_SCORE + AI_CONFIG.MAX_DEPTH; +} + +function isComputerWinScore(score) { + return score >= COMPUTER_WIN_SCORE - AI_CONFIG.MAX_DEPTH; +} + +function isHumanWinScore(score) { + return score <= HUMAN_WIN_SCORE + AI_CONFIG.MAX_DEPTH; +} + // Keep legacy methods for backward compatibility if needed GameState.prototype.checkRuns = function(player, col, row, colStep, rowStep) { let runCount = 0; @@ -973,12 +987,13 @@ function makeComputerMove(maxDepth) { bestScore = origin.score; - if (origin.score === HUMAN_WIN_SCORE) { + // Check for win/loss using helper functions that account for depth bonuses + if (isHumanWinScore(origin.score)) { // AI realizes it can lose isLossImminent = true; // Keep the best move from previous depth break; - } else if (origin.score === COMPUTER_WIN_SCORE) { + } else if (isComputerWinScore(origin.score)) { // AI knows how to win col = tentativeCol; isWinImminent = true; From 95fb9b69a90204a5440b0ec052a0a6429e6f85e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:25:20 +0000 Subject: [PATCH 7/9] Use AI_CONFIG constant for vertical threat weight and clarify comments Co-authored-by: CodeKunalTomar <111980003+CodeKunalTomar@users.noreply.github.com> --- Connect-4.js | 8 ++++---- ai-tests.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Connect-4.js b/Connect-4.js index d81acde..d663512 100644 --- a/Connect-4.js +++ b/Connect-4.js @@ -62,8 +62,8 @@ const OPENING_BOOK = { '2122': 2, // Block column 2 stack '5152': 5, // Block column 5 stack - // Human stacks center - MUST respond - '3132': 3, // Stack center with them + // Human stacks center - AI must contest by stacking on same column + '3132': 3, // AI contests center column (not cooperating, competing for control) '313231': 3, // Continue center control '31323132': 3, // Keep stacking center }; @@ -678,8 +678,8 @@ GameState.prototype.advancedEvaluate = function(player) { // 7. Vertical threat detection (ALL columns, not just edges) const aiVerticalThreats = this.detectVerticalThreats(2); const humanVerticalThreats = this.detectVerticalThreats(1); - score += aiVerticalThreats * 800; - score -= humanVerticalThreats * 1200; // Weight human threats higher (defensive priority) + score += aiVerticalThreats * AI_CONFIG.VERTICAL_THREAT_WEIGHT; + score -= humanVerticalThreats * (AI_CONFIG.VERTICAL_THREAT_WEIGHT * 1.5); // Weight human threats higher (defensive priority) return score; } diff --git a/ai-tests.js b/ai-tests.js index 892595d..e4be3b2 100644 --- a/ai-tests.js +++ b/ai-tests.js @@ -140,7 +140,7 @@ const AI_TESTS = [ ], expectedMove: 4, mustBlock: true, - description: "AI must block vertical threat on column 4 (the screenshot scenario)" + description: "AI must detect and block vertical threat on column 4 (center-adjacent column). This tests the fix for blind spots on non-edge columns." }, { name: "AI blocks column 3 (center) vertical stack", From 7daf046bc45e8626c9cbe656acf18aa51b58c8f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:27:28 +0000 Subject: [PATCH 8/9] Polish comments for consistency and conciseness Co-authored-by: CodeKunalTomar <111980003+CodeKunalTomar@users.noreply.github.com> --- Connect-4.js | 6 +++--- ai-tests.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Connect-4.js b/Connect-4.js index d663512..aac9219 100644 --- a/Connect-4.js +++ b/Connect-4.js @@ -62,8 +62,8 @@ const OPENING_BOOK = { '2122': 2, // Block column 2 stack '5152': 5, // Block column 5 stack - // Human stacks center - AI must contest by stacking on same column - '3132': 3, // AI contests center column (not cooperating, competing for control) + // Human stacks center - AI contests by stacking same column + '3132': 3, // Contest center column '313231': 3, // Continue center control '31323132': 3, // Keep stacking center }; @@ -1235,7 +1235,7 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) { }); } - // For top level, collect all moves tied for best move and use deterministic tie-breaking + // For top level, collect all moves tied for best score and use center-preference tie-breaking if (isTopLevel) { const candidates = []; for (let col = 0; col < TOTAL_COLUMNS; col++) { diff --git a/ai-tests.js b/ai-tests.js index e4be3b2..e2d2b2c 100644 --- a/ai-tests.js +++ b/ai-tests.js @@ -140,7 +140,7 @@ const AI_TESTS = [ ], expectedMove: 4, mustBlock: true, - description: "AI must detect and block vertical threat on column 4 (center-adjacent column). This tests the fix for blind spots on non-edge columns." + description: "Tests vertical threat detection on center-adjacent column" }, { name: "AI blocks column 3 (center) vertical stack", From 7b8c32489465fd91cf9c6f97d9e10d3ab02644dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:29:27 +0000 Subject: [PATCH 9/9] Fix tie-breaking logic to use bestScore instead of node.score Co-authored-by: CodeKunalTomar <111980003+CodeKunalTomar@users.noreply.github.com> --- Connect-4.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Connect-4.js b/Connect-4.js index aac9219..3d377bf 100644 --- a/Connect-4.js +++ b/Connect-4.js @@ -1240,8 +1240,8 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) { const candidates = []; for (let col = 0; col < TOTAL_COLUMNS; col++) { if (childNodes[col] !== undefined && - ((player === 2 && childNodes[col].score === node.score) || - (player === 1 && childNodes[col].score === node.score))) { + ((player === 2 && childNodes[col].score === bestScore) || + (player === 1 && childNodes[col].score === bestScore))) { candidates.push(col); } }