Compare commits

...

12 commits

Author SHA1 Message Date
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
22 changed files with 1341 additions and 475 deletions

3
.gitignore vendored
View file

@ -33,6 +33,9 @@ __lib_windows/
build-windows/ build-windows/
build_windows/ build_windows/
_oldscripts/ _oldscripts/
# Audit tooling virtualenv (tools/audit_pymethoddef.py)
.venv-audit/
assets/ assets/
cellular_automata_fire/ cellular_automata_fire/
deps/ deps/

View file

@ -1,6 +1,6 @@
# McRogueFace - Development Roadmap # McRogueFace - Development Roadmap
**Version**: 0.2.6-prerelease | **Era**: McRogueFace (2D roguelikes) **Version**: 0.2.7-prerelease | **Era**: McRogueFace (2D roguelikes) -- on the road to 1.0
For detailed architecture, philosophy, and decision framework, see the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki page. For per-issue tracking, see the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap). For detailed architecture, philosophy, and decision framework, see the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki page. For per-issue tracking, see the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap).
@ -10,7 +10,7 @@ For detailed architecture, philosophy, and decision framework, see the [Strategi
**Alpha 0.1** (2024) -- First complete release. Milestone: all datatypes behaving. **Alpha 0.1** (2024) -- First complete release. Milestone: all datatypes behaving.
**0.2 series** (Jan-Feb 2026) -- Weekly updates to GitHub. Key additions: **0.2 series** (Jan-Mar 2026) -- Weekly updates to GitHub. Key additions:
- 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization - 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization
- Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap - Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap
- Tiled and LDtk import with Wang tile / AutoRule resolution - Tiled and LDtk import with Wang tile / AutoRule resolution
@ -19,37 +19,52 @@ For detailed architecture, philosophy, and decision framework, see the [Strategi
- Multi-layer grid system with chunk-based rendering and dirty-flag caching - Multi-layer grid system with chunk-based rendering and dirty-flag caching
- Documentation macro system with auto-generated API docs, man pages, and type stubs - Documentation macro system with auto-generated API docs, man pages, and type stubs
- Windows cross-compilation, mobile-ish WASM support, SDL2_mixer audio - Windows cross-compilation, mobile-ish WASM support, SDL2_mixer audio
- Behavior/Trigger turn manager: `grid.step()`, entity labels, `cell_pos`, Dijkstra-backed pathfinding (#295-#303)
**Proving grounds**: Crypt of Sokoban (7DRL 2025) was the first complete game. 7DRL 2026 is the current target. **Proving grounds**: Crypt of Sokoban (7DRL 2025), then 7DRL 2026 -- both shipped on the same engine. The 2026 jam surfaced hotfix-worthy issues (SDL key scancodes, composite textures) that have since landed on master.
--- ---
## Current Focus: 7DRL 2026 ## Current Focus: API Freeze + Memory Safety Sweep
**Dates**: February 28 -- March 8, 2026 7DRL 2026 is behind us (Feb 28 -- Mar 8). The engine has two concurrent tracks to 1.0:
Engine preparation is complete. All 2D systems are production-ready. The jam will expose remaining rough edges in the workflow of building a complete game on McRogueFace. ### Track 1: API Freeze
The process is underway. Closed in this pass: camelCase module functions (#304), deprecated `sprite_number` (#305), legacy string enum comparisons (#306), `Color.__eq__`/`__ne__` (#307), `Grid.position` alias (#308).
Open prep items: Remaining freeze work:
- **#248** -- Crypt of Sokoban Remaster (game content for the jam) 1. Catalog every public Python class, method, and property -- audit against `stubs/mcrfpy.pyi` and generated docs
2. Identify any last naming/signature/default changes before committing
--- 3. Final breaking-change pass, bundled
## Post-7DRL: The Road to 1.0
After 7DRL, the priority shifts from feature development to **API stability**. 1.0 means the Python API is frozen: documented, stable, and not going to break.
### API Freeze Process
1. Catalog every public Python class, method, and property
2. Identify anything that should change before committing (naming, signatures, defaults)
3. Make breaking changes in a single coordinated pass
4. Document the stable API as the contract 4. Document the stable API as the contract
5. Experimental modules (3D/Voxel) get an explicit `experimental` label and are exempt from the freeze 5. Experimental modules (3D/Voxel) stay out of the freeze with an `experimental` label
### Post-Jam Priorities ### Track 2: Fuzz-Driven Bug Sweep
- Fix pain points discovered during actual 7DRL game development The libFuzzer+ASan harness (#283) has nine work tranches merged: build plumbing (W1), native harness (W2/W3), then six targeted fuzzers under `tests/fuzz/`:
- `fuzz_grid_entity` -- EntityCollection lifetime (W4, fixed #258-#263, #273, #274)
- `fuzz_property_types` -- refcount / type confusion (W5, fixed #267, #268, #272)
- `fuzz_anim_timer_scene` -- animation/timer/scene lifecycles (W6)
- `fuzz_fov` -- compute_fov parameters (W8, fixed #310)
- `fuzz_maps_procgen` -- HeightMap/DiscreteMap interfaces (W7)
- `fuzz_pathfinding_behavior` -- Dijkstra + turn manager (W9, fixed #311)
The active tier1 queue is empty. The last three findings (#309 Caption float→uint, #310 FOV enum, #311 DijkstraMap OOB) all landed on master in mid-April. Coverage extension to remaining public API surface is tracked under #312.
### Recently Shipped (April 2026)
- **#294** -- `entity.perspective_map` replaces flat `vector<UIGridPointState>` with a 3-state DiscreteMap (UNKNOWN/DISCOVERED/VISIBLE). Per-entity FOV memory is now serializable, swappable, and structurally enforces visible-as-subset-of-discovered.
- **#315** -- Pathfinding API extended with built-in heuristics (Euclidean/Manhattan/Chebyshev/Diagonal/Zero), multi-root Dijkstra, FLEE primitives (invert + descent), and an interactive demo. EntityBehavior SEEK/FLEE refactored to a `PathProvider` strategy.
- **Phase 5.2** -- six performance benchmark scripts under `tests/benchmarks/` covering grid.step(), FOV writeback cost, spatial hash vs. O(n), pathfinding with collision labels, multi-GridView render, and Dijkstra variants. Baselines under `tests/benchmarks/baseline/phase5_2/`.
- **Phase 5.3** -- documentation regenerated; `tools/generate_stubs_v2.py` rewritten as introspection-based so it can no longer drift from the C++ source.
### Active Follow-Ups
- **#312** Extend fuzz coverage to remaining public API surface
- **#313** Migrate `UIEntity::grid` from `shared_ptr<UIGrid>` to `shared_ptr<GridData>` (post-#252 refactor cleanup)
- **#314** API audit follow-through: close gaps from `docs/api-audit-2026-04.md`
- **#316** Sparse perspective writeback in `UIEntity::updateVisibility` (Phase 5.2 finding: full-grid demote+promote dominates over TCOD FOV cost)
### Other Post-7DRL Priorities
- Progress on the r/roguelikedev tutorial series (#167) - Progress on the r/roguelikedev tutorial series (#167)
- API consistency audit and freeze - Complete the API freeze catalog pass (#314)
- Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter - Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter
--- ---
@ -98,15 +113,18 @@ Rather than inverting the architecture to make McRogueFace a pip-installable pac
## Open Issues by Area ## Open Issues by Area
30 open issues across the tracker. Key groupings: 25 open issues across the tracker. Key groupings:
- **Multi-tile entities** (#233-#237) -- Oversized sprites, composite entities, origin offsets - **Recent follow-ups** (#312, #313, #314, #316) -- Fuzz coverage extension, UIEntity grid refactor, API audit follow-through, sparse perspective writeback
- **Grid enhancements** (#152, #149, #67) -- Sparse layers, refactoring, infinite worlds - **7DRL 2026 carry-over** (#248) -- Crypt of Sokoban remaster, superseded by the 7DRL 2026 entry but still relevant as a demo
- **Tooling / infrastructure** (#282, #255) -- Modern Clang for TSan/fuzzing, performance profiling
- **Demos / tutorials** (#167, #154, #156, #55) -- r/roguelikedev series, LLM agent simulations
- **Grid enhancements** (#152, #67) -- Sparse layers, infinite worlds
- **Performance** (#117, #124, #145) -- Memory pools, grid point animation, texture reuse - **Performance** (#117, #124, #145) -- Memory pools, grid point animation, texture reuse
- **LLM agent testbed** (#154, #156, #55) -- Multi-agent simulation, turn-based orchestration
- **Platform/distribution** (#70, #54, #62, #53) -- Packaging, Jupyter, multiple windows, input methods - **Platform/distribution** (#70, #54, #62, #53) -- Packaging, Jupyter, multiple windows, input methods
- **WASM tooling** (#238-#240) -- Debug infrastructure, automated browser testing, troubleshooting docs - **WASM tooling** (#239) -- Automated browser testing
- **Rendering** (#107, #218) -- Particle system, Color/Vector animation targets - **Rendering** (#107) -- Particle system
- **Deferred** (#220, #46, #45) -- Subinterpreter support / tests, accessibility modes
See the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current status. See the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current status.

View file

@ -1,6 +1,6 @@
# McRogueFace API Reference # McRogueFace API Reference
*Generated on 2026-04-18 07:28:57* *Generated on 2026-04-18 13:35:02*
*This documentation was dynamically generated from the compiled module.* *This documentation was dynamically generated from the compiled module.*
@ -10,7 +10,6 @@
- [Classes](#classes) - [Classes](#classes)
- [AStarPath](#astarpath) - [AStarPath](#astarpath)
- [Alignment](#alignment) - [Alignment](#alignment)
- [Animation](#animation)
- [Arc](#arc) - [Arc](#arc)
- [AutoRuleSet](#autoruleset) - [AutoRuleSet](#autoruleset)
- [BSP](#bsp) - [BSP](#bsp)
@ -35,6 +34,7 @@
- [Grid](#grid) - [Grid](#grid)
- [GridView](#gridview) - [GridView](#gridview)
- [HeightMap](#heightmap) - [HeightMap](#heightmap)
- [Heuristic](#heuristic)
- [InputState](#inputstate) - [InputState](#inputstate)
- [Key](#key) - [Key](#key)
- [Keyboard](#keyboard) - [Keyboard](#keyboard)
@ -356,122 +356,6 @@ Return an array of bytes representing an integer.
If signed is False and a negative integer is given, an OverflowError If signed is False and a negative integer is given, an OverflowError
is raised. is raised.
### Animation
Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, loop: bool = False, callback: Callable = None)
Create an animation that interpolates a property value over time.
Args:
property: Property name to animate. Valid properties depend on target type:
- Position/Size: 'x', 'y', 'w', 'h', 'pos', 'size'
- Appearance: 'fill_color', 'outline_color', 'outline', 'opacity'
- Sprite: 'sprite_index', 'scale'
- Grid: 'center', 'zoom'
- Caption: 'text'
- Sub-properties: 'fill_color.r', 'fill_color.g', 'fill_color.b', 'fill_color.a'
target: Target value for the animation. Type depends on property:
- float: For numeric properties (x, y, w, h, scale, opacity, zoom)
- int: For integer properties (sprite_index)
- tuple (r, g, b[, a]): For color properties
- tuple (x, y): For vector properties (pos, size, center)
- list[int]: For sprite animation sequences
- str: For text animation
duration: Animation duration in seconds.
easing: Easing function name. Options:
- 'linear' (default)
- 'easeIn', 'easeOut', 'easeInOut'
- 'easeInQuad', 'easeOutQuad', 'easeInOutQuad'
- 'easeInCubic', 'easeOutCubic', 'easeInOutCubic'
- 'easeInQuart', 'easeOutQuart', 'easeInOutQuart'
- 'easeInSine', 'easeOutSine', 'easeInOutSine'
- 'easeInExpo', 'easeOutExpo', 'easeInOutExpo'
- 'easeInCirc', 'easeOutCirc', 'easeInOutCirc'
- 'easeInElastic', 'easeOutElastic', 'easeInOutElastic'
- 'easeInBack', 'easeOutBack', 'easeInOutBack'
- 'easeInBounce', 'easeOutBounce', 'easeInOutBounce'
delta: If True, target is relative to start value (additive). Default False.
loop: If True, animation repeats from start when it reaches the end. Default False.
callback: Function(target, property, value) called when animation completes.
Not called for looping animations (since they never complete).
Example:
# Move a frame from current position to x=500 over 2 seconds
anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut')
anim.start(my_frame)
# Looping sprite animation
walk = mcrfpy.Animation('sprite_index', [0,1,2,3,2,1], 0.6, loop=True)
walk.start(my_sprite)
**Properties:**
- `duration` *(read-only)*: Animation duration in seconds (float, read-only). Total time for the animation to complete.
- `elapsed` *(read-only)*: Elapsed time in seconds (float, read-only). Time since the animation started.
- `is_complete` *(read-only)*: Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called.
- `is_delta` *(read-only)*: Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.
- `is_looping` *(read-only)*: Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end.
- `property` *(read-only)*: Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index').
**Methods:**
#### `complete() -> None`
Complete the animation immediately by jumping to the final value.
Note:
**Returns:** None Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.
#### `get_current_value() -> Any`
Get the current interpolated value of the animation.
Note:
**Returns:** Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str) Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).
#### `hasValidTarget() -> bool`
Check if the animation still has a valid target.
Note:
**Returns:** bool: True if the target still exists, False if it was destroyed Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.
#### `start(target: UIDrawable, conflict_mode: str = 'replace') -> None`
Start the animation on a target UI element.
Note:
**Arguments:**
- `target`: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)
- `conflict_mode`: How to handle conflicts if property is already animating: 'replace' (default) - complete existing animation and start new one; 'queue' - wait for existing animation to complete; 'error' - raise RuntimeError if property is busy
**Returns:** None
**Raises:** RuntimeError: When conflict_mode='error' and property is already animating The animation will automatically stop if the target is destroyed.
#### `stop() -> None`
Stop the animation without completing it.
Note:
**Returns:** None Unlike complete(), this does NOT apply the final value and does NOT trigger the callback. The animation is simply cancelled and will be removed from the AnimationManager.
#### `update(delta_time: float) -> bool`
Update the animation by the given time delta.
Note:
**Arguments:**
- `delta_time`: Time elapsed since last update in seconds
**Returns:** bool: True if animation is still running, False if complete Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.
### Arc ### Arc
*Inherits from: Drawable* *Inherits from: Drawable*
@ -1418,6 +1302,18 @@ Example:
**Methods:** **Methods:**
#### `descent_step(pos) -> Vector | None`
Get the adjacent cell with the lowest distance (steepest descent).
Unlike step_from (which follows the path set by path_from), descent_step
always returns the best neighbor in a single hop. Useful for AI that
reacts to the current distance field rather than following a fixed path.
**Arguments:**
- `pos`: Current position as Vector, Entity, or (x, y) tuple.
**Returns:** Next position as Vector, or None if pos is a local minimum or off-grid.
#### `distance(pos) -> float | None` #### `distance(pos) -> float | None`
Get distance from position to root. Get distance from position to root.
@ -1427,6 +1323,16 @@ Get distance from position to root.
**Returns:** Float distance, or None if position is unreachable. **Returns:** Float distance, or None if position is unreachable.
#### `invert() -> DijkstraMap`
Return a NEW DijkstraMap whose distance field is the safety field.
Cells near a root become high values and cells far from any root become
low values. Combined with step_from or descent_step, this gives flee
behavior: descend the inverted map to move away from the original roots.
The original DijkstraMap is unchanged.
**Returns:** New DijkstraMap with inverted distances.
#### `path_from(pos) -> AStarPath` #### `path_from(pos) -> AStarPath`
Get full path from position to root. Get full path from position to root.
@ -3052,6 +2958,99 @@ Return NEW HeightMap with uniform value where in range, 0.0 elsewhere.
**Raises:** ValueError: min > max **Raises:** ValueError: min > max
### Heuristic
*Inherits from: IntEnum*
Built-in A* heuristic function selector.
Values:
EUCLIDEAN: sqrt((dx)^2 + (dy)^2). Admissible, default.
MANHATTAN: |dx| + |dy|. Admissible on 4-connected grids.
CHEBYSHEV: max(|dx|, |dy|). Admissible on 8-connected (diag=1).
DIAGONAL: Octile distance. Admissible on 8-connected (diag=sqrt(2)).
ZERO: Always returns 0. A* degenerates to Dijkstra.
**Properties:**
- `denominator`: the denominator of a rational number in lowest terms
- `imag`: the imaginary part of a complex number
- `numerator`: the numerator of a rational number in lowest terms
- `real`: the real part of a complex number
**Methods:**
#### `as_integer_ratio(...)`
Return a pair of integers, whose ratio is equal to the original int.
The ratio is in lowest terms and has a positive denominator.
>>> (10).as_integer_ratio()
(10, 1)
>>> (-10).as_integer_ratio()
(-10, 1)
>>> (0).as_integer_ratio()
(0, 1)
#### `bit_count(...)`
Number of ones in the binary representation of the absolute value of self.
Also known as the population count.
>>> bin(13)
'0b1101'
>>> (13).bit_count()
3
#### `bit_length(...)`
Number of bits necessary to represent self in binary.
>>> bin(37)
'0b100101'
>>> (37).bit_length()
6
#### `conjugate(...)`
Returns self, the complex conjugate of any int.
#### `from_bytes(...)`
Return the integer represented by the given array of bytes.
bytes
Holds the array of bytes to convert. The argument must either
support the buffer protocol or be an iterable object producing bytes.
Bytes and bytearray are examples of built-in objects that support the
buffer protocol.
byteorder
The byte order used to represent the integer. If byteorder is 'big',
the most significant byte is at the beginning of the byte array. If
byteorder is 'little', the most significant byte is at the end of the
byte array. To request the native byte order of the host system, use
sys.byteorder as the byte order value. Default is to use 'big'.
signed
Indicates whether two's complement is used to represent the integer.
#### `is_integer(...)`
Returns True. Exists for duck type compatibility with float.is_integer.
#### `to_bytes(...)`
Return an array of bytes representing an integer.
length
Length of bytes object to use. An OverflowError is raised if the
integer is not representable with the given number of bytes. Default
is length 1.
byteorder
The byte order used to represent the integer. If byteorder is 'big',
the most significant byte is at the beginning of the byte array. If
byteorder is 'little', the most significant byte is at the end of the
byte array. To request the native byte order of the host system, use
sys.byteorder as the byte order value. Default is to use 'big'.
signed
Determines whether two's complement is used to represent the integer.
If signed is False and a negative integer is given, an OverflowError
is raised.
### InputState ### InputState
*Inherits from: IntEnum* *Inherits from: IntEnum*

View file

@ -108,7 +108,7 @@
<body> <body>
<div class="container"> <div class="container">
<h1>McRogueFace API Reference</h1> <h1>McRogueFace API Reference</h1>
<p><em>Generated on 2026-04-18 07:28:57</em></p> <p><em>Generated on 2026-04-18 13:35:02</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p> <p><em>This documentation was dynamically generated from the compiled module.</em></p>
<div class="toc"> <div class="toc">
@ -119,7 +119,6 @@
<ul> <ul>
<li><a href="#AStarPath">AStarPath</a></li> <li><a href="#AStarPath">AStarPath</a></li>
<li><a href="#Alignment">Alignment</a></li> <li><a href="#Alignment">Alignment</a></li>
<li><a href="#Animation">Animation</a></li>
<li><a href="#Arc">Arc</a></li> <li><a href="#Arc">Arc</a></li>
<li><a href="#AutoRuleSet">AutoRuleSet</a></li> <li><a href="#AutoRuleSet">AutoRuleSet</a></li>
<li><a href="#BSP">BSP</a></li> <li><a href="#BSP">BSP</a></li>
@ -144,6 +143,7 @@
<li><a href="#Grid">Grid</a></li> <li><a href="#Grid">Grid</a></li>
<li><a href="#GridView">GridView</a></li> <li><a href="#GridView">GridView</a></li>
<li><a href="#HeightMap">HeightMap</a></li> <li><a href="#HeightMap">HeightMap</a></li>
<li><a href="#Heuristic">Heuristic</a></li>
<li><a href="#InputState">InputState</a></li> <li><a href="#InputState">InputState</a></li>
<li><a href="#Key">Key</a></li> <li><a href="#Key">Key</a></li>
<li><a href="#Keyboard">Keyboard</a></li> <li><a href="#Keyboard">Keyboard</a></li>
@ -479,122 +479,6 @@ Also known as the population count.
</div> </div>
</div> </div>
<div class="method-section">
<h3 id="Animation"><span class="class-name">Animation</span></h3>
<p>Animation(property: str, target: Any, duration: float, easing: str = &#x27;linear&#x27;, delta: bool = False, loop: bool = False, callback: Callable = None)
Create an animation that interpolates a property value over time.
Args:
property: Property name to animate. Valid properties depend on target type:
- Position/Size: &#x27;x&#x27;, &#x27;y&#x27;, &#x27;w&#x27;, &#x27;h&#x27;, &#x27;pos&#x27;, &#x27;size&#x27;
- Appearance: &#x27;fill_color&#x27;, &#x27;outline_color&#x27;, &#x27;outline&#x27;, &#x27;opacity&#x27;
- Sprite: &#x27;sprite_index&#x27;, &#x27;scale&#x27;
- Grid: &#x27;center&#x27;, &#x27;zoom&#x27;
- Caption: &#x27;text&#x27;
- Sub-properties: &#x27;fill_color.r&#x27;, &#x27;fill_color.g&#x27;, &#x27;fill_color.b&#x27;, &#x27;fill_color.a&#x27;
target: Target value for the animation. Type depends on property:
- float: For numeric properties (x, y, w, h, scale, opacity, zoom)
- int: For integer properties (sprite_index)
- tuple (r, g, b[, a]): For color properties
- tuple (x, y): For vector properties (pos, size, center)
- list[int]: For sprite animation sequences
- str: For text animation
duration: Animation duration in seconds.
easing: Easing function name. Options:
- &#x27;linear&#x27; (default)
- &#x27;easeIn&#x27;, &#x27;easeOut&#x27;, &#x27;easeInOut&#x27;
- &#x27;easeInQuad&#x27;, &#x27;easeOutQuad&#x27;, &#x27;easeInOutQuad&#x27;
- &#x27;easeInCubic&#x27;, &#x27;easeOutCubic&#x27;, &#x27;easeInOutCubic&#x27;
- &#x27;easeInQuart&#x27;, &#x27;easeOutQuart&#x27;, &#x27;easeInOutQuart&#x27;
- &#x27;easeInSine&#x27;, &#x27;easeOutSine&#x27;, &#x27;easeInOutSine&#x27;
- &#x27;easeInExpo&#x27;, &#x27;easeOutExpo&#x27;, &#x27;easeInOutExpo&#x27;
- &#x27;easeInCirc&#x27;, &#x27;easeOutCirc&#x27;, &#x27;easeInOutCirc&#x27;
- &#x27;easeInElastic&#x27;, &#x27;easeOutElastic&#x27;, &#x27;easeInOutElastic&#x27;
- &#x27;easeInBack&#x27;, &#x27;easeOutBack&#x27;, &#x27;easeInOutBack&#x27;
- &#x27;easeInBounce&#x27;, &#x27;easeOutBounce&#x27;, &#x27;easeInOutBounce&#x27;
delta: If True, target is relative to start value (additive). Default False.
loop: If True, animation repeats from start when it reaches the end. Default False.
callback: Function(target, property, value) called when animation completes.
Not called for looping animations (since they never complete).
Example:
# Move a frame from current position to x=500 over 2 seconds
anim = mcrfpy.Animation(&#x27;x&#x27;, 500.0, 2.0, &#x27;easeInOut&#x27;)
anim.start(my_frame)
# Looping sprite animation
walk = mcrfpy.Animation(&#x27;sprite_index&#x27;, [0,1,2,3,2,1], 0.6, loop=True)
walk.start(my_sprite)
</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>duration</span> (read-only): Animation duration in seconds (float, read-only). Total time for the animation to complete.</li>
<li><span class='property-name'>elapsed</span> (read-only): Elapsed time in seconds (float, read-only). Time since the animation started.</li>
<li><span class='property-name'>is_complete</span> (read-only): Whether animation is complete (bool, read-only). True when elapsed &gt;= duration or complete() was called.</li>
<li><span class='property-name'>is_delta</span> (read-only): Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.</li>
<li><span class='property-name'>is_looping</span> (read-only): Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end.</li>
<li><span class='property-name'>property</span> (read-only): Target property name (str, read-only). The property being animated (e.g., &#x27;pos&#x27;, &#x27;opacity&#x27;, &#x27;sprite_index&#x27;).</li>
</ul>
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">complete() -> None</code></h5>
<p>Complete the animation immediately by jumping to the final value.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">get_current_value() -> Any</code></h5>
<p>Get the current interpolated value of the animation.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str) Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">hasValidTarget() -> bool</code></h5>
<p>Check if the animation still has a valid target.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> bool: True if the target still exists, False if it was destroyed Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">start(target: UIDrawable, conflict_mode: str = 'replace') -> None</code></h5>
<p>Start the animation on a target UI element.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>target</span>: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)</div>
<div><span class='arg-name'>conflict_mode</span>: How to handle conflicts if property is already animating: &#x27;replace&#x27; (default) - complete existing animation and start new one; &#x27;queue&#x27; - wait for existing animation to complete; &#x27;error&#x27; - raise RuntimeError if property is busy</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None</p>
<p style='margin-left: 20px;'><span class='raises'>Raises:</span> RuntimeError: When conflict_mode=&#x27;error&#x27; and property is already animating The animation will automatically stop if the target is destroyed.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">stop() -> None</code></h5>
<p>Stop the animation without completing it.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None Unlike complete(), this does NOT apply the final value and does NOT trigger the callback. The animation is simply cancelled and will be removed from the AnimationManager.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">update(delta_time: float) -> bool</code></h5>
<p>Update the animation by the given time delta.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>delta_time</span>: Time elapsed since last update in seconds</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> bool: True if animation is still running, False if complete Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.</p>
</div>
</div>
<div class="method-section"> <div class="method-section">
<h3 id="Arc"><span class="class-name">Arc</span></h3> <h3 id="Arc"><span class="class-name">Arc</span></h3>
<p><em>Inherits from: Drawable</em></p> <p><em>Inherits from: Drawable</em></p>
@ -1567,6 +1451,18 @@ Example:
</ul> </ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">descent_step(pos) -> Vector | None</code></h5>
<p>Get the adjacent cell with the lowest distance (steepest descent).
Unlike step_from (which follows the path set by path_from), descent_step
always returns the best neighbor in a single hop. Useful for AI that
reacts to the current distance field rather than following a fixed path.</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>pos</span>: Current position as Vector, Entity, or (x, y) tuple.</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Next position as Vector, or None if pos is a local minimum or off-grid.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">distance(pos) -> float | None</code></h5> <h5><code class="method-name">distance(pos) -> float | None</code></h5>
<p>Get distance from position to root.</p> <p>Get distance from position to root.</p>
@ -1576,6 +1472,16 @@ Example:
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Float distance, or None if position is unreachable.</p> <p style='margin-left: 20px;'><span class='returns'>Returns:</span> Float distance, or None if position is unreachable.</p>
</div> </div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">invert() -> DijkstraMap</code></h5>
<p>Return a NEW DijkstraMap whose distance field is the safety field.
Cells near a root become high values and cells far from any root become
low values. Combined with step_from or descent_step, this gives flee
behavior: descend the inverted map to move away from the original roots.
The original DijkstraMap is unchanged.</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> New DijkstraMap with inverted distances.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">path_from(pos) -> AStarPath</code></h5> <h5><code class="method-name">path_from(pos) -> AStarPath</code></h5>
<p>Get full path from position to root.</p> <p>Get full path from position to root.</p>
@ -3247,6 +3153,106 @@ Note:</p>
</div> </div>
</div> </div>
<div class="method-section">
<h3 id="Heuristic"><span class="class-name">Heuristic</span></h3>
<p><em>Inherits from: IntEnum</em></p>
<p>Built-in A* heuristic function selector.
Values:
EUCLIDEAN: sqrt((dx)^2 + (dy)^2). Admissible, default.
MANHATTAN: |dx| + |dy|. Admissible on 4-connected grids.
CHEBYSHEV: max(|dx|, |dy|). Admissible on 8-connected (diag=1).
DIAGONAL: Octile distance. Admissible on 8-connected (diag=sqrt(2)).
ZERO: Always returns 0. A* degenerates to Dijkstra.
</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>denominator</span>: the denominator of a rational number in lowest terms</li>
<li><span class='property-name'>imag</span>: the imaginary part of a complex number</li>
<li><span class='property-name'>numerator</span>: the numerator of a rational number in lowest terms</li>
<li><span class='property-name'>real</span>: the real part of a complex number</li>
</ul>
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">as_integer_ratio(...)</code></h5>
<p>Return a pair of integers, whose ratio is equal to the original int.
The ratio is in lowest terms and has a positive denominator.
&gt;&gt;&gt; (10).as_integer_ratio()
(10, 1)
&gt;&gt;&gt; (-10).as_integer_ratio()
(-10, 1)
&gt;&gt;&gt; (0).as_integer_ratio()
(0, 1)</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">bit_count(...)</code></h5>
<p>Number of ones in the binary representation of the absolute value of self.
Also known as the population count.
&gt;&gt;&gt; bin(13)
&#x27;0b1101&#x27;
&gt;&gt;&gt; (13).bit_count()
3</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">bit_length(...)</code></h5>
<p>Number of bits necessary to represent self in binary.
&gt;&gt;&gt; bin(37)
&#x27;0b100101&#x27;
&gt;&gt;&gt; (37).bit_length()
6</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">conjugate(...)</code></h5>
<p>Returns self, the complex conjugate of any int.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">from_bytes(...)</code></h5>
<p>Return the integer represented by the given array of bytes.
bytes
Holds the array of bytes to convert. The argument must either
support the buffer protocol or be an iterable object producing bytes.
Bytes and bytearray are examples of built-in objects that support the
buffer protocol.
byteorder
The byte order used to represent the integer. If byteorder is &#x27;big&#x27;,
the most significant byte is at the beginning of the byte array. If
byteorder is &#x27;little&#x27;, the most significant byte is at the end of the
byte array. To request the native byte order of the host system, use
sys.byteorder as the byte order value. Default is to use &#x27;big&#x27;.
signed
Indicates whether two&#x27;s complement is used to represent the integer.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">is_integer(...)</code></h5>
<p>Returns True. Exists for duck type compatibility with float.is_integer.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">to_bytes(...)</code></h5>
<p>Return an array of bytes representing an integer.
length
Length of bytes object to use. An OverflowError is raised if the
integer is not representable with the given number of bytes. Default
is length 1.
byteorder
The byte order used to represent the integer. If byteorder is &#x27;big&#x27;,
the most significant byte is at the beginning of the byte array. If
byteorder is &#x27;little&#x27;, the most significant byte is at the end of the
byte array. To request the native byte order of the host system, use
sys.byteorder as the byte order value. Default is to use &#x27;big&#x27;.
signed
Determines whether two&#x27;s complement is used to represent the integer.
If signed is False and a negative integer is given, an OverflowError
is raised.</p>
</div>
</div>
<div class="method-section"> <div class="method-section">
<h3 id="InputState"><span class="class-name">InputState</span></h3> <h3 id="InputState"><span class="class-name">InputState</span></h3>
<p><em>Inherits from: IntEnum</em></p> <p><em>Inherits from: IntEnum</em></p>

View file

@ -14,11 +14,11 @@
. ftr VB CB . ftr VB CB
. ftr VBI CBI . ftr VBI CBI
.\} .\}
.TH "MCRFPY" "3" "2026-04-18" "McRogueFace 0.2.7-prerelease-7drl2026-79-g59e7221" "" .TH "MCRFPY" "3" "2026-04-18" "McRogueFace 0.2.7-prerelease-7drl2026-93-g0f7254e" ""
.hy .hy
.SH McRogueFace API Reference .SH McRogueFace API Reference
.PP .PP
\f[I]Generated on 2026-04-18 07:28:57\f[R] \f[I]Generated on 2026-04-18 13:35:02\f[R]
.PP .PP
\f[I]This documentation was dynamically generated from the compiled \f[I]This documentation was dynamically generated from the compiled
module.\f[R] module.\f[R]
@ -33,8 +33,6 @@ AStarPath
.IP \[bu] 2 .IP \[bu] 2
Alignment Alignment
.IP \[bu] 2 .IP \[bu] 2
Animation
.IP \[bu] 2
Arc Arc
.IP \[bu] 2 .IP \[bu] 2
AutoRuleSet AutoRuleSet
@ -83,6 +81,8 @@ GridView
.IP \[bu] 2 .IP \[bu] 2
HeightMap HeightMap
.IP \[bu] 2 .IP \[bu] 2
Heuristic
.IP \[bu] 2
InputState InputState
.IP \[bu] 2 .IP \[bu] 2
Key Key
@ -436,146 +436,6 @@ signed Determines whether two\[cq]s complement is used to represent the
integer. integer.
If signed is False and a negative integer is given, an OverflowError is If signed is False and a negative integer is given, an OverflowError is
raised. raised.
.SS Animation
.PP
Animation(property: str, target: Any, duration: float, easing: str =
`linear', delta: bool = False, loop: bool = False, callback: Callable =
None)
.PP
Create an animation that interpolates a property value over time.
.PP
Args: property: Property name to animate.
Valid properties depend on target type: - Position/Size: `x', `y', `w',
`h', `pos', `size' - Appearance: `fill_color', `outline_color',
`outline', `opacity' - Sprite: `sprite_index', `scale' - Grid: `center',
`zoom' - Caption: `text' - Sub-properties: `fill_color.r',
`fill_color.g', `fill_color.b', `fill_color.a' target: Target value for
the animation.
Type depends on property: - float: For numeric properties (x, y, w, h,
scale, opacity, zoom) - int: For integer properties (sprite_index) -
tuple (r, g, b[, a]): For color properties - tuple (x, y): For vector
properties (pos, size, center) - list[int]: For sprite animation
sequences - str: For text animation duration: Animation duration in
seconds.
easing: Easing function name.
Options: - `linear' (default) - `easeIn', `easeOut', `easeInOut' -
`easeInQuad', `easeOutQuad', `easeInOutQuad' - `easeInCubic',
`easeOutCubic', `easeInOutCubic' - `easeInQuart', `easeOutQuart',
`easeInOutQuart' - `easeInSine', `easeOutSine', `easeInOutSine' -
`easeInExpo', `easeOutExpo', `easeInOutExpo' - `easeInCirc',
`easeOutCirc', `easeInOutCirc' - `easeInElastic', `easeOutElastic',
`easeInOutElastic' - `easeInBack', `easeOutBack', `easeInOutBack' -
`easeInBounce', `easeOutBounce', `easeInOutBounce' delta: If True,
target is relative to start value (additive).
Default False.
loop: If True, animation repeats from start when it reaches the end.
Default False.
callback: Function(target, property, value) called when animation
completes.
Not called for looping animations (since they never complete).
.PP
Example: # Move a frame from current position to x=500 over 2 seconds
anim = mcrfpy.Animation(`x', 500.0, 2.0, `easeInOut')
anim.start(my_frame)
.IP
.nf
\f[C]
# Looping sprite animation
walk = mcrfpy.Animation(\[aq]sprite_index\[aq], [0,1,2,3,2,1], 0.6, loop=True)
walk.start(my_sprite)
\f[R]
.fi
.PP
\f[B]Properties:\f[R] - \f[V]duration\f[R] \f[I](read-only)\f[R]:
Animation duration in seconds (float, read-only).
Total time for the animation to complete.
- \f[V]elapsed\f[R] \f[I](read-only)\f[R]: Elapsed time in seconds
(float, read-only).
Time since the animation started.
- \f[V]is_complete\f[R] \f[I](read-only)\f[R]: Whether animation is
complete (bool, read-only).
True when elapsed >= duration or complete() was called.
- \f[V]is_delta\f[R] \f[I](read-only)\f[R]: Whether animation uses delta
mode (bool, read-only).
In delta mode, the target value is added to the starting value.
- \f[V]is_looping\f[R] \f[I](read-only)\f[R]: Whether animation loops
(bool, read-only).
Looping animations repeat from the start when they reach the end.
- \f[V]property\f[R] \f[I](read-only)\f[R]: Target property name (str,
read-only).
The property being animated (e.g., `pos', `opacity', `sprite_index').
.PP
\f[B]Methods:\f[R]
.SS \f[V]complete() -> None\f[R]
.PP
Complete the animation immediately by jumping to the final value.
.PP
Note:
.PP
\f[B]Returns:\f[R] None Sets elapsed = duration and applies target value
immediately.
Completion callback will be called if set.
.SS \f[V]get_current_value() -> Any\f[R]
.PP
Get the current interpolated value of the animation.
.PP
Note:
.PP
\f[B]Returns:\f[R] Any: Current value (type depends on property: float,
int, Color tuple, Vector tuple, or str) Return type matches the target
property type.
For sprite_index returns int, for pos returns (x, y), for fill_color
returns (r, g, b, a).
.SS \f[V]hasValidTarget() -> bool\f[R]
.PP
Check if the animation still has a valid target.
.PP
Note:
.PP
\f[B]Returns:\f[R] bool: True if the target still exists, False if it
was destroyed Animations automatically clean up when targets are
destroyed.
Use this to check if manual cleanup is needed.
.SS \f[V]start(target: UIDrawable, conflict_mode: str = \[aq]replace\[aq]) -> None\f[R]
.PP
Start the animation on a target UI element.
.PP
Note:
.PP
\f[B]Arguments:\f[R] - \f[V]target\f[R]: The UI element to animate
(Frame, Caption, Sprite, Grid, or Entity) - \f[V]conflict_mode\f[R]: How
to handle conflicts if property is already animating: `replace'
(default) - complete existing animation and start new one; `queue' -
wait for existing animation to complete; `error' - raise RuntimeError if
property is busy
.PP
\f[B]Returns:\f[R] None
.PP
\f[B]Raises:\f[R] RuntimeError: When conflict_mode=`error' and property
is already animating The animation will automatically stop if the target
is destroyed.
.SS \f[V]stop() -> None\f[R]
.PP
Stop the animation without completing it.
.PP
Note:
.PP
\f[B]Returns:\f[R] None Unlike complete(), this does NOT apply the final
value and does NOT trigger the callback.
The animation is simply cancelled and will be removed from the
AnimationManager.
.SS \f[V]update(delta_time: float) -> bool\f[R]
.PP
Update the animation by the given time delta.
.PP
Note:
.PP
\f[B]Arguments:\f[R] - \f[V]delta_time\f[R]: Time elapsed since last
update in seconds
.PP
\f[B]Returns:\f[R] bool: True if animation is still running, False if
complete Typically called by AnimationManager automatically.
Manual calls only needed for custom animation control.
.SS Arc .SS Arc
.PP .PP
\f[I]Inherits from: Drawable\f[R] \f[I]Inherits from: Drawable\f[R]
@ -1602,6 +1462,19 @@ enemies: dist = dijkstra.distance(enemy.pos) if dist and dist < 10: step
position that distances are measured from (Vector, read-only). position that distances are measured from (Vector, read-only).
.PP .PP
\f[B]Methods:\f[R] \f[B]Methods:\f[R]
.SS \f[V]descent_step(pos) -> Vector | None\f[R]
.PP
Get the adjacent cell with the lowest distance (steepest descent).
Unlike step_from (which follows the path set by path_from), descent_step
always returns the best neighbor in a single hop.
Useful for AI that reacts to the current distance field rather than
following a fixed path.
.PP
\f[B]Arguments:\f[R] - \f[V]pos\f[R]: Current position as Vector,
Entity, or (x, y) tuple.
.PP
\f[B]Returns:\f[R] Next position as Vector, or None if pos is a local
minimum or off-grid.
.SS \f[V]distance(pos) -> float | None\f[R] .SS \f[V]distance(pos) -> float | None\f[R]
.PP .PP
Get distance from position to root. Get distance from position to root.
@ -1610,6 +1483,16 @@ Get distance from position to root.
y) tuple. y) tuple.
.PP .PP
\f[B]Returns:\f[R] Float distance, or None if position is unreachable. \f[B]Returns:\f[R] Float distance, or None if position is unreachable.
.SS \f[V]invert() -> DijkstraMap\f[R]
.PP
Return a NEW DijkstraMap whose distance field is the safety field.
Cells near a root become high values and cells far from any root become
low values.
Combined with step_from or descent_step, this gives flee behavior:
descend the inverted map to move away from the original roots.
The original DijkstraMap is unchanged.
.PP
\f[B]Returns:\f[R] New DijkstraMap with inverted distances.
.SS \f[V]path_from(pos) -> AStarPath\f[R] .SS \f[V]path_from(pos) -> AStarPath\f[R]
.PP .PP
Get full path from position to root. Get full path from position to root.
@ -3307,6 +3190,90 @@ or list, inclusive - \f[V]value\f[R]: Value to set for cells in range
\f[B]Returns:\f[R] HeightMap: New HeightMap (original is unchanged) \f[B]Returns:\f[R] HeightMap: New HeightMap (original is unchanged)
.PP .PP
\f[B]Raises:\f[R] ValueError: min > max \f[B]Raises:\f[R] ValueError: min > max
.SS Heuristic
.PP
\f[I]Inherits from: IntEnum\f[R]
.PP
Built-in A* heuristic function selector.
.PP
Values: EUCLIDEAN: sqrt((dx)\[ha]2 + (dy)\[ha]2).
Admissible, default.
MANHATTAN: |dx| + |dy|.
Admissible on 4-connected grids.
CHEBYSHEV: max(|dx|, |dy|).
Admissible on 8-connected (diag=1).
DIAGONAL: Octile distance.
Admissible on 8-connected (diag=sqrt(2)).
ZERO: Always returns 0.
A* degenerates to Dijkstra.
.PP
\f[B]Properties:\f[R] - \f[V]denominator\f[R]: the denominator of a
rational number in lowest terms - \f[V]imag\f[R]: the imaginary part of
a complex number - \f[V]numerator\f[R]: the numerator of a rational
number in lowest terms - \f[V]real\f[R]: the real part of a complex
number
.PP
\f[B]Methods:\f[R]
.SS \f[V]as_integer_ratio(...)\f[R]
.PP
Return a pair of integers, whose ratio is equal to the original int.
The ratio is in lowest terms and has a positive denominator.
>>> (10).as_integer_ratio() (10, 1) >>> (-10).as_integer_ratio() (-10,
1) >>> (0).as_integer_ratio() (0, 1)
.SS \f[V]bit_count(...)\f[R]
.PP
Number of ones in the binary representation of the absolute value of
self.
Also known as the population count.
>>> bin(13) `0b1101' >>> (13).bit_count() 3
.SS \f[V]bit_length(...)\f[R]
.PP
Number of bits necessary to represent self in binary.
>>> bin(37) `0b100101' >>> (37).bit_length() 6
.SS \f[V]conjugate(...)\f[R]
.PP
Returns self, the complex conjugate of any int.
.SS \f[V]from_bytes(...)\f[R]
.PP
Return the integer represented by the given array of bytes.
bytes Holds the array of bytes to convert.
The argument must either support the buffer protocol or be an iterable
object producing bytes.
Bytes and bytearray are examples of built-in objects that support the
buffer protocol.
byteorder The byte order used to represent the integer.
If byteorder is `big', the most significant byte is at the beginning of
the byte array.
If byteorder is `little', the most significant byte is at the end of the
byte array.
To request the native byte order of the host system, use sys.byteorder
as the byte order value.
Default is to use `big'.
signed Indicates whether two\[cq]s complement is used to represent the
integer.
.SS \f[V]is_integer(...)\f[R]
.PP
Returns True.
Exists for duck type compatibility with float.is_integer.
.SS \f[V]to_bytes(...)\f[R]
.PP
Return an array of bytes representing an integer.
length Length of bytes object to use.
An OverflowError is raised if the integer is not representable with the
given number of bytes.
Default is length 1.
byteorder The byte order used to represent the integer.
If byteorder is `big', the most significant byte is at the beginning of
the byte array.
If byteorder is `little', the most significant byte is at the end of the
byte array.
To request the native byte order of the host system, use sys.byteorder
as the byte order value.
Default is to use `big'.
signed Determines whether two\[cq]s complement is used to represent the
integer.
If signed is False and a negative integer is given, an OverflowError is
raised.
.SS InputState .SS InputState
.PP .PP
\f[I]Inherits from: IntEnum\f[R] \f[I]Inherits from: IntEnum\f[R]

View file

@ -814,14 +814,24 @@ PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = {
}; };
int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) { int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"z_index", "name", "grid_size", NULL}; // 1.0 API freeze: positional order is now (name, z_index, ...).
static const char* kwlist[] = {"name", "z_index", "grid_size", "grid", NULL};
int z_index = -1; int z_index = -1;
const char* name_str = nullptr; const char* name_str = nullptr;
PyObject* grid_size_obj = nullptr; PyObject* grid_size_obj = nullptr;
PyObject* grid_obj = nullptr;
int grid_x = 0, grid_y = 0; int grid_x = 0, grid_y = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izO", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOO", const_cast<char**>(kwlist),
&z_index, &name_str, &grid_size_obj)) { &name_str, &z_index, &grid_size_obj, &grid_obj)) {
return -1;
}
// Validate grid kwarg type up-front (before allocating data).
if (grid_obj && grid_obj != Py_None &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
return -1; return -1;
} }
@ -851,6 +861,15 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py
} }
self->grid.reset(); self->grid.reset();
// Auto-attach to grid via grid.add_layer(self) if grid= was supplied.
if (grid_obj && grid_obj != Py_None) {
PyObject* result = PyObject_CallMethod(grid_obj, "add_layer", "O", (PyObject*)self);
if (!result) {
return -1;
}
Py_DECREF(result);
}
return 0; return 0;
} }
@ -945,6 +964,90 @@ PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* arg
Py_RETURN_NONE; Py_RETURN_NONE;
} }
// =============================================================================
// Subscript protocol: layer[x, y] / layer[x, y] = value
// =============================================================================
// Helper: parse a 2-tuple key into (x, y) ints. Sets TypeError on bad keys.
static bool parse_subscript_key(PyObject* key, int* x, int* y) {
if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) {
PyErr_SetString(PyExc_TypeError,
"Layer indices must be a 2-tuple (x, y)");
return false;
}
PyObject* x_obj = PyTuple_GetItem(key, 0);
PyObject* y_obj = PyTuple_GetItem(key, 1);
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
PyErr_SetString(PyExc_TypeError, "Layer indices must be integers");
return false;
}
*x = (int)PyLong_AsLong(x_obj);
*y = (int)PyLong_AsLong(y_obj);
return true;
}
PyObject* PyGridLayerAPI::ColorLayer_subscript(PyColorLayerObject* self, PyObject* key) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
int x, y;
if (!parse_subscript_key(key, &x, &y)) return NULL;
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_Format(PyExc_IndexError,
"Position (%d, %d) out of bounds for ColorLayer of size (%d, %d)",
x, y, self->data->grid_x, self->data->grid_y);
return NULL;
}
const sf::Color& color = self->data->at(x, y);
// Wrap as mcrfpy.Color
auto* color_type = (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Color");
if (!color_type) return NULL;
PyColorObject* color_obj = (PyColorObject*)color_type->tp_alloc(color_type, 0);
Py_DECREF(color_type);
if (!color_obj) return NULL;
color_obj->data = color;
return (PyObject*)color_obj;
}
int PyGridLayerAPI::ColorLayer_subscript_assign(PyColorLayerObject* self, PyObject* key, PyObject* value) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return -1;
}
if (value == nullptr) {
PyErr_SetString(PyExc_TypeError, "cannot delete ColorLayer cells");
return -1;
}
int x, y;
if (!parse_subscript_key(key, &x, &y)) return -1;
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_Format(PyExc_IndexError,
"Position (%d, %d) out of bounds for ColorLayer of size (%d, %d)",
x, y, self->data->grid_x, self->data->grid_y);
return -1;
}
// PyColor::fromPy accepts Color objects, tuples, lists, ints; sets PyErr on failure.
sf::Color color = PyColor::fromPy(value);
if (PyErr_Occurred()) return -1;
self->data->at(x, y) = color;
self->data->markDirty(x, y);
return 0;
}
PyMappingMethods PyGridLayerAPI::ColorLayer_mapping_methods = {
.mp_length = nullptr,
.mp_subscript = (binaryfunc)PyGridLayerAPI::ColorLayer_subscript,
.mp_ass_subscript = (objobjargproc)PyGridLayerAPI::ColorLayer_subscript_assign,
};
PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) { PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) {
PyObject* color_obj; PyObject* color_obj;
if (!PyArg_ParseTuple(args, "O", &color_obj)) { if (!PyArg_ParseTuple(args, "O", &color_obj)) {
@ -1844,15 +1947,25 @@ PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = {
}; };
int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) { int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"z_index", "name", "texture", "grid_size", NULL}; // 1.0 API freeze: positional order is now (name, z_index, ...).
static const char* kwlist[] = {"name", "z_index", "texture", "grid_size", "grid", NULL};
int z_index = -1; int z_index = -1;
const char* name_str = nullptr; const char* name_str = nullptr;
PyObject* texture_obj = nullptr; PyObject* texture_obj = nullptr;
PyObject* grid_size_obj = nullptr; PyObject* grid_size_obj = nullptr;
PyObject* grid_obj = nullptr;
int grid_x = 0, grid_y = 0; int grid_x = 0, grid_y = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izOO", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOOO", const_cast<char**>(kwlist),
&z_index, &name_str, &texture_obj, &grid_size_obj)) { &name_str, &z_index, &texture_obj, &grid_size_obj, &grid_obj)) {
return -1;
}
// Validate grid kwarg type up-front (before allocating data).
if (grid_obj && grid_obj != Py_None &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
return -1; return -1;
} }
@ -1903,6 +2016,15 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb
} }
self->grid.reset(); self->grid.reset();
// Auto-attach to grid via grid.add_layer(self) if grid= was supplied.
if (grid_obj && grid_obj != Py_None) {
PyObject* result = PyObject_CallMethod(grid_obj, "add_layer", "O", (PyObject*)self);
if (!result) {
return -1;
}
Py_DECREF(result);
}
return 0; return 0;
} }
@ -1952,6 +2074,64 @@ PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args)
Py_RETURN_NONE; Py_RETURN_NONE;
} }
// =============================================================================
// TileLayer subscript: tl[x, y] / tl[x, y] = index
// =============================================================================
PyObject* PyGridLayerAPI::TileLayer_subscript(PyTileLayerObject* self, PyObject* key) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
int x, y;
if (!parse_subscript_key(key, &x, &y)) return NULL;
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_Format(PyExc_IndexError,
"Position (%d, %d) out of bounds for TileLayer of size (%d, %d)",
x, y, self->data->grid_x, self->data->grid_y);
return NULL;
}
return PyLong_FromLong(self->data->at(x, y));
}
int PyGridLayerAPI::TileLayer_subscript_assign(PyTileLayerObject* self, PyObject* key, PyObject* value) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return -1;
}
if (value == nullptr) {
PyErr_SetString(PyExc_TypeError, "cannot delete TileLayer cells");
return -1;
}
int x, y;
if (!parse_subscript_key(key, &x, &y)) return -1;
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_Format(PyExc_IndexError,
"Position (%d, %d) out of bounds for TileLayer of size (%d, %d)",
x, y, self->data->grid_x, self->data->grid_y);
return -1;
}
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "tile index must be an int");
return -1;
}
int index = (int)PyLong_AsLong(value);
self->data->at(x, y) = index;
self->data->markDirty(x, y);
return 0;
}
PyMappingMethods PyGridLayerAPI::TileLayer_mapping_methods = {
.mp_length = nullptr,
.mp_subscript = (binaryfunc)PyGridLayerAPI::TileLayer_subscript,
.mp_ass_subscript = (objobjargproc)PyGridLayerAPI::TileLayer_subscript_assign,
};
PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) { PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) {
int index; int index;
if (!PyArg_ParseTuple(args, "i", &index)) { if (!PyArg_ParseTuple(args, "i", &index)) {

View file

@ -218,6 +218,11 @@ public:
static int ColorLayer_set_grid(PyColorLayerObject* self, PyObject* value, void* closure); static int ColorLayer_set_grid(PyColorLayerObject* self, PyObject* value, void* closure);
static PyObject* ColorLayer_repr(PyColorLayerObject* self); static PyObject* ColorLayer_repr(PyColorLayerObject* self);
// Subscript protocol: layer[x, y] / layer[x, y] = value
static PyObject* ColorLayer_subscript(PyColorLayerObject* self, PyObject* key);
static int ColorLayer_subscript_assign(PyColorLayerObject* self, PyObject* key, PyObject* value);
static PyMappingMethods ColorLayer_mapping_methods;
// TileLayer methods // TileLayer methods
static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds); static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds); static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
@ -238,6 +243,11 @@ public:
static int TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, void* closure); static int TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, void* closure);
static PyObject* TileLayer_repr(PyTileLayerObject* self); static PyObject* TileLayer_repr(PyTileLayerObject* self);
// Subscript protocol: layer[x, y] / layer[x, y] = value
static PyObject* TileLayer_subscript(PyTileLayerObject* self, PyObject* key);
static int TileLayer_subscript_assign(PyTileLayerObject* self, PyObject* key, PyObject* value);
static PyMappingMethods TileLayer_mapping_methods;
// Method and getset arrays // Method and getset arrays
static PyMethodDef ColorLayer_methods[]; static PyMethodDef ColorLayer_methods[];
static PyGetSetDef ColorLayer_getsetters[]; static PyGetSetDef ColorLayer_getsetters[];
@ -259,6 +269,7 @@ namespace mcrfpydef {
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
}, },
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr, .tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
.tp_as_mapping = &PyGridLayerAPI::ColorLayer_mapping_methods, // layer[x, y]
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("ColorLayer(z_index=-1, name=None, grid_size=None)\n\n" .tp_doc = PyDoc_STR("ColorLayer(z_index=-1, name=None, grid_size=None)\n\n"
"A grid layer that stores RGBA colors per cell for background/overlay effects.\n\n" "A grid layer that stores RGBA colors per cell for background/overlay effects.\n\n"
@ -312,6 +323,7 @@ namespace mcrfpydef {
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
}, },
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr, .tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
.tp_as_mapping = &PyGridLayerAPI::TileLayer_mapping_methods, // layer[x, y]
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("TileLayer(z_index=-1, name=None, texture=None, grid_size=None)\n\n" .tp_doc = PyDoc_STR("TileLayer(z_index=-1, name=None, texture=None, grid_size=None)\n\n"
"A grid layer that stores sprite indices per cell for tile-based rendering.\n\n" "A grid layer that stores sprite indices per cell for tile-based rendering.\n\n"

View file

@ -497,9 +497,6 @@ PyObject* PyInit_mcrfpy()
/*grid layers (#147)*/ /*grid layers (#147)*/
&PyColorLayerType, &PyTileLayerType, &PyColorLayerType, &PyTileLayerType,
/*animation*/
&PyAnimationType,
/*timer*/ /*timer*/
&PyTimerType, &PyTimerType,
@ -574,6 +571,10 @@ PyObject* PyInit_mcrfpy()
/*shader uniform collection - returned by drawable.uniforms but not directly instantiable (#106)*/ /*shader uniform collection - returned by drawable.uniforms but not directly instantiable (#106)*/
&mcrfpydef::PyUniformCollectionType, &mcrfpydef::PyUniformCollectionType,
/*animation - constructed internally by drawable.animate() and returned by mcrfpy.animations,
but not directly instantiable from Python (pre-1.0 API freeze)*/
&PyAnimationType,
nullptr}; nullptr};
// Set up PyWindowType methods and getsetters before PyType_Ready // Set up PyWindowType methods and getsetters before PyType_Ready

View file

@ -2,6 +2,10 @@
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PythonObjectCache.h" #include "PythonObjectCache.h"
#include "PyAlignment.h" #include "PyAlignment.h"
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include <cmath> #include <cmath>
#include <sstream> #include <sstream>
@ -556,19 +560,22 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
float margin = 0.0f; float margin = 0.0f;
float horiz_margin = -1.0f; float horiz_margin = -1.0f;
float vert_margin = -1.0f; float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
static const char* kwlist[] = { static const char* kwlist[] = {
"center", "radius", "start_angle", "end_angle", "color", "thickness", "center", "radius", "start_angle", "end_angle", "color", "thickness",
"on_click", "visible", "opacity", "z_index", "name", "on_click", "visible", "opacity", "z_index", "name",
"align", "margin", "horiz_margin", "vert_margin", "align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr nullptr
}; };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfff", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfffO", const_cast<char**>(kwlist),
&center_obj, &radius, &start_angle, &end_angle, &center_obj, &radius, &start_angle, &end_angle,
&color_obj, &thickness, &color_obj, &thickness,
&click_handler, &visible, &opacity, &z_index, &name, &click_handler, &visible, &opacity, &z_index, &name,
&align_obj, &margin, &horiz_margin, &vert_margin)) { &align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1; return -1;
} }
@ -639,5 +646,8 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
// #184: Check if this is a Python subclass (for callback method support) // #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIArcType; self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIArcType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0; return 0;
} }

View file

@ -158,6 +158,33 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
} \ } \
} while (0) } while (0)
// Macro for auto-attaching a newly constructed UI drawable to a parent's children.
// Usage: UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
// parent_obj must be a Frame, Scene, or Grid (anything with a .children UICollection).
// Returns -1 on error (suitable for use in tp_init functions). No-op when parent_obj is null/None.
#define UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self) \
do { \
if ((parent_obj) && (parent_obj) != Py_None) { \
if (!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIFrameType) && \
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PySceneType) && \
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIGridType) && \
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIGridViewType)) { \
PyErr_SetString(PyExc_TypeError, "parent must be a Frame, Scene, or Grid"); \
return -1; \
} \
PyObject* _children = PyObject_GetAttrString((parent_obj), "children"); \
if (!_children) { \
return -1; \
} \
PyObject* _result = PyObject_CallMethod(_children, "append", "O", (PyObject*)(self)); \
Py_DECREF(_children); \
if (!_result) { \
return -1; \
} \
Py_DECREF(_result); \
} \
} while (0)
// Property getters/setters for visible and opacity // Property getters/setters for visible and opacity
template<typename T> template<typename T>
static PyObject* UIDrawable_get_visible(T* self, void* closure) static PyObject* UIDrawable_get_visible(T* self, void* closure)

View file

@ -7,6 +7,9 @@
#include "PyAlignment.h" #include "PyAlignment.h"
#include "PyShader.h" // #106: Shader support #include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support #include "PyUniformCollection.h" // #106: Uniform collection support
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
// UIDrawable methods now in UIBase.h // UIDrawable methods now in UIBase.h
#include <algorithm> #include <algorithm>
@ -448,23 +451,27 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
float margin = 0.0f; float margin = 0.0f;
float horiz_margin = -1.0f; float horiz_margin = -1.0f;
float vert_margin = -1.0f; float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
// Keywords list matches the new spec: positional args first, then all keyword args // Keywords list: pos and text are positional-or-keyword. Everything after
// the '$' separator (font and friends) is keyword-only.
static const char* kwlist[] = { static const char* kwlist[] = {
"pos", "font", "text", // Positional args (as per spec) "pos", "text",
// Keyword-only args // Keyword-only args follow:
"fill_color", "outline_color", "outline", "font_size", "on_click", "font", "fill_color", "outline_color", "outline", "font_size", "on_click",
"visible", "opacity", "z_index", "name", "x", "y", "visible", "opacity", "z_index", "name", "x", "y",
"align", "margin", "horiz_margin", "vert_margin", "align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr nullptr
}; };
// Parse arguments with | for optional positional args // '$' marker makes all following args keyword-only (Python 3.3+ format extension).
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizffOfff", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oz$OOOffOifizffOfffO", const_cast<char**>(kwlist),
&pos_obj, &font, &text, // Positional &pos_obj, &text, // pos+text are positional-or-keyword
&fill_color, &outline_color, &outline, &font_size, &click_handler, &font, &fill_color, &outline_color, &outline, &font_size, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y, &visible, &opacity, &z_index, &name, &x, &y,
&align_obj, &margin, &horiz_margin, &vert_margin)) { &align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1; return -1;
} }
@ -597,6 +604,9 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
// #184: Check if this is a Python subclass (for callback method support) // #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICaptionType; self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICaptionType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0; return 0;
} }

View file

@ -5,6 +5,10 @@
#include "PyColor.h" #include "PyColor.h"
#include "PythonObjectCache.h" #include "PythonObjectCache.h"
#include "PyAlignment.h" #include "PyAlignment.h"
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include <cmath> #include <cmath>
UICircle::UICircle() UICircle::UICircle()
@ -479,10 +483,13 @@ PyObject* UICircle::repr(PyUICircleObject* self) {
} }
int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) { int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
// 1.0 API freeze: positional order is now (center, radius, ...). The old
// (radius, center) ordering is no longer supported.
static const char* kwlist[] = { static const char* kwlist[] = {
"radius", "center", "fill_color", "outline_color", "outline", "center", "radius", "fill_color", "outline_color", "outline",
"on_click", "visible", "opacity", "z_index", "name", "on_click", "visible", "opacity", "z_index", "name",
"align", "margin", "horiz_margin", "vert_margin", NULL "align", "margin", "horiz_margin", "vert_margin",
"parent", NULL
}; };
float radius = 10.0f; float radius = 10.0f;
@ -501,11 +508,13 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
float margin = 0.0f; float margin = 0.0f;
float horiz_margin = -1.0f; float horiz_margin = -1.0f;
float vert_margin = -1.0f; float vert_margin = -1.0f;
PyObject* parent_obj = NULL; // Auto-attach parent (Frame, Scene, or Grid)
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfisOfff", (char**)kwlist, if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfOOfOpfisOfffO", (char**)kwlist,
&radius, &center_obj, &fill_color_obj, &outline_color_obj, &outline, &center_obj, &radius, &fill_color_obj, &outline_color_obj, &outline,
&click_obj, &visible, &opacity_val, &z_index, &name, &click_obj, &visible, &opacity_val, &z_index, &name,
&align_obj, &margin, &horiz_margin, &vert_margin)) { &align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1; return -1;
} }
@ -597,5 +606,8 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
// #184: Check if this is a Python subclass (for callback method support) // #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICircleType; self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICircleType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0; return 0;
} }

View file

@ -5,6 +5,7 @@
#include "UICaption.h" #include "UICaption.h"
#include "UISprite.h" #include "UISprite.h"
#include "UIGrid.h" #include "UIGrid.h"
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PythonObjectCache.h" #include "PythonObjectCache.h"
#include "PyAlignment.h" #include "PyAlignment.h"
@ -601,6 +602,7 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
float margin = 0.0f; float margin = 0.0f;
float horiz_margin = -1.0f; float horiz_margin = -1.0f;
float vert_margin = -1.0f; float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
// Keywords list matches the new spec: positional args first, then all keyword args // Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = { static const char* kwlist[] = {
@ -609,15 +611,17 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
"fill_color", "outline_color", "outline", "children", "on_click", "fill_color", "outline_color", "outline", "children", "on_click",
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree", "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree",
"align", "margin", "horiz_margin", "vert_margin", "align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr nullptr
}; };
// Parse arguments with | for optional positional args // Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfff", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfffO", const_cast<char**>(kwlist),
&pos_obj, &size_obj, // Positional &pos_obj, &size_obj, // Positional
&fill_color, &outline_color, &outline, &children_arg, &click_handler, &fill_color, &outline_color, &outline, &children_arg, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree, &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree,
&align_obj, &margin, &horiz_margin, &vert_margin)) { &align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1; return -1;
} }
@ -798,6 +802,9 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
// #184: Check if this is a Python subclass (for callback method support) // #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIFrameType; self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIFrameType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0; return 0;
} }

View file

@ -82,10 +82,19 @@ PyObject* UIGrid::subscript(PyUIGridObject* self, PyObject* key)
return (PyObject*)obj; return (PyObject*)obj;
} }
// Setitem on _GridData / Grid: GridPoints are views, not assignable.
static int UIGrid_subscript_assign(PyUIGridObject* self, PyObject* key, PyObject* value)
{
(void)self; (void)key; (void)value;
PyErr_SetString(PyExc_TypeError,
"Grid points are not assignable; modify properties on the returned point");
return -1;
}
PyMappingMethods UIGrid::mpmethods = { PyMappingMethods UIGrid::mpmethods = {
.mp_length = NULL, .mp_length = NULL,
.mp_subscript = (binaryfunc)UIGrid::subscript, .mp_subscript = (binaryfunc)UIGrid::subscript,
.mp_ass_subscript = NULL .mp_ass_subscript = (objobjargproc)UIGrid_subscript_assign,
}; };
// ========================================================================= // =========================================================================

View file

@ -1,6 +1,7 @@
// UIGridView.cpp - Rendering view for GridData (#252) // UIGridView.cpp - Rendering view for GridData (#252)
#include "UIGridView.h" #include "UIGridView.h"
#include "UIGrid.h" #include "UIGrid.h"
#include "UIGridPoint.h"
#include "UIEntity.h" #include "UIEntity.h"
#include "GameEngine.h" #include "GameEngine.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
@ -11,6 +12,7 @@
#include "PyPositionHelper.h" #include "PyPositionHelper.h"
#include "PyVector.h" #include "PyVector.h"
#include "PythonObjectCache.h" #include "PythonObjectCache.h"
#include "PySceneObject.h" // parent= kwarg: Scene is a valid parent type
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
@ -371,15 +373,45 @@ bool UIGridView::hasProperty(const std::string& name) const
int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds) int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
{ {
// Extract parent= up front so it doesn't confuse downstream parsing in
// either init mode. parent_obj is borrowed from the original kwds (which
// the caller owns and outlives this function), so no INCREF is needed --
// but we must not delete from the caller's dict, so make a working copy.
PyObject* parent_obj = nullptr;
PyObject* dispatch_kwds = kwds;
if (kwds) {
parent_obj = PyDict_GetItemString(kwds, "parent"); // borrowed ref
if (parent_obj) {
dispatch_kwds = PyDict_Copy(kwds);
if (!dispatch_kwds) return -1;
PyDict_DelItemString(dispatch_kwds, "parent");
}
}
// Determine mode by checking for 'grid' kwarg // Determine mode by checking for 'grid' kwarg
PyObject* grid_kwarg = nullptr; PyObject* grid_kwarg = nullptr;
if (kwds) { if (dispatch_kwds) {
grid_kwarg = PyDict_GetItemString(kwds, "grid"); // borrowed ref grid_kwarg = PyDict_GetItemString(dispatch_kwds, "grid"); // borrowed ref
} }
bool explicit_view = (grid_kwarg && grid_kwarg != Py_None); bool explicit_view = (grid_kwarg && grid_kwarg != Py_None);
if (explicit_view) { int rc = explicit_view
? init_explicit_view(self, args, dispatch_kwds)
: init_with_data(self, args, dispatch_kwds);
if (dispatch_kwds != kwds) Py_DECREF(dispatch_kwds);
if (rc != 0) return rc;
if (parent_obj) {
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
}
return 0;
}
int UIGridView::init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
{
{
// Mode 1: View of existing grid data // Mode 1: View of existing grid data
static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr}; static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr};
PyObject* grid_obj = nullptr; PyObject* grid_obj = nullptr;
@ -453,9 +485,6 @@ int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType; self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType;
return 0; return 0;
} else {
// Mode 2: Factory mode - create UIGrid internally
return init_with_data(self, args, kwds);
} }
} }
@ -780,6 +809,69 @@ int UIGridView::set_float_member_gv(PyUIGridViewObject* self, PyObject* value, v
return 0; return 0;
} }
// =========================================================================
// Subscript protocol: grid[x, y] -> GridPoint (delegates to GridData).
// Setitem raises TypeError (GridPoints are views, not assignable).
// =========================================================================
PyObject* UIGridView::subscript(PyUIGridViewObject* self, PyObject* key)
{
if (!self->data || !self->data->grid_data) {
PyErr_SetString(PyExc_RuntimeError, "Grid has no underlying data");
return NULL;
}
if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) {
PyErr_SetString(PyExc_TypeError, "Grid indices must be a 2-tuple (x, y)");
return NULL;
}
PyObject* x_obj = PyTuple_GetItem(key, 0);
PyObject* y_obj = PyTuple_GetItem(key, 1);
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
PyErr_SetString(PyExc_TypeError, "Grid indices must be integers");
return NULL;
}
int x = (int)PyLong_AsLong(x_obj);
int y = (int)PyLong_AsLong(y_obj);
auto& grid_data = self->data->grid_data;
if (x < 0 || x >= grid_data->grid_w) {
PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)",
x, grid_data->grid_w);
return NULL;
}
if (y < 0 || y >= grid_data->grid_h) {
PyErr_Format(PyExc_IndexError, "y index %d is out of range [0, %d)",
y, grid_data->grid_h);
return NULL;
}
// Reconstruct shared_ptr<UIGrid> from GridData via aliasing constructor
// (mirrors UIGridView::get_grid).
auto grid_ptr = static_cast<UIGrid*>(grid_data.get());
auto grid_as_uigrid = std::shared_ptr<UIGrid>(grid_data, grid_ptr);
auto type = &mcrfpydef::PyUIGridPointType;
auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0);
if (!obj) return NULL;
obj->grid = grid_as_uigrid;
obj->x = x;
obj->y = y;
return (PyObject*)obj;
}
int UIGridView::subscript_assign(PyUIGridViewObject* self, PyObject* key, PyObject* value)
{
(void)self; (void)key; (void)value;
PyErr_SetString(PyExc_TypeError,
"Grid points are not assignable; modify properties on the returned point");
return -1;
}
PyMappingMethods UIGridView::mpmethods = {
.mp_length = NULL,
.mp_subscript = (binaryfunc)UIGridView::subscript,
.mp_ass_subscript = (objobjargproc)UIGridView::subscript_assign,
};
// #252: PyObjectType typedef for UIDRAWABLE_* macros // #252: PyObjectType typedef for UIDRAWABLE_* macros
typedef PyUIGridViewObject PyObjectType; typedef PyUIGridViewObject PyObjectType;

View file

@ -86,6 +86,7 @@ public:
// Python API // Python API
// ========================================================================= // =========================================================================
static int init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds); static int init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
static int init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
static int init_with_data(PyUIGridViewObject* self, PyObject* args, PyObject* kwds); static int init_with_data(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
static PyObject* repr(PyUIGridViewObject* self); static PyObject* repr(PyUIGridViewObject* self);
@ -110,6 +111,12 @@ public:
static PyMethodDef methods[]; static PyMethodDef methods[];
static PyGetSetDef getsetters[]; static PyGetSetDef getsetters[];
// Subscript protocol: grid[x, y] (delegates to underlying GridData).
// Setitem raises TypeError (GridPoints are views).
static PyObject* subscript(PyUIGridViewObject* self, PyObject* key);
static int subscript_assign(PyUIGridViewObject* self, PyObject* key, PyObject* value);
static PyMappingMethods mpmethods;
}; };
// Forward declaration of methods array // Forward declaration of methods array
@ -139,6 +146,7 @@ namespace mcrfpydef {
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
}, },
.tp_repr = (reprfunc)UIGridView::repr, .tp_repr = (reprfunc)UIGridView::repr,
.tp_as_mapping = &UIGridView::mpmethods, // grid[x, y] (delegates to GridData)
.tp_getattro = UIGridView::getattro, // #252: attribute delegation to Grid .tp_getattro = UIGridView::getattro, // #252: attribute delegation to Grid
.tp_setattro = UIGridView::setattro, // #252: attribute delegation to Grid .tp_setattro = UIGridView::setattro, // #252: attribute delegation to Grid
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,

View file

@ -5,6 +5,10 @@
#include "PyColor.h" #include "PyColor.h"
#include "PythonObjectCache.h" #include "PythonObjectCache.h"
#include "PyAlignment.h" #include "PyAlignment.h"
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include <cmath> #include <cmath>
UILine::UILine() UILine::UILine()
@ -565,18 +569,21 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
float margin = 0.0f; float margin = 0.0f;
float horiz_margin = -1.0f; float horiz_margin = -1.0f;
float vert_margin = -1.0f; float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
static const char* kwlist[] = { static const char* kwlist[] = {
"start", "end", "thickness", "color", "start", "end", "thickness", "color",
"on_click", "visible", "opacity", "z_index", "name", "on_click", "visible", "opacity", "z_index", "name",
"align", "margin", "horiz_margin", "vert_margin", "align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr nullptr
}; };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfff", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfffO", const_cast<char**>(kwlist),
&start_obj, &end_obj, &thickness, &color_obj, &start_obj, &end_obj, &thickness, &color_obj,
&click_handler, &visible, &opacity, &z_index, &name, &click_handler, &visible, &opacity, &z_index, &name,
&align_obj, &margin, &horiz_margin, &vert_margin)) { &align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1; return -1;
} }
@ -663,5 +670,8 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
// #184: Check if this is a Python subclass (for callback method support) // #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUILineType; self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUILineType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0; return 0;
} }

View file

@ -3,6 +3,9 @@
#include "PyVector.h" #include "PyVector.h"
#include "PythonObjectCache.h" #include "PythonObjectCache.h"
#include "UIFrame.h" // #144: For snapshot= parameter #include "UIFrame.h" // #144: For snapshot= parameter
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include "PyAlignment.h" #include "PyAlignment.h"
#include "PyShader.h" // #106: Shader support #include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support #include "PyUniformCollection.h" // #106: Uniform collection support
@ -476,6 +479,7 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
float margin = 0.0f; float margin = 0.0f;
float horiz_margin = -1.0f; float horiz_margin = -1.0f;
float vert_margin = -1.0f; float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
// Keywords list matches the new spec: positional args first, then all keyword args // Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = { static const char* kwlist[] = {
@ -484,15 +488,17 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
"scale", "scale_x", "scale_y", "on_click", "scale", "scale_x", "scale_y", "on_click",
"visible", "opacity", "z_index", "name", "x", "y", "snapshot", "visible", "opacity", "z_index", "name", "x", "y", "snapshot",
"align", "margin", "horiz_margin", "vert_margin", "align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr nullptr
}; };
// Parse arguments with | for optional positional args // Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfff", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfffO", const_cast<char**>(kwlist),
&pos_obj, &texture, &sprite_index, // Positional &pos_obj, &texture, &sprite_index, // Positional
&scale, &scale_x, &scale_y, &click_handler, &scale, &scale_x, &scale_y, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y, &snapshot, &visible, &opacity, &z_index, &name, &x, &y, &snapshot,
&align_obj, &margin, &horiz_margin, &vert_margin)) { &align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
return -1; return -1;
} }
@ -627,6 +633,9 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
// #184: Check if this is a Python subclass (for callback method support) // #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUISpriteType; self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUISpriteType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0; return 0;
} }

View file

@ -92,6 +92,14 @@ class FOV(IntEnum):
SHADOW: int SHADOW: int
SYMMETRIC_SHADOWCAST: int SYMMETRIC_SHADOWCAST: int
class Heuristic(IntEnum):
"""Built-in A* heuristic function selector."""
CHEBYSHEV: int
DIAGONAL: int
EUCLIDEAN: int
MANHATTAN: int
ZERO: int
class InputState(IntEnum): class InputState(IntEnum):
"""Enum representing input event states (pressed/released).""" """Enum representing input event states (pressed/released)."""
PRESSED: int PRESSED: int
@ -255,34 +263,6 @@ class AStarPath:
"""Get and consume next step in the path.""" """Get and consume next step in the path."""
... ...
class Animation:
"""Create an animation that interpolates a property value over time."""
def __init__(self, property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, loop: bool = False, callback: Callable = None) -> None: ...
duration: float # Animation duration in seconds (float, read-only). Total time for the animation to complete.
elapsed: float # Elapsed time in seconds (float, read-only). Time since the animation started.
is_complete: bool # Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called.
is_delta: bool # Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.
is_looping: bool # Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end.
property: str # Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index').
def complete(self) -> None:
"""Complete the animation immediately by jumping to the final value."""
...
def get_current_value(self) -> Any:
"""Get the current interpolated value of the animation."""
...
def hasValidTarget(self) -> bool:
"""Check if the animation still has a valid target."""
...
def start(self, target: UIDrawable, conflict_mode: str = 'replace') -> None:
"""Start the animation on a target UI element."""
...
def stop(self) -> None:
"""Stop the animation without completing it."""
...
def update(self, delta_time: float) -> bool:
"""Update the animation by the given time delta."""
...
class Arc: class Arc:
"""An arc UI element for drawing curved line segments.""" """An arc UI element for drawing curved line segments."""
def __init__(self, center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs) -> None: ... def __init__(self, center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs) -> None: ...
@ -558,9 +538,15 @@ class DijkstraMap:
"""A Dijkstra distance map from a fixed root position.""" """A Dijkstra distance map from a fixed root position."""
def __init__(self, *args, **kwargs) -> None: ... def __init__(self, *args, **kwargs) -> None: ...
root: Vector # Root position that distances are measured from (Vector, read-only). root: Vector # Root position that distances are measured from (Vector, read-only).
def descent_step(self, pos) -> Vector | None:
"""Get the adjacent cell with the lowest distance (steepest descent)."""
...
def distance(self, pos) -> float | None: def distance(self, pos) -> float | None:
"""Get distance from position to root.""" """Get distance from position to root."""
... ...
def invert(self) -> DijkstraMap:
"""Return a NEW DijkstraMap whose distance field is the safety field."""
...
def path_from(self, pos) -> AStarPath: def path_from(self, pos) -> AStarPath:
"""Get full path from position to root.""" """Get full path from position to root."""
... ...

463
tools/audit_pymethoddef.py Executable file
View file

@ -0,0 +1,463 @@
#!/usr/bin/env python3
"""
audit_pymethoddef.py - Static-analysis tool for McRogueFace Python bindings.
Walks src/**/*.cpp, parses each file with tree-sitter-cpp, and locates every
`PyMethodDef <name>[] = {...}` and `PyGetSetDef <name>[] = {...}` declaration.
For each entry inside those array initializers, classifies the docstring slot:
MACRO - uses MCRF_METHOD(...) or MCRF_PROPERTY(...)
RAW_STRING - inline C string literal (or concatenated string literals)
NULL - explicit NULL literal
MISSING - entry too short to have a doc field (probably malformed)
The `MACRO` classification is the project's compliance target. RAW_STRING and
NULL entries should be migrated to the macro system before the 1.0 API freeze.
Sentinel terminator entries (e.g. `{NULL}`, `{0}`) are skipped.
Usage:
python3 tools/audit_pymethoddef.py [--strict] [--quiet]
[--paths PATH [PATH ...]]
Flags:
--strict Exit nonzero if any non-MACRO entries are found (CI mode).
--quiet Suppress per-file output, print only the summary.
--paths Restrict scan to the given files/directories. Defaults to src/.
"""
from __future__ import annotations
import argparse
import os
import sys
from collections import Counter, defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Optional, Tuple
try:
import tree_sitter_cpp
from tree_sitter import Language, Parser
except ImportError as e:
sys.stderr.write(
"ERROR: tree-sitter / tree-sitter-cpp not installed.\n"
"Activate the audit venv first:\n"
" source .venv-audit/bin/activate\n"
f"(import error: {e})\n"
)
sys.exit(2)
# ---------------------------------------------------------------------------
# Tree-sitter setup
# ---------------------------------------------------------------------------
_LANG = Language(tree_sitter_cpp.language())
_PARSER = Parser(_LANG)
# Indices of the docstring field in the struct initializer.
# PyMethodDef: {ml_name, ml_meth, ml_flags, ml_doc} -> idx 3
# PyGetSetDef: {name, get, set, doc, closure} -> idx 3
DOC_FIELD_INDEX = 3
EXPECTED_MIN_FIELDS = {
"PyMethodDef": 4, # need at least 4 to have ml_doc
"PyGetSetDef": 4, # need at least 4 to have doc (closure can be omitted)
}
# Punctuation/structural child node types we ignore when walking entry fields.
_PUNCT_TYPES = {"{", "}", ",", "(", ")", "[", "]", ";"}
# Macros that mark a docstring slot as compliant.
_COMPLIANT_MACROS = {"MCRF_METHOD", "MCRF_PROPERTY"}
# ---------------------------------------------------------------------------
# Data records
# ---------------------------------------------------------------------------
@dataclass
class EntryRecord:
file_path: Path
line: int # 1-based
array_kind: str # "PyMethodDef" or "PyGetSetDef"
array_name: str # e.g. "PyAnimation::methods"
entry_name: str # ml_name / name string, or "<unknown>"
classification: str # MACRO / RAW_STRING / NULL / MISSING
# ---------------------------------------------------------------------------
# Tree-sitter helpers
# ---------------------------------------------------------------------------
def _node_text(src: bytes, node) -> str:
return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
def _meaningful_children(node) -> List:
"""Return children of an initializer_list, skipping punctuation tokens."""
return [c for c in node.children if c.type not in _PUNCT_TYPES]
def _classify_doc_field(src: bytes, doc_node) -> str:
"""Map a docstring AST node to a classification string."""
t = doc_node.type
if t == "null":
return "NULL"
if t in ("string_literal", "concatenated_string", "raw_string_literal"):
return "RAW_STRING"
if t == "call_expression":
# The first child of call_expression is the callee identifier.
callee = doc_node.child_by_field_name("function")
if callee is None and doc_node.children:
callee = doc_node.children[0]
if callee is not None:
name = _node_text(src, callee).strip()
if name in _COMPLIANT_MACROS:
return "MACRO"
return "RAW_STRING" # call expression to something non-MACRO
if t == "identifier":
# Bare identifier in the doc slot - could be a #define alias. Treat as
# raw (non-compliant) so the user investigates it.
text = _node_text(src, doc_node).strip()
if text == "NULL":
return "NULL"
return "RAW_STRING"
# Anything else (parenthesized_expression, etc.) - inspect text fallback.
text = _node_text(src, doc_node).strip()
stripped = text.lstrip("(").lstrip()
for macro in _COMPLIANT_MACROS:
if stripped.startswith(macro + "("):
return "MACRO"
if stripped == "NULL":
return "NULL"
return "RAW_STRING"
def _entry_name(src: bytes, entry_node) -> str:
"""Return the first string literal in an entry initializer (the ml_name)."""
for c in _meaningful_children(entry_node):
if c.type == "string_literal":
# string_literal contains string_content children
for sub in c.children:
if sub.type == "string_content":
return _node_text(src, sub)
return _node_text(src, c).strip('"')
if c.type == "concatenated_string":
for lit in c.children:
if lit.type == "string_literal":
for sub in lit.children:
if sub.type == "string_content":
return _node_text(src, sub)
return _node_text(src, c)
# Stop at the first non-string field - the name should come first.
break
return "<unknown>"
def _is_sentinel(src: bytes, entry_node) -> bool:
"""True if the entry looks like a sentinel terminator (e.g. {NULL} / {0})."""
fields = _meaningful_children(entry_node)
if not fields:
return True
if len(fields) == 1:
only = fields[0]
if only.type == "null":
return True
if only.type == "number_literal" and _node_text(src, only).strip() == "0":
return True
# Some codebases write {NULL, NULL, NULL, NULL}. Treat all-NULL/0 as sentinel.
for f in fields:
if f.type == "null":
continue
if f.type == "number_literal" and _node_text(src, f).strip() == "0":
continue
return False
return True
def _array_name_from_init_declarator(src: bytes, init_decl) -> Optional[str]:
"""Extract the array name from an init_declarator containing array_declarator."""
arr = None
for c in init_decl.children:
if c.type == "array_declarator":
arr = c
break
if arr is None:
return None
# The first child is the declarator (identifier or qualified_identifier).
for c in arr.children:
if c.type in ("identifier", "qualified_identifier", "field_identifier"):
return _node_text(src, c)
return None
def _outer_initializer_list(init_decl):
"""Get the top-level initializer_list child of an init_declarator, if any."""
for c in init_decl.children:
if c.type == "initializer_list":
return c
return None
# ---------------------------------------------------------------------------
# Per-file scan
# ---------------------------------------------------------------------------
def _walk_declarations(node, out: list) -> None:
"""Collect all `declaration` nodes under `node` (recursive)."""
if node.type == "declaration":
out.append(node)
for c in node.children:
_walk_declarations(c, out)
def scan_file(path: Path) -> List[EntryRecord]:
try:
src = path.read_bytes()
except OSError as e:
sys.stderr.write(f"WARNING: cannot read {path}: {e}\n")
return []
tree = _PARSER.parse(src)
decls: list = []
_walk_declarations(tree.root_node, decls)
records: List[EntryRecord] = []
for decl in decls:
# Find the type_identifier child to determine if this is one of ours.
type_kind = None
for c in decl.children:
if c.type == "type_identifier":
txt = _node_text(src, c).strip()
if txt in EXPECTED_MIN_FIELDS:
type_kind = txt
break
if type_kind is None:
continue
# Each declaration may have multiple init_declarators (rare for arrays
# but cheap to handle).
for c in decl.children:
if c.type != "init_declarator":
continue
outer_init = _outer_initializer_list(c)
if outer_init is None:
continue # forward decl or extern - no initializer
array_name = _array_name_from_init_declarator(src, c) or "<anon>"
# Each direct child initializer_list is an entry.
for entry in outer_init.children:
if entry.type != "initializer_list":
continue
if _is_sentinel(src, entry):
continue
fields = _meaningful_children(entry)
line = entry.start_point[0] + 1 # tree-sitter is 0-based
name = _entry_name(src, entry)
if len(fields) <= DOC_FIELD_INDEX:
records.append(EntryRecord(
file_path=path,
line=line,
array_kind=type_kind,
array_name=array_name,
entry_name=name,
classification="MISSING",
))
continue
doc_node = fields[DOC_FIELD_INDEX]
classification = _classify_doc_field(src, doc_node)
records.append(EntryRecord(
file_path=path,
line=line,
array_kind=type_kind,
array_name=array_name,
entry_name=name,
classification=classification,
))
return records
# ---------------------------------------------------------------------------
# Path resolution
# ---------------------------------------------------------------------------
def _iter_cpp_files(roots: Iterable[Path]) -> Iterable[Path]:
for root in roots:
if root.is_file():
if root.suffix == ".cpp":
yield root
continue
if not root.exists():
sys.stderr.write(f"WARNING: path does not exist: {root}\n")
continue
for dirpath, _dirnames, filenames in os.walk(root):
for fn in filenames:
if fn.endswith(".cpp"):
yield Path(dirpath) / fn
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def _print_file_table(records: List[EntryRecord], project_root: Path) -> None:
by_file: dict[Path, List[EntryRecord]] = defaultdict(list)
for r in records:
by_file[r.file_path].append(r)
for path in sorted(by_file):
try:
rel = path.relative_to(project_root)
except ValueError:
rel = path
entries = sorted(by_file[path], key=lambda r: r.line)
# Compute column widths for this file.
loc_w = max(len(f"{rel}:{e.line}") for e in entries)
arr_w = max(len(e.array_name) for e in entries)
ent_w = max(len(e.entry_name) for e in entries)
loc_w = max(loc_w, len("file:line"))
arr_w = max(arr_w, len("array"))
ent_w = max(ent_w, len("entry"))
header = (
f"{'file:line':<{loc_w}} "
f"{'array':<{arr_w}} "
f"{'entry':<{ent_w}} "
f"classification"
)
print(header)
print("-" * len(header))
for e in entries:
loc = f"{rel}:{e.line}"
print(
f"{loc:<{loc_w}} "
f"{e.array_name:<{arr_w}} "
f"{e.entry_name:<{ent_w}} "
f"{e.classification}"
)
print()
def _print_summary(records: List[EntryRecord]) -> None:
total = len(records)
counts = Counter(r.classification for r in records)
macro = counts.get("MACRO", 0)
raw = counts.get("RAW_STRING", 0)
null = counts.get("NULL", 0)
missing = counts.get("MISSING", 0)
pct = (macro / total * 100.0) if total else 0.0
print("=" * 60)
print("PyMethodDef / PyGetSetDef Documentation Audit Summary")
print("=" * 60)
print(f"Total entries scanned : {total}")
print(f" MACRO compliant : {macro}")
print(f" RAW_STRING : {raw}")
print(f" NULL : {null}")
print(f" MISSING : {missing}")
print(f"MACRO compliance : {pct:.1f}%")
# Per-kind breakdown.
by_kind = defaultdict(Counter)
for r in records:
by_kind[r.array_kind][r.classification] += 1
if by_kind:
print()
print("Breakdown by kind:")
for kind in sorted(by_kind):
kc = by_kind[kind]
kt = sum(kc.values())
kp = (kc.get("MACRO", 0) / kt * 100.0) if kt else 0.0
print(
f" {kind:<13} total={kt:<4} "
f"MACRO={kc.get('MACRO', 0):<4} "
f"RAW={kc.get('RAW_STRING', 0):<4} "
f"NULL={kc.get('NULL', 0):<4} "
f"MISSING={kc.get('MISSING', 0):<4} "
f"({kp:.1f}% compliant)"
)
# Top offenders.
offenders: dict[Path, int] = defaultdict(int)
for r in records:
if r.classification != "MACRO":
offenders[r.file_path] += 1
if offenders:
print()
print("Top non-compliant files:")
ranked = sorted(offenders.items(), key=lambda kv: kv[1], reverse=True)
for path, count in ranked[:10]:
try:
rel = path.relative_to(Path.cwd())
except ValueError:
rel = path
print(f" {count:>4} {rel}")
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def _default_roots() -> List[Path]:
cwd = Path.cwd()
src = cwd / "src"
return [src] if src.exists() else [cwd]
def main(argv: Optional[List[str]] = None) -> int:
parser = argparse.ArgumentParser(
description=(
"Audit PyMethodDef / PyGetSetDef entries in McRogueFace C++ "
"sources for MCRF_METHOD / MCRF_PROPERTY documentation macro use."
),
)
parser.add_argument(
"--strict", action="store_true",
help="Exit nonzero if any non-MACRO entries are found (CI mode)."
)
parser.add_argument(
"--quiet", action="store_true",
help="Print only the summary, omit per-file tables."
)
parser.add_argument(
"--paths", nargs="+", type=Path, default=None,
help="Files or directories to scan (default: ./src)."
)
args = parser.parse_args(argv)
roots = args.paths if args.paths else _default_roots()
project_root = Path.cwd()
files = sorted(set(_iter_cpp_files(roots)))
if not files:
sys.stderr.write("WARNING: no .cpp files found.\n")
return 0
all_records: List[EntryRecord] = []
for f in files:
all_records.extend(scan_file(f))
if not args.quiet:
if all_records:
_print_file_table(all_records, project_root)
else:
print("(no PyMethodDef / PyGetSetDef arrays found)")
print()
_print_summary(all_records)
if args.strict:
non_macro = sum(
1 for r in all_records if r.classification != "MACRO"
)
if non_macro:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,16 @@
import mcrfpy
from mcrfpy import automation
import runpy, sys, pathlib
OUT = sys.argv[1] if len(sys.argv) > 1 else "image00.png"
demo_path = (pathlib.Path(__file__).resolve().parent.parent /
"tests" / "demo" / "audio_synth_demo.py")
runpy.run_path(str(demo_path), run_name="__demo__")
def shot(timer, runtime):
automation.screenshot(OUT)
print(f"Wrote {OUT}")
sys.exit(0)
mcrfpy.Timer("header_shot", shot, 150)

View file

@ -26,3 +26,24 @@ echo " HTML: docs/api_reference_dynamic.html"
echo " Markdown: docs/API_REFERENCE_DYNAMIC.md" echo " Markdown: docs/API_REFERENCE_DYNAMIC.md"
echo " Man page: docs/mcrfpy.3" echo " Man page: docs/mcrfpy.3"
echo " Stubs: stubs/mcrfpy.pyi" echo " Stubs: stubs/mcrfpy.pyi"
# ---------------------------------------------------------------------------
# Static-analysis audit: report MCRF_METHOD / MCRF_PROPERTY macro compliance
# across every PyMethodDef / PyGetSetDef array in src/. This is informational
# only (no --strict) so it cannot break the doc build, but the summary makes
# pre-1.0 documentation drift visible alongside doc generation.
#
# Requires the .venv-audit virtual environment with tree-sitter +
# tree-sitter-cpp installed. The audit is skipped silently if absent so
# contributors without the venv aren't blocked.
# ---------------------------------------------------------------------------
if [ -x "./.venv-audit/bin/python3" ] && [ -f "./tools/audit_pymethoddef.py" ]; then
echo ""
echo "=== PyMethodDef / PyGetSetDef Macro Compliance Audit ==="
./.venv-audit/bin/python3 ./tools/audit_pymethoddef.py --quiet || true
elif [ -f "./tools/audit_pymethoddef.py" ]; then
echo ""
echo "(skipping audit_pymethoddef.py: .venv-audit not found - run"
echo " 'python3 -m venv .venv-audit && .venv-audit/bin/pip install"
echo " tree-sitter tree-sitter-cpp' to enable)"
fi