From 98489a96fd9b00c48a8ecd59447fdbf863e9f2c2 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 21 Jun 2026 10:12:41 -0400 Subject: [PATCH] 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, 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 Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv --- docs/API_REFERENCE_DYNAMIC.md | 14 ++- docs/api_reference_dynamic.html | 16 ++-- docs/mcrfpy.3 | 21 ++-- src/McRFPy_Automation.cpp | 13 +-- src/McRFPy_Automation.h | 2 +- src/UIEntity.cpp | 29 ++++-- src/UIGridView.cpp | 18 +++- stubs/mcrfpy.pyi | 6 +- .../issue_317_scroll_position_test.py | 68 +++++++++++++ .../issue_318_gridview_texture_test.py | 70 ++++++++++++++ ...e_319_visible_entities_radius_none_test.py | 95 +++++++++++++++++++ tests/snapshots/api_surface.golden.txt | 6 +- 12 files changed, 303 insertions(+), 55 deletions(-) create mode 100644 tests/regression/issue_317_scroll_position_test.py create mode 100644 tests/regression/issue_318_gridview_texture_test.py create mode 100644 tests/regression/issue_319_visible_entities_radius_none_test.py diff --git a/docs/API_REFERENCE_DYNAMIC.md b/docs/API_REFERENCE_DYNAMIC.md index 00b6d35..e7fc5e1 100644 --- a/docs/API_REFERENCE_DYNAMIC.md +++ b/docs/API_REFERENCE_DYNAMIC.md @@ -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. diff --git a/docs/api_reference_dynamic.html b/docs/api_reference_dynamic.html index f511245..97c15a2 100644 --- a/docs/api_reference_dynamic.html +++ b/docs/api_reference_dynamic.html @@ -108,7 +108,7 @@

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.

@@ -2144,16 +2144,14 @@ Note:

-
visible_entities(fov=None, radius: int = -1) -> list[Entity]
-

Get list of other entities visible from this entity's position. - -Note:

+
visible_entities(fov=None, radius: int | None = None) -> list[Entity]
+

Get list of other entities visible from this entity's position.

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

@@ -2619,7 +2617,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.
  • @@ -2741,7 +2739,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.
  • diff --git a/docs/mcrfpy.3 b/docs/mcrfpy.3 index e390525..8171d95 100644 --- a/docs/mcrfpy.3 +++ b/docs/mcrfpy.3 @@ -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. diff --git a/src/McRFPy_Automation.cpp b/src/McRFPy_Automation.cpp index bf5de2e..0803924 100644 --- a/src/McRFPy_Automation.cpp +++ b/src/McRFPy_Automation.cpp @@ -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(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(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, diff --git a/src/McRFPy_Automation.h b/src/McRFPy_Automation.h index 6d67012..7be6f96 100644 --- a/src/McRFPy_Automation.h +++ b/src/McRFPy_Automation.h @@ -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); diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 934b462..f7d1d50 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -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(keywords), - &fov_arg, &radius)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", const_cast(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(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, diff --git a/src/UIGridView.cpp b/src/UIGridView.cpp index a275c6e..e87a43a 100644 --- a/src/UIGridView.cpp +++ b/src/UIGridView.cpp @@ -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, + // 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}, diff --git a/stubs/mcrfpy.pyi b/stubs/mcrfpy.pyi index 12482a9..fa99e81 100644 --- a/stubs/mcrfpy.pyi +++ b/stubs/mcrfpy.pyi @@ -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. diff --git a/tests/regression/issue_317_scroll_position_test.py b/tests/regression/issue_317_scroll_position_test.py new file mode 100644 index 0000000..f1dea7f --- /dev/null +++ b/tests/regression/issue_317_scroll_position_test.py @@ -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) diff --git a/tests/regression/issue_318_gridview_texture_test.py b/tests/regression/issue_318_gridview_texture_test.py new file mode 100644 index 0000000..b9d3485 --- /dev/null +++ b/tests/regression/issue_318_gridview_texture_test.py @@ -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, 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) diff --git a/tests/regression/issue_319_visible_entities_radius_none_test.py b/tests/regression/issue_319_visible_entities_radius_none_test.py new file mode 100644 index 0000000..15e746a --- /dev/null +++ b/tests/regression/issue_319_visible_entities_radius_none_test.py @@ -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) diff --git a/tests/snapshots/api_surface.golden.txt b/tests/snapshots/api_surface.golden.txt index 4646f25..89b030a 100644 --- a/tests/snapshots/api_surface.golden.txt +++ b/tests/snapshots/api_surface.golden.txt @@ -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)