diff --git a/connect_four.js b/connect_four.js index 4be09ac..fdd4850 100644 --- a/connect_four.js +++ b/connect_four.js @@ -1,6 +1,6 @@ /* ============================================================ * Connect Four — Browser Edition - * A single-file game: AI (minimax + alpha-beta), demo mode, + * A single-file game: AI (minimax + alpha-beta + heuristic), demo mode, * game log (localStorage), blunder mode, idle timeout. * * Include this script in an HTML page that has: @@ -168,6 +168,7 @@ function scanBoard(b) { function evaluateBoard(b, aiP, huP) { let score = 0; + let aiThreats = 0, huThreats = 0; // Center column bonus for (let r = 0; r < ROWS; r++) { @@ -177,16 +178,27 @@ function evaluateBoard(b, aiP, huP) { // Score a window of 4 cells by piece counts function scoreWindow(c, r, dc, dr) { - let ai = 0, hu = 0; + let ai = 0, hu = 0, emptyC = -1, emptyR = -1; for (let i = 0; i < 4; i++) { - const v = b[c + i * dc][r + i * dr]; + const cc = c + i * dc; + const rr = r + i * dr; + const v = b[cc][rr]; if (v === aiP) ai++; else if (v === huP) hu++; + else { emptyC = cc; emptyR = rr; } } if (ai > 0 && hu > 0) return 0; - if (ai === 3) return 50; + if (ai === 3) { + aiThreats++; + const playable = emptyR === 0 || b[emptyC][emptyR - 1] !== 0; + return playable ? 100 : 40; + } if (ai === 2) return 5; - if (hu === 3) return -50; + if (hu === 3) { + huThreats++; + const playable = emptyR === 0 || b[emptyC][emptyR - 1] !== 0; + return playable ? -100 : -40; + } if (hu === 2) return -5; return 0; } @@ -208,6 +220,10 @@ function evaluateBoard(b, aiP, huP) { for (let c = 0; c <= COLS - 4; c++) score += scoreWindow(c, r, 1, -1); + // Fork bonus: multiple threats are disproportionately dangerous + if (aiThreats >= 2) score += 200; + if (huThreats >= 2) score -= 200; + return score; } @@ -324,6 +340,7 @@ function checkGameEnd() { if (gameState !== State.DEMO) { games = logGame(games, gameMenuMode, gameLevel, won ? w : 0, currentMoves); + console.log(`Game: ${currentMoves} → ${won ? playerName(w) + " wins" : "Draw"}`); } gameState = won ? State.FINISHED_WIN : State.FINISHED_DRAW; demoResetTimer = performance.now() / 1000; diff --git a/connect_four.py b/connect_four.py index 02376aa..70ea28a 100644 --- a/connect_four.py +++ b/connect_four.py @@ -1,4 +1,4 @@ -"""Connect Four terminal game with AI, using Rich for display.""" +"""Connect Four terminal game with AI (minimax + alpha-beta + heuristic), using Rich for display.""" import os import queue @@ -271,6 +271,8 @@ def log_game(games: list[dict], game_menu_mode: int, level: int, winner: int, mo def evaluate_board(board: list[list[int]], ai_p: int, hu_p: int) -> int: score = 0 + ai_threats = 0 + hu_threats = 0 # Center column bonus for r in range(ROWS): @@ -281,21 +283,30 @@ def evaluate_board(board: list[list[int]], ai_p: int, hu_p: int) -> int: # Score a window of 4 cells by piece counts def score_window(c: int, r: int, dc: int, dr: int) -> int: - ai, hu = 0, 0 + nonlocal ai_threats, hu_threats + ai, hu, empty_c, empty_r = 0, 0, -1, -1 for i in range(4): - v = board[c + i * dc][r + i * dr] + cc = c + i * dc + rr = r + i * dr + v = board[cc][rr] if v == ai_p: ai += 1 elif v == hu_p: hu += 1 + else: + empty_c, empty_r = cc, rr if ai > 0 and hu > 0: return 0 if ai == 3: - return 50 + ai_threats += 1 + playable = empty_r == 0 or board[empty_c][empty_r - 1] != 0 + return 100 if playable else 40 if ai == 2: return 5 if hu == 3: - return -50 + hu_threats += 1 + playable = empty_r == 0 or board[empty_c][empty_r - 1] != 0 + return -100 if playable else -40 if hu == 2: return -5 return 0 @@ -317,6 +328,12 @@ def evaluate_board(board: list[list[int]], ai_p: int, hu_p: int) -> int: for c in range(COLS - 3): score += score_window(c, r, 1, -1) + # Fork bonus: multiple threats are disproportionately dangerous + if ai_threats >= 2: + score += 200 + if hu_threats >= 2: + score -= 200 + return score diff --git a/src/main.cpp b/src/main.cpp index 53befac..58fae4c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -268,6 +268,7 @@ int8_t scanBoard() { int evaluateBoard(int8_t aiP, int8_t huP) { int score = 0; + int aiThreats = 0, huThreats = 0; // Center column bonus for (int r = 0; r < ROWS; r++) { @@ -277,16 +278,27 @@ int evaluateBoard(int8_t aiP, int8_t huP) { // Score a window of 4 cells by piece counts auto scoreWindow = [&](int c, int r, int dc, int dr) -> int { - int ai = 0, hu = 0; + int ai = 0, hu = 0, emptyC = -1, emptyR = -1; for (int i = 0; i < 4; i++) { - int8_t v = board[c + i * dc][r + i * dr]; + int cc = c + i * dc; + int rr = r + i * dr; + int8_t v = board[cc][rr]; if (v == aiP) ai++; else if (v == huP) hu++; + else { emptyC = cc; emptyR = rr; } } if (ai > 0 && hu > 0) return 0; - if (ai == 3) return 50; + if (ai == 3) { + aiThreats++; + bool playable = emptyR == 0 || board[emptyC][emptyR - 1] != 0; + return playable ? 100 : 40; + } if (ai == 2) return 5; - if (hu == 3) return -50; + if (hu == 3) { + huThreats++; + bool playable = emptyR == 0 || board[emptyC][emptyR - 1] != 0; + return playable ? -100 : -40; + } if (hu == 2) return -5; return 0; }; @@ -296,6 +308,10 @@ int evaluateBoard(int8_t aiP, int8_t huP) { for (int r = 0; r < 3; r++) for (int c = 0; c < 4; c++) score += scoreWindow(c, r, 1, 1); for (int r = 3; r < 6; r++) for (int c = 0; c < 4; c++) score += scoreWindow(c, r, 1, -1); + // Fork bonus: multiple threats are disproportionately dangerous + if (aiThreats >= 2) score += 200; + if (huThreats >= 2) score -= 200; + return score; }