[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:
2026-03-27 16:59:55 +01:00
parent 54bae2faf5
commit b27032762e
3 changed files with 64 additions and 14 deletions
+22 -5
View File
@@ -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;