From f5f5e46b32f92d0f41e2d1f0142b5866921806a2 Mon Sep 17 00:00:00 2001 From: Seppe De Loore Date: Fri, 27 Mar 2026 16:19:54 +0100 Subject: [PATCH] [update] Multi-core and pondering --- src/game.cpp | 495 +++++++++++++++++++++++++++++++-------------------- src/game.h | 49 ++++- src/main.cpp | 73 +++++++- 3 files changed, 412 insertions(+), 205 deletions(-) diff --git a/src/game.cpp b/src/game.cpp index e98cc1e..9617e0b 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -5,6 +5,7 @@ // --- Global definitions --- Adafruit_ST7789 tft = Adafruit_ST7789(&SPI1, LCD_CS, LCD_DC, LCD_RST); +bool core1_separate_stack = true; int8_t board[COLS][ROWS]; WinPos winPos[4]; @@ -35,12 +36,12 @@ uint32_t lastFlash = 0; bool needRedraw = true; const int8_t colOrder[] = {3, 2, 4, 1, 5, 0, 6}; +AiJob aiJob = {}; -// --- Board helpers --- +// --- Board helpers (use global board) --- uint16_t playerColor(int8_t p) { return p == 1 ? C_P1 : C_P2; } uint16_t playerColorDim(int8_t p) { return p == 1 ? C_P1DIM : C_P2DIM; } - int cellX(int c) { return BRD_X + c * CELL + CELL / 2; } int cellY(int r) { return BRD_Y + (ROWS - 1 - r) * CELL + CELL / 2; } @@ -78,69 +79,45 @@ bool isWinPos(int c, int r) void randomizeDemoPlies() { uint8_t strong = random(4, 6), weak = random(2, 4); - if (random(2)) - { - demoPly[0] = strong; - demoPly[1] = weak; - } - else - { - demoPly[0] = weak; - demoPly[1] = strong; - } + if (random(2)) { demoPly[0] = strong; demoPly[1] = weak; } + else { demoPly[0] = weak; demoPly[1] = strong; } } -// --- Game logic --- +// --- Game logic (uses global board) --- int8_t scanBoard() { winCount = 0; for (int r = 0; r < ROWS; r++) - for (int c = 0; c <= COLS - 4; c++) - { + for (int c = 0; c <= COLS - 4; c++) { int8_t p = board[c][r]; - if (p && board[c + 1][r] == p && board[c + 2][r] == p && board[c + 3][r] == p) - { - for (int i = 0; i < 4; i++) - winPos[i] = {(int8_t)(c + i), (int8_t)r}; - winCount = 4; - return p; + if (p && board[c+1][r]==p && board[c+2][r]==p && board[c+3][r]==p) { + for (int i = 0; i < 4; i++) winPos[i] = {(int8_t)(c+i), (int8_t)r}; + winCount = 4; return p; } } for (int r = 0; r <= ROWS - 4; r++) - for (int c = 0; c < COLS; c++) - { + for (int c = 0; c < COLS; c++) { int8_t p = board[c][r]; - if (p && board[c][r + 1] == p && board[c][r + 2] == p && board[c][r + 3] == p) - { - for (int i = 0; i < 4; i++) - winPos[i] = {(int8_t)c, (int8_t)(r + i)}; - winCount = 4; - return p; + if (p && board[c][r+1]==p && board[c][r+2]==p && board[c][r+3]==p) { + for (int i = 0; i < 4; i++) winPos[i] = {(int8_t)c, (int8_t)(r+i)}; + winCount = 4; return p; } } for (int r = 0; r <= ROWS - 4; r++) - for (int c = 0; c <= COLS - 4; c++) - { + for (int c = 0; c <= COLS - 4; c++) { int8_t p = board[c][r]; - if (p && board[c + 1][r + 1] == p && board[c + 2][r + 2] == p && board[c + 3][r + 3] == p) - { - for (int i = 0; i < 4; i++) - winPos[i] = {(int8_t)(c + i), (int8_t)(r + i)}; - winCount = 4; - return p; + if (p && board[c+1][r+1]==p && board[c+2][r+2]==p && board[c+3][r+3]==p) { + for (int i = 0; i < 4; i++) winPos[i] = {(int8_t)(c+i), (int8_t)(r+i)}; + winCount = 4; return p; } } for (int r = 3; r < ROWS; r++) - for (int c = 0; c <= COLS - 4; c++) - { + for (int c = 0; c <= COLS - 4; c++) { int8_t p = board[c][r]; - if (p && board[c + 1][r - 1] == p && board[c + 2][r - 2] == p && board[c + 3][r - 3] == p) - { - for (int i = 0; i < 4; i++) - winPos[i] = {(int8_t)(c + i), (int8_t)(r - i)}; - winCount = 4; - return p; + if (p && board[c+1][r-1]==p && board[c+2][r-2]==p && board[c+3][r-3]==p) { + for (int i = 0; i < 4; i++) winPos[i] = {(int8_t)(c+i), (int8_t)(r-i)}; + winCount = 4; return p; } } return 0; @@ -148,50 +125,7 @@ int8_t scanBoard() int evaluateBoard(int8_t aiP, int8_t huP) { - int score = 0; - for (int r = 0; r < ROWS; r++) - { - if (board[3][r] == aiP) - score += 3; - else if (board[3][r] == huP) - score -= 3; - } - auto sw = [&](int c, int r, int dc, int dr) -> int - { - int ai = 0, hu = 0; - for (int i = 0; i < 4; i++) - { - int8_t v = board[c + i * dc][r + i * dr]; - if (v == aiP) - ai++; - else if (v == huP) - hu++; - } - if (ai && hu) - return 0; - if (ai == 3) - return 50; - if (ai == 2) - return 5; - if (hu == 3) - return -50; - if (hu == 2) - return -5; - return 0; - }; - for (int r = 0; r < 6; r++) - for (int c = 0; c < 4; c++) - score += sw(c, r, 1, 0); - for (int r = 0; r < 3; r++) - for (int c = 0; c < 7; c++) - score += sw(c, r, 0, 1); - for (int r = 0; r < 3; r++) - for (int c = 0; c < 4; c++) - score += sw(c, r, 1, 1); - for (int r = 3; r < 6; r++) - for (int c = 0; c < 4; c++) - score += sw(c, r, 1, -1); - return score; + return evaluateBoardB(board, aiP, huP); } bool checkGameEnd() @@ -199,10 +133,8 @@ bool checkGameEnd() winnerPlayer = scanBoard(); bool won = winnerPlayer != 0; bool draw = !won && isBoardFull(); - if (!won && !draw) - return false; - if (gameState != DEMO) - logGame(won ? winnerPlayer : 0); + if (!won && !draw) return false; + if (gameState != DEMO) logGame(won ? winnerPlayer : 0); gameState = won ? FINISHED_WIN : FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = millis(); @@ -211,139 +143,314 @@ bool checkGameEnd() return true; } -// --- AI --- +// ================================================================ +// Pure board functions (no globals — safe for core 1) +// ================================================================ -int minimax(int depth, int alpha, int beta, bool isMax, - int8_t aiP, int8_t huP) +int getFirstEmptyRowB(const int8_t b[][ROWS], int col) { - if (gameState == DEMO && depth >= currentLookAhead - 1) - { - int16_t rx, ry; - if (readRawTouch(rx, ry)) - { - abortAi = true; - return 0; - } - } - yield(); - if (abortAi) - return 0; + for (int r = 0; r < ROWS; r++) + if (b[col][r] == 0) return r; + return -1; +} - int8_t win = scanBoard(); - if (win == aiP) - return 1000 + depth; - if (win == huP) - return -1000 - depth; - if (depth == 0 || isBoardFull()) - return evaluateBoard(aiP, huP); +bool isBoardFullB(const int8_t b[][ROWS]) +{ + for (int c = 0; c < COLS; c++) + if (b[c][ROWS - 1] == 0) return false; + return true; +} + +int8_t scanBoardB(const int8_t b[][ROWS]) +{ + for (int r = 0; r < ROWS; r++) + for (int c = 0; c <= COLS - 4; c++) { + int8_t p = b[c][r]; + if (p && b[c+1][r]==p && b[c+2][r]==p && b[c+3][r]==p) return p; + } + for (int r = 0; r <= ROWS - 4; r++) + for (int c = 0; c < COLS; c++) { + int8_t p = b[c][r]; + if (p && b[c][r+1]==p && b[c][r+2]==p && b[c][r+3]==p) return p; + } + for (int r = 0; r <= ROWS - 4; r++) + for (int c = 0; c <= COLS - 4; c++) { + int8_t p = b[c][r]; + if (p && b[c+1][r+1]==p && b[c+2][r+2]==p && b[c+3][r+3]==p) return p; + } + for (int r = 3; r < ROWS; r++) + for (int c = 0; c <= COLS - 4; c++) { + int8_t p = b[c][r]; + if (p && b[c+1][r-1]==p && b[c+2][r-2]==p && b[c+3][r-3]==p) return p; + } + return 0; +} + +int evaluateBoardB(const int8_t b[][ROWS], int8_t aiP, int8_t huP) +{ + int score = 0; + for (int r = 0; r < ROWS; r++) { + if (b[3][r] == aiP) score += 3; + else if (b[3][r] == huP) score -= 3; + } + auto sw = [&](int c, int r, int dc, int dr) -> int { + int ai = 0, hu = 0; + for (int i = 0; i < 4; i++) { + int8_t v = b[c + i*dc][r + i*dr]; + if (v == aiP) ai++; else if (v == huP) hu++; + } + if (ai && hu) return 0; + if (ai == 3) return 50; if (ai == 2) return 5; + if (hu == 3) return -50; if (hu == 2) return -5; + return 0; + }; + for (int r = 0; r < 6; r++) for (int c = 0; c < 4; c++) score += sw(c,r,1,0); + for (int r = 0; r < 3; r++) for (int c = 0; c < 7; c++) score += sw(c,r,0,1); + for (int r = 0; r < 3; r++) for (int c = 0; c < 4; c++) score += sw(c,r,1,1); + for (int r = 3; r < 6; r++) for (int c = 0; c < 4; c++) score += sw(c,r,1,-1); + return score; +} + +int minimaxB(int8_t b[][ROWS], int depth, int alpha, int beta, + bool isMax, int8_t aiP, int8_t huP, volatile bool &abortFlag) +{ + if (abortFlag) return 0; + + int8_t win = scanBoardB(b); + if (win == aiP) return 1000 + depth; + if (win == huP) return -1000 - depth; + if (depth == 0 || isBoardFullB(b)) return evaluateBoardB(b, aiP, huP); int best = isMax ? -10000 : 10000; - for (int ci = 0; ci < COLS; ci++) - { + for (int ci = 0; ci < COLS; ci++) { int c = colOrder[ci]; - if (abortAi) - return 0; - int r = getFirstEmptyRow(c); - if (r == -1) - continue; - board[c][r] = isMax ? aiP : huP; - int score = minimax(depth - 1, alpha, beta, !isMax, aiP, huP); - board[c][r] = 0; - if (isMax) - { - if (score > best) - best = score; - if (best > alpha) - alpha = best; + if (abortFlag) return 0; + int r = getFirstEmptyRowB(b, c); + if (r == -1) continue; + b[c][r] = isMax ? aiP : huP; + int score = minimaxB(b, depth - 1, alpha, beta, !isMax, aiP, huP, abortFlag); + b[c][r] = 0; + if (isMax) { + if (score > best) best = score; + if (best > alpha) alpha = best; + } else { + if (score < best) best = score; + if (best < beta) beta = best; } - else - { - if (score < best) - best = score; - if (best < beta) - beta = best; - } - if (beta <= alpha) - break; + if (beta <= alpha) break; } return best; } +int computeAiMoveB(int8_t b[][ROWS], int8_t aiP, uint8_t ply, + bool useBlunder, volatile bool &abortFlag) +{ + int8_t huP = (aiP == 1) ? 2 : 1; + int bestScore = -30000, bestCol = 3; + bool found = false; + + // Phase 1a: instant win + for (int c = 0; c < COLS && !found; c++) { + int r = getFirstEmptyRowB(b, c); + if (r == -1) continue; + b[c][r] = aiP; + if (scanBoardB(b) == aiP) { b[c][r] = 0; bestCol = c; found = true; } + else b[c][r] = 0; + } + + // Phase 1b: block opponent + for (int c = 0; c < COLS && !found; c++) { + int r = getFirstEmptyRowB(b, c); + if (r == -1) continue; + b[c][r] = huP; + if (scanBoardB(b) == huP) { b[c][r] = 0; bestCol = c; found = true; } + else b[c][r] = 0; + } + + // Phase 2: blunder + if (!found && useBlunder && blunderEnabled && (random(100) < blunderChance)) { + int validCols[COLS], count = 0; + for (int c = 0; c < COLS; c++) + if (getFirstEmptyRowB(b, c) != -1) validCols[count++] = c; + if (count > 0) { bestCol = validCols[random(count)]; found = true; } + } + + // Phase 3: deep minimax + if (!found) { + for (int ci = 0; ci < COLS; ci++) { + int c = colOrder[ci]; + if (abortFlag) break; + int r = getFirstEmptyRowB(b, c); + if (r == -1) continue; + b[c][r] = aiP; + int score = minimaxB(b, ply, -30000, 30000, false, aiP, huP, abortFlag); + b[c][r] = 0; + if (score > bestScore) { bestScore = score; bestCol = c; } + } + } + + return bestCol; +} + +// --- AI on global board (core 0 only — for demo mode) --- + int computeAiMove(int8_t aiP) { abortAi = false; - int8_t huP = (aiP == 1) ? 2 : 1; - int bestScore = -30000, bestCol = 3; int originalPly = currentLookAhead; - if (gameState == DEMO) - currentLookAhead = demoPly[aiP - 1]; + if (gameState == DEMO) currentLookAhead = demoPly[aiP - 1]; + int result = computeAiMoveB(board, aiP, currentLookAhead, + gameState != DEMO, abortAi); + currentLookAhead = originalPly; + return result; +} - bool found = false; +// ================================================================ +// AI job control (core 0 posts, core 1 executes) +// ================================================================ - for (int c = 0; c < COLS && !found; c++) - { +// ================================================================ +// Parallel AI: root splitting across both cores +// ================================================================ + +int computeAiMoveParallel(int8_t aiP) +{ + int8_t huP = (aiP == 1) ? 2 : 1; + + // Phase 1a: instant win (fast, core 0 only) + for (int c = 0; c < COLS; c++) { int r = getFirstEmptyRow(c); - if (r == -1) - continue; + if (r == -1) continue; board[c][r] = aiP; - if (scanBoard() == aiP) - { - board[c][r] = 0; - bestCol = c; - found = true; - } - else - board[c][r] = 0; + bool wins = (scanBoardB(board) == aiP); + board[c][r] = 0; + if (wins) return c; } - for (int c = 0; c < COLS && !found; c++) - { + // Phase 1b: block opponent (fast, core 0 only) + for (int c = 0; c < COLS; c++) { int r = getFirstEmptyRow(c); - if (r == -1) - continue; + if (r == -1) continue; board[c][r] = huP; - if (scanBoard() == huP) - { - board[c][r] = 0; - bestCol = c; - found = true; - } - else - board[c][r] = 0; + bool wins = (scanBoardB(board) == huP); + board[c][r] = 0; + if (wins) return c; } - if (!found && blunderEnabled && gameState != DEMO && - (random(100) < blunderChance)) - { + // Phase 2: blunder + if (blunderEnabled && (random(100) < blunderChance)) { int validCols[COLS], count = 0; for (int c = 0; c < COLS; c++) - if (getFirstEmptyRow(c) != -1) - validCols[count++] = c; - bestCol = validCols[random(count)]; - found = true; + if (getFirstEmptyRow(c) != -1) validCols[count++] = c; + if (count > 0) return validCols[random(count)]; } - if (!found) - { - for (int ci = 0; ci < COLS; ci++) - { + // Phase 3: parallel minimax — split root columns across both cores + // Core 1 gets odd-indexed columns from colOrder: [2, 1, 0] (indices 1,3,5) + aiJob.abort = false; + aiJob.done = false; + memcpy(aiJob.boardCopy, board, sizeof(board)); + aiJob.aiPlayer = aiP; + aiJob.ply = currentLookAhead; + aiJob.searchCount = 0; + for (int ci = 1; ci < COLS; ci += 2) + aiJob.searchCols[aiJob.searchCount++] = colOrder[ci]; + aiJob.bestScore = -30000; + aiJob.bestCol = 3; + aiJob.type = JOB_SEARCH; // Core 1 starts immediately + + // Core 0 searches even-indexed columns: [3, 4, 5, 6] (indices 0,2,4,6) + int8_t boardCopy0[COLS][ROWS]; + memcpy(boardCopy0, board, sizeof(board)); + int bestScore0 = -30000, bestCol0 = 3; + volatile bool abort0 = false; + + for (int ci = 0; ci < COLS; ci += 2) { + int c = colOrder[ci]; + if (aiJob.abort) break; // External abort (e.g. returning to menu) + int r = getFirstEmptyRowB(boardCopy0, c); + if (r == -1) continue; + boardCopy0[c][r] = aiP; + int score = minimaxB(boardCopy0, currentLookAhead, -30000, 30000, + false, aiP, huP, abort0); + boardCopy0[c][r] = 0; + if (score > bestScore0) { bestScore0 = score; bestCol0 = c; } + } + + // Wait for core 1 to finish its columns + while (!aiJob.done && aiJob.type != JOB_NONE) delay(1); + + // Merge: pick the best result from either core + if ((int)aiJob.bestScore > bestScore0) + return aiJob.bestCol; + return bestCol0; +} + +void startPonder(int8_t aiP) +{ + memcpy(aiJob.boardCopy, board, sizeof(board)); + aiJob.aiPlayer = aiP; + aiJob.ply = currentLookAhead; + aiJob.done = false; + aiJob.abort = false; + memset((void *)aiJob.ponderValid, 0, sizeof(aiJob.ponderValid)); + memset((void *)aiJob.ponderResults, 3, sizeof(aiJob.ponderResults)); + aiJob.type = JOB_PONDER; +} + +void executeAiJob() +{ + if (aiJob.type == JOB_SEARCH) { + int8_t aiP = aiJob.aiPlayer; + int8_t huP = (aiP == 1) ? 2 : 1; + int bestScore = -30000, bestCol = 3; + + for (int i = 0; i < aiJob.searchCount; i++) { + int c = aiJob.searchCols[i]; + if (aiJob.abort) break; + int r = getFirstEmptyRowB(aiJob.boardCopy, c); + if (r == -1) continue; + aiJob.boardCopy[c][r] = aiP; + int score = minimaxB(aiJob.boardCopy, aiJob.ply, -30000, 30000, + false, aiP, huP, aiJob.abort); + aiJob.boardCopy[c][r] = 0; + if (score > bestScore) { bestScore = score; bestCol = c; } + } + + aiJob.bestScore = bestScore; + aiJob.bestCol = bestCol; + aiJob.done = true; + aiJob.type = JOB_NONE; + + } else if (aiJob.type == JOB_PONDER) { + int8_t aiP = aiJob.aiPlayer; + int8_t huP = (aiP == 1) ? 2 : 1; + + for (int ci = 0; ci < COLS; ci++) { int c = colOrder[ci]; - if (abortAi) - break; - int r = getFirstEmptyRow(c); - if (r == -1) + if (aiJob.abort) break; + + int8_t localBoard[COLS][ROWS]; + memcpy(localBoard, aiJob.boardCopy, sizeof(localBoard)); + + int r = getFirstEmptyRowB(localBoard, c); + if (r == -1) continue; + + localBoard[c][r] = huP; + if (scanBoardB(localBoard) != 0 || isBoardFullB(localBoard)) { + aiJob.ponderValid[c] = true; + aiJob.ponderResults[c] = 3; continue; - board[c][r] = aiP; - int score = minimax(currentLookAhead, -30000, 30000, false, aiP, huP); - board[c][r] = 0; - if (score > bestScore) - { - bestScore = score; - bestCol = c; + } + + int8_t best = computeAiMoveB(localBoard, aiP, aiJob.ply, + true, aiJob.abort); + if (!aiJob.abort) { + aiJob.ponderResults[c] = best; + aiJob.ponderValid[c] = true; } } + aiJob.done = true; + aiJob.type = JOB_NONE; } - - currentLookAhead = originalPly; - return bestCol; } diff --git a/src/game.h b/src/game.h index 843db7a..3c4357b 100644 --- a/src/game.h +++ b/src/game.h @@ -30,9 +30,37 @@ struct GameEntry String moves; }; +// --- AI job (core 1 communication) --- + +enum AiJobType : uint8_t +{ + JOB_NONE, + JOB_SEARCH, + JOB_PONDER +}; + +struct AiJob +{ + volatile AiJobType type; + volatile bool done; + volatile bool abort; + int8_t boardCopy[COLS][ROWS]; + int8_t aiPlayer; + uint8_t ply; + // JOB_SEARCH: core 1 searches these specific root columns + int8_t searchCols[COLS]; + int8_t searchCount; + volatile int bestScore; + volatile int8_t bestCol; + // JOB_PONDER: precomputed responses + volatile int8_t ponderResults[COLS]; + volatile bool ponderValid[COLS]; +}; + // --- Shared globals (defined in game.cpp) --- extern Adafruit_ST7789 tft; +extern bool core1_separate_stack; extern int8_t board[COLS][ROWS]; extern WinPos winPos[4]; @@ -63,6 +91,7 @@ extern uint32_t lastFlash; extern bool needRedraw; extern const int8_t colOrder[]; +extern AiJob aiJob; // --- Board helpers --- @@ -82,7 +111,23 @@ int8_t scanBoard(); int evaluateBoard(int8_t aiP, int8_t huP); bool checkGameEnd(); -// --- AI --- +// --- Pure board functions (safe for core 1 — no globals) --- + +int getFirstEmptyRowB(const int8_t b[][ROWS], int col); +bool isBoardFullB(const int8_t b[][ROWS]); +int8_t scanBoardB(const int8_t b[][ROWS]); +int evaluateBoardB(const int8_t b[][ROWS], int8_t aiP, int8_t huP); +int minimaxB(int8_t b[][ROWS], int depth, int alpha, int beta, + bool isMax, int8_t aiP, int8_t huP, volatile bool &abortFlag); +int computeAiMoveB(int8_t b[][ROWS], int8_t aiP, uint8_t ply, + bool useBlunder, volatile bool &abortFlag); + +// --- AI (core 0 — uses global board, for demo mode) --- -int minimax(int depth, int alpha, int beta, bool isMax, int8_t aiP, int8_t huP); int computeAiMove(int8_t aiP); + +// --- Parallel AI (root splitting across both cores) --- + +int computeAiMoveParallel(int8_t aiP); +void startPonder(int8_t aiP); +void executeAiJob(); diff --git a/src/main.cpp b/src/main.cpp index 8e7ff0e..3098ae1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,9 @@ /* ================================================================ * Connect Four — Pico 2W + PicoResTouch-LCD-2.8 - * Main entry: setup, loop, state dispatch + * Main entry: setup, loop, state dispatch, dual-core AI + * + * Core 0: Display, touch input, game state, animations + * Core 1: AI computation (minimax) and pondering * ================================================================ */ #include @@ -12,10 +15,29 @@ #include "storage.h" #include "display.h" -// --- State handlers --- +// ================================================================ +// Core 1: AI worker +// ================================================================ + +void setup1() +{ + // Core 1 has no SPI, no display, no touch — just CPU for minimax +} + +void loop1() +{ + if (aiJob.type != JOB_NONE) + executeAiJob(); +} + +// ================================================================ +// State handlers (core 0) +// ================================================================ void startGame(int mode) { + aiJob.abort = true; + aiJob.type = JOB_NONE; resetBoard(); gameMenuMode = mode; gameLevel = currentLookAhead; @@ -29,6 +51,8 @@ void startGame(int mode) void returnToMenu() { + aiJob.abort = true; + while (aiJob.type != JOB_NONE) delay(1); abortAi = true; resetBoard(); gameState = MENU; @@ -39,6 +63,8 @@ void returnToMenu() void startDemo() { + aiJob.abort = true; + while (aiJob.type != JOB_NONE) delay(1); resetBoard(); randomizeDemoPlies(); gameState = DEMO; @@ -51,14 +77,31 @@ void startDemo() void handleAiTurn() { int8_t aiP = (gameMenuMode == 0) ? 2 : 1; - int bestCol = computeAiMove(aiP); - if (abortAi) + + // Check if ponder already computed the response + int lastHumanCol = -1; + if (currentMoves.length() > 0) + lastHumanCol = currentMoves[currentMoves.length() - 1] - '0'; + + int bestCol; + + if (lastHumanCol >= 0 && lastHumanCol < COLS && + aiJob.type == JOB_NONE && aiJob.ponderValid[lastHumanCol]) { - returnToMenu(); - return; + // Ponder hit! Use pre-computed result instantly + bestCol = aiJob.ponderResults[lastHumanCol]; + delay(100); + } + else + { + // Ponder miss — parallel search on BOTH cores + aiJob.abort = true; + while (aiJob.type != JOB_NONE) delay(1); + drawGameStatus(); + bestCol = computeAiMoveParallel(aiP); + delay(100); } - delay(150); animateDrop(bestCol, aiP); activeCol = bestCol; @@ -67,6 +110,9 @@ void handleAiTurn() gameState = PLAYING; currentPlayer = (aiP == 1) ? 2 : 1; drawGameStatus(); + + // Start pondering on core 1: speculate on all human responses + startPonder(aiP); } else { @@ -82,6 +128,7 @@ void handleDemoStep() return; lastDemoMove = millis(); + // Demo runs on core 0 (blocking) to avoid SPI conflicts int bestCol = computeAiMove(currentPlayer); if (abortAi) { @@ -117,7 +164,9 @@ void handleFlash() startDemo(); } -// --- Setup --- +// ================================================================ +// Setup (core 0) +// ================================================================ void setup() { @@ -167,7 +216,9 @@ void setup() needRedraw = true; } -// --- Loop --- +// ================================================================ +// Loop (core 0) +// ================================================================ void loop() { @@ -200,6 +251,10 @@ void loop() int row = getFirstEmptyRow(col); if (row >= 0) { + // Cancel any running ponder before modifying board + aiJob.abort = true; + while (aiJob.type != JOB_NONE) delay(1); + animateDrop(col, currentPlayer); if (!checkGameEnd()) {