# AGENTS.md — JNR Engine ## Important Files Before starting any task, always read: - **DESIGN.md** — Game design document with vision, mechanics, and level plans - **TODO.md** — Current roadmap and next tasks to implement ## Project Overview 2D side-scrolling platformer (run-and-gun) written in C11 using SDL2, SDL2_image, and SDL2_mixer. Binary: `jnr`. Targets Linux (native), WebAssembly (Emscripten), Windows (MinGW cross-compile). ## Build Commands ```bash make # Release build (Linux) → ./jnr make run # Build + run make debug # Debug build: -g -O0 -DDEBUG make DEBUG=1 # Alternative debug flag make web # WASM build → dist-web/ make web-serve # WASM build + HTTP server on :8080 make windows # Cross-compile → dist-win64/ make k8s # Build web + container image + deploy to local k3s make clean # Remove all build artifacts ./deploy.sh # Full deploy: clean build → container → k3s rollout ``` Compiler flags: `-Wall -Wextra -std=c11 -I include -I src` There are no test or lint targets. Verify changes by building with `make` and confirming zero warnings. ### Cross-platform prerequisites - **WASM builds** require the Emscripten SDK. The `emsdk/` directory in the project root is gitignored; source the environment before building: ```bash source emsdk/emsdk_env.sh make web ``` - **Windows cross-compilation** requires MinGW (`x86_64-w64-mingw32-gcc`) and vendored SDL2 development libraries in `deps/win64/` (also gitignored). - **Web deployment** goes through Cloudflare CDN (`jnr.schick-web.site`). After deploying a new build, **purge the Cloudflare cache** so stale `.js`/`.wasm` files are not served. Emscripten's `.js` and `.wasm` outputs are tightly coupled (EM_ASM address tables must match); serving a cached `.js` with a fresh `.wasm` causes runtime crashes. ## Project Structure ``` include/ Global headers (config.h) src/ engine/ Engine subsystems (.c/.h pairs): physics, tilemap, entity, camera, etc. game/ Game logic (.c/.h pairs): player, enemy, level, levelgen, editor, etc. util/ Header-only utilities: vec2.h, darray.h main.c Entry point, game mode switching, level transitions assets/ levels/ .lvl level files (plain text) sounds/ .wav/.ogg audio sprites/ PNG spritesheets tiles/ Tileset PNGs web/ Emscripten HTML shell ``` Engine code lives in `src/engine/`, game code in `src/game/`. Each subsystem is a `.c`/`.h` pair. Header-only utilities use `static inline` functions. ## Code Style ### Formatting - **4 spaces** for indentation (no tabs in source; Makefile uses tabs) - **K&R brace style**: opening brace on same line - Pointer declaration: `Type *name` (space before `*`) - `const` for input-only pointer params: `const Tilemap *map` - No-parameter functions use `void`: `void physics_init(void)` - Unused parameters: `(void)param;` ### Naming Conventions | Kind | Convention | Example | |------|-----------|---------| | Functions | `snake_case`, module-prefixed | `player_update()`, `tilemap_load()` | | Types/Structs | `PascalCase` | `Entity`, `PlayerData`, `Tilemap` | | Enums | `PascalCase` type, `UPPER_SNAKE` values | `EntityType` / `ENT_PLAYER` | | Macros/Constants | `UPPER_SNAKE_CASE` | `MAX_ENTITIES`, `TILE_SIZE` | | Static (file-scope) vars | `s_` prefix | `s_gravity`, `s_renderer` | | Global vars | `g_` prefix | `g_engine`, `g_spritesheet` | | Local vars | Short `snake_case` | `dt`, `pos`, `em`, `tx` | | Function pointer types | `PascalCase` + `Fn` | `EntityUpdateFn` | ### Includes Order within each file: 1. Own module header (`"game/player.h"`) 2. Other project headers (`"engine/physics.h"`, `"game/sprites.h"`) 3. Standard library (``, ``, ``) 4. SDL headers (``) 5. Platform-conditional (`#ifdef __EMSCRIPTEN__`) Paths are forward-slash, relative to `src/` or `include/`: `"engine/core.h"`, `"config.h"`. ### Comments - **Section headers**: `/* ═══...═══ */` box-drawing block - **Subsections**: `/* ── Name ──────── */` light-line style - **Inline/doc comments**: `/* ... */` (C89-style, not `//`) - **Struct field comments**: trailing, aligned with whitespace padding ### Header Guards ```c #ifndef JNR_MODULE_H #define JNR_MODULE_H ... #endif /* JNR_MODULE_H */ ``` ### Types - `float` for positions, velocities, timers, physics (not `double`) - `Vec2` (float x, y) for all 2D quantities - `bool` from `` - `uint16_t` for tile IDs; `uint32_t` for bitfield flags and seeds - `SDL_Color` for colors; `SDL_Rect` for integer rectangles ### Error Handling - Return `bool` for success/failure - Return `NULL` from creation functions on failure - Errors to `stderr` via `fprintf(stderr, "...")` - Info to `stdout` via `printf(...)` - Warnings use `"Warning: ..."` prefix - Early return on failure; no goto-based cleanup ### Memory Management - `calloc(1, sizeof(T))` for entity data (zero-initialized) - `free(ptr); ptr = NULL;` in destroy callbacks - `memset(ptr, 0, sizeof(*ptr))` for struct re-initialization - Fixed-size arrays for most collections (entity pool, tile defs) - Dynamic allocation only for tile layers (`uint16_t *`) - `snprintf` / `strncpy` with explicit size limits for strings - `ASSET_PATH_MAX` (256) for path buffers ## TigerStyle Guidelines Follow the principles from [TigerStyle](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md), adapted for this C11 codebase. The design goals are **safety, performance, and developer experience**, in that order. ### Safety - **Simple, explicit control flow.** No recursion. Minimal abstractions — only where they genuinely model the domain. Every abstraction has a cost; prefer straightforward code. - **Put a limit on everything.** All loops and queues must have a fixed upper bound. Use `MAX_ENTITIES`, `MAX_ENTITY_SPAWNS`, `MAX_EXIT_ZONES`, etc. Where a loop cannot terminate (e.g. the game loop), document why. - **Assert preconditions, postconditions, and invariants.** Validate function arguments and return values. A function must not operate blindly on unchecked data. In C, use `assert()` or early-return checks with `fprintf(stderr, ...)` for runtime-recoverable cases. Split compound conditions: prefer `assert(a); assert(b);` over `assert(a && b);`. - **Assert the positive and negative space.** Check what you expect AND what you do not expect. Bugs live at the boundary between valid and invalid data. - **Static allocation after initialization.** Fixed-size arrays for collections (`MAX_ENTITIES` pool, tile defs). Dynamic allocation (`calloc`) only during level loading for tile layers. No allocation or reallocation during gameplay. - **Smallest possible scope for variables.** Declare variables close to where they are used. Minimize the number of live variables at any point. - **Hard limit of ~70 lines per function.** When splitting, centralize control flow (switch/if) in the parent function and push pure computation into helpers. Keep leaf functions pure. "Push `if`s up and `for`s down." - **Zero compiler warnings.** All builds must pass `-Wall -Wextra` with zero warnings. - **Handle all errors.** Every `fopen`, `calloc`, `snprintf` return must be checked. Early return on failure. ### Performance - **Think about performance in the design phase.** The biggest wins (1000x) come from structural decisions, not micro-optimization after the fact. - **Back-of-the-envelope sketches** for the four resources: network, disk, memory, CPU. For a game: frame budget is ~16ms at 60 Hz. Know where time goes. - **Optimize for the slowest resource first** (disk > memory > CPU), weighted by frequency. A cache miss that happens every frame matters more than a disk read that happens once at level load. - **Batch and amortize.** Sprite batching via `renderer_submit()`. Particle pools. Tile culling to the viewport. Don't iterate what you don't need to. - **Be explicit with the compiler.** Don't rely on the optimizer to fix structural problems. Extract hot inner loops into standalone helpers with primitive arguments when it helps clarity and performance. ### Developer Experience - **Get the nouns and verbs right.** Names should capture what a thing is or does. Take time to find the right name. Module-prefixed functions (`player_update`, `tilemap_load`) already enforce this. - **Add units or qualifiers last, sorted by descending significance.** For example, `latency_ms_max` not `max_latency_ms`. Related variables then align: `latency_ms_min`, `latency_ms_avg`. - **Always say why.** Comments explain rationale, not just what the code does. If a design decision isn't obvious, explain the tradeoff. Code alone is not documentation. - **Comments are sentences.** Capital letter, full stop, space after `/*`. End-of-line comments can be phrases without punctuation. - **Order matters for readability.** Put important things near the top of a file. Public API first, then helpers. In structs, fields before methods. - **Don't duplicate variables or alias them.** One source of truth per value. Reduces the chance of state getting out of sync. - **Calculate or check variables close to where they are used.** Don't introduce variables before they are needed. Minimize the gap between place-of-check and place-of-use. - **Descriptive commit messages.** Imperative mood. Inform the reader about the why, not just the what. The commit message is the permanent record — PR descriptions are not stored in git. ### Zero Technical Debt Do it right the first time. Don't allow known issues to slip through — exponential-complexity algorithms, unbounded loops, potential buffer overflows. What we ship is solid. We may lack features, but what we have meets our design goals. This is the only way to make steady incremental progress. ## Architecture Patterns ### Entity System - Fixed pool of `MAX_ENTITIES` (512) in `EntityManager` - Dispatch tables: `update_fn[type]`, `render_fn[type]`, `destroy_fn[type]` - `void *data` for type-specific data (cast in callbacks) - Each entity type: `_register()` sets callbacks, `_spawn()` creates instances - Entity registry maps string names to spawn functions for level loading ### Module Pattern - Public API: declared in header, module-prefixed (`camera_init`, `camera_follow`) - Private helpers: `static` in `.c` only - File-scope state: `static` variables with `s_` prefix - Forward declarations to break circular includes ### Level System - `.lvl` files: plain-text directives + tile grid data - `level_load()` for handcrafted levels from files - `level_load_generated()` for procedural levels from `Tilemap` structs - Exit zones trigger transitions; target strings: file path, `"generate"`, `"generate:station"`, or empty (victory) - Procedural generator: segment-based, theme-driven, difficulty-scaled ### Rendering - Sprite batching: submit to queue via `renderer_submit()`, flush layer-by-layer - Draw layers: BG → entities → FG → particles → HUD - Camera transforms world coords to screen coords ## Commit Messages - Imperative mood, concise - No co-authored-by or AI attribution - Example: "Add in-game level editor with auto-discovered tile/entity palettes" ## Key Constants (config.h) | Constant | Value | Notes | |----------|-------|-------| | `SCREEN_WIDTH` | 640 | Logical resolution | | `SCREEN_HEIGHT` | 360 | | | `TILE_SIZE` | 16 | Pixels per tile | | `TICK_RATE` | 60 | Fixed timestep Hz | | `DEFAULT_GRAVITY` | 980.0f | px/s² | | `MAX_ENTITIES` | 512 | Entity pool size | | `MAX_ENTITY_SPAWNS` | 512 | Per-level spawn slots | | `MAX_EXIT_ZONES` | 16 | Per-level exit zones |