Commit graph

447 commits

Author SHA1 Message Date
98489a96fd Fix verify-pass code bugs #317/#318/#319
Three small bugs surfaced by the #314 docstring-accuracy verify pass:

#317 automation.scroll() dropped the x of its position argument: scroll()
resolved (x, y) but called injectMouseEvent(MouseWheelScrolled, clicks, y),
passing the scroll amount as x. injectMouseEvent now takes the scroll delta as
its own parameter and scroll() forwards the real x/y.

#318 GridView.texture always returned None (a TODO stub). It now returns a
Texture wrapper sharing the underlying shared_ptr<PyTexture>, mirroring
Grid.texture. (mcrfpy.Grid and mcrfpy.GridView are the same type post-#252, so
this fixes both names.)

#319 Entity.visible_entities(radius=None) raised TypeError: radius was parsed
with the 'i' format code, which rejects None. It now parses radius as an object
and treats None / omitted / -1 as "use the grid's default fov_radius"; a
non-int, non-None radius raises a clear TypeError.

- regression tests for each under tests/regression/
- api_surface snapshot re-baselined (visible_entities signature; texture
  property now Texture | None) and docs/stubs regenerated; frozen docstring
  gate still 100%

closes #317
closes #318
closes #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 10:12:41 -04:00
265425321c Windowed perspective writeback in UIEntity::updateVisibility; closes #316
Clip the demote+promote passes in updateVisibility() to an AABB sized to
fov_radius instead of two full-W*H walks per entity. A prev_fov window cache
demotes last tick's promoted rect (not the current one) so a moving entity
leaves no trailing "ghost vision". On a 1000x1000 grid the Phase 5.2 benchmark's
flat ~25-36 ms/entity writeback overhead collapses to single-digit microseconds
(384x-6577x speedup on the cheap algorithms; below timing noise on the rest).

Adversarial verify caught a regression the happy-path test missed: the
documented from_bytes -> assign -> update_visibility() load/resume path left
permanent ghost-VISIBLE cells, because prev_fov only bounds engine-promoted
cells. Fixed with a one-shot perspective_full_demote_pending flag (full demote
only on the tick after an external assignment; per-turn hot path stays
windowed). Documented the engine-owned demote contract on the perspective_map
property.

- src/UIEntity.cpp/.h: windowed demote/promote, prev_fov cache, demote flag
- src/DiscreteMap.cpp/.h: demoteVisibleRect(x0, y0, x1, y1)
- tests/regression/issue_316_sparse_perspective_test.py: 7-section regression
  (equivalence matrix, radius-0, moving disjoint, trailing-edge, grid resize,
  load/resume assignment, AABB margin lock)
- docs regenerated for the perspective_map docstring change

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 09:40:05 -04:00
3f39ee0320 ROADMAP: mark #314 complete (F15 + strict gate); note #317-319
API audit follow-through done: F15 macro conversion (289 slots, 20 frozen
files, 100% compliant), accuracy-corrected, and locked by a strict
frozen-docstring gate in the doc pipeline. Verify-pass code bugs tracked as
#317/#318/#319.

closes #314

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:47:21 -04:00
1362d5155d Wire strict frozen-docstring gate into the doc pipeline (#314 F15)
generate_all_docs.sh now runs check_frozen_docstrings.sh as a hard gate after
doc generation: the frozen (non-3D) binding surface must stay 100% MCRF_*
macro compliant, or the doc build fails. This is the docstring analog of the
API-surface snapshot test -- it prevents future raw docstrings from silently
regressing the frozen 1.0 documentation contract. Skipped gracefully when
.venv-audit is absent; experimental src/3d/ bindings remain exempt.

Refs #314

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:45:11 -04:00
eafe65683f F15: correct docstring accuracy from adversarial verify pass (#314)
Follow-up to the macro conversion: an adversarial verify pass (one agent per
file vs. the C++ implementation + stubs) found 62 content issues; the real
ones are fixed here.

Callbacks (centralized):
- on_click: receives (pos, MouseButton, InputState), not str (8 files).
- on_enter/on_exit/on_move (UIBase.h): hover passes only (pos) -- removed the
  fictional button/action args.
- bounds/global_bounds (UIBase.h): mark (tuple, read-only).

Signatures / types:
- Grid.find_path: document heuristic + weight; get_dijkstra_map: document roots;
  compute_fov: FOV | int = FOV.BASIC (not the C constant FOV_BASIC) + Returns;
  at/is_in_fov: document (pos) and (x, y) call forms.
- get_metrics: document all 16 returned dict keys (was 8); bresenham: drop the
  bogus '*' keyword-only separator.
- Nullable defaults typed correctly: BSP seed/size, ColorLayer draw_fov/
  apply_perspective Color|None, Entity.visible_entities radius int=-1 (None is
  rejected by the 'i' parser -> see #319).
- Type-token fixes: GridView.center -> Vector; GridView.texture -> (None,
  read-only) (unimplemented, #318); GridPoint.grid_pos -> (tuple, read-only);
  EntityCollection.find -> Entity | list[Entity] | None; extend RuntimeError;
  UniformCollection.values -> list[float | tuple | None].
- automation: onScreen (x, y) form documented; scroll notes x is ignored (#317).

Also: correct stale AStarPath/DijkstraMap signatures in docs/api-audit-2026-04.md
(the bindings were right, the audit table was outdated). Rebaseline the API
snapshot golden and regenerate docs/stubs.

Code-level bugs surfaced by the pass are filed as #317, #318, #319.

Refs #314, #317, #318, #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:43:47 -04:00
5725a4f035 F15: convert frozen binding docstrings to MCRF_* macros (#314)
Convert 289 raw PyMethodDef/PyGetSetDef docstring slots to MCRF_METHOD /
MCRF_PROPERTY across the 20 frozen (non-3D) binding files, bringing the
frozen surface to 100% macro compliance (check_frozen_docstrings.sh PASS).
Done via a one-agent-per-file workflow gated by validate_file_docstrings.sh
and per-wave build/doc-rebuild checks.

- Adds #include "McRFPy_Doc.h" where missing; fills the lone genuine doc
  gap (UIGrid.at, which was MISSING a doc field in two arrays).
- McRFPy_Doc.h: comment documenting the MCRF_METHOD_DOC comma rule (the
  trap that broke the GridLayers conversion mid-run).
- Rebaseline api_surface golden: property types now resolve to real types
  instead of "Any" (e.g. grid_pos: Vector, on_cell_click: Callable | None),
  and 11 properties correctly flip rw->ro now that their docstrings carry
  "read-only" (collections, grid_size, hovered_cell, texture, view — all
  verified against NULL setter slots).
- Regenerate docs/stubs/man page from the new docstrings.

Module-level functions use MCRF_METHOD(<name>, ...) (expands identically to
the intended MCRF_FUNCTION; the audit's compliance set is METHOD/PROPERTY).
Experimental 3D/Voxel bindings (src/3d/) remain exempt from the freeze.

Pre-existing failures unrelated to this change: test_animation_*,
test_constructor_comprehensive (reference the removed mcrfpy.Animation and
old constructor arity).

Refs #314

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:20:55 -04:00
39c2340b19 Add docstring-macro validation scripts for #314 F15
- validate_file_docstrings.sh: per-file MCRF_* compliance check (PASS/FAIL),
  the completion signal for one-agent-per-file docstring conversion.
- check_frozen_docstrings.sh: strict gate over the frozen (non-3D) binding
  surface; locks F15 against regression (mirrors the API-surface snapshot test).

Both wrap tools/audit_pymethoddef.py (tree-sitter, .venv-audit).

Refs #314

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:11:15 -04:00
20b87a5993 ROADMAP: mark #313 shipped, #314 doc follow-through in progress
#313 merged to master (UIEntity::grid -> GridData, entity.texture).
#314 API-surface snapshot test landed; remaining doc loop (F15 macro
conversion + regen/verify against the 93-item catalog) now in progress.
Open-issue count 25 -> 24.

Refs #314

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:42:24 -04:00
74131e78e4 Update ROADMAP: bump to 0.2.8-7DRL-2026; record #313/#314 branch progress
- Version bumped to 0.2.8-7DRL-2026
- Note API-surface snapshot regression test locking freeze decisions (#314)
- Move #313 (UIEntity::grid -> shared_ptr<GridData>, new entity.texture) and
  #314 to an "On Deck (pending merge)" section; trim from Active Follow-Ups

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:30:28 -04:00
2d2c333cd7 Refactor UIEntity::grid to shared_ptr<GridData>; add entity.texture; closes #313
UIEntity now depends on the grid DATA layer only:
- GridData gains cell_width_px/cell_height_px (mirrored from the grid
  texture in UIGrid's 5-arg ctor; texture is write-once) so entity
  tile<->pixel math no longer reaches into rendering (getTexture()).
- GridData gains markDirty()/markCompositeDirty(): set the flag on the
  UIGrid subobject AND notify owning_view, covering both render paths.
  UIGrid disambiguates via 'using UIDrawable::markDirty' so all
  pre-existing UIGrid-receiver calls resolve exactly as before.
- The three Python wrappers that still need the full UIGrid (GridPoint
  from entity.at(), the _GridData fallback in get_grid, find_path's temp
  wrapper) reconstruct it via a single aliasing-downcast helper
  (grid_as_uigrid) that documents the never-independently-allocated
  GridData invariant; init/set_grid simplify (share grid_data directly).
  Removing the casts is deferred to #252.

entity.texture (new, frozen surface +1): thin get/set over the entity's
own UISprite. Entities render with their OWN texture (default_texture
fallback at construction); the grid's texture only determines cell size.
Setter preserves sprite_index; rejects non-Texture (TypeError),
null-data Texture wrappers (ValueError), and deletion.

Adversarial review fixes folded in:
- set_texture/get_texture guard uninitialized Entity wrappers
  (RuntimeError), isinstance errors, and null-data Textures.
- PyUIGridViewType tp_dealloc no longer unconditionally severs
  GridData::owning_view: gated on last-owner (#251 use_count pattern)
  plus owning-view identity. Previously ANY Grid wrapper GC while the
  view lived (e.g. scene.children.append(mcrfpy.Grid(...))) silently
  broke entity.grid -> Grid identity and data-layer dirty notification.

Tests: tests/regression/issue_313_entity_grid_data_test.py (texture
semantics, grid-cell-size invariance, entity.grid identity, #251 gate
survival, GridPoint outliving teardown, review-fix guards, owning_view
survival) + tests/unit/entity_texture_test.py. API snapshot golden
re-baselined: exactly +1 surface line (Entity.texture) + writability
probe flip. Docs/stubs regenerated. Native + Emscripten builds verified.

Known edges recorded in docs/api-audit-2026-04.md: texture read-back is
a fresh wrapper each get (no Texture __eq__); sprite_index not
re-validated against a new atlas. Multi-view markDirty broadcast and
pure-GridData wrappers remain deferred to #252. Addresses #314.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 00:51:22 -04:00
16adc92995 Add public API-surface snapshot regression test; lock #314 freeze decisions
Phase 1 of the #314/#313 plan (docs/plan-313-314.md): commit a deterministic
full-public-API-surface snapshot as the regression net BEFORE the #313
UIEntity::grid -> GridData refactor, so that refactor reduces to a reviewable diff.

tests/unit/api_surface_snapshot_test.py enumerates the complete public mcrfpy
surface (exported types' methods/properties with docstring-derived type + ro/rw,
12 enums with int values, module functions, singletons, the automation submodule)
and diffs it against tests/snapshots/api_surface.golden.txt. Re-baseline with
MCRF_UPDATE_API_SNAPSHOT=1.

Key design points (verified against source + live introspection):
- mcrfpy.Grid IS mcrfpy.GridView and delegates its real API to an internal
  _GridData via tp_getattro -- invisible to dir(). The test walks grid.grid_data
  AND probes delegation integrity on a live instance (69/69 members resolve).
- Behavioral writability probes compensate for docstring-derived ro/rw inference.
- Every exported class is classified FROZEN vs EXPERIMENTAL; the snapshot captures
  the classification so future additions force a deliberate freeze decision.

Freeze decisions recorded in docs/api-audit-2026-04.md:
- F3: grid_pos is the canonical cell-position name (matches the constructor kwarg);
  cell_pos/cell_x/cell_y documented as aliases. Docstrings aligned in UIEntity.cpp.
- F12: deprecated set_scale KEPT in the 1.0 surface (removal would be a new break).
- automation camelCase EXEMPT from the snake_case rule.
- EXPERIMENTAL (exempt): 3D/Voxel, Tiled/LDtk import, Shader, binding helpers.
- FROZEN: core UI/Grid/Entity/value types, enums, procgen (BSP/HeightMap/
  NoiseSource/DijkstraMap/AStarPath), Drawable (root).

Doc/stub regeneration and the entity.texture addition are deferred to Phase 2
(the #313 refactor). Addresses #314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 07:51:02 -04:00
70434fd1ee Fix outdated CLAUDE.md WASM audio entry; update triage notes
CLAUDE.md had Audio as Stubbed for the SDL2/WASM build, but
SDL2_mixer is fully wired up via -sUSE_SDL_MIXER=2. Update table
to reflect current state (discovered during blog draft review).

Also commits previously-staged triage completion notes in
docs/ISSUE_TRIAGE_2026-04.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:37:48 -04:00
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