Commit graph

435 commits

Author SHA1 Message Date
e42159608f CLAUDE.md: add Grid.step/behavior API Quick Reference snippets; closes kanboard #38
- Remove stale Animation deprecation block (mcrfpy.Animation export was removed in a8c29946, not just deprecated)
- Add Grid.step() turn manager usage example
- Add Entity.set_behavior() + Behavior enum examples (SEEK, WAYPOINT, etc.)
- Add Entity.labels, Entity.turn_order, Entity.cell_pos property examples
- Add Trigger callback pattern (enemy.step = on_trigger)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 23:41:24 -04:00
17664ba741 Fix capture_audio_synth_header.py for headless --exec mode
Timers never fire in --headless --exec mode (the game loop runs but
does not process the timer queue without explicit step() calls).
Replace the Timer-based screenshot with a step() loop that advances
the engine 30 ticks at 10ms each before calling automation.screenshot().

This fix was discovered and applied as part of blog post 0033 publication
(Kanboard #209 / #345).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 23:25:17 -04:00
988f0be369 Add header image capture script for blog post 0033
Headless screenshot tool for the audio_synth_demo SFXR scene.
Refs Kanboard #209 / #345.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 23:04:22 -04:00
98d2b36739 Regenerate docs and stubs after API freeze pass
Picks up the five 1.0 freeze commits (groups A-D + Grid follow-up):
  * mcrfpy.Animation removed from module exports
  * parent= kwarg on Frame/Caption/Sprite/Line/Circle/Arc/Grid
  * grid= kwarg on ColorLayer/TileLayer
  * Constructor positional reorders (Circle, Caption, Layers)
  * __getitem__/__setitem__ on ColorLayer/TileLayer/Grid

PyMethodDef/PyGetSetDef compliance baseline (per tools/audit_pymethoddef.py):
  PyGetSetDef: 190/410 MACRO (46.3%)
  PyMethodDef: 150/345 MACRO (43.5%)
413 raw-string docstring entries remain to migrate to MCRF_METHOD/
MCRF_PROPERTY in a separate sweep.
2026-04-18 13:35:26 -04:00
0f7254eaf4 Add parent= kwarg to Grid (follow-up to constructor surface freeze)
Group A flagged that Grid lacked the parent= kwarg added to all other UI
drawables. Grid is also a UIDrawable that can live in a Scene's or Frame's
children -- closing the gap for full constructor consistency.

Implementation handles parent= uniformly across both Grid init modes:
  Mode 1 (explicit view of existing GridData): grid=existing_grid
  Mode 2 (factory mode): grid_size=(w, h)

UIGridView::init now strips parent= from the kwds (using a dict copy --
caller's kwds is never mutated), dispatches to the appropriate mode helper,
and applies UIDRAWABLE_ATTACH_TO_PARENT on success. The mode-1 inline body
moved to a new init_explicit_view helper for clean separation.
2026-04-18 13:34:40 -04:00
157ba9d011 Merge branch 'constructor-surface-1.0': pre-1.0 constructor signature freeze
Group A of the API freeze pass. No backward-compat shims.

Added parent= kwarg to Frame, Caption, Sprite, Line, Circle, Arc:
  mcrfpy.Frame(pos=(0,0), size=(100,100), parent=scene)
Validates against Frame/Scene/Grid/GridView and auto-appends to
parent.children. New UIDRAWABLE_ATTACH_TO_PARENT macro in UIBase.h
keeps the six call sites one-liners.

Added grid= kwarg to ColorLayer, TileLayer:
  mcrfpy.ColorLayer(name='fog', grid=g)
Routes through grid.add_layer(self) and matches the existing
Entity(grid=...) attachment pattern.

Reordered:
  Circle: (center, radius, ...) -- was (radius, center, ...)
  ColorLayer / TileLayer: (name, z_index, ...) -- was (z_index, name, ...)

Caption font is now keyword-only via PyArg_ParseTupleAndKeywords '$'
separator. Positional order is now (pos, text); font, fill_color,
outline_color, etc. must be passed as kwargs.

Test impact: test_constructor_comprehensive.py:77 uses positional
Caption(pos, None, text), which now fails. Other test sites use
kwargs and survive.
2026-04-18 13:29:58 -04:00
e1b167530c Pre-1.0 constructor surface freeze for UI shapes and Grid layers
Group A: parent= auto-attach kwarg
  - Added parent= to Frame, Caption, Sprite, Line, Circle, Arc.
    Validates against Frame/Scene/Grid/GridView and appends self
    to parent.children. Mirrors the existing Entity(grid=...) pattern.
    Implemented as a UIDRAWABLE_ATTACH_TO_PARENT macro in UIBase.h
    so all six call sites stay one-liners.
  - Added grid= to ColorLayer and TileLayer. Validates against Grid /
    GridView and routes through grid.add_layer(self).

Sub-changes (also pre-1.0 breakage, no shims):
  - Circle: positional order swapped from (radius, center, ...) to
    (center, radius, ...). Old positional callers will now fail at
    PyArg_ParseTupleAndKeywords with a TypeError.
  - Caption: font is now keyword-only. kwlist reordered to put pos
    and text first; the '$' separator in the format string makes
    everything that follows (font, fill_color, ...) keyword-only.
  - ColorLayer / TileLayer: kwlist reordered so that name comes
    before z_index. Format strings updated to match.

UIBase.h gains a UIDRAWABLE_ATTACH_TO_PARENT macro that mirrors
the existing UIDRAWABLE_PROCESS_ALIGNMENT pattern: type-check,
fetch .children, call append, propagate any error as -1.

Old positional callers across the codebase (Caption with positional
font, Circle with positional radius-first) will need to be updated
separately. No backward-compat shims have been added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:28:59 -04:00
f4b85a38a9 Merge branch 'subscript-protocol-1.0': add __getitem__/__setitem__ to layers and Grid
Pre-1.0 API freeze (Group B). Pythonic subscript access on ColorLayer,
TileLayer, and Grid mirrors the existing dunder support on HeightMap and
DiscreteMap. Existing .at() / .set() methods unchanged.

  * ColorLayer[x, y] -> Color; assignment via Color or (r,g,b[,a]) tuple
  * TileLayer[x, y]  -> int (sprite index)
  * Grid[x, y]       -> GridPoint (assignment raises TypeError -- a
                        GridPoint is a view; mutate its properties instead)

Keys must be 2-tuples; anything else raises TypeError. Out-of-bounds
coordinates raise IndexError.
2026-04-18 13:04:07 -04:00
c52a6a0db6 Add subscript protocol to ColorLayer, TileLayer, and Grid
Implements __getitem__/__setitem__ on ColorLayer, TileLayer, and Grid
(GridView) for ergonomic cell access, mirroring the existing pattern on
HeightMap and DiscreteMap. Part of the 1.0 API freeze ergonomic pass --
no existing .at() / .set() methods are removed or changed.

* ColorLayer[x, y] returns mcrfpy.Color; assignment accepts a Color or a
  3/4-tuple via PyColor::fromPy.
* TileLayer[x, y] returns / accepts an int (sprite index, -1 transparent).
* Grid[x, y] returns the same GridPoint as Grid.at(x, y); assignment raises
  TypeError because GridPoints are views, not assignable values.
* Internal _GridData (PyUIGridType) gets the same TypeError-raising setitem
  for consistency.

Keys are 2-tuples (x, y); anything else raises TypeError. Out-of-bounds
coordinates raise IndexError. Subscript on Grid (the user-facing GridView,
#252) delegates to its underlying GridData via aliasing shared_ptr, the
same way UIGridView::get_grid wraps the data.
2026-04-18 13:02:44 -04:00
439317cc33 Merge branch 'remove-animation-export': drop mcrfpy.Animation from public exports
Pre-1.0 API freeze (Group C). PyAnimationType moves from exported_types[]
to internal_types[]; drawable.animate(...) and mcrfpy.animations remain
unchanged because they construct/return Animation via C++ (make_shared +
tp_alloc), not via the Python module attribute.

mcrfpy.Animation(...) is intentionally gone — Animation lifetime is bound
to a single target object, which is more naturally expressed as
obj.animate(...). No backward-compat shim per the freeze policy.

Tests that import mcrfpy.Animation will fail and need updating in a
separate pass.
2026-04-18 12:37:02 -04:00
a8c29946e3 Remove mcrfpy.Animation from public Python module exports
Move PyAnimationType from exported_types[] to internal_types[] in
McRFPy_API.cpp. The C++ Animation class continues to exist; it is
constructed internally by drawable.animate(...) via std::make_shared
and wrapped via tp_alloc against the (still PyType_Ready'd) type.

Direct construction via mcrfpy.Animation(...) is removed because every
Animation is bound to a target object, which is more naturally
expressed as obj.animate(...). Pre-1.0 API freeze, no compat shim.

Tests using mcrfpy.Animation(...) will need to be updated separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:36:28 -04:00
4ff2f85ade Merge branch 'audit-pymethoddef-tool': MCRF_METHOD compliance auditor
Adds tools/audit_pymethoddef.py for the pre-1.0 doc-macro audit (Group D).
Baseline on master: 45.0% MCRF_METHOD/MCRF_PROPERTY compliance (340 of 755
entries). 413 raw-string docstrings to migrate; 2 entries with no docstring
at all (src/UIGridPyMethods.cpp:883 and :987).
2026-04-18 12:31:27 -04:00
626d5ae708 Add tools/audit_pymethoddef.py - MCRF_METHOD/MCRF_PROPERTY compliance auditor
Static-analysis tool for the pre-1.0 API freeze: walks src/**/*.cpp with
tree-sitter-cpp, locates every PyMethodDef and PyGetSetDef array initializer,
and classifies each entry's docstring slot as MACRO / RAW_STRING / NULL /
MISSING. The MACRO classification (MCRF_METHOD or MCRF_PROPERTY from
McRFPy_Doc.h) is the project's compliance target; raw string literals and
NULL docs predate the macro system and need migration before the 1.0 freeze.

Features:
- Per-file table (file:line, array, entry, classification) plus summary
  footer with totals, per-kind breakdown, % MACRO compliance, and a
  ranked list of the worst-offender files.
- --strict exits nonzero when any non-MACRO entries are present (CI use).
- --quiet suppresses per-file tables and prints only the summary.
- --paths PATH [PATH ...] limits the scan to specific files / directories.
- Sentinel terminator entries ({NULL}, {0}) are filtered out.
- Concatenated string literals and parenthesized expressions are handled.

Dependencies live in a project-local .venv-audit (added to .gitignore) so
no system Python pollution. tools/generate_all_docs.sh now invokes the
auditor at the end (informational, no --strict) so doc regeneration
surfaces compliance drift alongside the generated docs themselves.

Initial baseline on the current tree: 755 entries scanned, 340 MACRO
compliant (45.0%). Top offenders: UIEntity.cpp (51), 3d/PyVoxelGrid.cpp
(36), 3d/Viewport3D.cpp (31), UIGridPyProperties.cpp (30),
3d/Entity3D.cpp (28).

Refs pre-1.0 documentation freeze.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:30:25 -04:00
7ce933098d ROADMAP: post-7DRL refresh -- fuzz sweep cleared, Phase 4.5/5.2/5.3 shipped
Reflects the actual state of master after the April 2026 push:
- Active tier1 queue is empty (#309/#310/#311 all closed in mid-April).
- Recently shipped section captures #294 (Entity.perspective_map as
  3-state DiscreteMap), #315 (pathfinding heuristics + multi-root + FLEE
  primitives + interactive demo), Phase 5.2 benchmarks, Phase 5.3 docs.
- Active follow-ups now reads #312 (fuzz extension), #313 (UIEntity::grid
  -> GridData), #314 (API audit follow-through), #316 (sparse perspective
  writeback).
- Open-issue count corrected 30 -> 25; outdated groupings (#233-#237
  multi-tile, #238-#240 WASM trio, #218 anim targets) replaced.
- Version bump 0.2.6 -> 0.2.7-prerelease.
2026-04-18 11:24:31 -04:00
3030ac488b Add interactive pathfinding demo for #315; closes #315
tests/demo/screens/pathfinding_demo.py runs three panels side-by-side:

  Panel 1 - A* with selectable heuristic. Keys 1-5 cycle EUCLIDEAN, MANHATTAN,
            CHEBYSHEV, DIAGONAL, ZERO. Q/W bump the weight by 0.25 to show
            weighted A* behaviour.
  Panel 2 - Dijkstra flood from a cursor-controlled root. Arrow keys move the
            cursor; the distance field re-renders as a blue gradient.
  Panel 3 - Multi-root FLEE: three guard entities flee from a shared set of
            threats using an inverted multi-root DijkstraMap, animated one
            step per timer tick. T adds a new threat; R resets.

Exercises the new surface: mcrfpy.Heuristic, Grid.find_path(heuristic=,
weight=), Grid.get_dijkstra_map(roots=...), DijkstraMap.invert(), and
DijkstraMap.descent_step().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 09:19:17 -04:00
17f2d6e1ef Refactor EntityBehavior SEEK/FLEE to use PathProvider strategy; refs #315
EntityBehavior no longer holds a direct DijkstraMap reference. A new
PathProvider interface has three concrete implementations:

- DijkstraProvider: steps along a (possibly inverted) DijkstraMap. SEEK
  descends a normal map toward roots; FLEE descends an inverted map away
  from threats.
- AStarProvider: follows a pre-computed AStarPath step-by-step.
- TargetProvider: takes a single (x, y) target and picks the Chebyshev
  neighbor closest to it each turn.

Entity.set_behavior() gains a pathfinder= kwarg accepting any of the above
(DijkstraMap, AStarPath, or (x, y) tuple). The old executeSeek/executeFlee
helpers collapse into a single executeProviderStep() that delegates to the
provider.

EntityBehavior.h forward-declares PathProvider so the header stays light.
EntityBehavior::reset() moves out of line to avoid pulling PathProvider
into the header.

New tests: tests/regression/issue_315_path_provider_test.py covers all three
providers driving SEEK, FLEE via inverted DijkstraMap, mid-run pathfinder
swap, and invalid-argument handling. grid_step_bench baseline refreshed
against the new provider dispatch path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 09:19:05 -04:00
767d0d4b0f Extend pathfinding API with heuristics, multi-root Dijkstra, and FLEE primitives; refs #315
Phase A (Python surface):
- New mcrfpy.Heuristic IntEnum: EUCLIDEAN, MANHATTAN, CHEBYSHEV, DIAGONAL, ZERO
- Grid.find_path() accepts heuristic= and weight= kwargs (weighted A*)
- Grid.get_dijkstra_map() accepts roots= (list of positions or DiscreteMap mask)

Phase B (FLEE primitives):
- DijkstraMap.invert() returns a new map with inverted distance field
- DijkstraMap.descent_step(pos) returns steepest-descent neighbor or None

DijkstraMap internally switched from the C++ TCODDijkstra wrapper to the C API
(TCOD_dijkstra_*) because multi-root compute and invert/get_descent are not
exposed on the wrapper. Single-root Dijkstra cache is preserved for backward
compatibility; multi-root and mask paths bypass the cache since cache keys
would be ill-defined.

New tests: heuristic_enum_test, find_path_heuristic_test, multi_root_dijkstra_test,
dijkstra_flee_test. Baseline JSONs for dijkstra_bench and gridview_render_bench
refreshed against the new implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 09:18:49 -04:00
2086d25581 Phase 5.3: documentation regeneration + introspection-based stub generator
Regenerate HTML/Markdown API reference, man page, and type stubs against
current committed HEAD (post API-freeze pass #304-#308, #252 overhaul, and
Phase 3/4/5.1/5.2 changes).

The previous tools/generate_stubs_v2.py hand-maintained hardcoded strings,
which drifted badly: stubs still contained removed module functions
(setScale, findAll, getMetrics, setDevConsole), lacked new types (GridView,
Behavior, Trigger, DiscreteMap, Viewport3D, Entity3D, Model3D, Billboard,
NoiseSource, WangSet, LdtkProject, HeightMap, DijkstraMap, AStarPath,
ColorLayer, TileLayer, etc.), and missed post-overhaul properties
(tile_width/tile_height, sprite_grid, perspective_map, cell_pos, labels,
turn_order, move_speed, etc.).

Rewrite the generator as a runtime-introspection script mirroring
generate_dynamic_docs.py's approach:
  - classify mcrfpy members (classes, enums, functions, constants, submodules)
  - parse signatures from docstring first line with proper paren-depth tracking
  - translate multi-form signatures (foo(x,y) or foo(pos)) to *args/**kwargs
  - sanitize docstring '...' placeholder params into '**kwargs'
  - emit IntEnum blocks for int-subclass types with uppercase members
  - discover delegated methods via instance probing (Grid/GridView -> _GridData)
  - conservative property type inference (only accept recognized primitives
    and CapitalCase class names in parenthesized hints)

Resulting stubs/mcrfpy.pyi (2069 lines) parses as valid Python.
Markdown/HTML/man-page regeneration is otherwise timestamp-only since the
introspection path was already current -- the stubs were the stale artifact.
2026-04-18 07:33:51 -04:00
59e722166a Phase 5.2: performance benchmark suite for grid/entity/FOV/pathfinding
Adds 6 benchmark scripts in tests/benchmarks/ covering all 5 scenarios from
Kanboard #37, plus a shared baseline helper:

  grid_step_bench.py        100 ent / 100x100 grid / 1000 grid.step() rounds
                            mix of IDLE/NOISE8/SEEK/FLEE behaviors
  fov_opt_bench.py          100 ent / 1000x1000 grid; entity.update_visibility()
                            (with DiscreteMap perspective writeback) vs bare
                            grid.compute_fov() (no writeback) across FOV
                            algorithms BASIC/SHADOW/SYMMETRIC_SHADOWCAST and
                            radii 8/16/32
  spatial_hash_bench.py     entities_in_radius() at radii (1,5,10,50) x
                            entity counts (100,1k,10k); compares against
                            naive O(n) baseline with hit-count validation
  pathfinding_bench.py      A* across grid sizes/densities/heuristics/weights,
                            plus with-vs-without `collide=` collision-label
                            comparison (0/10/100 blockers on 100x100)
  gridview_render_bench.py  1/2/4 GridViews on shared grid; uses
                            automation.screenshot() to force real renders in
                            headless mode (mcrfpy.step alone is render-stubbed)
  dijkstra_bench.py         single-root, multi-root, mask, invert, descent
  _baseline.py              writes baseline JSON to baseline/phase5_2/

All scripts emit JSON to stdout and write a baseline copy under
tests/benchmarks/baseline/phase5_2/ for regression comparison. All run
headless; pure time.perf_counter() timing for compute benches, screenshot
wall-time for the render bench (start/end_benchmark would only capture the
no-op headless game loop, so direct timing is used).

Notable findings captured in baselines:
- spatial hash: 5x to >300x speedup over naive O(n), hits validated identical
- update_visibility: ~25-37 ms/entity perspective writeback overhead on
  1000x1000 grid (full-grid demote+promote loop in UIEntity::updateVisibility)
  dominates over the actual TCOD FOV cost (~3-24 ms). Worth a follow-up issue
  for sparse perspective updating.
- gridview render: per-view cost scales near-linearly down (~78ms total for
  1, 2, or 4 views) -- the multi-view system shares state efficiently.

Refs Kanboard #37.
2026-04-18 06:45:40 -04:00
98a9497a6c Add Phase 5.1 end-to-end scenario test for Grid entity behaviors
tests/integration/grid_entity_e2e_test.py exercises the full
Grid + GridView + EntityBehavior stack over 100 turns with four
simultaneous entities on a 20x20 walled grid:

  Player  turn_order=0  manual placement, excluded from step()
  Guard   PATROL waypoints + target_label="player" + sight_radius=5
          step() callback switches to SEEK on TARGET trigger
  NPC     NOISE8 random 8-directional wander
  Trap    SLEEP turns=10, DONE callback fires then reverts to IDLE

Player is placed near the (15,15) waypoint so the Guard both
patrols (visits >=1 waypoint) and engages SEEK once in sight.
Verifies trigger count, DONE step index, behavior_type reversion,
and no crashes / Python exceptions over the full 100-turn run.

Kanboard card 36.
2026-04-18 05:44:06 -04:00
f797120d53 Replace UIEntity gridstate with DiscreteMap perspective_map; closes #294
Per-entity FOV memory moves from std::vector<UIGridPointState> (two-bool
visible/discovered pairs) to a 3-state DiscreteMap (0=UNKNOWN, 1=DISCOVERED,
2=VISIBLE), exposed as entity.perspective_map. The invariant
visible-subset-of-discovered becomes structural (single value per cell), and
the map is a live, serializable, first-class object rather than an implicit
internal array.

Changes:
- New DiscreteMap C++ class with shared ownership; PyDiscreteMapObject now
  holds shared_ptr<DiscreteMap>. UIEntity holds the same shared_ptr.
- New mcrfpy.Perspective IntEnum (UNKNOWN/DISCOVERED/VISIBLE), modelled on
  PyInputState.
- entity.perspective_map: lazy-allocated on first access with a grid;
  setter validates size against grid and raises ValueError on mismatch;
  None clears (next access lazy-reallocates fresh).
- updateVisibility() now demotes 2->1 then promotes visible cells to 2.
- entity.at(x, y) returns grid.at(x, y) when VISIBLE, else None.
- Fog-of-war rendering in UIGridView and UIGrid reads the 3-state map.
- Removed: UIEntity::gridstate, ensureGridstate(), entity.gridstate getter,
  UIGridPointState struct + PyUIGridPointStateType.
- Obsolete tests deleted (test_gridpointstate_point,
  issue_265_gridpointstate_dangle); 4 new tests cover lazy allocation,
  identity, serialization round-trip, size validation, and the
  visible-subset-of-discovered invariant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:27 -04:00
417fc43325 Bounds-check DijkstraMap coordinates at the Python boundary
DijkstraMap.distance, .path_from, and .step_from forwarded their
coordinate arguments straight into TCOD's dijkstra routines, which
assert and abort() the entire process on out-of-bounds input. No
recoverable Python exception; the whole interpreter dies.

Validate at the binding layer. Out-of-range coordinates raise
IndexError with a message identifying the map dimensions.

Fuzz corpus crash
tests/fuzz/crashes/pathfinding_behavior-crash-b7ea442fd31774b9b16c8ae99c728f609c8c25d8
now runs cleanly.

closes #311

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:32:19 -04:00
19b43ce5fa Route Grid.compute_fov algorithm through PyFOV::from_arg
The compute_fov binding parsed its `algorithm` argument as a raw int and
cast it directly to TCOD_fov_algorithm_t. Out-of-range values (e.g. -49
reinterpreted as 4294967247) triggered UBSan's "invalid enum load" deep
in GridData::computeFOV.

PyFOV::from_arg already does the right validation for every other
algorithm entry point: accepts the FOV IntEnum, ints in
[0, NB_FOV_ALGORITHMS), or None (default BASIC); raises ValueError
otherwise. Route the binding through it.

Fuzz corpus crash
tests/fuzz/crashes/fov-crash-d5da064d802ae2b5691c520907cd692d04de8bb2
now runs cleanly.

closes #310

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:32:09 -04:00
4fd718472a Clamp Caption numeric setters to prevent UBSan float->uint UB
The `outline` and `font_size` property setters (and the matching kwargs
in __init__) passed a raw float straight into SFML's setOutlineThickness
and setCharacterSize. setCharacterSize takes unsigned int, so any
negative or out-of-range float produced undefined behavior — UBSan's
"value outside the representable range" report.

Clamp before the cast. Negative outline is also nonsensical, so it's
clamped to 0 for consistency (not UB, but wrong).

Fuzz corpus crash
tests/fuzz/crashes/property_types-crash-1f7141d732736d04b99d20261abd766194246ea6
now runs cleanly.

closes #309

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:31:58 -04:00
328b37d02e Merge W9: fuzz_pathfinding_behavior target 2026-04-10 11:20:29 -04:00
e41ed258d2 Merge W8: fuzz_fov target
# Conflicts:
#	tests/fuzz/fuzz_fov.py
2026-04-10 11:20:29 -04:00
9216773b1e Merge W7: fuzz_maps_procgen target for HeightMap/DiscreteMap interfaces 2026-04-10 11:20:00 -04:00
2b78d0bd2c Merge W5: fuzz_property_types target for #267, #268, #272 2026-04-10 11:19:52 -04:00
53712f535b Merge W4: fuzz_grid_entity target for #258-#263, #273, #274 2026-04-10 11:19:34 -04:00
598f22060a Add fuzz_pathfinding_behavior target, addresses #283
Fuzzes grid.get_dijkstra_map with random roots/diagonal_cost/collide,
DijkstraMap.distance/path_from/step_from/to_heightmap queries, and
grid.step() with entity behavior callbacks that mutate the entity
list mid-iteration (adjacent to #273).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:18:01 -04:00
2d207bb876 Add fuzz_maps_procgen target for HeightMap/DiscreteMap interfaces, addresses #283
Fuzzes procgen through its standardized data-container interface: direct
HeightMap and DiscreteMap operations (scalar, binary, bitwise, subscript)
plus one-directional conversions NoiseSource.sample -> HeightMap,
BSP.to_heightmap, DiscreteMap.from_heightmap, and dm.to_heightmap.
Treating HM/DM as the unified surface covers every procgen system
without fuzzing each individually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:17:52 -04:00
56eeab2ff5 Add fuzz_anim_timer_scene target for lifecycle bug family, addresses #283
Targets #269 (PythonObjectCache race), #270 (GridLayer dangling parent),
#275 (UIEntity missing tp_dealloc), #277 (GridChunk dangling parent).
Exercises timer/animation callbacks that mutate scene and drawable
lifetimes across firing boundaries, including scene swap mid-callback
and closure captures that can survive past their target's lifetime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:16:40 -04:00
df4757c448 Add fuzz_property_types target for refcount/type-confusion bugs, addresses #283
Targets #267 (PyObject_GetAttrString reference leaks), #268 (sfVector2f
NULL deref), #272 (UniformCollection weak_ptr). Exercises every exposed
property on Frame/Caption/Sprite/Grid/Entity/TileLayer/ColorLayer/Color/
Vector with both correct-type and deliberately-wrong-type values, plus
hot-loop repeated GetAttrString to stress refcount sites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:15:39 -04:00
bba72cb33b Add fuzz_fov target, addresses #283
Random compute_fov/is_in_fov exercises with varying origin, radius,
light_walls, algorithm params including out-of-bounds origins and
extreme radii. Toggles grid.at(x,y).transparent between computes to
stress stale fov-state bugs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:15:01 -04:00
0d433c1410 Add fuzz_grid_entity target for EntityCollection bug family, addresses #283
Targets #258-#263 (gridstate overflow on entity transfer between
differently-sized grids), #273 (entity.die during iteration), #274
(spatial hash on set_grid). Dispatches 13 operations driven by
ByteStream from fuzz_common.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:13:16 -04:00
90a2945a9f Add native libFuzzer fuzz harness for Python API, addresses #283
Pivots away from atheris (which lacks Python 3.14 support) to a single
libFuzzer-linked executable that embeds CPython, registers mcrfpy, and
dispatches each iteration to a Python fuzz_one_input(data: bytes) function
loaded from tests/fuzz/fuzz_<target>.py by MCRF_FUZZ_TARGET env var.

libFuzzer instruments the C++ engine code where all #258-#278 bugs live;
Python drives the fuzzing logic via an in-house ByteStream replacement
for atheris.FuzzedDataProvider. Python-level exceptions are caught; only
ASan/UBSan signal real bugs.

CMake
- MCRF_FUZZER=ON builds mcrfpy_fuzz from all src/*.cpp except main.cpp
  plus tests/fuzz/fuzz_common.cpp, linked with -fsanitize=fuzzer,address,
  undefined. Asset+lib post-build copy added so the embedded interpreter
  finds its stdlib and default_font/default_texture load.

Makefile
- fuzz-build builds only mcrfpy_fuzz (fast iterate)
- fuzz loops over six targets setting MCRF_FUZZ_TARGET for each
- fuzz-long TARGET=x SECONDS=n for deep manual runs
- fuzz-repro TARGET=x CRASH=path for crash reproduction
- Shared ASAN_OPTIONS / PYTHONHOME env via FUZZ_ENV define

tests/fuzz
- fuzz_common.cpp: LLVMFuzzerInitialize bootstraps Python, imports target,
  resolves fuzz_one_input. LLVMFuzzerTestOneInput wraps bytes as PyBytes,
  calls target, swallows Python errors.
- fuzz_common.py: ByteStream byte consumer + safe_reset() + EXPECTED_EXCEPTIONS
- Six target stubs (grid_entity, property_types, anim_timer_scene,
  maps_procgen, fov, pathfinding_behavior) to be fleshed out in follow-up
- README with build/run/triage instructions

Verified end-to-end: make fuzz-build produces build-fuzz/mcrfpy_fuzz,
make fuzz FUZZ_SECONDS=3 ran all six targets (~2400-9800 exec/s each,
667-1883 coverage edges), make fuzz-repro loaded and replayed a corpus
input cleanly. No crashes from the stubs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:05:04 -04:00
1ce38b587b Merge W1: build plumbing for libFuzzer+ASan fuzz build 2026-04-10 10:51:19 -04:00
136d2a2a25 Add build plumbing for libFuzzer+ASan fuzz build, addresses #283
- CMakeLists MCRF_FUZZER option (clang-only, -fsanitize=fuzzer-no-link)
- Makefile fuzz-build/fuzz/fuzz-long/fuzz-repro/clean-fuzz targets
- CommandLineParser -- passthrough after --exec for forwarding libFuzzer argv
- McRFPy_API: forward script_args to sys.argv in --exec mode so atheris.Setup()
  sees libFuzzer flags; set sys.argv[0] to the exec script path to match Python
  script-mode conventions
- .gitignore build-fuzz/ and corpora/crashes dirs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:35:44 -04:00
6bf5c451a3 Add composite sprite_grid for multi-tile entities, closes #237
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>
2026-04-10 04:15:06 -04:00
f3ef81cf9c Add ThreadSanitizer build targets and free-threaded Python support, closes #281, closes #280
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>
2026-04-10 04:08:40 -04:00
a6a0722be6 Split UIGrid.cpp into three files for maintainability, closes #149
UIGrid.cpp (3338 lines) split into:
- UIGrid.cpp (1588 lines): core logic, rendering, init, click handling, cell callbacks
- UIGridPyMethods.cpp (1104 lines): Python method implementations and method tables
- UIGridPyProperties.cpp (597 lines): Python getter/setter implementations and getsetters table

All 277 tests pass with no regressions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 04:08:27 -04:00
edd0028270 Add WASM developer troubleshooting guide, closes #240
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>
2026-04-10 03:31:43 -04:00
af2eaf7a99 Add Gitea Actions CI workflow for automated testing, closes #285
Workflow runs on push to master and PRs:
- build-and-test: release build + full test suite
- debug-test: debug Python build + tests (catches refcount bugs)
- asan-test: ASan/UBSan build + tests (PRs only)
- valgrind-test: deep memory analysis (PRs only, 30min timeout)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 03:31:31 -04:00
5ce5b5779a Add Emscripten debug build targets with DWARF and source maps, closes #238
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>
2026-04-10 03:31:25 -04:00
a4e0b97ecb Add multi-tile entity support with tile_width/tile_height, closes #236
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>
2026-04-10 02:57:47 -04:00
9a06ae5d8e Add texture display bounds for non-uniform sprite content, closes #235
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>
2026-04-10 02:57:41 -04:00
2b0267430d Add regression tests for vector NULL safety and uniform owner validity, closes #287
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>
2026-04-10 02:57:32 -04:00
7d1066a5d5 Fix demo game fog layer accumulation and web IDBFS mkdir race
- 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>
2026-04-10 02:19:19 -04:00
cc50424372 Add regression tests for GridPointState dangle and refcount leaks, refs #287
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>
2026-04-10 02:06:10 -04:00
e0bcee12a3 Add DiscreteMap to_bytes/from_bytes serialization, closes #293
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>
2026-04-10 02:06:02 -04:00