350 lines
8.4 KiB
C++
350 lines
8.4 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);
|
|
|
|
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};
|
|
|
|
// --- Board helpers ---
|
|
|
|
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 ---
|
|
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// --- AI ---
|
|
|
|
int minimax(int depth, int alpha, int beta, bool isMax,
|
|
int8_t aiP, int8_t huP)
|
|
{
|
|
if (gameState == DEMO && depth >= currentLookAhead - 1)
|
|
{
|
|
int16_t rx, ry;
|
|
if (readRawTouch(rx, ry))
|
|
{
|
|
abortAi = true;
|
|
return 0;
|
|
}
|
|
}
|
|
yield();
|
|
if (abortAi)
|
|
return 0;
|
|
|
|
int8_t win = scanBoard();
|
|
if (win == aiP)
|
|
return 1000 + depth;
|
|
if (win == huP)
|
|
return -1000 - depth;
|
|
if (depth == 0 || isBoardFull())
|
|
return evaluateBoard(aiP, huP);
|
|
|
|
int best = isMax ? -10000 : 10000;
|
|
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;
|
|
}
|
|
else
|
|
{
|
|
if (score < best)
|
|
best = score;
|
|
if (best < beta)
|
|
beta = best;
|
|
}
|
|
if (beta <= alpha)
|
|
break;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
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];
|
|
|
|
bool found = false;
|
|
|
|
for (int c = 0; c < COLS && !found; c++)
|
|
{
|
|
int r = getFirstEmptyRow(c);
|
|
if (r == -1)
|
|
continue;
|
|
board[c][r] = aiP;
|
|
if (scanBoard() == aiP)
|
|
{
|
|
board[c][r] = 0;
|
|
bestCol = c;
|
|
found = true;
|
|
}
|
|
else
|
|
board[c][r] = 0;
|
|
}
|
|
|
|
for (int c = 0; c < COLS && !found; c++)
|
|
{
|
|
int r = getFirstEmptyRow(c);
|
|
if (r == -1)
|
|
continue;
|
|
board[c][r] = huP;
|
|
if (scanBoard() == huP)
|
|
{
|
|
board[c][r] = 0;
|
|
bestCol = c;
|
|
found = true;
|
|
}
|
|
else
|
|
board[c][r] = 0;
|
|
}
|
|
|
|
if (!found && blunderEnabled && gameState != DEMO &&
|
|
(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 (!found)
|
|
{
|
|
for (int ci = 0; ci < COLS; ci++)
|
|
{
|
|
int c = colOrder[ci];
|
|
if (abortAi)
|
|
break;
|
|
int r = getFirstEmptyRow(c);
|
|
if (r == -1)
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
currentLookAhead = originalPly;
|
|
return bestCol;
|
|
}
|