[fix] Add heuristic evaluation, fork detection, and Phase 1 win/block split to AI.
Minimax leaf nodes now return a positional score instead of 0, using playable-threat detection (±100), non-playable threats (±40), fork bonus (±200), two-in-a-row (±5), and center control (±3). Phase 1 is split into two passes so the AI never blocks when it can win. Game sequence is now auto-logged to the browser console on game end. Applied to all three implementations (C++, JS, Python). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+22
-5
@@ -1,6 +1,6 @@
|
|||||||
/* ============================================================
|
/* ============================================================
|
||||||
* Connect Four — Browser Edition
|
* 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.
|
* game log (localStorage), blunder mode, idle timeout.
|
||||||
*
|
*
|
||||||
* Include this script in an HTML page that has:
|
* Include this script in an HTML page that has:
|
||||||
@@ -168,6 +168,7 @@ function scanBoard(b) {
|
|||||||
|
|
||||||
function evaluateBoard(b, aiP, huP) {
|
function evaluateBoard(b, aiP, huP) {
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
let aiThreats = 0, huThreats = 0;
|
||||||
|
|
||||||
// Center column bonus
|
// Center column bonus
|
||||||
for (let r = 0; r < ROWS; r++) {
|
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
|
// Score a window of 4 cells by piece counts
|
||||||
function scoreWindow(c, r, dc, dr) {
|
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++) {
|
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++;
|
if (v === aiP) ai++;
|
||||||
else if (v === huP) hu++;
|
else if (v === huP) hu++;
|
||||||
|
else { emptyC = cc; emptyR = rr; }
|
||||||
}
|
}
|
||||||
if (ai > 0 && hu > 0) return 0;
|
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 (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;
|
if (hu === 2) return -5;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -208,6 +220,10 @@ function evaluateBoard(b, aiP, huP) {
|
|||||||
for (let c = 0; c <= COLS - 4; c++)
|
for (let c = 0; c <= COLS - 4; c++)
|
||||||
score += scoreWindow(c, r, 1, -1);
|
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;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +340,7 @@ function checkGameEnd() {
|
|||||||
|
|
||||||
if (gameState !== State.DEMO) {
|
if (gameState !== State.DEMO) {
|
||||||
games = logGame(games, gameMenuMode, gameLevel, won ? w : 0, currentMoves);
|
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;
|
gameState = won ? State.FINISHED_WIN : State.FINISHED_DRAW;
|
||||||
demoResetTimer = performance.now() / 1000;
|
demoResetTimer = performance.now() / 1000;
|
||||||
|
|||||||
+22
-5
@@ -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 os
|
||||||
import queue
|
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:
|
def evaluate_board(board: list[list[int]], ai_p: int, hu_p: int) -> int:
|
||||||
score = 0
|
score = 0
|
||||||
|
ai_threats = 0
|
||||||
|
hu_threats = 0
|
||||||
|
|
||||||
# Center column bonus
|
# Center column bonus
|
||||||
for r in range(ROWS):
|
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
|
# Score a window of 4 cells by piece counts
|
||||||
def score_window(c: int, r: int, dc: int, dr: int) -> int:
|
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):
|
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:
|
if v == ai_p:
|
||||||
ai += 1
|
ai += 1
|
||||||
elif v == hu_p:
|
elif v == hu_p:
|
||||||
hu += 1
|
hu += 1
|
||||||
|
else:
|
||||||
|
empty_c, empty_r = cc, rr
|
||||||
if ai > 0 and hu > 0:
|
if ai > 0 and hu > 0:
|
||||||
return 0
|
return 0
|
||||||
if ai == 3:
|
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:
|
if ai == 2:
|
||||||
return 5
|
return 5
|
||||||
if hu == 3:
|
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:
|
if hu == 2:
|
||||||
return -5
|
return -5
|
||||||
return 0
|
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):
|
for c in range(COLS - 3):
|
||||||
score += score_window(c, r, 1, -1)
|
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
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-4
@@ -268,6 +268,7 @@ int8_t scanBoard() {
|
|||||||
|
|
||||||
int evaluateBoard(int8_t aiP, int8_t huP) {
|
int evaluateBoard(int8_t aiP, int8_t huP) {
|
||||||
int score = 0;
|
int score = 0;
|
||||||
|
int aiThreats = 0, huThreats = 0;
|
||||||
|
|
||||||
// Center column bonus
|
// Center column bonus
|
||||||
for (int r = 0; r < ROWS; r++) {
|
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
|
// Score a window of 4 cells by piece counts
|
||||||
auto scoreWindow = [&](int c, int r, int dc, int dr) -> int {
|
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++) {
|
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++;
|
if (v == aiP) ai++;
|
||||||
else if (v == huP) hu++;
|
else if (v == huP) hu++;
|
||||||
|
else { emptyC = cc; emptyR = rr; }
|
||||||
}
|
}
|
||||||
if (ai > 0 && hu > 0) return 0;
|
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 (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;
|
if (hu == 2) return -5;
|
||||||
return 0;
|
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 = 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);
|
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;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user