457 lines
14 KiB
C++
457 lines
14 KiB
C++
#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;
|
|
}
|
|
}
|