The BFS solver couldn't account for obstacles blocking push paths -
knowing the button is reachable doesn't mean the player can get to
the correct side of the boulder. Reverse-pull guarantees solvability
by construction: start with boulder on button, simulate valid
un-pushes to move it away. Each un-push verifies both the new boulder
cell and the player's required push position are walkable.
Also fixes chest clumping: level 2 previously crammed 3 treasures +
boulder + button into a single room. Redesigned all level plans to
spread treasures across rooms (max 1 per room). Updated lv_planner
for procedural levels 9+ with the same constraint.
Level plans no longer specify "boulder" - it's auto-generated from
the button position with min_pulls scaling by depth (2-8).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs that could produce unsolvable puzzles:
1. Feature placement fallback used leaf_center without duplicate checking,
allowing treasures to spawn on the same cell as buttons in dense rooms
(level 2 puts 8 features in one room). Fixed with exhaustive cell scan
fallback.
2. Solvability checker ignored treasure entities entirely. Boulders cannot
be pushed through treasures (TreasureEntity.bump returns False for
non-player entities), so the solver now models them as boulder-blocking
obstacles while allowing player movement through them.
3. Added explicit button-blocked-by-obstacle check before running the
full BFS solver, catching the most common failure mode early.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces jam-quality code with production engine features (addresses #248):
- cos_level.py: Replace custom BinaryRoomNode/RoomGraph with mcrfpy.BSP
for room generation, using adjacency graph for corridor placement
- cos_solver.py: New Sokoban BFS solvability checker; levels retry up
to 10 times if unsolvable
- game.py: Add ColorLayer fog of war with room-reveal mechanic
(visible/discovered/unknown states), compute_fov per player move
- cos_entities.py: Enemy AI state machine (idle/aggressive/fleeing)
with A* pathfinding, fix duplicate direction bug on line 428/444
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full screen "wasm-game" for viewport compatibility between desktop and
web interfaces. Viewport modes ("fit", "center", and "stretch") should
now work the same way under WASM/SDL and SFML. This should also enable
android or web-for-mobile aspect ratios to be supported more easily.
Replace no-op audio stubs in SDL2Types.h with real SDL2_mixer-backed
implementations of SoundBuffer, Sound, and Music. This enables audio
playback in the browser with zero changes to Python bindings.
- Add -sUSE_SDL_MIXER=2 to Emscripten compile/link flags (CMakeLists.txt)
- Initialize Mix_OpenAudio in SDL2Renderer::init(), Mix_CloseAudio in shutdown()
- SoundBuffer: Mix_LoadWAV/Mix_LoadWAV_RW with duration computation
- Sound: channel-based playback with Mix_ChannelFinished tracking
- Music: global channel streaming via Mix_LoadMUS/Mix_PlayMusic
- Volume conversion: SFML 0-100 scale to SDL_mixer 0-128 scale
Known limitations on web: Music.duration and Music.position getters
return 0 (SDL_mixer 2.0.2 lacks Mix_MusicDuration/Mix_GetMusicPosition).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaced the NotImplementedError stub with a full animation
implementation. Entity3D now supports animating: x, y, z,
world_x, world_y, world_z, rotation, rot_y, scale, scale_x,
scale_y, scale_z, sprite_index, visible.
Added Entity3D as a third target type in the Animation system
(alongside UIDrawable and UIEntity), with startEntity3D(),
applyValue(Entity3D*), and proper callback support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EntityCollection3D now has API parity with UIEntityCollection:
- pop(index=-1): Remove and return entity at index
- find(name): Search by entity name, return Entity3D or None
- extend(iterable): Append multiple Entity3D objects
Also adds `name` property to Entity3D for use with find().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
screen_to_world() previously only intersected the Y=0 plane.
Now accepts an optional y_plane parameter (default 0.0) for
intersecting arbitrary horizontal planes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously enableRenderTexture() silently failed. Now emits a
stderr warning with the requested dimensions, consistent with
the engine's logging pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The root cause was PyViewport3DType being declared `static` in
Viewport3D.h, creating per-translation-unit copies. Entity3D.cpp's
copy was never passed through PyType_Ready, causing segfaults when
tp_alloc was called.
Changed `static` to `inline` (matching PyEntity3DType and
PyModel3DType patterns), and implemented get_viewport using the
standard type->tp_alloc pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add -sUSE_FREETYPE=1 to Emscripten build flags
- Extend Font class with FT_Library, FT_Face, and FT_Stroker handles
- Rewrite FontAtlas to use FreeType with on-demand stroked glyph loading
- Text outlines now use FT_Stroker for vector-based stroking before
rasterization, eliminating gaps at corners with thick outlines
- Use glTexSubImage2D for incremental atlas updates (major perf fix)
- Disable canvas border in shell.html per Emscripten docs (alignment fix)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>