Files
Verbatim/AGENTS.md
Emil 096f2e90a9 feat: multi-spectrum AI observation + tape recording system
Spectrum system (src/ai/spectrum.rs):
- 6 spectrums: materials, temperature, light, entities, density, velocity
- Each renders a different ASCII view of the same world state
- materials: material display chars + entity chars
- temperature: heat levels as .-=+oxX#
- light: brightness as .:+oO*
- entities: entity positions only, background dots
- density: material density as .:+oO#
- velocity: entity speed + CA activity as .:+oO,

Tape recording (src/ai/tape.rs):
- --mode tape CLI with --tape-interval, --tape-output, --tape-json
- Records all 6 spectrums + world state every N ticks
- Each frame: tick, depth, kills, score, camera, player HP/pos, entity count
- Outputs human-readable text and JSON formats

Pipe protocol additions:
- get_spectrum: single spectrum view by name
- get_all_spectrums: all 6 spectrums in one response
- Response.spectrums field added

Architecture priorities updated:
- Graphics mode is PRIMARY renderer
- ASCII mode is DEBUG backend
- Terminal mode is LEGACY
- AI uses multi-spectrum ASCII layers for observation

All 171 tests + 14 scenarios pass.
2026-06-21 18:27:23 +03:00

12 KiB
Raw Permalink Blame History

AGENTS.md

Build & Run

cargo build                          # debug build
cargo run --release -- --mode graphics    # Vulkan colored cells (PRIMARY, recommended)
cargo run --release -- --mode ascii       # Vulkan ASCII glyphs (debug backend)
cargo run -- --mode terminal         # ANSI terminal mode (legacy)
cargo run -- --mode pipe             # JSON stdin/stdout for AI agents
cargo run -- --mode test             # run all JSON scenarios
cargo run -- --mode headless --headless-ticks 60  # dump to headless_dump.txt
cargo run -- --mode capture --headless-ticks 60    # render graphics-like PNG to capture.png
cargo run --release -- --mode benchmark --benchmark-ticks 600 --benchmark-renderer graphics  # FPS benchmark
cargo run --release -- --mode tape --headless-ticks 300 --tape-interval 10 --tape-output tape.txt --tape-json tape.json  # multi-spectrum recording

Rust edition 2024, requires rustc >= 1.96. Vulkan 1.2+ required for ascii/graphics modes (falls back to terminal). Use --release for playable frame rates; GPU modes are CPU-bound in debug builds due to the cellular-automaton simulation. The GPU event loop is capped at 60 FPS.

Architecture Priorities

  • Graphics mode (--mode graphics) is the PRIMARY renderer — colored cells, 8x8 px, 16:9 window
  • ASCII mode (--mode ascii) is a DEBUG backend — same Vulkan pipeline with glyph atlas
  • Terminal mode (--mode terminal) is LEGACY — kept for headless/test compatibility
  • AI observation uses multi-spectrum ASCII layers via pipe protocol:
    • materials — material type per cell
    • temperature — heat levels encoded as characters
    • light — light intensity per cell
    • entities — entity positions and types only
    • density — material density visualization
    • velocity — entity movement speed and CA activity

Tape System

# Record all spectrums every 10 ticks for 300 ticks
cargo run --release -- --mode tape --headless-ticks 300 --tape-interval 10 \
  --tape-output tape.txt --tape-json tape.json

Each tape frame contains:

  • Tick number, depth, kills, score
  • Camera position
  • Player HP, position, entity count
  • All 6 spectrum layers as ASCII text

Pipe protocol spectrum commands:

  • {"cmd":"get_spectrum","spectrum":"materials","w":80,"h":25} — single spectrum
  • {"cmd":"get_all_spectrums","w":80,"h":25} — all spectrums at once

Tests

cargo test                           # all 171 integration tests
cargo test --test physics_sand       # single test file
cargo test --test slime              # slime-specific tests
cargo run -- --mode test --scenario-dir scenarios  # 14 JSON scenarios

Tests are in tests/*.rs, use verbatim::ai::GameSession API (not the render loop). Scenarios are in scenarios/*.json. Run python3 tools/render_dump.py to convert a headless_dump.txt frame to PNG for visual inspection. Use --mode capture to generate a graphics-like PNG directly without a window.

Controls

Terminal (--mode terminal):

  • a / d or / — move
  • w / / space — jump (press only)
  • h / j / k / l — shoot left / down / up / right
  • f — toggle fireball mode
  • > — descend when standing on stairs
  • e — use/equip first inventory item
  • r — drop first inventory item
  • 10 / x — paint material brush
  • q / ctrl-c — quit

GPU (--mode ascii / --mode graphics):

  • a / d or / — move
  • w / / space — jump
  • h / j / k / l — shoot left / down / up / right (keyboard fallback)
  • Left-click — shoot toward mouse cursor (Noita-style aiming)
  • Mouse position — player faces mouse direction (left/right)
  • f — toggle fireball mode
  • > / . — descend when standing on stairs
  • e — use/equip first inventory item
  • r — drop first inventory item
  • 10 / x — paint material brush
  • y / u / i / o — move camera offset
  • q / esc — quit

Shaders

GLSL shaders in assets/shaders/. Pre-compiled to SPIR-V via glslangValidator -V. Recompile after editing:

glslangValidator -V assets/shaders/cell.vert -o assets/shaders/cell_vert.spv
glslangValidator -V assets/shaders/cell.frag -o assets/shaders/cell_frag.spv
glslangValidator -V assets/shaders/graphics.vert -o assets/shaders/graphics_vert.spv
glslangValidator -V assets/shaders/graphics.frag -o assets/shaders/graphics_frag.spv

SPV files are committed. include_bytes! embeds them at compile time.

Architecture

Source of truth: Grid (250x250) of Cell structs. Each Cell stores material, temp, fg/bg color, variant inline. No double buffer. Grid is divided into 64x64 Chunks with active flags and per-chunk persistence.

Four entity kinds: Player, Goblin, Slime, Corpse. Three physics types: cellular (CA materials in grid), rigid (alive entities, AABB + slope stepping), ragdoll (corpses, Verlet constraints).

Three render modes: terminal (crossterm ANSI), ascii (Vulkan glyph atlas + instanced), graphics (Vulkan colored quads). All three read the same Grid + EntityManager and an optional LightGrid overlay.

Lighting pass: render::lighting::LightGrid is computed each frame on the CPU. Light sources are emitted by Lava and Fire cells. Light attenuates with distance and is blocked by solid cells (ray-cast line-of-sight). The ambient light level is configurable per mode; the default ambient is [100, 100, 120]. The Renderer trait and all renderers accept Option<&LightGrid>; UiLayer elements are drawn unlit on top.

GpuRenderer trait (main.rs): unifies VulkanRenderer and GraphicsRenderer behind run_gpu_mode<R>(). Both have identical event loops; only shader/instance format differs.

Game loop: Game::fixed_update() = activate chunks -> CA step -> rigid update -> ragdoll update -> slime AI -> goblin AI -> combat -> projectiles -> damage -> corpse decomposition -> status effects -> score -> item pickup. Called from event loop with fixed 16ms accumulator. Game::run() is terminal-only (uses InputHandler with crossterm). GPU modes use run_gpu_mode with WindowInput (winit PhysicalKey, layout-agnostic).

BodyTemplate (entity/body_template.rs): data-driven entity body definitions. JSON-serializable. build_humanoid() delegates to template_for_kind().apply_to(). To add a new creature shape, add a BodyTemplate constructor + match arm in template_for_kind. Each SubBody has a color: [u8; 4] field for per-part coloring.

Combat: player uses ranged projectiles (Arrow / Magic Bolt / Fireball). player_shoot() fires in the direction held. update_projectiles() moves projectiles, resolves entity hits (damage + knockback), and applies fireball ignition. Enemy contact damage still applies (Goblin 8 dmg, Slime 5 dmg per 20 ticks) reduced by equipped armor. Knockback applied to player.

Slime AI: update_slime_ai() makes slimes jump toward player every 60 ticks when within 40 cells. Jump power scales with proximity.

Goblin AI: update_goblin_ai() moves toward player when far, backs away when close, and flees when health < 10.

Corpse decomposition: decompose_corpses() turns dead Corpse bodies into Flesh cells in the grid over time.

RPG layer: Entity carries stats (strength, agility, toughness, willpower), level, XP, status effects (on_fire/poisoned/frozen/bleeding), and add_xp / xp_to_level / recalc_max_health. Status effects deal damage or expire in update_status_effects. EntityInfo exposes these for AI state.

Items & inventory: Item (weapon, armor, consumable), ItemManager, and inventory/equipment slots on Player. Items spawn in the world and are picked up on contact. Game::use_item(0) equips weapons/armor or consumes potions; Game::drop_item(0) returns an item to the world. Equipped weapon adds damage bonus to projectiles; equipped armor reduces enemy contact damage.

UI layer: ui::UiLayer overlays non-destructive UI on all renderers. Health bars above entities, bottom-line HUD, scrolling message log, floating damage numbers, screen-edge indicators, death screen, entity labels, status icons, minimap, and a character panel. UI is drawn unlit on top of the world. In GPU (ascii/graphics) and capture modes the UI is rendered as a separate 2x2 pixel-per-cell pass; terminal mode renders UI at full character size.

Chunk system: Grid is divided into 64x64 Chunks. Each chunk tracks active and modified. save_chunk(path, cx, cy) and load_chunk(path, cx, cy) serialize chunk cells via 12-byte binary format. Cell serialization is handled by Cell::to_bytes() / Cell::from_bytes().

Vertical descent: MaterialId::Stairs is a solid feature material. Player stands on stairs and presses > to descend. Game::descend() increments depth, resets the world, and respawns the player at the top. HUD shows current depth.

Key Conventions

  • No comments in code unless explicitly requested.
  • Cell colors are inline — renderers read cell.fg/cell.bg directly, never lookup MaterialRegistry in render path. Registry is for physics properties only (density, solid, flammable, etc.).
  • Vector movementPlayer::move_left/right sets velocity directly (set_horizontal_vel), no accumulation. stop_horizontal zeroes it. Jump is edge-triggered (press only, not held).
  • Slope steppingupdate_rigid_entity tries stepping up 1 cell before resolving X collision, enabling walking up slopes without jumping.
  • Adaptive viewportcheck_resize() in both Vulkan renderers recreates swapchain on window resize. grid_w/grid_h recalculated from extent / 16 (cell size = 16x16 px). Wayland uses window.inner_size() (surface extent is undefined).
  • Cell::new() copies colors from MaterialRegistry at creation time. Per-cell color variation is possible by modifying cell.fg/cell.bg after creation.
  • Auto-constraintsBodyTemplate::auto_constraints(n) connects all parts to all others (n^2). Works for any template shape. Simpler than manual constraint lists.
  • Item pickupGame::update_item_pickup() scans items within 1.5 cells of the player and adds them to player.inventory.
  • Stat-based health — Entity max health derived from base + toughness * 5 + level * 10. recalc_max_health() called on add_xp level-up.
  • Status effectsupdate_status_effects() applies damage for poison/bleeding/fire and cancels movement for frozen; effects expire when their timer reaches zero.

Module Layout

src/
  main.rs              # CLI, GpuRenderer trait, run_gpu_mode<R>(), event loops
  lib.rs               # pub mod declarations
  game.rs              # Game struct, world gen, fixed_update, collision, combat, slime AI
  input.rs             # Terminal input (crossterm, InputHandler) — terminal mode only
  world/               # Cell, MaterialId, MaterialRegistry, Grid, Chunk, CellularAutomaton
  physics/             # VerletSolver, SubBody (with color field), Constraint, resolve_grid_collision
  entity/              # Entity (rigid/ragdoll), EntityManager, Player, BodyTemplate, Item, ItemManager
  render/              # terminal.rs, vulkan.rs (ASCII), graphics.rs (cells), lighting.rs, window_input.rs
  ai/                  # GameSession, AiAction, pipe protocol, replay, scenarios
  ui/                  # UiLayer, HUD, messages, damage numbers, edge indicators

PLAN.md

Detailed roadmap with 8 phases, architecture decisions, milestones, and cross-platform support table. Read it for design context before making architectural changes. Player combat is ranged (projectiles), not melee — this is a design decision recorded in Phase 1.