#include "game.h" #include "touch.h" #include "storage.h" // --- 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]; int winCount = 0; State gameState = MENU; int8_t menuMode = 0; int8_t currentPlayer = 1; int8_t winnerPlayer = 0; int8_t activeCol = 3; int8_t gameMenuMode = 0; uint8_t demoPly[2] = {4, 4}; bool abortAi = false; String currentMoves; uint8_t gameLevel = 0; uint8_t currentLookAhead = DEFAULT_LOOK_AHEAD; bool blunderEnabled = BLUNDER_ENABLED; uint8_t blunderChance = BLUNDER_CHANCE; GameEntry gameLog[MAX_GAME_LOG]; uint8_t gameLogCount = 0; uint32_t lastActivityTime = 0; uint32_t demoResetTimer = 0; uint32_t lastDemoMove = 0; bool flashToggle = true; uint32_t lastFlash = 0; bool needRedraw = true; const int8_t colOrder[] = {3, 2, 4, 1, 5, 0, 6}; AiJob aiJob = {}; // --- 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; } void resetBoard() { memset(board, 0, sizeof(board)); winnerPlayer = 0; winCount = 0; } int getFirstEmptyRow(int col) { for (int r = 0; r < ROWS; r++) if (board[col][r] == 0) return r; return -1; } bool isBoardFull() { for (int c = 0; c < COLS; c++) if (board[c][ROWS - 1] == 0) return false; return true; } bool isWinPos(int c, int r) { for (int i = 0; i < winCount; i++) if (winPos[i].c == c && winPos[i].r == r) return true; return false; } 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; } } // --- 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++) { 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; } } for (int r = 0; r <= ROWS - 4; r++) 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; } } for (int r = 0; r <= ROWS - 4; r++) 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; } } for (int r = 3; r < ROWS; r++) 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; } } return 0; } int evaluateBoard(int8_t aiP, int8_t huP) { return evaluateBoardB(board, aiP, huP); } bool checkGameEnd() { winnerPlayer = scanBoard(); bool won = winnerPlayer != 0; bool draw = !won && isBoardFull(); if (!won && !draw) return false; if (gameState != DEMO) logGame(won ? winnerPlayer : 0); gameState = won ? FINISHED_WIN : FINISHED_DRAW; demoResetTimer = millis(); lastActivityTime = millis(); flashToggle = false; lastFlash = millis(); return true; } // ================================================================ // Pure board functions (no globals — safe for core 1) // ================================================================ int getFirstEmptyRowB(const int8_t b[][ROWS], int col) { for (int r = 0; r < ROWS; r++) if (b[col][r] == 0) return r; return -1; } 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++) { int c = colOrder[ci]; 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; } 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; int originalPly = currentLookAhead; if (gameState == DEMO) currentLookAhead = demoPly[aiP - 1]; int result = computeAiMoveB(board, aiP, currentLookAhead, gameState != DEMO, abortAi); currentLookAhead = originalPly; return result; } // ================================================================ // AI job control (core 0 posts, core 1 executes) // ================================================================ // ================================================================ // 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; board[c][r] = aiP; bool wins = (scanBoardB(board) == aiP); board[c][r] = 0; if (wins) return c; } // Phase 1b: block opponent (fast, core 0 only) for (int c = 0; c < COLS; c++) { int r = getFirstEmptyRow(c); if (r == -1) continue; board[c][r] = huP; bool wins = (scanBoardB(board) == huP); board[c][r] = 0; if (wins) return c; } // 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; if (count > 0) return validCols[random(count)]; } // 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 (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; } 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; } }