Game Config

Source: src/game_config.c · 250 lines · 12 functions.

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

selectedGame() · source line 44
static const game_config_game_t *selectedGame(void)
  1. Exit this functionreturn &games[selected_game];
Read original function source
static const game_config_game_t *selectedGame(void)
{
    return &games[selected_game];
}
readInteger() · source line 49
static int readInteger(const char *filename, const char *key, int fallback, int minimum, int maximum)
  1. Executechar path[48];
  2. Executeconst int written = snprintf(path, sizeof(path), "%s/%s", CONFIG_ROOT, filename);
  3. Decisionwritten < 0 || (size_t)written >= sizeof(path)
    True
    1. Exit this functionreturn fallback;
    False

    Continue after this decision.

  4. ExecuteFILE *file = fopen(path, "rb");
  5. Decisionfile == NULL
    True
    1. Exit this functionreturn fallback;
    False

    Continue after this decision.

  6. Executechar line[CONFIG_LINE_CHARS];
  7. Executeint value = fallback;
  8. WHILE loopfgets(line, sizeof(line), file) != NULL

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

      1. Executechar found_key[32];
      2. Executeint candidate;
      3. Decisionsscanf(line, " %31[^= ] = %d", found_key, &candidate) != 2
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      4. Decisionstrcmp(found_key, key) != 0
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      5. Decisioncandidate >= minimum && candidate <= maximum
        True
        1. Executevalue = candidate;
        False

        Continue after this decision.

      6. Exit enclosing loop or switchbreak;
  9. Executefclose(file);
  10. Exit this functionreturn value;
Read original function source
static int readInteger(const char *filename, const char *key, int fallback, int minimum, int maximum)
{
    char path[48];
    const int written = snprintf(path, sizeof(path), "%s/%s", CONFIG_ROOT, filename);
    if (written < 0 || (size_t)written >= sizeof(path)) return fallback;
    FILE *file = fopen(path, "rb");
    if (file == NULL) return fallback;

    char line[CONFIG_LINE_CHARS];
    int value = fallback;
    while (fgets(line, sizeof(line), file) != NULL) {
        char found_key[32];
        int candidate;
        if (sscanf(line, " %31[^= ] = %d", found_key, &candidate) != 2) continue;
        if (strcmp(found_key, key) != 0) continue;
        if (candidate >= minimum && candidate <= maximum) value = candidate;
        break;
    }
    fclose(file);
    return value;
}
writeInvadersConfig() · source line 71
static bool writeInvadersConfig(uint8_t lives)
  1. Executeconst char *final_path = CONFIG_ROOT "/invaders.cfg";
  2. Executeconst char *temporary_path = CONFIG_ROOT "/invaders.tmp";
  3. ExecuteFILE *file = fopen(temporary_path, "wb");
  4. Decisionfile == NULL
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  5. Executebool written = fprintf(file, "# Mini Arcade Invaders configuration\n" "# Remove this file to restore the built-in defaults.\n" "lives=%u\n", (unsigned)lives) > 0;
  6. Decisionfclose(file) != 0
    True
    1. Executewritten = false;
    False

    Continue after this decision.

  7. Decision!written
    True
      1. Executeremove(temporary_path);
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  8. Decisionrename(temporary_path, final_path) == 0
    True
    1. Exit this functionreturn true;
    False

    Continue after this decision.

  9. Executeremove(temporary_path);
  10. Exit this functionreturn false;
Read original function source
static bool writeInvadersConfig(uint8_t lives)
{
    const char *final_path = CONFIG_ROOT "/invaders.cfg";
    const char *temporary_path = CONFIG_ROOT "/invaders.tmp";
    FILE *file = fopen(temporary_path, "wb");
    if (file == NULL) return false;
    bool written = fprintf(file, "# Mini Arcade Invaders configuration\n"
                                  "# Remove this file to restore the built-in defaults.\n"
                                  "lives=%u\n", (unsigned)lives) > 0;
    if (fclose(file) != 0) written = false;
    if (!written) {
        remove(temporary_path);
        return false;
    }
    if (rename(temporary_path, final_path) == 0) return true;
    remove(temporary_path);
    return false;
}
gameConfigInvadersStartingLives() · source line 90
uint8_t gameConfigInvadersStartingLives(void)

Calls into this documentation:

  1. Exit this functionreturn (uint8_t)readInteger("invaders.cfg", "lives", 3, 1, 9);
Read original function source
uint8_t gameConfigInvadersStartingLives(void)
{
    return (uint8_t)readInteger("invaders.cfg", "lives", 3, 1, 9);
}
startGameConfig() · source line 95
void startGameConfig(uint32_t now_ms)

Calls into this documentation:

  1. Execute(void)now_ms;
  2. Executeactive = true;
  3. Executeview = GAME_CONFIG_GAMES;
  4. Executeselected_game = selected_option = 0U;
  5. Executewaiting_for_centre = true;
  6. Executerepeating = false;
  7. Executeheld_direction = 0;
  8. Executeinvaders_lives = gameConfigInvadersStartingLives();
Read original function source
void startGameConfig(uint32_t now_ms)
{
    (void)now_ms;
    active = true;
    view = GAME_CONFIG_GAMES;
    selected_game = selected_option = 0U;
    waiting_for_centre = true;
    repeating = false;
    held_direction = 0;
    invaders_lives = gameConfigInvadersStartingLives();
}
stopGameConfig() · source line 107
void stopGameConfig(void)
  1. Executeactive = false;
  2. Executeheld_direction = 0;
Read original function source
void stopGameConfig(void)
{
    active = false;
    held_direction = 0;
}
gameConfigAction() · source line 113
void gameConfigAction(bool increase, bool decrease, uint32_t now_ms)

Calls into this documentation:

  1. Execute(void)now_ms;
  2. Decision!active || (!increase && !decrease)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. Decisionview == GAME_CONFIG_GAMES
    True
      1. Executeview = GAME_CONFIG_OPTIONS;
      2. Executeselected_option = 0U;
      3. Executeinvaders_lives = gameConfigInvadersStartingLives();
      4. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decision!selectedGame()->configured
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisionselected_option == 0U
    True
      1. Executeint next = invaders_lives + (increase ? 1 : -1);
      2. Decisionnext < 1
        True
        1. Executenext = 1;
        False

        Continue after this decision.

      3. Decisionnext > 9
        True
        1. Executenext = 9;
        False

        Continue after this decision.

      4. Decision(uint8_t)next == invaders_lives
        True
        1. Exit this functionreturn;
        False

        Continue after this decision.

      5. DecisionwriteInvadersConfig((uint8_t)next)
        True
        1. Executeinvaders_lives = (uint8_t)next;
        False

        Continue after this decision.

    False
    1. Decisionremove(CONFIG_ROOT "/invaders.cfg") == 0 || errno == ENOENT
      True
        1. Executeinvaders_lives = 3U;
      False

      Continue after this decision.

Read original function source
void gameConfigAction(bool increase, bool decrease, uint32_t now_ms)
{
    (void)now_ms;
    if (!active || (!increase && !decrease)) return;
    if (view == GAME_CONFIG_GAMES) {
        view = GAME_CONFIG_OPTIONS;
        selected_option = 0U;
        invaders_lives = gameConfigInvadersStartingLives();
        return;
    }
    if (!selectedGame()->configured) return;
    if (selected_option == 0U) {
        int next = invaders_lives + (increase ? 1 : -1);
        if (next < 1) next = 1;
        if (next > 9) next = 9;
        if ((uint8_t)next == invaders_lives) return;
        if (writeInvadersConfig((uint8_t)next)) invaders_lives = (uint8_t)next;
    } else if (remove(CONFIG_ROOT "/invaders.cfg") == 0 || errno == ENOENT) {
        invaders_lives = 3U;
    }
}
moveGameSelection() · source line 135
static void moveGameSelection(int horizontal_direction, int vertical_direction)
  1. Executeconst unsigned count = sizeof(games) / sizeof(games[0]);
  2. Executeconst unsigned page_count = (count + GAME_CONFIG_PER_PAGE - 1U) / GAME_CONFIG_PER_PAGE;
  3. Executeunsigned page = selected_game / GAME_CONFIG_PER_PAGE;
  4. Executeunsigned column = (selected_game % GAME_CONFIG_PER_PAGE) / GAME_CONFIG_ROWS;
  5. Executeunsigned row = selected_game % GAME_CONFIG_ROWS;
  6. Executeunsigned page_items = count - page * GAME_CONFIG_PER_PAGE;
  7. Decisionpage_items > GAME_CONFIG_PER_PAGE
    True
    1. Executepage_items = GAME_CONFIG_PER_PAGE;
    False

    Continue after this decision.

  8. Executeunsigned rows = page_items > column * GAME_CONFIG_ROWS ? page_items - column * GAME_CONFIG_ROWS : 0U;
  9. Decisionrows > GAME_CONFIG_ROWS
    True
    1. Executerows = GAME_CONFIG_ROWS;
    False

    Continue after this decision.

  10. Decisionhorizontal_direction != 0
    True
      1. Executeconst unsigned other_column = column == 0U ? 1U : 0U;
      2. Executeunsigned other_rows = page_items > other_column * GAME_CONFIG_ROWS ? page_items - other_column * GAME_CONFIG_ROWS : 0U;
      3. Decisionother_rows > GAME_CONFIG_ROWS
        True
        1. Executeother_rows = GAME_CONFIG_ROWS;
        False

        Continue after this decision.

      4. Decisionother_rows > 0U
        True
          1. Executecolumn = other_column;
          2. Decisionrow >= other_rows
            True
            1. Executerow = other_rows - 1U;
            False

            Continue after this decision.

        False

        Continue after this decision.

    False
    1. Decisionvertical_direction != 0
      True
        1. Decision(vertical_direction < 0 && row > 0U) || (vertical_direction > 0 && row + 1U < rows)
          True
            1. Executerow = vertical_direction < 0 ? row - 1U : row + 1U;
          False
            1. DO / WHILE looprows == 0U

              Run body, then test condition; repeat while true.

                1. Executepage = vertical_direction < 0 ? (page + page_count - 1U) % page_count : (page + 1U) % page_count;
                2. Executepage_items = count - page * GAME_CONFIG_PER_PAGE;
                3. Decisionpage_items > GAME_CONFIG_PER_PAGE
                  True
                  1. Executepage_items = GAME_CONFIG_PER_PAGE;
                  False

                  Continue after this decision.

                4. Executerows = page_items > column * GAME_CONFIG_ROWS ? page_items - column * GAME_CONFIG_ROWS : 0U;
                5. Decisionrows > GAME_CONFIG_ROWS
                  True
                  1. Executerows = GAME_CONFIG_ROWS;
                  False

                  Continue after this decision.

            2. Executerow = vertical_direction < 0 ? rows - 1U : 0U;
      False

      Continue after this decision.

  11. Executeselected_game = page * GAME_CONFIG_PER_PAGE + column * GAME_CONFIG_ROWS + row;
Read original function source
static void moveGameSelection(int horizontal_direction, int vertical_direction)
{
    const unsigned count = sizeof(games) / sizeof(games[0]);
    const unsigned page_count = (count + GAME_CONFIG_PER_PAGE - 1U) / GAME_CONFIG_PER_PAGE;
    unsigned page = selected_game / GAME_CONFIG_PER_PAGE;
    unsigned column = (selected_game % GAME_CONFIG_PER_PAGE) / GAME_CONFIG_ROWS;
    unsigned row = selected_game % GAME_CONFIG_ROWS;
    unsigned page_items = count - page * GAME_CONFIG_PER_PAGE;
    if (page_items > GAME_CONFIG_PER_PAGE) page_items = GAME_CONFIG_PER_PAGE;
    unsigned rows = page_items > column * GAME_CONFIG_ROWS ? page_items - column * GAME_CONFIG_ROWS : 0U;
    if (rows > GAME_CONFIG_ROWS) rows = GAME_CONFIG_ROWS;

    if (horizontal_direction != 0) {
        const unsigned other_column = column == 0U ? 1U : 0U;
        unsigned other_rows = page_items > other_column * GAME_CONFIG_ROWS ?
            page_items - other_column * GAME_CONFIG_ROWS : 0U;
        if (other_rows > GAME_CONFIG_ROWS) other_rows = GAME_CONFIG_ROWS;
        if (other_rows > 0U) {
            column = other_column;
            if (row >= other_rows) row = other_rows - 1U;
        }
    } else if (vertical_direction != 0) {
        if ((vertical_direction < 0 && row > 0U) ||
            (vertical_direction > 0 && row + 1U < rows)) {
            row = vertical_direction < 0 ? row - 1U : row + 1U;
        } else {
            do {
                page = vertical_direction < 0 ? (page + page_count - 1U) % page_count :
                                                 (page + 1U) % page_count;
                page_items = count - page * GAME_CONFIG_PER_PAGE;
                if (page_items > GAME_CONFIG_PER_PAGE) page_items = GAME_CONFIG_PER_PAGE;
                rows = page_items > column * GAME_CONFIG_ROWS ?
                    page_items - column * GAME_CONFIG_ROWS : 0U;
                if (rows > GAME_CONFIG_ROWS) rows = GAME_CONFIG_ROWS;
            } while (rows == 0U);
            row = vertical_direction < 0 ? rows - 1U : 0U;
        }
    }
    selected_game = page * GAME_CONFIG_PER_PAGE + column * GAME_CONFIG_ROWS + row;
}
updateGameConfig() · source line 176
void updateGameConfig(int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)

Calls into this documentation:

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

    Continue after this decision.

  2. Decision!joystick_valid
    True
      1. Executewaiting_for_centre = true;
      2. Executeheld_direction = 0;
      3. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeconst int x_magnitude = x_offset < 0 ? -x_offset : x_offset;
  4. Executeconst int magnitude = y_offset < 0 ? -y_offset : y_offset;
  5. Decisionx_magnitude <= JOYSTICK_RELEASE && magnitude <= JOYSTICK_RELEASE
    True
      1. Executewaiting_for_centre = false;
      2. Executeheld_direction = 0;
      3. Executerepeating = false;
      4. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisionwaiting_for_centre || (x_magnitude < JOYSTICK_ENTER && magnitude < JOYSTICK_ENTER)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  7. Executeconst bool horizontal = x_magnitude >= magnitude;
  8. Executeconst int direction = horizontal ? (x_offset > 0 ? 1 : -1) : (y_offset > 0 ? -1 : 1);
  9. Executeconst int held = horizontal ? (direction > 0 ? 1 : 2) : (direction < 0 ? 3 : 4);
  10. Decisionheld == held_direction && now_ms - last_move < (repeating ? REPEAT_MS : REPEAT_START_MS)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  11. Executerepeating = held == held_direction;
  12. Executeheld_direction = held;
  13. Executelast_move = now_ms;
  14. Decisionview == GAME_CONFIG_GAMES
    True
      1. ExecutemoveGameSelection(horizontal ? direction : 0, horizontal ? 0 : direction);
    False
    1. DecisionselectedGame()->configured
      True
        1. Executeselected_option = selected_option == 0U ? 1U : 0U;
      False

      Continue after this decision.

Read original function source
void updateGameConfig(int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)
{
    if (!active) return;
    if (!joystick_valid) { waiting_for_centre = true; held_direction = 0; return; }
    const int x_magnitude = x_offset < 0 ? -x_offset : x_offset;
    const int magnitude = y_offset < 0 ? -y_offset : y_offset;
    if (x_magnitude <= JOYSTICK_RELEASE && magnitude <= JOYSTICK_RELEASE) {
        waiting_for_centre = false;
        held_direction = 0;
        repeating = false;
        return;
    }
    if (waiting_for_centre || (x_magnitude < JOYSTICK_ENTER && magnitude < JOYSTICK_ENTER)) return;
    const bool horizontal = x_magnitude >= magnitude;
    const int direction = horizontal ? (x_offset > 0 ? 1 : -1) : (y_offset > 0 ? -1 : 1);
    const int held = horizontal ? (direction > 0 ? 1 : 2) : (direction < 0 ? 3 : 4);
    if (held == held_direction && now_ms - last_move < (repeating ? REPEAT_MS : REPEAT_START_MS)) return;
    repeating = held == held_direction;
    held_direction = held;
    last_move = now_ms;
    if (view == GAME_CONFIG_GAMES) {
        moveGameSelection(horizontal ? direction : 0, horizontal ? 0 : direction);
    } else if (selectedGame()->configured) {
        selected_option = selected_option == 0U ? 1U : 0U;
    }
}
drawGameList() · source line 203
static void drawGameList(uint16_t *frame)

Calls into this documentation:

  1. Executeconst unsigned count = sizeof(games) / sizeof(games[0]);
  2. Executeconst unsigned first = selected_game / GAME_CONFIG_PER_PAGE * GAME_CONFIG_PER_PAGE;
  3. Executeclear(frame, COLOR_BLACK);
  4. Executedraw_text_centred(frame, 160, 4, "GAME CONFIG", 2, COLOR_CYAN);
  5. FOR loopunsigned index = first; index < count && index < first + GAME_CONFIG_PER_PAGE; ++index

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

      1. Executeconst unsigned position = index - first;
      2. Executeconst int x = position < GAME_CONFIG_ROWS ? 8 : 166;
      3. Executeconst int y = 39 + (int)(position % GAME_CONFIG_ROWS) * 24;
      4. Executedraw_text(frame, x, y, games[index].name, 1, index == selected_game ? COLOR_CYAN : COLOR_YELLOW);
  6. Executedraw_text_centred(frame, 160, 157, "STICK SELECT A/K OPEN BOOT BACK", 1, COLOR_LIGHTGREY);
Read original function source
static void drawGameList(uint16_t *frame)
{
    const unsigned count = sizeof(games) / sizeof(games[0]);
    const unsigned first = selected_game / GAME_CONFIG_PER_PAGE * GAME_CONFIG_PER_PAGE;
    clear(frame, COLOR_BLACK);
    draw_text_centred(frame, 160, 4, "GAME CONFIG", 2, COLOR_CYAN);
    for (unsigned index = first; index < count && index < first + GAME_CONFIG_PER_PAGE; ++index) {
        const unsigned position = index - first;
        const int x = position < GAME_CONFIG_ROWS ? 8 : 166;
        const int y = 39 + (int)(position % GAME_CONFIG_ROWS) * 24;
        draw_text(frame, x, y, games[index].name, 1,
                  index == selected_game ? COLOR_CYAN : COLOR_YELLOW);
    }
    draw_text_centred(frame, 160, 157, "STICK SELECT  A/K OPEN  BOOT BACK", 1, COLOR_LIGHTGREY);
}
drawGameOptions() · source line 219
static void drawGameOptions(uint16_t *frame)

Calls into this documentation:

  1. Executeclear(frame, COLOR_BLACK);
  2. Executedraw_text_centred(frame, 160, 4, selectedGame()->name, 2, COLOR_CYAN);
  3. Decision!selectedGame()->configured
    True
      1. Executedraw_text_centred(frame, 160, 66, "NO SETTINGS YET", 2, COLOR_YELLOW);
      2. Executedraw_text_centred(frame, 160, 100, "BUILT-IN DEFAULTS ACTIVE", 1, COLOR_LIGHTGREY);
      3. Executedraw_text_centred(frame, 160, 157, "BOOT BACK", 1, COLOR_LIGHTGREY);
      4. Exit this functionreturn;
    False

    Continue after this decision.

  4. Executechar value[24];
  5. Executesnprintf(value, sizeof(value), "%u", (unsigned)invaders_lives);
  6. Executedraw_text(frame, 16, 43, "STARTING LIVES", 1, selected_option == 0U ? COLOR_CYAN : COLOR_YELLOW);
  7. Executedraw_text(frame, 258, 43, value, 1, selected_option == 0U ? COLOR_CYAN : COLOR_WHITE);
  8. Executedraw_text(frame, 16, 73, "RESTORE DEFAULTS", 1, selected_option == 1U ? COLOR_CYAN : COLOR_YELLOW);
  9. Executedraw_text_centred(frame, 160, 119, "A INCREASE K DECREASE", 1, COLOR_GREEN);
  10. Executedraw_text_centred(frame, 160, 133, "A/K RESTORES DEFAULTS", 1, COLOR_GREEN);
  11. Executedraw_text_centred(frame, 160, 157, "UP/DOWN SELECT BOOT BACK", 1, COLOR_LIGHTGREY);
Read original function source
static void drawGameOptions(uint16_t *frame)
{
    clear(frame, COLOR_BLACK);
    draw_text_centred(frame, 160, 4, selectedGame()->name, 2, COLOR_CYAN);
    if (!selectedGame()->configured) {
        draw_text_centred(frame, 160, 66, "NO SETTINGS YET", 2, COLOR_YELLOW);
        draw_text_centred(frame, 160, 100, "BUILT-IN DEFAULTS ACTIVE", 1, COLOR_LIGHTGREY);
        draw_text_centred(frame, 160, 157, "BOOT BACK", 1, COLOR_LIGHTGREY);
        return;
    }
    char value[24];
    snprintf(value, sizeof(value), "%u", (unsigned)invaders_lives);
    draw_text(frame, 16, 43, "STARTING LIVES", 1,
              selected_option == 0U ? COLOR_CYAN : COLOR_YELLOW);
    draw_text(frame, 258, 43, value, 1, selected_option == 0U ? COLOR_CYAN : COLOR_WHITE);
    draw_text(frame, 16, 73, "RESTORE DEFAULTS", 1,
              selected_option == 1U ? COLOR_CYAN : COLOR_YELLOW);
    draw_text_centred(frame, 160, 119, "A INCREASE  K DECREASE", 1, COLOR_GREEN);
    draw_text_centred(frame, 160, 133, "A/K RESTORES DEFAULTS", 1, COLOR_GREEN);
    draw_text_centred(frame, 160, 157, "UP/DOWN SELECT  BOOT BACK", 1, COLOR_LIGHTGREY);
}
drawGameConfig() · source line 241
void drawGameConfig(uint16_t *frame)

Calls into this documentation:

  1. Decision!active || frame == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionview == GAME_CONFIG_GAMES
    True
    1. ExecutedrawGameList(frame);
    False
    1. ExecutedrawGameOptions(frame);
Read original function source
void drawGameConfig(uint16_t *frame)
{
    if (!active || frame == NULL) return;
    if (view == GAME_CONFIG_GAMES) drawGameList(frame);
    else drawGameOptions(frame);
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "game_config.h"

#include "game_colors.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>

#define CONFIG_ROOT "/spiffs"
#define CONFIG_LINE_CHARS 64
#define GAME_CONFIG_ROWS 4U
#define GAME_CONFIG_PER_PAGE (GAME_CONFIG_ROWS * 2U)
#define JOYSTICK_ENTER 600
#define JOYSTICK_RELEASE 350
#define REPEAT_START_MS 400U
#define REPEAT_MS 150U

typedef struct {
    const char *name;
    const char *filename;
    bool configured;
} game_config_game_t;

typedef enum { GAME_CONFIG_GAMES, GAME_CONFIG_OPTIONS } game_config_view_t;

static const game_config_game_t games[] = {
    {"CHECKERS", "checkers.cfg", false}, {"OTHELLO", "othello.cfg", false},
    {"CONNECT FOUR", "connect_four.cfg", false}, {"BATTLESHIPS", "battleships.cfg", false},
    {"ASTEROIDS", "asteroids.cfg", false}, {"TEMPEST", "tempest.cfg", false},
    {"INVADERS", "invaders.cfg", true}, {"YAHTZEE", "yahtzee.cfg", false},
    {"BLACKJACK", "blackjack.cfg", false}, {"PIG", "pig.cfg", false},
    {"MISSILE COMMAND", "missile_command.cfg", false}, {"SCRAMBLE", "scramble.cfg", false},
    {"DEFENDER", "defender.cfg", false}, {"GALAGA", "galaga.cfg", false},
    {"DUNGEON", "dungeon.cfg", false},
};

static bool active, waiting_for_centre, repeating;
static game_config_view_t view;
static unsigned selected_game, selected_option;
static int held_direction;
static uint32_t last_move;
static uint8_t invaders_lives;

static const game_config_game_t *selectedGame(void)
{
    return &games[selected_game];
}

static int readInteger(const char *filename, const char *key, int fallback, int minimum, int maximum)
{
    char path[48];
    const int written = snprintf(path, sizeof(path), "%s/%s", CONFIG_ROOT, filename);
    if (written < 0 || (size_t)written >= sizeof(path)) return fallback;
    FILE *file = fopen(path, "rb");
    if (file == NULL) return fallback;

    char line[CONFIG_LINE_CHARS];
    int value = fallback;
    while (fgets(line, sizeof(line), file) != NULL) {
        char found_key[32];
        int candidate;
        if (sscanf(line, " %31[^= ] = %d", found_key, &candidate) != 2) continue;
        if (strcmp(found_key, key) != 0) continue;
        if (candidate >= minimum && candidate <= maximum) value = candidate;
        break;
    }
    fclose(file);
    return value;
}

static bool writeInvadersConfig(uint8_t lives)
{
    const char *final_path = CONFIG_ROOT "/invaders.cfg";
    const char *temporary_path = CONFIG_ROOT "/invaders.tmp";
    FILE *file = fopen(temporary_path, "wb");
    if (file == NULL) return false;
    bool written = fprintf(file, "# Mini Arcade Invaders configuration\n"
                                  "# Remove this file to restore the built-in defaults.\n"
                                  "lives=%u\n", (unsigned)lives) > 0;
    if (fclose(file) != 0) written = false;
    if (!written) {
        remove(temporary_path);
        return false;
    }
    if (rename(temporary_path, final_path) == 0) return true;
    remove(temporary_path);
    return false;
}

uint8_t gameConfigInvadersStartingLives(void)
{
    return (uint8_t)readInteger("invaders.cfg", "lives", 3, 1, 9);
}

void startGameConfig(uint32_t now_ms)
{
    (void)now_ms;
    active = true;
    view = GAME_CONFIG_GAMES;
    selected_game = selected_option = 0U;
    waiting_for_centre = true;
    repeating = false;
    held_direction = 0;
    invaders_lives = gameConfigInvadersStartingLives();
}

void stopGameConfig(void)
{
    active = false;
    held_direction = 0;
}

void gameConfigAction(bool increase, bool decrease, uint32_t now_ms)
{
    (void)now_ms;
    if (!active || (!increase && !decrease)) return;
    if (view == GAME_CONFIG_GAMES) {
        view = GAME_CONFIG_OPTIONS;
        selected_option = 0U;
        invaders_lives = gameConfigInvadersStartingLives();
        return;
    }
    if (!selectedGame()->configured) return;
    if (selected_option == 0U) {
        int next = invaders_lives + (increase ? 1 : -1);
        if (next < 1) next = 1;
        if (next > 9) next = 9;
        if ((uint8_t)next == invaders_lives) return;
        if (writeInvadersConfig((uint8_t)next)) invaders_lives = (uint8_t)next;
    } else if (remove(CONFIG_ROOT "/invaders.cfg") == 0 || errno == ENOENT) {
        invaders_lives = 3U;
    }
}

static void moveGameSelection(int horizontal_direction, int vertical_direction)
{
    const unsigned count = sizeof(games) / sizeof(games[0]);
    const unsigned page_count = (count + GAME_CONFIG_PER_PAGE - 1U) / GAME_CONFIG_PER_PAGE;
    unsigned page = selected_game / GAME_CONFIG_PER_PAGE;
    unsigned column = (selected_game % GAME_CONFIG_PER_PAGE) / GAME_CONFIG_ROWS;
    unsigned row = selected_game % GAME_CONFIG_ROWS;
    unsigned page_items = count - page * GAME_CONFIG_PER_PAGE;
    if (page_items > GAME_CONFIG_PER_PAGE) page_items = GAME_CONFIG_PER_PAGE;
    unsigned rows = page_items > column * GAME_CONFIG_ROWS ? page_items - column * GAME_CONFIG_ROWS : 0U;
    if (rows > GAME_CONFIG_ROWS) rows = GAME_CONFIG_ROWS;

    if (horizontal_direction != 0) {
        const unsigned other_column = column == 0U ? 1U : 0U;
        unsigned other_rows = page_items > other_column * GAME_CONFIG_ROWS ?
            page_items - other_column * GAME_CONFIG_ROWS : 0U;
        if (other_rows > GAME_CONFIG_ROWS) other_rows = GAME_CONFIG_ROWS;
        if (other_rows > 0U) {
            column = other_column;
            if (row >= other_rows) row = other_rows - 1U;
        }
    } else if (vertical_direction != 0) {
        if ((vertical_direction < 0 && row > 0U) ||
            (vertical_direction > 0 && row + 1U < rows)) {
            row = vertical_direction < 0 ? row - 1U : row + 1U;
        } else {
            do {
                page = vertical_direction < 0 ? (page + page_count - 1U) % page_count :
                                                 (page + 1U) % page_count;
                page_items = count - page * GAME_CONFIG_PER_PAGE;
                if (page_items > GAME_CONFIG_PER_PAGE) page_items = GAME_CONFIG_PER_PAGE;
                rows = page_items > column * GAME_CONFIG_ROWS ?
                    page_items - column * GAME_CONFIG_ROWS : 0U;
                if (rows > GAME_CONFIG_ROWS) rows = GAME_CONFIG_ROWS;
            } while (rows == 0U);
            row = vertical_direction < 0 ? rows - 1U : 0U;
        }
    }
    selected_game = page * GAME_CONFIG_PER_PAGE + column * GAME_CONFIG_ROWS + row;
}

void updateGameConfig(int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)
{
    if (!active) return;
    if (!joystick_valid) { waiting_for_centre = true; held_direction = 0; return; }
    const int x_magnitude = x_offset < 0 ? -x_offset : x_offset;
    const int magnitude = y_offset < 0 ? -y_offset : y_offset;
    if (x_magnitude <= JOYSTICK_RELEASE && magnitude <= JOYSTICK_RELEASE) {
        waiting_for_centre = false;
        held_direction = 0;
        repeating = false;
        return;
    }
    if (waiting_for_centre || (x_magnitude < JOYSTICK_ENTER && magnitude < JOYSTICK_ENTER)) return;
    const bool horizontal = x_magnitude >= magnitude;
    const int direction = horizontal ? (x_offset > 0 ? 1 : -1) : (y_offset > 0 ? -1 : 1);
    const int held = horizontal ? (direction > 0 ? 1 : 2) : (direction < 0 ? 3 : 4);
    if (held == held_direction && now_ms - last_move < (repeating ? REPEAT_MS : REPEAT_START_MS)) return;
    repeating = held == held_direction;
    held_direction = held;
    last_move = now_ms;
    if (view == GAME_CONFIG_GAMES) {
        moveGameSelection(horizontal ? direction : 0, horizontal ? 0 : direction);
    } else if (selectedGame()->configured) {
        selected_option = selected_option == 0U ? 1U : 0U;
    }
}

static void drawGameList(uint16_t *frame)
{
    const unsigned count = sizeof(games) / sizeof(games[0]);
    const unsigned first = selected_game / GAME_CONFIG_PER_PAGE * GAME_CONFIG_PER_PAGE;
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 4, "GAME CONFIG", COLOR_CYAN, 2);
    for (unsigned index = first; index < count && index < first + GAME_CONFIG_PER_PAGE; ++index) {
        const unsigned position = index - first;
        const int x = position < GAME_CONFIG_ROWS ? 8 : 166;
        const int y = 39 + (int)(position % GAME_CONFIG_ROWS) * 24;
        drawPet2001TextTransparent(frame, x, y, games[index].name,
                                  index == selected_game ? COLOR_CYAN : COLOR_YELLOW, 1);
    }
    drawPet2001TextCentredTransparent(frame, 160, 157,
                                      "STICK SELECT  A/K OPEN  BOOT BACK", COLOR_LIGHTGREY, 1);
}

static void drawGameOptions(uint16_t *frame)
{
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 4, selectedGame()->name, COLOR_CYAN, 2);
    if (!selectedGame()->configured) {
        drawPet2001TextCentredTransparent(frame, 160, 66, "NO SETTINGS YET", COLOR_YELLOW, 2);
        drawPet2001TextCentredTransparent(frame, 160, 100,
                                          "BUILT-IN DEFAULTS ACTIVE", COLOR_LIGHTGREY, 1);
        drawPet2001TextCentredTransparent(frame, 160, 157, "BOOT BACK", COLOR_LIGHTGREY, 1);
        return;
    }
    char value[24];
    snprintf(value, sizeof(value), "%u", (unsigned)invaders_lives);
    drawPet2001TextTransparent(frame, 16, 43, "STARTING LIVES",
                               selected_option == 0U ? COLOR_CYAN : COLOR_YELLOW, 1);
    drawPet2001TextTransparent(frame, 258, 43, value,
                               selected_option == 0U ? COLOR_CYAN : COLOR_WHITE, 1);
    drawPet2001TextTransparent(frame, 16, 73, "RESTORE DEFAULTS",
                               selected_option == 1U ? COLOR_CYAN : COLOR_YELLOW, 1);
    drawPet2001TextCentredTransparent(frame, 160, 119, "A INCREASE  K DECREASE", COLOR_GREEN, 1);
    drawPet2001TextCentredTransparent(frame, 160, 133, "A/K RESTORES DEFAULTS", COLOR_GREEN, 1);
    drawPet2001TextCentredTransparent(frame, 160, 157, "UP/DOWN SELECT  BOOT BACK", COLOR_LIGHTGREY, 1);
}

void drawGameConfig(uint16_t *frame)
{
    if (!active || frame == NULL) return;
    if (view == GAME_CONFIG_GAMES) drawGameList(frame);
    else drawGameOptions(frame);
}