Fix verify-pass code bugs #317/#318/#319

Three small bugs surfaced by the #314 docstring-accuracy verify pass:

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

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

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

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

closes #317
closes #318
closes #319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
This commit is contained in:
John McCardle 2026-06-21 10:12:41 -04:00
commit 98489a96fd
12 changed files with 303 additions and 55 deletions

View file

@ -1,6 +1,6 @@
# McRogueFace API Reference
*Generated on 2026-06-21 09:39:04*
*Generated on 2026-06-21 10:11:50*
*This documentation was dynamically generated from the compiled module.*
@ -1989,19 +1989,17 @@ Note:
**Returns:** None Called automatically when the entity moves if the grid has FOV configured.
#### `visible_entities(fov=None, radius: int = -1) -> list[Entity]`
#### `visible_entities(fov=None, radius: int | None = None) -> list[Entity]`
Get list of other entities visible from this entity's position.
Note:
**Arguments:**
- `fov`: FOV algorithm to use (FOV enum or None to use grid.fov)
- `radius`: FOV radius as int; omit or pass -1 to use the grid's default fov_radius
- `radius`: FOV radius as int; pass None, omit, or pass -1 to use the grid's default fov_radius
**Returns:** List of Entity objects within field of view, excluding self
**Raises:** ValueError: If entity is not associated with a grid radius does not accept None; omit the argument entirely to use the grid default.
**Raises:** ValueError: If entity is not associated with a grid TypeError: If radius is neither an int nor None
### Entity3D
@ -2441,7 +2439,7 @@ Keyword Args:
- `rotate_with_camera`: Whether to rotate visually with parent Grid's camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.
- `rotation`: Rotation angle in degrees (clockwise around origin). Animatable property.
- `shader`: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.
- `texture` *(read-only)*: Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None.
- `texture` *(read-only)*: Texture used for tile rendering (Texture | None, read-only).
- `uniforms` *(read-only)*: Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.
- `vert_margin`: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
@ -2561,7 +2559,7 @@ Keyword Args:
- `rotate_with_camera`: Whether to rotate visually with parent Grid's camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.
- `rotation`: Rotation angle in degrees (clockwise around origin). Animatable property.
- `shader`: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.
- `texture` *(read-only)*: Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None.
- `texture` *(read-only)*: Texture used for tile rendering (Texture | None, read-only).
- `uniforms` *(read-only)*: Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.
- `vert_margin`: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.

View file

@ -108,7 +108,7 @@
<body>
<div class="container">
<h1>McRogueFace API Reference</h1>
<p><em>Generated on 2026-06-21 09:39:04</em></p>
<p><em>Generated on 2026-06-21 10:11:50</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
<div class="toc">
@ -2144,16 +2144,14 @@ Note:</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">visible_entities(fov=None, radius: int = -1) -> list[Entity]</code></h5>
<p>Get list of other entities visible from this entity&#x27;s position.
Note:</p>
<h5><code class="method-name">visible_entities(fov=None, radius: int | None = None) -> list[Entity]</code></h5>
<p>Get list of other entities visible from this entity&#x27;s position.</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>fov</span>: FOV algorithm to use (FOV enum or None to use grid.fov)</div>
<div><span class='arg-name'>radius</span>: FOV radius as int; omit or pass -1 to use the grid&#x27;s default fov_radius</div>
<div><span class='arg-name'>radius</span>: FOV radius as int; pass None, omit, or pass -1 to use the grid&#x27;s default fov_radius</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> List of Entity objects within field of view, excluding self</p>
<p style='margin-left: 20px;'><span class='raises'>Raises:</span> ValueError: If entity is not associated with a grid radius does not accept None; omit the argument entirely to use the grid default.</p>
<p style='margin-left: 20px;'><span class='raises'>Raises:</span> ValueError: If entity is not associated with a grid TypeError: If radius is neither an int nor None</p>
</div>
</div>
@ -2619,7 +2617,7 @@ Keyword Args:
<li><span class='property-name'>rotate_with_camera</span>: Whether to rotate visually with parent Grid&#x27;s camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.</li>
<li><span class='property-name'>rotation</span>: Rotation angle in degrees (clockwise around origin). Animatable property.</li>
<li><span class='property-name'>shader</span>: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.</li>
<li><span class='property-name'>texture</span> (read-only): Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None.</li>
<li><span class='property-name'>texture</span> (read-only): Texture used for tile rendering (Texture | None, read-only).</li>
<li><span class='property-name'>uniforms</span> (read-only): Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms[&#x27;name&#x27;] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.</li>
<li><span class='property-name'>vert_margin</span>: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
@ -2741,7 +2739,7 @@ Keyword Args:
<li><span class='property-name'>rotate_with_camera</span>: Whether to rotate visually with parent Grid&#x27;s camera_rotation (bool). False (default): stay screen-aligned. True: tilt with camera. Only affects children of UIGrid; ignored for other parents.</li>
<li><span class='property-name'>rotation</span>: Rotation angle in degrees (clockwise around origin). Animatable property.</li>
<li><span class='property-name'>shader</span>: Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.</li>
<li><span class='property-name'>texture</span> (read-only): Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None.</li>
<li><span class='property-name'>texture</span> (read-only): Texture used for tile rendering (Texture | None, read-only).</li>
<li><span class='property-name'>uniforms</span> (read-only): Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms[&#x27;name&#x27;] = value. Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.</li>
<li><span class='property-name'>vert_margin</span>: Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>

View file

@ -14,11 +14,11 @@
. ftr VB CB
. ftr VBI CBI
.\}
.TH "MCRFPY" "3" "2026-06-21" "McRogueFace 0.2.7-prerelease-7drl2026-107-g3f39ee0" ""
.TH "MCRFPY" "3" "2026-06-21" "McRogueFace 0.2.7-prerelease-7drl2026-108-g2654253" ""
.hy
.SH McRogueFace API Reference
.PP
\f[I]Generated on 2026-06-21 09:39:04\f[R]
\f[I]Generated on 2026-06-21 10:11:50\f[R]
.PP
\f[I]This documentation was dynamically generated from the compiled
module.\f[R]
@ -2201,22 +2201,19 @@ Note:
.PP
\f[B]Returns:\f[R] None Called automatically when the entity moves if
the grid has FOV configured.
.SS \f[V]visible_entities(fov=None, radius: int = -1) -> list[Entity]\f[R]
.SS \f[V]visible_entities(fov=None, radius: int | None = None) -> list[Entity]\f[R]
.PP
Get list of other entities visible from this entity\[cq]s position.
.PP
Note:
.PP
\f[B]Arguments:\f[R] - \f[V]fov\f[R]: FOV algorithm to use (FOV enum or
None to use grid.fov) - \f[V]radius\f[R]: FOV radius as int; omit or
pass -1 to use the grid\[cq]s default fov_radius
None to use grid.fov) - \f[V]radius\f[R]: FOV radius as int; pass None,
omit, or pass -1 to use the grid\[cq]s default fov_radius
.PP
\f[B]Returns:\f[R] List of Entity objects within field of view,
excluding self
.PP
\f[B]Raises:\f[R] ValueError: If entity is not associated with a grid
radius does not accept None; omit the argument entirely to use the grid
default.
TypeError: If radius is neither an int nor None
.SS Entity3D
.PP
Entity3D(pos=None, **kwargs)
@ -2724,8 +2721,7 @@ Animatable property.
When set, the drawable is rendered through the shader program.
Set to None to disable shader effects.
- \f[V]texture\f[R] \f[I](read-only)\f[R]: Texture used for tile
rendering (None, read-only).
Texture return is not yet implemented; always returns None.
rendering (Texture | None, read-only).
- \f[V]uniforms\f[R] \f[I](read-only)\f[R]: Collection of shader
uniforms (read-only access to collection).
Set uniforms via dict-like syntax: drawable.uniforms[`name'] = value.
@ -2889,8 +2885,7 @@ Animatable property.
When set, the drawable is rendered through the shader program.
Set to None to disable shader effects.
- \f[V]texture\f[R] \f[I](read-only)\f[R]: Texture used for tile
rendering (None, read-only).
Texture return is not yet implemented; always returns None.
rendering (Texture | None, read-only).
- \f[V]uniforms\f[R] \f[I](read-only)\f[R]: Collection of shader
uniforms (read-only access to collection).
Set uniforms via dict-like syntax: drawable.uniforms[`name'] = value.

View file

@ -114,7 +114,7 @@ sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
}
// Inject mouse event into the game engine
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button) {
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button, float scrollDelta) {
auto engine = getGameEngine();
if (!engine) return;
@ -141,8 +141,8 @@ void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y
break;
case sf::Event::MouseWheelScrolled:
event.mouseWheelScroll.wheel = sf::Mouse::VerticalWheel;
event.mouseWheelScroll.delta = static_cast<float>(x); // x is used for scroll amount
event.mouseWheelScroll.x = x;
event.mouseWheelScroll.delta = scrollDelta; // #317: scroll amount is its own arg
event.mouseWheelScroll.x = x; // position now honored on both axes
event.mouseWheelScroll.y = y;
break;
default:
@ -600,8 +600,9 @@ PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* k
}
}
// Inject scroll event
injectMouseEvent(sf::Event::MouseWheelScrolled, clicks, y);
// Inject scroll event (#317: forward the resolved x/y position; clicks is
// the scroll delta, passed via its own argument).
injectMouseEvent(sf::Event::MouseWheelScrolled, x, y, sf::Mouse::Left, static_cast<float>(clicks));
Py_RETURN_NONE;
}
@ -953,7 +954,7 @@ static PyMethodDef automationMethods[] = {
MCRF_ARGS_START
MCRF_ARG("clicks", "Number of scroll steps (positive = up, negative = down)")
MCRF_ARG("pos", "Position as (x, y) tuple, [x, y] list, Vector, or None for current position")
MCRF_NOTE("The x-coordinate of pos is currently unused; only the y-coordinate is applied to the scroll event position.")
MCRF_NOTE("Both the x and y of pos are applied to the scroll event position.")
)},
{"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(automation, mouseDown,

View file

@ -43,7 +43,7 @@ public:
static PyObject* _keyUp(PyObject* self, PyObject* args);
// Helper functions
static void injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button = sf::Mouse::Left);
static void injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button = sf::Mouse::Left, float scrollDelta = 0.0f);
static void injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key);
static void injectTextEvent(sf::Uint32 unicode);
static sf::Keyboard::Key stringToKey(const std::string& keyName);

View file

@ -1315,13 +1315,26 @@ PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyO
{
static const char* keywords[] = {"fov", "radius", nullptr};
PyObject* fov_arg = nullptr;
PyObject* radius_arg = nullptr;
int radius = -1; // -1 means use grid default
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oi", const_cast<char**>(keywords),
&fov_arg, &radius)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", const_cast<char**>(keywords),
&fov_arg, &radius_arg)) {
return NULL;
}
// #319: radius accepts an int, or None / omitted to use the grid default.
// The 'i' format code rejects None, so parse as an object and convert here.
if (radius_arg && radius_arg != Py_None) {
if (!PyLong_Check(radius_arg)) {
PyErr_SetString(PyExc_TypeError,
"visible_entities() radius must be an int or None");
return NULL;
}
radius = static_cast<int>(PyLong_AsLong(radius_arg));
if (radius == -1 && PyErr_Occurred()) return NULL;
}
// Check if entity has a grid
if (!self->data || !self->data->grid) {
PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to find visible entities");
@ -1449,14 +1462,14 @@ PyMethodDef UIEntity::methods[] = {
)},
{"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Entity, visible_entities,
MCRF_SIG("(fov=None, radius: int = -1)", "list[Entity]"),
MCRF_SIG("(fov=None, radius: int | None = None)", "list[Entity]"),
MCRF_DESC("Get list of other entities visible from this entity's position."),
MCRF_ARGS_START
MCRF_ARG("fov", "FOV algorithm to use (FOV enum or None to use grid.fov)")
MCRF_ARG("radius", "FOV radius as int; omit or pass -1 to use the grid's default fov_radius")
MCRF_ARG("radius", "FOV radius as int; pass None, omit, or pass -1 to use the grid's default fov_radius")
MCRF_RETURNS("List of Entity objects within field of view, excluding self")
MCRF_RAISES("ValueError", "If entity is not associated with a grid")
MCRF_NOTE("radius does not accept None; omit the argument entirely to use the grid default.")
MCRF_RAISES("TypeError", "If radius is neither an int nor None")
)},
{NULL, NULL, 0, NULL}
};
@ -1859,14 +1872,14 @@ PyMethodDef UIEntity_all_methods[] = {
)},
{"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Entity, visible_entities,
MCRF_SIG("(fov=None, radius: int = -1)", "list[Entity]"),
MCRF_SIG("(fov=None, radius: int | None = None)", "list[Entity]"),
MCRF_DESC("Get list of other entities visible from this entity's position."),
MCRF_ARGS_START
MCRF_ARG("fov", "FOV algorithm to use (FOV enum or None to use grid.fov)")
MCRF_ARG("radius", "FOV radius as int; omit or pass -1 to use the grid's default fov_radius")
MCRF_ARG("radius", "FOV radius as int; pass None, omit, or pass -1 to use the grid's default fov_radius")
MCRF_RETURNS("List of Entity objects within field of view, excluding self")
MCRF_RAISES("ValueError", "If entity is not associated with a grid")
MCRF_NOTE("radius does not accept None; omit the argument entirely to use the grid default.")
MCRF_RAISES("TypeError", "If radius is neither an int nor None")
)},
// #296 - Label methods
{"add_label", (PyCFunction)UIEntity::py_add_label, METH_O,

View file

@ -8,6 +8,7 @@
#include "Resources.h"
#include "Profiler.h"
#include "PyShader.h"
#include "PyTexture.h"
#include "PyUniformCollection.h"
#include "PyPositionHelper.h"
#include "PyVector.h"
@ -771,9 +772,16 @@ int UIGridView::set_fill_color(PyUIGridViewObject* self, PyObject* value, void*
PyObject* UIGridView::get_texture(PyUIGridViewObject* self, void* closure)
{
if (!self->data->ptex) Py_RETURN_NONE;
// TODO: return texture wrapper
Py_RETURN_NONE;
// #318: return a Texture wrapper sharing the underlying shared_ptr<PyTexture>,
// mirroring UIGrid::get_texture. None only when the view has no texture.
auto& texture = self->data->ptex;
if (!texture) Py_RETURN_NONE;
auto type = &mcrfpydef::PyTextureType;
auto obj = (PyTextureObject*)type->tp_alloc(type, 0);
if (!obj) return NULL;
obj->data = texture;
return (PyObject*)obj;
}
// Float member getters/setters for GridView-specific float members (center_x, center_y, zoom, camera_rotation)
@ -893,8 +901,10 @@ PyGetSetDef UIGridView::getsetters[] = {
MCRF_PROPERTY(zoom, "Zoom level for rendering (float). Values greater than 1.0 magnify; less than 1.0 shrink."), NULL},
{"fill_color", (getter)UIGridView::get_fill_color, (setter)UIGridView::set_fill_color,
MCRF_PROPERTY(fill_color, "Background fill color (Color). Drawn behind all tiles and entities."), NULL},
// #318/#252: this type is exposed as BOTH mcrfpy.Grid and mcrfpy.GridView, so the
// docstring is kept type-neutral (accurate for either name).
{"texture", (getter)UIGridView::get_texture, NULL,
MCRF_PROPERTY(texture, "Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None."), NULL},
MCRF_PROPERTY(texture, "Texture used for tile rendering (Texture | None, read-only)."), NULL},
// UIDrawable base properties - applied to GridView (the rendered object)
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
MCRF_PROPERTY(pos, "Position of the grid as Vector (Vector)."), (void*)PyObjectsEnum::UIGRIDVIEW},

View file

@ -725,7 +725,7 @@ class Entity:
def update_visibility(self) -> None:
"""Recompute which cells are visible from this entity's position and update perspective_map."""
...
def visible_entities(self, fov=None, radius: int = -1) -> list[Entity]:
def visible_entities(self, fov=None, radius: int | None = None) -> list[Entity]:
"""Get list of other entities visible from this entity's position."""
...
@ -903,7 +903,7 @@ class Grid:
rotation: Any # Rotation angle in degrees (clockwise around origin). Animatable property.
shader: Any # Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.
size: Vector # Size of the grid widget as Vector (Vector, width x height in pixels).
texture: None # Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None.
texture: Texture | None # Texture used for tile rendering (Texture | None, read-only).
uniforms: Any # Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/...
vert_margin: float # Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).
view: GridView | None # Auto-created GridView for rendering (GridView | None, read-only). When Grid is appended to a scene, this view is what actually renders.
@ -1013,7 +1013,7 @@ class GridView:
rotation: Any # Rotation angle in degrees (clockwise around origin). Animatable property.
shader: Any # Shader for GPU visual effects (Shader or None). When set, the drawable is rendered through the shader program. Set to None to disable shader effects.
size: Vector # Size of the grid widget as Vector (Vector, width x height in pixels).
texture: None # Texture used for tile rendering (None, read-only). Texture return is not yet implemented; always returns None.
texture: Texture | None # Texture used for tile rendering (Texture | None, read-only).
uniforms: Any # Collection of shader uniforms (read-only access to collection). Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. Supports float, vec2/3/...
vert_margin: float # Vertical margin override (float, 0 = use general margin). Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER).
view: GridView | None # Auto-created GridView for rendering (GridView | None, read-only). When Grid is appended to a scene, this view is what actually renders.

View file

@ -0,0 +1,68 @@
"""
Regression test for issue #317 -- automation.scroll() ignored the x-coordinate
of its position argument.
Root cause: scroll() resolved (x, y) from pos but called
injectMouseEvent(MouseWheelScrolled, clicks, y)
passing `clicks` as the x argument, so the resolved x was dropped AND the event's
mouseWheelScroll.x was set to the scroll amount. The fix gives the scroll delta
its own injectMouseEvent parameter and forwards the real x/y.
LIMITATION: no Python-observable handler currently consumes a scroll event's
position (GameEngine maps only the wheel *delta* to a wheel_up/wheel_down action),
so this test cannot assert the forwarded x end-to-end. It is therefore a smoke /
API-contract regression guard: scroll(clicks, pos=(x, y)) must accept any x, all
documented pos forms, and not raise after the injectMouseEvent refactor (which is
the part that changed how the scroll delta is passed). The x-forwarding itself is
verified by code inspection (McRFPy_Automation.cpp scroll/injectMouseEvent).
ASCII-only. Prints PASS/FAIL + exit.
"""
import mcrfpy
from mcrfpy import automation
import sys
failures = []
def check(label, fn):
try:
fn()
ok = True
except Exception as e:
ok = False
label = "%s (raised %s: %s)" % (label, type(e).__name__, e)
if not ok:
failures.append(label)
print((" ok " if ok else " FAIL ") + label)
# A scene must be current for event injection to have a target.
scene = mcrfpy.Scene("issue317")
mcrfpy.current_scene = scene
# Vary x across the position argument; previously x was silently dropped.
check("scroll(3) no position", lambda: automation.scroll(3))
check("scroll(3, pos=None)", lambda: automation.scroll(3, pos=None))
check("scroll(3, (100, 50)) tuple x!=0", lambda: automation.scroll(3, (100, 50)))
check("scroll(-2, (0, 50)) x==0", lambda: automation.scroll(-2, (0, 50)))
check("scroll(1, [250, 75]) list", lambda: automation.scroll(1, [250, 75]))
check("scroll(5, pos=(640, 480)) keyword", lambda: automation.scroll(5, pos=(640, 480)))
check("scroll(-5, mcrfpy.Vector(12, 34)) Vector",
lambda: automation.scroll(-5, mcrfpy.Vector(12, 34)))
# Same y, different x must both be accepted (the regression dropped x entirely).
check("scroll(2, (10, 200))", lambda: automation.scroll(2, (10, 200)))
check("scroll(2, (900, 200))", lambda: automation.scroll(2, (900, 200)))
print("")
if failures:
print("FAIL -- %d check(s) failed:" % len(failures))
for f in failures:
print(" - " + f)
sys.exit(1)
print("PASS -- automation.scroll accepts a full (x, y) position across all forms "
"(x-forwarding verified by code inspection; no observable consumer yet).")
sys.exit(0)

View file

@ -0,0 +1,70 @@
"""
Regression test for issue #318 -- GridView.texture always returned None.
Root cause: the getter had a `TODO: return texture wrapper` and fell through to
Py_RETURN_NONE even when self->data->ptex was non-null. The fix returns a
Texture wrapper sharing the underlying shared_ptr<PyTexture>, mirroring
UIGrid.texture.
Needs an asset; run via run_tests.py (cwd=build/) or from build/ so that
assets/kenney_tinydungeon.png resolves. ASCII-only. Prints PASS/FAIL + exit.
"""
import mcrfpy
import sys
failures = []
def check(label, cond):
if not cond:
failures.append(label)
print((" ok " if cond else " FAIL ") + label)
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# --- GridView over a textured grid: texture must now be returned -----------
grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture)
view = mcrfpy.GridView(grid=grid, pos=(0, 0), size=(160, 160))
t = view.texture
check("GridView.texture is not None when the view has a texture", t is not None)
check("GridView.texture is a Texture", isinstance(t, mcrfpy.Texture))
if isinstance(t, mcrfpy.Texture):
# Wrapper should describe the same underlying sheet as the source.
check("sprite_width matches source (16)", t.sprite_width == 16)
check("sprite_height matches source (16)", t.sprite_height == 16)
check("matches grid.texture sprite dims",
grid.texture is not None
and t.sprite_width == grid.texture.sprite_width
and t.sprite_height == grid.texture.sprite_height)
# --- A grid created without an explicit texture still gets the engine default
# texture, so its view's texture is also non-None (the getter's None branch is
# defensive-only -- no public API produces a textureless grid). -------------
bare_grid = mcrfpy.Grid(grid_size=(8, 8))
bare_view = mcrfpy.GridView(grid=bare_grid, pos=(0, 0), size=(80, 80))
check("default-textured grid's view exposes a Texture (not None)",
isinstance(bare_view.texture, mcrfpy.Texture))
check("view texture matches its grid's texture source",
bare_view.texture.source == bare_grid.texture.source)
# --- property is read-only (NULL setter) -----------------------------------
try:
view.texture = texture
check("GridView.texture is read-only (assignment raises)", False)
except AttributeError:
check("GridView.texture is read-only (assignment raises)", True)
except Exception as e:
check("GridView.texture is read-only (raised %s)" % type(e).__name__, False)
print("")
if failures:
print("FAIL -- %d check(s) failed:" % len(failures))
for f in failures:
print(" - " + f)
sys.exit(1)
print("PASS -- GridView.texture returns the tile Texture (or None when absent).")
sys.exit(0)

View file

@ -0,0 +1,95 @@
"""
Regression test for issue #319 -- Entity.visible_entities(radius=None) raised
TypeError instead of applying the grid default.
Root cause: radius was parsed with the PyArg 'i' format code ("|Oi"), which
rejects Python None. The fix parses radius as an object ("|OO") and treats
None / omitted / -1 as "use the grid's default fov_radius".
ASCII-only source. Prints PASS/FAIL and sys.exit(0/1).
"""
import mcrfpy
import sys
failures = []
def check(label, cond):
if not cond:
failures.append(label)
print((" ok " if cond else " FAIL ") + label)
# --- grid + entities -------------------------------------------------------
grid = mcrfpy.Grid(grid_size=(20, 20))
grid.fov_radius = 7
for gx in range(20):
for gy in range(20):
gp = grid.at(gx, gy)
gp.walkable = True
gp.transparent = True
seeker = mcrfpy.Entity(grid_pos=(10, 10))
grid.entities.append(seeker)
neighbor = mcrfpy.Entity(grid_pos=(11, 10))
grid.entities.append(neighbor)
# --- the bug: radius=None must NOT raise (it used to TypeError) -------------
try:
res_none = seeker.visible_entities(radius=None)
check("visible_entities(radius=None) does not raise", True)
check("returns a list", isinstance(res_none, list))
except TypeError as e:
check("visible_entities(radius=None) does not raise (got TypeError: %s)" % e, False)
res_none = []
# Adjacent transparent neighbor should be visible.
def positions(lst):
return {(int(e.cell_pos.x), int(e.cell_pos.y)) for e in lst}
check("neighbor (11,10) visible with radius=None", (11, 10) in positions(res_none))
check("self excluded from results", (10, 10) not in positions(res_none))
# --- equivalence: None, omitted, and -1 all mean 'use grid default' --------
res_omitted = seeker.visible_entities()
res_default = seeker.visible_entities(radius=-1)
check("radius=None matches omitted", positions(res_none) == positions(res_omitted))
check("radius=None matches radius=-1", positions(res_none) == positions(res_default))
# --- explicit int radius still works ---------------------------------------
try:
res_int = seeker.visible_entities(radius=3)
check("radius=3 (int) works", isinstance(res_int, list) and (11, 10) in positions(res_int))
except Exception as e:
check("radius=3 (int) works (raised %s)" % e, False)
# A tiny radius can still see the immediately-adjacent neighbor.
res_one = seeker.visible_entities(radius=1)
check("radius=1 sees adjacent neighbor", (11, 10) in positions(res_one))
# --- fov + radius=None combined (None must be fine alongside fov) -----------
try:
res_combo = seeker.visible_entities(fov=mcrfpy.FOV.SHADOW, radius=None)
check("visible_entities(fov=SHADOW, radius=None) works", isinstance(res_combo, list))
except Exception as e:
check("visible_entities(fov=SHADOW, radius=None) works (raised %s)" % e, False)
# --- invalid radius type must raise a clear TypeError ----------------------
try:
seeker.visible_entities(radius="not an int")
check("radius='str' raises TypeError", False)
except TypeError:
check("radius='str' raises TypeError", True)
except Exception as e:
check("radius='str' raises TypeError (got %s)" % type(e).__name__, False)
print("")
if failures:
print("FAIL -- %d check(s) failed:" % len(failures))
for f in failures:
print(" - " + f)
sys.exit(1)
print("PASS -- visible_entities accepts radius=None (and -1/omitted) as the grid default.")
sys.exit(0)

View file

@ -487,7 +487,7 @@ submodule automation
meth resize :: resize(width, height) or (size) -> None
meth set_behavior :: set_behavior(type, waypoints=None, turns: int = 0, path=None, pathfinder=None) -> None
meth update_visibility :: update_visibility() -> None
meth visible_entities :: visible_entities(fov=None, radius: int = -1) -> list[Entity]
meth visible_entities :: visible_entities(fov=None, radius: int | None = None) -> list[Entity]
[Font]
prop family: str (ro)
prop source: str (ro)
@ -558,7 +558,7 @@ submodule automation
prop rotate_with_camera: bool (rw)
prop rotation: Any (rw)
prop shader: Any (rw)
prop texture: None (ro)
prop texture: Texture | None (ro)
prop uniforms: Any (ro)
prop vert_margin: float (rw)
prop visible: bool (rw)
@ -598,7 +598,7 @@ submodule automation
prop rotate_with_camera: bool (rw)
prop rotation: Any (rw)
prop shader: Any (rw)
prop texture: None (ro)
prop texture: Texture | None (ro)
prop uniforms: Any (ro)
prop vert_margin: float (rw)
prop visible: bool (rw)