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:
parent
016ca693b5
commit
d2e4791f5a
25 changed files with 2109 additions and 636 deletions
|
|
@ -4,6 +4,7 @@
|
|||
#include "PyColor.h"
|
||||
#include "PyTexture.h"
|
||||
#include "PyFOV.h"
|
||||
#include "PyPositionHelper.h"
|
||||
#include <sstream>
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -562,10 +563,18 @@ void TileLayer::render(sf::RenderTarget& target,
|
|||
// =============================================================================
|
||||
|
||||
PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = {
|
||||
{"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS,
|
||||
"at(x, y) -> Color\n\nGet the color at cell position (x, y)."},
|
||||
{"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS | METH_KEYWORDS,
|
||||
"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(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(color)\n\nFill the entire layer with the specified color."},
|
||||
{"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;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args) {
|
||||
PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -678,9 +687,14 @@ PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args
|
|||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) {
|
||||
int x, y;
|
||||
PyObject* pos_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;
|
||||
}
|
||||
|
||||
|
|
@ -1108,10 +1122,18 @@ PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) {
|
|||
// =============================================================================
|
||||
|
||||
PyMethodDef PyGridLayerAPI::TileLayer_methods[] = {
|
||||
{"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS,
|
||||
"at(x, y) -> int\n\nGet the tile index at cell position (x, y). Returns -1 if no tile."},
|
||||
{"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS | METH_KEYWORDS,
|
||||
"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(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(index)\n\nFill the entire layer with the specified tile index."},
|
||||
{"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;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) {
|
||||
PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1210,8 +1232,14 @@ PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args)
|
|||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) {
|
||||
int x, y, index;
|
||||
if (!PyArg_ParseTuple(args, "iii", &x, &y, &index)) {
|
||||
PyObject* pos_obj;
|
||||
int index;
|
||||
if (!PyArg_ParseTuple(args, "Oi", &pos_obj, &index)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int x, y;
|
||||
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class PyGridLayerAPI {
|
|||
public:
|
||||
// ColorLayer methods
|
||||
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_fill(PyColorLayerObject* self, PyObject* args);
|
||||
static PyObject* ColorLayer_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
|
|
@ -217,7 +217,7 @@ public:
|
|||
|
||||
// TileLayer methods
|
||||
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_fill(PyTileLayerObject* self, PyObject* args);
|
||||
static PyObject* TileLayer_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "McRFPy_Automation.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "GameEngine.h"
|
||||
#include "PyPositionHelper.h"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
|
@ -235,43 +236,69 @@ PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Get current mouse position
|
||||
// Get current mouse position - returns Vector object
|
||||
PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) {
|
||||
int x, y;
|
||||
|
||||
auto engine = getGameEngine();
|
||||
if (!engine || !engine->getRenderTargetPtr()) {
|
||||
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
|
||||
x = simulated_mouse_pos.x;
|
||||
y = simulated_mouse_pos.y;
|
||||
}
|
||||
|
||||
// In headless mode, return the simulated mouse position (#111)
|
||||
if (engine->isHeadless()) {
|
||||
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
|
||||
else if (engine->isHeadless()) {
|
||||
// In headless mode, return the simulated mouse position (#111)
|
||||
x = simulated_mouse_pos.x;
|
||||
y = simulated_mouse_pos.y;
|
||||
}
|
||||
|
||||
// In windowed mode, return the actual mouse position relative to window
|
||||
if (auto* window = dynamic_cast<sf::RenderWindow*>(engine->getRenderTargetPtr())) {
|
||||
else if (auto* window = dynamic_cast<sf::RenderWindow*>(engine->getRenderTargetPtr())) {
|
||||
// In windowed mode, return the actual mouse position relative to window
|
||||
sf::Vector2i pos = sf::Mouse::getPosition(*window);
|
||||
return Py_BuildValue("(ii)", pos.x, pos.y);
|
||||
x = pos.x;
|
||||
y = pos.y;
|
||||
}
|
||||
else {
|
||||
// Fallback to simulated position
|
||||
x = simulated_mouse_pos.x;
|
||||
y = simulated_mouse_pos.y;
|
||||
}
|
||||
|
||||
// Fallback to simulated position
|
||||
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
|
||||
// Return a Vector object - get type from module to ensure we use the initialized type
|
||||
auto vector_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!vector_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Vector type not found in mcrfpy module");
|
||||
return NULL;
|
||||
}
|
||||
PyObject* result = PyObject_CallFunction((PyObject*)vector_type, "ff", (float)x, (float)y);
|
||||
Py_DECREF(vector_type);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get screen size
|
||||
// Get screen size - returns Vector object
|
||||
PyObject* McRFPy_Automation::_size(PyObject* self, PyObject* args) {
|
||||
// Get Vector type from module to ensure we use the initialized type
|
||||
auto vector_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!vector_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Vector type not found in mcrfpy module");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto engine = getGameEngine();
|
||||
if (!engine || !engine->getRenderTargetPtr()) {
|
||||
return Py_BuildValue("(ii)", 1024, 768); // Default size
|
||||
PyObject* result = PyObject_CallFunction((PyObject*)vector_type, "ff", 1024.0f, 768.0f); // Default size
|
||||
Py_DECREF(vector_type);
|
||||
return result;
|
||||
}
|
||||
|
||||
sf::Vector2u size = engine->getRenderTarget().getSize();
|
||||
return Py_BuildValue("(ii)", size.x, size.y);
|
||||
PyObject* result = PyObject_CallFunction((PyObject*)vector_type, "ff", (float)size.x, (float)size.y);
|
||||
Py_DECREF(vector_type);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if coordinates are on screen
|
||||
PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) {
|
||||
// Check if coordinates are on screen - accepts onScreen(x, y) or onScreen(pos)
|
||||
PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
if (!PyPosition_ParseInt(args, kwargs, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -288,14 +315,19 @@ PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) {
|
|||
}
|
||||
}
|
||||
|
||||
// Move mouse to position
|
||||
// Move mouse to position - accepts moveTo(pos, duration)
|
||||
PyObject* McRFPy_Automation::_moveTo(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "duration", NULL};
|
||||
int x, y;
|
||||
static const char* kwlist[] = {"pos", "duration", NULL};
|
||||
PyObject* pos_obj;
|
||||
float duration = 0.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
|
||||
&x, &y, &duration)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|f", const_cast<char**>(kwlist),
|
||||
&pos_obj, &duration)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int x, y;
|
||||
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -309,23 +341,29 @@ PyObject* McRFPy_Automation::_moveTo(PyObject* self, PyObject* args, PyObject* k
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Move mouse relative
|
||||
// Move mouse relative - accepts moveRel(offset, duration)
|
||||
PyObject* McRFPy_Automation::_moveRel(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"xOffset", "yOffset", "duration", NULL};
|
||||
int xOffset, yOffset;
|
||||
static const char* kwlist[] = {"offset", "duration", NULL};
|
||||
PyObject* offset_obj;
|
||||
float duration = 0.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
|
||||
&xOffset, &yOffset, &duration)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|f", const_cast<char**>(kwlist),
|
||||
&offset_obj, &duration)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current position
|
||||
int xOffset, yOffset;
|
||||
if (!PyPosition_FromObjectInt(offset_obj, &xOffset, &yOffset)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current position from Vector
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
// Extract position from Vector object
|
||||
int currentX, currentY;
|
||||
if (!PyArg_ParseTuple(pos, "ii", ¤tX, ¤tY)) {
|
||||
if (!PyPosition_FromObjectInt(pos, ¤tX, ¤tY)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -341,29 +379,35 @@ PyObject* McRFPy_Automation::_moveRel(PyObject* self, PyObject* args, PyObject*
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Click implementation
|
||||
// Click implementation - accepts click(pos, clicks, interval, button) or click() for current position
|
||||
PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "clicks", "interval", "button", NULL};
|
||||
int x = -1, y = -1;
|
||||
static const char* kwlist[] = {"pos", "clicks", "interval", "button", NULL};
|
||||
PyObject* pos_obj = Py_None;
|
||||
int clicks = 1;
|
||||
float interval = 0.0f;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iiifs", const_cast<char**>(kwlist),
|
||||
&x, &y, &clicks, &interval, &button)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Oifs", const_cast<char**>(kwlist),
|
||||
&pos_obj, &clicks, &interval, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
int x, y;
|
||||
|
||||
// If no position specified or None, use current position
|
||||
if (pos_obj == Py_None) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
if (!PyPosition_FromObjectInt(pos, &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
} else {
|
||||
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine button
|
||||
|
|
@ -391,40 +435,42 @@ PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kw
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Right click
|
||||
// Right click - accepts rightClick(pos) or rightClick() for current position
|
||||
PyObject* McRFPy_Automation::_rightClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
static const char* kwlist[] = {"pos", NULL};
|
||||
PyObject* pos_obj = Py_None;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast<char**>(kwlist), &pos_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Build new args with button="right"
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("right"));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
if (pos_obj != Py_None) {
|
||||
PyDict_SetItemString(newKwargs, "pos", pos_obj);
|
||||
}
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Double click
|
||||
// Double click - accepts doubleClick(pos) or doubleClick() for current position
|
||||
PyObject* McRFPy_Automation::_doubleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
static const char* kwlist[] = {"pos", NULL};
|
||||
PyObject* pos_obj = Py_None;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast<char**>(kwlist), &pos_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(2));
|
||||
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
if (pos_obj != Py_None) {
|
||||
PyDict_SetItemString(newKwargs, "pos", pos_obj);
|
||||
}
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
|
|
@ -540,27 +586,33 @@ PyObject* McRFPy_Automation::_hotkey(PyObject* self, PyObject* args) {
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Scroll wheel
|
||||
// Scroll wheel - accepts scroll(clicks, pos) or scroll(clicks) for current position
|
||||
PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"clicks", "x", "y", NULL};
|
||||
static const char* kwlist[] = {"clicks", "pos", NULL};
|
||||
int clicks;
|
||||
int x = -1, y = -1;
|
||||
PyObject* pos_obj = Py_None;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", const_cast<char**>(kwlist),
|
||||
&clicks, &x, &y)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|O", const_cast<char**>(kwlist),
|
||||
&clicks, &pos_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
int x, y;
|
||||
|
||||
// If no position specified or None, use current position
|
||||
if (pos_obj == Py_None) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
if (!PyPosition_FromObjectInt(pos, &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
} else {
|
||||
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Inject scroll event
|
||||
|
|
@ -569,65 +621,74 @@ PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* k
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Other click types using the main click function
|
||||
// Other click types using the main click function - accepts middleClick(pos) or middleClick()
|
||||
PyObject* McRFPy_Automation::_middleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
static const char* kwlist[] = {"pos", NULL};
|
||||
PyObject* pos_obj = Py_None;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast<char**>(kwlist), &pos_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("middle"));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
if (pos_obj != Py_None) {
|
||||
PyDict_SetItemString(newKwargs, "pos", pos_obj);
|
||||
}
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Triple click - accepts tripleClick(pos) or tripleClick()
|
||||
PyObject* McRFPy_Automation::_tripleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
static const char* kwlist[] = {"pos", NULL};
|
||||
PyObject* pos_obj = Py_None;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast<char**>(kwlist), &pos_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(3));
|
||||
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
if (pos_obj != Py_None) {
|
||||
PyDict_SetItemString(newKwargs, "pos", pos_obj);
|
||||
}
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Mouse button press/release
|
||||
// Mouse button press/release - accepts mouseDown(pos, button) or mouseDown() for current position
|
||||
PyObject* McRFPy_Automation::_mouseDown(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "button", NULL};
|
||||
int x = -1, y = -1;
|
||||
static const char* kwlist[] = {"pos", "button", NULL};
|
||||
PyObject* pos_obj = Py_None;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
|
||||
&x, &y, &button)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Os", const_cast<char**>(kwlist),
|
||||
&pos_obj, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
int x, y;
|
||||
|
||||
// If no position specified or None, use current position
|
||||
if (pos_obj == Py_None) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
if (!PyPosition_FromObjectInt(pos, &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
} else {
|
||||
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
sf::Mouse::Button sfButton = sf::Mouse::Left;
|
||||
|
|
@ -641,26 +702,33 @@ PyObject* McRFPy_Automation::_mouseDown(PyObject* self, PyObject* args, PyObject
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Mouse up - accepts mouseUp(pos, button) or mouseUp() for current position
|
||||
PyObject* McRFPy_Automation::_mouseUp(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "button", NULL};
|
||||
int x = -1, y = -1;
|
||||
static const char* kwlist[] = {"pos", "button", NULL};
|
||||
PyObject* pos_obj = Py_None;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
|
||||
&x, &y, &button)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Os", const_cast<char**>(kwlist),
|
||||
&pos_obj, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
int x, y;
|
||||
|
||||
// If no position specified or None, use current position
|
||||
if (pos_obj == Py_None) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
if (!PyPosition_FromObjectInt(pos, &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
} else {
|
||||
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
sf::Mouse::Button sfButton = sf::Mouse::Left;
|
||||
|
|
@ -674,15 +742,20 @@ PyObject* McRFPy_Automation::_mouseUp(PyObject* self, PyObject* args, PyObject*
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Drag operations
|
||||
// Drag operations - accepts dragTo(pos, duration, button)
|
||||
PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "duration", "button", NULL};
|
||||
int x, y;
|
||||
static const char* kwlist[] = {"pos", "duration", "button", NULL};
|
||||
PyObject* pos_obj;
|
||||
float duration = 0.0f;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
|
||||
&x, &y, &duration, &button)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|fs", const_cast<char**>(kwlist),
|
||||
&pos_obj, &duration, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int x, y;
|
||||
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -691,19 +764,20 @@ PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* k
|
|||
if (!pos) return NULL;
|
||||
|
||||
int startX, startY;
|
||||
if (!PyArg_ParseTuple(pos, "ii", &startX, &startY)) {
|
||||
if (!PyPosition_FromObjectInt(pos, &startX, &startY)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
|
||||
// Mouse down at current position
|
||||
PyObject* downArgs = Py_BuildValue("(ii)", startX, startY);
|
||||
// Mouse down at current position - create position tuple for the call
|
||||
PyObject* startPosObj = Py_BuildValue("(ii)", startX, startY);
|
||||
PyObject* downKwargs = PyDict_New();
|
||||
PyDict_SetItemString(downKwargs, "pos", startPosObj);
|
||||
PyDict_SetItemString(downKwargs, "button", PyUnicode_FromString(button));
|
||||
|
||||
PyObject* downResult = _mouseDown(self, downArgs, downKwargs);
|
||||
Py_DECREF(downArgs);
|
||||
PyObject* downResult = _mouseDown(self, PyTuple_New(0), downKwargs);
|
||||
Py_DECREF(startPosObj);
|
||||
Py_DECREF(downKwargs);
|
||||
if (!downResult) return NULL;
|
||||
Py_DECREF(downResult);
|
||||
|
|
@ -723,12 +797,13 @@ PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* k
|
|||
}
|
||||
|
||||
// Mouse up at target position
|
||||
PyObject* upArgs = Py_BuildValue("(ii)", x, y);
|
||||
PyObject* endPosObj = Py_BuildValue("(ii)", x, y);
|
||||
PyObject* upKwargs = PyDict_New();
|
||||
PyDict_SetItemString(upKwargs, "pos", endPosObj);
|
||||
PyDict_SetItemString(upKwargs, "button", PyUnicode_FromString(button));
|
||||
|
||||
PyObject* upResult = _mouseUp(self, upArgs, upKwargs);
|
||||
Py_DECREF(upArgs);
|
||||
PyObject* upResult = _mouseUp(self, PyTuple_New(0), upKwargs);
|
||||
Py_DECREF(endPosObj);
|
||||
Py_DECREF(upKwargs);
|
||||
if (!upResult) return NULL;
|
||||
Py_DECREF(upResult);
|
||||
|
|
@ -736,14 +811,20 @@ PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* k
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Drag relative - accepts dragRel(offset, duration, button)
|
||||
PyObject* McRFPy_Automation::_dragRel(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"xOffset", "yOffset", "duration", "button", NULL};
|
||||
int xOffset, yOffset;
|
||||
static const char* kwlist[] = {"offset", "duration", "button", NULL};
|
||||
PyObject* offset_obj;
|
||||
float duration = 0.0f;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
|
||||
&xOffset, &yOffset, &duration, &button)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|fs", const_cast<char**>(kwlist),
|
||||
&offset_obj, &duration, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int xOffset, yOffset;
|
||||
if (!PyPosition_FromObjectInt(offset_obj, &xOffset, &yOffset)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -752,20 +833,21 @@ PyObject* McRFPy_Automation::_dragRel(PyObject* self, PyObject* args, PyObject*
|
|||
if (!pos) return NULL;
|
||||
|
||||
int currentX, currentY;
|
||||
if (!PyArg_ParseTuple(pos, "ii", ¤tX, ¤tY)) {
|
||||
if (!PyPosition_FromObjectInt(pos, ¤tX, ¤tY)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
|
||||
// Call dragTo with absolute position
|
||||
PyObject* dragArgs = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset);
|
||||
PyObject* targetPos = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset);
|
||||
PyObject* dragKwargs = PyDict_New();
|
||||
PyDict_SetItemString(dragKwargs, "pos", targetPos);
|
||||
PyDict_SetItemString(dragKwargs, "duration", PyFloat_FromDouble(duration));
|
||||
PyDict_SetItemString(dragKwargs, "button", PyUnicode_FromString(button));
|
||||
|
||||
PyObject* result = _dragTo(self, dragArgs, dragKwargs);
|
||||
Py_DECREF(dragArgs);
|
||||
PyObject* result = _dragTo(self, PyTuple_New(0), dragKwargs);
|
||||
Py_DECREF(targetPos);
|
||||
Py_DECREF(dragKwargs);
|
||||
|
||||
return result;
|
||||
|
|
@ -777,37 +859,37 @@ static PyMethodDef automationMethods[] = {
|
|||
"screenshot(filename) - Save a screenshot to the specified file"},
|
||||
|
||||
{"position", McRFPy_Automation::_position, METH_NOARGS,
|
||||
"position() - Get current mouse position as (x, y) tuple"},
|
||||
"position() - Get current mouse position as Vector"},
|
||||
{"size", McRFPy_Automation::_size, METH_NOARGS,
|
||||
"size() - Get screen size as (width, height) tuple"},
|
||||
{"onScreen", McRFPy_Automation::_onScreen, METH_VARARGS,
|
||||
"onScreen(x, y) - Check if coordinates are within screen bounds"},
|
||||
"size() - Get screen size as Vector"},
|
||||
{"onScreen", (PyCFunction)McRFPy_Automation::_onScreen, METH_VARARGS | METH_KEYWORDS,
|
||||
"onScreen(pos) - Check if position is within screen bounds. Accepts (x,y) tuple, [x,y] list, or Vector."},
|
||||
|
||||
{"moveTo", (PyCFunction)McRFPy_Automation::_moveTo, METH_VARARGS | METH_KEYWORDS,
|
||||
"moveTo(x, y, duration=0.0) - Move mouse to absolute position"},
|
||||
"moveTo(pos, duration=0.0) - Move mouse to position. Accepts (x,y) tuple, [x,y] list, or Vector."},
|
||||
{"moveRel", (PyCFunction)McRFPy_Automation::_moveRel, METH_VARARGS | METH_KEYWORDS,
|
||||
"moveRel(xOffset, yOffset, duration=0.0) - Move mouse relative to current position"},
|
||||
"moveRel(offset, duration=0.0) - Move mouse relative to current position. Accepts (x,y) tuple, [x,y] list, or Vector."},
|
||||
{"dragTo", (PyCFunction)McRFPy_Automation::_dragTo, METH_VARARGS | METH_KEYWORDS,
|
||||
"dragTo(x, y, duration=0.0, button='left') - Drag mouse to position"},
|
||||
"dragTo(pos, duration=0.0, button='left') - Drag mouse to position. Accepts (x,y) tuple, [x,y] list, or Vector."},
|
||||
{"dragRel", (PyCFunction)McRFPy_Automation::_dragRel, METH_VARARGS | METH_KEYWORDS,
|
||||
"dragRel(xOffset, yOffset, duration=0.0, button='left') - Drag mouse relative to current position"},
|
||||
"dragRel(offset, duration=0.0, button='left') - Drag mouse relative to current position. Accepts (x,y) tuple, [x,y] list, or Vector."},
|
||||
|
||||
{"click", (PyCFunction)McRFPy_Automation::_click, METH_VARARGS | METH_KEYWORDS,
|
||||
"click(x=None, y=None, clicks=1, interval=0.0, button='left') - Click at position"},
|
||||
"click(pos=None, clicks=1, interval=0.0, button='left') - Click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."},
|
||||
{"rightClick", (PyCFunction)McRFPy_Automation::_rightClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"rightClick(x=None, y=None) - Right click at position"},
|
||||
"rightClick(pos=None) - Right click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."},
|
||||
{"middleClick", (PyCFunction)McRFPy_Automation::_middleClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"middleClick(x=None, y=None) - Middle click at position"},
|
||||
"middleClick(pos=None) - Middle click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."},
|
||||
{"doubleClick", (PyCFunction)McRFPy_Automation::_doubleClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"doubleClick(x=None, y=None) - Double click at position"},
|
||||
"doubleClick(pos=None) - Double click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."},
|
||||
{"tripleClick", (PyCFunction)McRFPy_Automation::_tripleClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"tripleClick(x=None, y=None) - Triple click at position"},
|
||||
"tripleClick(pos=None) - Triple click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."},
|
||||
{"scroll", (PyCFunction)McRFPy_Automation::_scroll, METH_VARARGS | METH_KEYWORDS,
|
||||
"scroll(clicks, x=None, y=None) - Scroll wheel at position"},
|
||||
"scroll(clicks, pos=None) - Scroll wheel at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."},
|
||||
{"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS,
|
||||
"mouseDown(x=None, y=None, button='left') - Press mouse button"},
|
||||
"mouseDown(pos=None, button='left') - Press mouse button at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."},
|
||||
{"mouseUp", (PyCFunction)McRFPy_Automation::_mouseUp, METH_VARARGS | METH_KEYWORDS,
|
||||
"mouseUp(x=None, y=None, button='left') - Release mouse button"},
|
||||
"mouseUp(pos=None, button='left') - Release mouse button at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."},
|
||||
|
||||
{"typewrite", (PyCFunction)McRFPy_Automation::_typewrite, METH_VARARGS | METH_KEYWORDS,
|
||||
"typewrite(message, interval=0.0) - Type text with optional interval between keystrokes"},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public:
|
|||
// Mouse position and screen info
|
||||
static PyObject* _position(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
|
||||
static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "PyCallable.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "GameEngine.h"
|
||||
#include "PyVector.h"
|
||||
|
||||
PyCallable::PyCallable(PyObject* _target)
|
||||
{
|
||||
|
|
@ -49,7 +50,16 @@ PyClickCallable::PyClickCallable()
|
|||
|
||||
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);
|
||||
if (!retval)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "PyDrawable.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "PyPositionHelper.h"
|
||||
|
||||
// Click property getter
|
||||
static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
|
||||
|
|
@ -102,7 +103,7 @@ static PyGetSetDef PyDrawable_getsetters[] = {
|
|||
{"on_click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
|
||||
MCRF_PROPERTY(on_click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
"Function receives (pos: Vector, button: str, action: str)."
|
||||
), NULL},
|
||||
{"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index,
|
||||
MCRF_PROPERTY(z_index,
|
||||
|
|
@ -130,10 +131,10 @@ static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUS
|
|||
}
|
||||
|
||||
// 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;
|
||||
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
|
||||
if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -142,10 +143,10 @@ static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args)
|
|||
}
|
||||
|
||||
// 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;
|
||||
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
|
||||
if (!PyPosition_ParseFloat(args, kwds, &w, &h)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -162,23 +163,27 @@ static PyMethodDef PyDrawable_methods[] = {
|
|||
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.")
|
||||
)},
|
||||
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS,
|
||||
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS | METH_KEYWORDS,
|
||||
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_ARGS_START
|
||||
MCRF_ARG("dx", "Horizontal offset in pixels")
|
||||
MCRF_ARG("dy", "Vertical offset in pixels")
|
||||
MCRF_NOTE("This modifies the x and y position properties by the given amounts.")
|
||||
MCRF_ARG("dx", "Horizontal offset in pixels (or use delta)")
|
||||
MCRF_ARG("dy", "Vertical offset in pixels (or use delta)")
|
||||
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_SIG("(width: float, height: float)", "None"),
|
||||
MCRF_SIG("(width, height) or (size)", "None"),
|
||||
MCRF_DESC("Resize the element to new dimensions."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("width", "New width in pixels")
|
||||
MCRF_ARG("height", "New height in pixels")
|
||||
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.")
|
||||
MCRF_ARG("width", "New width in pixels (or use size)")
|
||||
MCRF_ARG("height", "New height in pixels (or use size)")
|
||||
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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,32 @@
|
|||
#include "PyVector.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 {
|
||||
public:
|
||||
// Template structure for parsing results
|
||||
|
|
@ -19,6 +44,292 @@ public:
|
|||
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
|
||||
// Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector
|
||||
static ParseResult parse_position(PyObject* args, PyObject* kwds,
|
||||
|
|
@ -26,19 +337,15 @@ public:
|
|||
{
|
||||
ParseResult result;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
PyObject* pos_obj = nullptr;
|
||||
int start_index = arg_index ? *arg_index : 0;
|
||||
|
||||
// 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* second = PyTuple_GetItem(args, start_index + 1);
|
||||
|
||||
// Check if both are numbers
|
||||
if ((PyFloat_Check(first) || PyLong_Check(first)) &&
|
||||
(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);
|
||||
if (extract_number(first, &x) && extract_number(second, &y)) {
|
||||
result.x = x;
|
||||
result.y = y;
|
||||
result.has_position = true;
|
||||
|
|
@ -47,13 +354,12 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
// Check for single positional argument that might be tuple or Vector
|
||||
if (!kwds && PyTuple_Size(args) > start_index) {
|
||||
// Check for single positional argument that might be tuple, list, or Vector
|
||||
if (PyTuple_Size(args) > start_index) {
|
||||
PyObject* first = PyTuple_GetItem(args, start_index);
|
||||
PyVectorObject* vec = PyVector::from_arg(first);
|
||||
if (vec) {
|
||||
result.x = vec->data.x;
|
||||
result.y = vec->data.y;
|
||||
if (extract_from_iterable(first, &x, &y)) {
|
||||
result.x = x;
|
||||
result.y = y;
|
||||
result.has_position = true;
|
||||
if (arg_index) *arg_index += 1;
|
||||
return result;
|
||||
|
|
@ -67,20 +373,18 @@ public:
|
|||
PyObject* pos_kw = PyDict_GetItemString(kwds, "pos");
|
||||
|
||||
if (x_obj && y_obj) {
|
||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
||||
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
|
||||
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
|
||||
if (extract_number(x_obj, &x) && extract_number(y_obj, &y)) {
|
||||
result.x = x;
|
||||
result.y = y;
|
||||
result.has_position = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos_kw) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_kw);
|
||||
if (vec) {
|
||||
result.x = vec->data.x;
|
||||
result.y = vec->data.y;
|
||||
if (extract_from_iterable(pos_kw, &x, &y)) {
|
||||
result.x = x;
|
||||
result.y = y;
|
||||
result.has_position = true;
|
||||
return result;
|
||||
}
|
||||
|
|
@ -94,58 +398,14 @@ public:
|
|||
static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds)
|
||||
{
|
||||
ParseResultInt result;
|
||||
int x = 0, y = 0;
|
||||
|
||||
// Check for positional tuple (x, y) first
|
||||
if (!kwds && PyTuple_Size(args) >= 2) {
|
||||
PyObject* first = PyTuple_GetItem(args, 0);
|
||||
PyObject* second = PyTuple_GetItem(args, 1);
|
||||
|
||||
if (PyLong_Check(first) && PyLong_Check(second)) {
|
||||
result.x = PyLong_AsLong(first);
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Try the new simplified parser first
|
||||
if (ParseInt(args, kwds, &x, &y)) {
|
||||
result.x = x;
|
||||
result.y = y;
|
||||
result.has_position = true;
|
||||
PyErr_Clear(); // Clear any error set by ParseInt
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -154,11 +414,39 @@ public:
|
|||
// Error message helper
|
||||
static void set_position_error() {
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
|
|
@ -430,7 +430,7 @@ PyGetSetDef UIArc::getsetters[] = {
|
|||
{"thickness", (getter)UIArc::get_thickness, (setter)UIArc::set_thickness,
|
||||
"Line thickness", NULL},
|
||||
{"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-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UIARC},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,
|
||||
|
|
|
|||
37
src/UIBase.h
37
src/UIBase.h
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
#include "Python.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "PyPositionHelper.h"
|
||||
#include <memory>
|
||||
|
||||
class UIEntity;
|
||||
|
|
@ -52,10 +53,10 @@ static PyObject* UIDrawable_get_bounds(T* self, PyObject* Py_UNUSED(args))
|
|||
|
||||
// move method implementation (#98)
|
||||
template<typename T>
|
||||
static PyObject* UIDrawable_move(T* self, PyObject* args)
|
||||
static PyObject* UIDrawable_move(T* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
float dx, dy;
|
||||
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
|
||||
if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -65,10 +66,10 @@ static PyObject* UIDrawable_move(T* self, PyObject* args)
|
|||
|
||||
// resize method implementation (#98)
|
||||
template<typename T>
|
||||
static PyObject* UIDrawable_resize(T* self, PyObject* args)
|
||||
static PyObject* UIDrawable_resize(T* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
float w, h;
|
||||
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
|
||||
if (!PyPosition_ParseFloat(args, kwds, &w, &h)) {
|
||||
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_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_SIG("(dx: float, dy: float)", "None"), \
|
||||
MCRF_SIG("(dx, dy) or (delta)", "None"), \
|
||||
MCRF_DESC("Move the element by a relative offset."), \
|
||||
MCRF_ARGS_START \
|
||||
MCRF_ARG("dx", "Horizontal offset in pixels") \
|
||||
MCRF_ARG("dy", "Vertical offset in pixels") \
|
||||
MCRF_NOTE("This modifies the x and y position properties by the given amounts.") \
|
||||
MCRF_ARG("dx", "Horizontal offset in pixels (or use delta)") \
|
||||
MCRF_ARG("dy", "Vertical offset in pixels (or use delta)") \
|
||||
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_SIG("(width: float, height: float)", "None"), \
|
||||
MCRF_SIG("(width, height) or (size)", "None"), \
|
||||
MCRF_DESC("Resize the element to new dimensions."), \
|
||||
MCRF_ARGS_START \
|
||||
MCRF_ARG("width", "New width in pixels") \
|
||||
MCRF_ARG("height", "New height in pixels") \
|
||||
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") \
|
||||
MCRF_ARG("width", "New width in pixels (or use size)") \
|
||||
MCRF_ARG("height", "New height in pixels (or use size)") \
|
||||
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)
|
||||
|
|
@ -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, \
|
||||
MCRF_PROPERTY(on_enter, \
|
||||
"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}, \
|
||||
{"on_exit", (getter)UIDrawable::get_on_exit, (setter)UIDrawable::set_on_exit, \
|
||||
MCRF_PROPERTY(on_exit, \
|
||||
"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}, \
|
||||
{"hovered", (getter)UIDrawable::get_hovered, NULL, \
|
||||
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, \
|
||||
MCRF_PROPERTY(on_move, \
|
||||
"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." \
|
||||
), (void*)type_enum}
|
||||
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ PyGetSetDef UICaption::getsetters[] = {
|
|||
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(on_click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
"Function receives (pos: Vector, button: str, action: str)."
|
||||
), (void*)PyObjectsEnum::UICAPTION},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
MCRF_PROPERTY(z_index,
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ PyGetSetDef UICircle::getsetters[] = {
|
|||
{"outline", (getter)UICircle::get_outline, (setter)UICircle::set_outline,
|
||||
"Outline thickness (0 for no outline)", NULL},
|
||||
{"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-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UICIRCLE},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "Animation.h"
|
||||
#include "PyAnimation.h"
|
||||
#include "PyEasing.h"
|
||||
#include "PyPositionHelper.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
#include "UIEntityPyMethods.h"
|
||||
|
||||
|
|
@ -94,11 +95,10 @@ void UIEntity::updateVisibility()
|
|||
}
|
||||
}
|
||||
|
||||
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
|
||||
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(o, "ii", &x, &y)) {
|
||||
PyErr_SetString(PyExc_TypeError, "UIEntity.at requires two integer arguments: (x, y)");
|
||||
return NULL;
|
||||
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||||
return NULL; // Error already set by PyPosition_ParseInt
|
||||
}
|
||||
|
||||
if (self->data->grid == NULL) {
|
||||
|
|
@ -590,19 +590,12 @@ PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
|
|||
}
|
||||
|
||||
PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* keywords[] = {"target_x", "target_y", "x", "y", nullptr};
|
||||
int target_x = -1, target_y = -1;
|
||||
int target_x, target_y;
|
||||
|
||||
// Parse arguments - support both target_x/target_y and x/y parameter names
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(keywords),
|
||||
&target_x, &target_y)) {
|
||||
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;
|
||||
}
|
||||
// Parse position using flexible position helper
|
||||
// Supports: path_to(x, y), path_to((x, y)), path_to(pos=(x, y)), path_to(Vector(x, y))
|
||||
if (!PyPosition_ParseInt(args, kwds, &target_x, &target_y)) {
|
||||
return NULL; // Error already set by PyPosition_ParseInt
|
||||
}
|
||||
|
||||
// Check if entity has a grid
|
||||
|
|
@ -743,19 +736,32 @@ PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyO
|
|||
}
|
||||
|
||||
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"},
|
||||
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
||||
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
|
||||
"path_to(x: int, y: int) -> bool\n\n"
|
||||
"Find and follow path to target position using A* pathfinding.\n\n"
|
||||
"path_to(x, y) or path_to(target) -> list\n\n"
|
||||
"Find a path to the target position using Dijkstra pathfinding.\n\n"
|
||||
"Args:\n"
|
||||
" x: Target X coordinate\n"
|
||||
" y: Target Y coordinate\n\n"
|
||||
" x, y: Target coordinates as two integers, OR\n"
|
||||
" target: Target coordinates as tuple, list, or Vector\n\n"
|
||||
"Returns:\n"
|
||||
" True if a path was found and the entity started moving, False otherwise\n\n"
|
||||
"The entity will automatically move along the path over multiple frames.\n"
|
||||
"Call this again to change the target or repath."},
|
||||
" List of (x, y) tuples representing the path.\n\n"
|
||||
"Example:\n"
|
||||
" 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() -> None\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_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"},
|
||||
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
||||
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
|
||||
"path_to(x: int, y: int) -> bool\n\n"
|
||||
"Find and follow path to target position using A* pathfinding.\n\n"
|
||||
"path_to(x, y) or path_to(target) -> list\n\n"
|
||||
"Find a path to the target position using Dijkstra pathfinding.\n\n"
|
||||
"Args:\n"
|
||||
" x: Target X coordinate\n"
|
||||
" y: Target Y coordinate\n\n"
|
||||
" x, y: Target coordinates as two integers, OR\n"
|
||||
" target: Target coordinates as tuple, list, or Vector\n\n"
|
||||
"Returns:\n"
|
||||
" True if a path was found and the entity started moving, False otherwise\n\n"
|
||||
"The entity will automatically move along the path over multiple frames.\n"
|
||||
"Call this again to change the target or repath."},
|
||||
" List of (x, y) tuples representing the path.\n\n"
|
||||
"Example:\n"
|
||||
" 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() -> None\n\n"
|
||||
"Update entity's visibility state based on current FOV.\n\n"
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public:
|
|||
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 */ }
|
||||
|
||||
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* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@ PyGetSetDef UIFrame::getsetters[] = {
|
|||
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(on_click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
"Function receives (pos: Vector, button: str, action: str)."
|
||||
), (void*)PyObjectsEnum::UIFRAME},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
MCRF_PROPERTY(z_index,
|
||||
|
|
|
|||
303
src/UIGrid.cpp
303
src/UIGrid.cpp
|
|
@ -5,6 +5,7 @@
|
|||
#include "UIEntity.h"
|
||||
#include "Profiler.h"
|
||||
#include "PyFOV.h"
|
||||
#include "PyPositionHelper.h" // For standardized position argument parsing
|
||||
#include <algorithm>
|
||||
#include <cmath> // #142 - for std::floor, std::isnan
|
||||
#include <cstring> // #150 - for strcmp
|
||||
|
|
@ -685,15 +686,20 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
|
|||
|
||||
// Only fire if within valid grid bounds
|
||||
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);
|
||||
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
|
||||
Py_DECREF(args);
|
||||
if (!result) {
|
||||
std::cerr << "Cell click callback raised an exception:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else {
|
||||
Py_DECREF(result);
|
||||
// Create Vector object for cell position
|
||||
PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)cell_x, (float)cell_y);
|
||||
if (cell_pos) {
|
||||
PyObject* args = Py_BuildValue("(O)", cell_pos);
|
||||
Py_DECREF(cell_pos);
|
||||
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
|
||||
Py_DECREF(args);
|
||||
if (!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
|
||||
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);
|
||||
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
|
||||
Py_DECREF(args);
|
||||
if (!result) {
|
||||
std::cerr << "Cell click callback raised an exception:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else {
|
||||
Py_DECREF(result);
|
||||
// Create Vector object for cell position
|
||||
PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)cell_x, (float)cell_y);
|
||||
if (cell_pos) {
|
||||
PyObject* args = Py_BuildValue("(O)", cell_pos);
|
||||
Py_DECREF(cell_pos);
|
||||
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
|
||||
Py_DECREF(args);
|
||||
if (!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
|
||||
}
|
||||
|
|
@ -1141,34 +1152,12 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
|
|||
|
||||
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"x", "y", nullptr};
|
||||
int x = 0, y = 0;
|
||||
int x, y;
|
||||
|
||||
// First try to parse as two integers
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(keywords), &x, &y)) {
|
||||
PyErr_Clear();
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Use the flexible position parsing helper - accepts:
|
||||
// at(x, y), at((x, y)), at([x, y]), at(Vector(x, y)), at(pos=(x, y)), etc.
|
||||
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||||
return NULL; // Error already set by PyPosition_ParseInt
|
||||
}
|
||||
|
||||
// Range validation
|
||||
|
|
@ -1349,13 +1338,19 @@ int UIGrid::set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure)
|
|||
// Python API implementations for TCOD functionality
|
||||
PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* kwlist[] = {"x", "y", "radius", "light_walls", "algorithm", NULL};
|
||||
int x, y, radius = 0;
|
||||
static const char* kwlist[] = {"pos", "radius", "light_walls", "algorithm", NULL};
|
||||
PyObject* pos_obj = NULL;
|
||||
int radius = 0;
|
||||
int light_walls = 1;
|
||||
int algorithm = FOV_BASIC;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|ipi", const_cast<char**>(kwlist),
|
||||
&x, &y, &radius, &light_walls, &algorithm)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ipi", const_cast<char**>(kwlist),
|
||||
&pos_obj, &radius, &light_walls, &algorithm)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int x, y;
|
||||
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1367,10 +1362,10 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
|
|||
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;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1380,12 +1375,21 @@ PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args)
|
|||
|
||||
PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL};
|
||||
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;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast<char**>(kwlist),
|
||||
&x1, &y1, &x2, &y2, &diagonal_cost)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast<char**>(kwlist),
|
||||
&start_obj, &end_obj, &diagonal_cost)) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1408,12 +1412,17 @@ PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* k
|
|||
|
||||
PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* kwlist[] = {"root_x", "root_y", "diagonal_cost", NULL};
|
||||
int root_x, root_y;
|
||||
static const char* kwlist[] = {"root", "diagonal_cost", NULL};
|
||||
PyObject* root_obj = NULL;
|
||||
float diagonal_cost = 1.41f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|f", const_cast<char**>(kwlist),
|
||||
&root_x, &root_y, &diagonal_cost)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(kwlist),
|
||||
&root_obj, &diagonal_cost)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int root_x, root_y;
|
||||
if (!PyPosition_FromObjectInt(root_obj, &root_x, &root_y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1421,10 +1430,10 @@ PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyOb
|
|||
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;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1436,10 +1445,10 @@ PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args)
|
|||
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;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1456,13 +1465,21 @@ PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args)
|
|||
|
||||
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;
|
||||
|
||||
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)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast<char**>(kwlist),
|
||||
&x1, &y1, &x2, &y2, &diagonal_cost)) {
|
||||
int x1, y1, x2, y2;
|
||||
if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) {
|
||||
return NULL;
|
||||
}
|
||||
if (!PyPosition_FromObjectInt(end_obj, &x2, &y2)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1812,72 +1829,63 @@ PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) {
|
|||
PyMethodDef UIGrid::methods[] = {
|
||||
{"at", (PyCFunction)UIGrid::py_at, 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"
|
||||
"Args:\n"
|
||||
" x: X coordinate of the viewer\n"
|
||||
" y: Y coordinate of the viewer\n"
|
||||
" pos: Position as (x, y) tuple, list, or Vector\n"
|
||||
" radius: Maximum view distance (0 = unlimited)\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"
|
||||
"Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
||||
"is_in_fov(x: int, y: int) -> bool\n\n"
|
||||
"Updates the internal FOV state. Use is_in_fov(pos) to query visibility."},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
"is_in_fov(pos) -> bool\n\n"
|
||||
"Check if a cell is in the field of view.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate to check\n"
|
||||
" y: Y coordinate to check\n\n"
|
||||
" pos: Position as (x, y) tuple, list, or Vector\n\n"
|
||||
"Returns:\n"
|
||||
" True if the cell is visible, False otherwise\n\n"
|
||||
"Must call compute_fov() first to calculate visibility."},
|
||||
{"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"
|
||||
"Args:\n"
|
||||
" x1: Starting X coordinate\n"
|
||||
" y1: Starting Y coordinate\n"
|
||||
" x2: Target X coordinate\n"
|
||||
" y2: Target Y coordinate\n"
|
||||
" start: Starting position as (x, y) tuple, list, or Vector\n"
|
||||
" end: Target position as (x, y) tuple, list, or Vector\n"
|
||||
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||
"Returns:\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."},
|
||||
{"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"
|
||||
"Args:\n"
|
||||
" root_x: X coordinate of the root/target\n"
|
||||
" root_y: Y coordinate of the root/target\n"
|
||||
" root: Root position as (x, y) tuple, list, or Vector\n"
|
||||
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||
"Precomputes distances from all reachable cells to the root.\n"
|
||||
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
|
||||
"Useful for multiple entities pathfinding to the same target."},
|
||||
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS,
|
||||
"get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n"
|
||||
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS,
|
||||
"get_dijkstra_distance(pos) -> Optional[float]\n\n"
|
||||
"Get distance from Dijkstra root to position.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate to query\n"
|
||||
" y: Y coordinate to query\n\n"
|
||||
" pos: Position as (x, y) tuple, list, or Vector\n\n"
|
||||
"Returns:\n"
|
||||
" Distance as float, or None if position is unreachable or invalid\n\n"
|
||||
"Must call compute_dijkstra() first."},
|
||||
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
|
||||
"get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n"
|
||||
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS,
|
||||
"get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n"
|
||||
"Get path from position to Dijkstra root.\n\n"
|
||||
"Args:\n"
|
||||
" x: Starting X coordinate\n"
|
||||
" y: Starting Y coordinate\n\n"
|
||||
" pos: Position as (x, y) tuple, list, or Vector\n\n"
|
||||
"Returns:\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."},
|
||||
{"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"
|
||||
"Args:\n"
|
||||
" x1: Starting X coordinate\n"
|
||||
" y1: Starting Y coordinate\n"
|
||||
" x2: Target X coordinate\n"
|
||||
" y2: Target Y coordinate\n"
|
||||
" start: Starting position as (x, y) tuple, list, or Vector\n"
|
||||
" end: Target position as (x, y) tuple, list, or Vector\n"
|
||||
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||
"Returns:\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,
|
||||
{"at", (PyCFunction)UIGrid::py_at, 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"
|
||||
"Args:\n"
|
||||
" x: X coordinate of the viewer\n"
|
||||
" y: Y coordinate of the viewer\n"
|
||||
" pos: Position as (x, y) tuple, list, or Vector\n"
|
||||
" radius: Maximum view distance (0 = unlimited)\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"
|
||||
"Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
||||
"is_in_fov(x: int, y: int) -> bool\n\n"
|
||||
"Updates the internal FOV state. Use is_in_fov(pos) to query visibility."},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
"is_in_fov(pos) -> bool\n\n"
|
||||
"Check if a cell is in the field of view.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate to check\n"
|
||||
" y: Y coordinate to check\n\n"
|
||||
" pos: Position as (x, y) tuple, list, or Vector\n\n"
|
||||
"Returns:\n"
|
||||
" True if the cell is visible, False otherwise\n\n"
|
||||
"Must call compute_fov() first to calculate visibility."},
|
||||
{"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"
|
||||
"Args:\n"
|
||||
" x1: Starting X coordinate\n"
|
||||
" y1: Starting Y coordinate\n"
|
||||
" x2: Target X coordinate\n"
|
||||
" y2: Target Y coordinate\n"
|
||||
" start: Starting position as (x, y) tuple, list, or Vector\n"
|
||||
" end: Target position as (x, y) tuple, list, or Vector\n"
|
||||
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||
"Returns:\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."},
|
||||
{"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"
|
||||
"Args:\n"
|
||||
" root_x: X coordinate of the root/target\n"
|
||||
" root_y: Y coordinate of the root/target\n"
|
||||
" root: Root position as (x, y) tuple, list, or Vector\n"
|
||||
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||
"Precomputes distances from all reachable cells to the root.\n"
|
||||
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
|
||||
"Useful for multiple entities pathfinding to the same target."},
|
||||
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS,
|
||||
"get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n"
|
||||
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS,
|
||||
"get_dijkstra_distance(pos) -> Optional[float]\n\n"
|
||||
"Get distance from Dijkstra root to position.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate to query\n"
|
||||
" y: Y coordinate to query\n\n"
|
||||
" pos: Position as (x, y) tuple, list, or Vector\n\n"
|
||||
"Returns:\n"
|
||||
" Distance as float, or None if position is unreachable or invalid\n\n"
|
||||
"Must call compute_dijkstra() first."},
|
||||
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
|
||||
"get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n"
|
||||
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS,
|
||||
"get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n"
|
||||
"Get path from position to Dijkstra root.\n\n"
|
||||
"Args:\n"
|
||||
" x: Starting X coordinate\n"
|
||||
" y: Starting Y coordinate\n\n"
|
||||
" pos: Position as (x, y) tuple, list, or Vector\n\n"
|
||||
"Returns:\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."},
|
||||
{"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"
|
||||
"Args:\n"
|
||||
" x1: Starting X coordinate\n"
|
||||
" y1: Starting Y coordinate\n"
|
||||
" x2: Target X coordinate\n"
|
||||
" y2: Target Y coordinate\n"
|
||||
" start: Starting position as (x, y) tuple, list, or Vector\n"
|
||||
" end: Target position as (x, y) tuple, list, or Vector\n"
|
||||
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||
"Returns:\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,
|
||||
MCRF_PROPERTY(on_click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
"Function receives (pos: Vector, button: str, action: str)."
|
||||
), (void*)PyObjectsEnum::UIGRID},
|
||||
|
||||
{"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),
|
||||
// #142 - Grid cell mouse events
|
||||
{"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,
|
||||
"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,
|
||||
"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,
|
||||
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
|
||||
{NULL} /* Sentinel */
|
||||
|
|
@ -2249,29 +2248,39 @@ void UIGrid::updateCellHover(sf::Vector2f mousepos) {
|
|||
if (new_cell != hovered_cell) {
|
||||
// Fire exit callback for old cell
|
||||
if (hovered_cell.has_value() && on_cell_exit_callable) {
|
||||
PyObject* args = Py_BuildValue("(ii)", hovered_cell->x, hovered_cell->y);
|
||||
PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args);
|
||||
Py_DECREF(args);
|
||||
if (!result) {
|
||||
std::cerr << "Cell exit callback raised an exception:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else {
|
||||
Py_DECREF(result);
|
||||
// Create Vector object for cell position
|
||||
PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)hovered_cell->x, (float)hovered_cell->y);
|
||||
if (cell_pos) {
|
||||
PyObject* args = Py_BuildValue("(O)", cell_pos);
|
||||
Py_DECREF(cell_pos);
|
||||
PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args);
|
||||
Py_DECREF(args);
|
||||
if (!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
|
||||
if (new_cell.has_value() && on_cell_enter_callable) {
|
||||
PyObject* args = Py_BuildValue("(ii)", new_cell->x, new_cell->y);
|
||||
PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args);
|
||||
Py_DECREF(args);
|
||||
if (!result) {
|
||||
std::cerr << "Cell enter callback raised an exception:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else {
|
||||
Py_DECREF(result);
|
||||
// Create Vector object for cell position
|
||||
PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)new_cell->x, (float)new_cell->y);
|
||||
if (cell_pos) {
|
||||
PyObject* args = Py_BuildValue("(O)", cell_pos);
|
||||
Py_DECREF(cell_pos);
|
||||
PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args);
|
||||
Py_DECREF(args);
|
||||
if (!result) {
|
||||
std::cerr << "Cell enter callback raised an exception:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else {
|
||||
Py_DECREF(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -165,11 +165,11 @@ public:
|
|||
static int set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* py_at(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_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* py_get_dijkstra_path(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, 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_center_camera(PyUIGridObject* self, PyObject* args); // #169
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@ PyGetSetDef UILine::getsetters[] = {
|
|||
{"thickness", (getter)UILine::get_thickness, (setter)UILine::set_thickness,
|
||||
MCRF_PROPERTY(thickness, "Line thickness in pixels."), NULL},
|
||||
{"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},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
MCRF_PROPERTY(z_index, "Z-order for rendering (lower values rendered first)."),
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ PyGetSetDef UISprite::getsetters[] = {
|
|||
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(on_click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
"Function receives (pos: Vector, button: str, action: str)."
|
||||
), (void*)PyObjectsEnum::UISPRITE},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
MCRF_PROPERTY(z_index,
|
||||
|
|
|
|||
150
tests/test_callback_vector.py
Normal file
150
tests/test_callback_vector.py
Normal 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)
|
||||
152
tests/unit/automation_vector_test.py
Normal file
152
tests/unit/automation_vector_test.py
Normal 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)
|
||||
129
tests/unit/test_drawable_move_resize_position_parsing.py
Normal file
129
tests/unit/test_drawable_move_resize_position_parsing.py
Normal 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()
|
||||
135
tests/unit/test_entity_position_parsing.py
Normal file
135
tests/unit/test_entity_position_parsing.py
Normal 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)
|
||||
156
tests/unit/test_grid_pathfinding_positions.py
Normal file
156
tests/unit/test_grid_pathfinding_positions.py
Normal 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)
|
||||
149
tests/unit/test_layer_position_parsing.py
Normal file
149
tests/unit/test_layer_position_parsing.py
Normal 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)
|
||||
158
tests/unit/test_position_helper.py
Normal file
158
tests/unit/test_position_helper.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue