Othello

Source: src/othello.c · 475 lines · 22 functions.

Open the existing readable game overview →

Screen captures

Views of this part running on the Mini Arcade.

Othello game board on the Mini Arcade
Othello showing the board and available play state.
Othello at a later board state on the Mini Arcade
A second Othello board position.

Module entry points

These functions are exposed to the rest of the project. Open one to follow its decisions and the helpers it calls.

How to read these diagrams

Each function has its own expandable diagram using the actual expressions and statements from this file. Decisions split into True and False; loop bodies are indented. Returns end the function immediately; break and continue follow their C meanings. Arrows between statements show their order when execution reaches them.

These are structural source diagrams before preprocessing, not runtime traces. Conditional compilation and macros remain source expressions. Ternary choices remain inside their statement. Calls are linked separately; callback execution and task scheduling are not inferred.

Functions in this file

opponent() · source line 54
static uint8_t opponent(uint8_t piece)
  1. Exit this functionreturn piece == BLACK_PIECE ? WHITE_PIECE : BLACK_PIECE;
Read original function source
static uint8_t opponent(uint8_t piece)
{
    return piece == BLACK_PIECE
        ? WHITE_PIECE
        : BLACK_PIECE;
}
insideBoard() · source line 61
static bool insideBoard(int row, int column)
  1. Exit this functionreturn row >= 0 && row < 8 && column >= 0 && column < 8;
Read original function source
static bool insideBoard(int row, int column)
{
    return row >= 0 &&
        row < 8 &&
        column >= 0 &&
        column < 8;
}
countPieces() · source line 69
static uint8_t countPieces(uint8_t piece)
  1. Executeuint8_t total = 0;
  2. FOR loopuint8_t i = 0; i < 64; i++

    Repeat body while the loop condition permits; then continue below.

      1. Decisionboard[i] == piece
        True
        1. Executetotal++;
        False

        Continue after this decision.

  3. Exit this functionreturn total;
Read original function source
static uint8_t countPieces(uint8_t piece)
{
    uint8_t total = 0;

    for (uint8_t i = 0; i < 64; i++)
    {
        if (board[i] == piece)
            total++;
    }

    return total;
}
countFlips() · source line 82
static uint8_t countFlips(
    uint8_t cell,
    uint8_t piece,
    int8_t rowDirection,
    int8_t columnDirection
)

Calls into this documentation:

  1. Decisionboard[cell] != EMPTY
    True
    1. Exit this functionreturn 0;
    False

    Continue after this decision.

  2. Executeconst int startRow = cell / 8;
  3. Executeconst int startColumn = cell % 8;
  4. Executeint row = startRow + rowDirection;
  5. Executeint column = startColumn + columnDirection;
  6. Executeuint8_t flips = 0;
  7. Executeconst uint8_t otherPiece = opponent(piece);
  8. WHILE loopinsideBoard(row, column) && board[row * 8 + column] == otherPiece

    Repeat body while the loop condition permits; then continue below.

      1. Executeflips++;
      2. Executerow += rowDirection;
      3. Executecolumn += columnDirection;
  9. Decisionflips == 0 || !insideBoard(row, column)
    True
    1. Exit this functionreturn 0;
    False

    Continue after this decision.

  10. Decisionboard[row * 8 + column] != piece
    True
    1. Exit this functionreturn 0;
    False

    Continue after this decision.

  11. Exit this functionreturn flips;
Read original function source
static uint8_t countFlips(
    uint8_t cell,
    uint8_t piece,
    int8_t rowDirection,
    int8_t columnDirection
)
{
    if (board[cell] != EMPTY)
        return 0;

    const int startRow = cell / 8;
    const int startColumn = cell % 8;

    int row = startRow + rowDirection;
    int column = startColumn + columnDirection;

    uint8_t flips = 0;
    const uint8_t otherPiece = opponent(piece);

    while (insideBoard(row, column) &&
           board[row * 8 + column] == otherPiece)
    {
        flips++;
        row += rowDirection;
        column += columnDirection;
    }

    if (flips == 0 || !insideBoard(row, column))
        return 0;

    if (board[row * 8 + column] != piece)
        return 0;

    return flips;
}
legalMove() · source line 118
static bool legalMove(uint8_t cell, uint8_t piece)

Calls into this documentation:

  1. Decisioncell >= 64 || board[cell] != EMPTY
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. FOR loopuint8_t d = 0; d < 8; ++d

    Repeat body while the loop condition permits; then continue below.

      1. DecisioncountFlips( cell, piece, DIRECTIONS[d][0], DIRECTIONS[d][1] ) > 0
        True
          1. Exit this functionreturn true;
        False

        Continue after this decision.

  3. Exit this functionreturn false;
Read original function source
static bool legalMove(uint8_t cell, uint8_t piece)
{
    if (cell >= 64 || board[cell] != EMPTY)
        return false;

    for (uint8_t d = 0; d < 8; ++d)
    {
        if (countFlips(
                cell,
                piece,
                DIRECTIONS[d][0],
                DIRECTIONS[d][1]
            ) > 0)
        {
            return true;
        }
    }

    return false;
}
hasLegalMove() · source line 139
static bool hasLegalMove(uint8_t piece)

Calls into this documentation:

  1. FOR loopuint8_t i = 0; i < 64; i++

    Repeat body while the loop condition permits; then continue below.

      1. DecisionlegalMove(i, piece)
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool hasLegalMove(uint8_t piece)
{
    for (uint8_t i = 0; i < 64; i++)
    {
        if (legalMove(i, piece))
            return true;
    }

    return false;
}
makeMove() · source line 150
static void makeMove(uint8_t cell, uint8_t piece)

Calls into this documentation:

  1. Executeconst int startRow = cell / 8;
  2. Executeconst int startColumn = cell % 8;
  3. FOR loopuint8_t d = 0; d < 8; ++d

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst uint8_t flipCount = countFlips( cell, piece, DIRECTIONS[d][0], DIRECTIONS[d][1] );
      2. Executeint row = startRow + DIRECTIONS[d][0];
      3. Executeint column = startColumn + DIRECTIONS[d][1];
      4. FOR loopuint8_t i = 0; i < flipCount; i++

        Repeat body while the loop condition permits; then continue below.

          1. Executeboard[row * 8 + column] = piece;
          2. Executerow += DIRECTIONS[d][0];
          3. Executecolumn += DIRECTIONS[d][1];
  4. Executeboard[cell] = piece;
Read original function source
static void makeMove(uint8_t cell, uint8_t piece)
{
    const int startRow = cell / 8;
    const int startColumn = cell % 8;

    for (uint8_t d = 0; d < 8; ++d)
    {
        const uint8_t flipCount = countFlips(
            cell,
            piece,
            DIRECTIONS[d][0],
            DIRECTIONS[d][1]
        );

        int row = startRow + DIRECTIONS[d][0];
        int column = startColumn + DIRECTIONS[d][1];

        for (uint8_t i = 0; i < flipCount; i++)
        {
            board[row * 8 + column] = piece;
            row += DIRECTIONS[d][0];
            column += DIRECTIONS[d][1];
        }
    }
    board[cell] = piece;
}
resetBoard() · source line 178
static void resetBoard(void)
  1. FOR loopuint8_t i = 0; i < 64; i++

    Repeat body while the loop condition permits; then continue below.

    1. Executeboard[i] = EMPTY;
  2. Executeboard[3 * 8 + 3] = WHITE_PIECE;
  3. Executeboard[3 * 8 + 4] = BLACK_PIECE;
  4. Executeboard[4 * 8 + 3] = BLACK_PIECE;
  5. Executeboard[4 * 8 + 4] = WHITE_PIECE;
  6. ExecutecurrentPlayer = BLACK_PIECE;
  7. ExecuteselectedMove = 0;
Read original function source
static void resetBoard(void)
{
    for (uint8_t i = 0; i < 64; i++)
        board[i] = EMPTY;

    board[3 * 8 + 3] = WHITE_PIECE;
    board[3 * 8 + 4] = BLACK_PIECE;
    board[4 * 8 + 3] = BLACK_PIECE;
    board[4 * 8 + 4] = WHITE_PIECE;

    currentPlayer = BLACK_PIECE;
    selectedMove = 0;
}
refreshLegalMoves() · source line 192
static void refreshLegalMoves(void)

Calls into this documentation:

  1. ExecutelegalMoveCount = 0;
  2. FOR loopuint8_t cell = 0; cell < 64; ++cell

    Repeat body while the loop condition permits; then continue below.

      1. DecisionlegalMove(cell, currentPlayer)
        True
        1. ExecutelegalMoves[legalMoveCount++] = cell;
        False

        Continue after this decision.

  3. ExecuteselectedMove = 0;
  4. ExecutestickCentred = false;
  5. ExecuteheldDirection = 0;
Read original function source
static void refreshLegalMoves(void)
{
    legalMoveCount = 0;
    for (uint8_t cell = 0; cell < 64; ++cell) {
        if (legalMove(cell, currentPlayer)) legalMoves[legalMoveCount++] = cell;
    }
    selectedMove = 0;
    stickCentred = false;
    heldDirection = 0;
}
incrementScore() · source line 203
static void incrementScore(uint32_t *value)
  1. Decision*value < UINT32_MAX
    True
    1. Execute++*value;
    False

    Continue after this decision.

Read original function source
static void incrementScore(uint32_t *value)
{
    if (*value < UINT32_MAX) ++*value;
}
finishGame() · source line 208
static void finishGame(uint8_t winner)

Calls into this documentation:

  1. ExecutegameState = STATE_FINISHED;
  2. ExecutestateStarted = clockNow;
  3. ExecuteboardMessage = winner == EMPTY ? "DRAW" : winner == BLACK_PIECE ? "BLACK WINS" : "WHITE WINS";
  4. DecisionautoMode
    True
      1. Decisionwinner == goldEarningPiece && !addAIGold(OTHELLO_WIN_GOLD)
        True
        1. ExecutegoldSaveFailed = true;
        False

        Continue after this decision.

    False
      1. Decisionwinner == humanPiece
        True
          1. ExecuteincrementScore(&scores.wins);
          2. ExecuteincrementScore(&scores.streak);
          3. Decision!addUserGoldWon(OTHELLO_WIN_GOLD)
            True
            1. ExecutegoldSaveFailed = true;
            False

            Continue after this decision.

          4. ExecuteboardMessage = "YOU WIN";
        False
          1. Executescores.streak = 0;
          2. Decisionwinner == EMPTY
            True
            1. ExecuteincrementScore(&scores.draws);
            False
              1. ExecuteincrementScore(&scores.losses);
              2. ExecuteboardMessage = "CPU WINS";
      2. DecisionscoreStatus == SCORES_OK || scoreStatus == SCORES_NOT_FOUND
        True
          1. ExecutescoreSaveFailed = !game_scores_save(GAME_OTHELLO, &scores);
          2. Decision!scoreSaveFailed
            True
            1. ExecutescoreStatus = SCORES_OK;
            False

            Continue after this decision.

        False
        1. ExecutescoreSaveFailed = true;
  5. ExecuteESP_LOGI("othello", "%s; score save %s; Gold %s", boardMessage, scoreSaveFailed ? "ERROR" : autoMode ? "NOT REQUIRED" : "OK", goldSaveFailed ? "ERROR" : "OK");
Read original function source
static void finishGame(uint8_t winner)
{
    gameState = STATE_FINISHED;
    stateStarted = clockNow;
    boardMessage = winner == EMPTY ? "DRAW" : winner == BLACK_PIECE ? "BLACK WINS" : "WHITE WINS";
    if (autoMode) {
        if (winner == goldEarningPiece && !addAIGold(OTHELLO_WIN_GOLD)) goldSaveFailed = true;
    } else {
        if (winner == humanPiece) {
            incrementScore(&scores.wins);
            incrementScore(&scores.streak);
            if (!addUserGoldWon(OTHELLO_WIN_GOLD)) goldSaveFailed = true;
            boardMessage = "YOU WIN";
        } else {
            scores.streak = 0;
            if (winner == EMPTY) incrementScore(&scores.draws);
            else { incrementScore(&scores.losses); boardMessage = "CPU WINS"; }
        }
        if (scoreStatus == SCORES_OK || scoreStatus == SCORES_NOT_FOUND) {
            scoreSaveFailed = !game_scores_save(GAME_OTHELLO, &scores);
            if (!scoreSaveFailed) scoreStatus = SCORES_OK;
        } else scoreSaveFailed = true;
    }
    ESP_LOGI("othello", "%s; score save %s; Gold %s", boardMessage,
             scoreSaveFailed ? "ERROR" : autoMode ? "NOT REQUIRED" : "OK",
             goldSaveFailed ? "ERROR" : "OK");
}
finishTurn() · source line 236
static void finishTurn(void)

Calls into this documentation:

  1. Executeconst uint8_t nextPlayer = opponent(currentPlayer);
  2. Executeconst bool passed = !hasLegalMove(nextPlayer);
  3. Decision!passed
    True
    1. ExecutecurrentPlayer = nextPlayer;
    False
    1. Decision!hasLegalMove(currentPlayer)
      True
        1. Executeconst uint8_t black = countPieces(BLACK_PIECE);
        2. Executeconst uint8_t white = countPieces(WHITE_PIECE);
        3. ExecutefinishGame(black > white ? BLACK_PIECE : white > black ? WHITE_PIECE : EMPTY);
        4. Exit this functionreturn;
      False

      Continue after this decision.

  4. ExecuterefreshLegalMoves();
  5. ExecuteboardMessage = passed ? (autoMode ? "TURN PASSED" : currentPlayer == humanPiece ? "CPU PASS" : "YOU PASS") : autoMode || currentPlayer != humanPiece ? "CPU TURN" : "YOUR TURN";
  6. ExecutecomputerMoveDue = clockNow + (autoMode ? aiSpeedDelay(aiSpeedIndex) : 800U);
Read original function source
static void finishTurn(void)
{
    const uint8_t nextPlayer = opponent(currentPlayer);
    const bool passed = !hasLegalMove(nextPlayer);
    if (!passed) currentPlayer = nextPlayer;
    else if (!hasLegalMove(currentPlayer)) {
        const uint8_t black = countPieces(BLACK_PIECE);
        const uint8_t white = countPieces(WHITE_PIECE);
        finishGame(black > white ? BLACK_PIECE : white > black ? WHITE_PIECE : EMPTY);
        return;
    }
    refreshLegalMoves();
    boardMessage = passed ? (autoMode ? "TURN PASSED" : currentPlayer == humanPiece ? "CPU PASS" : "YOU PASS") :
                   autoMode || currentPlayer != humanPiece ? "CPU TURN" : "YOUR TURN";
    computerMoveDue = clockNow + (autoMode ? aiSpeedDelay(aiSpeedIndex) : 800U);
}
performMove() · source line 253
static void performMove(uint8_t index)

Calls into this documentation:

  1. Decisionindex >= legalMoveCount || !legalMove(legalMoves[index], currentPlayer)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. ExecutemakeMove(legalMoves[index], currentPlayer);
  3. ExecuteaudioPlay(AUDIO_SFX_BOARD_CAPTURE);
  4. ExecutefinishTurn();
Read original function source
static void performMove(uint8_t index)
{
    if (index >= legalMoveCount || !legalMove(legalMoves[index], currentPlayer)) return;
    makeMove(legalMoves[index], currentPlayer);
    audioPlay(AUDIO_SFX_BOARD_CAPTURE);
    finishTurn();
}
chooseComputerMove() · source line 261
static uint8_t chooseComputerMove(void)

Calls into this documentation:

  1. Executeint moveScores[64];
  2. Executeint bestScore = -10000;
  3. FOR loopuint8_t i = 0; i < legalMoveCount; ++i

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst uint8_t cell = legalMoves[i];
      2. Executeint flips = 0;
      3. FOR loopuint8_t d = 0; d < 8; ++d

        Repeat body while the loop condition permits; then continue below.

        1. Executeflips += countFlips(cell, currentPlayer, DIRECTIONS[d][0], DIRECTIONS[d][1]);
      4. ExecutemoveScores[i] = POSITION_WEIGHTS[cell] + flips * 3;
      5. DecisionmoveScores[i] > bestScore
        True
        1. ExecutebestScore = moveScores[i];
        False

        Continue after this decision.

  4. Executeuint8_t candidates[64], count = 0;
  5. FOR loopuint8_t i = 0; i < legalMoveCount; ++i

    Repeat body while the loop condition permits; then continue below.

      1. DecisionmoveScores[i] >= bestScore - 8
        True
        1. Executecandidates[count++] = i;
        False

        Continue after this decision.

  6. Exit this functionreturn count > 0 ? candidates[esp_random() % count] : 0;
Read original function source
static uint8_t chooseComputerMove(void)
{
    int moveScores[64];
    int bestScore = -10000;
    for (uint8_t i = 0; i < legalMoveCount; ++i) {
        const uint8_t cell = legalMoves[i];
        int flips = 0;
        for (uint8_t d = 0; d < 8; ++d)
            flips += countFlips(cell, currentPlayer, DIRECTIONS[d][0], DIRECTIONS[d][1]);
        moveScores[i] = POSITION_WEIGHTS[cell] + flips * 3;
        if (moveScores[i] > bestScore) bestScore = moveScores[i];
    }
    uint8_t candidates[64], count = 0;
    for (uint8_t i = 0; i < legalMoveCount; ++i) {
        if (moveScores[i] >= bestScore - 8) candidates[count++] = i;
    }
    return count > 0 ? candidates[esp_random() % count] : 0;
}
choosePiece() · source line 280
static void choosePiece(void)
  1. ExecutegameState = STATE_CHOOSE_PIECE;
  2. ExecuteautoMode = false;
  3. ExecutehumanPiece = BLACK_PIECE;
  4. ExecutestickCentred = false;
  5. ExecuteheldDirection = 0;
Read original function source
static void choosePiece(void)
{
    gameState = STATE_CHOOSE_PIECE;
    autoMode = false;
    humanPiece = BLACK_PIECE;
    stickCentred = false;
    heldDirection = 0;
}
beginGame() · source line 289
static void beginGame(void)

Calls into this documentation:

  1. ExecuteresetBoard();
  2. ExecutegameState = STATE_PLAYING;
  3. DecisionautoMode
    True
    1. ExecutegoldEarningPiece = esp_random() % 2 == 0 ? BLACK_PIECE : WHITE_PIECE;
    False

    Continue after this decision.

  4. ExecuterefreshLegalMoves();
  5. ExecuteboardMessage = autoMode || humanPiece != BLACK_PIECE ? "CPU TURN" : "YOUR TURN";
  6. ExecutecomputerMoveDue = clockNow + (autoMode ? aiSpeedDelay(aiSpeedIndex) : 800U);
Read original function source
static void beginGame(void)
{
    resetBoard();
    gameState = STATE_PLAYING;
    if (autoMode) goldEarningPiece = esp_random() % 2 == 0 ? BLACK_PIECE : WHITE_PIECE;
    refreshLegalMoves();
    boardMessage = autoMode || humanPiece != BLACK_PIECE ? "CPU TURN" : "YOUR TURN";
    computerMoveDue = clockNow + (autoMode ? aiSpeedDelay(aiSpeedIndex) : 800U);
}
startOthello() · source line 299
void startOthello(uint32_t now_ms)

Calls into this documentation:

  1. ExecuteclockNow = stateStarted = now_ms;
  2. Executerunning = true;
  3. ExecutescoreSaveFailed = goldSaveFailed = false;
  4. Executescores = (game_scores_t){0};
  5. ExecutescoreStatus = game_scores_load(GAME_OTHELLO, &scores);
  6. ExecuteaiSpeedIndex = AI_SPEED_DEFAULT_INDEX;
  7. ExecuteresetBoard();
  8. ExecutechoosePiece();
  9. ExecutegameState = STATE_SHOW_SCORES;
  10. ExecuteESP_LOGI("othello", "START: %s", game_scores_status_text(scoreStatus));
Read original function source
void startOthello(uint32_t now_ms)
{
    clockNow = stateStarted = now_ms;
    running = true;
    scoreSaveFailed = goldSaveFailed = false;
    scores = (game_scores_t){0};
    scoreStatus = game_scores_load(GAME_OTHELLO, &scores);
    aiSpeedIndex = AI_SPEED_DEFAULT_INDEX;
    resetBoard();
    choosePiece();
    gameState = STATE_SHOW_SCORES;
    ESP_LOGI("othello", "START: %s", game_scores_status_text(scoreStatus));
}
stopOthello() · source line 313
void stopOthello(void)
  1. Executerunning = false;
  2. ExecuteESP_LOGI("othello", "STOP");
Read original function source
void stopOthello(void)
{
    running = false;
    ESP_LOGI("othello", "STOP");
}
othelloAction() · source line 319
void othelloAction(bool select, bool speed, bool choose, uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. ExecuteclockNow = now_ms;
  3. Decisionchoose
    True
      1. ExecutechoosePiece();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  4. DecisiongameState == STATE_SHOW_SCORES
    True
      1. Decisionselect
        True
        1. ExecutechoosePiece();
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. DecisiongameState == STATE_CHOOSE_PIECE
    True
      1. Decisionselect
        True
        1. ExecutebeginGame();
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. DecisionautoMode
    True
      1. Decisionspeed
        True
          1. ExecutecycleAISpeed(&aiSpeedIndex);
          2. ExecutecomputerMoveDue = now_ms + aiSpeedDelay(aiSpeedIndex);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decisionselect && gameState == STATE_FINISHED
    True
      1. ExecutechoosePiece();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  8. Decisionselect && gameState == STATE_PLAYING && currentPlayer == humanPiece
    True
    1. ExecuteperformMove(selectedMove);
    False

    Continue after this decision.

Read original function source
void othelloAction(bool select, bool speed, bool choose, uint32_t now_ms)
{
    if (!running) return;
    clockNow = now_ms;
    if (choose) { choosePiece(); return; }
    if (gameState == STATE_SHOW_SCORES) {
        if (select) choosePiece();
        return;
    }
    if (gameState == STATE_CHOOSE_PIECE) {
        if (select) beginGame();
        return;
    }
    if (autoMode) {
        if (speed) {
            cycleAISpeed(&aiSpeedIndex);
            computerMoveDue = now_ms + aiSpeedDelay(aiSpeedIndex);
        }
        return;
    }
    if (select && gameState == STATE_FINISHED) { choosePiece(); return; }
    if (select && gameState == STATE_PLAYING && currentPlayer == humanPiece) performMove(selectedMove);
}
updateOthello() · source line 343
void updateOthello(int x_offset, int y_offset, bool valid, bool left, bool right,
                     uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. ExecuteclockNow = now_ms;
  3. DecisiongameState == STATE_SHOW_SCORES && now_ms - stateStarted >= 2000U
    True
    1. ExecutechoosePiece();
    False

    Continue after this decision.

  4. DecisiongameState == STATE_FINISHED && autoMode
    True
      1. Decisionnow_ms - stateStarted >= aiRestartDelay()
        True
        1. ExecutebeginGame();
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. DecisiongameState == STATE_PLAYING && (autoMode || currentPlayer != humanPiece)
    True
      1. Decision(int32_t)(now_ms - computerMoveDue) >= 0
        True
          1. DecisionlegalMoveCount == 0
            True
            1. ExecutefinishTurn();
            False
            1. ExecuteperformMove(chooseComputerMove());
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decision!valid
    True
    1. ExecutestickCentred = false;
    False

    Continue after this decision.

  7. Executeconst int ax = x_offset < 0 ? -x_offset : x_offset;
  8. Executeconst int ay = y_offset < 0 ? -y_offset : y_offset;
  9. Decisionvalid && ax <= 350 && ay <= 350
    True
    1. ExecutestickCentred = true;
    False

    Continue after this decision.

  10. Executeint direction = 0;
  11. Decisionleft || right
    True
      1. Executedirection = (int)right - (int)left;
      2. Decisiondirection == 0
        True
          1. ExecuteheldDirection = 0;
          2. Executerepeating = false;
          3. Exit this functionreturn;
        False

        Continue after this decision.

    False
      1. Decision!valid
        True
          1. ExecuteheldDirection = 0;
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Decisionax <= 350 && ay <= 350
        True
          1. ExecuteheldDirection = 0;
          2. Executerepeating = false;
          3. Exit this functionreturn;
        False

        Continue after this decision.

      3. Decision!stickCentred
        True
        1. Exit this functionreturn;
        False

        Continue after this decision.

      4. Decisionax >= ay && ax >= 600
        True
        1. Executedirection = x_offset > 0 ? 1 : -1;
        False
        1. Decisionay >= 600
          True
          1. Executedirection = y_offset < 0 ? 1 : -1;
          False

          Continue after this decision.

  12. Decisiondirection == 0
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  13. DecisionheldDirection == direction && now_ms - lastStickMove < (repeating ? 150U : 400U)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  14. Executerepeating = heldDirection == direction;
  15. ExecuteheldDirection = direction;
  16. ExecutelastStickMove = now_ms;
  17. DecisiongameState == STATE_CHOOSE_PIECE
    True
      1. Executeint choice = autoMode ? 2 : humanPiece == BLACK_PIECE ? 0 : 1;
      2. Executechoice = (choice + direction + 3) % 3;
      3. ExecuteautoMode = choice == 2;
      4. ExecutehumanPiece = choice == 1 ? WHITE_PIECE : BLACK_PIECE;
    False
    1. DecisiongameState == STATE_PLAYING && legalMoveCount > 0
      True
        1. ExecuteselectedMove = (uint8_t)((selectedMove + direction + legalMoveCount) % legalMoveCount);
      False

      Continue after this decision.

Read original function source
void updateOthello(int x_offset, int y_offset, bool valid, bool left, bool right,
                     uint32_t now_ms)
{
    if (!running) return;
    clockNow = now_ms;
    if (gameState == STATE_SHOW_SCORES && now_ms - stateStarted >= 2000U) choosePiece();
    if (gameState == STATE_FINISHED && autoMode) {
        if (now_ms - stateStarted >= aiRestartDelay()) beginGame();
        return;
    }
    if (gameState == STATE_PLAYING && (autoMode || currentPlayer != humanPiece)) {
        if ((int32_t)(now_ms - computerMoveDue) >= 0) {
            if (legalMoveCount == 0) finishTurn();
            else performMove(chooseComputerMove());
        }
        return;
    }
    if (!valid) stickCentred = false;
    const int ax = x_offset < 0 ? -x_offset : x_offset;
    const int ay = y_offset < 0 ? -y_offset : y_offset;
    if (valid && ax <= 350 && ay <= 350) stickCentred = true;
    int direction = 0;
    if (left || right) {
        direction = (int)right - (int)left;
        if (direction == 0) { heldDirection = 0; repeating = false; return; }
    } else {
        if (!valid) { heldDirection = 0; return; }
        if (ax <= 350 && ay <= 350) {
            heldDirection = 0; repeating = false; return;
        }
        if (!stickCentred) return;
        if (ax >= ay && ax >= 600) direction = x_offset > 0 ? 1 : -1;
        else if (ay >= 600) direction = y_offset < 0 ? 1 : -1;
    }
    if (direction == 0) return;
    if (heldDirection == direction && now_ms - lastStickMove < (repeating ? 150U : 400U)) return;
    repeating = heldDirection == direction;
    heldDirection = direction;
    lastStickMove = now_ms;
    if (gameState == STATE_CHOOSE_PIECE) {
        int choice = autoMode ? 2 : humanPiece == BLACK_PIECE ? 0 : 1;
        choice = (choice + direction + 3) % 3;
        autoMode = choice == 2;
        humanPiece = choice == 1 ? WHITE_PIECE : BLACK_PIECE;
    } else if (gameState == STATE_PLAYING && legalMoveCount > 0) {
        selectedMove = (uint8_t)((selectedMove + direction + legalMoveCount) % legalMoveCount);
    }
}
drawPiece() · source line 392
static void drawPiece(uint16_t *frame, uint8_t square, uint8_t piece)

Calls into this documentation:

  1. Executeconst int x = BOARD_X + (square % 8) * CELL_SIZE + 10;
  2. Executeconst int y = BOARD_Y + (square / 8) * CELL_SIZE + 10;
  3. Executefill_circle(frame, x, y, 8, piece == BLACK_PIECE ? 0 : 0xffff);
  4. Executedraw_ring(frame, x, y, 8, piece == BLACK_PIECE ? 0xffff : 0);
Read original function source
static void drawPiece(uint16_t *frame, uint8_t square, uint8_t piece)
{
    const int x = BOARD_X + (square % 8) * CELL_SIZE + 10;
    const int y = BOARD_Y + (square / 8) * CELL_SIZE + 10;
    fill_circle(frame, x, y, 8, piece == BLACK_PIECE ? 0 : 0xffff);
    draw_ring(frame, x, y, 8, piece == BLACK_PIECE ? 0xffff : 0);

}
drawOthello() · source line 401
void drawOthello(uint16_t *frame)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeclear(frame, 0);
  3. Executechar line[64];
  4. DecisiongameState == STATE_SHOW_SCORES || gameState == STATE_CHOOSE_PIECE
    True
      1. Executedraw_text_centred(frame, 160, 8, "OTHELLO", 3, 0x07ff);
      2. DecisiongameState == STATE_SHOW_SCORES
        True
          1. Executesnprintf(line, sizeof(line), "WINS %u LOSSES %u", (unsigned)scores.wins, (unsigned)scores.losses);
          2. Executedraw_text_centred(frame, 160, 55, line, 1, 0xffff);
          3. Executesnprintf(line, sizeof(line), "DRAWS %u STREAK %u", (unsigned)scores.draws, (unsigned)scores.streak);
          4. Executedraw_text_centred(frame, 160, 78, line, 1, 0xffff);
        False
          1. Executeconst char *labels[] = {"BLACK", "WHITE", "AUTO"};
          2. Executeconst int selected = autoMode ? 2 : humanPiece == BLACK_PIECE ? 0 : 1;
          3. FOR loopint i = 0; i < 3; ++i

            Repeat body while the loop condition permits; then continue below.

              1. Executedraw_rect(frame, 8 + i * 104, 58, 96, 40, i == selected ? 0xffe0 : 0x7bef);
              2. Executedraw_text_centred(frame, 56 + i * 104, 72, labels[i], 2, 0xffff);
          4. Executedraw_text_centred(frame, 160, 116, "D/B/STICK CHOOSE A/K START", 1, 0xffff);
      3. Executedraw_text_centred(frame, 160, 139, scoreSaveFailed ? "SCORE SAVE ERROR" : game_scores_status_text(scoreStatus), 1, 0xffe0);
      4. Executedraw_text_centred(frame, 160, 157, "BOOT BACK", 1, 0x7bef);
      5. Exit this functionreturn;
    False

    Continue after this decision.

  5. FOR loopuint8_t square = 0; square < 64; ++square

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst int row = square / 8, column = square % 8;
      2. Executefill_rect(frame, BOARD_X + column * CELL_SIZE, BOARD_Y + row * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1, 0x0320);
      3. Decisionboard[square] != EMPTY
        True
        1. ExecutedrawPiece(frame, square, board[square]);
        False
        1. DecisiongameState == STATE_PLAYING && legalMove(square, currentPlayer)
          True
          1. Executefill_circle(frame, BOARD_X + column * CELL_SIZE + 10, BOARD_Y + row * CELL_SIZE + 10, 2, 0xc618);
          False

          Continue after this decision.

  6. DecisiongameState == STATE_PLAYING && legalMoveCount > 0
    True
      1. Executeconst uint8_t cell = legalMoves[selectedMove];
      2. Executedraw_rect(frame, BOARD_X + (cell % 8) * CELL_SIZE + 2, BOARD_Y + (cell / 8) * CELL_SIZE + 2, 16, 16, 0xffe0);
    False

    Continue after this decision.

  7. ExecutedrawGameGold(frame, 236, 3, 82, true);
  8. Executedraw_text(frame, 170, 4, "TURN", 1, 0x07ff);
  9. Executedraw_text(frame, 170, 16, currentPlayer == BLACK_PIECE ? "BLACK" : "WHITE", 1, 0xffff);
  10. Executesnprintf(line, sizeof(line), "B %u W %u", (unsigned)countPieces(BLACK_PIECE), (unsigned)countPieces(WHITE_PIECE));
  11. Executedraw_text(frame, 170, 36, line, 1, 0xffff);
  12. Executedraw_text(frame, 170, 53, boardMessage, 1, 0xffe0);
  13. DecisiongameState == STATE_PLAYING && legalMoveCount > 0
    True
      1. Executeconst uint8_t cell = legalMoves[selectedMove];
      2. Executesnprintf(line, sizeof(line), "PLACE %c%u", 'A' + cell % 8, (unsigned)(cell / 8 + 1));
      3. Executedraw_text(frame, 170, 70, line, 1, 0xffff);
    False

    Continue after this decision.

  14. DecisionautoMode
    True
      1. Executesnprintf(line, sizeof(line), "AI %s WINNER %s", aiSpeedShortText(aiSpeedIndex), goldEarningPiece == BLACK_PIECE ? "B" : "W");
      2. Executedraw_text(frame, 170, 87, line, 1, 0x07ff);
      3. DecisiongameState == STATE_FINISHED
        True
          1. Executeconst uint32_t elapsed = clockNow - stateStarted;
          2. Executeconst uint32_t delay = aiRestartDelay();
          3. Executeconst unsigned seconds = elapsed >= delay ? 0 : (unsigned)((delay - elapsed + 999) / 1000);
          4. Executesnprintf(line, sizeof(line), "NEXT GAME %u", seconds);
          5. Executedraw_text(frame, 170, 102, line, 1, 0xffff);
        False

        Continue after this decision.

      4. Executedraw_text(frame, 170, 121, "E SPEED C STOP", 1, 0xffff);
    False
      1. Executedraw_text(frame, 170, 87, "WIN 35 GOLD", 1, 0xfea0);
      2. Executedraw_text(frame, 170, 104, "D/B/STICK NEXT/PREV", 1, 0xffff);
      3. Executedraw_text(frame, 170, 121, gameState == STATE_FINISHED ? "A/K NEW GAME" : "A/K PLAY MOVE", 1, 0xffff);
  15. Executeconst char *status = goldSaveFailed ? "GOLD SAVE ERROR" : scoreSaveFailed ? "SCORE SAVE ERROR" : scoreStatus != SCORES_OK && scoreStatus != SCORES_NOT_FOUND ? game_scores_status_text(scoreStatus) : !aiTimingStorageHealthy() ? "AI TIMING ERROR" : "C CHOOSE BOOT BACK";
  16. Executedraw_text(frame, 170, 151, status, 1, 0xffe0);
Read original function source
void drawOthello(uint16_t *frame)
{
    if (!running) return;
    clear(frame, 0);
    char line[64];
    if (gameState == STATE_SHOW_SCORES || gameState == STATE_CHOOSE_PIECE) {
        draw_text_centred(frame, 160, 8, "OTHELLO", 3, 0x07ff);
        if (gameState == STATE_SHOW_SCORES) {
            snprintf(line, sizeof(line), "WINS %u  LOSSES %u", (unsigned)scores.wins, (unsigned)scores.losses);
            draw_text_centred(frame, 160, 55, line, 1, 0xffff);
            snprintf(line, sizeof(line), "DRAWS %u  STREAK %u", (unsigned)scores.draws, (unsigned)scores.streak);
            draw_text_centred(frame, 160, 78, line, 1, 0xffff);
        } else {
            const char *labels[] = {"BLACK", "WHITE", "AUTO"};
            const int selected = autoMode ? 2 : humanPiece == BLACK_PIECE ? 0 : 1;
            for (int i = 0; i < 3; ++i) {
                draw_rect(frame, 8 + i * 104, 58, 96, 40, i == selected ? 0xffe0 : 0x7bef);
                draw_text_centred(frame, 56 + i * 104, 72, labels[i], 2, 0xffff);
            }
            draw_text_centred(frame, 160, 116, "D/B/STICK CHOOSE  A/K START", 1, 0xffff);
        }
        draw_text_centred(frame, 160, 139, scoreSaveFailed ? "SCORE SAVE ERROR" : game_scores_status_text(scoreStatus), 1, 0xffe0);
        draw_text_centred(frame, 160, 157, "BOOT BACK", 1, 0x7bef);
        return;
    }
    for (uint8_t square = 0; square < 64; ++square) {
        const int row = square / 8, column = square % 8;
        fill_rect(frame, BOARD_X + column * CELL_SIZE, BOARD_Y + row * CELL_SIZE,
                     CELL_SIZE - 1, CELL_SIZE - 1, 0x0320);
        if (board[square] != EMPTY) drawPiece(frame, square, board[square]);
        else if (gameState == STATE_PLAYING && legalMove(square, currentPlayer))
            fill_circle(frame, BOARD_X + column * CELL_SIZE + 10,
                           BOARD_Y + row * CELL_SIZE + 10, 2, 0xc618);
    }
    if (gameState == STATE_PLAYING && legalMoveCount > 0) {
        const uint8_t cell = legalMoves[selectedMove];
        draw_rect(frame, BOARD_X + (cell % 8) * CELL_SIZE + 2,
                        BOARD_Y + (cell / 8) * CELL_SIZE + 2, 16, 16, 0xffe0);
    }
    drawGameGold(frame, 236, 3, 82, true);
    draw_text(frame, 170, 4, "TURN", 1, 0x07ff);
    draw_text(frame, 170, 16, currentPlayer == BLACK_PIECE ? "BLACK" : "WHITE", 1, 0xffff);
    snprintf(line, sizeof(line), "B %u  W %u", (unsigned)countPieces(BLACK_PIECE), (unsigned)countPieces(WHITE_PIECE));
    draw_text(frame, 170, 36, line, 1, 0xffff);
    draw_text(frame, 170, 53, boardMessage, 1, 0xffe0);
    if (gameState == STATE_PLAYING && legalMoveCount > 0) {
        const uint8_t cell = legalMoves[selectedMove];
        snprintf(line, sizeof(line), "PLACE %c%u", 'A' + cell % 8, (unsigned)(cell / 8 + 1));
        draw_text(frame, 170, 70, line, 1, 0xffff);
    }
    if (autoMode) {
        snprintf(line, sizeof(line), "AI %s WINNER %s", aiSpeedShortText(aiSpeedIndex), goldEarningPiece == BLACK_PIECE ? "B" : "W");
        draw_text(frame, 170, 87, line, 1, 0x07ff);
        if (gameState == STATE_FINISHED) {
            const uint32_t elapsed = clockNow - stateStarted;
            const uint32_t delay = aiRestartDelay();
            const unsigned seconds = elapsed >= delay ? 0 : (unsigned)((delay - elapsed + 999) / 1000);
            snprintf(line, sizeof(line), "NEXT GAME %u", seconds);
            draw_text(frame, 170, 102, line, 1, 0xffff);
        }
        draw_text(frame, 170, 121, "E SPEED  C STOP", 1, 0xffff);
    } else {
        draw_text(frame, 170, 87, "WIN 35 GOLD", 1, 0xfea0);
        draw_text(frame, 170, 104, "D/B/STICK NEXT/PREV", 1, 0xffff);
        draw_text(frame, 170, 121, gameState == STATE_FINISHED ? "A/K NEW GAME" : "A/K PLAY MOVE", 1, 0xffff);
    }
    const char *status = goldSaveFailed ? "GOLD SAVE ERROR" : scoreSaveFailed ? "SCORE SAVE ERROR" :
                         scoreStatus != SCORES_OK && scoreStatus != SCORES_NOT_FOUND ? game_scores_status_text(scoreStatus) :
                         !aiTimingStorageHealthy() ? "AI TIMING ERROR" : "C CHOOSE  BOOT BACK";
    draw_text(frame, 170, 151, status, 1, 0xffe0);
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "othello.h"
#include "audio.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include "game_scores.h"
#include "gold.h"
#include "ai_speed.h"
#include "esp_random.h"
#include "esp_log.h"
#include <stdio.h>

// Rules and positional AI ported from the other LilyGO project's othello.cpp.
#define OTHELLO_WIN_GOLD 35U
#define BOARD_X 4
#define BOARD_Y 5
#define CELL_SIZE 20

enum { EMPTY, BLACK_PIECE, WHITE_PIECE };
typedef enum { STATE_SHOW_SCORES, STATE_CHOOSE_PIECE, STATE_PLAYING, STATE_FINISHED } GameState;
static uint8_t board[64];
static game_scores_t scores;
static game_score_result_t scoreStatus;
static GameState gameState;
static uint8_t legalMoves[64], legalMoveCount, selectedMove;
static uint8_t humanPiece, currentPlayer, goldEarningPiece, aiSpeedIndex;
static uint32_t stateStarted, computerMoveDue, clockNow, lastStickMove;
static bool running, autoMode, stickCentred, repeating, scoreSaveFailed, goldSaveFailed;
static int heldDirection;
static const char *boardMessage;

static const int8_t DIRECTIONS[8][2] =
{
    { -1, -1 },
    { -1,  0 },
    { -1,  1 },
    {  0, -1 },
    {  0,  1 },
    {  1, -1 },
    {  1,  0 },
    {  1,  1 }
};

static const int16_t POSITION_WEIGHTS[64] =
{
    120, -25,  20,   5,   5,  20, -25, 120,
    -25, -40,  -5,  -5,  -5,  -5, -40, -25,
    20,  -5,  15,   3,   3,  15,  -5,  20,
    5,  -5,   3,   3,   3,   3,  -5,   5,
    5,  -5,   3,   3,   3,   3,  -5,   5,
    20,  -5,  15,   3,   3,  15,  -5,  20,
    -25, -40,  -5,  -5,  -5,  -5, -40, -25,
    120, -25,  20,   5,   5,  20, -25, 120
};

static uint8_t opponent(uint8_t piece)
{
    return piece == BLACK_PIECE
        ? WHITE_PIECE
        : BLACK_PIECE;
}

static bool insideBoard(int row, int column)
{
    return row >= 0 &&
        row < 8 &&
        column >= 0 &&
        column < 8;
}

static uint8_t countPieces(uint8_t piece)
{
    uint8_t total = 0;

    for (uint8_t i = 0; i < 64; i++)
    {
        if (board[i] == piece)
            total++;
    }

    return total;
}

static uint8_t countFlips(
    uint8_t cell,
    uint8_t piece,
    int8_t rowDirection,
    int8_t columnDirection
)
{
    if (board[cell] != EMPTY)
        return 0;

    const int startRow = cell / 8;
    const int startColumn = cell % 8;

    int row = startRow + rowDirection;
    int column = startColumn + columnDirection;

    uint8_t flips = 0;
    const uint8_t otherPiece = opponent(piece);

    while (insideBoard(row, column) &&
           board[row * 8 + column] == otherPiece)
    {
        flips++;
        row += rowDirection;
        column += columnDirection;
    }

    if (flips == 0 || !insideBoard(row, column))
        return 0;

    if (board[row * 8 + column] != piece)
        return 0;

    return flips;
}

static bool legalMove(uint8_t cell, uint8_t piece)
{
    if (cell >= 64 || board[cell] != EMPTY)
        return false;

    for (uint8_t d = 0; d < 8; ++d)
    {
        if (countFlips(
                cell,
                piece,
                DIRECTIONS[d][0],
                DIRECTIONS[d][1]
            ) > 0)
        {
            return true;
        }
    }

    return false;
}

static bool hasLegalMove(uint8_t piece)
{
    for (uint8_t i = 0; i < 64; i++)
    {
        if (legalMove(i, piece))
            return true;
    }

    return false;
}

static void makeMove(uint8_t cell, uint8_t piece)
{
    const int startRow = cell / 8;
    const int startColumn = cell % 8;

    for (uint8_t d = 0; d < 8; ++d)
    {
        const uint8_t flipCount = countFlips(
            cell,
            piece,
            DIRECTIONS[d][0],
            DIRECTIONS[d][1]
        );

        int row = startRow + DIRECTIONS[d][0];
        int column = startColumn + DIRECTIONS[d][1];

        for (uint8_t i = 0; i < flipCount; i++)
        {
            board[row * 8 + column] = piece;
            row += DIRECTIONS[d][0];
            column += DIRECTIONS[d][1];
        }
    }
    board[cell] = piece;
}


static void resetBoard(void)
{
    for (uint8_t i = 0; i < 64; i++)
        board[i] = EMPTY;

    board[3 * 8 + 3] = WHITE_PIECE;
    board[3 * 8 + 4] = BLACK_PIECE;
    board[4 * 8 + 3] = BLACK_PIECE;
    board[4 * 8 + 4] = WHITE_PIECE;

    currentPlayer = BLACK_PIECE;
    selectedMove = 0;
}

static void refreshLegalMoves(void)
{
    legalMoveCount = 0;
    for (uint8_t cell = 0; cell < 64; ++cell) {
        if (legalMove(cell, currentPlayer)) legalMoves[legalMoveCount++] = cell;
    }
    selectedMove = 0;
    stickCentred = false;
    heldDirection = 0;
}

static void incrementScore(uint32_t *value)
{
    if (*value < UINT32_MAX) ++*value;
}

static void finishGame(uint8_t winner)
{
    gameState = STATE_FINISHED;
    stateStarted = clockNow;
    boardMessage = winner == EMPTY ? "DRAW" : winner == BLACK_PIECE ? "BLACK WINS" : "WHITE WINS";
    if (autoMode) {
        if (winner == goldEarningPiece && !addAIGold(OTHELLO_WIN_GOLD)) goldSaveFailed = true;
    } else {
        if (winner == humanPiece) {
            incrementScore(&scores.wins);
            incrementScore(&scores.streak);
            if (!addUserGoldWon(OTHELLO_WIN_GOLD)) goldSaveFailed = true;
            boardMessage = "YOU WIN";
        } else {
            scores.streak = 0;
            if (winner == EMPTY) incrementScore(&scores.draws);
            else { incrementScore(&scores.losses); boardMessage = "CPU WINS"; }
        }
        if (scoreStatus == SCORES_OK || scoreStatus == SCORES_NOT_FOUND) {
            scoreSaveFailed = !game_scores_save(GAME_OTHELLO, &scores);
            if (!scoreSaveFailed) scoreStatus = SCORES_OK;
        } else scoreSaveFailed = true;
    }
    ESP_LOGI("othello", "%s; score save %s; Gold %s", boardMessage,
             scoreSaveFailed ? "ERROR" : autoMode ? "NOT REQUIRED" : "OK",
             goldSaveFailed ? "ERROR" : "OK");
}

static void finishTurn(void)
{
    const uint8_t nextPlayer = opponent(currentPlayer);
    const bool passed = !hasLegalMove(nextPlayer);
    if (!passed) currentPlayer = nextPlayer;
    else if (!hasLegalMove(currentPlayer)) {
        const uint8_t black = countPieces(BLACK_PIECE);
        const uint8_t white = countPieces(WHITE_PIECE);
        finishGame(black > white ? BLACK_PIECE : white > black ? WHITE_PIECE : EMPTY);
        return;
    }
    refreshLegalMoves();
    boardMessage = passed ? (autoMode ? "TURN PASSED" : currentPlayer == humanPiece ? "CPU PASS" : "YOU PASS") :
                   autoMode || currentPlayer != humanPiece ? "CPU TURN" : "YOUR TURN";
    computerMoveDue = clockNow + (autoMode ? aiSpeedDelay(aiSpeedIndex) : 800U);
}

static void performMove(uint8_t index)
{
    if (index >= legalMoveCount || !legalMove(legalMoves[index], currentPlayer)) return;
    makeMove(legalMoves[index], currentPlayer);
    audioPlay(AUDIO_SFX_BOARD_CAPTURE);
    finishTurn();
}

static uint8_t chooseComputerMove(void)
{
    int moveScores[64];
    int bestScore = -10000;
    for (uint8_t i = 0; i < legalMoveCount; ++i) {
        const uint8_t cell = legalMoves[i];
        int flips = 0;
        for (uint8_t d = 0; d < 8; ++d)
            flips += countFlips(cell, currentPlayer, DIRECTIONS[d][0], DIRECTIONS[d][1]);
        moveScores[i] = POSITION_WEIGHTS[cell] + flips * 3;
        if (moveScores[i] > bestScore) bestScore = moveScores[i];
    }
    uint8_t candidates[64], count = 0;
    for (uint8_t i = 0; i < legalMoveCount; ++i) {
        if (moveScores[i] >= bestScore - 8) candidates[count++] = i;
    }
    return count > 0 ? candidates[esp_random() % count] : 0;
}

static void choosePiece(void)
{
    gameState = STATE_CHOOSE_PIECE;
    autoMode = false;
    humanPiece = BLACK_PIECE;
    stickCentred = false;
    heldDirection = 0;
}

static void beginGame(void)
{
    resetBoard();
    gameState = STATE_PLAYING;
    if (autoMode) goldEarningPiece = esp_random() % 2 == 0 ? BLACK_PIECE : WHITE_PIECE;
    refreshLegalMoves();
    boardMessage = autoMode || humanPiece != BLACK_PIECE ? "CPU TURN" : "YOUR TURN";
    computerMoveDue = clockNow + (autoMode ? aiSpeedDelay(aiSpeedIndex) : 800U);
}

void startOthello(uint32_t now_ms)
{
    clockNow = stateStarted = now_ms;
    running = true;
    scoreSaveFailed = goldSaveFailed = false;
    scores = (game_scores_t){0};
    scoreStatus = game_scores_load(GAME_OTHELLO, &scores);
    aiSpeedIndex = AI_SPEED_DEFAULT_INDEX;
    resetBoard();
    choosePiece();
    gameState = STATE_SHOW_SCORES;
    ESP_LOGI("othello", "START: %s", game_scores_status_text(scoreStatus));
}

void stopOthello(void)
{
    running = false;
    ESP_LOGI("othello", "STOP");
}

void othelloAction(bool select, bool speed, bool choose, uint32_t now_ms)
{
    if (!running) return;
    clockNow = now_ms;
    if (choose) { choosePiece(); return; }
    if (gameState == STATE_SHOW_SCORES) {
        if (select) choosePiece();
        return;
    }
    if (gameState == STATE_CHOOSE_PIECE) {
        if (select) beginGame();
        return;
    }
    if (autoMode) {
        if (speed) {
            cycleAISpeed(&aiSpeedIndex);
            computerMoveDue = now_ms + aiSpeedDelay(aiSpeedIndex);
        }
        return;
    }
    if (select && gameState == STATE_FINISHED) { choosePiece(); return; }
    if (select && gameState == STATE_PLAYING && currentPlayer == humanPiece) performMove(selectedMove);
}

void updateOthello(int x_offset, int y_offset, bool valid, bool left, bool right,
                     uint32_t now_ms)
{
    if (!running) return;
    clockNow = now_ms;
    if (gameState == STATE_SHOW_SCORES && now_ms - stateStarted >= 2000U) choosePiece();
    if (gameState == STATE_FINISHED && autoMode) {
        if (now_ms - stateStarted >= aiRestartDelay()) beginGame();
        return;
    }
    if (gameState == STATE_PLAYING && (autoMode || currentPlayer != humanPiece)) {
        if ((int32_t)(now_ms - computerMoveDue) >= 0) {
            if (legalMoveCount == 0) finishTurn();
            else performMove(chooseComputerMove());
        }
        return;
    }
    if (!valid) stickCentred = false;
    const int ax = x_offset < 0 ? -x_offset : x_offset;
    const int ay = y_offset < 0 ? -y_offset : y_offset;
    if (valid && ax <= 350 && ay <= 350) stickCentred = true;
    int direction = 0;
    if (left || right) {
        direction = (int)right - (int)left;
        if (direction == 0) { heldDirection = 0; repeating = false; return; }
    } else {
        if (!valid) { heldDirection = 0; return; }
        if (ax <= 350 && ay <= 350) {
            heldDirection = 0; repeating = false; return;
        }
        if (!stickCentred) return;
        if (ax >= ay && ax >= 600) direction = x_offset > 0 ? 1 : -1;
        else if (ay >= 600) direction = y_offset < 0 ? 1 : -1;
    }
    if (direction == 0) return;
    if (heldDirection == direction && now_ms - lastStickMove < (repeating ? 150U : 400U)) return;
    repeating = heldDirection == direction;
    heldDirection = direction;
    lastStickMove = now_ms;
    if (gameState == STATE_CHOOSE_PIECE) {
        int choice = autoMode ? 2 : humanPiece == BLACK_PIECE ? 0 : 1;
        choice = (choice + direction + 3) % 3;
        autoMode = choice == 2;
        humanPiece = choice == 1 ? WHITE_PIECE : BLACK_PIECE;
    } else if (gameState == STATE_PLAYING && legalMoveCount > 0) {
        selectedMove = (uint8_t)((selectedMove + direction + legalMoveCount) % legalMoveCount);
    }
}

static void drawPiece(uint16_t *frame, uint8_t square, uint8_t piece)
{
    const int x = BOARD_X + (square % 8) * CELL_SIZE + 10;
    const int y = BOARD_Y + (square / 8) * CELL_SIZE + 10;
    fill_circle(frame, x, y, 8, piece == BLACK_PIECE ? 0 : 0xffff);
    draw_ring(frame, x, y, 8, piece == BLACK_PIECE ? 0xffff : 0);

}

void drawOthello(uint16_t *frame)
{
    if (!running) return;
    clear(frame, 0);
    char line[64];
    if (gameState == STATE_SHOW_SCORES || gameState == STATE_CHOOSE_PIECE) {
        drawPet2001TextCentredTransparent(frame, 160, 8, "OTHELLO", 0x07ff, 3);
        if (gameState == STATE_SHOW_SCORES) {
            snprintf(line, sizeof(line), "WINS %u  LOSSES %u", (unsigned)scores.wins, (unsigned)scores.losses);
            drawPet2001TextCentredTransparent(frame, 160, 55, line, 0xffff, 1);
            snprintf(line, sizeof(line), "DRAWS %u  STREAK %u", (unsigned)scores.draws, (unsigned)scores.streak);
            drawPet2001TextCentredTransparent(frame, 160, 78, line, 0xffff, 1);
        } else {
            const char *labels[] = {"BLACK", "WHITE", "AUTO"};
            const int selected = autoMode ? 2 : humanPiece == BLACK_PIECE ? 0 : 1;
            for (int i = 0; i < 3; ++i) {
                draw_rect(frame, 8 + i * 104, 58, 96, 40, i == selected ? 0xffe0 : 0x7bef);
                drawPet2001TextCentredTransparent(frame, 56 + i * 104, 72, labels[i], 0xffff, 2);
            }
            drawPet2001TextCentredTransparent(frame, 160, 116, "D/B/STICK CHOOSE  A/K START", 0xffff, 1);
        }
        drawPet2001TextCentredTransparent(frame, 160, 139,
                                           scoreSaveFailed ? "SCORE SAVE ERROR" : game_scores_status_text(scoreStatus),
                                           0xffe0, 1);
        drawPet2001TextCentredTransparent(frame, 160, 157, "BOOT BACK", 0x7bef, 1);
        return;
    }
    for (uint8_t square = 0; square < 64; ++square) {
        const int row = square / 8, column = square % 8;
        fill_rect(frame, BOARD_X + column * CELL_SIZE, BOARD_Y + row * CELL_SIZE,
                     CELL_SIZE - 1, CELL_SIZE - 1, 0x0320);
        if (board[square] != EMPTY) drawPiece(frame, square, board[square]);
        else if (gameState == STATE_PLAYING && legalMove(square, currentPlayer))
            fill_circle(frame, BOARD_X + column * CELL_SIZE + 10,
                           BOARD_Y + row * CELL_SIZE + 10, 2, 0xc618);
    }
    if (gameState == STATE_PLAYING && legalMoveCount > 0) {
        const uint8_t cell = legalMoves[selectedMove];
        draw_rect(frame, BOARD_X + (cell % 8) * CELL_SIZE + 2,
                        BOARD_Y + (cell / 8) * CELL_SIZE + 2, 16, 16, 0xffe0);
    }
    drawGameGold(frame, 236, 3, 82, true);
    drawPet2001TextTransparent(frame, 168, 4, "TURN", 0x07ff, 1);
    drawPet2001TextTransparent(frame, 168, 16, currentPlayer == BLACK_PIECE ? "BLACK" : "WHITE", 0xffff, 1);
    snprintf(line, sizeof(line), "B %u  W %u", (unsigned)countPieces(BLACK_PIECE), (unsigned)countPieces(WHITE_PIECE));
    drawPet2001TextTransparent(frame, 168, 36, line, 0xffff, 1);
    drawPet2001TextTransparent(frame, 168, 53, boardMessage, 0xffe0, 1);
    if (gameState == STATE_PLAYING && legalMoveCount > 0) {
        const uint8_t cell = legalMoves[selectedMove];
        snprintf(line, sizeof(line), "PLACE %c%u", 'A' + cell % 8, (unsigned)(cell / 8 + 1));
        drawPet2001TextTransparent(frame, 168, 70, line, 0xffff, 1);
    }
    if (autoMode) {
        snprintf(line, sizeof(line), "AI %s WINNER %s", aiSpeedShortText(aiSpeedIndex), goldEarningPiece == BLACK_PIECE ? "B" : "W");
        drawPet2001TextTransparent(frame, 168, 87, line, 0x07ff, 1);
        if (gameState == STATE_FINISHED) {
            const uint32_t elapsed = clockNow - stateStarted;
            const uint32_t delay = aiRestartDelay();
            const unsigned seconds = elapsed >= delay ? 0 : (unsigned)((delay - elapsed + 999) / 1000);
            snprintf(line, sizeof(line), "NEXT GAME %u", seconds);
            drawPet2001TextTransparent(frame, 168, 102, line, 0xffff, 1);
        }
        drawPet2001TextTransparent(frame, 168, 121, "E SPEED  C STOP", 0xffff, 1);
    } else {
        drawPet2001TextTransparent(frame, 168, 87, "WIN 35 GOLD", 0xfea0, 1);
        drawPet2001TextTransparent(frame, 168, 104, "D/B/STICK NEXT/PREV", 0xffff, 1);
        drawPet2001TextTransparent(frame, 168, 121,
                                   gameState == STATE_FINISHED ? "A/K NEW GAME" : "A/K PLAY MOVE", 0xffff, 1);
    }
    const char *status = goldSaveFailed ? "GOLD SAVE ERROR" : scoreSaveFailed ? "SCORE SAVE ERROR" :
                         scoreStatus != SCORES_OK && scoreStatus != SCORES_NOT_FOUND ? game_scores_status_text(scoreStatus) :
                         !aiTimingStorageHealthy() ? "AI TIMING ERROR" : "C CHOOSE  BOOT BACK";
    drawPet2001TextTransparent(frame, 168, 151, status, 0xffe0, 1);
}