Commit Graph
35 Commits
Author SHA1 Message Date
Emil 2eb9de6502 fix: use window inner_size() for resize on Wayland
Wayland surface always reports current_extent as 0xFFFFFFFF (undefined),
meaning the swapchain size is determined by the application, not the
surface. Previous code returned early when this happened, so resize
never worked — cells stretched to fill the window.

Fix: when current_extent is 0xFFFFFFFF, use winit's window.inner_size()
to get the actual pixel dimensions. This works on both X11 and Wayland.

Both renderers (ascii + graphics) updated.
109 tests, 0 failures
2026-06-21 01:17:32 +03:00
Emil 57a88fd41e feat: adaptive viewport — window resize expands camera, no stretching
Both renderers (ascii + graphics) now support dynamic window resize:

- check_resize() called at start of every render() frame
- Compares surface capabilities with current swapchain extent
- If changed: recreates swapchain, image views, framebuffers, command buffers
- Recalculates grid_w/grid_h from new extent / cell_size (16x16)
- Reallocates instance buffer if grid cell count changed
- Old swapchain properly destroyed after new one created
- Handles window minimize (skip if extent = 0)
- physical_device field added back to both renderers (needed for surface caps query)

main.rs: vw/vh queried every frame from renderer.grid_w()/grid_h()
instead of cached once at startup. Camera center uses dynamic dimensions.

Result: resize window → more/fewer cells visible, cells stay 16x16 pixels.
109 tests, 0 failures
2026-06-21 01:13:34 +03:00
Emil 3235957a26 feat: square cells (16x16), slope stepping collision
Square cells:
- CHAR_W and CHAR_H both 16 (was 8x16, non-square)
- Window size updated to 160*16 x 50*16 = 2560x800
- Applies to both ascii and graphics renderers

Slope stepping collision:
- Before resolving X, check if new position overlaps solid
- If overlap, try stepping up 1 cell — if clear, snap up (walk up slope)
- If can't step up, resolve X normally (wall block)
- aabb_overlaps_solid() helper for fast overlap check
- Player can now walk up 1-cell-high steps and slopes

109 tests, 0 failures
2026-06-21 01:06:37 +03:00
Emil 45d2767719 fix: clean up dead code warnings in vulkan.rs
Remove unused fields from VulkanRenderer: pixel_w, pixel_h,
physical_device, queue_family, swapchain_format. Mark unused
device param in create_swapchain as _device.

Only winit deprecation warnings remain (create_window, run).
2026-06-21 01:02:20 +03:00
Emil 019ae8f090 feat: three render modes — terminal, ascii, graphics
Three render modes:
- --mode terminal: pure ANSI ASCII in terminal
- --mode ascii: Vulkan window with ASCII characters (glyph atlas)
- --mode graphics: Vulkan window with colored cells (no glyphs, each
  material = unique base color, no lighting yet)

Graphics renderer:
- Same Vulkan pipeline as ASCII but without glyph atlas
- Simpler shaders: graphics.vert/frag just output instance color
- No descriptor set, no texture sampling
- Each cell = colored quad, material color fills entire cell
- Entities rendered as colored shapes (player=yellow, goblin=green)

Default mode changed to --mode ascii
109 tests, 0 failures
2026-06-21 00:59:19 +03:00
Emil 020914ab99 refactor: --mode window is now Vulkan, remove softbuffer fallback
- --mode window = Vulkan renderer (was softbuffer CPU)
- --mode vulkan removed (merged into --mode window)
- softbuffer dependency removed
- log dependency removed (unused)
- Old window.rs (softbuffer CPU renderer) deleted
- Fallback: if Vulkan init fails, falls back to --mode terminal
- 109 tests, 0 failures
2026-06-21 00:53:20 +03:00
Emil 8b79b99ab2 docs: update PLAN.md — two render modes, cross-platform, current state
Render modes:
- --mode window: ASCII mode (Vulkan instanced, glyph atlas) — DONE
- --mode graphics: Graphics mode (colored cells, no glyphs) — FUTURE
  Each material gets unique base color, no lighting yet (Phase 4b)
- --mode terminal: ANSI ASCII — always available

Cross-platform section added:
- All deps cross-platform: winit, ash, ash-window, softbuffer, fontdue
- ash_window auto-selects surface extension per platform
- Layout-agnostic input via winit PhysicalKey
- Render mode availability table per platform
- Vulkan surface extensions per OS

Updated:
- Current state: 109 tests, 14 scenarios, Vulkan renderer working
- Numbers: ~6500 lines, 30+ commits
- Testing strategy: detailed test counts per category
- File structure: window.rs, window_input.rs, graphics.rs
- Milestones: 0.5 = graphics mode + Phase 4b
- Performance: Vulkan ~14ms measured
2026-06-21 00:48:47 +03:00
Emil fdc1f416c7 fix: platform-agnostic Vulkan instance extensions
Use ash_window::enumerate_required_extensions() instead of hardcoded
VK_KHR_xlib_surface / VK_KHR_wayland_surface. This automatically
selects the correct surface extension per platform:
- Linux X11: VK_KHR_xlib_surface
- Linux Wayland: VK_KHR_wayland_surface
- Windows: VK_KHR_win32_surface
- macOS: VK_EXT_metal_surface (via MoltenVK)

No platform-specific code in the renderer — fully cross-platform.

109 tests, 0 failures
2026-06-21 00:43:26 +03:00
Emil 139b60b8af fix: flip Y in vertex shader, dynamic viewport/scissor, clean debug
- Fix: Y coordinate was inverted (1.0 - 2.0*y → 2.0*y - 1.0)
  Vulkan clip space Y is already down-up, no need to flip
- Fix: dynamic viewport/scissor with viewport_count(1) + scissor_count(1)
  Required by validation layer even with dynamic state
- Clean: removed debug eprintlns, restored default clear color
- Shaders recompiled to SPIR-V
2026-06-21 00:41:34 +03:00
Emil 6e9e90fb34 feat: Vulkan renderer with instanced rendering (Phase 4)
VulkanRenderer using ash 0.38 + winit:
- Instance with KHR_surface + xlib + wayland extensions
- Physical device selection (first GPU with graphics+present)
- Swapchain (FIFO present mode, B8G8R8A8_UNORM)
- Render pass with color attachment
- Graphics pipeline with instanced rendering:
  - Vertex buffer: unit quad (4 verts, 6 indices)
  - Instance buffer: 8000 CellInstance structs (32 bytes each)
  - 2 vertex bindings: per-vertex (quad pos) + per-instance (grid pos,
    atlas UV, fg/bg colors)
  - Push constants: screen size + cell size
- Glyph atlas: R8_UNORM texture, uploaded via staging buffer
  - fontdue rasterizes DejaVu Sans Mono at startup
  - Linear filtering sampler
- Descriptor set: combined image sampler for atlas
- 2 frames in flight with semaphores + fences
- Persistent mapped instance buffer (zero-copy per frame)

Shaders (pre-compiled SPIR-V):
- cell.vert: positions quad from instance data, maps to clip space
- cell.frag: samples atlas alpha, blends fg/bg

--mode vulkan: tries Vulkan, falls back to softbuffer window mode
109 tests, 0 failures
2026-06-21 00:34:01 +03:00
Emil 390eca9cda fix: winit + softbuffer for layout-agnostic input and better FPS
Replaced minifb with winit + softbuffer:

Input (layout-agnostic):
- winit uses PhysicalKey<KeyCode> which maps to physical key positions
- KeyCode::KeyA = physical A key, regardless of keyboard layout
- Works on Russian, Arabic, any layout — no key mapping needed
- HashSet<KeyCode> tracks pressed/released state
- Proper Press/Release events from OS, no timeout hacks

FPS improvements:
- Glyph atlas pre-built at startup (alpha bitmap, zero per-frame alloc)
- render_to_buffer: skip empty cells entirely (no draw call)
- blend_fast: bitshift instead of division for alpha blending
- Buffer fill via .fill() instead of nested loop
- copy_from_slice for pixel transfer to softbuffer (memcpy speed)
- ControlFlow::Poll for maximum frame rate

109 tests, 0 failures
2026-06-21 00:11:12 +03:00
Emil 65752b4cfe fix: window input uses get_keys() for held state, glyph atlas for FPS
Input fix: get_keys_pressed(No) only fires on initial press, not held.
Switched to get_keys() which returns all currently-down keys every frame.
Jump still edge-triggered via jump_was_down flag.

Performance fix: pre-build glyph atlas at startup (all ASCII chars
rasterized once into 128x256 alpha bitmap). No per-frame cloning.
draw_cell reads atlas alpha + blends fg/bg inline. Zero allocations
in render loop.

109 tests, 0 warnings
2026-06-21 00:01:28 +03:00
Emil 19bd883645 feat: window mode with minifb — direct keyboard polling, no terminal
New --mode window (now default):
- minifb creates a real OS window (no terminal needed)
- fontdue rasterizes DejaVu Sans Mono glyphs to pixel buffer
- ASCII characters rendered as colored pixels — same aesthetic
- Keyboard polled via get_pressed_keys() every frame:
  - Instant response, no terminal delay
  - Proper multi-key support (W+A strafing works)
  - No Release event hacks needed
  - No key repeat timeout hacks
  - Instant stop when key released

Controls: WASD/arrows, 1-9/0 paint, X erase, HJKL camera, Q/Esc quit
--mode terminal still available as fallback

109 tests, 0 warnings, 0 failures
2026-06-20 23:58:05 +03:00
Emil 6d98c09ece fix: request keyboard enhancement flags for proper Release events
Root cause of both issues: terminals don't send Release events by
default. When pressing W while holding A, terminal stops repeating A
(but doesn't send Release), so A times out and strafing breaks.
On key release, 400ms timeout means 400ms of extra movement.

Fix: PushKeyboardEnhancementFlags(REPORT_EVENT_TYPES) in terminal init.
This asks the terminal to send proper Press/Release/Repeat events for
ALL keys, not just special ones.

With Release events:
- Key release is instant (no 400ms timeout delay)
- Pressing W doesn't cancel A's held state (terminal sends Release
  only when A is actually released)
- A/D strafing works while W is held

Fallback: if terminal doesn't support enhancement flags (old xterm),
got_release flag stays false and 150ms timeout is used. Once any
Release event is received, all keys switch to Release-based mode
(infinite timeout, rely on actual Release events).

109 tests, 0 warnings, 0 failures
2026-06-20 23:48:03 +03:00
Emil 61e2cf1544 fix: dual-timeout held keys to fix terminal initial repeat delay
Root cause: terminals wait 300-500ms after initial Press before
sending the first Repeat event. With 60ms timeout, the key was
released during this gap, causing step-pause-accelerate pattern.

Fix: two timeout phases:
- After Press (no Repeat seen yet): 400ms timeout
  Covers terminal's initial repeat delay, key stays held
- After first Repeat: 80ms timeout
  Repeats arrive every ~30ms, 80ms is safe margin
  Quick release detection once repeating starts

Result: smooth continuous movement from first keypress, no gap
2026-06-20 23:43:31 +03:00
Emil 9458d265f0 fix: background input thread for responsive non-blocking input
Problem: event::poll(0ms) once per frame missed events between frames,
causing laggy/missed input. Terminal key repeat timing was unpredictable.

Fix: dedicated input thread that blocks on event::read() and pushes
all events to an mpsc channel. Game loop drains channel with try_iter()
each frame — gets every event without blocking, zero missed inputs.

- InputHandler::start() spawns background thread
- InputHandler::stop() cleans up on game exit
- try_iter() collects all accumulated events per frame
- Removed 16ms sleep (no longer needed, input doesn't block)
- Hold timeout reduced to 60ms (events arrive more reliably now)
- 109 tests, 0 warnings, 0 failures
2026-06-20 23:41:33 +03:00
Emil 3aa67140c8 feat: vector-based movement for responsive control and air strafing
Changed from physics-based (accumulate + damping) to vector-based:

- Horizontal velocity is SET directly, not accumulated
  - Press A → cvx = -0.5 (instant, no ramp-up)
  - Release → cvx = 0 (instant stop, no sliding)
  - No horizontal damping = precise control
- Air strafing: same horizontal speed in air as on ground
  - Can change direction mid-jump freely
- Vertical: gravity + minimal damping (0.99) for natural fall
  - Jump sets cvy directly, gravity pulls back
- move_speed 0.5 (was 0.3), max_vel 2.0 (was 1.0)
- Player: move_dir tracks current direction, stop_horizontal() clears velocity
- handle_input: calls stop_horizontal when no movement key held

109 tests, 0 warnings, 0 failures
2026-06-20 23:35:41 +03:00
Emil 4b66be755c docs: add Phase 1.5 UI Layer to PLAN.md
Non-destructive overlay architecture:
- UiLayer: sparse map of screen positions → (char, fg, bg)
- Composites on top of world render, never writes to grid/entities
- Health bars, HUD, message log, inventory overlay, minimap, menus
- Terminal: HashMap overlay after world draw
- Vulkan: separate render pass on top of world
- Pipe protocol exports world state only (not UI)
- Key principle: UI reads state, renders visuals, never mutates game
- Added: ui/ module in file structure, 0.25 milestone, locked decision
2026-06-20 23:32:29 +03:00
Emil e1eb9cc7cd fix: terminal input - timeout-based held keys, press-only jump
Terminals don't send Release events for most keys, so held keys
stayed active forever. Two problems:

1. W = infinite jump: Jump was in held_actions, fired every tick
   while held. check_on_ground returned true immediately after
   landing, causing auto-bounce.
   Fix: Jump fires only on initial Press event (jump_pressed flag),
   not on Repeat or held state.

2. A ignores D: When both A and D were held, both applied velocity
   and cancelled out. But terminal never sends Release for A, so
   D couldn't take over.
   Fix: Left/Right are mutually exclusive — most recently pressed
   key wins (compares last_seen timestamps).

3. Held key timeout: Keys expire after 80ms without a Repeat event,
   simulating Release for terminals that don't send it.

109 tests, 0 warnings, 0 failures
2026-06-20 23:30:37 +03:00
Emil 00c6b3bf87 fix: held-key input model for smooth movement
Problem: terminal key repeat sends one event, then pauses ~500ms,
then floods repeats. This caused stop-then-accelerate movement.

Fix: track held keys (Press/Release events), apply movement every
tick based on held state, not per-event. Jump still fires once per
press. Paint and quit are one-shot actions.

- InputHandler: held: Vec<HeldKey> tracks Left/Right/Jump/Camera
- update(): drains all pending events, updates held state
- held_actions(): returns active movement actions per tick
- handle_input: applies held actions every frame
- Camera pan reduced to 2/tick (was 5) for smoothness
- 109 tests, 0 warnings, 0 failures
2026-06-20 23:28:11 +03:00
Emil 61b5d551ae test: 109 Rust tests + 14 JSON scenarios, polish physics
Tests added:
- physics_materials.rs: 15 tests (fire, smoke, steam, grass, dirt, bone, wood, stone, lava temp)
- physics_interactions.rs: 12 tests (lava+water, fire spread, acid interactions, sand+water)
- player_controls.rs: 12 tests (move L/R, jump, double jump, rapid input, wait, continuous)
- collision_robust.rs: 10 tests (walls, ceiling, sliding, corridor, slope, dirt wall, unstick)
- ragdoll_death.rs: 7 tests (death transition, ragdoll fall, progressive damage, corpse)
- determinism.rs: 8 tests (same seed, replay exact, replay partial, recording, 100-tick)
- edge_cases.rs: 19 tests (boundary, stress 20 goblins, fill/clear/paint, gravity, camera)
- 6 new JSON scenarios (fire chain, lava pool, sand bury, entity fall, steam, acid wall)

Fixes:
- AABB resolve_aabb_y: add vertical overlap check (was pushing player down when jumping)
- AABB resolve_aabb_x: velocity-aware resolution (player was stuck against walls)
- check_on_ground: use fractional position check (was always true)
- jump_force 1.5 (was 0.8) for 27-body entity
- Gravity read from self.verlet (was from clone, SetGravity didn't work)
- u8 overflow in lava color (saturating_add)

109 Rust tests, 14 JSON scenarios, 0 warnings, 0 failures
2026-06-20 22:42:29 +03:00
Emil 8b1f5d4768 fix: u8 overflow in lava color calculation (saturating_add) 2026-06-20 22:14:26 +03:00
Emil bcbf474df6 docs: add 4 new phases to PLAN.md
- Phase 4b: Graphics over ASCII (lighting, particles, textures, post-processing)
- Phase 6: Multi-layer world (separate grids for air, pressure, temp, light)
- Phase 7: AI agent integration (LLM via pipe + RL with tensor state)
- Phase 8: Web arena (WASM render, WebSocket server, multiplayer, training pipeline)
- Updated: milestones to 0.8 + 1.0 Q3 2027
- Updated: open questions, file structure, testing strategy, perf targets
2026-06-20 22:10:33 +03:00
Emil 99d49dd213 docs: add PLAN.md with roadmap, architecture, milestones 2026-06-20 22:05:29 +03:00
Emil e954cacdb5 fix: add sleep to game loop, panic hook restores terminal
- 16ms sleep per frame prevents 100% CPU spin
- Panic hook restores terminal (disable raw mode, leave alternate screen)
  before printing panic message, so errors are visible
- Remove debug eprintlns
2026-06-20 22:03:30 +03:00
Emil 159b7c6f61 fix: surface renderer errors, clean up all warnings
- Renderer init/render/shutdown errors now printed instead of silently
  swallowed with let _ =
- Auto-fixed all unused imports and variables (cargo fix)
- Zero compiler warnings
2026-06-20 22:00:36 +03:00
Emil 1bda28a02b fix: surface renderer errors instead of silently ignoring 2026-06-20 21:58:47 +03:00
Emil a66b4ac0cb fix: AABB capsule collider for rigid entities, sliding on surfaces
- Replace per-body collision with single AABB collider for rigid entities
- Entity has half_w/half_h defining the collider box
- Separate axis resolution: X first (min-penetration push), then Y
  (velocity-direction push to handle floor/ceiling correctly)
- Sliding: when hitting a wall, only X velocity is zeroed, Y continues
- check_on_ground uses AABB bottom edge, not individual bodies
- Fix: X resolution uses minimum penetration (was velocity-based,
  which pushed player through walls when velocity was zero)
- Default scene: added water pool, lava pool, wood structure, sand dune,
  acid pool, stone wall obstacle
- 28/28 tests passing
2026-06-20 21:56:13 +03:00
Emil a731d5cbb7 feat: 5x5 entity (27 bodies), terminal viewport up to 250x120
- Entity: 5x5 grid body + 2-cell arm = 27 sub-bodies, radius 0.5
- Terminal: default 200x60, max 250x120 (was 250x80)
- 31 constraints (full grid + arm)
- Spawn height adjusted for 5-tall body
2026-06-20 21:43:50 +03:00
Emil 595edc3006 feat: simpler entities - 3x3 rectangle with hand (10 sub-bodies)
- Entity: 10 sub-bodies (was 16), radius 0.45 (was 0.7)
- Shape: 3x3 body rectangle + 1 hand cell on right side
- Much more compact, world appears larger relative to character
- 13 constraints (grid pattern + hand attachment)
- Spawn height adjusted for smaller body
2026-06-20 21:42:16 +03:00
Emil e96744b958 feat: rigid living entities, ragdoll corpses, fix WASD movement
- Entity: rigid body mode for alive entities (single object, no wobble)
  - Center position + velocity, bodies positioned from rest offsets
  - Collision: all 16 bodies checked, push applied to center
  - Movement: velocity applied to center, all bodies move together
- Entity: ragdoll mode for corpses (loose Verlet constraints)
  - kill() switches rigid->ragdoll, gives each body inherited velocity
  - Constraints go slack (stiffness=0), bodies fall independently
- Player: move_left/right now applies velocity to entity center (was: head only)
- Physics: lava heat conductivity 0.05 (was 0.5), initial temp 1500 (was 1200)
  Lava cooling threshold 400 (was 800), heat transfer rate 0.1 (was 0.5)
  Lava stays hot long enough for entities to take damage
- Tests: 28/28 passing, updated for new lava and rigid body behavior
2026-06-20 21:39:26 +03:00
Emil 9fe7137e45 feat: bigger characters (16 sub-bodies), wider terminal viewport
- Entity: 16 sub-bodies (was 7), radius 0.7 (was 0.4), full humanoid
  shape: head, shoulders, torso, arms, hips, legs, feet
- Terminal: viewport up to 250x80 (was 120x50), uses full terminal size
- Camera: uses renderer viewport dimensions instead of hardcoded values
- Physics: constraint correction clamped to 0.5 cells max, 8 substeps
  (was 4), gravity 0.04 (was 0.08), collision pass after each constraint
  iteration to prevent tunneling with larger bodies
- Player: move_speed 0.3, jump_force 1.2 for bigger body
- Spawn: 6 cells above surface (was 4) for taller body
2026-06-20 21:27:11 +03:00
Emil 7f4b1dde5e feat: AI integration - pipe protocol, test framework, replay system
AI Session API (src/ai/session.rs):
- GameSession wraps Game with seeded determinism
- init/init_empty, step(n), perform_action, get_state
- get_cell, get_region, get_entities, get_player, count_material

JSON Pipe Protocol (src/ai/protocol.rs):
- --mode pipe: stdin/stdout JSON line protocol
- Commands: init, step, action, get_state, get_view, get_cell,
  get_region, get_entities, get_player, count_material, find_material,
  record_start/stop, replay_save, run_scenario, quit

Test Framework:
- 27 Rust integration tests (tests/*.rs) covering sand, water, lava,
  acid, fire physics + entity movement, damage, ragdoll
- 8 JSON scenarios (scenarios/*.json) runnable via --mode test
- Scenario assertions: cell_is, cell_is_not, no_material_in_region,
  material_count_in_region, entity_alive/dead, player_on_ground, etc.

Replay System (src/ai/replay.rs):
- Record all actions + seed for deterministic playback
- ReplayPlayer::play() and play_until_tick() for debugging
- --mode replay --replay-file PATH

Other changes:
- src/lib.rs: library target for integration tests
- Collision: post-constraint collision pass to reduce tunneling
- serde + serde_json dependencies
- All enums use rename_all = snake_case for JSON compatibility
2026-06-20 18:53:21 +03:00
Emil 3cd94a386a fix: collision tunneling, spawn at surface, physics substepping
- Fix: rewrite resolve_grid_collision to push body out of solid cells
  when center is inside (was only pushing to small radius from center)
- Fix: max velocity clamp (0.8) + 4x substepping prevents tunneling
- Fix: player/goblin spawn at terrain surface instead of mid-air
- Fix: gravity reduced from 0.3 to 0.08 for substepping
- Add: headless mode (--headless-ticks N) dumps grid state to file
- Add: velocity zeroing on collision for stable resting
2026-06-20 18:26:33 +03:00
Emil 68f6292c4d feat: Verbatim MVP - terminal renderer, cellular automaton, Verlet physics
- World: 250x250 grid with 14 materials (sand, water, lava, stone, wood, etc.)
- Physics: cellular automaton for materials + Verlet solver for entities
- Entity: multi-cell humanoid (7 sub-bodies with distance constraints)
- Render: terminal renderer with ANSI colors and diff-based updates
- Game loop: fixed 60Hz timestep with accumulator pattern
- Input: WASD movement, number keys for material painting
2026-06-20 18:21:26 +03:00