Battleships

Source: src/battleships.c · 429 lines · 34 functions.

Screen captures

Views of this part running on the Mini Arcade.

Battleships game screen on the Mini Arcade
Battleships showing its game board and status display.

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

inside() · source line 45
static bool inside(int row, int column)
  1. Exit this functionreturn row >= 0 && row < GRID_SIZE && column >= 0 && column < GRID_SIZE;
Read original function source
static bool inside(int row, int column) { return row >= 0 && row < GRID_SIZE && column >= 0 && column < GRID_SIZE; }
say() · source line 46
static void say(const char *text)
  1. Executesnprintf(message, sizeof(message), "%s", text);
Read original function source
static void say(const char *text) { snprintf(message, sizeof(message), "%s", text); }
reset_navigation() · source line 47
static void reset_navigation(void)
  1. Executestick_centred = false;
  2. Executeheld_direction = 0;
  3. Executerepeating = false;
Read original function source
static void reset_navigation(void) { stick_centred = false; held_direction = 0; repeating = false; }
clear_board() · source line 49
static void clear_board(board_t *board)
  1. FOR loopint row = 0; row < GRID_SIZE; ++row

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

    1. FOR loopint column = 0; column < GRID_SIZE; ++column

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

      1. Executeboard->cells[row][column] = (cell_t){NO_SHIP, SHOT_UNKNOWN};
  2. FOR loopint ship = 0; ship < SHIP_COUNT; ++ship

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

    1. Executeboard->ships[ship] = (ship_t){ship_lengths[ship], 0, false};
Read original function source
static void clear_board(board_t *board)
{
    for (int row = 0; row < GRID_SIZE; ++row)
        for (int column = 0; column < GRID_SIZE; ++column)
            board->cells[row][column] = (cell_t){NO_SHIP, SHOT_UNKNOWN};
    for (int ship = 0; ship < SHIP_COUNT; ++ship)
        board->ships[ship] = (ship_t){ship_lengths[ship], 0, false};
}
clear_game() · source line 58
static void clear_game(void)

Calls into this documentation:

  1. Executeclear_board(&player_board);
  2. Executeclear_board(&enemy_board);
  3. Executeplayer_ai.count = enemy_ai.count = 0;
  4. Executeselected_row = selected_column = 0;
  5. Executeplacing_ship = SHIP_COUNT - 1;
  6. Executehorizontal = true;
  7. Executeplayer_turn = true;
  8. Executereset_navigation();
Read original function source
static void clear_game(void)
{
    clear_board(&player_board); clear_board(&enemy_board);
    player_ai.count = enemy_ai.count = 0;
    selected_row = selected_column = 0;
    placing_ship = SHIP_COUNT - 1; horizontal = true; player_turn = true;
    reset_navigation();
}
choose_mode() · source line 67
static void choose_mode(void)

Calls into this documentation:

  1. Executegame_state = STATE_CHOOSE_MODE;
  2. Executeauto_mode = false;
  3. Executerandom_placement = false;
  4. Executefleet_error = false;
  5. Executesay("");
  6. Executereset_navigation();
Read original function source
static void choose_mode(void)
{
    game_state = STATE_CHOOSE_MODE; auto_mode = false; random_placement = false;
    fleet_error = false; say(""); reset_navigation();
}
can_place() · source line 73
static bool can_place(const board_t *board, int row, int column, uint8_t length, bool is_horizontal)

Calls into this documentation:

  1. Executeconst int end_row = row + (is_horizontal ? 0 : length - 1);
  2. Executeconst int end_column = column + (is_horizontal ? length - 1 : 0);
  3. Decision!inside(row, column) || !inside(end_row, end_column)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  4. FOR loopint part = 0; part < length; ++part

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

      1. Executeconst int ship_row = row + (is_horizontal ? 0 : part);
      2. Executeconst int ship_column = column + (is_horizontal ? part : 0);
      3. FOR loopint row_step = -1; row_step <= 1; ++row_step

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

        1. FOR loopint column_step = -1; column_step <= 1; ++column_step

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

            1. Executeconst int near_row = ship_row + row_step, near_column = ship_column + column_step;
            2. Decisioninside(near_row, near_column) && board->cells[near_row][near_column].ship_index != NO_SHIP
              True
              1. Exit this functionreturn false;
              False

              Continue after this decision.

  5. Exit this functionreturn true;
Read original function source
static bool can_place(const board_t *board, int row, int column, uint8_t length, bool is_horizontal)
{
    const int end_row = row + (is_horizontal ? 0 : length - 1);
    const int end_column = column + (is_horizontal ? length - 1 : 0);
    if (!inside(row, column) || !inside(end_row, end_column)) return false;
    for (int part = 0; part < length; ++part) {
        const int ship_row = row + (is_horizontal ? 0 : part);
        const int ship_column = column + (is_horizontal ? part : 0);
        for (int row_step = -1; row_step <= 1; ++row_step)
            for (int column_step = -1; column_step <= 1; ++column_step) {
                const int near_row = ship_row + row_step, near_column = ship_column + column_step;
                if (inside(near_row, near_column) && board->cells[near_row][near_column].ship_index != NO_SHIP) return false;
            }
    }
    return true;
}
place_ship() · source line 90
static void place_ship(board_t *board, uint8_t ship_index, uint8_t row, uint8_t column, bool is_horizontal)
  1. Executeship_t *ship = &board->ships[ship_index];
  2. FOR loopint part = 0; part < ship->length; ++part

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

    1. Executeboard->cells[row + (is_horizontal ? 0 : part)][column + (is_horizontal ? part : 0)].ship_index = ship_index;
  3. Executeship->placed = true;
  4. Executeship->hits = 0;
Read original function source
static void place_ship(board_t *board, uint8_t ship_index, uint8_t row, uint8_t column, bool is_horizontal)
{
    ship_t *ship = &board->ships[ship_index];
    for (int part = 0; part < ship->length; ++part)
        board->cells[row + (is_horizontal ? 0 : part)][column + (is_horizontal ? part : 0)].ship_index = ship_index;
    ship->placed = true; ship->hits = 0;
}
place_random_fleet() · source line 98
static bool place_random_fleet(board_t *board)

Calls into this documentation:

  1. FOR loopint fleet_try = 0; fleet_try < 100; ++fleet_try

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

      1. Executeclear_board(board);
      2. Executebool complete = true;
      3. FOR loopint ship = SHIP_COUNT - 1; ship >= 0; --ship

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

          1. Executebool placed = false;
          2. FOR loopint try = 0; try < 250; ++try

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

              1. Executeconst bool is_horizontal = esp_random() % 2U == 0;
              2. Executeconst uint8_t row = esp_random() % GRID_SIZE, column = esp_random() % GRID_SIZE;
              3. Decisioncan_place(board, row, column, board->ships[ship].length, is_horizontal)
                True
                  1. Executeplace_ship(board, ship, row, column, is_horizontal);
                  2. Executeplaced = true;
                  3. Exit enclosing loop or switchbreak;
                False

                Continue after this decision.

          3. Decision!placed
            True
              1. Executecomplete = false;
              2. Exit enclosing loop or switchbreak;
            False

            Continue after this decision.

      4. Decisioncomplete
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool place_random_fleet(board_t *board)
{
    for (int fleet_try = 0; fleet_try < 100; ++fleet_try) {
        clear_board(board); bool complete = true;
        for (int ship = SHIP_COUNT - 1; ship >= 0; --ship) {
            bool placed = false;
            for (int try = 0; try < 250; ++try) {
                const bool is_horizontal = esp_random() % 2U == 0;
                const uint8_t row = esp_random() % GRID_SIZE, column = esp_random() % GRID_SIZE;
                if (can_place(board, row, column, board->ships[ship].length, is_horizontal)) {
                    place_ship(board, ship, row, column, is_horizontal); placed = true; break;
                }
            }
            if (!placed) { complete = false; break; }
        }
        if (complete) return true;
    }
    return false;
}
all_sunk() · source line 118
static bool all_sunk(const board_t *board)
  1. FOR loopint ship = 0; ship < SHIP_COUNT; ++ship

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

    1. Decision!board->ships[ship].placed || board->ships[ship].hits < board->ships[ship].length
      True
      1. Exit this functionreturn false;
      False

      Continue after this decision.

  2. Exit this functionreturn true;
Read original function source
static bool all_sunk(const board_t *board)
{
    for (int ship = 0; ship < SHIP_COUNT; ++ship)
        if (!board->ships[ship].placed || board->ships[ship].hits < board->ships[ship].length) return false;
    return true;
}
fire_at() · source line 125
static shot_result_t fire_at(board_t *board, uint8_t row, uint8_t column)
  1. Executecell_t *cell = &board->cells[row][column];
  2. Decisioncell->shot != SHOT_UNKNOWN
    True
    1. Exit this functionreturn RESULT_INVALID;
    False

    Continue after this decision.

  3. Decisioncell->ship_index == NO_SHIP
    True
      1. Executecell->shot = SHOT_MISS;
      2. Exit this functionreturn RESULT_MISS;
    False

    Continue after this decision.

  4. Executecell->shot = SHOT_HIT;
  5. Executeship_t *ship = &board->ships[cell->ship_index];
  6. Execute++ship->hits;
  7. Exit this functionreturn ship->hits >= ship->length ? RESULT_SUNK : RESULT_HIT;
Read original function source
static shot_result_t fire_at(board_t *board, uint8_t row, uint8_t column)
{
    cell_t *cell = &board->cells[row][column];
    if (cell->shot != SHOT_UNKNOWN) return RESULT_INVALID;
    if (cell->ship_index == NO_SHIP) { cell->shot = SHOT_MISS; return RESULT_MISS; }
    cell->shot = SHOT_HIT;
    ship_t *ship = &board->ships[cell->ship_index];
    ++ship->hits;
    return ship->hits >= ship->length ? RESULT_SUNK : RESULT_HIT;
}
move_cell() · source line 136
static void move_cell(int direction, bool unknown_only)
  1. Executeint position = selected_row * GRID_SIZE + selected_column;
  2. FOR loopint count = 0; count < 100; ++count

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

      1. Executeposition = (position + direction + 100) % 100;
      2. Executeconst int row = position / GRID_SIZE, column = position % GRID_SIZE;
      3. Decision!unknown_only || enemy_board.cells[row][column].shot == SHOT_UNKNOWN
        True
          1. Executeselected_row = row;
          2. Executeselected_column = column;
          3. Exit this functionreturn;
        False

        Continue after this decision.

Read original function source
static void move_cell(int direction, bool unknown_only)
{
    int position = selected_row * GRID_SIZE + selected_column;
    for (int count = 0; count < 100; ++count) {
        position = (position + direction + 100) % 100;
        const int row = position / GRID_SIZE, column = position % GRID_SIZE;
        if (!unknown_only || enemy_board.cells[row][column].shot == SHOT_UNKNOWN) {
            selected_row = row; selected_column = column; return;
        }
    }
}
queued() · source line 148
static bool queued(const ai_state_t *ai, uint8_t row, uint8_t column)
  1. FOR loopint index = 0; index < ai->count; ++index

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

    1. Decisionai->rows[index] == row && ai->columns[index] == column
      True
      1. Exit this functionreturn true;
      False

      Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool queued(const ai_state_t *ai, uint8_t row, uint8_t column)
{
    for (int index = 0; index < ai->count; ++index)
        if (ai->rows[index] == row && ai->columns[index] == column) return true;
    return false;
}
queue_target() · source line 155
static void queue_target(ai_state_t *ai, const board_t *board, int row, int column)

Calls into this documentation:

  1. Decision!inside(row, column) || board->cells[row][column].shot != SHOT_UNKNOWN || queued(ai, row, column) || ai->count >= 100
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeai->rows[ai->count] = row;
  3. Executeai->columns[ai->count] = column;
  4. Execute++ai->count;
Read original function source
static void queue_target(ai_state_t *ai, const board_t *board, int row, int column)
{
    if (!inside(row, column) || board->cells[row][column].shot != SHOT_UNKNOWN || queued(ai, row, column) || ai->count >= 100) return;
    ai->rows[ai->count] = row; ai->columns[ai->count] = column; ++ai->count;
}
remember_hit() · source line 161
static void remember_hit(ai_state_t *ai, const board_t *board, uint8_t row, uint8_t column, shot_result_t result)

Calls into this documentation:

  1. Decisionresult == RESULT_SUNK
    True
      1. Executeai->count = 0;
      2. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionresult != RESULT_HIT
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executequeue_target(ai, board, row - 1, column);
  4. Executequeue_target(ai, board, row + 1, column);
  5. Executequeue_target(ai, board, row, column - 1);
  6. Executequeue_target(ai, board, row, column + 1);
Read original function source
static void remember_hit(ai_state_t *ai, const board_t *board, uint8_t row, uint8_t column, shot_result_t result)
{
    if (result == RESULT_SUNK) { ai->count = 0; return; }
    if (result != RESULT_HIT) return;
    queue_target(ai, board, row - 1, column); queue_target(ai, board, row + 1, column);
    queue_target(ai, board, row, column - 1); queue_target(ai, board, row, column + 1);
}
choose_ai_target() · source line 169
static bool choose_ai_target(ai_state_t *ai, const board_t *board, uint8_t *row, uint8_t *column)
  1. WHILE loopai->count > 0

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

      1. Execute--ai->count;
      2. Execute*row = ai->rows[ai->count];
      3. Execute*column = ai->columns[ai->count];
      4. Decisionboard->cells[*row][*column].shot == SHOT_UNKNOWN
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

  2. Executeunsigned available = 0;
  3. FOR loopint r = 0; r < GRID_SIZE; ++r

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

    1. FOR loopint c = 0; c < GRID_SIZE; ++c

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

      1. Decisionboard->cells[r][c].shot == SHOT_UNKNOWN
        True
        1. Execute++available;
        False

        Continue after this decision.

  4. Decisionavailable == 0
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  5. Executeunsigned selection = esp_random() % available;
  6. FOR loopint r = 0; r < GRID_SIZE; ++r

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

    1. FOR loopint c = 0; c < GRID_SIZE; ++c

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

      1. Decisionboard->cells[r][c].shot == SHOT_UNKNOWN && selection-- == 0
        True
          1. Execute*row = r;
          2. Execute*column = c;
          3. Exit this functionreturn true;
        False

        Continue after this decision.

  7. Exit this functionreturn false;
Read original function source
static bool choose_ai_target(ai_state_t *ai, const board_t *board, uint8_t *row, uint8_t *column)
{
    while (ai->count > 0) {
        --ai->count; *row = ai->rows[ai->count]; *column = ai->columns[ai->count];
        if (board->cells[*row][*column].shot == SHOT_UNKNOWN) return true;
    }
    unsigned available = 0;
    for (int r = 0; r < GRID_SIZE; ++r) for (int c = 0; c < GRID_SIZE; ++c)
        if (board->cells[r][c].shot == SHOT_UNKNOWN) ++available;
    if (available == 0) return false;
    unsigned selection = esp_random() % available;
    for (int r = 0; r < GRID_SIZE; ++r) for (int c = 0; c < GRID_SIZE; ++c)
        if (board->cells[r][c].shot == SHOT_UNKNOWN && selection-- == 0) { *row = r; *column = c; return true; }
    return false;
}
increment() · source line 185
static void increment(uint32_t *value)
  1. Decision*value < UINT32_MAX
    True
    1. Execute++*value;
    False

    Continue after this decision.

Read original function source
static void increment(uint32_t *value) { if (*value < UINT32_MAX) ++*value; }
finish_game() · source line 187
static void finish_game(bool player_won)

Calls into this documentation:

  1. Executegame_state = STATE_FINISHED;
  2. Executestate_started = now;
  3. Executereset_navigation();
  4. Decisionauto_mode
    True
      1. Executesay(player_won ? "LEFT WINS" : "RIGHT WINS");
      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisionplayer_won
    True
      1. Executeincrement(&scores.wins);
      2. Executeincrement(&scores.streak);
      3. Decision!addUserGoldWon(BATTLESHIPS_WIN_GOLD)
        True
        1. Executegold_save_failed = true;
        False

        Continue after this decision.

      4. Executesay("YOU WIN");
    False
      1. Executeincrement(&scores.losses);
      2. Executescores.streak = 0;
      3. Executesay("ESP WINS");
  6. Decisionscore_status == SCORES_OK || score_status == SCORES_NOT_FOUND
    True
      1. Executescore_save_failed = !game_scores_save(GAME_BATTLESHIPS, &scores);
      2. Decision!score_save_failed
        True
        1. Executescore_status = SCORES_OK;
        False

        Continue after this decision.

    False
    1. Executescore_save_failed = true;
Read original function source
static void finish_game(bool player_won)
{
    game_state = STATE_FINISHED; state_started = now; reset_navigation();
    if (auto_mode) { say(player_won ? "LEFT WINS" : "RIGHT WINS"); return; }
    if (player_won) { increment(&scores.wins); increment(&scores.streak); if (!addUserGoldWon(BATTLESHIPS_WIN_GOLD)) gold_save_failed = true; say("YOU WIN"); }
    else { increment(&scores.losses); scores.streak = 0; say("ESP WINS"); }
    if (score_status == SCORES_OK || score_status == SCORES_NOT_FOUND) {
        score_save_failed = !game_scores_save(GAME_BATTLESHIPS, &scores);
        if (!score_save_failed) score_status = SCORES_OK;
    } else score_save_failed = true;
}
finish_shot() · source line 199
static void finish_shot(const board_t *target, shot_result_t result, uint8_t row, uint8_t column, bool attacker_is_player)

Calls into this documentation:

  1. ExecuteaudioPlay(result == RESULT_MISS ? AUDIO_SFX_BOARD_PLACE : AUDIO_SFX_BOARD_CAPTURE);
  2. Executeconst char *name = auto_mode ? (attacker_is_player ? "LEFT" : "RIGHT") : (attacker_is_player ? "YOU" : "ESP");
  3. Executeconst char *result_text = result == RESULT_SUNK ? "SUNK" : result == RESULT_HIT ? "HIT" : "MISS";
  4. Executesnprintf(message, sizeof(message), "%s %s %c%u", name, result_text, 'A' + column, (unsigned)row + 1U);
  5. Decisionall_sunk(target)
    True
      1. Executefinish_game(attacker_is_player);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. Executeplayer_turn = !attacker_is_player;
  7. Executeai_due = now + aiSpeedDelay(ai_speed);
  8. Executereset_navigation();
  9. Decisionplayer_turn && !auto_mode && enemy_board.cells[selected_row][selected_column].shot != SHOT_UNKNOWN
    True
    1. Executemove_cell(1, true);
    False

    Continue after this decision.

Read original function source
static void finish_shot(const board_t *target, shot_result_t result, uint8_t row, uint8_t column, bool attacker_is_player)
{
    audioPlay(result == RESULT_MISS ? AUDIO_SFX_BOARD_PLACE : AUDIO_SFX_BOARD_CAPTURE);
    const char *name = auto_mode ? (attacker_is_player ? "LEFT" : "RIGHT") : (attacker_is_player ? "YOU" : "ESP");
    const char *result_text = result == RESULT_SUNK ? "SUNK" : result == RESULT_HIT ? "HIT" : "MISS";
    snprintf(message, sizeof(message), "%s %s %c%u", name, result_text, 'A' + column, (unsigned)row + 1U);
    if (all_sunk(target)) { finish_game(attacker_is_player); return; }
    player_turn = !attacker_is_player; ai_due = now + aiSpeedDelay(ai_speed); reset_navigation();
    if (player_turn && !auto_mode && enemy_board.cells[selected_row][selected_column].shot != SHOT_UNKNOWN) move_cell(1, true);
}
perform_ai_shot() · source line 210
static void perform_ai_shot(board_t *target, ai_state_t *ai, bool attacker_is_player)

Calls into this documentation:

  1. Executeuint8_t row, column;
  2. Decision!choose_ai_target(ai, target, &row, &column)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeconst shot_result_t result = fire_at(target, row, column);
  4. Executeremember_hit(ai, target, row, column, result);
  5. Executeselected_row = row;
  6. Executeselected_column = column;
  7. Executefinish_shot(target, result, row, column, attacker_is_player);
Read original function source
static void perform_ai_shot(board_t *target, ai_state_t *ai, bool attacker_is_player)
{
    uint8_t row, column;
    if (!choose_ai_target(ai, target, &row, &column)) return;
    const shot_result_t result = fire_at(target, row, column);
    remember_hit(ai, target, row, column, result); selected_row = row; selected_column = column;
    finish_shot(target, result, row, column, attacker_is_player);
}
start_combat() · source line 219
static void start_combat(void)

Calls into this documentation:

  1. Executeplayer_ai.count = enemy_ai.count = 0;
  2. Executeselected_row = selected_column = 0;
  3. Executeplayer_turn = true;
  4. Executegame_state = STATE_PLAYING;
  5. Executefleet_error = false;
  6. Executesay(auto_mode ? "AUTO BATTLE" : "YOUR SHOT");
  7. Executeai_due = now + aiSpeedDelay(ai_speed);
  8. Executereset_navigation();
Read original function source
static void start_combat(void)
{
    player_ai.count = enemy_ai.count = 0; selected_row = selected_column = 0; player_turn = true;
    game_state = STATE_PLAYING; fleet_error = false; say(auto_mode ? "AUTO BATTLE" : "YOUR SHOT");
    ai_due = now + aiSpeedDelay(ai_speed); reset_navigation();
}
fleet_error_state() · source line 226
static void fleet_error_state(void)

Calls into this documentation:

  1. Executechoose_mode();
  2. Executefleet_error = true;
  3. Executesay("FLEET ERROR - RETRY");
  4. ExecuteESP_LOGE("battleships", "Fleet placement failed");
Read original function source
static void fleet_error_state(void)
{
    choose_mode(); fleet_error = true; say("FLEET ERROR - RETRY");
    ESP_LOGE("battleships", "Fleet placement failed");
}
prepare_random_game() · source line 232
static void prepare_random_game(void)

Calls into this documentation:

  1. Executeclear_game();
  2. Decision!place_random_fleet(&player_board) || !place_random_fleet(&enemy_board)
    True
      1. Executefleet_error_state();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executestart_combat();
Read original function source
static void prepare_random_game(void)
{
    clear_game();
    if (!place_random_fleet(&player_board) || !place_random_fleet(&enemy_board)) { fleet_error_state(); return; }
    start_combat();
}
start_manual_placement() · source line 239
static void start_manual_placement(void)

Calls into this documentation:

  1. Executeclear_game();
  2. Executegame_state = STATE_PLACING_POSITION;
  3. Executesay("SELECT START");
Read original function source
static void start_manual_placement(void)
{
    clear_game(); game_state = STATE_PLACING_POSITION; say("SELECT START");
}
confirm_placement() · source line 244
static void confirm_placement(void)

Calls into this documentation:

  1. Executeconst uint8_t length = player_board.ships[placing_ship].length;
  2. Decision!can_place(&player_board, selected_row, selected_column, length, horizontal)
    True
      1. Executegame_state = STATE_PLACING_POSITION;
      2. Executesay("NO FIT");
      3. Executereset_navigation();
      4. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeplace_ship(&player_board, placing_ship, selected_row, selected_column, horizontal);
  4. ExecuteaudioPlay(AUDIO_SFX_BOARD_PLACE);
  5. Decisionplacing_ship == 0
    True
      1. Decision!place_random_fleet(&enemy_board)
        True
          1. Executefleet_error_state();
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Executestart_combat();
      3. Exit this functionreturn;
    False

    Continue after this decision.

  6. Execute--placing_ship;
  7. Executegame_state = STATE_PLACING_POSITION;
  8. Executesay("SELECT START");
  9. Executereset_navigation();
Read original function source
static void confirm_placement(void)
{
    const uint8_t length = player_board.ships[placing_ship].length;
    if (!can_place(&player_board, selected_row, selected_column, length, horizontal)) {
        game_state = STATE_PLACING_POSITION; say("NO FIT"); reset_navigation(); return;
    }
    place_ship(&player_board, placing_ship, selected_row, selected_column, horizontal);
    audioPlay(AUDIO_SFX_BOARD_PLACE);
    if (placing_ship == 0) {
        if (!place_random_fleet(&enemy_board)) { fleet_error_state(); return; }
        start_combat(); return;
    }
    --placing_ship; game_state = STATE_PLACING_POSITION; say("SELECT START"); reset_navigation();
}
startBattleships() · source line 259
void startBattleships(uint32_t now_ms)

Calls into this documentation:

  1. Executenow = state_started = now_ms;
  2. Executerunning = true;
  3. Executescore_save_failed = gold_save_failed = false;
  4. Executescores = (game_scores_t){0};
  5. Executescore_status = game_scores_load(GAME_BATTLESHIPS, &scores);
  6. Executeai_speed = AI_SPEED_DEFAULT_INDEX;
  7. Executeclear_game();
  8. Executechoose_mode();
  9. Executegame_state = STATE_SHOW_SCORES;
  10. ExecuteESP_LOGI("battleships", "START: %s", game_scores_status_text(score_status));
Read original function source
void startBattleships(uint32_t now_ms)
{
    now = state_started = now_ms; running = true; score_save_failed = gold_save_failed = false;
    scores = (game_scores_t){0}; score_status = game_scores_load(GAME_BATTLESHIPS, &scores);
    ai_speed = AI_SPEED_DEFAULT_INDEX; clear_game(); choose_mode(); game_state = STATE_SHOW_SCORES;
    ESP_LOGI("battleships", "START: %s", game_scores_status_text(score_status));
}
stopBattleships() · source line 267
void stopBattleships(void)
  1. Executerunning = false;
  2. ExecuteESP_LOGI("battleships", "STOP");
Read original function source
void stopBattleships(void) { running = false; ESP_LOGI("battleships", "STOP"); }
battleshipsAction() · source line 269
void battleshipsAction(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. Executenow = now_ms;
  3. Decisionchoose
    True
      1. Executechoose_mode();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decisiongame_state == STATE_SHOW_SCORES
    True
      1. Decisionselect
        True
        1. Executechoose_mode();
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisiongame_state == STATE_CHOOSE_MODE
    True
      1. Decisionselect
        True
          1. Executefleet_error = false;
          2. Decisionauto_mode
            True
            1. Executeprepare_random_game();
            False
              1. Executerandom_placement = false;
              2. Executegame_state = STATE_CHOOSE_PLACEMENT;
              3. Executereset_navigation();
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisiongame_state == STATE_CHOOSE_PLACEMENT
    True
      1. Decisionselect
        True
          1. Decisionrandom_placement
            True
            1. Executeprepare_random_game();
            False
            1. Executestart_manual_placement();
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decisionauto_mode
    True
      1. Decisionspeed
        True
          1. ExecutecycleAISpeed(&ai_speed);
          2. Executeai_due = now + aiSpeedDelay(ai_speed);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  8. Decision!select
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  9. Decisiongame_state == STATE_FINISHED
    True
    1. Executechoose_mode();
    False
    1. Decisiongame_state == STATE_PLACING_POSITION
      True
        1. Executegame_state = STATE_PLACING_ORIENTATION;
        2. Executesay("CHOOSE H/V");
        3. Executereset_navigation();
      False
      1. Decisiongame_state == STATE_PLACING_ORIENTATION
        True
        1. Executeconfirm_placement();
        False
        1. Decisiongame_state == STATE_PLAYING && player_turn
          True
            1. Executeconst shot_result_t result = fire_at(&enemy_board, selected_row, selected_column);
            2. Decisionresult != RESULT_INVALID
              True
              1. Executefinish_shot(&enemy_board, result, selected_row, selected_column, true);
              False

              Continue after this decision.

          False

          Continue after this decision.

Read original function source
void battleshipsAction(bool select, bool speed, bool choose, uint32_t now_ms)
{
    if (!running) return;
    now = now_ms;
    if (choose) { choose_mode(); return; }
    if (game_state == STATE_SHOW_SCORES) { if (select) choose_mode(); return; }
    if (game_state == STATE_CHOOSE_MODE) {
        if (select) { fleet_error = false; if (auto_mode) prepare_random_game(); else { random_placement = false; game_state = STATE_CHOOSE_PLACEMENT; reset_navigation(); } }
        return;
    }
    if (game_state == STATE_CHOOSE_PLACEMENT) { if (select) { if (random_placement) prepare_random_game(); else start_manual_placement(); } return; }
    if (auto_mode) { if (speed) { cycleAISpeed(&ai_speed); ai_due = now + aiSpeedDelay(ai_speed); } return; }
    if (!select) return;
    if (game_state == STATE_FINISHED) choose_mode();
    else if (game_state == STATE_PLACING_POSITION) { game_state = STATE_PLACING_ORIENTATION; say("CHOOSE H/V"); reset_navigation(); }
    else if (game_state == STATE_PLACING_ORIENTATION) confirm_placement();
    else if (game_state == STATE_PLAYING && player_turn) {
        const shot_result_t result = fire_at(&enemy_board, selected_row, selected_column);
        if (result != RESULT_INVALID) finish_shot(&enemy_board, result, selected_row, selected_column, true);
    }
}
updateBattleships() · source line 291
void updateBattleships(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. Executenow = now_ms;
  3. Decisiongame_state == STATE_SHOW_SCORES
    True
      1. Decisionnow - state_started >= SCORE_DISPLAY_MS
        True
        1. Executechoose_mode();
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decisiongame_state == STATE_FINISHED
    True
      1. Decisionauto_mode && now - state_started >= aiRestartDelay()
        True
        1. Executeprepare_random_game();
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisiongame_state == STATE_PLAYING && (auto_mode || !player_turn)
    True
      1. Decision(int32_t)(now - ai_due) >= 0
        True
          1. Decisionplayer_turn
            True
            1. Executeperform_ai_shot(&enemy_board, &player_ai, true);
            False
            1. Executeperform_ai_shot(&player_board, &enemy_ai, false);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

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

    Continue after this decision.

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

    Continue after this decision.

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

        Continue after this decision.

    False
      1. Decision!valid || (ax <= 350 && ay <= 350)
        True
          1. Executeheld_direction = 0;
          2. Executerepeating = false;
          3. Exit this functionreturn;
        False

        Continue after this decision.

      2. Decision!stick_centred
        True
        1. Exit this functionreturn;
        False

        Continue after this decision.

      3. 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.

  11. Decisiondirection == 0 || (held_direction == direction && now - last_move < (repeating ? 150U : 400U))
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  12. Executerepeating = held_direction == direction;
  13. Executeheld_direction = direction;
  14. Executelast_move = now;
  15. Decisiongame_state == STATE_CHOOSE_MODE
    True
    1. Executeauto_mode = !auto_mode;
    False
    1. Decisiongame_state == STATE_CHOOSE_PLACEMENT
      True
      1. Executerandom_placement = !random_placement;
      False
      1. Decisiongame_state == STATE_PLACING_POSITION
        True
        1. Executemove_cell(direction, false);
        False
        1. Decisiongame_state == STATE_PLACING_ORIENTATION
          True
          1. Executehorizontal = !horizontal;
          False
          1. Decisiongame_state == STATE_PLAYING
            True
            1. Executemove_cell(direction, true);
            False

            Continue after this decision.

Read original function source
void updateBattleships(int x_offset, int y_offset, bool valid, bool left, bool right, uint32_t now_ms)
{
    if (!running) return;
    now = now_ms;
    if (game_state == STATE_SHOW_SCORES) { if (now - state_started >= SCORE_DISPLAY_MS) choose_mode(); return; }
    if (game_state == STATE_FINISHED) { if (auto_mode && now - state_started >= aiRestartDelay()) prepare_random_game(); return; }
    if (game_state == STATE_PLAYING && (auto_mode || !player_turn)) {
        if ((int32_t)(now - ai_due) >= 0) { if (player_turn) perform_ai_shot(&enemy_board, &player_ai, true); else perform_ai_shot(&player_board, &enemy_ai, false); }
        return;
    }
    if (!valid) stick_centred = false;
    const int ax = x_offset < 0 ? -x_offset : x_offset, ay = y_offset < 0 ? -y_offset : y_offset;
    if (valid && ax <= 350 && ay <= 350) stick_centred = true;
    int direction = 0;
    if (left || right) { direction = (int)right - (int)left; if (direction == 0) { held_direction = 0; repeating = false; return; } }
    else {
        if (!valid || (ax <= 350 && ay <= 350)) { held_direction = 0; repeating = false; return; }
        if (!stick_centred) 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 || (held_direction == direction && now - last_move < (repeating ? 150U : 400U))) return;
    repeating = held_direction == direction; held_direction = direction; last_move = now;
    if (game_state == STATE_CHOOSE_MODE) auto_mode = !auto_mode;
    else if (game_state == STATE_CHOOSE_PLACEMENT) random_placement = !random_placement;
    else if (game_state == STATE_PLACING_POSITION) move_cell(direction, false);
    else if (game_state == STATE_PLACING_ORIENTATION) horizontal = !horizontal;
    else if (game_state == STATE_PLAYING) move_cell(direction, true);
}
storage_message() · source line 321
static const char *storage_message(void)

Calls into this documentation:

  1. Decisiongold_save_failed || !goldStorageHealthy()
    True
    1. Exit this functionreturn "GOLD SAVE ERROR";
    False

    Continue after this decision.

  2. Decisionscore_save_failed
    True
    1. Exit this functionreturn "SCORE SAVE ERROR";
    False

    Continue after this decision.

  3. Decisionscore_status != SCORES_OK && score_status != SCORES_NOT_FOUND
    True
    1. Exit this functionreturn game_scores_status_text(score_status);
    False

    Continue after this decision.

  4. Decision!aiTimingStorageHealthy()
    True
    1. Exit this functionreturn "AI TIMING ERROR";
    False

    Continue after this decision.

  5. Exit this functionreturn NULL;
Read original function source
static const char *storage_message(void)
{
    if (gold_save_failed || !goldStorageHealthy()) return "GOLD SAVE ERROR";
    if (score_save_failed) return "SCORE SAVE ERROR";
    if (score_status != SCORES_OK && score_status != SCORES_NOT_FOUND) return game_scores_status_text(score_status);
    if (!aiTimingStorageHealthy()) return "AI TIMING ERROR";
    return NULL;
}
draw_grid() · source line 330
static void draw_grid(uint16_t *frame, const board_t *board, int board_x, bool hide_ships, bool selected)

Calls into this documentation:

  1. FOR loopint row = 0; row < GRID_SIZE; ++row

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

    1. FOR loopint column = 0; column < GRID_SIZE; ++column

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

        1. Executeconst cell_t *cell = &board->cells[row][column];
        2. Executeconst int x = board_x + column * CELL_SIZE, y = BOARD_Y + row * CELL_SIZE;
        3. Executeuint16_t color = COLOR_BLACK;
        4. Decision!hide_ships && cell->ship_index != NO_SHIP
          True
          1. Executecolor = COLOR_BLUE;
          False

          Continue after this decision.

        5. Decisioncell->shot == SHOT_MISS
          True
          1. Executecolor = COLOR_DIMGRAY;
          False
          1. Decisioncell->shot == SHOT_HIT
            True
            1. Executecolor = COLOR_RED;
            False

            Continue after this decision.

        6. Executefill_rect(frame, x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2, color);
        7. Decisioncell->shot == SHOT_MISS
          True
          1. Executefill_circle(frame, x + 6, y + 6, 2, COLOR_CYAN);
          False
          1. Decisioncell->shot == SHOT_HIT
            True
              1. Executedraw_line(frame, x + 3, y + 3, x + 8, y + 8, COLOR_YELLOW);
              2. Executedraw_line(frame, x + 8, y + 3, x + 3, y + 8, COLOR_YELLOW);
            False

            Continue after this decision.

        8. Executedraw_rect(frame, x, y, CELL_SIZE, CELL_SIZE, selected && row == selected_row && column == selected_column ? COLOR_YELLOW : COLOR_LIGHTGRAY);
Read original function source
static void draw_grid(uint16_t *frame, const board_t *board, int board_x, bool hide_ships, bool selected)
{
    for (int row = 0; row < GRID_SIZE; ++row) for (int column = 0; column < GRID_SIZE; ++column) {
        const cell_t *cell = &board->cells[row][column]; const int x = board_x + column * CELL_SIZE, y = BOARD_Y + row * CELL_SIZE;
        uint16_t color = COLOR_BLACK;
        if (!hide_ships && cell->ship_index != NO_SHIP) color = COLOR_BLUE;
        if (cell->shot == SHOT_MISS) color = COLOR_DIMGRAY; else if (cell->shot == SHOT_HIT) color = COLOR_RED;
        fill_rect(frame, x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2, color);
        if (cell->shot == SHOT_MISS) fill_circle(frame, x + 6, y + 6, 2, COLOR_CYAN);
        else if (cell->shot == SHOT_HIT) { draw_line(frame, x + 3, y + 3, x + 8, y + 8, COLOR_YELLOW); draw_line(frame, x + 8, y + 3, x + 3, y + 8, COLOR_YELLOW); }
        draw_rect(frame, x, y, CELL_SIZE, CELL_SIZE, selected && row == selected_row && column == selected_column ? COLOR_YELLOW : COLOR_LIGHTGRAY);
    }
}
draw_choice() · source line 344
static void draw_choice(uint16_t *frame, const char *left_text, const char *right_text, bool right_selected)

Calls into this documentation:

  1. Executedraw_rect(frame, 15, 55, 135, 40, right_selected ? COLOR_DIMGRAY : COLOR_YELLOW);
  2. Executedraw_rect(frame, 170, 55, 135, 40, right_selected ? COLOR_YELLOW : COLOR_DIMGRAY);
  3. Executedraw_text_centred(frame, 82, 68, left_text, 2, COLOR_WHITE);
  4. Executedraw_text_centred(frame, 237, 68, right_text, 2, COLOR_WHITE);
  5. Executedraw_text_centred(frame, 160, 111, "D/B/STICK CHOOSE A/K SELECT", 1, COLOR_WHITE);
Read original function source
static void draw_choice(uint16_t *frame, const char *left_text, const char *right_text, bool right_selected)
{
    draw_rect(frame, 15, 55, 135, 40, right_selected ? COLOR_DIMGRAY : COLOR_YELLOW);
    draw_rect(frame, 170, 55, 135, 40, right_selected ? COLOR_YELLOW : COLOR_DIMGRAY);
    draw_text_centred(frame, 82, 68, left_text, 2, COLOR_WHITE); draw_text_centred(frame, 237, 68, right_text, 2, COLOR_WHITE);
    draw_text_centred(frame, 160, 111, "D/B/STICK CHOOSE A/K SELECT", 1, COLOR_WHITE);
}
draw_mode_choice() · source line 352
static void draw_mode_choice(uint16_t *frame, bool auto_selected)

Calls into this documentation:

  1. Executedraw_text_centred(frame, 82, 68, "PLAY", 2, auto_selected ? COLOR_LIGHTGRAY : COLOR_YELLOW);
  2. Executedraw_text_centred(frame, 237, 68, "AUTO", 2, auto_selected ? COLOR_YELLOW : COLOR_LIGHTGRAY);
  3. Executedraw_text_centred(frame, 160, 111, "D/B/STICK CHOOSE A/K SELECT", 1, COLOR_WHITE);
Read original function source
static void draw_mode_choice(uint16_t *frame, bool auto_selected)
{
    draw_text_centred(frame, 82, 68, "PLAY", 2,
                      auto_selected ? COLOR_LIGHTGRAY : COLOR_YELLOW);
    draw_text_centred(frame, 237, 68, "AUTO", 2,
                      auto_selected ? COLOR_YELLOW : COLOR_LIGHTGRAY);
    draw_text_centred(frame, 160, 111, "D/B/STICK CHOOSE A/K SELECT", 1, COLOR_WHITE);
}
drawBattleships() · source line 361
void drawBattleships(uint16_t *frame)

Calls into this documentation:

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

    Continue after this decision.

  2. Executeclear(frame, COLOR_BLACK);
  3. Executechar line[64];
  4. Executeconst char *status = storage_message();
  5. Decisiongame_state == STATE_SHOW_SCORES || game_state == STATE_CHOOSE_MODE || game_state == STATE_CHOOSE_PLACEMENT
    True
      1. Executedraw_text_centred(frame, 160, 8, game_state == STATE_CHOOSE_PLACEMENT ? "PLACE FLEET" : "BATTLESHIPS", 3, COLOR_CYAN);
      2. Decisiongame_state == STATE_SHOW_SCORES
        True
          1. Executesnprintf(line, sizeof(line), "WINS %u", (unsigned)scores.wins);
          2. Executedraw_text_centred(frame, 160, 52, line, 2, COLOR_LIME);
          3. Executesnprintf(line, sizeof(line), "LOSSES %u", (unsigned)scores.losses);
          4. Executedraw_text_centred(frame, 160, 77, line, 2, COLOR_RED);
          5. Executesnprintf(line, sizeof(line), "STREAK %u", (unsigned)scores.streak);
          6. Executedraw_text_centred(frame, 160, 102, line, 2, COLOR_GOLD);
        False
        1. Decisiongame_state == STATE_CHOOSE_MODE
          True
          1. Executedraw_mode_choice(frame, auto_mode);
          False
          1. Executedraw_choice(frame, "MANUAL", "RANDOM", random_placement);
      3. Executedraw_text_centred(frame, 160, 133, fleet_error ? message : status != NULL ? status : game_scores_status_text(score_status), 1, COLOR_YELLOW);
      4. Executedraw_text_centred(frame, 160, 157, "C CHOOSE BOOT BACK", 1, COLOR_DIMGRAY);
      5. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisiongame_state == STATE_PLACING_POSITION || game_state == STATE_PLACING_ORIENTATION
    True
      1. Executesnprintf(line, sizeof(line), "SHIP %u", (unsigned)player_board.ships[placing_ship].length);
      2. Executedraw_text(frame, 4, 4, line, 1, COLOR_CYAN);
      3. Executedraw_grid(frame, &player_board, LEFT_BOARD_X, false, true);
      4. Executesnprintf(line, sizeof(line), "%c%u", 'A' + selected_column, (unsigned)selected_row + 1U);
      5. Executedraw_text_centred(frame, 220, 27, line, 3, COLOR_YELLOW);
      6. Executedraw_text_centred(frame, 187, 66, "H", 3, horizontal ? COLOR_YELLOW : COLOR_DIMGRAY);
      7. Executedraw_text_centred(frame, 253, 66, "V", 3, horizontal ? COLOR_DIMGRAY : COLOR_YELLOW);
      8. Executedraw_text_centred(frame, 220, 101, game_state == STATE_PLACING_ORIENTATION ? "D/B/STICK H/V" : "D/B/STICK CELL", 1, COLOR_WHITE);
      9. Executedraw_text_centred(frame, 220, 118, game_state == STATE_PLACING_ORIENTATION ? "A/K PLACE" : "A/K SELECT", 1, COLOR_WHITE);
      10. Executedraw_text_centred(frame, 160, 145, message, 1, COLOR_YELLOW);
      11. Executedraw_text_centred(frame, 160, 160, status != NULL ? status : "C CHOOSE BOOT BACK", 1, COLOR_DIMGRAY);
      12. Exit this functionreturn;
    False

    Continue after this decision.

  7. Executedraw_text_centred(frame, 62, 5, auto_mode ? "LEFT" : "YOU", 1, COLOR_CYAN);
  8. Executedraw_text_centred(frame, 258, 5, auto_mode ? "RIGHT" : "ESP", 1, COLOR_CYAN);
  9. Executedraw_grid(frame, &player_board, LEFT_BOARD_X, false, false);
  10. Executedraw_grid(frame, &enemy_board, RIGHT_BOARD_X, true, game_state == STATE_PLAYING && player_turn && !auto_mode);
  11. ExecutedrawGameGold(frame, 126, 2, 68, true);
  12. Executedraw_text_centred(frame, 160, 38, auto_mode ? (player_turn ? "LEFT" : "RIGHT") : (player_turn ? "YOUR" : "ESP"), 1, COLOR_WHITE);
  13. Executedraw_text_centred(frame, 160, 49, game_state == STATE_FINISHED ? "FINISHED" : "TURN", 1, COLOR_WHITE);
  14. Executesnprintf(line, sizeof(line), "%c%u", 'A' + selected_column, (unsigned)selected_row + 1U);
  15. Executedraw_text_centred(frame, 160, 66, line, 2, COLOR_CYAN);
  16. Decisionauto_mode
    True
      1. Executedraw_text_centred(frame, 160, 89, aiSpeedShortText(ai_speed), 1, COLOR_WHITE);
      2. Executedraw_text_centred(frame, 160, 102, "E SPEED", 1, COLOR_WHITE);
      3. Executedraw_text_centred(frame, 160, 115, "C STOP", 1, COLOR_WHITE);
      4. Decisiongame_state == STATE_FINISHED
        True
          1. Executeconst uint32_t delay = aiRestartDelay(), elapsed = now - state_started;
          2. Executesnprintf(line, sizeof(line), "NEXT %u", elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U));
          3. Executedraw_text_centred(frame, 160, 130, line, 1, COLOR_CYAN);
        False

        Continue after this decision.

    False
      1. Executedraw_text_centred(frame, 160, 89, "D/B/STICK", 1, COLOR_WHITE);
      2. Executedraw_text_centred(frame, 160, 102, "PREV/NEXT", 1, COLOR_WHITE);
      3. Executedraw_text_centred(frame, 160, 115, game_state == STATE_FINISHED ? "A/K NEW" : "A/K FIRE", 1, COLOR_WHITE);
      4. Executedraw_text_centred(frame, 160, 130, "C CHOOSE", 1, COLOR_WHITE);
  17. Executedraw_text_centred(frame, 160, 145, message, 1, COLOR_YELLOW);
  18. Executedraw_text_centred(frame, 160, 160, status != NULL ? status : "BOOT BACK HOLD BOOT HOME", 1, COLOR_DIMGRAY);
Read original function source
void drawBattleships(uint16_t *frame)
{
    if (!running) return;
    clear(frame, COLOR_BLACK);
    char line[64];
    const char *status = storage_message();
    if (game_state == STATE_SHOW_SCORES || game_state == STATE_CHOOSE_MODE || game_state == STATE_CHOOSE_PLACEMENT) {
        draw_text_centred(frame, 160, 8, game_state == STATE_CHOOSE_PLACEMENT ? "PLACE FLEET" : "BATTLESHIPS", 3, COLOR_CYAN);
        if (game_state == STATE_SHOW_SCORES) {
            snprintf(line, sizeof(line), "WINS %u", (unsigned)scores.wins); draw_text_centred(frame, 160, 52, line, 2, COLOR_LIME);
            snprintf(line, sizeof(line), "LOSSES %u", (unsigned)scores.losses); draw_text_centred(frame, 160, 77, line, 2, COLOR_RED);
            snprintf(line, sizeof(line), "STREAK %u", (unsigned)scores.streak); draw_text_centred(frame, 160, 102, line, 2, COLOR_GOLD);
        } else if (game_state == STATE_CHOOSE_MODE) draw_mode_choice(frame, auto_mode);
        else draw_choice(frame, "MANUAL", "RANDOM", random_placement);
        draw_text_centred(frame, 160, 133, fleet_error ? message : status != NULL ? status : game_scores_status_text(score_status), 1, COLOR_YELLOW);
        draw_text_centred(frame, 160, 157, "C CHOOSE BOOT BACK", 1, COLOR_DIMGRAY); return;
    }
    if (game_state == STATE_PLACING_POSITION || game_state == STATE_PLACING_ORIENTATION) {
        snprintf(line, sizeof(line), "SHIP %u", (unsigned)player_board.ships[placing_ship].length); draw_text(frame, 4, 4, line, 1, COLOR_CYAN);
        draw_grid(frame, &player_board, LEFT_BOARD_X, false, true);
        snprintf(line, sizeof(line), "%c%u", 'A' + selected_column, (unsigned)selected_row + 1U); draw_text_centred(frame, 220, 27, line, 3, COLOR_YELLOW);
        draw_text_centred(frame, 187, 66, "H", 3, horizontal ? COLOR_YELLOW : COLOR_DIMGRAY); draw_text_centred(frame, 253, 66, "V", 3, horizontal ? COLOR_DIMGRAY : COLOR_YELLOW);
        draw_text_centred(frame, 220, 101, game_state == STATE_PLACING_ORIENTATION ? "D/B/STICK H/V" : "D/B/STICK CELL", 1, COLOR_WHITE);
        draw_text_centred(frame, 220, 118, game_state == STATE_PLACING_ORIENTATION ? "A/K PLACE" : "A/K SELECT", 1, COLOR_WHITE);
        draw_text_centred(frame, 160, 145, message, 1, COLOR_YELLOW); draw_text_centred(frame, 160, 160, status != NULL ? status : "C CHOOSE BOOT BACK", 1, COLOR_DIMGRAY); return;
    }
    draw_text_centred(frame, 62, 5, auto_mode ? "LEFT" : "YOU", 1, COLOR_CYAN); draw_text_centred(frame, 258, 5, auto_mode ? "RIGHT" : "ESP", 1, COLOR_CYAN);
    draw_grid(frame, &player_board, LEFT_BOARD_X, false, false); draw_grid(frame, &enemy_board, RIGHT_BOARD_X, true, game_state == STATE_PLAYING && player_turn && !auto_mode);
    drawGameGold(frame, 126, 2, 68, true);
    draw_text_centred(frame, 160, 38, auto_mode ? (player_turn ? "LEFT" : "RIGHT") : (player_turn ? "YOUR" : "ESP"), 1, COLOR_WHITE);
    draw_text_centred(frame, 160, 49, game_state == STATE_FINISHED ? "FINISHED" : "TURN", 1, COLOR_WHITE);
    snprintf(line, sizeof(line), "%c%u", 'A' + selected_column, (unsigned)selected_row + 1U); draw_text_centred(frame, 160, 66, line, 2, COLOR_CYAN);
    if (auto_mode) {
        draw_text_centred(frame, 160, 89, aiSpeedShortText(ai_speed), 1, COLOR_WHITE); draw_text_centred(frame, 160, 102, "E SPEED", 1, COLOR_WHITE); draw_text_centred(frame, 160, 115, "C STOP", 1, COLOR_WHITE);
        if (game_state == STATE_FINISHED) { const uint32_t delay = aiRestartDelay(), elapsed = now - state_started; snprintf(line, sizeof(line), "NEXT %u", elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U)); draw_text_centred(frame, 160, 130, line, 1, COLOR_CYAN); }
    } else {
        draw_text_centred(frame, 160, 89, "D/B/STICK", 1, COLOR_WHITE); draw_text_centred(frame, 160, 102, "PREV/NEXT", 1, COLOR_WHITE);
        draw_text_centred(frame, 160, 115, game_state == STATE_FINISHED ? "A/K NEW" : "A/K FIRE", 1, COLOR_WHITE); draw_text_centred(frame, 160, 130, "C CHOOSE", 1, COLOR_WHITE);
    }
    draw_text_centred(frame, 160, 145, message, 1, COLOR_YELLOW); draw_text_centred(frame, 160, 160, status != NULL ? status : "BOOT BACK HOLD BOOT HOME", 1, COLOR_DIMGRAY);
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "battleships.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>

#define SCORE_DISPLAY_MS 2000U
#define BATTLESHIPS_WIN_GOLD 10U
#define GRID_SIZE 10
#define SHIP_COUNT 11
#define NO_SHIP 0xffU
#define CELL_SIZE 12
#define LEFT_BOARD_X 2
#define RIGHT_BOARD_X 198
#define BOARD_Y 20

static const uint8_t ship_lengths[SHIP_COUNT] = {2,2,2,3,3,3,4,4,5,5,7};

typedef enum { SHOT_UNKNOWN, SHOT_MISS, SHOT_HIT } shot_state_t;
typedef enum { RESULT_INVALID, RESULT_MISS, RESULT_HIT, RESULT_SUNK } shot_result_t;
typedef enum { STATE_SHOW_SCORES, STATE_CHOOSE_MODE, STATE_CHOOSE_PLACEMENT,
               STATE_PLACING_POSITION, STATE_PLACING_ORIENTATION, STATE_PLAYING,
               STATE_FINISHED } game_state_t;
typedef struct { uint8_t ship_index; shot_state_t shot; } cell_t;
typedef struct { uint8_t length, hits; bool placed; } ship_t;
typedef struct { cell_t cells[GRID_SIZE][GRID_SIZE]; ship_t ships[SHIP_COUNT]; } board_t;
typedef struct { uint8_t rows[100], columns[100], count; } ai_state_t;

static board_t player_board, enemy_board;
static ai_state_t player_ai, enemy_ai;
static game_scores_t scores;
static game_score_result_t score_status;
static game_state_t game_state;
static uint8_t selected_row, selected_column, placing_ship, ai_speed;
static uint32_t now, state_started, ai_due, last_move;
static bool running, auto_mode, random_placement, horizontal, player_turn;
static bool stick_centred, repeating, score_save_failed, gold_save_failed, fleet_error;
static int held_direction;
static char message[48];

static bool inside(int row, int column) { return row >= 0 && row < GRID_SIZE && column >= 0 && column < GRID_SIZE; }
static void say(const char *text) { snprintf(message, sizeof(message), "%s", text); }
static void reset_navigation(void) { stick_centred = false; held_direction = 0; repeating = false; }

static void clear_board(board_t *board)
{
    for (int row = 0; row < GRID_SIZE; ++row)
        for (int column = 0; column < GRID_SIZE; ++column)
            board->cells[row][column] = (cell_t){NO_SHIP, SHOT_UNKNOWN};
    for (int ship = 0; ship < SHIP_COUNT; ++ship)
        board->ships[ship] = (ship_t){ship_lengths[ship], 0, false};
}

static void clear_game(void)
{
    clear_board(&player_board); clear_board(&enemy_board);
    player_ai.count = enemy_ai.count = 0;
    selected_row = selected_column = 0;
    placing_ship = SHIP_COUNT - 1; horizontal = true; player_turn = true;
    reset_navigation();
}

static void choose_mode(void)
{
    game_state = STATE_CHOOSE_MODE; auto_mode = false; random_placement = false;
    fleet_error = false; say(""); reset_navigation();
}

static bool can_place(const board_t *board, int row, int column, uint8_t length, bool is_horizontal)
{
    const int end_row = row + (is_horizontal ? 0 : length - 1);
    const int end_column = column + (is_horizontal ? length - 1 : 0);
    if (!inside(row, column) || !inside(end_row, end_column)) return false;
    for (int part = 0; part < length; ++part) {
        const int ship_row = row + (is_horizontal ? 0 : part);
        const int ship_column = column + (is_horizontal ? part : 0);
        for (int row_step = -1; row_step <= 1; ++row_step)
            for (int column_step = -1; column_step <= 1; ++column_step) {
                const int near_row = ship_row + row_step, near_column = ship_column + column_step;
                if (inside(near_row, near_column) && board->cells[near_row][near_column].ship_index != NO_SHIP) return false;
            }
    }
    return true;
}

static void place_ship(board_t *board, uint8_t ship_index, uint8_t row, uint8_t column, bool is_horizontal)
{
    ship_t *ship = &board->ships[ship_index];
    for (int part = 0; part < ship->length; ++part)
        board->cells[row + (is_horizontal ? 0 : part)][column + (is_horizontal ? part : 0)].ship_index = ship_index;
    ship->placed = true; ship->hits = 0;
}

static bool place_random_fleet(board_t *board)
{
    for (int fleet_try = 0; fleet_try < 100; ++fleet_try) {
        clear_board(board); bool complete = true;
        for (int ship = SHIP_COUNT - 1; ship >= 0; --ship) {
            bool placed = false;
            for (int try = 0; try < 250; ++try) {
                const bool is_horizontal = esp_random() % 2U == 0;
                const uint8_t row = esp_random() % GRID_SIZE, column = esp_random() % GRID_SIZE;
                if (can_place(board, row, column, board->ships[ship].length, is_horizontal)) {
                    place_ship(board, ship, row, column, is_horizontal); placed = true; break;
                }
            }
            if (!placed) { complete = false; break; }
        }
        if (complete) return true;
    }
    return false;
}

static bool all_sunk(const board_t *board)
{
    for (int ship = 0; ship < SHIP_COUNT; ++ship)
        if (!board->ships[ship].placed || board->ships[ship].hits < board->ships[ship].length) return false;
    return true;
}

static shot_result_t fire_at(board_t *board, uint8_t row, uint8_t column)
{
    cell_t *cell = &board->cells[row][column];
    if (cell->shot != SHOT_UNKNOWN) return RESULT_INVALID;
    if (cell->ship_index == NO_SHIP) { cell->shot = SHOT_MISS; return RESULT_MISS; }
    cell->shot = SHOT_HIT;
    ship_t *ship = &board->ships[cell->ship_index];
    ++ship->hits;
    return ship->hits >= ship->length ? RESULT_SUNK : RESULT_HIT;
}

static void move_cell(int direction, bool unknown_only)
{
    int position = selected_row * GRID_SIZE + selected_column;
    for (int count = 0; count < 100; ++count) {
        position = (position + direction + 100) % 100;
        const int row = position / GRID_SIZE, column = position % GRID_SIZE;
        if (!unknown_only || enemy_board.cells[row][column].shot == SHOT_UNKNOWN) {
            selected_row = row; selected_column = column; return;
        }
    }
}

static bool queued(const ai_state_t *ai, uint8_t row, uint8_t column)
{
    for (int index = 0; index < ai->count; ++index)
        if (ai->rows[index] == row && ai->columns[index] == column) return true;
    return false;
}

static void queue_target(ai_state_t *ai, const board_t *board, int row, int column)
{
    if (!inside(row, column) || board->cells[row][column].shot != SHOT_UNKNOWN || queued(ai, row, column) || ai->count >= 100) return;
    ai->rows[ai->count] = row; ai->columns[ai->count] = column; ++ai->count;
}

static void remember_hit(ai_state_t *ai, const board_t *board, uint8_t row, uint8_t column, shot_result_t result)
{
    if (result == RESULT_SUNK) { ai->count = 0; return; }
    if (result != RESULT_HIT) return;
    queue_target(ai, board, row - 1, column); queue_target(ai, board, row + 1, column);
    queue_target(ai, board, row, column - 1); queue_target(ai, board, row, column + 1);
}

static bool choose_ai_target(ai_state_t *ai, const board_t *board, uint8_t *row, uint8_t *column)
{
    while (ai->count > 0) {
        --ai->count; *row = ai->rows[ai->count]; *column = ai->columns[ai->count];
        if (board->cells[*row][*column].shot == SHOT_UNKNOWN) return true;
    }
    unsigned available = 0;
    for (int r = 0; r < GRID_SIZE; ++r) for (int c = 0; c < GRID_SIZE; ++c)
        if (board->cells[r][c].shot == SHOT_UNKNOWN) ++available;
    if (available == 0) return false;
    unsigned selection = esp_random() % available;
    for (int r = 0; r < GRID_SIZE; ++r) for (int c = 0; c < GRID_SIZE; ++c)
        if (board->cells[r][c].shot == SHOT_UNKNOWN && selection-- == 0) { *row = r; *column = c; return true; }
    return false;
}

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

static void finish_game(bool player_won)
{
    game_state = STATE_FINISHED; state_started = now; reset_navigation();
    if (auto_mode) { say(player_won ? "LEFT WINS" : "RIGHT WINS"); return; }
    if (player_won) { increment(&scores.wins); increment(&scores.streak); if (!addUserGoldWon(BATTLESHIPS_WIN_GOLD)) gold_save_failed = true; say("YOU WIN"); }
    else { increment(&scores.losses); scores.streak = 0; say("ESP WINS"); }
    if (score_status == SCORES_OK || score_status == SCORES_NOT_FOUND) {
        score_save_failed = !game_scores_save(GAME_BATTLESHIPS, &scores);
        if (!score_save_failed) score_status = SCORES_OK;
    } else score_save_failed = true;
}

static void finish_shot(const board_t *target, shot_result_t result, uint8_t row, uint8_t column, bool attacker_is_player)
{
    audioPlay(result == RESULT_MISS ? AUDIO_SFX_BOARD_PLACE : AUDIO_SFX_BOARD_CAPTURE);
    const char *name = auto_mode ? (attacker_is_player ? "LEFT" : "RIGHT") : (attacker_is_player ? "YOU" : "ESP");
    const char *result_text = result == RESULT_SUNK ? "SUNK" : result == RESULT_HIT ? "HIT" : "MISS";
    snprintf(message, sizeof(message), "%s %s %c%u", name, result_text, 'A' + column, (unsigned)row + 1U);
    if (all_sunk(target)) { finish_game(attacker_is_player); return; }
    player_turn = !attacker_is_player; ai_due = now + aiSpeedDelay(ai_speed); reset_navigation();
    if (player_turn && !auto_mode && enemy_board.cells[selected_row][selected_column].shot != SHOT_UNKNOWN) move_cell(1, true);
}

static void perform_ai_shot(board_t *target, ai_state_t *ai, bool attacker_is_player)
{
    uint8_t row, column;
    if (!choose_ai_target(ai, target, &row, &column)) return;
    const shot_result_t result = fire_at(target, row, column);
    remember_hit(ai, target, row, column, result); selected_row = row; selected_column = column;
    finish_shot(target, result, row, column, attacker_is_player);
}

static void start_combat(void)
{
    player_ai.count = enemy_ai.count = 0; selected_row = selected_column = 0; player_turn = true;
    game_state = STATE_PLAYING; fleet_error = false; say(auto_mode ? "AUTO BATTLE" : "YOUR SHOT");
    ai_due = now + aiSpeedDelay(ai_speed); reset_navigation();
}

static void fleet_error_state(void)
{
    choose_mode(); fleet_error = true; say("FLEET ERROR - RETRY");
    ESP_LOGE("battleships", "Fleet placement failed");
}

static void prepare_random_game(void)
{
    clear_game();
    if (!place_random_fleet(&player_board) || !place_random_fleet(&enemy_board)) { fleet_error_state(); return; }
    start_combat();
}

static void start_manual_placement(void)
{
    clear_game(); game_state = STATE_PLACING_POSITION; say("SELECT START");
}

static void confirm_placement(void)
{
    const uint8_t length = player_board.ships[placing_ship].length;
    if (!can_place(&player_board, selected_row, selected_column, length, horizontal)) {
        game_state = STATE_PLACING_POSITION; say("NO FIT"); reset_navigation(); return;
    }
    place_ship(&player_board, placing_ship, selected_row, selected_column, horizontal);
    audioPlay(AUDIO_SFX_BOARD_PLACE);
    if (placing_ship == 0) {
        if (!place_random_fleet(&enemy_board)) { fleet_error_state(); return; }
        start_combat(); return;
    }
    --placing_ship; game_state = STATE_PLACING_POSITION; say("SELECT START"); reset_navigation();
}

void startBattleships(uint32_t now_ms)
{
    now = state_started = now_ms; running = true; score_save_failed = gold_save_failed = false;
    scores = (game_scores_t){0}; score_status = game_scores_load(GAME_BATTLESHIPS, &scores);
    ai_speed = AI_SPEED_DEFAULT_INDEX; clear_game(); choose_mode(); game_state = STATE_SHOW_SCORES;
    ESP_LOGI("battleships", "START: %s", game_scores_status_text(score_status));
}

void stopBattleships(void) { running = false; ESP_LOGI("battleships", "STOP"); }

void battleshipsAction(bool select, bool speed, bool choose, uint32_t now_ms)
{
    if (!running) return;
    now = now_ms;
    if (choose) { choose_mode(); return; }
    if (game_state == STATE_SHOW_SCORES) { if (select) choose_mode(); return; }
    if (game_state == STATE_CHOOSE_MODE) {
        if (select) { fleet_error = false; if (auto_mode) prepare_random_game(); else { random_placement = false; game_state = STATE_CHOOSE_PLACEMENT; reset_navigation(); } }
        return;
    }
    if (game_state == STATE_CHOOSE_PLACEMENT) { if (select) { if (random_placement) prepare_random_game(); else start_manual_placement(); } return; }
    if (auto_mode) { if (speed) { cycleAISpeed(&ai_speed); ai_due = now + aiSpeedDelay(ai_speed); } return; }
    if (!select) return;
    if (game_state == STATE_FINISHED) choose_mode();
    else if (game_state == STATE_PLACING_POSITION) { game_state = STATE_PLACING_ORIENTATION; say("CHOOSE H/V"); reset_navigation(); }
    else if (game_state == STATE_PLACING_ORIENTATION) confirm_placement();
    else if (game_state == STATE_PLAYING && player_turn) {
        const shot_result_t result = fire_at(&enemy_board, selected_row, selected_column);
        if (result != RESULT_INVALID) finish_shot(&enemy_board, result, selected_row, selected_column, true);
    }
}

void updateBattleships(int x_offset, int y_offset, bool valid, bool left, bool right, uint32_t now_ms)
{
    if (!running) return;
    now = now_ms;
    if (game_state == STATE_SHOW_SCORES) { if (now - state_started >= SCORE_DISPLAY_MS) choose_mode(); return; }
    if (game_state == STATE_FINISHED) { if (auto_mode && now - state_started >= aiRestartDelay()) prepare_random_game(); return; }
    if (game_state == STATE_PLAYING && (auto_mode || !player_turn)) {
        if ((int32_t)(now - ai_due) >= 0) { if (player_turn) perform_ai_shot(&enemy_board, &player_ai, true); else perform_ai_shot(&player_board, &enemy_ai, false); }
        return;
    }
    if (!valid) stick_centred = false;
    const int ax = x_offset < 0 ? -x_offset : x_offset, ay = y_offset < 0 ? -y_offset : y_offset;
    if (valid && ax <= 350 && ay <= 350) stick_centred = true;
    int direction = 0;
    if (left || right) { direction = (int)right - (int)left; if (direction == 0) { held_direction = 0; repeating = false; return; } }
    else {
        if (!valid || (ax <= 350 && ay <= 350)) { held_direction = 0; repeating = false; return; }
        if (!stick_centred) 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 || (held_direction == direction && now - last_move < (repeating ? 150U : 400U))) return;
    repeating = held_direction == direction; held_direction = direction; last_move = now;
    if (game_state == STATE_CHOOSE_MODE) auto_mode = !auto_mode;
    else if (game_state == STATE_CHOOSE_PLACEMENT) random_placement = !random_placement;
    else if (game_state == STATE_PLACING_POSITION) move_cell(direction, false);
    else if (game_state == STATE_PLACING_ORIENTATION) horizontal = !horizontal;
    else if (game_state == STATE_PLAYING) move_cell(direction, true);
}

static const char *storage_message(void)
{
    if (gold_save_failed || !goldStorageHealthy()) return "GOLD SAVE ERROR";
    if (score_save_failed) return "SCORE SAVE ERROR";
    if (score_status != SCORES_OK && score_status != SCORES_NOT_FOUND) return game_scores_status_text(score_status);
    if (!aiTimingStorageHealthy()) return "AI TIMING ERROR";
    return NULL;
}

static void draw_grid(uint16_t *frame, const board_t *board, int board_x, bool hide_ships, bool selected)
{
    for (int row = 0; row < GRID_SIZE; ++row) for (int column = 0; column < GRID_SIZE; ++column) {
        const cell_t *cell = &board->cells[row][column]; const int x = board_x + column * CELL_SIZE, y = BOARD_Y + row * CELL_SIZE;
        uint16_t color = COLOR_BLACK;
        if (!hide_ships && cell->ship_index != NO_SHIP) color = COLOR_BLUE;
        if (cell->shot == SHOT_MISS) color = COLOR_DIMGRAY; else if (cell->shot == SHOT_HIT) color = COLOR_RED;
        fill_rect(frame, x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2, color);
        if (cell->shot == SHOT_MISS) fill_circle(frame, x + 6, y + 6, 2, COLOR_CYAN);
        else if (cell->shot == SHOT_HIT) { draw_line(frame, x + 3, y + 3, x + 8, y + 8, COLOR_YELLOW); draw_line(frame, x + 8, y + 3, x + 3, y + 8, COLOR_YELLOW); }
        draw_rect(frame, x, y, CELL_SIZE, CELL_SIZE, selected && row == selected_row && column == selected_column ? COLOR_YELLOW : COLOR_LIGHTGRAY);
    }
}

static void draw_choice(uint16_t *frame, const char *left_text, const char *right_text, bool right_selected)
{
    draw_rect(frame, 15, 55, 135, 40, right_selected ? COLOR_DIMGRAY : COLOR_YELLOW);
    draw_rect(frame, 170, 55, 135, 40, right_selected ? COLOR_YELLOW : COLOR_DIMGRAY);
    drawPet2001TextCentredTransparent(frame, 82, 68, left_text, COLOR_WHITE, 2);
    drawPet2001TextCentredTransparent(frame, 237, 68, right_text, COLOR_WHITE, 2);
    drawPet2001TextCentredTransparent(frame, 160, 111, "D/B/STICK CHOOSE A/K SELECT", COLOR_WHITE, 1);
}

static void draw_mode_choice(uint16_t *frame, bool auto_selected)
{
    drawPet2001TextCentredTransparent(frame, 82, 68, "PLAY",
                                       auto_selected ? COLOR_LIGHTGRAY : COLOR_YELLOW, 2);
    drawPet2001TextCentredTransparent(frame, 237, 68, "AUTO",
                                       auto_selected ? COLOR_YELLOW : COLOR_LIGHTGRAY, 2);
    drawPet2001TextCentredTransparent(frame, 160, 111, "D/B/STICK CHOOSE A/K SELECT", COLOR_WHITE, 1);
}

void drawBattleships(uint16_t *frame)
{
    if (!running) return;
    clear(frame, COLOR_BLACK);
    char line[64];
    const char *status = storage_message();
    if (game_state == STATE_SHOW_SCORES || game_state == STATE_CHOOSE_MODE || game_state == STATE_CHOOSE_PLACEMENT) {
        drawPet2001TextCentredTransparent(frame, 160, 8,
                                           game_state == STATE_CHOOSE_PLACEMENT ? "PLACE FLEET" : "BATTLESHIPS",
                                           COLOR_CYAN, 3);
        if (game_state == STATE_SHOW_SCORES) {
            snprintf(line, sizeof(line), "WINS %u", (unsigned)scores.wins);
            drawPet2001TextCentredTransparent(frame, 160, 52, line, COLOR_LIME, 2);
            snprintf(line, sizeof(line), "LOSSES %u", (unsigned)scores.losses);
            drawPet2001TextCentredTransparent(frame, 160, 77, line, COLOR_RED, 2);
            snprintf(line, sizeof(line), "STREAK %u", (unsigned)scores.streak);
            drawPet2001TextCentredTransparent(frame, 160, 102, line, COLOR_GOLD, 2);
        } else if (game_state == STATE_CHOOSE_MODE) draw_mode_choice(frame, auto_mode);
        else draw_choice(frame, "MANUAL", "RANDOM", random_placement);
        drawPet2001TextCentredTransparent(frame, 160, 133,
                                           fleet_error ? message : status != NULL ? status : game_scores_status_text(score_status),
                                           COLOR_YELLOW, 1);
        drawPet2001TextCentredTransparent(frame, 160, 157, "C CHOOSE BOOT BACK", COLOR_DIMGRAY, 1); return;
    }
    if (game_state == STATE_PLACING_POSITION || game_state == STATE_PLACING_ORIENTATION) {
        snprintf(line, sizeof(line), "SHIP %u", (unsigned)player_board.ships[placing_ship].length);
        drawPet2001TextTransparent(frame, 4, 4, line, COLOR_CYAN, 1);
        draw_grid(frame, &player_board, LEFT_BOARD_X, false, true);
        snprintf(line, sizeof(line), "%c%u", 'A' + selected_column, (unsigned)selected_row + 1U);
        drawPet2001TextCentredTransparent(frame, 220, 27, line, COLOR_YELLOW, 3);
        drawPet2001TextCentredTransparent(frame, 187, 66, "H", horizontal ? COLOR_YELLOW : COLOR_DIMGRAY, 3);
        drawPet2001TextCentredTransparent(frame, 253, 66, "V", horizontal ? COLOR_DIMGRAY : COLOR_YELLOW, 3);
        drawPet2001TextCentredTransparent(frame, 220, 101,
                                           game_state == STATE_PLACING_ORIENTATION ? "D/B/STICK H/V" : "D/B/STICK CELL",
                                           COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 220, 118,
                                           game_state == STATE_PLACING_ORIENTATION ? "A/K PLACE" : "A/K SELECT",
                                           COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 145, message, COLOR_YELLOW, 1);
        drawPet2001TextCentredTransparent(frame, 160, 160,
                                           status != NULL ? status : "C CHOOSE BOOT BACK", COLOR_DIMGRAY, 1); return;
    }
    drawPet2001TextCentredTransparent(frame, 62, 5, auto_mode ? "LEFT" : "YOU", COLOR_CYAN, 1);
    drawPet2001TextCentredTransparent(frame, 258, 5, auto_mode ? "RIGHT" : "ESP", COLOR_CYAN, 1);
    draw_grid(frame, &player_board, LEFT_BOARD_X, false, false); draw_grid(frame, &enemy_board, RIGHT_BOARD_X, true, game_state == STATE_PLAYING && player_turn && !auto_mode);
    drawGameGold(frame, 126, 2, 68, true);
    drawPet2001TextCentredTransparent(frame, 160, 38,
                                       auto_mode ? (player_turn ? "LEFT" : "RIGHT") : (player_turn ? "YOUR" : "ESP"),
                                       COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 160, 49, game_state == STATE_FINISHED ? "FINISHED" : "TURN", COLOR_WHITE, 1);
    snprintf(line, sizeof(line), "%c%u", 'A' + selected_column, (unsigned)selected_row + 1U);
    drawPet2001TextCentredTransparent(frame, 160, 66, line, COLOR_CYAN, 2);
    if (auto_mode) {
        drawPet2001TextCentredTransparent(frame, 160, 89, aiSpeedShortText(ai_speed), COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 102, "E SPEED", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 115, "C STOP", COLOR_WHITE, 1);
        if (game_state == STATE_FINISHED) { const uint32_t delay = aiRestartDelay(), elapsed = now - state_started; snprintf(line, sizeof(line), "NEXT %u", elapsed >= delay ? 0U : (unsigned)((delay - elapsed + 999U) / 1000U)); drawPet2001TextCentredTransparent(frame, 160, 130, line, COLOR_CYAN, 1); }
    } else {
        drawPet2001TextCentredTransparent(frame, 160, 89, "D/B/STICK", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 102, "PREV/NEXT", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 115, game_state == STATE_FINISHED ? "A/K NEW" : "A/K FIRE", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 130, "C CHOOSE", COLOR_WHITE, 1);
    }
    drawPet2001TextCentredTransparent(frame, 160, 145, message, COLOR_YELLOW, 1);
    drawPet2001TextCentredTransparent(frame, 160, 160,
                                       status != NULL ? status : "BOOT BACK HOLD BOOT HOME", COLOR_DIMGRAY, 1);
}