Commit graph

479 commits

Author SHA1 Message Date
80908f771c UNWRITTEN: complete short-form RPG at games/unwritten/
A 30-45 minute turn-based RPG built overnight as an engine showcase:
six recruitable characters (party of three), four recurring NPCs,
choice-driven dialogue with visible-but-locked skill checks, shops,
loot, levels, two bosses with in-battle Talk options, the reverted
grey-town beat, and an epilogue assembled from the player's actual
choices. 33 dialogue scenes / 171 nodes. Terrain is ColorLayers only;
kenney_tinydungeon supplies characters, items, and props.

Creative direction, story, dialogue, maps, and systems design by
Fable; implementation by four Opus subagents (framework, battle,
overworld, integration). Verified by tests/playthrough.py: a scripted
headless run of all three acts, 20/20 beats passing, plus per-system
unit tests (script integrity, UI kit, 225-battle balance sim,
overworld).

Run: cd build && ./mcrogueface --exec ../games/unwritten/main.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vxWJ6GY384SmZiMVXgrF4
2026-07-03 10:11:12 -04:00
d94a25706a ROADMAP: tier1 memory-model batch (#326-#330) resolved; Gauntlet (#340) shipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:50:56 -04:00
506ad57cc6 Gauntlet #340: menu layout polish; commit first captured baseline; closes #340
Menu rows no longer clip at the right edge (baseline text shortened, info column
moved left). Commit the first real windowed baseline (run of 2026-07-02 on the
dev machine, all six trials scored, full get_metrics snapshots at peak) with a
provenance/variance caveat recorded as DESIGN.md deviation 10 -- scores for
callback-heavy trials vary several-fold on a busy desktop, so recapture on a
quiet system before trusting fine-grained deltas.

The Gauntlet delivers: interactive menu/trial/results scenes with the
instrument-panel HUD (sparkline, budget-colored FPS, load/ramp readout), six
subsystem stress trials with geometric auto-ramp and p95-vs-16.67ms scoring,
JSON baseline diffing with letter grades, and a headless suite test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:48:22 -04:00
1aa6624758 Gauntlet #340: strike-based hard-cap bail (3 over-cap samples per window)
Three full windowed runs showed 10x-100x per-trial score variance: a single
stray desktop frame >100 ms (compositor/GC hiccup) instantly zeroed trials
whose held p95 was 6-9 ms. Require 3 over-cap samples in a hold window before
bailing -- a truly overloaded engine trips that within ~50 ms of wall time,
while lone hiccups now just count toward the window's p95. Documented as
DESIGN.md deviations 8 and 9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:48:22 -04:00
d651a86776 Gauntlet #340: fix first-run baseline self-compare; pathfinder ramp start
First full run exposed two issues: (1) write_run promotes baseline.json before
the results scene loads it, so a first run compared against itself and showed
'= same' instead of FIRST RUN -- detect via the _promoted_baseline flag;
(2) pathfinder_rush started at 5 queries/tick, but queries burst on one 100 ms
tick frame, so the very first hold window failed (p95 38.9 ms) and scored 0 --
start the ramp at 1 query/tick so the sustainable level is actually found.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:48:22 -04:00
23b02b87ea Gauntlet #340: menu + results scenes, baseline IO, run driver
gauntlet_main.py: interactive app (menu with color-cycling title + baselines,
per-trial scenes, manual/-+/auto-ramp controls, results table with vs-baseline
deltas and letter grades). baseline_io.py: latest/baseline JSON read/write/compare
with geomean scoring. run_gauntlet.py: unattended full auto-ramp run that writes
the baseline and screenshots the results scene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:48:22 -04:00
b20903b9ed Gauntlet #340: shared HUD overlay + entity-swarm motion
Instrument-panel HUD: trial banner, 60-bar frame-time sparkline with 16.7 ms
hairline, and FPS / p95 / draw-calls / load readout (budget-colored). Give swarm
entities a nonzero move_speed so they spread visually instead of piling on target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:48:22 -04:00
9f371bccd3 Gauntlet #340: headless trials smoke test
setup / set_load (two levels) / tick / teardown per trial with count assertions;
suite-friendly, no rendering. Passes headless.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:48:22 -04:00
859730f02d Gauntlet #340: spec, package skeleton, scoring, six trials
Add the DESIGN.md (spec copy + implementation deviations), the trials package
(Trial base + ordered registry + six stress trials: entity swarm, animation
storm, grid titan, pathfinder rush, ui avalanche, sightline siege), and the
RampController/scoring logic. All pure Python under tests/benchmarks/gauntlet/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:48:22 -04:00
c6b21b9a6b Regenerate API docs; add threading note to reference intro
Add a one-line threading-contract note to the API reference intro (HTML +
Markdown generators) and regenerate all documentation artifacts (HTML,
Markdown, man page, type stubs) to pick up the updated mcrfpy.lock() docstring.

Refs #327

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:23:28 -04:00
115b394503 Freeze value semantics + record 1.0 compatibility policy
Add docs/api-stability.md recording the three frozen 1.0 decisions: Color/Vector
value semantics (#326), the edit() context-manager bulk-edit convention (#328),
and subinterpreter exclusion (#330), plus a pointer to the api-surface snapshot
test as the enforcement mechanism.

Strengthen the PyColor and PyVector class docstrings to state value semantics as
a frozen contract with the supported idioms (read-modify-writeback and
whole-value assignment). Add the subinterpreter-unsupported note and policy
links to README.

closes #326, closes #328, closes #330

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:23:11 -04:00
b32c5597fb Document threading contract; mcrfpy.lock() is the off-main-thread rule
Add docs/threading-model.md stating the normative rule (any access to mcrfpy
objects from a non-main thread must happen inside `with mcrfpy.lock():`;
behavior outside the lock is undefined), the FrameLock/safe-window design, and
a worked worker-thread example. Strengthen the mcrfpy.lock() docstring to state
the contract with an MCRF_LINK to the new page.

closes #327

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:23:05 -04:00
19d80b4502 Back EntityCollection with std::vector for O(1) indexing; closes #329
grid.entities wrapped std::list and did std::advance() from begin() on every
indexed access, so grid.entities[i] was O(n) and a full indexed scan O(n^2).
Switch the backing container (GridData::entities and the EntityCollection /
iterator wrappers) to std::vector.

Changes:
- GridData::entities is now std::vector<shared_ptr<UIEntity>>.
- UIEntityCollection getitem uses (*vec)[index] -- O(1) random access.
- The Python iterator is now index-based (Py_ssize_t cursor) instead of
  storing a container iterator. This removes any possibility of a dangling
  iterator when the collection is mutated mid-iteration; the existing
  start_size guard still raises RuntimeError on a size change, and the cursor
  is bounds-checked against the live size so access stays memory-safe.

Iterator/mutation semantics (unchanged, now pinned by tests): iterating
grid.entities and changing its size mid-iteration raises RuntimeError.
grid.entities[i] indexing, negative indexing, IndexError on out-of-range,
and slicing behavior are all preserved.

Audit of call sites: all other consumers (UIEntity add/remove/set_grid,
UIGrid render, ImGui explorer, slice/pop/insert/setitem) use
begin/end/erase/push_back/find_if/rbegin, which are container-agnostic and
remain O(1) on vector iterators. grid.step() already snapshots entities into
a local vector before executing behaviors, so entity death (self-removal)
mid-step cannot invalidate the loop; a regression test now pins this.
UIEntity::releasePyIdentity() is still invoked at every grid-exit path.

The separate 3D EntityCollection3D (Entity3D) is unrelated and untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:16:04 -04:00
dabca794ed Add tests pinning EntityCollection semantics and O(1) indexing (#329)
Pre-change TDD tests for the std::list -> std::vector container swap:

- tests/unit/entity_collection_mutation_test.py pins observable behavior:
  positive/negative indexing, out-of-range IndexError, slicing (returns a
  fresh list; slice wrappers are not cache-identity), in-order iteration,
  RuntimeError on size change during iteration, and grid.step() immunity to
  mid-step entity self-removal.
- tests/regression/issue_329_entity_index_perf_test.py guards against a
  regression to O(n) indexed access / O(n^2) full-scan.

Both pass against the pre-swap std::list implementation, locking in the
public contract before the internal container change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:13:18 -04:00
35c630888c Remove tools/gitea_issues.py
One-off issue-fetch script with a hardcoded (now-invalidated) token;
not part of the project. Issue queries go through the forgejo MCP
server now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:58:26 -04:00
bd6e3723f7 ROADMAP: Forgejo URLs; log memory-model review issues #326-#338
Refresh open-issue count 21 -> 34 after the pre-1.0 memory-model
review filed #326-#338 (2026-07-02). Surface the five tier1 freeze
decisions (#326-#330) under Active Follow-Ups so they're visible as
blockers to locking the 1.0 API contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:49:29 -04:00
76f1060169 Migrate docs and links to Forgejo instance (dev.ffwf.net/forgejo)
The issue tracker moved from gamedev.ffwf.net/gitea to
dev.ffwf.net/forgejo. Update all URLs in CLAUDE.md, README.md,
web/index.html, and tools/gitea_issues.py. CLAUDE.md also documents
the new resource-oriented forgejo MCP server (list/get/create/edit/
delete/link/unlink_gitea) and retires the stale v0.07 label-bug
workaround -- label operations re-validated working 2026-07-02.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:49:22 -04:00
f625d07870 ROADMAP: log 0.2.8 release; clarify fuzz-batch bugs are now fixed
Version header and Recently Shipped section hadn't been updated since
cf844f4 (version bump + Linux package libtcod SONAME fix), which was
already pushed to origin/master. Also notes the 0.2.8 git tag is still
local-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvvE6TB2cScBVbeap2btHw
2026-06-30 23:28:05 -04:00
cf844f47e5 Set version 0.2.8 for release; fix Linux package libtcod SONAME
Release the commits since 0.2.7-prerelease-7drl2026 as 0.2.8, dropping
the in-development -7DRL-2026 suffix.

Shipped libtcod (Linux .so / Windows .dll / Emscripten .a) rebuilt from
submodule 79abc66 so release binaries carry the #321 FOV overflow fix.
The Linux .so is built SDL3=OFF with vendored utf8proc so it stays
self-contained (only libz + system libs), matching prior releases.

Fix tools/package.sh: ship libtcod under its DT_NEEDED SONAME
(libtcod.so.2) plus a libtcod.so compat symlink, mirroring the SFML
packaging. It previously shipped only the bare libtcod.so, so the Linux
package could not resolve libtcod on a clean machine -- an absolute dev
RUNPATH masked this, and it also affected 0.2.7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvvE6TB2cScBVbeap2btHw
2026-06-30 19:41:58 -04:00
e5f858d1aa ROADMAP: mark #321-#325 merged to master; refresh open-issue count to 21
The fuzz-surfaced bug batch is now on master, not pending. Drop the
stale "pending merge" bullet and decrement the tracker count (26 -> 21,
verified against Gitea). Keep the libtcod.so rebuild release note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvvE6TB2cScBVbeap2btHw
2026-06-30 18:17:17 -04:00
5ff1198a7d ROADMAP: mark #322-#325 fixed (fuzz safety batch verified under UBSan/ASan)
The #312 fuzz-surfaced bug batch (#321-#325) is now fully fixed on-branch.
Moves #322-#325 to Recently Shipped with the A/B verification evidence,
clears the Active Follow-Ups, and notes the batch is pending merge to master.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvvE6TB2cScBVbeap2btHw
2026-06-26 23:53:09 -04:00
9298aea072 Reject non-finite floats before int casts; closes #323, closes #324, closes #325
Three Python bindings cast user-supplied doubles to narrower integer types
without checking for NaN/inf, which is undefined behavior (UBSan aborts):

  #323 SoundBuffer.pitch_shift: factor only rejected <= 0.0, but every NaN
       comparison is false, so a NaN factor reached
       static_cast<size_t>(frames / factor) (AudioEffects.cpp:27,
       nan->unsigned long). Binding now rejects non-finite factors with
       ValueError; pitchShift() guard hardened as defense-in-depth.

  #324 Texture.hsl_shift: NaN/inf hue/sat/lit shifts propagated to NaN r/g/b
       and the (sf::Uint8)(c * 255.0f) casts (PyTexture.cpp:458,
       nan->unsigned char). Binding now rejects non-finite shifts with
       ValueError before any pixel math.

  #325 Vector.int: (long)std::floor(component) on inf/NaN, or a finite value
       outside [LONG_MIN, LONG_MAX] (PyVector.cpp:610, inf->long). Property
       now range-checks both components and raises OverflowError.

Verified by A/B replay of the #312 audio_dsp / texture_factory /
property_types crash corpus under clang-18 UBSan/ASan: each input aborts
pre-fix at the exact cited line and runs clean post-fix. Three new
regression tests cover NaN/inf/out-of-range inputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvvE6TB2cScBVbeap2btHw
2026-06-26 23:53:02 -04:00
a2c76c3643 Check Python C-API returns in terrain_enum builders; closes #322
WangSet.terrain_enum (and the parallel AutoRuleSet.terrain_enum) built a
Python IntEnum from parsed terrain/color names but ignored the return codes
of PyLong_FromLong, PyUnicode_FromString, and PyDict_SetItemString. A name
carrying invalid UTF-8 -- trivially reachable from an untrusted .tsx/.ldtk
import -- makes those calls fail and leave an exception pending. The code
then called PyObject_Call with that error already set, tripping CPython's
_PyErr_Occurred assertion and aborting (libFuzzer: deadly signal).

Both functions now check every C-API return, clean up their references, and
return NULL so the real exception (e.g. UnicodeDecodeError) propagates.

Verified by A/B replay of the #312 fuzz_import_parsers crash corpus under
clang-18 ASan/UBSan: the saved crash input aborts pre-fix inside
PyWangSet::terrain_enum -> PyObject_Call and runs clean post-fix. New
regression test crafts a .tsx with invalid-UTF-8 wangset/wangcolor names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvvE6TB2cScBVbeap2btHw
2026-06-26 23:52:38 -04:00
79df32317b Bump libtcod-headless to 79abc66 (FOV overflow fixes); closes #321
#321 was an ASan heap-buffer-overflow in view_array_insert
(fov_permissive2.c:97) reached via ColorLayer.draw_fov ->
GridData::computeFOV -> TCOD_map_compute_fov_permissive2, surfaced by the
#312 fuzz_fov campaign. The "bad-free in ~GridData" in the issue title is
the downstream symptom of this active_views buffer overflow.

The bump pulls in four upstream FOV commits:
  - off-by-one in view_array_insert overflowing active_views  <- root cause
  - undersized bumps buffer overflow on tiny permissive maps
  - MRPAS obstacle buffer overflow / malloc(0) on small maps
  - regression tests for FOV

Verified by A/B replay of the fuzz_fov crash corpus under clang-18 ASan,
same build config (clang-18, -DLIBTCOD_SDL3=OFF, identical sanitizer
flags) with only the submodule commit differing:
  pre-fix  (8835239): fov-crash-5f34f68e and fov-crash-a451db54 abort with
                      heap-buffer-overflow in view_array_insert inside
                      GridData::computeFOV
  post-fix (79abc66): all crash inputs clean; 45s / 21,952-run fov fuzz
                      smoke clean, no new crashes

Note: __lib/libtcod.so (gitignored, the shipped desktop lib) must be
rebuilt from this submodule commit for release binaries to carry the fix.

Also updates ROADMAP: #321 moved to Recently Shipped; open-issue count
refreshed to the post-#312 tracker state.

closes #321

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvvE6TB2cScBVbeap2btHw
2026-06-26 20:57:38 -04:00
515a53ac17 Polish #315 pathfinding demo: panel backdrops, base layer, fixed layout
Rework pathfinding_demo.py into a clearer visual explainer. Each of the
three panels now has a backdrop frame, a floor/wall base ColorLayer so it
reads as a full grid, a title above, and a short live readout below. The
multi-root FLEE panel gains a faint safety-gradient backdrop and tighter
guard-stepping. Layout is pinned to the fixed 1024x768 headless render
size. Loads clean under --headless --exec.

Addresses #315

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvvE6TB2cScBVbeap2btHw
2026-06-26 20:57:23 -04:00
246ed886db Fold Tier C surface into existing fuzz targets; closes #312
Extends the five existing targets to cover the remaining gaps from #312
without new files:

- property_types     Line/Circle/Arc setters, Scene.children collection ops
                     (index/count/find/insert/slice/pop), module functions
                     find/find_all/bresenham/lock. Benchmark triplet excluded
                     (end_benchmark writes a file per call).
- grid_entity        grid.at / [x,y] / entities_in_radius / center_camera /
                     hovered_cell, and GridPoint named-layer __getattr__/
                     __setattr__.
- pathfinding_behavior  Grid.find_path + full AStarPath (peek/__len__/__bool__/
                     iteration) that path_from didn't reach.
- fov                ColorLayer perspective (apply/update/clear_perspective)
                     and draw_fov.
- maps_procgen       ColorLayer/TileLayer apply_threshold/apply_ranges/
                     apply_gradient from HeightMap sources.

The full instrumented campaign surfaced five new bugs, filed as #321 (HIGH
ColorLayer.draw_fov bad-free), #322 (WangSet.terrain_enum error-pending
abort), #323/#324/#325 (float->int UB in pitch_shift/hsl_shift/Vector). Per
decision, this issue delivers fuzz coverage only; the bugs are tracked
separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 16:45:03 -04:00
925699ef0b Add 4 libFuzzer targets for Tier A/B API surface; addresses #312
New targets under tests/fuzz/, wired into Makefile FUZZ_TARGETS, each with a
seed corpus (parser seeds are real fixtures prefixed with a loader selector
byte):

- fuzz_audio_dsp        SoundBuffer factories + 14 DSP effects + concat/mix.
                        Self-contained (CPU sample math, no device).
- fuzz_import_parsers   TileSetFile/TileMapFile/LdtkProject. Loaders take a
                        path, so each iteration writes mutated bytes to a temp
                        file; OSError (IOError) is swallowed as an expected
                        parse-failure outcome.
- fuzz_texture_factory  Texture.from_bytes/composite/hsl_shift byte ingestion.
                        Multiplication-overflow path documented as out of scope
                        (would OOM, not crash cleanly).
- fuzz_shader_bindings  uniforms[] + PropertyBinding/CallableBinding lifetime,
                        target Drawable destroyed mid-flight (#270/#271/#277
                        pattern). Degrades to pure binding-lifetime fuzzing if
                        shaders are unavailable.

All four signature-validated against the live mcrfpy API before running.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 16:44:47 -04:00
ea9251c4e8 Fix build_debug_libs.sh flag quoting so --asan/--tsan can rebuild
The sanitizer build paths passed multi-word flags as an unquoted string, so
the cmake invocation word-split "-fsanitize=address,undefined
-fno-omit-frame-pointer" and handed -fno-omit-frame-pointer to cmake as a
bare unknown argument ("CMake Error: Unknown argument"). Switch to a bash
array so each -DCMAKE_*_FLAGS value stays a single argument. Unblocks
rebuilding the instrumented __lib_debug libraries the fuzz build links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 16:44:33 -04:00
ab6aecd0b0 Add read-only Caption.font getter; addresses #320
The Caption class docstring listed `font` under its Attributes, but no getter
existed -- the PyUICaptionObject.font slot was GC-managed yet never populated by
init(). Wire it up:
  - init() now stores the supplied Font (incref) or, when none/None was given, a
    wrapper around the engine default font, so the getter reflects what is
    actually rendered rather than returning None.
  - New read-only `font` getset (consistent with Sprite.texture being read-only).

Regenerated API docs/stubs/man page and rebaselined the api-surface snapshot
(one added line: Caption `prop font: Font (ro)`). Frozen docstring gate 100%,
suite 297/297.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 12:18:53 -04:00
5eecb2b2b0 Rewrite stale Animation-ctor unit tests to drawable.animate()
test_animation_raii and test_animation_property_locking called the
mcrfpy.Animation(...) constructor, which was removed from the module export
during the API freeze. The Animation type still exists (returned by
drawable.animate()) and every behavior these tests check is intact:
  - hasValidTarget()/complete()/stop() on the returned handle (weak-target RAII)
  - conflict_mode 'replace'/'queue'/'error' + invalid-mode ValueError (#120)

Ported both to drawable.animate(prop, target, seconds, easing, conflict_mode=).
This file is the suite's only conflict_mode coverage, so it was rewritten rather
than deleted. Durations converted ms -> s. Suite now 297/297.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 12:11:37 -04:00
415ee02438 Fix Caption constructor positional signature to match docstring; closes #320
The frozen docstring advertised Caption(pos, font, text, ...) -- font as the
2nd positional, text the 3rd -- matching the Sprite/Entity convention. But
UICaption::init laid its two positional slots out as (pos, text) with font
keyword-only (format "|Oz$...", kwlist {"pos","text",...}), so:
  - Caption((x,y), None, "text") raised "at most 2 positional arguments"
  - Caption((x,y), "string")     bound the string to text, not font

Reorder the positional-or-keyword slots to (pos, font, text) so the impl
matches its public, #314-locked docstring and its sibling constructors.

Behavior change (audited safe): Caption((x,y), "string") now binds the string
to font (-> TypeError, font must be a Font). Zero live callers use the old
(pos, text) 2-positional form; the legacy (text, x, y) callers in
src/scripts/text_input_widget*.py already fail under the current impl.

Adds tests/regression/issue_320_caption_positional_font_test.py (11 checks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 12:11:27 -04:00
2ada7178ad ROADMAP: mark #317/#318/#319 shipped; open count now 22
#317/#318/#319 (the #314 verify-pass code bugs) are fixed and merged; moved from
Active Follow-Ups to Recently Shipped. #312 remains the sole active follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
2026-06-21 10:14:11 -04:00
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