Entities can now specify per-tile sprite indices via the sprite_grid
property. When set, each tile in a multi-tile entity renders its own
sprite from the texture atlas instead of the single entity sprite.
API:
entity.tile_size = (3, 2)
entity.sprite_grid = [[10, 11, 12], [20, 21, 22]]
entity.sprite_grid = None # revert to single sprite
Accepts nested lists, flat lists, or tuples. Use -1 for empty tiles.
Dimensions must match tile_width x tile_height.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CMake: Add MCRF_FREE_THREADED_PYTHON option to link python3.14t with
Py_GIL_DISABLED. Extends __lib_debug/ link path for free-threaded builds.
Makefile: Add `make tsan` and `make tsan-test` targets for ThreadSanitizer
builds using free-threaded CPython. Add build-tsan to clean-debug.
The instrumented libtcod build script (tools/build_debug_libs.sh) was
included in the prior commit - it builds libtcod-headless with ASan/TSan
instrumentation for full sanitizer coverage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers build issues, runtime debugging, browser dev tools, deployment
sizing, embedding, and known limitations for Emscripten/WebAssembly builds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds MCRF_WASM_DEBUG CMake option that enables -g4, -gsource-map, and
--emit-symbol-map for WASM builds. New Makefile targets: wasm-debug,
playground-debug, serve-wasm-debug, serve-playground-debug.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entities can now span multiple grid cells via tile_width and tile_height
properties (default 1x1). Frustum culling accounts for entity footprint,
and spatial hash queries return multi-tile entities for all covered cells.
API: entity.tile_size = (2, 2) or entity.tile_width = 2; entity.tile_height = 3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Textures can now specify display_size and display_origin to crop sprite
rendering to a sub-region within each atlas cell. This supports texture
atlases where content doesn't fill the entire cell (e.g., 16x24 sprites
centered in 32x32 cells).
API: Texture("sprites.png", 32, 32, display_size=(16, 24), display_origin=(8, 4))
Properties: display_width, display_height, display_offset_x, display_offset_y
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers #268 (sfVector2f_to_PyObject NULL propagation) and #272
(UniformCollection weak_ptr validity check). Combined with existing
tests for #258-#278, this completes regression coverage for the
full memory safety audit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add stairs position to occupied set to prevent enemy/item overlap
- Remove old fog layer before creating new one on level transitions
- Reset fog_layer reference on new game to avoid stale grid reference
- Wrap FS.mkdir('/save') in try/catch for page reload resilience
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers two previously untested bug families from the memory safety audit:
- #265: GridPointState references after entity grid transfer
- #267, #275: Reference count leaks in collection/property access loops
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
to_bytes() returns raw uint8 cell data as Python bytes object.
from_bytes(data, size, enum=None) is a classmethod that constructs
a new DiscreteMap from serialized data with dimension validation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UIFrame, UICaption, and UISprite now accept "pos" as an alias for "position"
in the animation property system. UICaption and UISprite gain Vector2f
setProperty/getProperty overrides enabling animate("pos", (x, y), duration).
Color compound animation (fill_color, outline_color) was already supported.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies that die() during grid.entities iteration raises RuntimeError
(iterator invalidation protection), that the safe collect-then-die
pattern works, and that die() properly removes from spatial hash.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When transferring an entity to a new grid via entity.grid = new_grid,
the entity was removed from the old grid's spatial hash but never
inserted into the new one. This made it invisible to spatial queries
on the destination grid.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UIEntity::setProperty() now calls spatial_hash.update() when draw_x/draw_y
change, matching the Python property setter behavior. Added
enable_shared_from_this<UIEntity> to support shared_from_this() in the
setProperty path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GridLayer, UIGridPoint, and GridChunk each stored a raw GridData* that
could dangle if the grid was destroyed while external shared_ptrs
(e.g. Python layer wrappers) still referenced child objects. The
destructor now nulls all parent_grid pointers before cleanup. All
usage sites already had null guards, so this completes the fix.
These were the last three unfixed bugs from the memory safety audit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
#286: Change detect_leaks=0 to detect_leaks=1 in asan-test target.
LSAN suppressions for CPython intentional leaks (interned strings, type
objects, small int cache, etc.) were already in sanitizers/asan.supp.
Now that #266 and #275 are fixed, real McRogueFace leaks will be caught.
#284: Add make massif-test target that runs stress_test_suite.py under
Valgrind Massif for heap profiling. Output goes to build-debug/massif.out,
viewable with ms_print.
Closes#286, closes#284
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Breaking API change: removes 4 camelCase function aliases from the mcrfpy
module. The snake_case equivalents (set_scale, find_all, get_metrics,
set_dev_console) remain and are the canonical API going forward.
- Removed setScale, findAll, getMetrics, setDevConsole from mcrfpyMethods[]
- Updated game scripts to use snake_case names
- Updated test scripts to use snake_case names
- Removed camelCase entries from type stubs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Layers with z_index <= 0 now render below entities (ground level), and
only layers with z_index > 0 render above entities. Previously z_index=0
was treated as "above entities" which was unintuitive -- entities stand
on ground level (z=0) with their feet, so z=0 layers should be beneath.
Changed in both UIGrid.cpp and UIGridView.cpp render methods:
- "z_index >= 0" to "z_index > 0" for break condition
- "z_index < 0" to "z_index <= 0" for skip condition
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes a systemic bug where Python tp_getset property setters bypassed the
render cache dirty flag system (#144). The animation/C++ setProperty() path
had correct dirty propagation, but direct Python property assignments
(e.g. frame.x = 50, caption.text = "Hello") did not invalidate the parent
Frame's render cache when clip_children or cache_subtree was enabled.
Changes by file:
- UIDrawable.cpp: Add markCompositeDirty() to set_float_member (x/y),
set_pos, set_grid_pos; add markDirty() for w/h resize
- UICaption.cpp: Add markDirty() to set_text, set_color_member,
set_float_member (outline/font_size); markCompositeDirty() for position
- UICollection.cpp: Add markContentDirty() on owner in append, remove,
pop, insert, extend, setitem, and slice assignment/deletion
- UISprite.cpp: Add markDirty() to scale/sprite_index/texture setters;
markCompositeDirty() to position setters
- UICircle.cpp: Add markDirty() to radius/fill_color/outline_color/outline;
markCompositeDirty() to center setter
- UILine.cpp: Add markDirty() to start/end/color/thickness setters
- UIArc.cpp: Add markDirty() to radius/angles/color/thickness setters;
markCompositeDirty() to center setter
- UIGrid.cpp: Add markDirty() to center/zoom/camera_rotation/fill_color/
size/perspective/fov setters
Closes#288, closes#289, closes#290, closes#291
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create self-contained demo game script (src/scripts_demo/game.py) showcasing:
BSP dungeon generation, Wang tile autotiling, FOV with fog of war,
turn-based bump combat, enemy AI, items/treasure, title screen
- Add MCRF_DEMO CMake option for building with demo scripts
- Add web/index.html landing page with dark theme, controls reference,
feature list, and links to GitHub/Gitea
- Build with: emcmake cmake -DMCRF_SDL2=ON -DMCRF_DEMO=ON -DMCRF_GAME_SHELL=ON
Note: Makefile wasm-demo/serve-demo targets also added locally but Makefile
is gitignored. Use CMake directly or force-add the Makefile to track it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Categorize open issues #53–#304 into 14 system-related groups,
prioritized by impact. Recommends tackling dirty-flag bugs (#288-#291)
and dangling-pointer bugs (#270, #271, #277) first.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All tutorial parts (1-13) used the old string-based key/action
comparison API removed in 6d5e99a. Every handle_keys function now
uses mcrfpy.Key.* and mcrfpy.InputState.PRESSED enums.
Additional fixes across all parts:
- Replace manual FOV computation with ColorLayer.draw_fov() which
handles FOV calculation and explored-state tracking in one call
- Replace old grid.add_layer("color") with ColorLayer() constructor
- Fix entity removal bug: entities.remove(index) -> remove(entity_ref)
- Remove manual exploration tracking (draw_fov handles it internally)
- Use tuple positions for compute_fov/is_in_fov: (x, y) not x, y
All 14 parts (0-13) tested and passing in headless mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removed custom __eq__/__ne__ that allowed comparing enums to legacy string
names (e.g., Key.ESCAPE == "Escape"). Removed _legacy_names dicts and
to_legacy_string() functions. Kept from_legacy_string() in PyKey.cpp as
it's used by C++ event dispatch. Updated ~50 Python test/demo/cookbook
files to use enum members instead of string comparisons. Also updates
grid.position -> grid.pos in files that had both types of changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Grid.position was a redundant alias for Grid.pos. Removed get_position/
set_position functions, getsetters entry, and setProperty/getProperty/
hasProperty branches. Updated all tests to use grid.pos.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sprite_number was a legacy alias for sprite_index. All code should use
sprite_index directly. Removed from getsetters, setProperty/getProperty/
hasProperty in UISprite and UIEntity, animation property handling, and
type stubs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review of session 1b14b941 found two issues:
- Exported type count was 44 in audit doc but array has 46 entries
(EntityCollection3DIterType and one other were not counted)
- No regression test existed for the Color.__eq__/__ne__ fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous label IDs were incorrect (possibly from an older state).
Updated to match the live Gitea API as of 2026-04-09. Key differences:
- ID 2 is Minor Feature (was Alpha Release)
- ID 3 is Tiny Feature (was Bugfix)
- ID 8 is Bugfix (was tier2-foundation)
- System labels start at 9 (were at 11)
- Priority labels start at 17 (were at 7)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Color had __hash__ but no __eq__/__ne__, violating the Python convention
that hashable objects must support equality comparison. Two Color objects
with identical RGBA values would not compare equal.
Now supports comparison with Color objects, tuples, and lists:
Color(255, 0, 0) == Color(255, 0, 0) # True
Color(255, 0, 0) == (255, 0, 0) # True
Color(255, 0, 0) != (0, 0, 0) # True
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add find_all(), get_metrics(), set_scale(), and set_dev_console() as
snake_case alternatives to the existing camelCase names. The camelCase
versions remain for backward compatibility but their docstrings now
note the preferred snake_case form. All will be removed in 1.0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace placeholder docstrings ("SFML Vector Object", "SFML Font Object",
etc.) with comprehensive constructor signatures, argument descriptions,
and property listings matching the documentation standard used by other
types like Color and Frame.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mcrfpy.Grid() now creates a GridView that internally owns a GridData (UIGrid).
The old UIGrid type is renamed to _GridData (internal). Attribute access on Grid
delegates to the underlying UIGrid via tp_getattro/tp_setattro, so all existing
Grid properties (grid_w, grid_h, entities, cells, layers, etc.) work transparently.
Key changes:
- GridView init has two modes: factory (Grid(grid_size=...)) and explicit view
(Grid(grid=existing_grid, ...)) for future multi-view support
- Entity.grid getter returns GridView wrapper via owning_view back-reference
- Entity.grid setter accepts GridView objects
- GridLayer set_grid handles GridView (extracts underlying UIGrid)
- UIDrawable::removeFromParent handles UIGRIDVIEW type correctly
- UIFrame children init accepts GridView objects
- Animation system supports GridView (center, zoom, shader.* properties)
- PythonObjectCache registration preserves subclass identity
- All 263 tests pass (100%)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `collide` kwarg to Grid.find_path() and Grid.get_dijkstra_map() that
treats entities bearing a given label as impassable obstacles via
mark-and-restore on the TCOD walkability map. Dijkstra cache key now
includes collide label for separate caching. Add Entity.find_path()
convenience method that delegates to the grid.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
frame_children_mutation_test: validates remove(), property mutation via
stored refs, fill_color persistence, iteration mutation, pop(), and
while-loop clearing — with visual screenshot verification.
parent_none_removal_test: validates that setting .parent = None removes
children from Frame.children and scene.children, Entity.grid = None
removal, and Grid overlay children removal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generates a network of bezier-curved dirt paths connecting POIs placed
via grid-distributed random selection. Uses minimum spanning tree with
extra fork edges, noise-offset control points for organic curves, and
the "pathways" edge-type Wang set for directional path tiles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Advances to 83b3e6ce which adds Dijkstra distance maps, multi-root
Dijkstra, A* heuristic functions, and pathfinding demo.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Grid.__init__() now auto-creates a GridView that shares the Grid's
data via aliasing shared_ptr. This enables the Grid/GridView split:
- PyUIGridObject gains a `view` member (shared_ptr<UIGridView>)
- Grid.view property exposes the auto-created GridView (read-only)
- Rendering property setters (center_x/y, zoom, camera_rotation, x, y,
w, h) sync changes to the view automatically
- Grid still works as UIDrawable in scenes (no substitution) — backward
compatible with all existing code and subclasses
- GridView.grid returns the original Grid with identity preservation
- Explicit GridViews (created by user) are independent of Grid's own
rendering properties
Addresses #252. All 260 tests pass, no breaking changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GridView is a new UIDrawable that renders a GridData object independently.
Multiple GridViews can reference the same Grid for split-screen, minimap,
or different camera/zoom perspectives on shared grid state.
- New files: UIGridView.h/cpp with full rendering pipeline (copied from
UIGrid::render, adapted to use grid_data pointer)
- Add UIGRIDVIEW to PyObjectsEnum
- Add UIGRIDVIEW cases to all switch(derived_type()) sites:
Animation.cpp, UICollection.cpp, UIDrawable.cpp, McRFPy_API.cpp,
ImGuiSceneExplorer.cpp
- Python type: mcrfpy.GridView(grid=, pos=, size=, zoom=, fill_color=)
- Properties: grid, center, zoom, fill_color, texture
- Register type with metaclass for callback support
All 258 existing tests pass. New GridView test suite added.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix Entity3D self-reference cycle: replace raw `self` pointer with
`pyobject` strong-ref pattern matching UIEntity (closes#266)
- TileLayer inherits Grid texture when none set, in all three attachment
paths: constructor, add_layer(), and .grid property (closes#254)
- Add SpatialHash::queryCell() for O(1) entity-at-cell lookup; fix
missing spatial_hash.insert() in Entity.__init__ grid= kwarg path;
use queryCell in GridPoint.entities (closes#253)
- Add FOV dirty flag and parameter cache to skip redundant computeFOV
calls when map unchanged and params match (closes#292)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Phase 3 fix for #266 removed UIEntity::self which prevented
tp_dealloc from ever running. However, this also allowed Python
subclass wrappers (GameEntity, ZoneExit, etc.) to be GC'd while
the C++ entity lived on in a grid. Later access via grid.entities
returned a base Entity wrapper, losing all subclass methods.
Fix: Add UIEntity::pyobject field that holds a strong reference to
the Python wrapper. Set in init(), cleared when the entity leaves
a grid (die(), set_grid(None), collection removal). This keeps
subclass identity alive while in a grid, but allows proper GC when
the entity is removed. Added releasePyIdentity() helper called at
all grid exit points.
Regression test exercises Liber Noster patterns: subclass hierarchy,
isinstance() checks, combat mixins, tooltip/send methods, GC
survival, die(), pop(), remove(), and stress test with 20 entities.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UniformCollection accessor methods checked the raw collection pointer
but never checked if the owning object was still alive. Changed owner
field from weak_ptr<UIDrawable> to weak_ptr<void> (type-erased) so
both UIDrawable and UIEntity owners can be tracked. Set owner in both
get_uniforms() paths. All accessors now check owner.lock() before
dereferencing the raw collection pointer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add mutex lock to PythonObjectCache::lookup() - cache.find() was
unprotected against concurrent modification. Document that entity.die()
must not be called during iteration over grid.entities.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>