[update] Multi-core and pondering
This commit is contained in:
+301
-194
@@ -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
@@ -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
@@ -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())
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user