Checkers

Source: src/checkers.c · 556 lines · 26 functions.

Screen captures

Views of this part running on the Mini Arcade.

Checkers game screen on the Mini Arcade
Checkers showing the board and playing pieces.

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

squareMask() · source line 36
static uint64_t squareMask(uint8_t square)
  1. Exit this functionreturn 1ULL << square;
Read original function source
static uint64_t squareMask(uint8_t square)
{
    return 1ULL << square;
}
opponent() · source line 41
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 48
static bool insideBoard(int8_t row,int8_t column)
  1. Exit this functionreturn row >= 0 && row < 8 && column >= 0 && column < 8;
Read original function source
static bool insideBoard(int8_t row,int8_t column)
{
    return row >= 0 && row < 8 && column >= 0 && column < 8;
}
pieceMask() · source line 53
static uint64_t pieceMask(uint8_t piece)
  1. Exit this functionreturn piece == BLACK_PIECE ? board.black : board.white;
Read original function source
static uint64_t pieceMask(uint8_t piece)
{
    return piece == BLACK_PIECE
        ? board.black
        : board.white;
}
occupied() · source line 60
static bool occupied(uint8_t square)

Calls into this documentation:

  1. Exit this functionreturn (board.black | board.white) & squareMask(square);
Read original function source
static bool occupied(uint8_t square)
{
    return (board.black | board.white) & squareMask(square);
}
isKing() · source line 65
static bool isKing(uint8_t square)

Calls into this documentation:

  1. Exit this functionreturn board.kings & squareMask(square);
Read original function source
static bool isKing(uint8_t square)
{
    return board.kings & squareMask(square);
}
countBits() · source line 70
static uint8_t countBits(uint64_t value)
  1. Executeuint8_t count = 0;
  2. WHILE loopvalue

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

      1. Executecount += value & 1ULL;
      2. Executevalue >>= 1;
  3. Exit this functionreturn count;
Read original function source
static uint8_t countBits(uint64_t value)
{
    uint8_t count = 0;

    while (value)
    {
        count += value & 1ULL;
        value >>= 1;
    }

    return count;
}
countPieces() · source line 83
static uint8_t countPieces(uint8_t piece)

Calls into this documentation:

  1. Exit this functionreturn countBits(pieceMask(piece));
Read original function source
static uint8_t countPieces(uint8_t piece)
{
    return countBits(pieceMask(piece));
}
directionAllowed() · source line 88
static bool directionAllowed(uint8_t piece,bool king,int8_t rowDirection)
  1. Decisionking
    True
    1. Exit this functionreturn true;
    False

    Continue after this decision.

  2. Exit this functionreturn piece == BLACK_PIECE ? rowDirection > 0 : rowDirection < 0;
Read original function source
static bool directionAllowed(uint8_t piece,bool king,int8_t rowDirection)
{
    if (king)
        return true;

    return piece == BLACK_PIECE
        ? rowDirection > 0
        : rowDirection < 0;
}
generateMoves() · source line 98
static uint8_t generateMoves(
    uint8_t piece,
    Move *moves,
    uint8_t onlyFrom)

Calls into this documentation:

  1. Executeuint8_t count = 0;
  2. Executebool captureAvailable = false;
  3. Executeconst uint64_t ownPieces = pieceMask(piece);
  4. Executeconst uint64_t otherPieces = pieceMask(opponent(piece));
  5. FOR loopuint8_t from = 0; from < 64; from++

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

      1. Decision!(ownPieces & squareMask(from)) || (onlyFrom != NO_SQUARE && from != onlyFrom)
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst int8_t row = from / 8;
      3. Executeconst int8_t column = from % 8;
      4. Executeconst bool king = isKing(from);
      5. FOR loopint8_t rowDirection = -1; rowDirection <= 1; rowDirection += 2

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

          1. Decision!directionAllowed(piece,king,rowDirection)
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. FOR loopint8_t columnDirection = -1; columnDirection <= 1; columnDirection += 2

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

              1. Executeconst int8_t middleRow = row + rowDirection;
              2. Executeconst int8_t middleColumn = column + columnDirection;
              3. Executeconst int8_t toRow = row + rowDirection * 2;
              4. Executeconst int8_t toColumn = column + columnDirection * 2;
              5. Decision!insideBoard(middleRow,middleColumn) || !insideBoard(toRow,toColumn)
                True
                1. Next loop iterationcontinue;
                False

                Continue after this decision.

              6. Executeconst uint8_t middle = middleRow * 8 + middleColumn;
              7. Executeconst uint8_t to = toRow * 8 + toColumn;
              8. Decision(otherPieces & squareMask(middle)) && !occupied(to)
                True
                  1. ExecutecaptureAvailable = true;
                  2. Decisioncount < MAX_MOVES
                    True
                    1. Executemoves[count++] = (Move){from,to,middle,true};
                    False

                    Continue after this decision.

                False

                Continue after this decision.

  6. DecisioncaptureAvailable || onlyFrom != NO_SQUARE
    True
    1. Exit this functionreturn count;
    False

    Continue after this decision.

  7. FOR loopuint8_t from = 0; from < 64; from++

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

      1. Decision!(ownPieces & squareMask(from))
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeconst int8_t row = from / 8;
      3. Executeconst int8_t column = from % 8;
      4. Executeconst bool king = isKing(from);
      5. FOR loopint8_t rowDirection = -1; rowDirection <= 1; rowDirection += 2

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

          1. Decision!directionAllowed(piece,king,rowDirection)
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. FOR loopint8_t columnDirection = -1; columnDirection <= 1; columnDirection += 2

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

              1. Executeconst int8_t toRow = row + rowDirection;
              2. Executeconst int8_t toColumn = column + columnDirection;
              3. Decision!insideBoard(toRow,toColumn)
                True
                1. Next loop iterationcontinue;
                False

                Continue after this decision.

              4. Executeconst uint8_t to = toRow * 8 + toColumn;
              5. Decision!occupied(to) && count < MAX_MOVES
                True
                1. Executemoves[count++] = (Move){from,to,NO_SQUARE,false};
                False

                Continue after this decision.

  8. Exit this functionreturn count;
Read original function source
static uint8_t generateMoves(
    uint8_t piece,
    Move *moves,
    uint8_t onlyFrom)
{
    uint8_t count = 0;
    bool captureAvailable = false;
    const uint64_t ownPieces = pieceMask(piece);
    const uint64_t otherPieces = pieceMask(opponent(piece));

    for (uint8_t from = 0; from < 64; from++)
    {
        if (!(ownPieces & squareMask(from)) ||
            (onlyFrom != NO_SQUARE && from != onlyFrom))
            continue;

        const int8_t row = from / 8;
        const int8_t column = from % 8;
        const bool king = isKing(from);

        for (int8_t rowDirection = -1;
            rowDirection <= 1;
            rowDirection += 2)
        {
            if (!directionAllowed(piece,king,rowDirection))
                continue;

            for (int8_t columnDirection = -1;
                columnDirection <= 1;
                columnDirection += 2)
            {
                const int8_t middleRow = row + rowDirection;
                const int8_t middleColumn = column + columnDirection;
                const int8_t toRow = row + rowDirection * 2;
                const int8_t toColumn = column + columnDirection * 2;

                if (!insideBoard(middleRow,middleColumn) ||
                    !insideBoard(toRow,toColumn))
                    continue;

                const uint8_t middle = middleRow * 8 + middleColumn;
                const uint8_t to = toRow * 8 + toColumn;

                if ((otherPieces & squareMask(middle)) &&
                    !occupied(to))
                {
                    captureAvailable = true;

                    if (count < MAX_MOVES)
                        moves[count++] = (Move){from,to,middle,true};
                }
            }
        }
    }

    if (captureAvailable || onlyFrom != NO_SQUARE)
        return count;

    for (uint8_t from = 0; from < 64; from++)
    {
        if (!(ownPieces & squareMask(from)))
            continue;

        const int8_t row = from / 8;
        const int8_t column = from % 8;
        const bool king = isKing(from);

        for (int8_t rowDirection = -1;
            rowDirection <= 1;
            rowDirection += 2)
        {
            if (!directionAllowed(piece,king,rowDirection))
                continue;

            for (int8_t columnDirection = -1;
                columnDirection <= 1;
                columnDirection += 2)
            {
                const int8_t toRow = row + rowDirection;
                const int8_t toColumn = column + columnDirection;

                if (!insideBoard(toRow,toColumn))
                    continue;

                const uint8_t to = toRow * 8 + toColumn;

                if (!occupied(to) && count < MAX_MOVES)
                    moves[count++] = (Move){from,to,NO_SQUARE,false};
            }
        }
    }

    return count;
}
resetBoard() · source line 193
static void resetBoard(void)

Calls into this documentation:

  1. Executeboard = (Board){0};
  2. FOR loopuint8_t row = 0; row < 3; row++

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

      1. FOR loopuint8_t column = 0; column < 8; column++

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

          1. Decision(row + column) % 2 == 1
            True
            1. Executeboard.black |= squareMask(row * 8 + column);
            False

            Continue after this decision.

  3. FOR loopuint8_t row = 5; row < 8; row++

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

      1. FOR loopuint8_t column = 0; column < 8; column++

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

          1. Decision(row + column) % 2 == 1
            True
            1. Executeboard.white |= squareMask(row * 8 + column);
            False

            Continue after this decision.

  4. ExecutecurrentPlayer = BLACK_PIECE;
  5. ExecuteforcedFrom = NO_SQUARE;
  6. ExecuteselectedMove = 0;
  7. ExecutenoProgressMoves = 0;
Read original function source
static void resetBoard(void)
{
    board = (Board){0};

    for (uint8_t row = 0; row < 3; row++)
    {
        for (uint8_t column = 0; column < 8; column++)
        {
            if ((row + column) % 2 == 1)
                board.black |= squareMask(row * 8 + column);
        }
    }

    for (uint8_t row = 5; row < 8; row++)
    {
        for (uint8_t column = 0; column < 8; column++)
        {
            if ((row + column) % 2 == 1)
                board.white |= squareMask(row * 8 + column);
        }
    }

    currentPlayer = BLACK_PIECE;
    forcedFrom = NO_SQUARE;
    selectedMove = 0;
    noProgressMoves = 0;
}
applyMove() · source line 221
static bool applyMove(const Move move)

Calls into this documentation:

  1. Executeconst uint64_t fromMask = squareMask(move.from);
  2. Executeconst uint64_t toMask = squareMask(move.to);
  3. Executeconst bool king = board.kings & fromMask;
  4. DecisioncurrentPlayer == BLACK_PIECE
    True
      1. Executeboard.black &= ~fromMask;
      2. Executeboard.black |= toMask;
    False
      1. Executeboard.white &= ~fromMask;
      2. Executeboard.white |= toMask;
  5. Executeboard.kings &= ~fromMask;
  6. Decisionking
    True
    1. Executeboard.kings |= toMask;
    False

    Continue after this decision.

  7. Decisionmove.capture
    True
      1. Executeconst uint64_t capturedMask = squareMask(move.captured);
      2. Executeboard.black &= ~capturedMask;
      3. Executeboard.white &= ~capturedMask;
      4. Executeboard.kings &= ~capturedMask;
    False

    Continue after this decision.

  8. Executeconst uint8_t destinationRow = move.to / 8;
  9. Executeconst bool promoted = !king && ((currentPlayer == BLACK_PIECE && destinationRow == 7) || (currentPlayer == WHITE_PIECE && destinationRow == 0));
  10. Decisionpromoted
    True
    1. Executeboard.kings |= toMask;
    False

    Continue after this decision.

  11. Decisionmove.capture || promoted
    True
    1. ExecutenoProgressMoves = 0;
    False
    1. DecisionnoProgressMoves < UINT8_MAX
      True
      1. ExecutenoProgressMoves++;
      False

      Continue after this decision.

  12. Exit this functionreturn promoted;
Read original function source
static bool applyMove(const Move move)
{
    const uint64_t fromMask = squareMask(move.from);
    const uint64_t toMask = squareMask(move.to);
    const bool king = board.kings & fromMask;

    if (currentPlayer == BLACK_PIECE)
    {
        board.black &= ~fromMask;
        board.black |= toMask;
    }
    else
    {
        board.white &= ~fromMask;
        board.white |= toMask;
    }

    board.kings &= ~fromMask;

    if (king)
        board.kings |= toMask;

    if (move.capture)
    {
        const uint64_t capturedMask = squareMask(move.captured);
        board.black &= ~capturedMask;
        board.white &= ~capturedMask;
        board.kings &= ~capturedMask;
    }

    const uint8_t destinationRow = move.to / 8;
    const bool promoted = !king &&
        ((currentPlayer == BLACK_PIECE && destinationRow == 7) ||
         (currentPlayer == WHITE_PIECE && destinationRow == 0));

    if (promoted)
        board.kings |= toMask;

    if (move.capture || promoted)
        noProgressMoves = 0;
    else if (noProgressMoves < UINT8_MAX)
        noProgressMoves++;

    return promoted;
}
refreshLegalMoves() · source line 267
static void refreshLegalMoves(void)

Calls into this documentation:

  1. ExecutelegalMoveCount = generateMoves(currentPlayer, legalMoves, forcedFrom);
  2. ExecuteselectedMove = 0;
  3. ExecutestickCentred = false;
  4. ExecuteheldDirection = 0;
Read original function source
static void refreshLegalMoves(void)
{
    legalMoveCount = generateMoves(currentPlayer, legalMoves, forcedFrom);
    selectedMove = 0;
    stickCentred = false;
    heldDirection = 0;
}
incrementScore() · source line 275
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 280
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(CHECKERS_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(CHECKERS_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_CHECKERS, &scores);
          2. Decision!scoreSaveFailed
            True
            1. ExecutescoreStatus = SCORES_OK;
            False

            Continue after this decision.

        False
        1. ExecutescoreSaveFailed = true;
  5. ExecuteESP_LOGI("checkers", "%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(CHECKERS_WIN_GOLD)) goldSaveFailed = true;
    } else {
        if (winner == humanPiece) {
            incrementScore(&scores.wins);
            incrementScore(&scores.streak);
            if (!addUserGoldWon(CHECKERS_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_CHECKERS, &scores);
            if (!scoreSaveFailed) scoreStatus = SCORES_OK;
        } else scoreSaveFailed = true;
    }
    ESP_LOGI("checkers", "%s; score save %s; Gold %s", boardMessage,
             scoreSaveFailed ? "ERROR" : autoMode ? "NOT REQUIRED" : "OK",
             goldSaveFailed ? "ERROR" : "OK");
}
finishTurn() · source line 308
static void finishTurn(void)

Calls into this documentation:

  1. DecisionnoProgressMoves >= NO_PROGRESS_DRAW_LIMIT
    True
      1. ExecutefinishGame(EMPTY);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst uint8_t nextPlayer = opponent(currentPlayer);
  3. DecisioncountPieces(nextPlayer) == 0
    True
      1. ExecutefinishGame(currentPlayer);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  4. ExecutecurrentPlayer = nextPlayer;
  5. ExecuteforcedFrom = NO_SQUARE;
  6. ExecuterefreshLegalMoves();
  7. DecisionlegalMoveCount == 0
    True
      1. ExecutefinishGame(opponent(currentPlayer));
      2. Exit this functionreturn;
    False

    Continue after this decision.

  8. ExecuteboardMessage = autoMode || currentPlayer != humanPiece ? "CPU TURN" : "YOUR TURN";
  9. ExecutecomputerMoveDue = clockNow + aiSpeedDelay(aiSpeedIndex);
Read original function source
static void finishTurn(void)
{
    if (noProgressMoves >= NO_PROGRESS_DRAW_LIMIT) { finishGame(EMPTY); return; }
    const uint8_t nextPlayer = opponent(currentPlayer);
    if (countPieces(nextPlayer) == 0) { finishGame(currentPlayer); return; }
    currentPlayer = nextPlayer;
    forcedFrom = NO_SQUARE;
    refreshLegalMoves();
    if (legalMoveCount == 0) { finishGame(opponent(currentPlayer)); return; }
    boardMessage = autoMode || currentPlayer != humanPiece ? "CPU TURN" : "YOUR TURN";
    computerMoveDue = clockNow + aiSpeedDelay(aiSpeedIndex);
}
performMove() · source line 321
static void performMove(uint8_t index)

Calls into this documentation:

  1. Decisionindex >= legalMoveCount
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst Move move = legalMoves[index];
  3. Executeconst bool promoted = applyMove(move);
  4. ExecuteaudioPlay(move.capture ? AUDIO_SFX_BOARD_CAPTURE : AUDIO_SFX_BOARD_PLACE);
  5. Decisionmove.capture && !promoted
    True
      1. ExecuteforcedFrom = move.to;
      2. ExecuterefreshLegalMoves();
      3. DecisionlegalMoveCount > 0
        True
          1. ExecuteboardMessage = "JUMP AGAIN";
          2. ExecutecomputerMoveDue = clockNow + aiSpeedDelay(aiSpeedIndex);
          3. Exit this functionreturn;
        False

        Continue after this decision.

    False

    Continue after this decision.

  6. ExecutefinishTurn();
Read original function source
static void performMove(uint8_t index)
{
    if (index >= legalMoveCount) return;
    const Move move = legalMoves[index];
    const bool promoted = applyMove(move);
    audioPlay(move.capture ? AUDIO_SFX_BOARD_CAPTURE : AUDIO_SFX_BOARD_PLACE);
    if (move.capture && !promoted) {
        forcedFrom = move.to;
        refreshLegalMoves();
        if (legalMoveCount > 0) {
            boardMessage = "JUMP AGAIN";
            computerMoveDue = clockNow + aiSpeedDelay(aiSpeedIndex);
            return;
        }
    }
    finishTurn();
}
chooseComputerMove() · source line 339
static uint8_t chooseComputerMove(void)

Calls into this documentation:

  1. Executeuint8_t best = 0;
  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 Move move = legalMoves[i];
      2. Executeconst bool king = isKing(move.from);
      3. Executeconst uint8_t row = move.to / 8, column = move.to % 8;
      4. Executeint score = (int)(esp_random() % 12);
      5. Decisionmove.capture
        True
        1. Executescore += 100;
        False

        Continue after this decision.

      6. Decision!king && ((currentPlayer == BLACK_PIECE && row == 7) || (currentPlayer == WHITE_PIECE && row == 0))
        True
        1. Executescore += 35;
        False

        Continue after this decision.

      7. Decisionking
        True
        1. Executescore += 8;
        False

        Continue after this decision.

      8. Decisioncolumn >= 2 && column <= 5
        True
        1. Executescore += 5;
        False

        Continue after this decision.

      9. Decisioncolumn == 0 || column == 7
        True
        1. Executescore += 3;
        False

        Continue after this decision.

      10. Decisionscore > bestScore
        True
          1. ExecutebestScore = score;
          2. Executebest = i;
        False

        Continue after this decision.

  4. Exit this functionreturn best;
Read original function source
static uint8_t chooseComputerMove(void)
{
    uint8_t best = 0;
    int bestScore = -10000;
    for (uint8_t i = 0; i < legalMoveCount; ++i) {
        const Move move = legalMoves[i];
        const bool king = isKing(move.from);
        const uint8_t row = move.to / 8, column = move.to % 8;
        int score = (int)(esp_random() % 12);
        if (move.capture) score += 100;
        if (!king && ((currentPlayer == BLACK_PIECE && row == 7) ||
                      (currentPlayer == WHITE_PIECE && row == 0))) score += 35;
        if (king) score += 8;
        if (column >= 2 && column <= 5) score += 5;
        if (column == 0 || column == 7) score += 3;
        if (score > bestScore) { bestScore = score; best = i; }
    }
    return best;
}
choosePiece() · source line 359
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 368
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 + aiSpeedDelay(aiSpeedIndex);
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 + aiSpeedDelay(aiSpeedIndex);
}
startCheckers() · source line 378
void startCheckers(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_CHECKERS, &scores);
  6. ExecuteaiSpeedIndex = AI_SPEED_DEFAULT_INDEX;
  7. ExecuteresetBoard();
  8. ExecutechoosePiece();
  9. ExecutegameState = STATE_SHOW_SCORES;
  10. ExecuteESP_LOGI("checkers", "START: %s", game_scores_status_text(scoreStatus));
Read original function source
void startCheckers(uint32_t now_ms)
{
    clockNow = stateStarted = now_ms;
    running = true;
    scoreSaveFailed = goldSaveFailed = false;
    scores = (game_scores_t){0};
    scoreStatus = game_scores_load(GAME_CHECKERS, &scores);
    aiSpeedIndex = AI_SPEED_DEFAULT_INDEX;
    resetBoard();
    choosePiece();
    gameState = STATE_SHOW_SCORES;
    ESP_LOGI("checkers", "START: %s", game_scores_status_text(scoreStatus));
}
stopCheckers() · source line 392
void stopCheckers(void)
  1. Executerunning = false;
  2. ExecuteESP_LOGI("checkers", "STOP");
Read original function source
void stopCheckers(void)
{
    running = false;
    ESP_LOGI("checkers", "STOP");
}
checkersAction() · source line 398
void checkersAction(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 checkersAction(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);
}
updateCheckers() · source line 422
void updateCheckers(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. ExecutefinishGame(opponent(currentPlayer));
            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 updateCheckers(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) finishGame(opponent(currentPlayer));
            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 471
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);
  5. DecisionisKing(square)
    True
      1. Executedraw_ring(frame, x, y, 4, 0xfea0);
      2. Executedraw_ring(frame, x, y, 5, 0xfea0);
    False

    Continue after this decision.

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);
    if (isKing(square)) {
        draw_ring(frame, x, y, 4, 0xfea0);
        draw_ring(frame, x, y, 5, 0xfea0);
    }
}
drawCheckers() · source line 483
void drawCheckers(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, "CHECKERS", 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. Executeconst bool selected = gameState == STATE_PLAYING && legalMoveCount > 0 && square == legalMoves[selectedMove].from;
      3. Executefill_rect(frame, BOARD_X + column * 20, BOARD_Y + row * 20, 20, 20, selected ? 0xffe0 : ((row + column) & 1) ? 0x4208 : 0xad55);
      4. Decisionboard.black & squareMask(square)
        True
        1. ExecutedrawPiece(frame, square, BLACK_PIECE);
        False
        1. Decisionboard.white & squareMask(square)
          True
          1. ExecutedrawPiece(frame, square, WHITE_PIECE);
          False

          Continue after this decision.

  6. DecisiongameState == STATE_PLAYING && legalMoveCount > 0
    True
      1. Executeconst Move move = legalMoves[selectedMove];
      2. Executedraw_rect(frame, BOARD_X + (move.to % 8) * 20 + 2, BOARD_Y + (move.to / 8) * 20 + 2, 16, 16, 0x07e0);
    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 Move move = legalMoves[selectedMove];
      2. Executesnprintf(line, sizeof(line), "%c%u / %c%u", 'A' + move.from % 8, (unsigned)(move.from / 8 + 1), 'A' + move.to % 8, (unsigned)(move.to / 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 drawCheckers(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, "CHECKERS", 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;
        const bool selected = gameState == STATE_PLAYING && legalMoveCount > 0 && square == legalMoves[selectedMove].from;
        fill_rect(frame, BOARD_X + column * 20, BOARD_Y + row * 20, 20, 20,
                     selected ? 0xffe0 : ((row + column) & 1) ? 0x4208 : 0xad55);
        if (board.black & squareMask(square)) drawPiece(frame, square, BLACK_PIECE);
        else if (board.white & squareMask(square)) drawPiece(frame, square, WHITE_PIECE);
    }
    if (gameState == STATE_PLAYING && legalMoveCount > 0) {
        const Move move = legalMoves[selectedMove];
        draw_rect(frame, BOARD_X + (move.to % 8) * 20 + 2, BOARD_Y + (move.to / 8) * 20 + 2, 16, 16, 0x07e0);
    }
    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 Move move = legalMoves[selectedMove];
        snprintf(line, sizeof(line), "%c%u / %c%u", 'A' + move.from % 8, (unsigned)(move.from / 8 + 1),
                 'A' + move.to % 8, (unsigned)(move.to / 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 "checkers.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 ported from the other LilyGO project's checkers.cpp.
#define MAX_MOVES 64
#define NO_SQUARE 255
#define NO_PROGRESS_DRAW_LIMIT 80
#define CHECKERS_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;
typedef struct { uint64_t black, white, kings; } Board;
typedef struct { uint8_t from, to, captured; bool capture; } Move;
static Board board;
static game_scores_t scores;
static game_score_result_t scoreStatus;
static Move legalMoves[MAX_MOVES];
static GameState gameState;
static uint8_t legalMoveCount, selectedMove, forcedFrom;
static uint8_t humanPiece, currentPlayer, goldEarningPiece, noProgressMoves, aiSpeedIndex;
static uint32_t stateStarted, computerMoveDue, clockNow, lastStickMove;
static bool running, autoMode, stickCentred, repeating, scoreSaveFailed, goldSaveFailed;
static int heldDirection;
static const char *boardMessage;

static uint64_t squareMask(uint8_t square)
{
    return 1ULL << square;
}

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

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

static uint64_t pieceMask(uint8_t piece)
{
    return piece == BLACK_PIECE
        ? board.black
        : board.white;
}

static bool occupied(uint8_t square)
{
    return (board.black | board.white) & squareMask(square);
}

static bool isKing(uint8_t square)
{
    return board.kings & squareMask(square);
}

static uint8_t countBits(uint64_t value)
{
    uint8_t count = 0;

    while (value)
    {
        count += value & 1ULL;
        value >>= 1;
    }

    return count;
}

static uint8_t countPieces(uint8_t piece)
{
    return countBits(pieceMask(piece));
}

static bool directionAllowed(uint8_t piece,bool king,int8_t rowDirection)
{
    if (king)
        return true;

    return piece == BLACK_PIECE
        ? rowDirection > 0
        : rowDirection < 0;
}

static uint8_t generateMoves(
    uint8_t piece,
    Move *moves,
    uint8_t onlyFrom)
{
    uint8_t count = 0;
    bool captureAvailable = false;
    const uint64_t ownPieces = pieceMask(piece);
    const uint64_t otherPieces = pieceMask(opponent(piece));

    for (uint8_t from = 0; from < 64; from++)
    {
        if (!(ownPieces & squareMask(from)) ||
            (onlyFrom != NO_SQUARE && from != onlyFrom))
            continue;

        const int8_t row = from / 8;
        const int8_t column = from % 8;
        const bool king = isKing(from);

        for (int8_t rowDirection = -1;
            rowDirection <= 1;
            rowDirection += 2)
        {
            if (!directionAllowed(piece,king,rowDirection))
                continue;

            for (int8_t columnDirection = -1;
                columnDirection <= 1;
                columnDirection += 2)
            {
                const int8_t middleRow = row + rowDirection;
                const int8_t middleColumn = column + columnDirection;
                const int8_t toRow = row + rowDirection * 2;
                const int8_t toColumn = column + columnDirection * 2;

                if (!insideBoard(middleRow,middleColumn) ||
                    !insideBoard(toRow,toColumn))
                    continue;

                const uint8_t middle = middleRow * 8 + middleColumn;
                const uint8_t to = toRow * 8 + toColumn;

                if ((otherPieces & squareMask(middle)) &&
                    !occupied(to))
                {
                    captureAvailable = true;

                    if (count < MAX_MOVES)
                        moves[count++] = (Move){from,to,middle,true};
                }
            }
        }
    }

    if (captureAvailable || onlyFrom != NO_SQUARE)
        return count;

    for (uint8_t from = 0; from < 64; from++)
    {
        if (!(ownPieces & squareMask(from)))
            continue;

        const int8_t row = from / 8;
        const int8_t column = from % 8;
        const bool king = isKing(from);

        for (int8_t rowDirection = -1;
            rowDirection <= 1;
            rowDirection += 2)
        {
            if (!directionAllowed(piece,king,rowDirection))
                continue;

            for (int8_t columnDirection = -1;
                columnDirection <= 1;
                columnDirection += 2)
            {
                const int8_t toRow = row + rowDirection;
                const int8_t toColumn = column + columnDirection;

                if (!insideBoard(toRow,toColumn))
                    continue;

                const uint8_t to = toRow * 8 + toColumn;

                if (!occupied(to) && count < MAX_MOVES)
                    moves[count++] = (Move){from,to,NO_SQUARE,false};
            }
        }
    }

    return count;
}

static void resetBoard(void)
{
    board = (Board){0};

    for (uint8_t row = 0; row < 3; row++)
    {
        for (uint8_t column = 0; column < 8; column++)
        {
            if ((row + column) % 2 == 1)
                board.black |= squareMask(row * 8 + column);
        }
    }

    for (uint8_t row = 5; row < 8; row++)
    {
        for (uint8_t column = 0; column < 8; column++)
        {
            if ((row + column) % 2 == 1)
                board.white |= squareMask(row * 8 + column);
        }
    }

    currentPlayer = BLACK_PIECE;
    forcedFrom = NO_SQUARE;
    selectedMove = 0;
    noProgressMoves = 0;
}

static bool applyMove(const Move move)
{
    const uint64_t fromMask = squareMask(move.from);
    const uint64_t toMask = squareMask(move.to);
    const bool king = board.kings & fromMask;

    if (currentPlayer == BLACK_PIECE)
    {
        board.black &= ~fromMask;
        board.black |= toMask;
    }
    else
    {
        board.white &= ~fromMask;
        board.white |= toMask;
    }

    board.kings &= ~fromMask;

    if (king)
        board.kings |= toMask;

    if (move.capture)
    {
        const uint64_t capturedMask = squareMask(move.captured);
        board.black &= ~capturedMask;
        board.white &= ~capturedMask;
        board.kings &= ~capturedMask;
    }

    const uint8_t destinationRow = move.to / 8;
    const bool promoted = !king &&
        ((currentPlayer == BLACK_PIECE && destinationRow == 7) ||
         (currentPlayer == WHITE_PIECE && destinationRow == 0));

    if (promoted)
        board.kings |= toMask;

    if (move.capture || promoted)
        noProgressMoves = 0;
    else if (noProgressMoves < UINT8_MAX)
        noProgressMoves++;

    return promoted;
}

static void refreshLegalMoves(void)
{
    legalMoveCount = generateMoves(currentPlayer, legalMoves, forcedFrom);
    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(CHECKERS_WIN_GOLD)) goldSaveFailed = true;
    } else {
        if (winner == humanPiece) {
            incrementScore(&scores.wins);
            incrementScore(&scores.streak);
            if (!addUserGoldWon(CHECKERS_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_CHECKERS, &scores);
            if (!scoreSaveFailed) scoreStatus = SCORES_OK;
        } else scoreSaveFailed = true;
    }
    ESP_LOGI("checkers", "%s; score save %s; Gold %s", boardMessage,
             scoreSaveFailed ? "ERROR" : autoMode ? "NOT REQUIRED" : "OK",
             goldSaveFailed ? "ERROR" : "OK");
}

static void finishTurn(void)
{
    if (noProgressMoves >= NO_PROGRESS_DRAW_LIMIT) { finishGame(EMPTY); return; }
    const uint8_t nextPlayer = opponent(currentPlayer);
    if (countPieces(nextPlayer) == 0) { finishGame(currentPlayer); return; }
    currentPlayer = nextPlayer;
    forcedFrom = NO_SQUARE;
    refreshLegalMoves();
    if (legalMoveCount == 0) { finishGame(opponent(currentPlayer)); return; }
    boardMessage = autoMode || currentPlayer != humanPiece ? "CPU TURN" : "YOUR TURN";
    computerMoveDue = clockNow + aiSpeedDelay(aiSpeedIndex);
}

static void performMove(uint8_t index)
{
    if (index >= legalMoveCount) return;
    const Move move = legalMoves[index];
    const bool promoted = applyMove(move);
    audioPlay(move.capture ? AUDIO_SFX_BOARD_CAPTURE : AUDIO_SFX_BOARD_PLACE);
    if (move.capture && !promoted) {
        forcedFrom = move.to;
        refreshLegalMoves();
        if (legalMoveCount > 0) {
            boardMessage = "JUMP AGAIN";
            computerMoveDue = clockNow + aiSpeedDelay(aiSpeedIndex);
            return;
        }
    }
    finishTurn();
}

static uint8_t chooseComputerMove(void)
{
    uint8_t best = 0;
    int bestScore = -10000;
    for (uint8_t i = 0; i < legalMoveCount; ++i) {
        const Move move = legalMoves[i];
        const bool king = isKing(move.from);
        const uint8_t row = move.to / 8, column = move.to % 8;
        int score = (int)(esp_random() % 12);
        if (move.capture) score += 100;
        if (!king && ((currentPlayer == BLACK_PIECE && row == 7) ||
                      (currentPlayer == WHITE_PIECE && row == 0))) score += 35;
        if (king) score += 8;
        if (column >= 2 && column <= 5) score += 5;
        if (column == 0 || column == 7) score += 3;
        if (score > bestScore) { bestScore = score; best = i; }
    }
    return best;
}

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 + aiSpeedDelay(aiSpeedIndex);
}

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

void stopCheckers(void)
{
    running = false;
    ESP_LOGI("checkers", "STOP");
}

void checkersAction(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 updateCheckers(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) finishGame(opponent(currentPlayer));
            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);
    if (isKing(square)) {
        draw_ring(frame, x, y, 4, 0xfea0);
        draw_ring(frame, x, y, 5, 0xfea0);
    }
}

void drawCheckers(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, "CHECKERS", 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;
        const bool selected = gameState == STATE_PLAYING && legalMoveCount > 0 && square == legalMoves[selectedMove].from;
        fill_rect(frame, BOARD_X + column * 20, BOARD_Y + row * 20, 20, 20,
                     selected ? 0xffe0 : ((row + column) & 1) ? 0x4208 : 0xad55);
        if (board.black & squareMask(square)) drawPiece(frame, square, BLACK_PIECE);
        else if (board.white & squareMask(square)) drawPiece(frame, square, WHITE_PIECE);
    }
    if (gameState == STATE_PLAYING && legalMoveCount > 0) {
        const Move move = legalMoves[selectedMove];
        draw_rect(frame, BOARD_X + (move.to % 8) * 20 + 2, BOARD_Y + (move.to / 8) * 20 + 2, 16, 16, 0x07e0);
    }
    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 Move move = legalMoves[selectedMove];
        snprintf(line, sizeof(line), "%c%u / %c%u", 'A' + move.from % 8, (unsigned)(move.from / 8 + 1),
                 'A' + move.to % 8, (unsigned)(move.to / 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);
}