Positions are always mcrfpy.Vector, Vector/tuple/iterables expected as inputs, and for position-only inputs we permit x,y args to prevent requiring double-parens

This commit is contained in:
John McCardle 2026-01-05 10:16:16 -05:00
commit d2e4791f5a
25 changed files with 2109 additions and 636 deletions

View file

@ -4,6 +4,7 @@
#include "PyColor.h" #include "PyColor.h"
#include "PyTexture.h" #include "PyTexture.h"
#include "PyFOV.h" #include "PyFOV.h"
#include "PyPositionHelper.h"
#include <sstream> #include <sstream>
// ============================================================================= // =============================================================================
@ -562,10 +563,18 @@ void TileLayer::render(sf::RenderTarget& target,
// ============================================================================= // =============================================================================
PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = { PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = {
{"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS, {"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS | METH_KEYWORDS,
"at(x, y) -> Color\n\nGet the color at cell position (x, y)."}, "at(pos) -> Color\nat(x, y) -> Color\n\n"
"Get the color at cell position.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" x, y: Position as separate integer arguments"},
{"set", (PyCFunction)PyGridLayerAPI::ColorLayer_set, METH_VARARGS, {"set", (PyCFunction)PyGridLayerAPI::ColorLayer_set, METH_VARARGS,
"set(x, y, color)\n\nSet the color at cell position (x, y)."}, "set(pos, color)\n\n"
"Set the color at cell position.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" color: Color object or (r, g, b[, a]) tuple"},
{"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS, {"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS,
"fill(color)\n\nFill the entire layer with the specified color."}, "fill(color)\n\nFill the entire layer with the specified color."},
{"fill_rect", (PyCFunction)PyGridLayerAPI::ColorLayer_fill_rect, METH_VARARGS | METH_KEYWORDS, {"fill_rect", (PyCFunction)PyGridLayerAPI::ColorLayer_fill_rect, METH_VARARGS | METH_KEYWORDS,
@ -646,9 +655,9 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py
return 0; return 0;
} }
PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args) { PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
int x, y; int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) { if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL; return NULL;
} }
@ -678,9 +687,14 @@ PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args
} }
PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) { PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) {
int x, y; PyObject* pos_obj;
PyObject* color_obj; PyObject* color_obj;
if (!PyArg_ParseTuple(args, "iiO", &x, &y, &color_obj)) { if (!PyArg_ParseTuple(args, "OO", &pos_obj, &color_obj)) {
return NULL;
}
int x, y;
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
return NULL; return NULL;
} }
@ -1108,10 +1122,18 @@ PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) {
// ============================================================================= // =============================================================================
PyMethodDef PyGridLayerAPI::TileLayer_methods[] = { PyMethodDef PyGridLayerAPI::TileLayer_methods[] = {
{"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS, {"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS | METH_KEYWORDS,
"at(x, y) -> int\n\nGet the tile index at cell position (x, y). Returns -1 if no tile."}, "at(pos) -> int\nat(x, y) -> int\n\n"
"Get the tile index at cell position. Returns -1 if no tile.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" x, y: Position as separate integer arguments"},
{"set", (PyCFunction)PyGridLayerAPI::TileLayer_set, METH_VARARGS, {"set", (PyCFunction)PyGridLayerAPI::TileLayer_set, METH_VARARGS,
"set(x, y, index)\n\nSet the tile index at cell position (x, y). Use -1 for no tile."}, "set(pos, index)\n\n"
"Set the tile index at cell position. Use -1 for no tile.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" index: Tile index (-1 for no tile)"},
{"fill", (PyCFunction)PyGridLayerAPI::TileLayer_fill, METH_VARARGS, {"fill", (PyCFunction)PyGridLayerAPI::TileLayer_fill, METH_VARARGS,
"fill(index)\n\nFill the entire layer with the specified tile index."}, "fill(index)\n\nFill the entire layer with the specified tile index."},
{"fill_rect", (PyCFunction)PyGridLayerAPI::TileLayer_fill_rect, METH_VARARGS | METH_KEYWORDS, {"fill_rect", (PyCFunction)PyGridLayerAPI::TileLayer_fill_rect, METH_VARARGS | METH_KEYWORDS,
@ -1190,9 +1212,9 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb
return 0; return 0;
} }
PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) { PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
int x, y; int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) { if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL; return NULL;
} }
@ -1210,8 +1232,14 @@ PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args)
} }
PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) { PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) {
int x, y, index; PyObject* pos_obj;
if (!PyArg_ParseTuple(args, "iii", &x, &y, &index)) { int index;
if (!PyArg_ParseTuple(args, "Oi", &pos_obj, &index)) {
return NULL;
}
int x, y;
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
return NULL; return NULL;
} }

View file

@ -200,7 +200,7 @@ class PyGridLayerAPI {
public: public:
// ColorLayer methods // ColorLayer methods
static int ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds); static int ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args); static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args); static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args);
static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args); static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args);
static PyObject* ColorLayer_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds); static PyObject* ColorLayer_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
@ -217,7 +217,7 @@ public:
// 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); static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args); static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args);
static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args); static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args);
static PyObject* TileLayer_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds); static PyObject* TileLayer_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds);

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@ public:
// Mouse position and screen info // Mouse position and screen info
static PyObject* _position(PyObject* self, PyObject* args); static PyObject* _position(PyObject* self, PyObject* args);
static PyObject* _size(PyObject* self, PyObject* args); static PyObject* _size(PyObject* self, PyObject* args);
static PyObject* _onScreen(PyObject* self, PyObject* args); static PyObject* _onScreen(PyObject* self, PyObject* args, PyObject* kwargs);
// Mouse movement // Mouse movement
static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs); static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs);

View file

@ -1,6 +1,7 @@
#include "PyCallable.h" #include "PyCallable.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "GameEngine.h" #include "GameEngine.h"
#include "PyVector.h"
PyCallable::PyCallable(PyObject* _target) PyCallable::PyCallable(PyObject* _target)
{ {
@ -49,7 +50,16 @@ PyClickCallable::PyClickCallable()
void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::string action) void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::string action)
{ {
PyObject* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), action.c_str()); // Create a Vector object for the position
PyObject* pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", mousepos.x, mousepos.y);
if (!pos) {
std::cerr << "Failed to create Vector object for click callback" << std::endl;
PyErr_Print();
PyErr_Clear();
return;
}
PyObject* args = Py_BuildValue("(Oss)", pos, button.c_str(), action.c_str());
Py_DECREF(pos); // Py_BuildValue increments the refcount
PyObject* retval = PyCallable::call(args, NULL); PyObject* retval = PyCallable::call(args, NULL);
if (!retval) if (!retval)
{ {

View file

@ -1,13 +1,14 @@
#include "PyDrawable.h" #include "PyDrawable.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "McRFPy_Doc.h" #include "McRFPy_Doc.h"
#include "PyPositionHelper.h"
// Click property getter // Click property getter
static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure) static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
{ {
if (!self->data->click_callable) if (!self->data->click_callable)
Py_RETURN_NONE; Py_RETURN_NONE;
PyObject* ptr = self->data->click_callable->borrow(); PyObject* ptr = self->data->click_callable->borrow();
if (ptr && ptr != Py_None) if (ptr && ptr != Py_None)
return ptr; return ptr;
@ -35,20 +36,20 @@ static PyObject* PyDrawable_get_z_index(PyDrawableObject* self, void* closure)
return PyLong_FromLong(self->data->z_index); return PyLong_FromLong(self->data->z_index);
} }
// Z-index property setter // Z-index property setter
static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure) static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure)
{ {
if (!PyLong_Check(value)) { if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "z_index must be an integer"); PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
return -1; return -1;
} }
int val = PyLong_AsLong(value); int val = PyLong_AsLong(value);
self->data->z_index = val; self->data->z_index = val;
// Mark scene as needing resort // Mark scene as needing resort
self->data->notifyZIndexChanged(); self->data->notifyZIndexChanged();
return 0; return 0;
} }
@ -65,7 +66,7 @@ static int PyDrawable_set_visible(PyDrawableObject* self, PyObject* value, void*
PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1; return -1;
} }
self->data->visible = (value == Py_True); self->data->visible = (value == Py_True);
return 0; return 0;
} }
@ -88,11 +89,11 @@ static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void*
PyErr_SetString(PyExc_TypeError, "opacity must be a number"); PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1; return -1;
} }
// Clamp to valid range // Clamp to valid range
if (val < 0.0f) val = 0.0f; if (val < 0.0f) val = 0.0f;
if (val > 1.0f) val = 1.0f; if (val > 1.0f) val = 1.0f;
self->data->opacity = val; self->data->opacity = val;
return 0; return 0;
} }
@ -102,7 +103,7 @@ static PyGetSetDef PyDrawable_getsetters[] = {
{"on_click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click, {"on_click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
MCRF_PROPERTY(on_click, MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. " "Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click." "Function receives (pos: Vector, button: str, action: str)."
), NULL}, ), NULL},
{"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index, {"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index,
MCRF_PROPERTY(z_index, MCRF_PROPERTY(z_index,
@ -130,25 +131,25 @@ static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUS
} }
// move method implementation (#98) // move method implementation (#98)
static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args) static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args, PyObject* kwds)
{ {
float dx, dy; float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) {
return NULL; return NULL;
} }
self->data->move(dx, dy); self->data->move(dx, dy);
Py_RETURN_NONE; Py_RETURN_NONE;
} }
// resize method implementation (#98) // resize method implementation (#98)
static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args) static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args, PyObject* kwds)
{ {
float w, h; float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) { if (!PyPosition_ParseFloat(args, kwds, &w, &h)) {
return NULL; return NULL;
} }
self->data->resize(w, h); self->data->resize(w, h);
Py_RETURN_NONE; Py_RETURN_NONE;
} }
@ -162,23 +163,27 @@ static PyMethodDef PyDrawable_methods[] = {
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds")
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.")
)}, )},
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS, {"move", (PyCFunction)PyDrawable_move, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Drawable, move, MCRF_METHOD(Drawable, move,
MCRF_SIG("(dx: float, dy: float)", "None"), MCRF_SIG("(dx, dy) or (delta)", "None"),
MCRF_DESC("Move the element by a relative offset."), MCRF_DESC("Move the element by a relative offset."),
MCRF_ARGS_START MCRF_ARGS_START
MCRF_ARG("dx", "Horizontal offset in pixels") MCRF_ARG("dx", "Horizontal offset in pixels (or use delta)")
MCRF_ARG("dy", "Vertical offset in pixels") MCRF_ARG("dy", "Vertical offset in pixels (or use delta)")
MCRF_NOTE("This modifies the x and y position properties by the given amounts.") MCRF_ARG("delta", "Offset as tuple, list, or Vector: (dx, dy)")
MCRF_NOTE("This modifies the x and y position properties by the given amounts. "
"Accepts move(dx, dy), move((dx, dy)), move(Vector), or move(pos=(dx, dy)).")
)}, )},
{"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS, {"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Drawable, resize, MCRF_METHOD(Drawable, resize,
MCRF_SIG("(width: float, height: float)", "None"), MCRF_SIG("(width, height) or (size)", "None"),
MCRF_DESC("Resize the element to new dimensions."), MCRF_DESC("Resize the element to new dimensions."),
MCRF_ARGS_START MCRF_ARGS_START
MCRF_ARG("width", "New width in pixels") MCRF_ARG("width", "New width in pixels (or use size)")
MCRF_ARG("height", "New height in pixels") MCRF_ARG("height", "New height in pixels (or use size)")
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") MCRF_ARG("size", "Size as tuple, list, or Vector: (width, height)")
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content. "
"Accepts resize(w, h), resize((w, h)), resize(Vector), or resize(pos=(w, h)).")
)}, )},
{NULL} // Sentinel {NULL} // Sentinel
}; };
@ -208,4 +213,4 @@ namespace mcrfpydef {
.tp_init = (initproc)PyDrawable_init, .tp_init = (initproc)PyDrawable_init,
.tp_new = PyType_GenericNew, .tp_new = PyType_GenericNew,
}; };
} }

View file

@ -3,7 +3,32 @@
#include "PyVector.h" #include "PyVector.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
// Helper class for standardized position argument parsing across UI classes // ============================================================================
// PyPositionHelper - Reusable position argument parsing for McRogueFace API
// ============================================================================
//
// This helper provides standardized parsing for position arguments that can be
// specified in multiple formats:
// - Two separate args: func(x, y)
// - A tuple: func((x, y))
// - A Vector object: func(Vector(x, y))
// - Any iterable with len() == 2: func([x, y])
// - Keyword args: func(x=x, y=y) or func(pos=(x,y))
//
// Usage patterns:
// // For methods with only position args (like Grid.at()):
// int x, y;
// if (!PyPosition_ParseInt(args, kwds, &x, &y)) return NULL;
//
// // For extracting position from a single PyObject:
// float x, y;
// if (!PyPosition_FromObject(obj, &x, &y)) return NULL;
//
// // For more complex parsing with additional args:
// auto result = PyPositionHelper::parse_position(args, kwds);
// if (!result.has_position) { ... }
// ============================================================================
class PyPositionHelper { class PyPositionHelper {
public: public:
// Template structure for parsing results // Template structure for parsing results
@ -12,33 +37,315 @@ public:
float y = 0.0f; float y = 0.0f;
bool has_position = false; bool has_position = false;
}; };
struct ParseResultInt { struct ParseResultInt {
int x = 0; int x = 0;
int y = 0; int y = 0;
bool has_position = false; bool has_position = false;
}; };
private:
// Internal helper: extract two numeric values from a 2-element iterable
// Returns true on success, false on failure (does NOT set Python error)
static bool extract_from_iterable(PyObject* obj, float* out_x, float* out_y) {
// First check if it's a Vector (most specific)
PyTypeObject* vector_type = (PyTypeObject*)PyObject_GetAttrString(
McRFPy_API::mcrf_module, "Vector");
if (vector_type) {
if (PyObject_IsInstance(obj, (PyObject*)vector_type)) {
PyVectorObject* vec = (PyVectorObject*)obj;
*out_x = vec->data.x;
*out_y = vec->data.y;
Py_DECREF(vector_type);
return true;
}
Py_DECREF(vector_type);
} else {
PyErr_Clear(); // Clear any error from GetAttrString
}
// Check for tuple (common case, optimized)
if (PyTuple_Check(obj)) {
if (PyTuple_Size(obj) != 2) return false;
PyObject* x_obj = PyTuple_GetItem(obj, 0);
PyObject* y_obj = PyTuple_GetItem(obj, 1);
if (!extract_number(x_obj, out_x) || !extract_number(y_obj, out_y)) {
return false;
}
return true;
}
// Check for list (also common)
if (PyList_Check(obj)) {
if (PyList_Size(obj) != 2) return false;
PyObject* x_obj = PyList_GetItem(obj, 0);
PyObject* y_obj = PyList_GetItem(obj, 1);
if (!extract_number(x_obj, out_x) || !extract_number(y_obj, out_y)) {
return false;
}
return true;
}
// Generic iterable fallback: check __len__ and index
// This handles any object that implements sequence protocol
if (PySequence_Check(obj)) {
Py_ssize_t len = PySequence_Size(obj);
if (len != 2) {
PyErr_Clear(); // Clear size error
return false;
}
PyObject* x_obj = PySequence_GetItem(obj, 0);
if (!x_obj) { PyErr_Clear(); return false; }
PyObject* y_obj = PySequence_GetItem(obj, 1);
if (!y_obj) { Py_DECREF(x_obj); PyErr_Clear(); return false; }
bool success = extract_number(x_obj, out_x) && extract_number(y_obj, out_y);
Py_DECREF(x_obj);
Py_DECREF(y_obj);
return success;
}
return false;
}
// Internal helper: extract integer values from a 2-element iterable
static bool extract_from_iterable_int(PyObject* obj, int* out_x, int* out_y) {
// First check if it's a Vector
PyTypeObject* vector_type = (PyTypeObject*)PyObject_GetAttrString(
McRFPy_API::mcrf_module, "Vector");
if (vector_type) {
if (PyObject_IsInstance(obj, (PyObject*)vector_type)) {
PyVectorObject* vec = (PyVectorObject*)obj;
*out_x = static_cast<int>(vec->data.x);
*out_y = static_cast<int>(vec->data.y);
Py_DECREF(vector_type);
return true;
}
Py_DECREF(vector_type);
} else {
PyErr_Clear();
}
// Check for tuple
if (PyTuple_Check(obj)) {
if (PyTuple_Size(obj) != 2) return false;
PyObject* x_obj = PyTuple_GetItem(obj, 0);
PyObject* y_obj = PyTuple_GetItem(obj, 1);
if (!extract_int(x_obj, out_x) || !extract_int(y_obj, out_y)) {
return false;
}
return true;
}
// Check for list
if (PyList_Check(obj)) {
if (PyList_Size(obj) != 2) return false;
PyObject* x_obj = PyList_GetItem(obj, 0);
PyObject* y_obj = PyList_GetItem(obj, 1);
if (!extract_int(x_obj, out_x) || !extract_int(y_obj, out_y)) {
return false;
}
return true;
}
// Generic sequence fallback
if (PySequence_Check(obj)) {
Py_ssize_t len = PySequence_Size(obj);
if (len != 2) {
PyErr_Clear();
return false;
}
PyObject* x_obj = PySequence_GetItem(obj, 0);
if (!x_obj) { PyErr_Clear(); return false; }
PyObject* y_obj = PySequence_GetItem(obj, 1);
if (!y_obj) { Py_DECREF(x_obj); PyErr_Clear(); return false; }
bool success = extract_int(x_obj, out_x) && extract_int(y_obj, out_y);
Py_DECREF(x_obj);
Py_DECREF(y_obj);
return success;
}
return false;
}
// Extract a float from a numeric Python object
static bool extract_number(PyObject* obj, float* out) {
if (PyFloat_Check(obj)) {
*out = static_cast<float>(PyFloat_AsDouble(obj));
return true;
}
if (PyLong_Check(obj)) {
*out = static_cast<float>(PyLong_AsLong(obj));
return true;
}
return false;
}
// Extract an int from a numeric Python object (integers only)
static bool extract_int(PyObject* obj, int* out) {
if (PyLong_Check(obj)) {
*out = static_cast<int>(PyLong_AsLong(obj));
return true;
}
// Also accept float but only if it's a whole number
if (PyFloat_Check(obj)) {
double val = PyFloat_AsDouble(obj);
if (val == static_cast<double>(static_cast<int>(val))) {
*out = static_cast<int>(val);
return true;
}
}
return false;
}
public:
// ========================================================================
// Simple API: Parse position from a single PyObject
// ========================================================================
// Extract float position from any supported format
// Sets Python error and returns false on failure
static bool FromObject(PyObject* obj, float* out_x, float* out_y) {
if (extract_from_iterable(obj, out_x, out_y)) {
return true;
}
PyErr_SetString(PyExc_TypeError,
"Expected a position as (x, y) tuple, [x, y] list, Vector, or other 2-element sequence");
return false;
}
// Extract integer position from any supported format
// Sets Python error and returns false on failure
static bool FromObjectInt(PyObject* obj, int* out_x, int* out_y) {
if (extract_from_iterable_int(obj, out_x, out_y)) {
return true;
}
PyErr_SetString(PyExc_TypeError,
"Expected integer position as (x, y) tuple, [x, y] list, Vector, or other 2-element sequence");
return false;
}
// ========================================================================
// Method argument API: Parse position from args tuple
// ========================================================================
// Parse float position from method arguments
// Supports: func(x, y) or func((x, y)) or func(Vector) or func(iterable)
// Sets Python error and returns false on failure
static bool ParseFloat(PyObject* args, PyObject* kwds, float* out_x, float* out_y) {
// First try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj) {
if (extract_number(x_obj, out_x) && extract_number(y_obj, out_y)) {
return true;
}
}
if (pos_obj) {
if (extract_from_iterable(pos_obj, out_x, out_y)) {
return true;
}
}
}
Py_ssize_t nargs = PyTuple_Size(args);
// Try two separate numeric arguments: func(x, y)
if (nargs >= 2) {
PyObject* first = PyTuple_GetItem(args, 0);
PyObject* second = PyTuple_GetItem(args, 1);
if (extract_number(first, out_x) && extract_number(second, out_y)) {
return true;
}
}
// Try single iterable argument: func((x, y)) or func(Vector) or func([x, y])
if (nargs == 1) {
PyObject* first = PyTuple_GetItem(args, 0);
if (extract_from_iterable(first, out_x, out_y)) {
return true;
}
}
PyErr_SetString(PyExc_TypeError,
"Position can be specified as: (x, y), ((x,y)), pos=(x,y), Vector, or 2-element sequence");
return false;
}
// Parse integer position from method arguments
// Supports: func(x, y) or func((x, y)) or func(Vector) or func(iterable)
// Sets Python error and returns false on failure
static bool ParseInt(PyObject* args, PyObject* kwds, int* out_x, int* out_y) {
// First try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj) {
if (extract_int(x_obj, out_x) && extract_int(y_obj, out_y)) {
return true;
}
}
if (pos_obj) {
if (extract_from_iterable_int(pos_obj, out_x, out_y)) {
return true;
}
}
}
Py_ssize_t nargs = PyTuple_Size(args);
// Try two separate integer arguments: func(x, y)
if (nargs >= 2) {
PyObject* first = PyTuple_GetItem(args, 0);
PyObject* second = PyTuple_GetItem(args, 1);
if (extract_int(first, out_x) && extract_int(second, out_y)) {
return true;
}
}
// Try single iterable argument: func((x, y)) or func(Vector) or func([x, y])
if (nargs == 1) {
PyObject* first = PyTuple_GetItem(args, 0);
if (extract_from_iterable_int(first, out_x, out_y)) {
return true;
}
}
PyErr_SetString(PyExc_TypeError,
"Position must be integers specified as: (x, y), ((x,y)), pos=(x,y), Vector, or 2-element sequence");
return false;
}
// ========================================================================
// Legacy struct-based API (for compatibility with existing code)
// ========================================================================
// Parse position from multiple formats for UI class constructors // Parse position from multiple formats for UI class constructors
// Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector // Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector
static ParseResult parse_position(PyObject* args, PyObject* kwds, static ParseResult parse_position(PyObject* args, PyObject* kwds,
int* arg_index = nullptr) int* arg_index = nullptr)
{ {
ParseResult result; ParseResult result;
float x = 0.0f, y = 0.0f; float x = 0.0f, y = 0.0f;
PyObject* pos_obj = nullptr;
int start_index = arg_index ? *arg_index : 0; int start_index = arg_index ? *arg_index : 0;
// Check for positional tuple (x, y) first // Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) > start_index + 1) { if (PyTuple_Size(args) > start_index + 1) {
PyObject* first = PyTuple_GetItem(args, start_index); PyObject* first = PyTuple_GetItem(args, start_index);
PyObject* second = PyTuple_GetItem(args, start_index + 1); PyObject* second = PyTuple_GetItem(args, start_index + 1);
// Check if both are numbers // Check if both are numbers
if ((PyFloat_Check(first) || PyLong_Check(first)) && if (extract_number(first, &x) && extract_number(second, &y)) {
(PyFloat_Check(second) || PyLong_Check(second))) {
x = PyFloat_Check(first) ? PyFloat_AsDouble(first) : PyLong_AsLong(first);
y = PyFloat_Check(second) ? PyFloat_AsDouble(second) : PyLong_AsLong(second);
result.x = x; result.x = x;
result.y = y; result.y = y;
result.has_position = true; result.has_position = true;
@ -46,119 +353,100 @@ public:
return result; return result;
} }
} }
// Check for single positional argument that might be tuple or Vector // Check for single positional argument that might be tuple, list, or Vector
if (!kwds && PyTuple_Size(args) > start_index) { if (PyTuple_Size(args) > start_index) {
PyObject* first = PyTuple_GetItem(args, start_index); PyObject* first = PyTuple_GetItem(args, start_index);
PyVectorObject* vec = PyVector::from_arg(first); if (extract_from_iterable(first, &x, &y)) {
if (vec) { result.x = x;
result.x = vec->data.x; result.y = y;
result.y = vec->data.y;
result.has_position = true; result.has_position = true;
if (arg_index) *arg_index += 1; if (arg_index) *arg_index += 1;
return result; return result;
} }
} }
// Try keyword arguments // Try keyword arguments
if (kwds) { if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x"); PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y"); PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_kw = PyDict_GetItemString(kwds, "pos"); PyObject* pos_kw = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj) { if (x_obj && y_obj) {
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) && if (extract_number(x_obj, &x) && extract_number(y_obj, &y)) {
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) { result.x = x;
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj); result.y = y;
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.has_position = true; result.has_position = true;
return result; return result;
} }
} }
if (pos_kw) { if (pos_kw) {
PyVectorObject* vec = PyVector::from_arg(pos_kw); if (extract_from_iterable(pos_kw, &x, &y)) {
if (vec) { result.x = x;
result.x = vec->data.x; result.y = y;
result.y = vec->data.y;
result.has_position = true; result.has_position = true;
return result; return result;
} }
} }
} }
return result; return result;
} }
// Parse integer position for Grid.at() and similar // Parse integer position for Grid.at() and similar
static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds) static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds)
{ {
ParseResultInt result; ParseResultInt result;
int x = 0, y = 0;
// Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) >= 2) { // Try the new simplified parser first
PyObject* first = PyTuple_GetItem(args, 0); if (ParseInt(args, kwds, &x, &y)) {
PyObject* second = PyTuple_GetItem(args, 1); result.x = x;
result.y = y;
if (PyLong_Check(first) && PyLong_Check(second)) { result.has_position = true;
result.x = PyLong_AsLong(first); PyErr_Clear(); // Clear any error set by ParseInt
result.y = PyLong_AsLong(second);
result.has_position = true;
return result;
}
} }
// Check for single tuple argument
if (!kwds && PyTuple_Size(args) == 1) {
PyObject* first = PyTuple_GetItem(args, 0);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* x_obj = PyTuple_GetItem(first, 0);
PyObject* y_obj = PyTuple_GetItem(first, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
}
}
// Try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj && PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
if (pos_obj && PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if (PyLong_Check(x_val) && PyLong_Check(y_val)) {
result.x = PyLong_AsLong(x_val);
result.y = PyLong_AsLong(y_val);
result.has_position = true;
return result;
}
}
}
return result; return result;
} }
// Error message helper // Error message helper
static void set_position_error() { static void set_position_error() {
PyErr_SetString(PyExc_TypeError, PyErr_SetString(PyExc_TypeError,
"Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), or pos=Vector"); "Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), Vector, or 2-element sequence");
} }
static void set_position_int_error() { static void set_position_int_error() {
PyErr_SetString(PyExc_TypeError, PyErr_SetString(PyExc_TypeError,
"Position must be specified as: (x, y), x=x, y=y, ((x,y)), or pos=(x,y) with integer values"); "Position must be integers specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), Vector, or 2-element sequence");
} }
}; };
// ============================================================================
// Convenience macros/functions for common use patterns
// ============================================================================
// Parse integer position from method args - simplest API
// Usage: if (!PyPosition_ParseInt(args, kwds, &x, &y)) return NULL;
inline bool PyPosition_ParseInt(PyObject* args, PyObject* kwds, int* x, int* y) {
return PyPositionHelper::ParseInt(args, kwds, x, y);
}
// Parse float position from method args
// Usage: if (!PyPosition_ParseFloat(args, kwds, &x, &y)) return NULL;
inline bool PyPosition_ParseFloat(PyObject* args, PyObject* kwds, float* x, float* y) {
return PyPositionHelper::ParseFloat(args, kwds, x, y);
}
// Extract integer position from a single Python object
// Usage: if (!PyPosition_FromObjectInt(obj, &x, &y)) return NULL;
inline bool PyPosition_FromObjectInt(PyObject* obj, int* x, int* y) {
return PyPositionHelper::FromObjectInt(obj, x, y);
}
// Extract float position from a single Python object
// Usage: if (!PyPosition_FromObject(obj, &x, &y)) return NULL;
inline bool PyPosition_FromObject(PyObject* obj, float* x, float* y) {
return PyPositionHelper::FromObject(obj, x, y);
}

View file

@ -430,7 +430,7 @@ PyGetSetDef UIArc::getsetters[] = {
{"thickness", (getter)UIArc::get_thickness, (setter)UIArc::set_thickness, {"thickness", (getter)UIArc::get_thickness, (setter)UIArc::set_thickness,
"Line thickness", NULL}, "Line thickness", NULL},
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
"Callable executed when arc is clicked.", (void*)PyObjectsEnum::UIARC}, "Callable executed when arc is clicked. Function receives (pos: Vector, button: str, action: str).", (void*)PyObjectsEnum::UIARC},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
"Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UIARC}, "Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UIARC},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "Python.h" #include "Python.h"
#include "McRFPy_Doc.h" #include "McRFPy_Doc.h"
#include "PyPositionHelper.h"
#include <memory> #include <memory>
class UIEntity; class UIEntity;
@ -52,23 +53,23 @@ static PyObject* UIDrawable_get_bounds(T* self, PyObject* Py_UNUSED(args))
// move method implementation (#98) // move method implementation (#98)
template<typename T> template<typename T>
static PyObject* UIDrawable_move(T* self, PyObject* args) static PyObject* UIDrawable_move(T* self, PyObject* args, PyObject* kwds)
{ {
float dx, dy; float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) {
return NULL; return NULL;
} }
self->data->move(dx, dy); self->data->move(dx, dy);
Py_RETURN_NONE; Py_RETURN_NONE;
} }
// resize method implementation (#98) // resize method implementation (#98)
template<typename T> template<typename T>
static PyObject* UIDrawable_resize(T* self, PyObject* args) static PyObject* UIDrawable_resize(T* self, PyObject* args, PyObject* kwds)
{ {
float w, h; float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) { if (!PyPosition_ParseFloat(args, kwds, &w, &h)) {
return NULL; return NULL;
} }
@ -97,23 +98,25 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") \ MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") \
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") \ MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") \
)}, \ )}, \
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS, \ {"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS | METH_KEYWORDS, \
MCRF_METHOD(Drawable, move, \ MCRF_METHOD(Drawable, move, \
MCRF_SIG("(dx: float, dy: float)", "None"), \ MCRF_SIG("(dx, dy) or (delta)", "None"), \
MCRF_DESC("Move the element by a relative offset."), \ MCRF_DESC("Move the element by a relative offset."), \
MCRF_ARGS_START \ MCRF_ARGS_START \
MCRF_ARG("dx", "Horizontal offset in pixels") \ MCRF_ARG("dx", "Horizontal offset in pixels (or use delta)") \
MCRF_ARG("dy", "Vertical offset in pixels") \ MCRF_ARG("dy", "Vertical offset in pixels (or use delta)") \
MCRF_NOTE("This modifies the x and y position properties by the given amounts.") \ MCRF_ARG("delta", "Offset as tuple, list, or Vector: (dx, dy)") \
MCRF_NOTE("Accepts move(dx, dy), move((dx, dy)), move(Vector), or move(pos=(dx, dy)).") \
)}, \ )}, \
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS, \ {"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS | METH_KEYWORDS, \
MCRF_METHOD(Drawable, resize, \ MCRF_METHOD(Drawable, resize, \
MCRF_SIG("(width: float, height: float)", "None"), \ MCRF_SIG("(width, height) or (size)", "None"), \
MCRF_DESC("Resize the element to new dimensions."), \ MCRF_DESC("Resize the element to new dimensions."), \
MCRF_ARGS_START \ MCRF_ARGS_START \
MCRF_ARG("width", "New width in pixels") \ MCRF_ARG("width", "New width in pixels (or use size)") \
MCRF_ARG("height", "New height in pixels") \ MCRF_ARG("height", "New height in pixels (or use size)") \
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") \ MCRF_ARG("size", "Size as tuple, list, or Vector: (width, height)") \
MCRF_NOTE("Accepts resize(w, h), resize((w, h)), resize(Vector), or resize(pos=(w, h)).") \
)} )}
// Macro to add common UIDrawable methods to a method array (includes animate for UIDrawable derivatives) // Macro to add common UIDrawable methods to a method array (includes animate for UIDrawable derivatives)
@ -222,12 +225,12 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
{"on_enter", (getter)UIDrawable::get_on_enter, (setter)UIDrawable::set_on_enter, \ {"on_enter", (getter)UIDrawable::get_on_enter, (setter)UIDrawable::set_on_enter, \
MCRF_PROPERTY(on_enter, \ MCRF_PROPERTY(on_enter, \
"Callback for mouse enter events. " \ "Callback for mouse enter events. " \
"Called with (x, y, button, action) when mouse enters this element's bounds." \ "Called with (pos: Vector, button: str, action: str) when mouse enters this element's bounds." \
), (void*)type_enum}, \ ), (void*)type_enum}, \
{"on_exit", (getter)UIDrawable::get_on_exit, (setter)UIDrawable::set_on_exit, \ {"on_exit", (getter)UIDrawable::get_on_exit, (setter)UIDrawable::set_on_exit, \
MCRF_PROPERTY(on_exit, \ MCRF_PROPERTY(on_exit, \
"Callback for mouse exit events. " \ "Callback for mouse exit events. " \
"Called with (x, y, button, action) when mouse leaves this element's bounds." \ "Called with (pos: Vector, button: str, action: str) when mouse leaves this element's bounds." \
), (void*)type_enum}, \ ), (void*)type_enum}, \
{"hovered", (getter)UIDrawable::get_hovered, NULL, \ {"hovered", (getter)UIDrawable::get_hovered, NULL, \
MCRF_PROPERTY(hovered, \ MCRF_PROPERTY(hovered, \
@ -237,7 +240,7 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
{"on_move", (getter)UIDrawable::get_on_move, (setter)UIDrawable::set_on_move, \ {"on_move", (getter)UIDrawable::get_on_move, (setter)UIDrawable::set_on_move, \
MCRF_PROPERTY(on_move, \ MCRF_PROPERTY(on_move, \
"Callback for mouse movement within bounds. " \ "Callback for mouse movement within bounds. " \
"Called with (x, y, button, action) for each mouse movement while inside. " \ "Called with (pos: Vector, button: str, action: str) for each mouse movement while inside. " \
"Performance note: Called frequently during movement - keep handlers fast." \ "Performance note: Called frequently during movement - keep handlers fast." \
), (void*)type_enum} ), (void*)type_enum}

View file

@ -280,7 +280,7 @@ PyGetSetDef UICaption::getsetters[] = {
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click, MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. " "Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click." "Function receives (pos: Vector, button: str, action: str)."
), (void*)PyObjectsEnum::UICAPTION}, ), (void*)PyObjectsEnum::UICAPTION},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
MCRF_PROPERTY(z_index, MCRF_PROPERTY(z_index,

View file

@ -385,7 +385,7 @@ PyGetSetDef UICircle::getsetters[] = {
{"outline", (getter)UICircle::get_outline, (setter)UICircle::set_outline, {"outline", (getter)UICircle::get_outline, (setter)UICircle::set_outline,
"Outline thickness (0 for no outline)", NULL}, "Outline thickness (0 for no outline)", NULL},
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
"Callable executed when circle is clicked.", (void*)PyObjectsEnum::UICIRCLE}, "Callable executed when circle is clicked. Function receives (pos: Vector, button: str, action: str).", (void*)PyObjectsEnum::UICIRCLE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
"Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UICIRCLE}, "Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UICIRCLE},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,

View file

@ -10,6 +10,7 @@
#include "Animation.h" #include "Animation.h"
#include "PyAnimation.h" #include "PyAnimation.h"
#include "PyEasing.h" #include "PyEasing.h"
#include "PyPositionHelper.h"
// UIDrawable methods now in UIBase.h // UIDrawable methods now in UIBase.h
#include "UIEntityPyMethods.h" #include "UIEntityPyMethods.h"
@ -94,18 +95,17 @@ void UIEntity::updateVisibility()
} }
} }
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) { PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
int x, y; int x, y;
if (!PyArg_ParseTuple(o, "ii", &x, &y)) { if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
PyErr_SetString(PyExc_TypeError, "UIEntity.at requires two integer arguments: (x, y)"); return NULL; // Error already set by PyPosition_ParseInt
return NULL;
} }
if (self->data->grid == NULL) { if (self->data->grid == NULL) {
PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid"); PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid");
return NULL; return NULL;
} }
// Lazy initialize gridstate if needed // Lazy initialize gridstate if needed
if (self->data->gridstate.size() == 0) { if (self->data->gridstate.size() == 0) {
self->data->gridstate.resize(self->data->grid->grid_x * self->data->grid->grid_y); self->data->gridstate.resize(self->data->grid->grid_x * self->data->grid->grid_y);
@ -115,13 +115,13 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
state.discovered = false; state.discovered = false;
} }
} }
// Bounds check // Bounds check
if (x < 0 || x >= self->data->grid->grid_x || y < 0 || y >= self->data->grid->grid_y) { if (x < 0 || x >= self->data->grid->grid_x || y < 0 || y >= self->data->grid->grid_y) {
PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of bounds", x, y); PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of bounds", x, y);
return NULL; return NULL;
} }
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState");
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
Py_DECREF(type); Py_DECREF(type);
@ -590,21 +590,14 @@ PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
} }
PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"target_x", "target_y", "x", "y", nullptr}; int target_x, target_y;
int target_x = -1, target_y = -1;
// Parse position using flexible position helper
// Parse arguments - support both target_x/target_y and x/y parameter names // Supports: path_to(x, y), path_to((x, y)), path_to(pos=(x, y)), path_to(Vector(x, y))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(keywords), if (!PyPosition_ParseInt(args, kwds, &target_x, &target_y)) {
&target_x, &target_y)) { return NULL; // Error already set by PyPosition_ParseInt
PyErr_Clear();
// Try alternative parameter names
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiii", const_cast<char**>(keywords),
&target_x, &target_y, &target_x, &target_y)) {
PyErr_SetString(PyExc_TypeError, "path_to() requires target_x and target_y integer arguments");
return NULL;
}
} }
// Check if entity has a grid // Check if entity has a grid
if (!self->data || !self->data->grid) { if (!self->data || !self->data->grid) {
PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to compute paths"); PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to compute paths");
@ -743,19 +736,32 @@ PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyO
} }
PyMethodDef UIEntity::methods[] = { PyMethodDef UIEntity::methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O}, {"at", (PyCFunction)UIEntity::at, METH_VARARGS | METH_KEYWORDS,
"at(x, y) or at(pos) -> GridPointState\n\n"
"Get the grid point state at the specified position.\n\n"
"Args:\n"
" x, y: Grid coordinates as two integers, OR\n"
" pos: Grid coordinates as tuple, list, or Vector\n\n"
"Returns:\n"
" GridPointState for the entity's view of that grid cell.\n\n"
"Example:\n"
" state = entity.at(5, 3)\n"
" state = entity.at((5, 3))\n"
" state = entity.at(pos=(5, 3))"},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, {"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"}, {"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
"path_to(x: int, y: int) -> bool\n\n" "path_to(x, y) or path_to(target) -> list\n\n"
"Find and follow path to target position using A* pathfinding.\n\n" "Find a path to the target position using Dijkstra pathfinding.\n\n"
"Args:\n" "Args:\n"
" x: Target X coordinate\n" " x, y: Target coordinates as two integers, OR\n"
" y: Target Y coordinate\n\n" " target: Target coordinates as tuple, list, or Vector\n\n"
"Returns:\n" "Returns:\n"
" True if a path was found and the entity started moving, False otherwise\n\n" " List of (x, y) tuples representing the path.\n\n"
"The entity will automatically move along the path over multiple frames.\n" "Example:\n"
"Call this again to change the target or repath."}, " path = entity.path_to(10, 5)\n"
" path = entity.path_to((10, 5))\n"
" path = entity.path_to(pos=(10, 5))"},
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
"update_visibility() -> None\n\n" "update_visibility() -> None\n\n"
"Update entity's visibility state based on current FOV.\n\n" "Update entity's visibility state based on current FOV.\n\n"
@ -799,19 +805,32 @@ PyMethodDef UIEntity_all_methods[] = {
MCRF_RAISES("ValueError", "If property name is not valid for Entity (x, y, sprite_scale, sprite_index)") MCRF_RAISES("ValueError", "If property name is not valid for Entity (x, y, sprite_scale, sprite_index)")
MCRF_NOTE("Entity animations use grid coordinates for x/y, not pixel coordinates.") MCRF_NOTE("Entity animations use grid coordinates for x/y, not pixel coordinates.")
)}, )},
{"at", (PyCFunction)UIEntity::at, METH_O}, {"at", (PyCFunction)UIEntity::at, METH_VARARGS | METH_KEYWORDS,
"at(x, y) or at(pos) -> GridPointState\n\n"
"Get the grid point state at the specified position.\n\n"
"Args:\n"
" x, y: Grid coordinates as two integers, OR\n"
" pos: Grid coordinates as tuple, list, or Vector\n\n"
"Returns:\n"
" GridPointState for the entity's view of that grid cell.\n\n"
"Example:\n"
" state = entity.at(5, 3)\n"
" state = entity.at((5, 3))\n"
" state = entity.at(pos=(5, 3))"},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, {"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"}, {"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
"path_to(x: int, y: int) -> bool\n\n" "path_to(x, y) or path_to(target) -> list\n\n"
"Find and follow path to target position using A* pathfinding.\n\n" "Find a path to the target position using Dijkstra pathfinding.\n\n"
"Args:\n" "Args:\n"
" x: Target X coordinate\n" " x, y: Target coordinates as two integers, OR\n"
" y: Target Y coordinate\n\n" " target: Target coordinates as tuple, list, or Vector\n\n"
"Returns:\n" "Returns:\n"
" True if a path was found and the entity started moving, False otherwise\n\n" " List of (x, y) tuples representing the path.\n\n"
"The entity will automatically move along the path over multiple frames.\n" "Example:\n"
"Call this again to change the target or repath."}, " path = entity.path_to(10, 5)\n"
" path = entity.path_to((10, 5))\n"
" path = entity.path_to(pos=(10, 5))"},
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
"update_visibility() -> None\n\n" "update_visibility() -> None\n\n"
"Update entity's visibility state based on current FOV.\n\n" "Update entity's visibility state based on current FOV.\n\n"

View file

@ -89,7 +89,7 @@ public:
void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; } void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; }
void resize(float w, float h) { /* Entities don't support direct resizing */ } void resize(float w, float h) { /* Entities don't support direct resizing */ }
static PyObject* at(PyUIEntityObject* self, PyObject* o); static PyObject* at(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds); static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds);

View file

@ -444,7 +444,7 @@ PyGetSetDef UIFrame::getsetters[] = {
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click, MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. " "Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click." "Function receives (pos: Vector, button: str, action: str)."
), (void*)PyObjectsEnum::UIFRAME}, ), (void*)PyObjectsEnum::UIFRAME},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
MCRF_PROPERTY(z_index, MCRF_PROPERTY(z_index,

View file

@ -5,6 +5,7 @@
#include "UIEntity.h" #include "UIEntity.h"
#include "Profiler.h" #include "Profiler.h"
#include "PyFOV.h" #include "PyFOV.h"
#include "PyPositionHelper.h" // For standardized position argument parsing
#include <algorithm> #include <algorithm>
#include <cmath> // #142 - for std::floor, std::isnan #include <cmath> // #142 - for std::floor, std::isnan
#include <cstring> // #150 - for strcmp #include <cstring> // #150 - for strcmp
@ -685,15 +686,20 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
// Only fire if within valid grid bounds // Only fire if within valid grid bounds
if (cell_x >= 0 && cell_x < this->grid_x && cell_y >= 0 && cell_y < this->grid_y) { if (cell_x >= 0 && cell_x < this->grid_x && cell_y >= 0 && cell_y < this->grid_y) {
PyObject* args = Py_BuildValue("(ii)", cell_x, cell_y); // Create Vector object for cell position
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args); PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)cell_x, (float)cell_y);
Py_DECREF(args); if (cell_pos) {
if (!result) { PyObject* args = Py_BuildValue("(O)", cell_pos);
std::cerr << "Cell click callback raised an exception:" << std::endl; Py_DECREF(cell_pos);
PyErr_Print(); PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
PyErr_Clear(); Py_DECREF(args);
} else { if (!result) {
Py_DECREF(result); std::cerr << "Cell click callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
} }
} }
} }
@ -709,15 +715,20 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
// Only fire if within valid grid bounds // Only fire if within valid grid bounds
if (cell_x >= 0 && cell_x < this->grid_x && cell_y >= 0 && cell_y < this->grid_y) { if (cell_x >= 0 && cell_x < this->grid_x && cell_y >= 0 && cell_y < this->grid_y) {
PyObject* args = Py_BuildValue("(ii)", cell_x, cell_y); // Create Vector object for cell position
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args); PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)cell_x, (float)cell_y);
Py_DECREF(args); if (cell_pos) {
if (!result) { PyObject* args = Py_BuildValue("(O)", cell_pos);
std::cerr << "Cell click callback raised an exception:" << std::endl; Py_DECREF(cell_pos);
PyErr_Print(); PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
PyErr_Clear(); Py_DECREF(args);
} else { if (!result) {
Py_DECREF(result); std::cerr << "Cell click callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
} }
// Don't return this - no click_callable to call // Don't return this - no click_callable to call
} }
@ -1141,36 +1152,14 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{ {
static const char* keywords[] = {"x", "y", nullptr}; int x, y;
int x = 0, y = 0;
// Use the flexible position parsing helper - accepts:
// First try to parse as two integers // at(x, y), at((x, y)), at([x, y]), at(Vector(x, y)), at(pos=(x, y)), etc.
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(keywords), &x, &y)) { if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
PyErr_Clear(); return NULL; // Error already set by PyPosition_ParseInt
// Try to parse as a single tuple argument
PyObject* pos_tuple = nullptr;
if (PyArg_ParseTuple(args, "O", &pos_tuple)) {
if (PyTuple_Check(pos_tuple) && PyTuple_Size(pos_tuple) == 2) {
PyObject* x_obj = PyTuple_GetItem(pos_tuple, 0);
PyObject* y_obj = PyTuple_GetItem(pos_tuple, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
x = PyLong_AsLong(x_obj);
y = PyLong_AsLong(y_obj);
} else {
PyErr_SetString(PyExc_TypeError, "Grid indices must be integers");
return NULL;
}
} else {
PyErr_SetString(PyExc_TypeError, "at() takes two integers or a tuple of two integers");
return NULL;
}
} else {
PyErr_SetString(PyExc_TypeError, "at() takes two integers or a tuple of two integers");
return NULL;
}
} }
// Range validation // Range validation
if (x < 0 || x >= self->data->grid_x) { if (x < 0 || x >= self->data->grid_x) {
PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)", x, self->data->grid_x); PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)", x, self->data->grid_x);
@ -1349,16 +1338,22 @@ int UIGrid::set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure)
// Python API implementations for TCOD functionality // Python API implementations for TCOD functionality
PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds) PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{ {
static const char* kwlist[] = {"x", "y", "radius", "light_walls", "algorithm", NULL}; static const char* kwlist[] = {"pos", "radius", "light_walls", "algorithm", NULL};
int x, y, radius = 0; PyObject* pos_obj = NULL;
int radius = 0;
int light_walls = 1; int light_walls = 1;
int algorithm = FOV_BASIC; int algorithm = FOV_BASIC;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|ipi", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ipi", const_cast<char**>(kwlist),
&x, &y, &radius, &light_walls, &algorithm)) { &pos_obj, &radius, &light_walls, &algorithm)) {
return NULL; return NULL;
} }
int x, y;
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
return NULL;
}
// Compute FOV // Compute FOV
self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm); self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
@ -1367,33 +1362,42 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
Py_RETURN_NONE; Py_RETURN_NONE;
} }
PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args) PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{ {
int x, y; int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) { if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL; return NULL;
} }
bool in_fov = self->data->isInFOV(x, y); bool in_fov = self->data->isInFOV(x, y);
return PyBool_FromLong(in_fov); return PyBool_FromLong(in_fov);
} }
PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{ {
static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL}; static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL};
int x1, y1, x2, y2; PyObject* start_obj = NULL;
PyObject* end_obj = NULL;
float diagonal_cost = 1.41f; float diagonal_cost = 1.41f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast<char**>(kwlist),
&x1, &y1, &x2, &y2, &diagonal_cost)) { &start_obj, &end_obj, &diagonal_cost)) {
return NULL; return NULL;
} }
int x1, y1, x2, y2;
if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) {
return NULL;
}
if (!PyPosition_FromObjectInt(end_obj, &x2, &y2)) {
return NULL;
}
std::vector<std::pair<int, int>> path = self->data->findPath(x1, y1, x2, y2, diagonal_cost); std::vector<std::pair<int, int>> path = self->data->findPath(x1, y1, x2, y2, diagonal_cost);
PyObject* path_list = PyList_New(path.size()); PyObject* path_list = PyList_New(path.size());
if (!path_list) return NULL; if (!path_list) return NULL;
for (size_t i = 0; i < path.size(); i++) { for (size_t i = 0; i < path.size(); i++) {
PyObject* coord = Py_BuildValue("(ii)", path[i].first, path[i].second); PyObject* coord = Py_BuildValue("(ii)", path[i].first, path[i].second);
if (!coord) { if (!coord) {
@ -1402,80 +1406,93 @@ PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* k
} }
PyList_SET_ITEM(path_list, i, coord); PyList_SET_ITEM(path_list, i, coord);
} }
return path_list; return path_list;
} }
PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds) PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{ {
static const char* kwlist[] = {"root_x", "root_y", "diagonal_cost", NULL}; static const char* kwlist[] = {"root", "diagonal_cost", NULL};
int root_x, root_y; PyObject* root_obj = NULL;
float diagonal_cost = 1.41f; float diagonal_cost = 1.41f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|f", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(kwlist),
&root_x, &root_y, &diagonal_cost)) { &root_obj, &diagonal_cost)) {
return NULL; return NULL;
} }
int root_x, root_y;
if (!PyPosition_FromObjectInt(root_obj, &root_x, &root_y)) {
return NULL;
}
self->data->computeDijkstra(root_x, root_y, diagonal_cost); self->data->computeDijkstra(root_x, root_y, diagonal_cost);
Py_RETURN_NONE; Py_RETURN_NONE;
} }
PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args) PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{ {
int x, y; int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) { if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL; return NULL;
} }
float distance = self->data->getDijkstraDistance(x, y); float distance = self->data->getDijkstraDistance(x, y);
if (distance < 0) { if (distance < 0) {
Py_RETURN_NONE; // Invalid position Py_RETURN_NONE; // Invalid position
} }
return PyFloat_FromDouble(distance); return PyFloat_FromDouble(distance);
} }
PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args) PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{ {
int x, y; int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) { if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL; return NULL;
} }
std::vector<std::pair<int, int>> path = self->data->getDijkstraPath(x, y); std::vector<std::pair<int, int>> path = self->data->getDijkstraPath(x, y);
PyObject* path_list = PyList_New(path.size()); PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) { for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second); PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // Steals reference PyList_SetItem(path_list, i, pos); // Steals reference
} }
return path_list; return path_list;
} }
PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{ {
int x1, y1, x2, y2; static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL};
PyObject* start_obj = NULL;
PyObject* end_obj = NULL;
float diagonal_cost = 1.41f; float diagonal_cost = 1.41f;
static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast<char**>(kwlist),
&start_obj, &end_obj, &diagonal_cost)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast<char**>(kwlist),
&x1, &y1, &x2, &y2, &diagonal_cost)) {
return NULL; return NULL;
} }
int x1, y1, x2, y2;
if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) {
return NULL;
}
if (!PyPosition_FromObjectInt(end_obj, &x2, &y2)) {
return NULL;
}
// Compute A* path // Compute A* path
std::vector<std::pair<int, int>> path = self->data->computeAStarPath(x1, y1, x2, y2, diagonal_cost); std::vector<std::pair<int, int>> path = self->data->computeAStarPath(x1, y1, x2, y2, diagonal_cost);
// Convert to Python list // Convert to Python list
PyObject* path_list = PyList_New(path.size()); PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) { for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second); PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // Steals reference PyList_SetItem(path_list, i, pos); // Steals reference
} }
return path_list; return path_list;
} }
@ -1812,72 +1829,63 @@ PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) {
PyMethodDef UIGrid::methods[] = { PyMethodDef UIGrid::methods[] = {
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" "compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
"Compute field of view from a position.\n\n" "Compute field of view from a position.\n\n"
"Args:\n" "Args:\n"
" x: X coordinate of the viewer\n" " pos: Position as (x, y) tuple, list, or Vector\n"
" y: Y coordinate of the viewer\n"
" radius: Maximum view distance (0 = unlimited)\n" " radius: Maximum view distance (0 = unlimited)\n"
" light_walls: Whether walls are lit when visible\n" " light_walls: Whether walls are lit when visible\n"
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n" " algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
"Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."}, "Updates the internal FOV state. Use is_in_fov(pos) to query visibility."},
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS,
"is_in_fov(x: int, y: int) -> bool\n\n" "is_in_fov(pos) -> bool\n\n"
"Check if a cell is in the field of view.\n\n" "Check if a cell is in the field of view.\n\n"
"Args:\n" "Args:\n"
" x: X coordinate to check\n" " pos: Position as (x, y) tuple, list, or Vector\n\n"
" y: Y coordinate to check\n\n"
"Returns:\n" "Returns:\n"
" True if the cell is visible, False otherwise\n\n" " True if the cell is visible, False otherwise\n\n"
"Must call compute_fov() first to calculate visibility."}, "Must call compute_fov() first to calculate visibility."},
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS, {"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Find A* path between two points.\n\n" "Find A* path between two points.\n\n"
"Args:\n" "Args:\n"
" x1: Starting X coordinate\n" " start: Starting position as (x, y) tuple, list, or Vector\n"
" y1: Starting Y coordinate\n" " end: Target position as (x, y) tuple, list, or Vector\n"
" x2: Target X coordinate\n"
" y2: Target Y coordinate\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n" "Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n"
"Uses A* algorithm with walkability from grid cells."}, "Uses A* algorithm with walkability from grid cells."},
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS, {"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
"compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n" "compute_dijkstra(root, diagonal_cost: float = 1.41) -> None\n\n"
"Compute Dijkstra map from root position.\n\n" "Compute Dijkstra map from root position.\n\n"
"Args:\n" "Args:\n"
" root_x: X coordinate of the root/target\n" " root: Root position as (x, y) tuple, list, or Vector\n"
" root_y: Y coordinate of the root/target\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Precomputes distances from all reachable cells to the root.\n" "Precomputes distances from all reachable cells to the root.\n"
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n" "Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
"Useful for multiple entities pathfinding to the same target."}, "Useful for multiple entities pathfinding to the same target."},
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS, {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n" "get_dijkstra_distance(pos) -> Optional[float]\n\n"
"Get distance from Dijkstra root to position.\n\n" "Get distance from Dijkstra root to position.\n\n"
"Args:\n" "Args:\n"
" x: X coordinate to query\n" " pos: Position as (x, y) tuple, list, or Vector\n\n"
" y: Y coordinate to query\n\n"
"Returns:\n" "Returns:\n"
" Distance as float, or None if position is unreachable or invalid\n\n" " Distance as float, or None if position is unreachable or invalid\n\n"
"Must call compute_dijkstra() first."}, "Must call compute_dijkstra() first."},
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS, {"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n" "get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n"
"Get path from position to Dijkstra root.\n\n" "Get path from position to Dijkstra root.\n\n"
"Args:\n" "Args:\n"
" x: Starting X coordinate\n" " pos: Position as (x, y) tuple, list, or Vector\n\n"
" y: Starting Y coordinate\n\n"
"Returns:\n" "Returns:\n"
" List of (x, y) tuples representing path to root, empty if unreachable\n\n" " List of (x, y) tuples representing path to root, empty if unreachable\n\n"
"Must call compute_dijkstra() first. Path includes start but not root position."}, "Must call compute_dijkstra() first. Path includes start but not root position."},
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS, {"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
"compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "compute_astar_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Compute A* path between two points.\n\n" "Compute A* path between two points.\n\n"
"Args:\n" "Args:\n"
" x1: Starting X coordinate\n" " start: Starting position as (x, y) tuple, list, or Vector\n"
" y1: Starting Y coordinate\n" " end: Target position as (x, y) tuple, list, or Vector\n"
" x2: Target X coordinate\n"
" y2: Target Y coordinate\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n" "Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n"
@ -1917,72 +1925,63 @@ PyMethodDef UIGrid_all_methods[] = {
UIDRAWABLE_METHODS, UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" "compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
"Compute field of view from a position.\n\n" "Compute field of view from a position.\n\n"
"Args:\n" "Args:\n"
" x: X coordinate of the viewer\n" " pos: Position as (x, y) tuple, list, or Vector\n"
" y: Y coordinate of the viewer\n"
" radius: Maximum view distance (0 = unlimited)\n" " radius: Maximum view distance (0 = unlimited)\n"
" light_walls: Whether walls are lit when visible\n" " light_walls: Whether walls are lit when visible\n"
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n" " algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
"Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."}, "Updates the internal FOV state. Use is_in_fov(pos) to query visibility."},
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS,
"is_in_fov(x: int, y: int) -> bool\n\n" "is_in_fov(pos) -> bool\n\n"
"Check if a cell is in the field of view.\n\n" "Check if a cell is in the field of view.\n\n"
"Args:\n" "Args:\n"
" x: X coordinate to check\n" " pos: Position as (x, y) tuple, list, or Vector\n\n"
" y: Y coordinate to check\n\n"
"Returns:\n" "Returns:\n"
" True if the cell is visible, False otherwise\n\n" " True if the cell is visible, False otherwise\n\n"
"Must call compute_fov() first to calculate visibility."}, "Must call compute_fov() first to calculate visibility."},
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS, {"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Find A* path between two points.\n\n" "Find A* path between two points.\n\n"
"Args:\n" "Args:\n"
" x1: Starting X coordinate\n" " start: Starting position as (x, y) tuple, list, or Vector\n"
" y1: Starting Y coordinate\n" " end: Target position as (x, y) tuple, list, or Vector\n"
" x2: Target X coordinate\n"
" y2: Target Y coordinate\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n" "Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n"
"Uses A* algorithm with walkability from grid cells."}, "Uses A* algorithm with walkability from grid cells."},
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS, {"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
"compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n" "compute_dijkstra(root, diagonal_cost: float = 1.41) -> None\n\n"
"Compute Dijkstra map from root position.\n\n" "Compute Dijkstra map from root position.\n\n"
"Args:\n" "Args:\n"
" root_x: X coordinate of the root/target\n" " root: Root position as (x, y) tuple, list, or Vector\n"
" root_y: Y coordinate of the root/target\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Precomputes distances from all reachable cells to the root.\n" "Precomputes distances from all reachable cells to the root.\n"
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n" "Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
"Useful for multiple entities pathfinding to the same target."}, "Useful for multiple entities pathfinding to the same target."},
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS, {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n" "get_dijkstra_distance(pos) -> Optional[float]\n\n"
"Get distance from Dijkstra root to position.\n\n" "Get distance from Dijkstra root to position.\n\n"
"Args:\n" "Args:\n"
" x: X coordinate to query\n" " pos: Position as (x, y) tuple, list, or Vector\n\n"
" y: Y coordinate to query\n\n"
"Returns:\n" "Returns:\n"
" Distance as float, or None if position is unreachable or invalid\n\n" " Distance as float, or None if position is unreachable or invalid\n\n"
"Must call compute_dijkstra() first."}, "Must call compute_dijkstra() first."},
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS, {"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n" "get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n"
"Get path from position to Dijkstra root.\n\n" "Get path from position to Dijkstra root.\n\n"
"Args:\n" "Args:\n"
" x: Starting X coordinate\n" " pos: Position as (x, y) tuple, list, or Vector\n\n"
" y: Starting Y coordinate\n\n"
"Returns:\n" "Returns:\n"
" List of (x, y) tuples representing path to root, empty if unreachable\n\n" " List of (x, y) tuples representing path to root, empty if unreachable\n\n"
"Must call compute_dijkstra() first. Path includes start but not root position."}, "Must call compute_dijkstra() first. Path includes start but not root position."},
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS, {"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
"compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "compute_astar_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Compute A* path between two points.\n\n" "Compute A* path between two points.\n\n"
"Args:\n" "Args:\n"
" x1: Starting X coordinate\n" " start: Starting position as (x, y) tuple, list, or Vector\n"
" y1: Starting Y coordinate\n" " end: Target position as (x, y) tuple, list, or Vector\n"
" x2: Target X coordinate\n"
" y2: Target Y coordinate\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n" "Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n"
@ -2055,7 +2054,7 @@ PyGetSetDef UIGrid::getsetters[] = {
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click, MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. " "Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click." "Function receives (pos: Vector, button: str, action: str)."
), (void*)PyObjectsEnum::UIGRID}, ), (void*)PyObjectsEnum::UIGRID},
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
@ -2083,11 +2082,11 @@ PyGetSetDef UIGrid::getsetters[] = {
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID), UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID),
// #142 - Grid cell mouse events // #142 - Grid cell mouse events
{"on_cell_enter", (getter)UIGrid::get_on_cell_enter, (setter)UIGrid::set_on_cell_enter, {"on_cell_enter", (getter)UIGrid::get_on_cell_enter, (setter)UIGrid::set_on_cell_enter,
"Callback when mouse enters a grid cell. Called with (cell_x, cell_y).", NULL}, "Callback when mouse enters a grid cell. Called with (cell_pos: Vector).", NULL},
{"on_cell_exit", (getter)UIGrid::get_on_cell_exit, (setter)UIGrid::set_on_cell_exit, {"on_cell_exit", (getter)UIGrid::get_on_cell_exit, (setter)UIGrid::set_on_cell_exit,
"Callback when mouse exits a grid cell. Called with (cell_x, cell_y).", NULL}, "Callback when mouse exits a grid cell. Called with (cell_pos: Vector).", NULL},
{"on_cell_click", (getter)UIGrid::get_on_cell_click, (setter)UIGrid::set_on_cell_click, {"on_cell_click", (getter)UIGrid::get_on_cell_click, (setter)UIGrid::set_on_cell_click,
"Callback when a grid cell is clicked. Called with (cell_x, cell_y).", NULL}, "Callback when a grid cell is clicked. Called with (cell_pos: Vector).", NULL},
{"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL, {"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL,
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL}, "Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
{NULL} /* Sentinel */ {NULL} /* Sentinel */
@ -2249,29 +2248,39 @@ void UIGrid::updateCellHover(sf::Vector2f mousepos) {
if (new_cell != hovered_cell) { if (new_cell != hovered_cell) {
// Fire exit callback for old cell // Fire exit callback for old cell
if (hovered_cell.has_value() && on_cell_exit_callable) { if (hovered_cell.has_value() && on_cell_exit_callable) {
PyObject* args = Py_BuildValue("(ii)", hovered_cell->x, hovered_cell->y); // Create Vector object for cell position
PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args); PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)hovered_cell->x, (float)hovered_cell->y);
Py_DECREF(args); if (cell_pos) {
if (!result) { PyObject* args = Py_BuildValue("(O)", cell_pos);
std::cerr << "Cell exit callback raised an exception:" << std::endl; Py_DECREF(cell_pos);
PyErr_Print(); PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args);
PyErr_Clear(); Py_DECREF(args);
} else { if (!result) {
Py_DECREF(result); std::cerr << "Cell exit callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
} }
} }
// Fire enter callback for new cell // Fire enter callback for new cell
if (new_cell.has_value() && on_cell_enter_callable) { if (new_cell.has_value() && on_cell_enter_callable) {
PyObject* args = Py_BuildValue("(ii)", new_cell->x, new_cell->y); // Create Vector object for cell position
PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args); PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)new_cell->x, (float)new_cell->y);
Py_DECREF(args); if (cell_pos) {
if (!result) { PyObject* args = Py_BuildValue("(O)", cell_pos);
std::cerr << "Cell enter callback raised an exception:" << std::endl; Py_DECREF(cell_pos);
PyErr_Print(); PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args);
PyErr_Clear(); Py_DECREF(args);
} else { if (!result) {
Py_DECREF(result); std::cerr << "Cell enter callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
} }
} }

View file

@ -165,11 +165,11 @@ public:
static int set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure); static int set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure);
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args); static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args); static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args); static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115 static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169 static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169

View file

@ -451,7 +451,7 @@ PyGetSetDef UILine::getsetters[] = {
{"thickness", (getter)UILine::get_thickness, (setter)UILine::set_thickness, {"thickness", (getter)UILine::get_thickness, (setter)UILine::set_thickness,
MCRF_PROPERTY(thickness, "Line thickness in pixels."), NULL}, MCRF_PROPERTY(thickness, "Line thickness in pixels."), NULL},
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click, "Callable executed when line is clicked."), MCRF_PROPERTY(on_click, "Callable executed when line is clicked. Function receives (pos: Vector, button: str, action: str)."),
(void*)PyObjectsEnum::UILINE}, (void*)PyObjectsEnum::UILINE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
MCRF_PROPERTY(z_index, "Z-order for rendering (lower values rendered first)."), MCRF_PROPERTY(z_index, "Z-order for rendering (lower values rendered first)."),

View file

@ -343,7 +343,7 @@ PyGetSetDef UISprite::getsetters[] = {
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click, MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. " "Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click." "Function receives (pos: Vector, button: str, action: str)."
), (void*)PyObjectsEnum::UISPRITE}, ), (void*)PyObjectsEnum::UISPRITE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
MCRF_PROPERTY(z_index, MCRF_PROPERTY(z_index,

View file

@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Test that callbacks return Vector objects instead of separate x, y values."""
import sys
import mcrfpy
# Track test results
results = []
def test_click_callback_signature(pos, button, action):
"""Test on_click callback receives Vector."""
# Check if pos is a Vector
if isinstance(pos, mcrfpy.Vector):
results.append(("on_click pos is Vector", True))
print(f"PASS: on_click receives Vector: {pos}")
else:
results.append(("on_click pos is Vector", False))
print(f"FAIL: on_click receives {type(pos).__name__} instead of Vector: {pos}")
# Verify button and action are strings
if isinstance(button, str) and isinstance(action, str):
results.append(("on_click button/action are strings", True))
print(f"PASS: button={button!r}, action={action!r}")
else:
results.append(("on_click button/action are strings", False))
print(f"FAIL: button={type(button).__name__}, action={type(action).__name__}")
def test_on_enter_callback_signature(pos, button, action):
"""Test on_enter callback receives Vector."""
if isinstance(pos, mcrfpy.Vector):
results.append(("on_enter pos is Vector", True))
print(f"PASS: on_enter receives Vector: {pos}")
else:
results.append(("on_enter pos is Vector", False))
print(f"FAIL: on_enter receives {type(pos).__name__} instead of Vector")
def test_on_exit_callback_signature(pos, button, action):
"""Test on_exit callback receives Vector."""
if isinstance(pos, mcrfpy.Vector):
results.append(("on_exit pos is Vector", True))
print(f"PASS: on_exit receives Vector: {pos}")
else:
results.append(("on_exit pos is Vector", False))
print(f"FAIL: on_exit receives {type(pos).__name__} instead of Vector")
def test_on_move_callback_signature(pos, button, action):
"""Test on_move callback receives Vector."""
if isinstance(pos, mcrfpy.Vector):
results.append(("on_move pos is Vector", True))
print(f"PASS: on_move receives Vector: {pos}")
else:
results.append(("on_move pos is Vector", False))
print(f"FAIL: on_move receives {type(pos).__name__} instead of Vector")
def test_cell_click_callback_signature(cell_pos):
"""Test on_cell_click callback receives Vector."""
if isinstance(cell_pos, mcrfpy.Vector):
results.append(("on_cell_click pos is Vector", True))
print(f"PASS: on_cell_click receives Vector: {cell_pos}")
else:
results.append(("on_cell_click pos is Vector", False))
print(f"FAIL: on_cell_click receives {type(cell_pos).__name__} instead of Vector")
def test_cell_enter_callback_signature(cell_pos):
"""Test on_cell_enter callback receives Vector."""
if isinstance(cell_pos, mcrfpy.Vector):
results.append(("on_cell_enter pos is Vector", True))
print(f"PASS: on_cell_enter receives Vector: {cell_pos}")
else:
results.append(("on_cell_enter pos is Vector", False))
print(f"FAIL: on_cell_enter receives {type(cell_pos).__name__} instead of Vector")
def test_cell_exit_callback_signature(cell_pos):
"""Test on_cell_exit callback receives Vector."""
if isinstance(cell_pos, mcrfpy.Vector):
results.append(("on_cell_exit pos is Vector", True))
print(f"PASS: on_cell_exit receives Vector: {cell_pos}")
else:
results.append(("on_cell_exit pos is Vector", False))
print(f"FAIL: on_cell_exit receives {type(cell_pos).__name__} instead of Vector")
def run_test(runtime):
"""Set up test and simulate interactions."""
print("=" * 50)
print("Testing callback Vector return values")
print("=" * 50)
# Create a test scene
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create a Frame with callbacks
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
frame.on_click = test_click_callback_signature
frame.on_enter = test_on_enter_callback_signature
frame.on_exit = test_on_exit_callback_signature
frame.on_move = test_on_move_callback_signature
ui.append(frame)
# Create a Grid with cell callbacks
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(pos=(350, 100), size=(200, 200), grid_size=(10, 10), texture=texture)
grid.on_cell_click = test_cell_click_callback_signature
grid.on_cell_enter = test_cell_enter_callback_signature
grid.on_cell_exit = test_cell_exit_callback_signature
ui.append(grid)
mcrfpy.setScene("test")
print("\n--- Test Setup Complete ---")
print("To test interactively:")
print(" - Click on the Frame (left side) to test on_click")
print(" - Move mouse over Frame to test on_enter/on_exit/on_move")
print(" - Click on the Grid (right side) to test on_cell_click")
print(" - Move mouse over Grid to test on_cell_enter/on_cell_exit")
print("\nPress Escape to exit.")
# For headless testing, simulate a callback call directly
print("\n--- Simulating callback calls ---")
# Test that the callbacks are set up correctly
test_click_callback_signature(mcrfpy.Vector(150, 150), "left", "start")
test_on_enter_callback_signature(mcrfpy.Vector(100, 100), "enter", "start")
test_on_exit_callback_signature(mcrfpy.Vector(300, 300), "exit", "start")
test_on_move_callback_signature(mcrfpy.Vector(125, 175), "move", "start")
test_cell_click_callback_signature(mcrfpy.Vector(5, 3))
test_cell_enter_callback_signature(mcrfpy.Vector(2, 7))
test_cell_exit_callback_signature(mcrfpy.Vector(8, 1))
# Print summary
print("\n" + "=" * 50)
print("SUMMARY")
print("=" * 50)
passed = sum(1 for _, success in results if success)
failed = sum(1 for _, success in results if not success)
print(f"Passed: {passed}")
print(f"Failed: {failed}")
if failed == 0:
print("\nAll tests PASSED!")
sys.exit(0)
else:
print("\nSome tests FAILED!")
for name, success in results:
if not success:
print(f" FAILED: {name}")
sys.exit(1)
# Run the test
mcrfpy.setTimer("test", run_test, 100)

View file

@ -0,0 +1,152 @@
"""Test automation module with new position parsing and Vector returns"""
import mcrfpy
from mcrfpy import automation
import sys
# Track test results
passed = 0
failed = 0
def test(name, condition):
global passed, failed
if condition:
print(f" PASS: {name}")
passed += 1
else:
print(f" FAIL: {name}")
failed += 1
print("Testing automation module updates...")
print()
# Test 1: position() returns Vector
print("1. Testing position() returns Vector...")
pos = automation.position()
test("position() returns Vector type", type(pos).__name__ == "Vector")
test("position has x attribute", hasattr(pos, 'x'))
test("position has y attribute", hasattr(pos, 'y'))
print()
# Test 2: size() returns Vector
print("2. Testing size() returns Vector...")
sz = automation.size()
test("size() returns Vector type", type(sz).__name__ == "Vector")
test("size has x attribute", hasattr(sz, 'x'))
test("size has y attribute", hasattr(sz, 'y'))
test("size.x > 0", sz.x > 0)
test("size.y > 0", sz.y > 0)
print()
# Test 3: onScreen() accepts various position formats
print("3. Testing onScreen() with various position formats...")
# Move mouse to a known position first
automation.moveTo((100, 100))
test("onScreen((100, 100)) with tuple", automation.onScreen((100, 100)) == True)
test("onScreen([50, 50]) with list", automation.onScreen([50, 50]) == True)
test("onScreen(mcrfpy.Vector(200, 200)) with Vector", automation.onScreen(mcrfpy.Vector(200, 200)) == True)
# Should be off-screen (negative)
test("onScreen((-10, -10)) returns False", automation.onScreen((-10, -10)) == False)
print()
# Test 4: moveTo() accepts position as grouped argument
print("4. Testing moveTo() with grouped position...")
automation.moveTo((150, 150))
pos = automation.position()
test("moveTo((150, 150)) moves to correct x", int(pos.x) == 150)
test("moveTo((150, 150)) moves to correct y", int(pos.y) == 150)
automation.moveTo([200, 200])
pos = automation.position()
test("moveTo([200, 200]) with list", int(pos.x) == 200 and int(pos.y) == 200)
automation.moveTo(mcrfpy.Vector(250, 250))
pos = automation.position()
test("moveTo(Vector(250, 250)) with Vector", int(pos.x) == 250 and int(pos.y) == 250)
print()
# Test 5: moveRel() accepts offset as grouped argument
print("5. Testing moveRel() with grouped offset...")
automation.moveTo((100, 100)) # Start position
automation.moveRel((50, 50)) # Relative move
pos = automation.position()
test("moveRel((50, 50)) from (100, 100)", int(pos.x) == 150 and int(pos.y) == 150)
print()
# Test 6: click() accepts optional position as grouped argument
print("6. Testing click() with grouped position...")
# Click at current position (no args should work)
try:
automation.click()
test("click() with no args (current position)", True)
except:
test("click() with no args (current position)", False)
try:
automation.click((200, 200))
test("click((200, 200)) with tuple", True)
except:
test("click((200, 200)) with tuple", False)
try:
automation.click([300, 300], clicks=2)
test("click([300, 300], clicks=2) with list", True)
except:
test("click([300, 300], clicks=2) with list", False)
print()
# Test 7: scroll() accepts position as second grouped argument
print("7. Testing scroll() with grouped position...")
try:
automation.scroll(3) # No position - use current
test("scroll(3) without position", True)
except:
test("scroll(3) without position", False)
try:
automation.scroll(3, (100, 100))
test("scroll(3, (100, 100)) with tuple", True)
except:
test("scroll(3, (100, 100)) with tuple", False)
print()
# Test 8: mouseDown/mouseUp with grouped position
print("8. Testing mouseDown/mouseUp with grouped position...")
try:
automation.mouseDown((100, 100))
automation.mouseUp((100, 100))
test("mouseDown/mouseUp((100, 100)) with tuple", True)
except:
test("mouseDown/mouseUp((100, 100)) with tuple", False)
print()
# Test 9: dragTo() with grouped position
print("9. Testing dragTo() with grouped position...")
automation.moveTo((100, 100))
try:
automation.dragTo((200, 200))
test("dragTo((200, 200)) with tuple", True)
except Exception as e:
print(f" Error: {e}")
test("dragTo((200, 200)) with tuple", False)
print()
# Test 10: dragRel() with grouped offset
print("10. Testing dragRel() with grouped offset...")
automation.moveTo((100, 100))
try:
automation.dragRel((50, 50))
test("dragRel((50, 50)) with tuple", True)
except Exception as e:
print(f" Error: {e}")
test("dragRel((50, 50)) with tuple", False)
print()
# Summary
print("=" * 40)
print(f"Results: {passed} passed, {failed} failed")
if failed == 0:
print("All tests passed!")
sys.exit(0)
else:
print("Some tests failed")
sys.exit(1)

View file

@ -0,0 +1,129 @@
"""Test that Drawable.move() and Drawable.resize() accept flexible position arguments."""
import mcrfpy
import sys
def run_tests():
"""Test the new position parsing for move() and resize()."""
errors = []
# Create a test scene
scene = mcrfpy.Scene("test_drawable_methods")
# Create a Frame to test with (since Drawable is abstract)
frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50))
scene.children.append(frame)
# Test 1: move() with two separate arguments (original behavior)
try:
frame.x = 100
frame.y = 100
frame.move(10, 20)
if not (frame.x == 110 and frame.y == 120):
errors.append(f"move(10, 20) failed: got ({frame.x}, {frame.y}), expected (110, 120)")
else:
print("PASS: move(dx, dy) with two arguments works")
except Exception as e:
errors.append(f"move(10, 20) raised: {e}")
# Test 2: move() with a tuple
try:
frame.x = 100
frame.y = 100
frame.move((15, 25))
if not (frame.x == 115 and frame.y == 125):
errors.append(f"move((15, 25)) failed: got ({frame.x}, {frame.y}), expected (115, 125)")
else:
print("PASS: move((dx, dy)) with tuple works")
except Exception as e:
errors.append(f"move((15, 25)) raised: {e}")
# Test 3: move() with a list
try:
frame.x = 100
frame.y = 100
frame.move([5, 10])
if not (frame.x == 105 and frame.y == 110):
errors.append(f"move([5, 10]) failed: got ({frame.x}, {frame.y}), expected (105, 110)")
else:
print("PASS: move([dx, dy]) with list works")
except Exception as e:
errors.append(f"move([5, 10]) raised: {e}")
# Test 4: move() with a Vector
try:
frame.x = 100
frame.y = 100
vec = mcrfpy.Vector(12, 18)
frame.move(vec)
if not (frame.x == 112 and frame.y == 118):
errors.append(f"move(Vector(12, 18)) failed: got ({frame.x}, {frame.y}), expected (112, 118)")
else:
print("PASS: move(Vector) works")
except Exception as e:
errors.append(f"move(Vector) raised: {e}")
# Test 5: resize() with two separate arguments (original behavior)
try:
frame.resize(200, 150)
if not (frame.w == 200 and frame.h == 150):
errors.append(f"resize(200, 150) failed: got ({frame.w}, {frame.h}), expected (200, 150)")
else:
print("PASS: resize(w, h) with two arguments works")
except Exception as e:
errors.append(f"resize(200, 150) raised: {e}")
# Test 6: resize() with a tuple
try:
frame.resize((180, 120))
if not (frame.w == 180 and frame.h == 120):
errors.append(f"resize((180, 120)) failed: got ({frame.w}, {frame.h}), expected (180, 120)")
else:
print("PASS: resize((w, h)) with tuple works")
except Exception as e:
errors.append(f"resize((180, 120)) raised: {e}")
# Test 7: resize() with a list
try:
frame.resize([100, 80])
if not (frame.w == 100 and frame.h == 80):
errors.append(f"resize([100, 80]) failed: got ({frame.w}, {frame.h}), expected (100, 80)")
else:
print("PASS: resize([w, h]) with list works")
except Exception as e:
errors.append(f"resize([100, 80]) raised: {e}")
# Test 8: resize() with a Vector
try:
vec = mcrfpy.Vector(250, 200)
frame.resize(vec)
if not (frame.w == 250 and frame.h == 200):
errors.append(f"resize(Vector(250, 200)) failed: got ({frame.w}, {frame.h}), expected (250, 200)")
else:
print("PASS: resize(Vector) works")
except Exception as e:
errors.append(f"resize(Vector) raised: {e}")
# Test 9: move() with keyword argument pos
try:
frame.x = 100
frame.y = 100
frame.move(pos=(7, 13))
if not (frame.x == 107 and frame.y == 113):
errors.append(f"move(pos=(7, 13)) failed: got ({frame.x}, {frame.y}), expected (107, 113)")
else:
print("PASS: move(pos=(dx, dy)) with keyword works")
except Exception as e:
errors.append(f"move(pos=(7, 13)) raised: {e}")
# Summary
if errors:
print("\nFAILURES:")
for e in errors:
print(f" - {e}")
sys.exit(1)
else:
print("\nAll tests passed!")
sys.exit(0)
# Run tests
run_tests()

View file

@ -0,0 +1,135 @@
"""Test Entity.at() and Entity.path_to() position argument parsing.
These methods should accept:
- Two separate integers: method(x, y)
- A tuple: method((x, y))
- Keyword arguments: method(x=x, y=y) or method(pos=(x, y))
- A Vector: method(Vector(x, y))
"""
import mcrfpy
import sys
def run_tests():
# Create a grid with some walkable cells
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(320, 320))
# Make the grid walkable
for x in range(10):
for y in range(10):
grid.at(x, y).walkable = True
# Create an entity at (2, 2)
entity = mcrfpy.Entity(grid_pos=(2, 2), grid=grid)
print("Testing Entity.at() position parsing...")
# Test 1: Two separate integers
try:
state1 = entity.at(3, 3)
print(" PASS: entity.at(3, 3)")
except Exception as e:
print(f" FAIL: entity.at(3, 3) - {e}")
return False
# Test 2: Tuple argument
try:
state2 = entity.at((4, 4))
print(" PASS: entity.at((4, 4))")
except Exception as e:
print(f" FAIL: entity.at((4, 4)) - {e}")
return False
# Test 3: Keyword arguments
try:
state3 = entity.at(x=5, y=5)
print(" PASS: entity.at(x=5, y=5)")
except Exception as e:
print(f" FAIL: entity.at(x=5, y=5) - {e}")
return False
# Test 4: pos= keyword argument
try:
state4 = entity.at(pos=(6, 6))
print(" PASS: entity.at(pos=(6, 6))")
except Exception as e:
print(f" FAIL: entity.at(pos=(6, 6)) - {e}")
return False
# Test 5: List argument
try:
state5 = entity.at([7, 7])
print(" PASS: entity.at([7, 7])")
except Exception as e:
print(f" FAIL: entity.at([7, 7]) - {e}")
return False
# Test 6: Vector argument
try:
vec = mcrfpy.Vector(8, 8)
state6 = entity.at(vec)
print(" PASS: entity.at(Vector(8, 8))")
except Exception as e:
print(f" FAIL: entity.at(Vector(8, 8)) - {e}")
return False
print("\nTesting Entity.path_to() position parsing...")
# Test 1: Two separate integers
try:
path1 = entity.path_to(5, 5)
print(" PASS: entity.path_to(5, 5)")
except Exception as e:
print(f" FAIL: entity.path_to(5, 5) - {e}")
return False
# Test 2: Tuple argument
try:
path2 = entity.path_to((6, 6))
print(" PASS: entity.path_to((6, 6))")
except Exception as e:
print(f" FAIL: entity.path_to((6, 6)) - {e}")
return False
# Test 3: Keyword arguments
try:
path3 = entity.path_to(x=7, y=7)
print(" PASS: entity.path_to(x=7, y=7)")
except Exception as e:
print(f" FAIL: entity.path_to(x=7, y=7) - {e}")
return False
# Test 4: pos= keyword argument
try:
path4 = entity.path_to(pos=(8, 8))
print(" PASS: entity.path_to(pos=(8, 8))")
except Exception as e:
print(f" FAIL: entity.path_to(pos=(8, 8)) - {e}")
return False
# Test 5: List argument
try:
path5 = entity.path_to([9, 9])
print(" PASS: entity.path_to([9, 9])")
except Exception as e:
print(f" FAIL: entity.path_to([9, 9]) - {e}")
return False
# Test 6: Vector argument
try:
vec = mcrfpy.Vector(4, 4)
path6 = entity.path_to(vec)
print(" PASS: entity.path_to(Vector(4, 4))")
except Exception as e:
print(f" FAIL: entity.path_to(Vector(4, 4)) - {e}")
return False
print("\nAll tests passed!")
return True
# Run tests immediately (no game loop needed for these)
if run_tests():
print("PASS")
sys.exit(0)
else:
print("FAIL")
sys.exit(1)

View file

@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Test Grid pathfinding methods with new position parsing.
Tests that Grid.find_path, Grid.compute_fov, etc. accept positions
in multiple formats: tuples, lists, Vectors.
"""
import mcrfpy
import sys
def run_tests():
"""Run all grid pathfinding position parsing tests."""
print("Testing Grid pathfinding position parsing...")
# Create a test grid
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture, pos=(0, 0), size=(320, 320))
# Set up walkability: all cells walkable initially
for y in range(10):
for x in range(10):
cell = grid.at((x, y))
cell.walkable = True
# Add a wall in the middle
grid.at((5, 5)).walkable = False
print(" Grid created with walkable cells and one wall at (5,5)")
# ============ Test find_path ============
print("\n Testing find_path...")
# Test with tuple positions
path1 = grid.find_path((0, 0), (3, 3))
assert path1 is not None, "find_path with tuples returned None"
assert len(path1) > 0, "find_path with tuples returned empty path"
print(f" find_path((0,0), (3,3)) -> {len(path1)} steps: PASS")
# Test with list positions
path2 = grid.find_path([0, 0], [3, 3])
assert path2 is not None, "find_path with lists returned None"
assert len(path2) > 0, "find_path with lists returned empty path"
print(f" find_path([0,0], [3,3]) -> {len(path2)} steps: PASS")
# Test with Vector positions
start_vec = mcrfpy.Vector(0, 0)
end_vec = mcrfpy.Vector(3, 3)
path3 = grid.find_path(start_vec, end_vec)
assert path3 is not None, "find_path with Vectors returned None"
assert len(path3) > 0, "find_path with Vectors returned empty path"
print(f" find_path(Vector(0,0), Vector(3,3)) -> {len(path3)} steps: PASS")
# Test path with diagonal_cost parameter
path4 = grid.find_path((0, 0), (3, 3), diagonal_cost=1.41)
assert path4 is not None, "find_path with diagonal_cost returned None"
print(f" find_path with diagonal_cost=1.41: PASS")
# ============ Test compute_fov / is_in_fov ============
print("\n Testing compute_fov / is_in_fov...")
# All cells transparent for FOV testing
for y in range(10):
for x in range(10):
cell = grid.at((x, y))
cell.transparent = True
# Test compute_fov with tuple
grid.compute_fov((5, 5), radius=5)
print(" compute_fov((5,5), radius=5): PASS")
# Test is_in_fov with tuple
in_fov1 = grid.is_in_fov((5, 5))
assert in_fov1 == True, "Center should be in FOV"
print(f" is_in_fov((5,5)) = {in_fov1}: PASS")
# Test is_in_fov with list
in_fov2 = grid.is_in_fov([4, 5])
assert in_fov2 == True, "Adjacent cell should be in FOV"
print(f" is_in_fov([4,5]) = {in_fov2}: PASS")
# Test is_in_fov with Vector
pos_vec = mcrfpy.Vector(6, 5)
in_fov3 = grid.is_in_fov(pos_vec)
assert in_fov3 == True, "Adjacent cell should be in FOV"
print(f" is_in_fov(Vector(6,5)) = {in_fov3}: PASS")
# Test compute_fov with Vector
center_vec = mcrfpy.Vector(3, 3)
grid.compute_fov(center_vec, radius=3)
print(" compute_fov(Vector(3,3), radius=3): PASS")
# ============ Test compute_dijkstra / get_dijkstra_* ============
print("\n Testing Dijkstra methods...")
# Test compute_dijkstra with tuple
grid.compute_dijkstra((0, 0))
print(" compute_dijkstra((0,0)): PASS")
# Test get_dijkstra_distance with tuple
dist1 = grid.get_dijkstra_distance((3, 3))
assert dist1 is not None, "Distance should not be None for reachable cell"
print(f" get_dijkstra_distance((3,3)) = {dist1:.2f}: PASS")
# Test get_dijkstra_distance with list
dist2 = grid.get_dijkstra_distance([2, 2])
assert dist2 is not None, "Distance should not be None for reachable cell"
print(f" get_dijkstra_distance([2,2]) = {dist2:.2f}: PASS")
# Test get_dijkstra_distance with Vector
dist3 = grid.get_dijkstra_distance(mcrfpy.Vector(1, 1))
assert dist3 is not None, "Distance should not be None for reachable cell"
print(f" get_dijkstra_distance(Vector(1,1)) = {dist3:.2f}: PASS")
# Test get_dijkstra_path with tuple
dpath1 = grid.get_dijkstra_path((3, 3))
assert dpath1 is not None, "Dijkstra path should not be None"
print(f" get_dijkstra_path((3,3)) -> {len(dpath1)} steps: PASS")
# Test get_dijkstra_path with Vector
dpath2 = grid.get_dijkstra_path(mcrfpy.Vector(4, 4))
assert dpath2 is not None, "Dijkstra path should not be None"
print(f" get_dijkstra_path(Vector(4,4)) -> {len(dpath2)} steps: PASS")
# ============ Test compute_astar_path ============
print("\n Testing compute_astar_path...")
# Test with tuples
apath1 = grid.compute_astar_path((0, 0), (3, 3))
assert apath1 is not None, "A* path should not be None"
print(f" compute_astar_path((0,0), (3,3)) -> {len(apath1)} steps: PASS")
# Test with lists
apath2 = grid.compute_astar_path([1, 1], [4, 4])
assert apath2 is not None, "A* path should not be None"
print(f" compute_astar_path([1,1], [4,4]) -> {len(apath2)} steps: PASS")
# Test with Vectors
apath3 = grid.compute_astar_path(mcrfpy.Vector(2, 2), mcrfpy.Vector(7, 7))
assert apath3 is not None, "A* path should not be None"
print(f" compute_astar_path(Vector(2,2), Vector(7,7)) -> {len(apath3)} steps: PASS")
print("\n" + "="*50)
print("All grid pathfinding position tests PASSED!")
print("="*50)
return True
# Run tests
try:
success = run_tests()
if success:
print("\nPASS")
sys.exit(0)
except Exception as e:
print(f"\nFAIL: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View file

@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Test ColorLayer and TileLayer position parsing with new PyPositionHelper pattern."""
import sys
import mcrfpy
def test_colorlayer_at():
"""Test ColorLayer.at() with various position formats."""
print("Testing ColorLayer.at() position parsing...")
# Create a grid and color layer
grid = mcrfpy.Grid(grid_size=(10, 10))
layer = mcrfpy.ColorLayer(z_index=-1, grid_size=(10, 10))
grid.layers.append(layer)
# Set a color at position
layer.set((5, 5), mcrfpy.Color(255, 0, 0))
# Test at() with tuple
c1 = layer.at((5, 5))
assert c1.r == 255 and c1.g == 0 and c1.b == 0, f"Failed: tuple position - got {c1.r},{c1.g},{c1.b}"
print(" - tuple position: PASS")
# Test at() with two args
c2 = layer.at(5, 5)
assert c2.r == 255 and c2.g == 0 and c2.b == 0, f"Failed: two args - got {c2.r},{c2.g},{c2.b}"
print(" - two args: PASS")
# Test at() with list (if supported)
c3 = layer.at([5, 5])
assert c3.r == 255 and c3.g == 0 and c3.b == 0, f"Failed: list position - got {c3.r},{c3.g},{c3.b}"
print(" - list position: PASS")
# Test at() with Vector
vec = mcrfpy.Vector(5, 5)
c4 = layer.at(vec)
assert c4.r == 255 and c4.g == 0 and c4.b == 0, f"Failed: Vector position - got {c4.r},{c4.g},{c4.b}"
print(" - Vector position: PASS")
print("ColorLayer.at(): ALL PASS")
def test_colorlayer_set():
"""Test ColorLayer.set() with grouped position."""
print("Testing ColorLayer.set() grouped position...")
grid = mcrfpy.Grid(grid_size=(10, 10))
layer = mcrfpy.ColorLayer(z_index=-1, grid_size=(10, 10))
grid.layers.append(layer)
# Test set() with tuple position
layer.set((3, 4), mcrfpy.Color(0, 255, 0))
c = layer.at((3, 4))
assert c.g == 255, f"Failed: tuple position - got g={c.g}"
print(" - tuple position: PASS")
# Test set() with list position
layer.set([7, 8], (0, 0, 255)) # Also test tuple color
c2 = layer.at((7, 8))
assert c2.b == 255, f"Failed: list position - got b={c2.b}"
print(" - list position: PASS")
# Test set() with Vector position
layer.set(mcrfpy.Vector(1, 1), mcrfpy.Color(128, 128, 128))
c3 = layer.at((1, 1))
assert c3.r == 128, f"Failed: Vector position - got r={c3.r}"
print(" - Vector position: PASS")
print("ColorLayer.set(): ALL PASS")
def test_tilelayer_at():
"""Test TileLayer.at() with various position formats."""
print("Testing TileLayer.at() position parsing...")
# Create a grid and tile layer
grid = mcrfpy.Grid(grid_size=(10, 10))
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
layer = mcrfpy.TileLayer(z_index=-1, texture=texture, grid_size=(10, 10))
grid.layers.append(layer)
# Set a tile at position
layer.set((5, 5), 42)
# Test at() with tuple
t1 = layer.at((5, 5))
assert t1 == 42, f"Failed: tuple position - got {t1}"
print(" - tuple position: PASS")
# Test at() with two args
t2 = layer.at(5, 5)
assert t2 == 42, f"Failed: two args - got {t2}"
print(" - two args: PASS")
# Test at() with list
t3 = layer.at([5, 5])
assert t3 == 42, f"Failed: list position - got {t3}"
print(" - list position: PASS")
# Test at() with Vector
t4 = layer.at(mcrfpy.Vector(5, 5))
assert t4 == 42, f"Failed: Vector position - got {t4}"
print(" - Vector position: PASS")
print("TileLayer.at(): ALL PASS")
def test_tilelayer_set():
"""Test TileLayer.set() with grouped position."""
print("Testing TileLayer.set() grouped position...")
grid = mcrfpy.Grid(grid_size=(10, 10))
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
layer = mcrfpy.TileLayer(z_index=-1, texture=texture, grid_size=(10, 10))
grid.layers.append(layer)
# Test set() with tuple position
layer.set((3, 4), 10)
assert layer.at((3, 4)) == 10, "Failed: tuple position"
print(" - tuple position: PASS")
# Test set() with list position
layer.set([7, 8], 20)
assert layer.at((7, 8)) == 20, "Failed: list position"
print(" - list position: PASS")
# Test set() with Vector position
layer.set(mcrfpy.Vector(1, 1), 30)
assert layer.at((1, 1)) == 30, "Failed: Vector position"
print(" - Vector position: PASS")
print("TileLayer.set(): ALL PASS")
# Run all tests
try:
test_colorlayer_at()
test_colorlayer_set()
test_tilelayer_at()
test_tilelayer_set()
print("\n=== ALL TESTS PASSED ===")
sys.exit(0)
except AssertionError as e:
print(f"\nTEST FAILED: {e}")
sys.exit(1)
except Exception as e:
print(f"\nTEST ERROR: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View file

@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Test script for PyPositionHelper - validates Grid.at() position parsing.
This tests the standardized position argument parsing that supports:
- Two separate args: func(x, y)
- A tuple: func((x, y))
- A list: func([x, y])
- A Vector object: func(Vector(x, y))
- Keyword args: func(x=x, y=y) or func(pos=(x,y))
"""
import sys
import mcrfpy
def test_grid_at_position_parsing():
"""Test all the different ways to call Grid.at() with positions."""
# Create a test scene and grid
scene = mcrfpy.Scene("test_position")
# Create a grid with enough cells to test indexing
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
errors = []
# Test 1: Two separate integer arguments
try:
point1 = grid.at(3, 4)
if point1 is None:
errors.append("Test 1 FAIL: grid.at(3, 4) returned None")
else:
print("Test 1 PASS: grid.at(3, 4) works")
except Exception as e:
errors.append(f"Test 1 FAIL: grid.at(3, 4) raised {type(e).__name__}: {e}")
# Test 2: Tuple argument
try:
point2 = grid.at((5, 6))
if point2 is None:
errors.append("Test 2 FAIL: grid.at((5, 6)) returned None")
else:
print("Test 2 PASS: grid.at((5, 6)) works")
except Exception as e:
errors.append(f"Test 2 FAIL: grid.at((5, 6)) raised {type(e).__name__}: {e}")
# Test 3: List argument
try:
point3 = grid.at([7, 8])
if point3 is None:
errors.append("Test 3 FAIL: grid.at([7, 8]) returned None")
else:
print("Test 3 PASS: grid.at([7, 8]) works")
except Exception as e:
errors.append(f"Test 3 FAIL: grid.at([7, 8]) raised {type(e).__name__}: {e}")
# Test 4: Vector argument
try:
vec = mcrfpy.Vector(2, 3)
point4 = grid.at(vec)
if point4 is None:
errors.append("Test 4 FAIL: grid.at(Vector(2, 3)) returned None")
else:
print("Test 4 PASS: grid.at(Vector(2, 3)) works")
except Exception as e:
errors.append(f"Test 4 FAIL: grid.at(Vector(2, 3)) raised {type(e).__name__}: {e}")
# Test 5: Keyword arguments x=, y=
try:
point5 = grid.at(x=1, y=2)
if point5 is None:
errors.append("Test 5 FAIL: grid.at(x=1, y=2) returned None")
else:
print("Test 5 PASS: grid.at(x=1, y=2) works")
except Exception as e:
errors.append(f"Test 5 FAIL: grid.at(x=1, y=2) raised {type(e).__name__}: {e}")
# Test 6: pos= keyword with tuple
try:
point6 = grid.at(pos=(4, 5))
if point6 is None:
errors.append("Test 6 FAIL: grid.at(pos=(4, 5)) returned None")
else:
print("Test 6 PASS: grid.at(pos=(4, 5)) works")
except Exception as e:
errors.append(f"Test 6 FAIL: grid.at(pos=(4, 5)) raised {type(e).__name__}: {e}")
# Test 7: pos= keyword with Vector
try:
vec2 = mcrfpy.Vector(6, 7)
point7 = grid.at(pos=vec2)
if point7 is None:
errors.append("Test 7 FAIL: grid.at(pos=Vector(6, 7)) returned None")
else:
print("Test 7 PASS: grid.at(pos=Vector(6, 7)) works")
except Exception as e:
errors.append(f"Test 7 FAIL: grid.at(pos=Vector(6, 7)) raised {type(e).__name__}: {e}")
# Test 8: pos= keyword with list
try:
point8 = grid.at(pos=[8, 9])
if point8 is None:
errors.append("Test 8 FAIL: grid.at(pos=[8, 9]) returned None")
else:
print("Test 8 PASS: grid.at(pos=[8, 9]) works")
except Exception as e:
errors.append(f"Test 8 FAIL: grid.at(pos=[8, 9]) raised {type(e).__name__}: {e}")
# Test 9: Out of range should raise IndexError (not TypeError)
try:
grid.at(100, 100)
errors.append("Test 9 FAIL: grid.at(100, 100) should have raised IndexError")
except IndexError:
print("Test 9 PASS: grid.at(100, 100) raises IndexError")
except Exception as e:
errors.append(f"Test 9 FAIL: grid.at(100, 100) raised {type(e).__name__} instead of IndexError: {e}")
# Test 10: Invalid type should raise TypeError
try:
grid.at("invalid")
errors.append("Test 10 FAIL: grid.at('invalid') should have raised TypeError")
except TypeError:
print("Test 10 PASS: grid.at('invalid') raises TypeError")
except Exception as e:
errors.append(f"Test 10 FAIL: grid.at('invalid') raised {type(e).__name__} instead of TypeError: {e}")
# Test 11: Float integers should work (e.g., 3.0 is valid as int)
try:
point11 = grid.at(3.0, 4.0)
if point11 is None:
errors.append("Test 11 FAIL: grid.at(3.0, 4.0) returned None")
else:
print("Test 11 PASS: grid.at(3.0, 4.0) works (float integers)")
except Exception as e:
errors.append(f"Test 11 FAIL: grid.at(3.0, 4.0) raised {type(e).__name__}: {e}")
# Test 12: Non-integer float should raise TypeError
try:
grid.at(3.5, 4.5)
errors.append("Test 12 FAIL: grid.at(3.5, 4.5) should have raised TypeError")
except TypeError:
print("Test 12 PASS: grid.at(3.5, 4.5) raises TypeError for non-integer floats")
except Exception as e:
errors.append(f"Test 12 FAIL: grid.at(3.5, 4.5) raised {type(e).__name__} instead of TypeError: {e}")
# Summary
print()
print("=" * 50)
if errors:
print(f"FAILED: {len(errors)} test(s) failed")
for err in errors:
print(f" - {err}")
sys.exit(1)
else:
print("SUCCESS: All 12 tests passed!")
sys.exit(0)
# Run tests immediately (no game loop needed for this)
test_grid_at_position_parsing()