[update] Multi-core and pondering

This commit is contained in:
2026-03-27 16:19:54 +01:00
parent 42d82c22a8
commit f5f5e46b32
3 changed files with 412 additions and 205 deletions
+301 -194
View File
@@ -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;
}
+47 -2
View File
@@ -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();
+64 -9
View File
@@ -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 <Arduino.h>
@@ -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())
{