OLED dungeon (ESP32-S3 + joystick)
An original first-party monochrome raycasting game running as real ESP-IDF firmware on the emulated ESP32-S3. Choose Select, focus the canvas, and use WASD to move/turn plus Space to fire; or drag the physical joystick cap and click it without moving to fire. The fixed 20 Hz game loop samples real ADC1/GPIO input on G1, G2, and G10, renders a complete 128×128 page-major frame, and sends it over hardware I²C on G8/G9 to the real 1.5″ SH1107 model. Split Keep opens with a visible horned warden, then branches through auto-opening doors and north/south routes; clear five wardens to unlock the exit while hunting health and ammunition. Muzzle flash, hit and damage feedback, an objective HUD, win/death screens, and deterministic restart all run on-device. It needs no account, backend, internet, colour display, or PSRAM; the Code tab shows the complete MIT source and portable host harness read-only.

What's on the bench
- Battery
- ESP32-S3
- Joystick
- OLED 1.5″ 128×128
- Resistor
How it's wired
- Battery · pos→ESP32-S3 · vin
- Battery · neg→ESP32-S3 · gnd
- ESP32-S3 · 3v3→OLED 1.5″ 128×128 · vcc
- ESP32-S3 · gnd2→OLED 1.5″ 128×128 · gnd
- ESP32-S3 · g8→OLED 1.5″ 128×128 · sda
- ESP32-S3 · g9→OLED 1.5″ 128×128 · scl
- ESP32-S3 · 3v3→Joystick · vcc
- ESP32-S3 · gnd2→Joystick · gnd
- ESP32-S3 · g1→Joystick · vrx
- ESP32-S3 · g2→Joystick · vry
- ESP32-S3 · g10→Joystick · sw
- ESP32-S3 · 3v3→Resistor · a
- Resistor · b→Joystick · sw
The code
The exact ESP-IDF v5.3.5 project the bundled firmware was compiled from, shown read-only in the simulator’s Code tab.
README.md
# OLED dungeon — original ESP32-S3 raycasting game This is the complete first-party source for Breadboard's **OLED dungeon (ESP32-S3 + joystick)** example (`esp32s3-oled-fps`). It is a small monochrome first-person game written from scratch for BRE-209. It is not a port, compatibility layer, or adaptation of Doom or another game. ## Original content specification The single 16×16 level is called **Split Keep**. The player begins at cell `(2, 7)`, facing an immediately visible courtyard warden, and the exit is at `(13, 7)`. A hollow central keep separates them. Its west and east doors open automatically at close range; a north breach and a south breach provide two longer routes. Five original horned wardens, two ammunition bundles, and two health markers are placed only on flood-fill-reachable floor cells. The exit stays locked until all five wardens are defeated. Wall ids 1–4 use four ordered dither densities rather than image textures. All title, HUD, enemy, pickup, weapon, and end-screen graphics are hand-authored geometric primitives or the project's own 3×5 block glyphs. There is no audio. The portable simulation uses signed Q16.16 positions, an unsigned 256-step turn angle, a generated Q15 sine table, a fixed 20 Hz tick, and a fixed-seed unsigned LCG used only to choose enemy axis ordering. Rendering casts one integer DDA ray per 128-pixel OLED column directly into the monochrome SH1107's page-major 2,048-byte framebuffer. Each frame is sent over 1 MHz hardware I²C as one valid Co-framed transaction per page. It has no color framebuffer and uses no floating point at runtime. Game states are title/instructions, play, win, and death. The joystick's Y axis moves, X turns, and its cap starts, fires, or restarts; with the Select tool active, the app also maps WASD/arrows and Space to the same physical input path while the canvas is focused. Gameplay includes collision, auto-opening doors, a hitscan repeater, projectile-free contact wardens, health/ammunition pickups, a two-line health/ammo/kill/gate HUD, muzzle flash, hit and damage feedback, and deterministic restart. ## Licensing boundary and inventory Every file in this project, including the level, renderer, glyphs, generated mathematical tables, tests, and documentation, is original Breadboard code and content under the adjacent MIT `LICENSE`. No third-party game code, maps, art, audio, names, or story content was consulted, copied, translated, traced, or adapted. In particular, this project contains no Doom-compatible code or data. The ESP-IDF platform build links Espressif's Apache-2.0 components. The exact redistributed binary's licenses, NOTICE, SPDX SBOM, component warnings, input hashes, and reproducibility evidence live beside `game.bin` under `packages/app/public/firmware/esp32s3-oled-fps/`; they are release artifacts, not source dependencies of the portable core. ## Portable host verification After loading the repository toolchain, configure with any CMake 3.16+ and a C11 compiler: ```sh cmake -S firmware-src/esp32s3-oled-fps/host -B /tmp/oled-fps-host cmake --build /tmp/oled-fps-host ctest --test-dir /tmp/oled-fps-host --output-on-failure node firmware-src/esp32s3-oled-fps/scripts/generate_tables.mjs ``` The harness compiles with strict warnings and enables AddressSanitizer plus UndefinedBehaviorSanitizer on Linux/GNU or non-Apple Clang by default. AppleClang 17's AddressSanitizer runtime can hang before `main` while reserving its dynamic shadow on macOS, so the canonical CMake path selects UndefinedBehaviorSanitizer alone on AppleClang. A network-disabled Debian 13.5/GCC 14.2.0 container run remains the release AddressSanitizer-plus-UndefinedBehaviorSanitizer gate and is retained in the release manifest. The harness proves level reachability and all four wall materials; collision; doors; pickups; hits, enemy removal, and depleted ammunition; win/death/restart; extreme map-edge rays; and two identical runs of a 360-tick trace. The reviewed golden result is: ```text state=6f7c3444 frame=2b999c0d trace=00187486 ``` `scripts/generate_tables.mjs` checks synchronization by default and writes only with `--write`. ## Firmware build The release target is ESP-IDF v5.3.5 at commit `0c45a102a96e82000f145b786a20de229b9f8557`, target `esp32s3`, 4 MB flash, and no PSRAM. With that exact checkout exported as `IDF_PATH`, each release candidate was built from a distinct clean, real (non-symlinked) directory: ```sh source "$IDF_PATH/export.sh" >/dev/null idf.py -C firmware-src/esp32s3-oled-fps -B "$BUILD_ROOT/build" \ -D SDKCONFIG="$BUILD_ROOT/sdkconfig" \ -D SDKCONFIG_DEFAULTS="$REPO_ROOT/firmware-src/esp32s3-oled-fps/sdkconfig.defaults" \ -D IDF_TARGET=esp32s3 build ``` The release-time verification used clean roots `.bre209-review-a` and `.bre209-review-b`. Their application, ELF, generated sdkconfig, link map, and canonical SPDX outputs were byte-identical; path-bearing raw SPDX and project-description files are recorded separately. The committed manifest is an attestation of those release builds, while ordinary app CI re-hashes the committed evidence rather than rebuilding ESP-IDF. The shipped application is 212,336 bytes with SHA-256 `1c654047cb03ce4e0cef41b7a396bdbe59dfb2de48d21d4a2da9fc7894651728`. The release manifest beside it records every output hash and tool version. ## Display-controller reference The SH1107 initialization follows Sino Wealth's SH1107/SH1107G v2.1 controller datasheet: command `0x20` selects page addressing, while `0xAD, 0x8B` enables the internal DC-DC converter when the display is turned on. Reference: <https://www.laskakit.cz/user/related_files/sh1107_v2-1.pdf>, command sections 3 and 10 (pages 24 and 32 in the document pagination). ## Browser-emulator performance evidence These are emulator measurements, not real-hardware claims. They were collected from a production `pnpm build` of the local BRE-209 release candidate, using the exact shipped application above and production worker bundle SHA-256 `5bdd6eecaa5772510053305c41c3db1ec264e076d875f9d671411a230710a08f`. The reference host was a MacBook Air (Mac17,4), Apple M5 with 10 cores and 32 GB RAM, running arm64 macOS 27.0 build 26A5421a and Chrome 151.0.7922.174. The viewport was 1920×1080 at the example's normal 110% canvas zoom. The exact worktree's production Vite preview was isolated on port 4174; unrelated local servers remained idle and were not opened by the benchmark. Three independent 60-second wall-time active-play runs drove production WASD/Space input and observed production serial markers without DEV hooks. Separate exact-artifact trials measured a short physical cap click from its decisive pointer-up event to the first changed OLED-region screenshot: | Run | Wall time | Frames | Achieved pacing | Rendered rate | Cap-to-OLED | | --- | ---: | ---: | ---: | ---: | ---: | | 1 | 60.054 s | 1,220 | 1.0157× | 20.315 fps | 88.5 ms | | 2 | 60.020 s | 1,220 | 1.0163× | 20.327 fps | 102.3 ms | | 3 | 60.227 s | 1,220 | 1.0128× | 20.257 fps | 76.6 ms | Median pacing was 1.0157×, median rendered rate was 20.315 fps, and median input-to-visible-frame latency was 88.5 ms. Latency is the midpoint of the interval between the last completed unchanged OLED screenshot and the first completed changed screenshot; conservative completion-time upper bounds were 114.1, 135.6, and 101.5 ms. This includes browser screenshot transfer overhead. An earlier 400 kHz I²C candidate missed the normal-response gate with a 165.2 ms median; the accepted 1 MHz page-framed transport is the measured fix. The three accepted throughput runs produced 27, 21, and 58 distinct frame CRCs, reached play/death/restart states, and had no reset, watchdog, long stall, or browser console error. The accepted five-minute active-play soak ran for 300.100 wall seconds, advanced 6,020 frames, achieved 1.0030× pacing and 20.060 fps, and observed 28 distinct OLED CRCs through the deliberately bounded visible serial window. It reached play/death/restart states under continuing movement and fire input. There was no repeated boot marker, watchdog, stuck input, OLED freeze, or browser console error. After its three-minute settling interval, forced-GC measurements over the final two minutes changed the main renderer heap from 15,294,824 to 14,513,272 bytes (-781,552 bytes), while documents stayed at 1, DOM nodes changed from 2,152 to 2,146, and event listeners changed from 633 to 349. This shows no continuing heap, DOM, or listener accumulation. Finally, six interleaved 15-second headless traces in the order JIT off/on/on/ off/off/on used `Simulation.interact` for the same scripted cap and axis input. All six produced the same 15 serial samples, three-event stream, final visual state, frame CRC sequence, state CRC sequence, and final 2,048-byte framebuffer SHA-256 `555282e04487bfaa8c458f90403f535bfe7aada59b0725c8f9df164ac809b00b`.
LICENSE
MIT License Copyright (c) 2026 Murray Stott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
CMakeLists.txt
cmake_minimum_required(VERSION 3.16) # Restrict the graph to the game and its declared transitive dependencies. # ESP-IDF's internal Wi-Fi/PHY/Ethernet master switches default on whenever # those components enter the graph and cannot be overridden from sdkconfig, so # component exclusion is the fail-closed control rather than an unused `n`. set(COMPONENTS main) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(oled-fps-s3) idf_build_get_property(oled_fps_build_components BUILD_COMPONENTS) set(oled_fps_forbidden_components bt esp_coex esp_eth esp_netif esp_netif_stack esp_phy esp_psram esp_wifi ieee802154 lwip openthread wpa_supplicant ) foreach(component IN LISTS oled_fps_forbidden_components) if(component IN_LIST oled_fps_build_components) message(FATAL_ERROR "radio/network component entered OLED FPS build: ${component}") endif() endforeach()
sdkconfig.defaults
CONFIG_IDF_TARGET="esp32s3" # Exact release geometry: 4 MB flash and one 1.5 MB factory app. The project # CMake guard excludes the esp_psram component entirely. CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_COMPILER_OPTIMIZATION_PERF=y CONFIG_ESP32S3_DATA_CACHE_32KB=y CONFIG_FREERTOS_UNICORE=y # This is a self-contained display/input game. The project CMake allowlist # excludes and then rejects every radio/network component. Their Kconfig # symbols are consequently absent rather than silently retaining IDF defaults. # UART0 only. The default USB-Serial-JTAG secondary duplicates every byte in # the emulator's merged serial stream. CONFIG_ESP_CONSOLE_SECONDARY_NONE=y CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 # Preserve normal watchdog protections. The 20 Hz loop yields every tick. CONFIG_ESP_TASK_WDT_INIT=y # Pin every app-descriptor input and remove path/time macros from the build. CONFIG_APP_PROJECT_VER_FROM_CONFIG=y CONFIG_APP_PROJECT_VER="bre209-1" CONFIG_APP_REPRODUCIBLE_BUILD=y
sdkconfig.diagnostic
CONFIG_OLED_FPS_DIAGNOSTIC=y
partitions.csv
# Name, Type, SubType, Offset, Size nvs, data, nvs, 0x9000, 0x6000 factory, app, factory, 0x10000, 0x180000
main/CMakeLists.txt
idf_component_register( SRCS "app_main.c" "content.c" "display_sh1107.c" "game.c" "input.c" "render.c" INCLUDE_DIRS "." PRIV_REQUIRES driver esp_adc esp_driver_gpio esp_driver_uart vfs )
main/Kconfig.projbuild
menu "OLED dungeon" config OLED_FPS_DIAGNOSTIC bool "Run the ADC/GPIO/SH1107 diagnostic loop instead of the game" default n help Builds the Phase-2 platform probe. It samples the real ADC1/GPIO inputs, sends full 2048-byte check frames over I2C0, and prints one structured diagnostic line per second. Leave disabled for game.bin. endmenu
main/app_main.c
/* ESP-IDF platform loop for the original BRE-209 OLED dungeon. MIT. */ #include <inttypes.h> #include <stdio.h> #include <string.h> #include "driver/uart.h" #include "driver/uart_vfs.h" #include "esp_check.h" #include "esp_err.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "display_sh1107.h" #include "game.h" #include "input.h" #include "render.h" #include "sdkconfig.h" #if !CONFIG_OLED_FPS_DIAGNOSTIC static const char *mode_name(FpsMode mode) { switch (mode) { case FPS_MODE_TITLE: return "title"; case FPS_MODE_PLAY: return "play"; case FPS_MODE_WIN: return "win"; case FPS_MODE_DEATH: return "death"; default: return "unknown"; } } static void print_events(FpsTickEvents events, const FpsGameState *state) { if (events.shot) { printf("[oled-fps] event=shot hit=%u killed=%u\n", events.hit ? 1u : 0u, events.killed ? 1u : 0u); } if (events.won) { printf("[oled-fps] event=win tick=%" PRIu32 "\n", state->tick); } if (events.died) { printf("[oled-fps] event=death tick=%" PRIu32 "\n", state->tick); } if (events.restarted) { printf("[oled-fps] event=restart tick=%" PRIu32 "\n", state->tick); } } #endif #if CONFIG_OLED_FPS_DIAGNOSTIC static void render_diagnostic(uint8_t framebuffer[OLED_FPS_FB_SIZE], uint32_t frame) { for (size_t i = 0u; i < OLED_FPS_FB_SIZE; i++) { const uint8_t page = (uint8_t)(i / OLED_FPS_FB_WIDTH); const uint8_t column = (uint8_t)(i % OLED_FPS_FB_WIDTH); const uint8_t bit = (uint8_t)(page & 7u); framebuffer[i] = (uint8_t)(((column + frame) & 8u) != 0u ? (uint8_t)(UINT8_C(1) << bit) : (uint8_t)(UINT8_C(0x80) >> bit)); } } #endif void app_main(void) { static FpsDisplay display; static FpsInputDevice input_device; static FpsGameState game; static uint8_t framebuffer[OLED_FPS_FB_SIZE]; ESP_ERROR_CHECK(uart_driver_install(UART_NUM_0, 256, 0, 0, NULL, 0)); uart_vfs_dev_use_driver(UART_NUM_0); (void)setvbuf(stdout, NULL, _IONBF, 0); printf("[oled-fps] boot version=2 display=sh1107 128x128\n"); ESP_ERROR_CHECK(fps_display_init(&display)); ESP_ERROR_CHECK(fps_input_init(&input_device)); ESP_ERROR_CHECK(fps_input_calibrate(&input_device, 16u)); fps_game_init(&game, 1u, OLED_FPS_FIXED_SEED); fps_render_frame(&game, framebuffer); ESP_ERROR_CHECK(fps_display_frame(&display, framebuffer)); printf("[oled-fps] ready level=1 tick_hz=20 render_hz=20\n"); TickType_t last_wake = xTaskGetTickCount(); uint32_t frame = 0u; for (;;) { FpsInput input = { 0 }; ESP_ERROR_CHECK(fps_input_sample(&input_device, &input)); #if CONFIG_OLED_FPS_DIAGNOSTIC render_diagnostic(framebuffer, frame); #else const FpsTickEvents events = fps_game_tick(&game, input); fps_render_frame(&game, framebuffer); print_events(events, &game); #endif ESP_ERROR_CHECK(fps_display_frame(&display, framebuffer)); frame++; if ((frame % OLED_FPS_TICK_HZ) == 0u) { #if CONFIG_OLED_FPS_DIAGNOSTIC printf("[oled-fps] diag frame=%" PRIu32 " raw_x=%" PRId32 " raw_y=%" PRId32 " turn=%d move=%d switch=%u frame_crc=%08" PRIx32 "\n", frame, input_device.raw_x, input_device.raw_y, input.turn, input.move, input.fire ? 1u : 0u, fps_frame_crc32(framebuffer)); #else printf("[oled-fps] sample tick=%" PRIu32 " frame=%" PRIu32 " state=%s hp=%u ammo=%u kills=%u enemies=%u crc32=%08" PRIx32 " state_crc=%08" PRIx32 "\n", game.tick, frame, mode_name(game.mode), game.health, game.ammo, game.kills, fps_game_alive_enemies(&game), fps_frame_crc32(framebuffer), fps_game_state_crc32(&game)); #endif } xTaskDelayUntil(&last_wake, pdMS_TO_TICKS(1000u / OLED_FPS_TICK_HZ)); } }
main/content.c
/* * The level, placements, names, and geometric content in this file were * designed from scratch for BRE-209. No third-party game maps or art were * consulted or adapted. License: MIT. */ #include "content.h" #define F(c) ((fps_fixed_t)((c) * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF)) /* * 16x16 "Split Keep". A west courtyard gives the player an immediate, clear * encounter before the hollow central keep. The keep can be crossed through * two auto-opening doors or bypassed through either the north or south breach. * Values 1..4 are distinct wall materials and 5 is a door. Empty cells are * zero. Every placement and route was designed specifically for this game. */ static const uint8_t LEVEL_1_CELLS[OLED_FPS_MAP_WIDTH * OLED_FPS_MAP_HEIGHT] = { 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,4,4,0,0,0,4,4,4,0,0,0,0,1, 1,0,0,0,0,0,0,2,2,2,2,2,0,0,0,1, 1,0,0,0,0,0,0,2,0,0,0,2,0,0,0,1, 1,0,0,3,3,0,0,2,0,3,0,2,0,0,0,1, 1,0,0,0,0,0,0,2,0,3,0,2,0,0,0,1, 1,0,0,0,0,0,0,5,0,0,0,5,0,0,0,1, 1,0,0,0,0,0,0,2,0,3,0,2,0,0,0,1, 1,0,0,3,3,0,0,2,0,0,0,2,0,0,0,1, 1,0,0,0,0,0,0,2,0,0,0,2,0,0,0,1, 1,0,0,0,0,0,0,2,2,2,2,2,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,0,0,4,4,0,0,0,0,0,4,4,0,0,0,1, 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, }; static const FpsEnemySpawn LEVEL_1_ENEMIES[] = { { 5u, 7u, 1u }, { 9u, 7u, 2u }, { 13u, 5u, 2u }, { 13u, 10u, 2u }, { 5u, 12u, 2u }, }; static const FpsPickupSpawn LEVEL_1_PICKUPS[] = { { 5u, 5u, FPS_PICKUP_AMMO }, { 9u, 9u, FPS_PICKUP_HEALTH }, { 13u, 12u, FPS_PICKUP_AMMO }, { 3u, 3u, FPS_PICKUP_HEALTH }, }; static const FpsDoorSpawn LEVEL_1_DOORS[] = { { 7u, 7u }, { 11u, 7u }, }; _Static_assert(sizeof(LEVEL_1_ENEMIES) / sizeof(LEVEL_1_ENEMIES[0]) <= OLED_FPS_MAX_ENEMIES, "level enemy inventory exceeds the fixed game-state capacity"); _Static_assert(sizeof(LEVEL_1_PICKUPS) / sizeof(LEVEL_1_PICKUPS[0]) <= OLED_FPS_MAX_PICKUPS, "level pickup inventory exceeds the fixed game-state capacity"); _Static_assert(sizeof(LEVEL_1_DOORS) / sizeof(LEVEL_1_DOORS[0]) <= OLED_FPS_MAX_DOORS, "level door inventory exceeds the fixed game-state capacity"); static const FpsLevel LEVEL_1 = { .cells = LEVEL_1_CELLS, .width = OLED_FPS_MAP_WIDTH, .height = OLED_FPS_MAP_HEIGHT, .start_x = F(2), .start_y = F(7), .start_angle = 0u, .exit_x = F(13), .exit_y = F(7), .enemies = LEVEL_1_ENEMIES, .enemy_count = (uint8_t)(sizeof(LEVEL_1_ENEMIES) / sizeof(LEVEL_1_ENEMIES[0])), .pickups = LEVEL_1_PICKUPS, .pickup_count = (uint8_t)(sizeof(LEVEL_1_PICKUPS) / sizeof(LEVEL_1_PICKUPS[0])), .doors = LEVEL_1_DOORS, .door_count = (uint8_t)(sizeof(LEVEL_1_DOORS) / sizeof(LEVEL_1_DOORS[0])), }; const FpsLevel *fps_content_level(uint8_t level_id) { (void)level_id; return &LEVEL_1; } #undef F
main/content.h
/* First-party MIT-licensed game content for BRE-209. */ #ifndef OLED_FPS_CONTENT_H #define OLED_FPS_CONTENT_H #include <stdint.h> #include "game.h" typedef struct { uint8_t cell_x; uint8_t cell_y; uint8_t health; } FpsEnemySpawn; typedef struct { uint8_t cell_x; uint8_t cell_y; uint8_t kind; } FpsPickupSpawn; typedef struct { uint8_t cell_x; uint8_t cell_y; } FpsDoorSpawn; typedef struct { const uint8_t *cells; uint8_t width; uint8_t height; fps_fixed_t start_x; fps_fixed_t start_y; uint8_t start_angle; fps_fixed_t exit_x; fps_fixed_t exit_y; const FpsEnemySpawn *enemies; uint8_t enemy_count; const FpsPickupSpawn *pickups; uint8_t pickup_count; const FpsDoorSpawn *doors; uint8_t door_count; } FpsLevel; const FpsLevel *fps_content_level(uint8_t level_id); #endif
main/display_sh1107.c
/* Hardware-I2C SH1107 transport for BRE-209. First-party MIT code. */ #include "display_sh1107.h" #include <string.h> #include "freertos/FreeRTOS.h" enum { OLED_ADDRESS = 0x3d, /* The initial 400 kHz correctness run was clean but its 128x128 full-frame * title transition measured a 165.2 ms median. The modeled S3/SH1107 seam * accepts fast-mode-plus timing, so the release performance candidate uses * the plan's explicitly permitted measured 1 MHz path. */ I2C_CLOCK_HZ = 1000000, I2C_TIMEOUT_MS = 100, }; static esp_err_t send_commands(FpsDisplay *display, const uint8_t *commands, size_t count) { uint8_t packet[40]; if (count + 1u > sizeof(packet)) { return ESP_ERR_INVALID_SIZE; } packet[0] = 0x00u; memcpy(&packet[1], commands, count); return i2c_master_write_to_device(I2C_NUM_0, OLED_ADDRESS, packet, count + 1u, pdMS_TO_TICKS(I2C_TIMEOUT_MS)); } esp_err_t fps_display_init(FpsDisplay *display) { memset(display, 0, sizeof(*display)); const i2c_config_t bus_config = { .mode = I2C_MODE_MASTER, .sda_io_num = GPIO_NUM_8, .scl_io_num = GPIO_NUM_9, .sda_pullup_en = GPIO_PULLUP_DISABLE, .scl_pullup_en = GPIO_PULLUP_DISABLE, .master.clk_speed = I2C_CLOCK_HZ, .clk_flags = 0, }; esp_err_t err = i2c_param_config(I2C_NUM_0, &bus_config); if (err != ESP_OK) { return err; } err = i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0u, 0u, 0); if (err != ESP_OK) { return err; } static const uint8_t init_commands[] = { 0xaeu, /* display off */ 0x20u, /* page addressing mode (power-on default) */ 0xa8u, 0x7fu,/* multiplex 1/128 */ 0xd3u, 0x00u,/* display offset */ 0xdcu, 0x00u,/* display start line */ 0x81u, 0x4fu,/* contrast */ 0xadu, 0x8bu,/* internal DC-DC enabled when the display turns on */ 0xa4u, /* display follows RAM */ 0xa6u, /* normal display */ 0xafu, /* display on */ }; return send_commands(display, init_commands, sizeof(init_commands)); } esp_err_t fps_display_frame(FpsDisplay *display, const uint8_t framebuffer[OLED_FPS_FB_SIZE]) { for (uint8_t page = 0u; page < OLED_FPS_FB_HEIGHT / 8u; page++) { /* Co=1 makes each command byte return to control-byte parsing. Co=0, * D/C=1 then streams the complete page. One write per page preserves * the real SH1107 protocol while halving ESP-IDF transaction setup. */ display->transfer[0] = 0x80u; display->transfer[1] = (uint8_t)(0xb0u | page); display->transfer[2] = 0x80u; display->transfer[3] = 0x00u; display->transfer[4] = 0x80u; display->transfer[5] = 0x10u; display->transfer[6] = 0x40u; memcpy(&display->transfer[7], &framebuffer[(size_t)page * OLED_FPS_FB_WIDTH], OLED_FPS_FB_WIDTH); const esp_err_t err = i2c_master_write_to_device(I2C_NUM_0, OLED_ADDRESS, display->transfer, sizeof(display->transfer), pdMS_TO_TICKS(I2C_TIMEOUT_MS)); if (err != ESP_OK) { return err; } } return ESP_OK; }
main/display_sh1107.h
/* ESP-IDF SH1107 transport for the original BRE-209 game. License: MIT. */ #ifndef OLED_FPS_DISPLAY_SH1107_H #define OLED_FPS_DISPLAY_SH1107_H #include <stdint.h> #include "driver/i2c.h" #include "esp_err.h" #include "game.h" typedef struct { /* Co-framed page select (6 bytes), data control byte, then 128 pixels. */ uint8_t transfer[OLED_FPS_FB_WIDTH + 7u]; } FpsDisplay; esp_err_t fps_display_init(FpsDisplay *display); esp_err_t fps_display_frame(FpsDisplay *display, const uint8_t framebuffer[OLED_FPS_FB_SIZE]); #endif
main/game.c
/* Original, platform-free game simulation for BRE-209. License: MIT. */ #include "game.h" #include <limits.h> #include <string.h> #include "content.h" #include "generated_tables.h" _Static_assert(sizeof(int16_t) == 2u, "the trig table requires 16-bit int16_t"); _Static_assert(sizeof(int32_t) == 4u, "Q16.16 requires 32-bit int32_t"); _Static_assert(sizeof(uint32_t) == 4u, "the deterministic RNG requires uint32_t"); _Static_assert(OLED_FPS_MAP_WIDTH <= 24u && OLED_FPS_MAP_HEIGHT <= 24u, "DDA and compact coordinates are sized for maps up to 24x24"); _Static_assert(OLED_FPS_FB_SIZE == 2048u, "SH1107 page framebuffer must be 2048 bytes"); _Static_assert((int64_t)OLED_FPS_FIXED_ONE * OLED_FPS_MAP_WIDTH < INT32_MAX, "map coordinates must fit signed Q16.16"); enum { PLAYER_RADIUS = 11796, /* 0.18 cells in Q16.16. */ PLAYER_SPEED = 4259, /* 0.065 cells per 20 Hz tick at full input. */ ENEMY_SPEED = 1311, /* 0.020 cells per AI step. */ DOOR_RANGE = 98304, /* 1.5 cells. */ PICKUP_RANGE = 24576, /* 0.375 cells. */ EXIT_RANGE = 28672, /* 0.4375 cells. */ ENEMY_ATTACK_RANGE = 36045, /* 0.55 cells. */ ENEMY_ATTACK_DAMAGE = 12, ENEMY_ATTACK_COOLDOWN = 16, }; static int32_t clamp_axis(int16_t value) { if (value < -127) { return -127; } if (value > 127) { return 127; } return value; } static int32_t abs_fixed(fps_fixed_t value) { /* Coordinates and deltas are bounded to 24 cells, so INT32_MIN is not a * reachable input and signed negation remains defined. */ return value < 0 ? -value : value; } static int64_t distance_sq(fps_fixed_t ax, fps_fixed_t ay, fps_fixed_t bx, fps_fixed_t by) { const int64_t dx = (int64_t)ax - bx; const int64_t dy = (int64_t)ay - by; return dx * dx + dy * dy; } static int32_t fixed_floor_cell(fps_fixed_t value) { if (value >= 0) { return value / OLED_FPS_FIXED_ONE; } return -1 - (int32_t)((-(int64_t)value - 1) / OLED_FPS_FIXED_ONE); } static int32_t q15_floor(int64_t value) { /* Preserve the arithmetic-right-shift result intentionally used by the * fixed-point design without relying on implementation-defined shifting * of negative signed integers. All callers are bounded far from INT64_MIN. */ if (value >= 0) { return (int32_t)(value / INT64_C(32768)); } return -1 - (int32_t)((-value - 1) / INT64_C(32768)); } static uint32_t next_random(FpsGameState *state) { /* Numerical Recipes LCG, with fixed unsigned overflow defined by C. */ state->rng = state->rng * UINT32_C(1664525) + UINT32_C(1013904223); return state->rng; } static void reset_level(FpsGameState *state, uint8_t level_id, uint32_t seed, FpsMode mode) { const FpsLevel *level = fps_content_level(level_id); memset(state, 0, sizeof(*state)); state->rng = seed; state->initial_seed = seed; state->level_id = level_id; state->player_x = level->start_x; state->player_y = level->start_y; state->player_angle = level->start_angle; state->health = 100u; state->ammo = 12u; state->mode = mode; state->enemy_count = level->enemy_count; for (uint8_t i = 0u; i < level->enemy_count; i++) { state->enemies[i].x = (fps_fixed_t)(level->enemies[i].cell_x * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF); state->enemies[i].y = (fps_fixed_t)(level->enemies[i].cell_y * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF); state->enemies[i].health = level->enemies[i].health; state->enemies[i].alive = true; } state->pickup_count = level->pickup_count; for (uint8_t i = 0u; i < level->pickup_count; i++) { state->pickups[i].x = (fps_fixed_t)(level->pickups[i].cell_x * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF); state->pickups[i].y = (fps_fixed_t)(level->pickups[i].cell_y * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF); state->pickups[i].kind = level->pickups[i].kind; state->pickups[i].active = true; } state->door_count = level->door_count; for (uint8_t i = 0u; i < level->door_count; i++) { state->doors[i].cell_x = level->doors[i].cell_x; state->doors[i].cell_y = level->doors[i].cell_y; } } void fps_game_init(FpsGameState *state, uint8_t level_id, uint32_t seed) { reset_level(state, level_id, seed, FPS_MODE_TITLE); } uint8_t fps_game_cell(const FpsGameState *state, int32_t cell_x, int32_t cell_y) { const FpsLevel *level = fps_content_level(state->level_id); if (cell_x < 0 || cell_y < 0 || cell_x >= level->width || cell_y >= level->height) { return 1u; } const uint8_t material = level->cells[(size_t)cell_y * level->width + (size_t)cell_x]; if (material != 5u) { return material; } for (uint8_t i = 0u; i < state->door_count; i++) { const FpsDoor *door = &state->doors[i]; if (door->cell_x == (uint8_t)cell_x && door->cell_y == (uint8_t)cell_y) { return door->open ? 0u : 5u; } } return 5u; } static bool position_is_open(const FpsGameState *state, fps_fixed_t x, fps_fixed_t y, int32_t radius) { const fps_fixed_t left = x - radius; const fps_fixed_t right = x + radius; const fps_fixed_t top = y - radius; const fps_fixed_t bottom = y + radius; return fps_game_cell(state, fixed_floor_cell(left), fixed_floor_cell(top)) == 0u && fps_game_cell(state, fixed_floor_cell(right), fixed_floor_cell(top)) == 0u && fps_game_cell(state, fixed_floor_cell(left), fixed_floor_cell(bottom)) == 0u && fps_game_cell(state, fixed_floor_cell(right), fixed_floor_cell(bottom)) == 0u; } static void open_nearby_doors(FpsGameState *state) { const int64_t range_sq = (int64_t)DOOR_RANGE * DOOR_RANGE; for (uint8_t i = 0u; i < state->door_count; i++) { FpsDoor *door = &state->doors[i]; const fps_fixed_t x = (fps_fixed_t)(door->cell_x * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF); const fps_fixed_t y = (fps_fixed_t)(door->cell_y * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF); if (!door->open && distance_sq(state->player_x, state->player_y, x, y) <= range_sq) { door->open = true; } } } static void move_player(FpsGameState *state, int32_t move_axis) { const int16_t dir_x = FPS_SIN_Q15[(uint8_t)(state->player_angle + 64u)]; const int16_t dir_y = FPS_SIN_Q15[state->player_angle]; const fps_fixed_t step = (fps_fixed_t)(((int64_t)PLAYER_SPEED * move_axis) / 127); const fps_fixed_t dx = (fps_fixed_t)q15_floor((int64_t)dir_x * step); const fps_fixed_t dy = (fps_fixed_t)q15_floor((int64_t)dir_y * step); const fps_fixed_t next_x = state->player_x + dx; if (position_is_open(state, next_x, state->player_y, PLAYER_RADIUS)) { state->player_x = next_x; } const fps_fixed_t next_y = state->player_y + dy; if (position_is_open(state, state->player_x, next_y, PLAYER_RADIUS)) { state->player_y = next_y; } } static bool line_is_clear(const FpsGameState *state, fps_fixed_t to_x, fps_fixed_t to_y) { const int64_t dx = (int64_t)to_x - state->player_x; const int64_t dy = (int64_t)to_y - state->player_y; for (int32_t step = 1; step < 32; step++) { const fps_fixed_t x = state->player_x + (fps_fixed_t)((dx * step) / 32); const fps_fixed_t y = state->player_y + (fps_fixed_t)((dy * step) / 32); if (fps_game_cell(state, fixed_floor_cell(x), fixed_floor_cell(y)) != 0u) { return false; } } return true; } static bool fire_weapon(FpsGameState *state, bool *killed) { *killed = false; if (state->ammo == 0u) { return false; } state->ammo--; const int16_t dir_x = FPS_SIN_Q15[(uint8_t)(state->player_angle + 64u)]; const int16_t dir_y = FPS_SIN_Q15[state->player_angle]; int32_t best = INT32_MAX; FpsEnemy *target = NULL; for (uint8_t i = 0u; i < state->enemy_count; i++) { FpsEnemy *enemy = &state->enemies[i]; if (!enemy->alive) { continue; } const int64_t dx = (int64_t)enemy->x - state->player_x; const int64_t dy = (int64_t)enemy->y - state->player_y; const int32_t forward = q15_floor(dx * dir_x + dy * dir_y); const int32_t lateral = q15_floor(dx * -dir_y + dy * dir_x); if (forward <= 0 || (int64_t)abs_fixed(lateral) * 5 > forward || forward >= best || !line_is_clear(state, enemy->x, enemy->y)) { continue; } best = forward; target = enemy; } if (target == NULL) { return false; } state->hit_ticks = 4u; target->hit_ticks = 4u; if (target->health > 1u) { target->health--; } else { target->health = 0u; target->alive = false; state->kills++; *killed = true; } return true; } static void collect_pickups(FpsGameState *state) { const int64_t range_sq = (int64_t)PICKUP_RANGE * PICKUP_RANGE; for (uint8_t i = 0u; i < state->pickup_count; i++) { FpsPickup *pickup = &state->pickups[i]; if (!pickup->active || distance_sq(state->player_x, state->player_y, pickup->x, pickup->y) > range_sq) { continue; } if (pickup->kind == FPS_PICKUP_HEALTH) { const uint16_t healed = (uint16_t)state->health + 30u; state->health = (uint8_t)(healed > 100u ? 100u : healed); } else if (pickup->kind == FPS_PICKUP_AMMO) { const uint16_t loaded = (uint16_t)state->ammo + 6u; state->ammo = (uint8_t)(loaded > 20u ? 20u : loaded); } pickup->active = false; } } static bool try_enemy_move(FpsGameState *state, FpsEnemy *enemy, fps_fixed_t dx, fps_fixed_t dy, bool x_first) { const fps_fixed_t next_x = enemy->x + dx; const fps_fixed_t next_y = enemy->y + dy; bool moved = false; if (x_first) { if (position_is_open(state, next_x, enemy->y, PLAYER_RADIUS)) { enemy->x = next_x; moved = true; } if (position_is_open(state, enemy->x, next_y, PLAYER_RADIUS)) { enemy->y = next_y; moved = true; } } else { if (position_is_open(state, enemy->x, next_y, PLAYER_RADIUS)) { enemy->y = next_y; moved = true; } if (position_is_open(state, next_x, enemy->y, PLAYER_RADIUS)) { enemy->x = next_x; moved = true; } } return moved; } static bool update_enemies(FpsGameState *state) { const int64_t attack_sq = (int64_t)ENEMY_ATTACK_RANGE * ENEMY_ATTACK_RANGE; bool died = false; for (uint8_t i = 0u; i < state->enemy_count; i++) { FpsEnemy *enemy = &state->enemies[i]; if (!enemy->alive) { continue; } if (enemy->attack_cooldown > 0u) { enemy->attack_cooldown--; } if (enemy->hit_ticks > 0u) { enemy->hit_ticks--; } const int64_t d2 = distance_sq(state->player_x, state->player_y, enemy->x, enemy->y); if (d2 <= attack_sq) { if (enemy->attack_cooldown == 0u) { enemy->attack_cooldown = ENEMY_ATTACK_COOLDOWN; state->hurt_ticks = 6u; if (state->health <= ENEMY_ATTACK_DAMAGE) { state->health = 0u; state->mode = FPS_MODE_DEATH; died = true; } else { state->health = (uint8_t)(state->health - ENEMY_ATTACK_DAMAGE); } } continue; } if ((state->tick & 1u) != 0u) { continue; } const fps_fixed_t raw_x = state->player_x - enemy->x; const fps_fixed_t raw_y = state->player_y - enemy->y; const int32_t ax = abs_fixed(raw_x); const int32_t ay = abs_fixed(raw_y); const int32_t scale = (ax > ay ? ax : ay) + (ax > ay ? ay : ax) / 2; if (scale == 0) { continue; } const fps_fixed_t dx = (fps_fixed_t)(((int64_t)raw_x * ENEMY_SPEED) / scale); const fps_fixed_t dy = (fps_fixed_t)(((int64_t)raw_y * ENEMY_SPEED) / scale); const bool x_first = (next_random(state) & 1u) != 0u; (void)try_enemy_move(state, enemy, dx, dy, x_first); } return died; } static bool player_reached_exit(const FpsGameState *state) { const FpsLevel *level = fps_content_level(state->level_id); const int64_t range_sq = (int64_t)EXIT_RANGE * EXIT_RANGE; return distance_sq(state->player_x, state->player_y, level->exit_x, level->exit_y) <= range_sq; } static void update_exit_progress(FpsGameState *state) { const FpsLevel *level = fps_content_level(state->level_id); const int64_t distance = distance_sq(state->player_x, state->player_y, level->exit_x, level->exit_y); const int64_t one = OLED_FPS_FIXED_ONE; uint8_t milestone = 0u; if (distance <= (int64_t)2 * one * 2 * one) { milestone = 3u; } else if (distance <= (int64_t)6 * one * 6 * one) { milestone = 2u; } else if (distance <= (int64_t)10 * one * 10 * one) { milestone = 1u; } /* Progress is a reached milestone, not a compass: exploring backward must * not erase an accomplishment already shown on the one-digit OLED HUD. */ if (milestone > state->exit_progress) { state->exit_progress = milestone; } } FpsTickEvents fps_game_tick(FpsGameState *state, FpsInput input) { FpsTickEvents events = { 0 }; const bool rising_fire = input.fire && !state->fire_was_down; const uint8_t level_id = state->level_id; const uint32_t initial_seed = state->initial_seed; if (state->mode == FPS_MODE_TITLE) { state->tick++; state->fire_was_down = input.fire; if (rising_fire) { reset_level(state, level_id, initial_seed, FPS_MODE_PLAY); state->fire_was_down = true; events.restarted = true; } return events; } if (state->mode == FPS_MODE_WIN || state->mode == FPS_MODE_DEATH) { state->tick++; state->fire_was_down = input.fire; if (rising_fire) { reset_level(state, level_id, initial_seed, FPS_MODE_PLAY); state->fire_was_down = true; events.restarted = true; } return events; } state->tick++; if (state->muzzle_ticks > 0u) { state->muzzle_ticks--; } if (state->hit_ticks > 0u) { state->hit_ticks--; } if (state->hurt_ticks > 0u) { state->hurt_ticks--; } const int32_t turn_axis = clamp_axis(input.turn); const int32_t move_axis = clamp_axis(input.move); const int32_t turn_step = turn_axis / 32; state->player_angle = (uint8_t)(state->player_angle + turn_step); open_nearby_doors(state); if (move_axis != 0) { move_player(state, move_axis); open_nearby_doors(state); } if (rising_fire) { events.shot = state->ammo > 0u; if (events.shot) { state->muzzle_ticks = 3u; } events.hit = fire_weapon(state, &events.killed); } collect_pickups(state); events.died = update_enemies(state); update_exit_progress(state); if (!events.died && fps_game_alive_enemies(state) == 0u && player_reached_exit(state)) { state->exit_progress = 4u; state->mode = FPS_MODE_WIN; events.won = true; } state->fire_was_down = input.fire; return events; } uint8_t fps_game_alive_enemies(const FpsGameState *state) { uint8_t count = 0u; for (uint8_t i = 0u; i < state->enemy_count; i++) { if (state->enemies[i].alive) { count++; } } return count; } static uint32_t crc_byte(uint32_t crc, uint8_t value) { crc ^= value; for (uint8_t bit = 0u; bit < 8u; bit++) { const uint32_t mask = UINT32_C(0) - (crc & 1u); crc = (crc >> 1u) ^ (UINT32_C(0xedb88320) & mask); } return crc; } static uint32_t crc_u32(uint32_t crc, uint32_t value) { for (uint8_t shift = 0u; shift < 32u; shift += 8u) { crc = crc_byte(crc, (uint8_t)(value >> shift)); } return crc; } uint32_t fps_game_state_crc32(const FpsGameState *state) { /* Serialize fields explicitly: struct padding is intentionally excluded. */ uint32_t crc = UINT32_C(0xffffffff); crc = crc_u32(crc, state->tick); crc = crc_u32(crc, state->rng); crc = crc_u32(crc, state->initial_seed); crc = crc_u32(crc, (uint32_t)state->player_x); crc = crc_u32(crc, (uint32_t)state->player_y); crc = crc_byte(crc, state->player_angle); crc = crc_byte(crc, state->health); crc = crc_byte(crc, state->ammo); crc = crc_byte(crc, state->kills); crc = crc_byte(crc, state->exit_progress); crc = crc_byte(crc, state->muzzle_ticks); crc = crc_byte(crc, state->hit_ticks); crc = crc_byte(crc, state->hurt_ticks); crc = crc_byte(crc, state->level_id); crc = crc_byte(crc, state->enemy_count); crc = crc_byte(crc, state->pickup_count); crc = crc_byte(crc, state->door_count); crc = crc_byte(crc, (uint8_t)state->mode); crc = crc_byte(crc, state->fire_was_down ? 1u : 0u); for (uint8_t i = 0u; i < state->enemy_count; i++) { const FpsEnemy *enemy = &state->enemies[i]; crc = crc_u32(crc, (uint32_t)enemy->x); crc = crc_u32(crc, (uint32_t)enemy->y); crc = crc_byte(crc, enemy->health); crc = crc_byte(crc, enemy->attack_cooldown); crc = crc_byte(crc, enemy->hit_ticks); crc = crc_byte(crc, enemy->alive ? 1u : 0u); } for (uint8_t i = 0u; i < state->pickup_count; i++) { const FpsPickup *pickup = &state->pickups[i]; crc = crc_u32(crc, (uint32_t)pickup->x); crc = crc_u32(crc, (uint32_t)pickup->y); crc = crc_byte(crc, pickup->kind); crc = crc_byte(crc, pickup->active ? 1u : 0u); } for (uint8_t i = 0u; i < state->door_count; i++) { const FpsDoor *door = &state->doors[i]; crc = crc_byte(crc, door->cell_x); crc = crc_byte(crc, door->cell_y); crc = crc_byte(crc, door->open ? 1u : 0u); } return ~crc; }
main/game.h
/* * Original OLED dungeon game core for Breadboard (BRE-209). * * This file is first-party MIT-licensed code. The portable core depends only * on C11 fixed-width integer types and is shared unchanged by the ESP-IDF * firmware and the host verification harness. */ #ifndef OLED_FPS_GAME_H #define OLED_FPS_GAME_H #include <stdbool.h> #include <stddef.h> #include <stdint.h> #ifdef __cplusplus extern "C" { #endif #define OLED_FPS_FB_WIDTH 128u #define OLED_FPS_FB_HEIGHT 128u #define OLED_FPS_FB_SIZE ((OLED_FPS_FB_WIDTH * OLED_FPS_FB_HEIGHT) / 8u) #define OLED_FPS_MAP_WIDTH 16u #define OLED_FPS_MAP_HEIGHT 16u #define OLED_FPS_MAX_ENEMIES 6u #define OLED_FPS_MAX_PICKUPS 4u #define OLED_FPS_MAX_DOORS 4u #define OLED_FPS_TICK_HZ 20u #define OLED_FPS_FIXED_ONE INT32_C(65536) #define OLED_FPS_FIXED_HALF INT32_C(32768) #define OLED_FPS_FIXED_SEED UINT32_C(0x4f4c4544) typedef int32_t fps_fixed_t; typedef enum { FPS_MODE_TITLE = 0, FPS_MODE_PLAY = 1, FPS_MODE_WIN = 2, FPS_MODE_DEATH = 3, } FpsMode; typedef enum { FPS_PICKUP_HEALTH = 1, FPS_PICKUP_AMMO = 2, } FpsPickupKind; typedef struct { /* Both axes are normalized to [-127, 127]. Positive move is forward; * positive turn rotates clockwise in the game's top-down coordinate * system. */ int16_t turn; int16_t move; bool fire; } FpsInput; typedef struct { fps_fixed_t x; fps_fixed_t y; uint8_t health; uint8_t attack_cooldown; uint8_t hit_ticks; bool alive; } FpsEnemy; typedef struct { fps_fixed_t x; fps_fixed_t y; uint8_t kind; bool active; } FpsPickup; typedef struct { uint8_t cell_x; uint8_t cell_y; bool open; } FpsDoor; typedef struct { uint32_t tick; uint32_t rng; uint32_t initial_seed; fps_fixed_t player_x; fps_fixed_t player_y; uint8_t player_angle; uint8_t health; uint8_t ammo; uint8_t kills; uint8_t exit_progress; uint8_t muzzle_ticks; uint8_t hit_ticks; uint8_t hurt_ticks; uint8_t level_id; uint8_t enemy_count; uint8_t pickup_count; uint8_t door_count; FpsMode mode; bool fire_was_down; FpsEnemy enemies[OLED_FPS_MAX_ENEMIES]; FpsPickup pickups[OLED_FPS_MAX_PICKUPS]; FpsDoor doors[OLED_FPS_MAX_DOORS]; } FpsGameState; typedef struct { bool shot; bool hit; bool killed; bool won; bool died; bool restarted; } FpsTickEvents; void fps_game_init(FpsGameState *state, uint8_t level_id, uint32_t seed); FpsTickEvents fps_game_tick(FpsGameState *state, FpsInput input); uint8_t fps_game_cell(const FpsGameState *state, int32_t cell_x, int32_t cell_y); uint8_t fps_game_alive_enemies(const FpsGameState *state); uint32_t fps_game_state_crc32(const FpsGameState *state); #ifdef __cplusplus } #endif #endif
main/input.c
/* Real ADC1/GPIO joystick path for BRE-209. First-party MIT code. */ #include "input.h" #include <string.h> #include "driver/gpio.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" enum { JOYSTICK_X_CHANNEL = ADC_CHANNEL_0, /* GPIO1 on ESP32-S3. */ JOYSTICK_Y_CHANNEL = ADC_CHANNEL_1, /* GPIO2 on ESP32-S3. */ JOYSTICK_SWITCH_GPIO = GPIO_NUM_10, DEAD_ZONE_ENTER = 240, DEAD_ZONE_EXIT = 160, AXIS_FULL_SCALE = 1700, BUTTON_DEBOUNCE_TICKS = 2, }; static int16_t normalize_axis(int32_t raw, int32_t center, bool *active) { const int32_t delta = raw - center; const int32_t magnitude = delta < 0 ? -delta : delta; if (*active) { if (magnitude <= DEAD_ZONE_EXIT) { *active = false; return 0; } } else if (magnitude < DEAD_ZONE_ENTER) { return 0; } else { *active = true; } int32_t normalized = (delta * 127) / AXIS_FULL_SCALE; if (normalized < -127) { normalized = -127; } else if (normalized > 127) { normalized = 127; } return (int16_t)normalized; } esp_err_t fps_input_init(FpsInputDevice *device) { memset(device, 0, sizeof(*device)); const adc_oneshot_unit_init_cfg_t unit_config = { .unit_id = ADC_UNIT_1, .ulp_mode = ADC_ULP_MODE_DISABLE, }; esp_err_t err = adc_oneshot_new_unit(&unit_config, &device->adc); if (err != ESP_OK) { return err; } const adc_oneshot_chan_cfg_t channel_config = { .atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_DEFAULT, }; err = adc_oneshot_config_channel(device->adc, JOYSTICK_X_CHANNEL, &channel_config); if (err != ESP_OK) { return err; } err = adc_oneshot_config_channel(device->adc, JOYSTICK_Y_CHANNEL, &channel_config); if (err != ESP_OK) { return err; } const gpio_config_t switch_config = { .pin_bit_mask = UINT64_C(1) << JOYSTICK_SWITCH_GPIO, .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; err = gpio_config(&switch_config); if (err != ESP_OK) { return err; } device->center_x = 2048; device->center_y = 2048; return ESP_OK; } esp_err_t fps_input_calibrate(FpsInputDevice *device, uint8_t samples) { if (samples == 0u) { return ESP_ERR_INVALID_ARG; } int32_t sum_x = 0; int32_t sum_y = 0; for (uint8_t i = 0u; i < samples; i++) { int raw_x = 0; int raw_y = 0; esp_err_t err = adc_oneshot_read(device->adc, JOYSTICK_X_CHANNEL, &raw_x); if (err != ESP_OK) { return err; } err = adc_oneshot_read(device->adc, JOYSTICK_Y_CHANNEL, &raw_y); if (err != ESP_OK) { return err; } sum_x += raw_x; sum_y += raw_y; vTaskDelay(pdMS_TO_TICKS(2)); } device->center_x = sum_x / samples; device->center_y = sum_y / samples; return ESP_OK; } esp_err_t fps_input_sample(FpsInputDevice *device, FpsInput *input) { int raw_x = 0; int raw_y = 0; esp_err_t err = adc_oneshot_read(device->adc, JOYSTICK_X_CHANNEL, &raw_x); if (err != ESP_OK) { return err; } err = adc_oneshot_read(device->adc, JOYSTICK_Y_CHANNEL, &raw_y); if (err != ESP_OK) { return err; } device->raw_x = raw_x; device->raw_y = raw_y; const bool candidate = gpio_get_level(JOYSTICK_SWITCH_GPIO) == 0; if (candidate == device->button_candidate) { if (device->button_stable_ticks < BUTTON_DEBOUNCE_TICKS) { device->button_stable_ticks++; } } else { device->button_candidate = candidate; device->button_stable_ticks = 1u; } if (device->button_stable_ticks >= BUTTON_DEBOUNCE_TICKS) { device->button_down = candidate; } input->turn = normalize_axis(raw_x, device->center_x, &device->x_active); input->move = normalize_axis(raw_y, device->center_y, &device->y_active); input->fire = device->button_down; return ESP_OK; }
main/input.h
/* ESP-IDF ADC/GPIO input adapter for the original BRE-209 game. MIT. */ #ifndef OLED_FPS_INPUT_H #define OLED_FPS_INPUT_H #include <stdbool.h> #include <stdint.h> #include "esp_adc/adc_oneshot.h" #include "esp_err.h" #include "game.h" typedef struct { adc_oneshot_unit_handle_t adc; int32_t center_x; int32_t center_y; int32_t raw_x; int32_t raw_y; bool x_active; bool y_active; bool button_candidate; bool button_down; uint8_t button_stable_ticks; } FpsInputDevice; esp_err_t fps_input_init(FpsInputDevice *device); esp_err_t fps_input_calibrate(FpsInputDevice *device, uint8_t samples); esp_err_t fps_input_sample(FpsInputDevice *device, FpsInput *input); #endif
main/render.c
/* Original integer DDA renderer and geometric OLED art for BRE-209. MIT. */ #include "render.h" #include <limits.h> #include <string.h> #include "content.h" #include "generated_tables.h" enum { VIEW_HEIGHT = 104, HORIZON_Y = 50, FOV_TURN_UNITS = 44, /* Approximately 62 degrees in the 8-bit turn space. */ MIN_RAY_DISTANCE = 8192, }; static void set_pixel(uint8_t *framebuffer, int32_t x, int32_t y, bool on) { if (x < 0 || x >= (int32_t)OLED_FPS_FB_WIDTH || y < 0 || y >= (int32_t)OLED_FPS_FB_HEIGHT) { return; } const size_t index = (size_t)(y >> 3) * OLED_FPS_FB_WIDTH + (size_t)x; const uint8_t mask = (uint8_t)(1u << (uint32_t)(y & 7)); if (on) { framebuffer[index] |= mask; } else { framebuffer[index] &= (uint8_t)~mask; } } static void fill_rect(uint8_t *framebuffer, int32_t x, int32_t y, int32_t width, int32_t height, bool on) { for (int32_t py = y; py < y + height; py++) { for (int32_t px = x; px < x + width; px++) { set_pixel(framebuffer, px, py, on); } } } #define G(a, b, c, d, e) ((uint16_t)(((a) << 12) | ((b) << 9) | ((c) << 6) | ((d) << 3) | (e))) /* Hand-authored 3x5 block glyphs for this game's deliberately small copy. */ static uint16_t glyph(char c) { switch (c) { case 'A': return G(2, 5, 7, 5, 5); case 'B': return G(6, 5, 6, 5, 6); case 'C': return G(3, 4, 4, 4, 3); case 'D': return G(6, 5, 5, 5, 6); case 'E': return G(7, 4, 6, 4, 7); case 'F': return G(7, 4, 6, 4, 4); case 'G': return G(3, 4, 5, 5, 3); case 'H': return G(5, 5, 7, 5, 5); case 'I': return G(7, 2, 2, 2, 7); case 'J': return G(1, 1, 1, 5, 2); case 'K': return G(5, 5, 6, 5, 5); case 'L': return G(4, 4, 4, 4, 7); case 'M': return G(5, 7, 7, 5, 5); case 'N': return G(5, 7, 7, 7, 5); case 'O': return G(2, 5, 5, 5, 2); case 'P': return G(6, 5, 6, 4, 4); case 'Q': return G(2, 5, 5, 3, 1); case 'R': return G(6, 5, 6, 5, 5); case 'S': return G(3, 4, 2, 1, 6); case 'T': return G(7, 2, 2, 2, 2); case 'U': return G(5, 5, 5, 5, 7); case 'V': return G(5, 5, 5, 5, 2); case 'W': return G(5, 5, 7, 7, 5); case 'X': return G(5, 5, 2, 5, 5); case 'Y': return G(5, 5, 2, 2, 2); case 'Z': return G(7, 1, 2, 4, 7); case '0': return G(7, 5, 5, 5, 7); case '1': return G(2, 6, 2, 2, 7); case '2': return G(6, 1, 7, 4, 7); case '3': return G(6, 1, 3, 1, 6); case '4': return G(5, 5, 7, 1, 1); case '5': return G(7, 4, 6, 1, 6); case '6': return G(3, 4, 6, 5, 2); case '7': return G(7, 1, 2, 2, 2); case '8': return G(2, 5, 2, 5, 2); case '9': return G(2, 5, 3, 1, 6); case ':': return G(0, 2, 0, 2, 0); case '-': return G(0, 0, 7, 0, 0); case '/': return G(1, 1, 2, 4, 4); case '.': return G(0, 0, 0, 0, 2); default: return 0u; } } #undef G static void draw_char(uint8_t *framebuffer, int32_t x, int32_t y, char c) { const uint16_t bits = glyph(c); for (int32_t row = 0; row < 5; row++) { const uint32_t shift = (uint32_t)((4 - row) * 3); const uint8_t row_bits = (uint8_t)((bits >> shift) & 7u); for (int32_t col = 0; col < 3; col++) { if ((row_bits & (uint8_t)(4u >> (uint32_t)col)) != 0u) { set_pixel(framebuffer, x + col, y + row, true); } } } } static void draw_text(uint8_t *framebuffer, int32_t x, int32_t y, const char *text) { while (*text != '\0') { draw_char(framebuffer, x, y, *text++); x += 4; } } static void draw_char_scaled(uint8_t *framebuffer, int32_t x, int32_t y, char c, int32_t scale) { const uint16_t bits = glyph(c); for (int32_t row = 0; row < 5; row++) { const uint32_t shift = (uint32_t)((4 - row) * 3); const uint8_t row_bits = (uint8_t)((bits >> shift) & 7u); for (int32_t col = 0; col < 3; col++) { if ((row_bits & (uint8_t)(4u >> (uint32_t)col)) != 0u) { fill_rect(framebuffer, x + col * scale, y + row * scale, scale, scale, true); } } } } static void draw_text_scaled(uint8_t *framebuffer, int32_t x, int32_t y, const char *text, int32_t scale) { while (*text != '\0') { draw_char_scaled(framebuffer, x, y, *text++, scale); x += 4 * scale; } } static int32_t centered_text_x(const char *text, int32_t scale) { const int32_t width = (int32_t)strlen(text) * 4 * scale - scale; return ((int32_t)OLED_FPS_FB_WIDTH - width) / 2; } static void draw_u8_3(uint8_t *framebuffer, int32_t x, int32_t y, uint8_t value) { draw_char(framebuffer, x, y, (char)('0' + value / 100u)); draw_char(framebuffer, x + 4, y, (char)('0' + (value / 10u) % 10u)); draw_char(framebuffer, x + 8, y, (char)('0' + value % 10u)); } static int32_t abs_i32(int32_t value) { return value < 0 ? -value : value; } static int32_t q15_floor(int64_t value) { /* Match an arithmetic 15-bit right shift while keeping negative values * fully defined by C11. Ray/map bounds keep value far from INT64_MIN. */ if (value >= 0) { return (int32_t)(value / INT64_C(32768)); } return -1 - (int32_t)((-value - 1) / INT64_C(32768)); } static int32_t fixed_floor_cell(fps_fixed_t value) { if (value >= 0) { return value / OLED_FPS_FIXED_ONE; } return -1 - (int32_t)((-(int64_t)value - 1) / OLED_FPS_FIXED_ONE); } static uint8_t wall_density(uint8_t material, bool y_side, int32_t distance) { static const uint8_t density_by_material[6] = { 0u, 16u, 12u, 9u, 6u, 14u }; uint8_t density = material < 6u ? density_by_material[material] : 8u; const int32_t distance_band = distance >> 17; /* One band per two cells. */ if (distance_band > 0) { const uint8_t fade = (uint8_t)(distance_band > 4 ? 4 : distance_band); density = density > fade ? (uint8_t)(density - fade) : 1u; } if (y_side && density > 2u) { density = (uint8_t)(density - 2u); } return density; } static void draw_world_backdrop(const FpsGameState *state, uint8_t *framebuffer) { /* Sparse ceiling motes and a perspective floor make the view readable * without pretending the one-bit panel can carry colour or gray pixels. */ for (int32_t y = 4; y < HORIZON_Y - 5; y += 7) { const int32_t phase = (int32_t)((state->player_angle >> 3u) + (uint8_t)y) & 15; for (int32_t x = phase; x < (int32_t)OLED_FPS_FB_WIDTH; x += 23) { set_pixel(framebuffer, x, y, true); } } for (int32_t x = 0; x < (int32_t)OLED_FPS_FB_WIDTH; x += 2) { set_pixel(framebuffer, x, HORIZON_Y, true); } for (int32_t y = HORIZON_Y + 8; y < VIEW_HEIGHT; y++) { const int32_t row = y - HORIZON_Y; const int32_t stride = row < 18 ? 17 : (row < 32 ? 11 : 7); const int32_t phase = ((state->player_x >> 13) + row * 3) % stride; if ((row & 3) == 0 || row == 12 || row == 22 || row == 34 || row == 44) { for (int32_t x = phase; x < (int32_t)OLED_FPS_FB_WIDTH; x += stride) { set_pixel(framebuffer, x, y, true); } } } } static void cast_walls(const FpsGameState *state, uint8_t *framebuffer, int32_t depth[OLED_FPS_FB_WIDTH]) { for (int32_t column = 0; column < (int32_t)OLED_FPS_FB_WIDTH; column++) { const int32_t offset = (column * FOV_TURN_UNITS) / ((int32_t)OLED_FPS_FB_WIDTH - 1) - FOV_TURN_UNITS / 2; const uint8_t angle = (uint8_t)(state->player_angle + offset); const int32_t ray_x = FPS_SIN_Q15[(uint8_t)(angle + 64u)]; const int32_t ray_y = FPS_SIN_Q15[angle]; int32_t map_x = fixed_floor_cell(state->player_x); int32_t map_y = fixed_floor_cell(state->player_y); int32_t step_x; int32_t step_y; int32_t side_x; int32_t side_y; const int32_t delta_x = ray_x == 0 ? INT32_MAX / 4 : (int32_t)((INT64_C(1) << 31) / abs_i32(ray_x)); const int32_t delta_y = ray_y == 0 ? INT32_MAX / 4 : (int32_t)((INT64_C(1) << 31) / abs_i32(ray_y)); if (ray_x < 0) { step_x = -1; side_x = (int32_t)(((int64_t)(state->player_x - (map_x * OLED_FPS_FIXED_ONE)) << 15) / -ray_x); } else if (ray_x > 0) { step_x = 1; side_x = (int32_t)(((int64_t)(((map_x + 1) * OLED_FPS_FIXED_ONE) - state->player_x) << 15) / ray_x); } else { step_x = 0; side_x = INT32_MAX / 4; } if (ray_y < 0) { step_y = -1; side_y = (int32_t)(((int64_t)(state->player_y - (map_y * OLED_FPS_FIXED_ONE)) << 15) / -ray_y); } else if (ray_y > 0) { step_y = 1; side_y = (int32_t)(((int64_t)(((map_y + 1) * OLED_FPS_FIXED_ONE) - state->player_y) << 15) / ray_y); } else { step_y = 0; side_y = INT32_MAX / 4; } uint8_t material = 1u; bool y_side = false; int32_t distance = MIN_RAY_DISTANCE; for (uint8_t step = 0u; step < 48u; step++) { if (side_x < side_y) { distance = side_x; side_x = side_x > INT32_MAX - delta_x ? INT32_MAX : side_x + delta_x; map_x += step_x; y_side = false; } else { distance = side_y; side_y = side_y > INT32_MAX - delta_y ? INT32_MAX : side_y + delta_y; map_y += step_y; y_side = true; } material = fps_game_cell(state, map_x, map_y); if (material != 0u) { break; } } if (distance < MIN_RAY_DISTANCE) { distance = MIN_RAY_DISTANCE; } depth[column] = distance; int32_t height = (int32_t)(((int64_t)VIEW_HEIGHT * 3 * OLED_FPS_FIXED_ONE) / (2 * (int64_t)distance)); if (height > VIEW_HEIGHT) { height = VIEW_HEIGHT; } if (height < 1) { height = 1; } const int32_t top = HORIZON_Y - height / 2; const uint8_t density = wall_density(material, y_side, distance); fill_rect(framebuffer, column, top, 1, height, false); for (int32_t y = top; y < top + height; y++) { const uint8_t threshold = FPS_BAYER4[((uint32_t)y & 3u) * 4u + ((uint32_t)column & 3u)]; if (threshold < density) { set_pixel(framebuffer, column, y, true); } } } } enum { BILLBOARD_ENEMY = 0, BILLBOARD_EXIT = 3, }; static void draw_billboard(const FpsGameState *state, uint8_t *framebuffer, const int32_t depth[OLED_FPS_FB_WIDTH], fps_fixed_t x, fps_fixed_t y, uint8_t kind, bool hit) { const int32_t dir_x = FPS_SIN_Q15[(uint8_t)(state->player_angle + 64u)]; const int32_t dir_y = FPS_SIN_Q15[state->player_angle]; const int64_t dx = (int64_t)x - state->player_x; const int64_t dy = (int64_t)y - state->player_y; const int32_t forward = q15_floor(dx * dir_x + dy * dir_y); const int32_t lateral = q15_floor(dx * -dir_y + dy * dir_x); if (forward <= MIN_RAY_DISTANCE || (int64_t)abs_i32(lateral) * 2 > forward) { return; } const int32_t center_x = 64 + (int32_t)(((int64_t)lateral * 92) / forward); int32_t size = (int32_t)(((int64_t)58 * OLED_FPS_FIXED_ONE) / forward); if (size < 5) { size = 5; } if (size > 74) { size = 74; } const int32_t center_y = HORIZON_Y + size / 5; const int32_t half = size / 2; for (int32_t px = center_x - half; px <= center_x + half; px++) { if (px < 0 || px >= (int32_t)OLED_FPS_FB_WIDTH || forward >= depth[px]) { continue; } for (int32_t py = center_y - half; py <= center_y + half; py++) { const int32_t sx = px - center_x; const int32_t sy = py - center_y; const int32_t third = half > 5 ? half / 3 : 1; bool on = false; if (kind == BILLBOARD_ENEMY) { /* Original horned keep-warden. Bright eyes and a broad body * remain legible even when perspective shrinks it. */ const int32_t eye = half > 9 ? 1 : 0; const bool horns = sy <= -third && sy >= -half && abs_i32(sx) >= third && abs_i32(sx) <= half - (sy + half) / 3; const bool head = sy >= -third * 2 && sy <= 0 && abs_i32(sx) <= third; const bool eyes = sy >= -third && sy <= -third + eye && (abs_i32(sx - third / 2) <= eye || abs_i32(sx + third / 2) <= eye); const bool torso = sy > -third / 2 && sy <= third && abs_i32(sx) <= half - abs_i32(sy) / 2; const bool arms = sy >= 0 && sy <= third && abs_i32(sx) >= third && abs_i32(sx) <= half; const bool legs = sy > third && sy <= half && abs_i32(sx) >= third / 3 && abs_i32(sx) <= third; on = horns || eyes || arms || legs || (head && (hit || ((sx + sy) & 1) == 0)) || (torso && (hit || ((sx - sy) & 2) == 0)); } else if (kind == FPS_PICKUP_HEALTH) { const int32_t stroke = size > 14 ? 2 : 1; on = (abs_i32(sx) <= stroke && abs_i32(sy) <= half * 2 / 3) || (abs_i32(sy) <= stroke && abs_i32(sx) <= half * 2 / 3) || abs_i32(sx) == half || abs_i32(sy) == half; } else if (kind == FPS_PICKUP_AMMO) { const int32_t cartridge = half > 4 ? half / 3 : 1; on = (abs_i32(sx) >= cartridge - 1 && abs_i32(sx) <= cartridge + 1 && sy >= -half / 2 && sy <= half) || (abs_i32(sx) <= half * 2 / 3 && abs_i32(sy - half) <= 1); } else if (kind == BILLBOARD_EXIT) { const bool locked = fps_game_alive_enemies(state) != 0u; const int32_t post = half > 8 ? 2 : 1; const bool frame = (abs_i32(abs_i32(sx) - half) <= post && sy >= -half) || (abs_i32(sy + half) <= post && abs_i32(sx) <= half); const bool rune = locked ? abs_i32(abs_i32(sx) - abs_i32(sy)) <= post : (abs_i32(sx) <= post && sy >= -third && sy <= third); on = frame || rune; } if (on) { set_pixel(framebuffer, px, py, true); } } } } typedef struct { fps_fixed_t x; fps_fixed_t y; int64_t distance; uint8_t kind; bool hit; } RenderEntity; static void draw_entities(const FpsGameState *state, uint8_t *framebuffer, const int32_t depth[OLED_FPS_FB_WIDTH]) { RenderEntity entities[OLED_FPS_MAX_ENEMIES + OLED_FPS_MAX_PICKUPS + 1u]; uint8_t count = 0u; for (uint8_t i = 0u; i < state->pickup_count; i++) { const FpsPickup *pickup = &state->pickups[i]; if (pickup->active) { entities[count++] = (RenderEntity){ pickup->x, pickup->y, (int64_t)(pickup->x - state->player_x) * (pickup->x - state->player_x) + (int64_t)(pickup->y - state->player_y) * (pickup->y - state->player_y), pickup->kind, false, }; } } for (uint8_t i = 0u; i < state->enemy_count; i++) { const FpsEnemy *enemy = &state->enemies[i]; if (enemy->alive) { entities[count++] = (RenderEntity){ enemy->x, enemy->y, (int64_t)(enemy->x - state->player_x) * (enemy->x - state->player_x) + (int64_t)(enemy->y - state->player_y) * (enemy->y - state->player_y), BILLBOARD_ENEMY, enemy->hit_ticks > 0u, }; } } const FpsLevel *level = fps_content_level(state->level_id); entities[count++] = (RenderEntity){ level->exit_x, level->exit_y, (int64_t)(level->exit_x - state->player_x) * (level->exit_x - state->player_x) + (int64_t)(level->exit_y - state->player_y) * (level->exit_y - state->player_y), BILLBOARD_EXIT, false, }; /* Painter's order for overlapping sprites; the wall depth buffer remains * authoritative per column. The fixed inventory keeps this tiny. */ for (uint8_t first = 0u; first < count; first++) { uint8_t farthest = first; for (uint8_t next = (uint8_t)(first + 1u); next < count; next++) { if (entities[next].distance > entities[farthest].distance) { farthest = next; } } const RenderEntity swap = entities[first]; entities[first] = entities[farthest]; entities[farthest] = swap; draw_billboard(state, framebuffer, depth, entities[first].x, entities[first].y, entities[first].kind, entities[first].hit); } } static void draw_hud(const FpsGameState *state, uint8_t *framebuffer) { fill_rect(framebuffer, 0, VIEW_HEIGHT, OLED_FPS_FB_WIDTH, OLED_FPS_FB_HEIGHT - VIEW_HEIGHT, false); for (int32_t x = 0; x < (int32_t)OLED_FPS_FB_WIDTH; x += 2) { set_pixel(framebuffer, x, VIEW_HEIGHT, true); } draw_char(framebuffer, 2, 107, 'H'); draw_u8_3(framebuffer, 7, 107, state->health); draw_char(framebuffer, 39, 107, 'A'); draw_u8_3(framebuffer, 44, 107, state->ammo); draw_char(framebuffer, 78, 107, 'K'); draw_char(framebuffer, 83, 107, (char)('0' + state->kills)); draw_char(framebuffer, 87, 107, '/'); draw_char(framebuffer, 91, 107, (char)('0' + state->enemy_count)); draw_text(framebuffer, 2, 119, fps_game_alive_enemies(state) == 0u ? "GATE OPEN" : "GATE LOCKED"); draw_text(framebuffer, 92, 119, "WASD"); } static void draw_title(uint8_t *framebuffer) { for (int32_t x = 5; x < 123; x++) { set_pixel(framebuffer, x, 5, true); set_pixel(framebuffer, x, 75, true); } for (int32_t y = 5; y <= 75; y++) { set_pixel(framebuffer, 5, y, true); set_pixel(framebuffer, 122, y, true); } draw_text_scaled(framebuffer, centered_text_x("SPLIT", 2), 13, "SPLIT", 2); draw_text_scaled(framebuffer, centered_text_x("KEEP", 2), 27, "KEEP", 2); /* The same original warden silhouette as the game, enlarged into a title * crest so the objective is obvious before the first input. */ for (int32_t y = 43; y <= 68; y++) { const int32_t dy = y - 55; for (int32_t x = 49; x <= 79; x++) { const int32_t dx = x - 64; const bool horns = y < 50 && abs_i32(dx) >= 7 && abs_i32(dx) <= 14; const bool eyes = y >= 51 && y <= 53 && (abs_i32(dx - 5) <= 1 || abs_i32(dx + 5) <= 1); const bool body = y >= 49 && y <= 64 && abs_i32(dx) <= 11 - abs_i32(dy) / 3; const bool legs = y > 63 && (abs_i32(dx - 5) <= 2 || abs_i32(dx + 5) <= 2); if (horns || eyes || legs || (body && ((x + y) & 1) == 0)) { set_pixel(framebuffer, x, y, true); } } } draw_text(framebuffer, centered_text_x("KILL 5 WARDENS", 1), 82, "KILL 5 WARDENS"); draw_text(framebuffer, centered_text_x("WASD MOVE TURN", 1), 94, "WASD MOVE TURN"); draw_text(framebuffer, centered_text_x("SPACE OR CAP FIRE", 1), 105, "SPACE OR CAP FIRE"); draw_text(framebuffer, centered_text_x("PRESS FIRE", 1), 119, "PRESS FIRE"); } static void draw_end(uint8_t *framebuffer, bool won) { draw_text_scaled(framebuffer, centered_text_x(won ? "KEEP CLEAR" : "YOU FELL", 2), 18, won ? "KEEP CLEAR" : "YOU FELL", 2); if (won) { for (int32_t y = 48; y < 82; y++) { const int32_t half = (y - 48) / 2; set_pixel(framebuffer, 64 - half, y, true); set_pixel(framebuffer, 64 + half, y, true); } } else { for (int32_t i = 0; i < 25; i++) { set_pixel(framebuffer, 52 + i, 48 + i, true); set_pixel(framebuffer, 76 - i, 48 + i, true); } } draw_text(framebuffer, centered_text_x(won ? "WARDENS 5/5" : "TRY THE SOUTH PATH", 1), 94, won ? "WARDENS 5/5" : "TRY THE SOUTH PATH"); draw_text(framebuffer, centered_text_x("SPACE OR CAP", 1), 116, "SPACE OR CAP"); } static void draw_weapon_and_feedback(const FpsGameState *state, uint8_t *framebuffer) { /* Chunky first-person repeater: outline, twin grip rails and sight. */ for (int32_t y = 82; y < VIEW_HEIGHT; y++) { const int32_t half = 3 + (y - 82) / 2; set_pixel(framebuffer, 64 - half, y, true); set_pixel(framebuffer, 64 + half, y, true); if (y >= 91 && ((y + state->tick) & 2u) == 0u) { set_pixel(framebuffer, 61, y, true); set_pixel(framebuffer, 67, y, true); } } fill_rect(framebuffer, 62, 78, 5, 8, true); fill_rect(framebuffer, 63, 76, 3, 4, false); set_pixel(framebuffer, 64, 75, true); if (state->muzzle_ticks > 0u) { for (int32_t ray = -5; ray <= 5; ray++) { set_pixel(framebuffer, 64 + ray, 73 - abs_i32(ray), true); if ((ray & 1) == 0) { set_pixel(framebuffer, 64 + ray / 2, 68 + abs_i32(ray) / 2, true); } } } if (state->hit_ticks > 0u) { for (int32_t i = 0; i < 6; i++) { set_pixel(framebuffer, 58 + i, 45 + i, true); set_pixel(framebuffer, 70 - i, 45 + i, true); set_pixel(framebuffer, 58 + i, 56 - i, true); set_pixel(framebuffer, 70 - i, 56 - i, true); } } else { for (int32_t i = -3; i <= 3; i++) { if (i < -1 || i > 1) { set_pixel(framebuffer, 64 + i, HORIZON_Y, true); set_pixel(framebuffer, 64, HORIZON_Y + i, true); } } } if (state->hurt_ticks > 0u) { for (int32_t i = 0; i < 18; i++) { set_pixel(framebuffer, i, i / 2, true); set_pixel(framebuffer, 127 - i, i / 2, true); set_pixel(framebuffer, i, VIEW_HEIGHT - 1 - i / 2, true); set_pixel(framebuffer, 127 - i, VIEW_HEIGHT - 1 - i / 2, true); } } } void fps_render_frame(const FpsGameState *state, uint8_t framebuffer[OLED_FPS_FB_SIZE]) { memset(framebuffer, 0, OLED_FPS_FB_SIZE); if (state->mode == FPS_MODE_TITLE) { draw_title(framebuffer); return; } if (state->mode == FPS_MODE_WIN) { draw_end(framebuffer, true); return; } if (state->mode == FPS_MODE_DEATH) { draw_end(framebuffer, false); return; } int32_t depth[OLED_FPS_FB_WIDTH]; draw_world_backdrop(state, framebuffer); cast_walls(state, framebuffer, depth); draw_entities(state, framebuffer, depth); draw_weapon_and_feedback(state, framebuffer); draw_hud(state, framebuffer); } uint32_t fps_frame_crc32(const uint8_t framebuffer[OLED_FPS_FB_SIZE]) { uint32_t crc = UINT32_C(0xffffffff); for (size_t i = 0u; i < OLED_FPS_FB_SIZE; i++) { crc ^= framebuffer[i]; for (uint8_t bit = 0u; bit < 8u; bit++) { const uint32_t mask = UINT32_C(0) - (crc & 1u); crc = (crc >> 1u) ^ (UINT32_C(0xedb88320) & mask); } } return ~crc; }
main/render.h
/* First-party MIT-licensed integer OLED renderer for BRE-209. */ #ifndef OLED_FPS_RENDER_H #define OLED_FPS_RENDER_H #include <stdint.h> #include "game.h" void fps_render_frame(const FpsGameState *state, uint8_t framebuffer[OLED_FPS_FB_SIZE]); uint32_t fps_frame_crc32(const uint8_t framebuffer[OLED_FPS_FB_SIZE]); #endif
main/generated_tables.h
/* GENERATED by scripts/generate_tables.mjs. DO NOT EDIT. * Mathematical lookup data only; first-party generator licensed MIT. */ #ifndef OLED_FPS_GENERATED_TABLES_H #define OLED_FPS_GENERATED_TABLES_H #include <stdint.h> static const int16_t FPS_SIN_Q15[256] = { 0, 804, 1608, 2410, 3212, 4011, 4808, 5602, 6393, 7179, 7962, 8739, 9512, 10278, 11039, 11793, 12539, 13279, 14010, 14732, 15446, 16151, 16846, 17530, 18204, 18868, 19519, 20159, 20787, 21403, 22005, 22594, 23170, 23731, 24279, 24811, 25329, 25832, 26319, 26790, 27245, 27683, 28105, 28510, 28898, 29268, 29621, 29956, 30273, 30571, 30852, 31113, 31356, 31580, 31785, 31971, 32137, 32285, 32412, 32521, 32609, 32678, 32728, 32757, 32767, 32757, 32728, 32678, 32609, 32521, 32412, 32285, 32137, 31971, 31785, 31580, 31356, 31113, 30852, 30571, 30273, 29956, 29621, 29268, 28898, 28510, 28105, 27683, 27245, 26790, 26319, 25832, 25329, 24811, 24279, 23731, 23170, 22594, 22005, 21403, 20787, 20159, 19519, 18868, 18204, 17530, 16846, 16151, 15446, 14732, 14010, 13279, 12539, 11793, 11039, 10278, 9512, 8739, 7962, 7179, 6393, 5602, 4808, 4011, 3212, 2410, 1608, 804, 0, -804, -1608, -2410, -3212, -4011, -4808, -5602, -6393, -7179, -7962, -8739, -9512, -10278, -11039, -11793, -12539, -13279, -14010, -14732, -15446, -16151, -16846, -17530, -18204, -18868, -19519, -20159, -20787, -21403, -22005, -22594, -23170, -23731, -24279, -24811, -25329, -25832, -26319, -26790, -27245, -27683, -28105, -28510, -28898, -29268, -29621, -29956, -30273, -30571, -30852, -31113, -31356, -31580, -31785, -31971, -32137, -32285, -32412, -32521, -32609, -32678, -32728, -32757, -32767, -32757, -32728, -32678, -32609, -32521, -32412, -32285, -32137, -31971, -31785, -31580, -31356, -31113, -30852, -30571, -30273, -29956, -29621, -29268, -28898, -28510, -28105, -27683, -27245, -26790, -26319, -25832, -25329, -24811, -24279, -23731, -23170, -22594, -22005, -21403, -20787, -20159, -19519, -18868, -18204, -17530, -16846, -16151, -15446, -14732, -14010, -13279, -12539, -11793, -11039, -10278, -9512, -8739, -7962, -7179, -6393, -5602, -4808, -4011, -3212, -2410, -1608, -804, }; static const uint8_t FPS_BAYER4[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5, }; #endif
scripts/generate_tables.mjs
#!/usr/bin/env node /* * Generates the only lookup data used by the portable game core. The values * are derived mathematical data, not third-party game assets. License: MIT. */ import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const target = fileURLToPath(new URL('../main/generated_tables.h', import.meta.url)); const sine = Array.from({ length: 256 }, (_, index) => Math.round(Math.sin((index * Math.PI) / 128) * 32767), ); const bayer4 = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5]; const rows = (values, width) => { const out = []; for (let offset = 0; offset < values.length; offset += width) { out.push(` ${values.slice(offset, offset + width).join(', ')},`); } return out.join('\n'); }; const generated = `/* GENERATED by scripts/generate_tables.mjs. DO NOT EDIT.\n * Mathematical lookup data only; first-party generator licensed MIT. */\n#ifndef OLED_FPS_GENERATED_TABLES_H\n#define OLED_FPS_GENERATED_TABLES_H\n\n#include <stdint.h>\n\nstatic const int16_t FPS_SIN_Q15[256] = {\n${rows(sine, 16)}\n};\n\nstatic const uint8_t FPS_BAYER4[16] = {\n${rows(bayer4, 16)}\n};\n\n#endif\n`; if (process.argv.includes('--write')) { writeFileSync(target, generated); process.stdout.write(`wrote ${target}\n`); } else { let actual = ''; try { actual = readFileSync(target, 'utf8'); } catch { // The diagnostic below covers a missing file without hiding the cause. } if (actual !== generated) { process.stderr.write('generated_tables.h is stale; run generate_tables.mjs --write\n'); process.exitCode = 1; } else { process.stdout.write('generated_tables.h is synchronized\n'); } }
scripts/finalize_spdx.py
#!/usr/bin/env python3 """Canonicalize and fail-closed scan the OLED FPS linked-result SPDX file. The upstream esp-idf-sbom tool supplies the ESP-IDF component graph. This small wrapper makes volatile document metadata reproducible, grounds the two NOASSERTION package records that are known locally (the pinned IDF framework and this first-party component), grounds the bootloader-only micro-ecc record in its exact pinned BSD-2-Clause license, adds checksummed first-party file records, and preserves the raw NOASSERTION finding as an explicit resolved warning. It does not remove packages or relationships from the tool output. """ from __future__ import annotations import argparse import hashlib import json import re from pathlib import Path IDF_COMMIT = "0c45a102a96e82000f145b786a20de229b9f8557" IDF_LICENSE_SHA256 = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" IDF_LICENSE_BYTES = 11358 MICRO_ECC_COMMIT = "24c60e243580c7868f4334a1ba3123481fe1aa48" MICRO_ECC_LICENSE_SHA256 = "ffd8b033d2df7568c25a98866bd92b1656f7da82d7f813c0b2eb85ec36611193" MICRO_ECC_LICENSE_BYTES = 1300 PROJECT_ID = "SPDXRef-PROJECT-oled-fps-s3" FRAMEWORK_ID = "SPDXRef-FRAMEWORK-esp-idf" MAIN_ID = "SPDXRef-COMPONENT-main" BOOTLOADER_ID = "SPDXRef-COMPONENT-bootloader" MICRO_ECC_ID = "SPDXRef-SUBMODULE-bootloader-subproject-components-micro-ecc-micro-ecc" GPL = re.compile(r"(?:^|[^A-Z])(A?GPL|LGPL)-", re.IGNORECASE) # These are the exact expressions emitted for this linked result. CC0-1.0 # and Unlicense are established public-domain-dedication precedents in # docs/internal/dependency-audit-2026-08-09.md; the exact ESP-IDF toolchain # expressions are already retained in packages/compile/toolchain-idf/sbom/. # Exact matching makes a dependency-license change fail closed. APPROVED_LICENSE_EXPRESSIONS = { "Apache-2.0", "Apache-2.0 AND (CC0-1.0 OR Unlicense)", "Apache-2.0 AND BSD-2-Clause", "Apache-2.0 AND BSD-3-Clause", "Apache-2.0 AND BSD-3-Clause AND CC0-1.0", "Apache-2.0 AND BSD-3-Clause AND CC0-1.0 AND MIT", "Apache-2.0 AND CC0-1.0", "Apache-2.0 AND ISC", "Apache-2.0 AND MIT", "Apache-2.0 WITH LLVM-exception AND BSD-2-Clause-Views AND BSD-3-Clause", "BSD-2-Clause", "BSD-3-Clause", "MIT", } MICRO_ECC_WARNING = ( "unlinked SPDX inventory warning: " f"{MICRO_ECC_ID} (micro-ecc) has PackageLicenseConcluded NOASSERTION; " f"{BOOTLOADER_ID} DEPENDS_ON it, but neither package is reachable from " f"{PROJECT_ID} via DEPENDS_ON; it is bootloader-only inventory and is not " "linked into game.bin. The complete SBOM inventory is not unknown-license clean." ) # Exact first-party inputs that determine the application image or its # generated lookup table. Host tests, diagnostics, docs, and this release tool # are deliberately not firmware inputs. SOURCE_INPUTS = ( "CMakeLists.txt", "LICENSE", "main/CMakeLists.txt", "main/Kconfig.projbuild", "main/app_main.c", "main/content.c", "main/content.h", "main/display_sh1107.c", "main/display_sh1107.h", "main/game.c", "main/game.h", "main/generated_tables.h", "main/input.c", "main/input.h", "main/render.c", "main/render.h", "partitions.csv", "scripts/generate_tables.mjs", "sdkconfig.defaults", ) def digest(path: Path, algorithm: str = "sha256") -> str: value = hashlib.new(algorithm) with path.open("rb") as source: while chunk := source.read(1024 * 1024): value.update(chunk) return value.hexdigest() def replace_tag(block: str, tag: str, value: str) -> str: pattern = re.compile(rf"^{re.escape(tag)}: .*?$", re.MULTILINE) if not pattern.search(block): raise SystemExit(f"SPDX block lacks required {tag} tag") return pattern.sub(f"{tag}: {value}", block, count=1) def insert_before_relationships(block: str, line: str) -> str: marker = "\nRelationship:" index = block.find(marker) if index < 0: return block.rstrip() + "\n" + line return block[:index] + "\n" + line + block[index:] def package_id(block: str) -> str | None: if "PackageName:" not in block: return None match = re.search(r"^SPDXID: (SPDXRef-\S+)$", block, re.MULTILINE) return match.group(1) if match else None def file_id(path: str) -> str: clean = re.sub(r"[^A-Za-z0-9.-]+", "-", path).strip("-") return f"SPDXRef-FILE-oled-fps-{clean}" def source_file_blocks(root: Path) -> tuple[list[str], list[str]]: blocks: list[str] = [] sha1_values: list[str] = [] for relative in SOURCE_INPUTS: path = root / relative if not path.is_file() or path.is_symlink(): raise SystemExit(f"firmware input missing or unsafe: {relative}") if GPL.search(path.read_text(encoding="utf-8")): raise SystemExit(f"GPL-family identifier in first-party firmware input: {relative}") sha1 = digest(path, "sha1") sha256 = digest(path) sha1_values.append(sha1) blocks.append( "\n".join( ( f"FileName: ./{relative}", f"SPDXID: {file_id(relative)}", f"FileChecksum: SHA1: {sha1}", f"FileChecksum: SHA256: {sha256}", "LicenseConcluded: MIT", "LicenseInfoInFile: MIT", "FileCopyrightText: <text>Copyright (c) 2026 Murray Stott</text>", ) ) ) return blocks, sha1_values def normalize_relationships(block: str) -> str: lines = block.rstrip().splitlines() relationships = sorted(line for line in lines if line.startswith("Relationship: ")) lines = [line for line in lines if not line.startswith("Relationship: ")] return "\n".join(lines + relationships) def parse_packages(blocks: list[str]) -> dict[str, dict[str, str]]: packages: dict[str, dict[str, str]] = {} for block in blocks: identifier = package_id(block) if identifier is None: continue name = re.search(r"^PackageName: (.+)$", block, re.MULTILINE) license_value = re.search( r"^PackageLicenseConcluded: (.+)$", block, re.MULTILINE ) if not name or not license_value: raise SystemExit(f"incomplete SPDX package block: {identifier}") if identifier in packages: raise SystemExit(f"duplicate SPDX package id: {identifier}") packages[identifier] = { "spdxId": identifier, "name": name.group(1), "licenseConcluded": license_value.group(1), } return packages def license_summary(blocks: list[str]) -> dict[str, object]: packages = parse_packages(blocks) if PROJECT_ID not in packages: raise SystemExit("SPDX project package is missing") edges: dict[str, set[str]] = {} for block in blocks: for source, relation, target in re.findall( r"^Relationship: (SPDXRef-\S+) (DEPENDS_ON|CONTAINS) (SPDXRef-\S+)$", block, re.MULTILINE, ): if relation != "DEPENDS_ON": continue if source not in packages: raise SystemExit(f"dangling DEPENDS_ON source: {source}") if target not in packages: raise SystemExit(f"dangling DEPENDS_ON target: {target}") edges.setdefault(source, set()).add(target) reachable = {PROJECT_ID} pending = [PROJECT_ID] while pending: source = pending.pop() for target in edges.get(source, set()): if target not in reachable: reachable.add(target) pending.append(target) summary = [packages[identifier] for identifier in sorted(reachable)] for package in summary: license_value = package["licenseConcluded"] if license_value in {"", "NONE", "NOASSERTION"}: raise SystemExit( "unknown license in linked closure: " f"{package['spdxId']} ({package['name']})" ) if GPL.search(license_value): raise SystemExit( "GPL-family license in linked closure: " f"{package['spdxId']} ({license_value})" ) if license_value not in APPROVED_LICENSE_EXPRESSIONS: raise SystemExit( "unreviewed license expression in linked closure: " f"{package['spdxId']} ({license_value})" ) unknown_inventory: list[dict[str, str]] = [] for identifier, package in sorted(packages.items()): license_value = package["licenseConcluded"] if GPL.search(license_value): raise SystemExit( "GPL-family license in complete SPDX inventory: " f"{identifier} ({license_value})" ) if license_value in {"", "NONE", "NOASSERTION"}: unknown_inventory.append(package) elif license_value not in APPROVED_LICENSE_EXPRESSIONS: raise SystemExit( "unreviewed license expression in complete SPDX inventory: " f"{identifier} ({license_value})" ) if unknown_inventory: raise SystemExit( "unexpected unknown-license inventory: " + ", ".join(package["spdxId"] for package in unknown_inventory) ) if packages.get(MICRO_ECC_ID, {}).get("licenseConcluded") != "BSD-2-Clause": raise SystemExit("micro-ecc canonical license conclusion is not BSD-2-Clause") if MICRO_ECC_ID in reachable or BOOTLOADER_ID in reachable: raise SystemExit("bootloader-only micro-ecc unexpectedly reached game.bin closure") if MICRO_ECC_ID not in edges.get(BOOTLOADER_ID, set()): raise SystemExit("micro-ecc bootloader-only reachability evidence is missing") return { "schemaVersion": 1, "inventoryPackageCount": len(packages), "linkedClosure": summary, "warnings": [ { "code": "RAW_UNLINKED_UNKNOWN_LICENSE_RESOLVED", "message": MICRO_ECC_WARNING, "spdxId": MICRO_ECC_ID, "name": "micro-ecc", "rawLicenseConcluded": "NOASSERTION", "canonicalLicenseConcluded": "BSD-2-Clause", "resolution": ( f"Pinned micro-ecc {MICRO_ECC_COMMIT} LICENSE.txt; " f"{MICRO_ECC_LICENSE_BYTES} bytes; SHA-256 " f"{MICRO_ECC_LICENSE_SHA256}." ), "reachableFromProject": False, "incomingDependency": f"{BOOTLOADER_ID} DEPENDS_ON {MICRO_ECC_ID}", } ], } def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--input", required=True, type=Path) parser.add_argument("--output", required=True, type=Path) parser.add_argument("--summary-output", type=Path) parser.add_argument("--project-root", required=True, type=Path) parser.add_argument("--idf-license", required=True, type=Path) parser.add_argument("--micro-ecc-license", required=True, type=Path) args = parser.parse_args() if args.idf_license.stat().st_size != IDF_LICENSE_BYTES: raise SystemExit("pinned ESP-IDF LICENSE byte size changed") if digest(args.idf_license) != IDF_LICENSE_SHA256: raise SystemExit("pinned ESP-IDF LICENSE SHA-256 changed") if args.micro_ecc_license.stat().st_size != MICRO_ECC_LICENSE_BYTES: raise SystemExit("pinned micro-ecc LICENSE.txt byte size changed") if digest(args.micro_ecc_license) != MICRO_ECC_LICENSE_SHA256: raise SystemExit("pinned micro-ecc LICENSE.txt SHA-256 changed") source = args.input.read_text(encoding="utf-8") if "SPDXVersion: SPDX-2.2" not in source or "DocumentName: oled-fps-s3" not in source: raise SystemExit("unexpected SPDX document identity") if f"git+https://github.com/espressif/esp-idf.git@{IDF_COMMIT}" not in source: raise SystemExit("SPDX document does not pin the required ESP-IDF commit") raw_blocks = source.rstrip().split("\n\n") header_blocks: list[str] = [] entity_blocks: list[str] = [] for block in raw_blocks: if "PackageName:" in block or "FileName:" in block: entity_blocks.append(block) else: header_blocks.append(block) # The tool command, UUID, and wall clock are not dependency evidence. if not header_blocks or not header_blocks[0].startswith("# Generated by esp-idf-sbom 1.2.0"): raise SystemExit("unexpected esp-idf-sbom generator header") header_blocks[0] = ( "# Generated by esp-idf-sbom 1.2.0 from the pinned linked-result metadata; " "canonicalized by scripts/finalize_spdx.py" ) document_index = next( (i for i, block in enumerate(header_blocks) if "SPDXID: SPDXRef-DOCUMENT" in block), None, ) if document_index is None: raise SystemExit("SPDX document header is missing") document = replace_tag( header_blocks[document_index], "DocumentNamespace", "https://breadboard.build/spdx/esp32s3-oled-fps/idf-v5.3.5", ) document = replace_tag(document, "Created", "2026-08-30T00:00:00Z") header_blocks[document_index] = document source_blocks, source_sha1 = source_file_blocks(args.project_root) verification = hashlib.sha1("".join(sorted(source_sha1)).encode("ascii")).hexdigest() found: set[str] = set() normalized_entities: list[str] = [] for original in entity_blocks: block = original identifier = package_id(block) if identifier == FRAMEWORK_ID: found.add(identifier) block = replace_tag(block, "PackageLicenseConcluded", "Apache-2.0") block = replace_tag(block, "PackageLicenseDeclared", "Apache-2.0") block = insert_before_relationships( block, "PackageComment: <text>esp-idf-sbom 1.2.0 reported NOASSERTION for " f"the framework container. The pinned {IDF_COMMIT} checkout's root " f"LICENSE is Apache-2.0, {IDF_LICENSE_BYTES} bytes, SHA-256 " f"{IDF_LICENSE_SHA256}.</text>", ) elif identifier == MAIN_ID: found.add(identifier) block = replace_tag(block, "FilesAnalyzed", "true") block = insert_before_relationships( block, f"PackageVerificationCode: {verification}" ) block = replace_tag(block, "PackageLicenseConcluded", "MIT") block = replace_tag(block, "PackageLicenseDeclared", "MIT") block = replace_tag( block, "PackageCopyrightText", "<text>Copyright (c) 2026 Murray Stott</text>", ) block = insert_before_relationships( block, "PackageComment: <text>Original first-party OLED dungeon source and " "geometric content. No third-party game code or assets.</text>", ) for relative in SOURCE_INPUTS: block += f"\nRelationship: {MAIN_ID} CONTAINS {file_id(relative)}" block += ( f"\nRelationship: {file_id('main/generated_tables.h')} GENERATED_FROM " f"{file_id('scripts/generate_tables.mjs')}" ) elif identifier == MICRO_ECC_ID: found.add(identifier) if "PackageVersion: 1.1" not in block or MICRO_ECC_COMMIT not in block: raise SystemExit("unexpected pinned micro-ecc package identity") if "PackageLicenseConcluded: NOASSERTION" not in block: raise SystemExit("raw micro-ecc NOASSERTION finding is missing") block = replace_tag(block, "PackageLicenseConcluded", "BSD-2-Clause") block = replace_tag(block, "PackageLicenseDeclared", "BSD-2-Clause") block = replace_tag( block, "PackageCopyrightText", "<text>Copyright (c) 2014, Kenneth MacKay</text>", ) block = insert_before_relationships( block, f"PackageComment: <text>{MICRO_ECC_WARNING} Resolution: Pinned " f"micro-ecc {MICRO_ECC_COMMIT} LICENSE.txt; " f"{MICRO_ECC_LICENSE_BYTES} bytes; SHA-256 " f"{MICRO_ECC_LICENSE_SHA256}.</text>", ) normalized_entities.append(normalize_relationships(block)) if found != {FRAMEWORK_ID, MAIN_ID, MICRO_ECC_ID}: raise SystemExit(f"expected license augmentation packages missing: {sorted(found)}") normalized_entities.extend(source_blocks) normalized_entities.sort( key=lambda block: re.search(r"^SPDXID: (\S+)$", block, re.MULTILINE).group(1) ) complete_blocks = header_blocks + normalized_entities output = "\n\n".join(complete_blocks).rstrip() + "\n" if re.search(r"/(?:Users|private/tmp|tmp)/", output): raise SystemExit("canonical SPDX still contains an absolute local path") summary = license_summary(complete_blocks) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(output, encoding="utf-8") if args.summary_output: args.summary_output.write_text( json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) linked_count = len(summary["linkedClosure"]) print( f"linked SPDX closure: {linked_count} packages; complete canonical inventory " "reviewed-expression/GPL/unknown scan clean" ) print(f"WARNING (raw finding resolved in canonical SPDX): {MICRO_ECC_WARNING}") if __name__ == "__main__": main()
scripts/scan_source_licenses.py
#!/usr/bin/env python3 """Inventory and fail-closed license-scan the complete OLED FPS source tree. The project is a deliberately small first-party MIT tree. This scanner walks every source-tree entry (including host tests, diagnostics, docs, generators, and release tools), rejects links/non-regular files, records content hashes, and fails on third-party/dependency directory markers or copyleft-family license declarations. The exact rejection-gate implementation lines in the SPDX finalizer are pinned by line-content hash and count; any additional match, including pasted licensed material, fails the scan. """ from __future__ import annotations import argparse import hashlib import json import os import re from collections import Counter from pathlib import Path MIT_LICENSE_SHA256 = "a1bb8ae1dc6cf542f0624f3ec62fa7e7747c18bbd33dfff1127432f1fb61e487" MIT_LICENSE_BYTES = 1069 FORBIDDEN_DIRECTORY_NAMES = { ".git", "build", "components", "managed_components", "node_modules", "third_party", "third-party", "vendor", } ALLOWED_SPECIAL_NAMES = { "CMakeLists.txt", "Kconfig.projbuild", "LICENSE", "README.md", "partitions.csv", "sdkconfig.defaults", "sdkconfig.diagnostic", } ALLOWED_SUFFIXES = {".c", ".h", ".md", ".mjs", ".py", ".txt"} COPYLEFT_FAMILY = re.compile( r"(?<![A-Za-z])(?:A?G" r"PL|LG" r"PL)(?:v?[0-9.]+)?(?![A-Za-z])", re.IGNORECASE, ) DECLARATION = re.compile( r"(?:SPDX-License-Identifier|License)\s*:\s*([^\r\n*]+)", re.IGNORECASE ) EXPECTED_COPYLEFT_LINE_HASHES = { ("scripts/finalize_spdx.py", "b3aea5624238b21d40daf259758d3266d1df4b14c9591e2503f7effd8c4de5f4"): 1, ("scripts/finalize_spdx.py", "553c343b94c151e1da9535f6c0f1225fc97003b56dc1b275cba7884e87ab53a8"): 1, ("scripts/finalize_spdx.py", "3c8608c505ad8f9de479e0e24149e78effe791067e427245be8dd90691adc14a"): 1, ("scripts/finalize_spdx.py", "79b634005c4cec50c9e8598d8d9c089b02894cbad17d1b1e82cc56f3dcf41ae6"): 2, ("scripts/finalize_spdx.py", "6881028cbb3ff91477536de9814f56cae6606b13bf89a51251ad80875e8232ec"): 1, ("scripts/finalize_spdx.py", "d149d4a3f22d87caf2f8a27b0b05acba0769874493ab03713c4df56429b1c35f"): 1, ("scripts/finalize_spdx.py", "d27d220323b98f5fea85ae5f6ed3b6b33df741c1e5cc24f8b934c4fd625fd497"): 1, } def digest(path: Path) -> str: value = hashlib.sha256() with path.open("rb") as source: while chunk := source.read(1024 * 1024): value.update(chunk) return value.hexdigest() def tree_hash(files: list[dict[str, object]]) -> str: value = hashlib.sha256() for record in files: value.update(str(record["path"]).encode("utf-8")) value.update(b"\0") value.update(str(record["sha256"]).encode("ascii")) value.update(b"\0") value.update(str(record["bytes"]).encode("ascii")) value.update(b"\n") return value.hexdigest() def scan(root: Path) -> dict[str, object]: root = root.resolve(strict=True) license_path = root / "LICENSE" if license_path.stat().st_size != MIT_LICENSE_BYTES or digest(license_path) != MIT_LICENSE_SHA256: raise SystemExit("adjacent MIT LICENSE bytes/hash changed") records: list[dict[str, object]] = [] copyleft_mentions: list[dict[str, object]] = [] copyleft_line_counts: Counter[tuple[str, str]] = Counter() declarations: list[dict[str, str]] = [] for directory, dirnames, filenames in os.walk(root, followlinks=False): directory_path = Path(directory) for name in sorted(dirnames): child = directory_path / name relative = child.relative_to(root).as_posix() if child.is_symlink(): raise SystemExit(f"source tree contains directory symlink: {relative}") if name in FORBIDDEN_DIRECTORY_NAMES: raise SystemExit(f"source tree contains forbidden dependency/build directory: {relative}") for name in sorted(filenames): path = directory_path / name relative = path.relative_to(root).as_posix() if path.is_symlink() or not path.is_file(): raise SystemExit(f"source tree contains unsafe entry: {relative}") if name not in ALLOWED_SPECIAL_NAMES and path.suffix not in ALLOWED_SUFFIXES: raise SystemExit(f"source tree contains unreviewed file type: {relative}") raw = path.read_bytes() if b"\0" in raw: raise SystemExit(f"source tree contains unexpected binary data: {relative}") try: text = raw.decode("utf-8") except UnicodeDecodeError as error: raise SystemExit(f"source tree contains non-UTF-8 text: {relative}") from error found_declarations = [ match.group(1).strip().rstrip(".;") for match in DECLARATION.finditer(text) ] for expression in found_declarations: if expression != "MIT": raise SystemExit(f"unreviewed source license declaration in {relative}: {expression}") declarations.append({"path": relative, "expression": expression}) for line_number, line in enumerate(text.splitlines(), start=1): if not COPYLEFT_FAMILY.search(line): continue line_hash = hashlib.sha256(line.strip().encode("utf-8")).hexdigest() copyleft_line_counts[(relative, line_hash)] += 1 copyleft_mentions.append( { "path": relative, "line": line_number, "lineSha256": line_hash, "classification": "exact-pinned-rejection-gate-line", } ) records.append( { "path": relative, "bytes": len(raw), "sha256": hashlib.sha256(raw).hexdigest(), "licenseConcluded": "MIT", "licenseBasis": "adjacent-root-license", } ) records.sort(key=lambda record: str(record["path"])) declarations.sort(key=lambda record: (record["path"], record["expression"])) expected_counts = Counter(EXPECTED_COPYLEFT_LINE_HASHES) if copyleft_line_counts != expected_counts: unexpected = copyleft_line_counts - expected_counts missing = expected_counts - copyleft_line_counts raise SystemExit( "copyleft-family text differs from exact pinned rejection-gate lines; " f"unexpected={dict(unexpected)} missing={dict(missing)}" ) copyleft_mentions.sort(key=lambda record: (str(record["path"]), int(record["line"]))) if not records: raise SystemExit("source tree is empty") return { "schemaVersion": 1, "projectRoot": "firmware-src/esp32s3-oled-fps", "scope": "complete-source-tree-including-host-tests-diagnostics-docs-generators-and-release-tools", "rootLicense": { "expression": "MIT", "path": "LICENSE", "bytes": MIT_LICENSE_BYTES, "sha256": MIT_LICENSE_SHA256, }, "fileCount": len(records), "totalBytes": sum(int(record["bytes"]) for record in records), "treeHash": { "algorithm": "SHA256 over path + NUL + lowercase SHA256 + NUL + decimal byte size + LF, sorted by path", "sha256": tree_hash(records), }, "files": records, "explicitLicenseDeclarations": declarations, "copyleftFamilyMentions": copyleft_mentions, "dependencyDirectoryIndicators": [], "conclusion": { "allFilesInventoried": True, "allFilesMit": True, "unknownLicenses": [], "copyleftFamilyDependenciesOrDeclaredLicenses": [], "thirdPartySourceTrees": [], }, } def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--project-root", required=True, type=Path) parser.add_argument("--output", required=True, type=Path) args = parser.parse_args() result = scan(args.project_root) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") print( "source license scan: " f"{result['fileCount']} files / {result['totalBytes']} bytes; " "MIT-only; no third-party source tree, unknown license, or copyleft-family dependency/declaration" ) if __name__ == "__main__": main()
host/CMakeLists.txt
cmake_minimum_required(VERSION 3.16) project(oled_fps_host C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS OFF) add_executable(oled_fps_host_test test_main.c ../main/content.c ../main/game.c ../main/render.c ) target_include_directories(oled_fps_host_test PRIVATE ../main) target_compile_options(oled_fps_host_test PRIVATE -Wall -Wextra -Werror -Wpedantic ) option(OLED_FPS_SANITIZERS "Enable the platform-supported runtime sanitizers" ON) if(OLED_FPS_SANITIZERS AND CMAKE_C_COMPILER_ID MATCHES "Clang|GNU") if(APPLE AND CMAKE_C_COMPILER_ID STREQUAL "AppleClang") # AppleClang 17's AddressSanitizer runtime can hang before main while # reserving its dynamic shadow. Keep the canonical macOS CMake path # useful and deterministic with UndefinedBehaviorSanitizer alone. set(OLED_FPS_SANITIZER_FLAGS -fsanitize=undefined -fno-omit-frame-pointer) else() set(OLED_FPS_SANITIZER_FLAGS -fsanitize=address,undefined -fno-omit-frame-pointer) endif() target_compile_options(oled_fps_host_test PRIVATE ${OLED_FPS_SANITIZER_FLAGS} ) target_link_options(oled_fps_host_test PRIVATE ${OLED_FPS_SANITIZER_FLAGS} ) endif() enable_testing() add_test(NAME oled_fps_portable COMMAND oled_fps_host_test)
host/test_main.c
/* Portable deterministic acceptance harness for BRE-209. License: MIT. */ #include <assert.h> #include <inttypes.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "content.h" #include "game.h" #include "generated_tables.h" #include "render.h" /* Acceptance checks must remain active in Release/NDEBUG builds. */ #undef assert #define assert(condition) do { \ if (!(condition)) { \ fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, \ #condition); \ abort(); \ } \ } while (0) typedef struct { uint32_t state_crc; uint32_t frame_crc; uint32_t trace_crc; } TraceResult; static uint32_t mix_crc(uint32_t acc, uint32_t value) { acc ^= value + UINT32_C(0x9e3779b9) + (acc << 6u) + (acc >> 2u); return acc; } static void begin_play(FpsGameState *state) { fps_game_init(state, 1u, OLED_FPS_FIXED_SEED); FpsTickEvents events = fps_game_tick(state, (FpsInput){ .fire = true }); assert(events.restarted); assert(state->mode == FPS_MODE_PLAY); (void)fps_game_tick(state, (FpsInput){ 0 }); } static TraceResult run_fixed_trace(void) { FpsGameState state; uint8_t framebuffer[OLED_FPS_FB_SIZE]; begin_play(&state); uint32_t trace_crc = UINT32_C(0x20920920); for (uint32_t tick = 0u; tick < 360u; tick++) { FpsInput input = { 0 }; if (tick < 42u) { input.move = 110; } else if (tick < 72u) { input.turn = 88; } else if (tick < 132u) { input.move = 100; } else if (tick < 168u) { input.turn = -72; } else if (tick < 230u) { input.move = -84; } else { input.turn = (tick & 16u) != 0u ? 48 : -48; input.move = 58; } input.fire = tick == 12u || tick == 90u || tick == 180u || tick == 260u; (void)fps_game_tick(&state, input); fps_render_frame(&state, framebuffer); if ((tick % 20u) == 19u) { trace_crc = mix_crc(trace_crc, fps_game_state_crc32(&state)); trace_crc = mix_crc(trace_crc, fps_frame_crc32(framebuffer)); } if (state.mode != FPS_MODE_PLAY) { break; } } fps_render_frame(&state, framebuffer); return (TraceResult){ .state_crc = fps_game_state_crc32(&state), .frame_crc = fps_frame_crc32(framebuffer), .trace_crc = trace_crc, }; } static FpsTickEvents traced_tick(FpsGameState *state, FpsInput input, uint32_t *trace_crc) { uint8_t framebuffer[OLED_FPS_FB_SIZE]; const FpsTickEvents events = fps_game_tick(state, input); fps_render_frame(state, framebuffer); *trace_crc = mix_crc(*trace_crc, fps_game_state_crc32(state)); *trace_crc = mix_crc(*trace_crc, fps_frame_crc32(framebuffer)); return events; } static void trace_turn_to(FpsGameState *state, uint8_t target, uint32_t *trace_crc) { for (uint16_t ticks = 0u; state->player_angle != target && ticks < 256u; ticks++) { int16_t delta = (int16_t)target - (int16_t)state->player_angle; if (delta > 128) { delta -= 256; } else if (delta < -128) { delta += 256; } const int16_t step = delta < -3 ? -3 : delta > 3 ? 3 : delta; (void)traced_tick(state, (FpsInput){ .turn = (int16_t)(step * 32) }, trace_crc); assert(state->mode == FPS_MODE_PLAY); } assert(state->player_angle == target); } static void trace_move_x(FpsGameState *state, fps_fixed_t target, uint8_t angle, uint32_t *trace_crc) { trace_turn_to(state, angle, trace_crc); const bool increasing = angle == 0u; for (uint16_t ticks = 0u; ticks < 400u; ticks++) { if ((increasing && state->player_x >= target) || (!increasing && state->player_x <= target)) { return; } (void)traced_tick(state, (FpsInput){ .move = 127 }, trace_crc); if (state->mode != FPS_MODE_PLAY) { fprintf(stderr, "X waypoint ended mode=%u from (%" PRId32 ",%" PRId32 ") target=%" PRId32 " angle=%u hp=%u kills=%u\n", (unsigned)state->mode, state->player_x, state->player_y, target, angle, state->health, state->kills); } assert(state->mode == FPS_MODE_PLAY); } assert(false && "scripted X movement did not reach its waypoint"); } static void trace_move_y(FpsGameState *state, fps_fixed_t target, uint8_t angle, uint32_t *trace_crc) { trace_turn_to(state, angle, trace_crc); const bool increasing = angle == 64u; for (uint16_t ticks = 0u; ticks < 400u; ticks++) { if ((increasing && state->player_y >= target) || (!increasing && state->player_y <= target)) { return; } (void)traced_tick(state, (FpsInput){ .move = 127 }, trace_crc); if (state->mode != FPS_MODE_PLAY) { fprintf(stderr, "Y waypoint ended mode=%u from (%" PRId32 ",%" PRId32 ") target=%" PRId32 " angle=%u hp=%u kills=%u\n", (unsigned)state->mode, state->player_x, state->player_y, target, angle, state->health, state->kills); } assert(state->mode == FPS_MODE_PLAY); } assert(false && "scripted Y movement did not reach its waypoint"); } static uint8_t angle_toward(const FpsGameState *state, fps_fixed_t x, fps_fixed_t y) { const int64_t dx = (int64_t)x - state->player_x; const int64_t dy = (int64_t)y - state->player_y; int64_t best_dot = INT64_MIN; uint8_t best_angle = 0u; for (uint16_t angle = 0u; angle < 256u; angle++) { const int64_t dir_x = FPS_SIN_Q15[(uint8_t)(angle + 64u)]; const int64_t dir_y = FPS_SIN_Q15[angle]; const int64_t dot = dx * dir_x + dy * dir_y; if (dot > best_dot) { best_dot = dot; best_angle = (uint8_t)angle; } } return best_angle; } static void trace_kill_enemy(FpsGameState *state, uint8_t enemy_index, uint32_t *trace_crc) { assert(enemy_index < state->enemy_count); for (uint8_t shot_index = 0u; state->enemies[enemy_index].alive && shot_index < 3u; shot_index++) { const FpsEnemy *enemy = &state->enemies[enemy_index]; trace_turn_to(state, angle_toward(state, enemy->x, enemy->y), trace_crc); const FpsTickEvents shot = traced_tick(state, (FpsInput){ .fire = true }, trace_crc); if (!shot.shot || !shot.hit) { fprintf(stderr, "enemy %u missed from (%" PRId32 ",%" PRId32 ") toward (%" PRId32 ",%" PRId32 ") angle=%u ammo=%u\n", enemy_index, state->player_x, state->player_y, enemy->x, enemy->y, state->player_angle, state->ammo); } assert(shot.shot && shot.hit); (void)traced_tick(state, (FpsInput){ 0 }, trace_crc); } assert(!state->enemies[enemy_index].alive); } static uint32_t run_complete_win_trace(void) { FpsGameState state; uint32_t trace_crc = UINT32_C(0x209a11ce); begin_play(&state); /* A complete no-teleport run: clear the visible courtyard warden, take * the north bypass, sweep the east yard and keep, cross the south bypass * for the final warden, then return to the now-unlocked gate. */ trace_kill_enemy(&state, 0u, &trace_crc); trace_move_y(&state, OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 192u, &trace_crc); trace_move_x(&state, 13 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 0u, &trace_crc); trace_kill_enemy(&state, 2u, &trace_crc); trace_move_y(&state, 7 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 64u, &trace_crc); trace_kill_enemy(&state, 3u, &trace_crc); trace_move_x(&state, 9 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 128u, &trace_crc); trace_move_x(&state, 8 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 128u, &trace_crc); trace_move_y(&state, 4 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 192u, &trace_crc); trace_kill_enemy(&state, 1u, &trace_crc); trace_move_y(&state, 7 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 64u, &trace_crc); trace_move_x(&state, 13 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 0u, &trace_crc); trace_move_y(&state, 12 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 64u, &trace_crc); trace_move_x(&state, 5 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 128u, &trace_crc); trace_move_y(&state, 7 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 192u, &trace_crc); trace_move_x(&state, 8 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF, 0u, &trace_crc); trace_kill_enemy(&state, 4u, &trace_crc); assert(state.kills == state.enemy_count); trace_turn_to(&state, 0u, &trace_crc); for (uint16_t ticks = 0u; state.mode == FPS_MODE_PLAY && ticks < 160u; ticks++) { (void)traced_tick(&state, (FpsInput){ .move = 127 }, &trace_crc); } assert(state.mode == FPS_MODE_WIN); const FpsTickEvents restart = traced_tick(&state, (FpsInput){ .fire = true }, &trace_crc); assert(restart.restarted && state.mode == FPS_MODE_PLAY); return trace_crc; } static uint32_t run_complete_combat_trace(void) { FpsGameState state; uint32_t trace_crc = UINT32_C(0x209c0b47); begin_play(&state); /* The first warden is deliberately visible and shootable from spawn. */ const uint8_t enemies_before = fps_game_alive_enemies(&state); for (uint8_t attempt = 0u; attempt < 8u && fps_game_alive_enemies(&state) == enemies_before; attempt++) { trace_turn_to(&state, angle_toward(&state, state.enemies[0].x, state.enemies[0].y), &trace_crc); const FpsTickEvents shot = traced_tick(&state, (FpsInput){ .fire = true }, &trace_crc); assert(shot.shot); (void)traced_tick(&state, (FpsInput){ 0 }, &trace_crc); } assert(fps_game_alive_enemies(&state) < enemies_before); while (state.ammo > 0u) { const FpsTickEvents shot = traced_tick(&state, (FpsInput){ .fire = true }, &trace_crc); assert(shot.shot); (void)traced_tick(&state, (FpsInput){ 0 }, &trace_crc); } const FpsTickEvents empty = traced_tick(&state, (FpsInput){ .fire = true }, &trace_crc); assert(!empty.shot && !empty.hit); return trace_crc; } static uint32_t run_complete_death_trace(void) { FpsGameState state; uint32_t trace_crc = UINT32_C(0x209dea7a); begin_play(&state); /* Do nothing: the visible courtyard warden must close and defeat the * player, proving the opening encounter is active rather than decoration. */ for (uint16_t ticks = 0u; state.mode == FPS_MODE_PLAY && ticks < 1000u; ticks++) { (void)traced_tick(&state, (FpsInput){ 0 }, &trace_crc); } assert(state.mode == FPS_MODE_DEATH && state.health == 0u); const FpsTickEvents restart = traced_tick(&state, (FpsInput){ .fire = true }, &trace_crc); assert(restart.restarted && state.mode == FPS_MODE_PLAY && state.health == 100u); return trace_crc; } static uint32_t run_complete_collision_trace(void) { FpsGameState state; uint32_t trace_crc = UINT32_C(0x209c0111); begin_play(&state); trace_turn_to(&state, 128u, &trace_crc); for (uint16_t ticks = 0u; ticks < 180u; ticks++) { (void)traced_tick(&state, (FpsInput){ .move = 127 }, &trace_crc); assert(state.mode == FPS_MODE_PLAY); } const fps_fixed_t stopped_x = state.player_x; for (uint8_t ticks = 0u; ticks < 20u; ticks++) { (void)traced_tick(&state, (FpsInput){ .move = 127 }, &trace_crc); } assert(state.player_x == stopped_x); assert(state.player_x > OLED_FPS_FIXED_ONE); return trace_crc; } static void test_complete_gameplay_traces(void) { const uint32_t win_first = run_complete_win_trace(); const uint32_t win_second = run_complete_win_trace(); const uint32_t combat_first = run_complete_combat_trace(); const uint32_t combat_second = run_complete_combat_trace(); const uint32_t death_first = run_complete_death_trace(); const uint32_t death_second = run_complete_death_trace(); const uint32_t collision_first = run_complete_collision_trace(); const uint32_t collision_second = run_complete_collision_trace(); assert(win_first == win_second); assert(combat_first == combat_second); assert(death_first == death_second); assert(collision_first == collision_second); printf("complete win=%08" PRIx32 " combat=%08" PRIx32 " death=%08" PRIx32 " collision=%08" PRIx32 "\n", win_first, combat_first, death_first, collision_first); fflush(stdout); assert(win_first == UINT32_C(0x41759c53)); assert(combat_first == UINT32_C(0xd3647ce3)); assert(death_first == UINT32_C(0xb3168e7e)); assert(collision_first == UINT32_C(0x1478e42c)); } static void test_title_and_renderer(void) { FpsGameState state; uint8_t framebuffer[OLED_FPS_FB_SIZE]; uint8_t combat_frame[OLED_FPS_FB_SIZE]; fps_game_init(&state, 1u, OLED_FPS_FIXED_SEED); fps_render_frame(&state, framebuffer); assert(state.mode == FPS_MODE_TITLE); assert(fps_frame_crc32(framebuffer) != 0u); begin_play(&state); fps_render_frame(&state, framebuffer); const uint32_t opening_crc = fps_frame_crc32(framebuffer); const FpsTickEvents opening_shot = fps_game_tick(&state, (FpsInput){ .fire = true }); assert(opening_shot.shot && opening_shot.hit && opening_shot.killed); fps_render_frame(&state, combat_frame); assert(fps_frame_crc32(combat_frame) != opening_crc); assert(state.muzzle_ticks > 0u && state.hit_ticks > 0u); (void)fps_game_tick(&state, (FpsInput){ 0 }); for (uint16_t angle = 0u; angle < 256u; angle += 17u) { state.player_angle = (uint8_t)angle; fps_render_frame(&state, framebuffer); assert(fps_frame_crc32(framebuffer) != 0u); } /* Exercise near-boundary DDA divisions without moving into a wall. */ state.player_x = OLED_FPS_FIXED_ONE + 1; state.player_y = OLED_FPS_FIXED_ONE + 1; fps_render_frame(&state, framebuffer); assert(fps_frame_crc32(framebuffer) != 0u); } static bool route_exists(const FpsLevel *level, uint8_t min_y, uint8_t max_y, bool doors_are_open) { bool seen[OLED_FPS_MAP_WIDTH * OLED_FPS_MAP_HEIGHT] = { false }; uint8_t queue_x[OLED_FPS_MAP_WIDTH * OLED_FPS_MAP_HEIGHT]; uint8_t queue_y[OLED_FPS_MAP_WIDTH * OLED_FPS_MAP_HEIGHT]; size_t head = 0u; size_t tail = 0u; const uint8_t start_x = (uint8_t)(level->start_x >> 16); const uint8_t start_y = (uint8_t)(level->start_y >> 16); const uint8_t exit_x = (uint8_t)(level->exit_x >> 16); const uint8_t exit_y = (uint8_t)(level->exit_y >> 16); if (start_y < min_y || start_y > max_y || exit_y < min_y || exit_y > max_y) { return false; } queue_x[tail] = start_x; queue_y[tail++] = start_y; seen[(size_t)start_y * level->width + start_x] = true; while (head < tail) { const uint8_t x = queue_x[head]; const uint8_t y = queue_y[head++]; if (x == exit_x && y == exit_y) { return true; } static const int8_t DX[4] = { 1, -1, 0, 0 }; static const int8_t DY[4] = { 0, 0, 1, -1 }; for (uint8_t direction = 0u; direction < 4u; direction++) { const int32_t nx = (int32_t)x + DX[direction]; const int32_t ny = (int32_t)y + DY[direction]; if (nx < 0 || ny < min_y || nx >= level->width || ny > max_y) { continue; } const size_t index = (size_t)ny * level->width + (size_t)nx; const uint8_t cell = level->cells[index]; if (!seen[index] && (cell == 0u || (doors_are_open && cell == 5u))) { seen[index] = true; queue_x[tail] = (uint8_t)nx; queue_y[tail++] = (uint8_t)ny; } } } return false; } static void test_original_level_inventory_and_reachability(void) { const FpsLevel *level = fps_content_level(1u); bool seen[OLED_FPS_MAP_WIDTH * OLED_FPS_MAP_HEIGHT] = { false }; uint8_t queue_x[OLED_FPS_MAP_WIDTH * OLED_FPS_MAP_HEIGHT]; uint8_t queue_y[OLED_FPS_MAP_WIDTH * OLED_FPS_MAP_HEIGHT]; size_t head = 0u; size_t tail = 0u; const uint8_t start_x = (uint8_t)(level->start_x >> 16); const uint8_t start_y = (uint8_t)(level->start_y >> 16); queue_x[tail] = start_x; queue_y[tail++] = start_y; seen[(size_t)start_y * level->width + start_x] = true; uint8_t materials = 0u; for (size_t i = 0u; i < (size_t)level->width * level->height; i++) { const uint8_t cell = level->cells[i]; if (cell >= 1u && cell <= 4u) { materials |= (uint8_t)(1u << (cell - 1u)); } } while (head < tail) { const uint8_t x = queue_x[head]; const uint8_t y = queue_y[head++]; static const int8_t DX[4] = { 1, -1, 0, 0 }; static const int8_t DY[4] = { 0, 0, 1, -1 }; for (uint8_t direction = 0u; direction < 4u; direction++) { const int32_t nx = (int32_t)x + DX[direction]; const int32_t ny = (int32_t)y + DY[direction]; if (nx < 0 || ny < 0 || nx >= level->width || ny >= level->height) { continue; } const size_t index = (size_t)ny * level->width + (size_t)nx; const uint8_t cell = level->cells[index]; if (!seen[index] && (cell == 0u || cell == 5u)) { seen[index] = true; queue_x[tail] = (uint8_t)nx; queue_y[tail++] = (uint8_t)ny; } } } const size_t exit_index = (size_t)(level->exit_y >> 16) * level->width + (size_t)(level->exit_x >> 16); assert(seen[exit_index]); /* With both doors closed, independent north-only and south-only bands * still connect start to exit. The direct door route is a third choice. */ assert(route_exists(level, 1u, 7u, false)); assert(route_exists(level, 7u, 14u, false)); assert(materials == 0x0fu); assert(level->enemy_count == 5u && level->pickup_count == 4u); assert(level->enemies[0].cell_x == 5u && level->enemies[0].cell_y == 7u && level->enemies[0].health == 1u); for (uint8_t i = 0u; i < level->enemy_count; i++) { assert(seen[(size_t)level->enemies[i].cell_y * level->width + level->enemies[i].cell_x]); } for (uint8_t i = 0u; i < level->pickup_count; i++) { assert(seen[(size_t)level->pickups[i].cell_y * level->width + level->pickups[i].cell_x]); } } static void test_collision_doors_and_pickups(void) { FpsGameState state; begin_play(&state); state.player_x = OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF; state.player_y = OLED_FPS_FIXED_ONE + 12000; state.player_angle = 192u; /* north, toward the boundary wall */ const fps_fixed_t before = state.player_y; for (int i = 0; i < 20; i++) { (void)fps_game_tick(&state, (FpsInput){ .move = 127 }); } assert(state.player_y >= OLED_FPS_FIXED_ONE); assert(state.player_y <= before); state.player_x = 6 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF; state.player_y = 7 * OLED_FPS_FIXED_ONE + OLED_FPS_FIXED_HALF; (void)fps_game_tick(&state, (FpsInput){ 0 }); assert(state.doors[0].open); state.health = 60u; state.player_x = state.pickups[1].x; state.player_y = state.pickups[1].y; (void)fps_game_tick(&state, (FpsInput){ 0 }); assert(state.health == 90u); assert(!state.pickups[1].active); state.ammo = 1u; state.player_x = state.pickups[0].x; state.player_y = state.pickups[0].y; (void)fps_game_tick(&state, (FpsInput){ 0 }); assert(state.ammo == 7u); assert(!state.pickups[0].active); } static void test_combat_and_empty_weapon(void) { FpsGameState state; begin_play(&state); state.player_angle = 0u; state.enemies[0].x = state.player_x + OLED_FPS_FIXED_ONE; state.enemies[0].y = state.player_y; state.enemies[0].health = 1u; state.enemies[0].alive = true; FpsTickEvents events = fps_game_tick(&state, (FpsInput){ .fire = true }); assert(events.shot && events.hit && events.killed); assert(!state.enemies[0].alive); assert(state.kills == 1u && state.muzzle_ticks > 0u && state.hit_ticks > 0u); (void)fps_game_tick(&state, (FpsInput){ 0 }); state.ammo = 0u; events = fps_game_tick(&state, (FpsInput){ .fire = true }); assert(!events.shot && !events.hit); } static void test_win_death_and_restart(void) { FpsGameState state; const FpsLevel *level = fps_content_level(1u); begin_play(&state); state.player_x = level->exit_x; state.player_y = level->exit_y; FpsTickEvents events = fps_game_tick(&state, (FpsInput){ 0 }); assert(!events.won && state.mode == FPS_MODE_PLAY); for (uint8_t i = 0u; i < state.enemy_count; i++) { state.enemies[i].alive = false; } state.kills = state.enemy_count; events = fps_game_tick(&state, (FpsInput){ 0 }); assert(events.won); assert(state.mode == FPS_MODE_WIN); events = fps_game_tick(&state, (FpsInput){ .fire = true }); assert(events.restarted); assert(state.mode == FPS_MODE_PLAY && state.health == 100u && state.ammo == 12u); (void)fps_game_tick(&state, (FpsInput){ 0 }); state.health = 12u; state.enemies[0].alive = true; state.enemies[0].attack_cooldown = 0u; state.enemies[0].x = state.player_x + 1000; state.enemies[0].y = state.player_y; events = fps_game_tick(&state, (FpsInput){ 0 }); assert(events.died); assert(state.mode == FPS_MODE_DEATH && state.health == 0u); events = fps_game_tick(&state, (FpsInput){ .fire = true }); assert(events.restarted); assert(state.mode == FPS_MODE_PLAY && state.health == 100u); } static void test_restart_restores_caller_seed(void) { const uint32_t caller_seed = UINT32_C(0x13579bdf); const FpsLevel *level = fps_content_level(1u); FpsGameState state; fps_game_init(&state, 1u, caller_seed); FpsTickEvents events = fps_game_tick(&state, (FpsInput){ .fire = true }); assert(events.restarted && state.mode == FPS_MODE_PLAY); assert(state.initial_seed == caller_seed && state.rng == caller_seed); /* Model an evolved live RNG independently of enemy placement/timing. The * restart contract is specifically that this value is not reused. */ state.rng = UINT32_C(0x2468ace0); assert(state.rng != caller_seed); for (uint8_t i = 0u; i < state.enemy_count; i++) { state.enemies[i].alive = false; } state.player_x = level->exit_x; state.player_y = level->exit_y; events = fps_game_tick(&state, (FpsInput){ 0 }); assert(events.won && state.rng != caller_seed); events = fps_game_tick(&state, (FpsInput){ .fire = true }); assert(events.restarted && state.mode == FPS_MODE_PLAY); assert(state.initial_seed == caller_seed && state.rng == caller_seed); } static void test_exit_progress_milestones(void) { const FpsLevel *level = fps_content_level(1u); FpsGameState state; begin_play(&state); for (uint8_t i = 0u; i < state.enemy_count; i++) { state.enemies[i].alive = false; } assert(state.exit_progress == 0u); state.player_y = level->exit_y; state.player_x = level->exit_x - 9 * OLED_FPS_FIXED_ONE; (void)fps_game_tick(&state, (FpsInput){ 0 }); assert(state.exit_progress == 1u); state.player_x = level->exit_x - 12 * OLED_FPS_FIXED_ONE; (void)fps_game_tick(&state, (FpsInput){ 0 }); assert(state.exit_progress == 1u); state.player_x = level->exit_x - 5 * OLED_FPS_FIXED_ONE; (void)fps_game_tick(&state, (FpsInput){ 0 }); assert(state.exit_progress == 2u); state.player_x = level->exit_x - OLED_FPS_FIXED_ONE; (void)fps_game_tick(&state, (FpsInput){ 0 }); assert(state.exit_progress == 3u && state.mode == FPS_MODE_PLAY); state.player_x = level->exit_x; const FpsTickEvents events = fps_game_tick(&state, (FpsInput){ 0 }); assert(events.won && state.exit_progress == 4u); } int main(void) { test_original_level_inventory_and_reachability(); test_title_and_renderer(); test_collision_doors_and_pickups(); test_combat_and_empty_weapon(); test_win_death_and_restart(); test_restart_restores_caller_seed(); test_exit_progress_milestones(); test_complete_gameplay_traces(); const TraceResult first = run_fixed_trace(); const TraceResult second = run_fixed_trace(); assert(first.state_crc == second.state_crc); assert(first.frame_crc == second.frame_crc); assert(first.trace_crc == second.trace_crc); /* These constants make content, simulation, and selected frames a reviewable * golden contract. Update only after intentionally reviewing the visuals. */ const TraceResult golden = { .state_crc = UINT32_C(0x6f7c3444), .frame_crc = UINT32_C(0x2b999c0d), .trace_crc = UINT32_C(0x00187486), }; printf("state=%08" PRIx32 " frame=%08" PRIx32 " trace=%08" PRIx32 "\n", first.state_crc, first.frame_crc, first.trace_crc); if (first.state_crc != golden.state_crc || first.frame_crc != golden.frame_crc || first.trace_crc != golden.trace_crc) { fputs("golden mismatch\n", stderr); return 1; } puts("oled-fps portable tests: ok"); return 0; }



