Compare commits

..

No commits in common. "19ded088b06edf093f789caa10e44faad3a6fb12" and "51e96c0c6b9981085ab9a15922f687ad64b7d18e" have entirely different histories.

22 changed files with 55 additions and 1527 deletions

View file

@ -3,7 +3,6 @@
#include "UIEntity.h"
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
#include "PythonObjectCache.h"
#include <cmath>
#include <algorithm>
@ -369,14 +368,9 @@ void Animation::triggerCallback() {
Py_DECREF(args);
if (!result) {
std::cerr << "Animation callback raised an exception:" << std::endl;
// Print error but don't crash
PyErr_Print();
PyErr_Clear();
// Check if we should exit on exception
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
McRFPy_API::signalPythonException();
}
PyErr_Clear(); // Clear the error state
} else {
Py_DECREF(result);
}

View file

@ -122,12 +122,6 @@ CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& confi
continue;
}
if (arg == "--continue-after-exceptions") {
config.exit_on_exception = false;
current_arg++;
continue;
}
// If no flags matched, treat as positional argument (script name)
if (arg[0] != '-') {
config.script_path = arg;
@ -166,8 +160,6 @@ void CommandLineParser::print_help() {
<< " --audio-off : disable audio\n"
<< " --audio-on : enable audio (even in headless mode)\n"
<< " --screenshot [path] : take a screenshot in headless mode\n"
<< " --continue-after-exceptions : don't exit on Python callback exceptions\n"
<< " (default: exit on first exception)\n"
<< "\n"
<< "Arguments:\n"
<< " file : program read from script file\n"

View file

@ -289,11 +289,6 @@ void GameEngine::run()
if (config.auto_exit_after_exec && timers.empty()) {
running = false;
}
// Check if a Python exception has signaled exit
if (McRFPy_API::shouldExit()) {
running = false;
}
}
// Clean up before exiting the run loop

View file

@ -153,7 +153,6 @@ public:
std::shared_ptr<Timer> getTimer(const std::string& name);
void setWindowScale(float);
bool isHeadless() const { return headless; }
const McRogueFaceConfig& getConfig() const { return config; }
void processEvent(const sf::Event& event);
// Window property accessors

View file

@ -27,9 +27,6 @@ std::shared_ptr<PyFont> McRFPy_API::default_font;
std::shared_ptr<PyTexture> McRFPy_API::default_texture;
PyObject* McRFPy_API::mcrf_module;
// Exception handling state
std::atomic<bool> McRFPy_API::exception_occurred{false};
std::atomic<int> McRFPy_API::exit_code{0};
static PyMethodDef mcrfpyMethods[] = {
@ -1150,20 +1147,3 @@ PyObject* McRFPy_API::_getMetrics(PyObject* self, PyObject* args) {
return dict;
}
// Exception handling implementation
void McRFPy_API::signalPythonException() {
// Check if we should exit on exception (consult config via game)
if (game && !game->isHeadless()) {
// In windowed mode, respect the config setting
// Access config through game engine - but we need to check the config
}
// For now, always signal - the game loop will check the config
exception_occurred.store(true);
exit_code.store(1);
}
bool McRFPy_API::shouldExit() {
return exception_occurred.load();
}

View file

@ -2,7 +2,6 @@
#include "Common.h"
#include "Python.h"
#include <list>
#include <atomic>
#include "PyFont.h"
#include "PyTexture.h"
@ -86,10 +85,4 @@ public:
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
static void updatePythonScenes(float dt);
static void triggerResize(int width, int height);
// Exception handling - signal game loop to exit on unhandled Python exceptions
static std::atomic<bool> exception_occurred;
static std::atomic<int> exit_code;
static void signalPythonException(); // Called by exception handlers
static bool shouldExit(); // Checked by game loop
};

View file

@ -31,10 +31,6 @@ struct McRogueFaceConfig {
// Auto-exit when no timers remain (for --headless --exec automation)
bool auto_exit_after_exec = false;
// Exception handling: exit on first Python callback exception (default: true)
// Use --continue-after-exceptions to disable
bool exit_on_exception = true;
};
#endif // MCROGUEFACE_CONFIG_H

View file

@ -1,6 +1,4 @@
#include "PyCallable.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
PyCallable::PyCallable(PyObject* _target)
{
@ -53,14 +51,9 @@ void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::strin
PyObject* retval = PyCallable::call(args, NULL);
if (!retval)
{
std::cerr << "Click callback raised an exception:" << std::endl;
std::cout << "ClickCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print();
PyErr_Clear();
// Check if we should exit on exception
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
McRFPy_API::signalPythonException();
}
} else if (retval != Py_None)
{
std::cout << "ClickCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
@ -88,14 +81,9 @@ void PyKeyCallable::call(std::string key, std::string action)
PyObject* retval = PyCallable::call(args, NULL);
if (!retval)
{
std::cerr << "Key callback raised an exception:" << std::endl;
std::cout << "KeyCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print();
PyErr_Clear();
// Check if we should exit on exception
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
McRFPy_API::signalPythonException();
}
} else if (retval != Py_None)
{
std::cout << "KeyCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;

View file

@ -8,8 +8,6 @@ PyGetSetDef PyVector::getsetters[] = {
MCRF_PROPERTY(x, "X coordinate of the vector (float)"), (void*)0},
{"y", (getter)PyVector::get_member, (setter)PyVector::set_member,
MCRF_PROPERTY(y, "Y coordinate of the vector (float)"), (void*)1},
{"int", (getter)PyVector::get_int, NULL,
MCRF_PROPERTY(int, "Integer tuple (floor of x and y) for use as dict keys. Read-only."), NULL},
{NULL}
};
@ -62,13 +60,6 @@ PyMethodDef PyVector::methods[] = {
MCRF_DESC("Create a copy of this vector."),
MCRF_RETURNS("Vector: New Vector object with same x and y values")
)},
{"floor", (PyCFunction)PyVector::floor, METH_NOARGS,
MCRF_METHOD(Vector, floor,
MCRF_SIG("()", "Vector"),
MCRF_DESC("Return a new vector with floored (integer) coordinates."),
MCRF_RETURNS("Vector: New Vector with floor(x) and floor(y)")
MCRF_NOTE("Useful for grid-based positioning. For a hashable tuple, use the .int property instead.")
)},
{NULL}
};
@ -111,19 +102,6 @@ namespace mcrfpydef {
.nb_matrix_multiply = 0,
.nb_inplace_matrix_multiply = 0
};
PySequenceMethods PyVector_as_sequence = {
.sq_length = PyVector::sequence_length,
.sq_concat = 0,
.sq_repeat = 0,
.sq_item = PyVector::sequence_item,
.was_sq_slice = 0,
.sq_ass_item = 0,
.was_sq_ass_slice = 0,
.sq_contains = 0,
.sq_inplace_concat = 0,
.sq_inplace_repeat = 0
};
}
PyVector::PyVector(sf::Vector2f target)
@ -420,58 +398,22 @@ PyObject* PyVector::richcompare(PyObject* left, PyObject* right, int op)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
float left_x, left_y, right_x, right_y;
// Extract left operand values
if (PyObject_IsInstance(left, (PyObject*)type)) {
PyVectorObject* vec = (PyVectorObject*)left;
left_x = vec->data.x;
left_y = vec->data.y;
} else if (PyTuple_Check(left) && PyTuple_Size(left) == 2) {
PyObject* x_obj = PyTuple_GetItem(left, 0);
PyObject* y_obj = PyTuple_GetItem(left, 1);
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
left_x = (float)PyFloat_AsDouble(x_obj);
left_y = (float)PyFloat_AsDouble(y_obj);
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
} else {
if (!PyObject_IsInstance(left, (PyObject*)type) || !PyObject_IsInstance(right, (PyObject*)type)) {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
// Extract right operand values
if (PyObject_IsInstance(right, (PyObject*)type)) {
PyVectorObject* vec = (PyVectorObject*)right;
right_x = vec->data.x;
right_y = vec->data.y;
} else if (PyTuple_Check(right) && PyTuple_Size(right) == 2) {
PyObject* x_obj = PyTuple_GetItem(right, 0);
PyObject* y_obj = PyTuple_GetItem(right, 1);
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
right_x = (float)PyFloat_AsDouble(x_obj);
right_y = (float)PyFloat_AsDouble(y_obj);
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
PyVectorObject* vec1 = (PyVectorObject*)left;
PyVectorObject* vec2 = (PyVectorObject*)right;
bool result = false;
switch (op) {
case Py_EQ:
result = (left_x == right_x && left_y == right_y);
result = (vec1->data.x == vec2->data.x && vec1->data.y == vec2->data.y);
break;
case Py_NE:
result = (left_x != right_x || left_y != right_y);
result = (vec1->data.x != vec2->data.x || vec1->data.y != vec2->data.y);
break;
default:
Py_INCREF(Py_NotImplemented);
@ -565,47 +507,3 @@ PyObject* PyVector::copy(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
return (PyObject*)result;
}
PyObject* PyVector::floor(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(std::floor(self->data.x), std::floor(self->data.y));
}
return (PyObject*)result;
}
// Sequence protocol implementation
Py_ssize_t PyVector::sequence_length(PyObject* self)
{
return 2; // Vectors always have exactly 2 elements
}
PyObject* PyVector::sequence_item(PyObject* obj, Py_ssize_t index)
{
PyVectorObject* self = (PyVectorObject*)obj;
// Note: Python already handles negative index normalization when sq_length is defined
// So v[-1] arrives here as index=1, v[-2] as index=0
// Out-of-range negative indices (like v[-3]) arrive as negative values (e.g., -1)
if (index == 0) {
return PyFloat_FromDouble(self->data.x);
} else if (index == 1) {
return PyFloat_FromDouble(self->data.y);
} else {
PyErr_SetString(PyExc_IndexError, "Vector index out of range (must be 0 or 1)");
return NULL;
}
}
// Property: .int - returns integer tuple for use as dict keys
PyObject* PyVector::get_int(PyObject* obj, void* closure)
{
PyVectorObject* self = (PyVectorObject*)obj;
long ix = (long)std::floor(self->data.x);
long iy = (long)std::floor(self->data.y);
return Py_BuildValue("(ll)", ix, iy);
}

View file

@ -45,23 +45,14 @@ public:
static PyObject* distance_to(PyVectorObject*, PyObject*);
static PyObject* angle(PyVectorObject*, PyObject*);
static PyObject* copy(PyVectorObject*, PyObject*);
static PyObject* floor(PyVectorObject*, PyObject*);
// Sequence protocol
static Py_ssize_t sequence_length(PyObject*);
static PyObject* sequence_item(PyObject*, Py_ssize_t);
// Additional properties
static PyObject* get_int(PyObject*, void*);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
// Forward declare the PyNumberMethods and PySequenceMethods structures
// Forward declare the PyNumberMethods structure
extern PyNumberMethods PyVector_as_number;
extern PySequenceMethods PyVector_as_sequence;
static PyTypeObject PyVectorType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -70,7 +61,6 @@ namespace mcrfpydef {
.tp_itemsize = 0,
.tp_repr = PyVector::repr,
.tp_as_number = &PyVector_as_number,
.tp_as_sequence = &PyVector_as_sequence,
.tp_hash = PyVector::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Vector Object"),

View file

@ -1,8 +1,6 @@
#include "Timer.h"
#include "PythonObjectCache.h"
#include "PyCallable.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
Timer::Timer(PyObject* _target, int _interval, int now, bool _once)
: callback(std::make_shared<PyCallable>(_target)), interval(_interval), last_ran(now),
@ -54,14 +52,9 @@ bool Timer::test(int now)
if (!retval)
{
std::cerr << "Timer callback raised an exception:" << std::endl;
std::cout << "Timer callback has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print();
PyErr_Clear();
// Check if we should exit on exception
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
McRFPy_API::signalPythonException();
}
} else if (retval != Py_None)
{
std::cout << "Timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;

View file

@ -790,151 +790,30 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
{
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
return NULL;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Drawable"))) {
PyErr_SetString(PyExc_TypeError,
"UICollection.remove requires a UI element (Frame, Caption, Sprite, Grid)");
return NULL;
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> search_drawable = nullptr;
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
search_drawable = ((PyUIFrameObject*)o)->data;
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
search_drawable = ((PyUICaptionObject*)o)->data;
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
search_drawable = ((PyUISpriteObject*)o)->data;
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
search_drawable = ((PyUIGridObject*)o)->data;
}
if (!search_drawable) {
PyErr_SetString(PyExc_TypeError,
"UICollection.remove requires a UI element (Frame, Caption, Sprite, Grid)");
return NULL;
}
// Search for the object and remove first occurrence
for (auto it = vec->begin(); it != vec->end(); ++it) {
if (it->get() == search_drawable.get()) {
vec->erase(it);
McRFPy_API::markSceneNeedsSort();
Py_RETURN_NONE;
}
}
PyErr_SetString(PyExc_ValueError, "element not in UICollection");
return NULL;
}
PyObject* UICollection::pop(PyUICollectionObject* self, PyObject* args)
if (!PyLong_Check(o))
{
Py_ssize_t index = -1; // Default to last element
if (!PyArg_ParseTuple(args, "|n", &index)) {
return NULL;
}
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
return NULL;
}
if (vec->empty()) {
PyErr_SetString(PyExc_IndexError, "pop from empty UICollection");
PyErr_SetString(PyExc_TypeError, "UICollection.remove requires an integer index to remove");
return NULL;
}
long index = PyLong_AsLong(o);
// Handle negative indexing
Py_ssize_t size = static_cast<Py_ssize_t>(vec->size());
if (index < 0) {
index += size;
}
while (index < 0) index += self->data->size();
if (index < 0 || index >= size) {
PyErr_SetString(PyExc_IndexError, "pop index out of range");
return NULL;
}
// Get the element before removing
std::shared_ptr<UIDrawable> drawable = (*vec)[index];
// Remove from vector
vec->erase(vec->begin() + index);
McRFPy_API::markSceneNeedsSort();
// Convert to Python object and return
return convertDrawableToPython(drawable);
}
PyObject* UICollection::insert(PyUICollectionObject* self, PyObject* args)
if (index >= self->data->size())
{
Py_ssize_t index;
PyObject* o;
if (!PyArg_ParseTuple(args, "nO", &index, &o)) {
PyErr_SetString(PyExc_ValueError, "Index out of range");
return NULL;
}
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
return NULL;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Drawable"))) {
PyErr_SetString(PyExc_TypeError,
"UICollection.insert requires a UI element (Frame, Caption, Sprite, Grid)");
return NULL;
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> drawable = nullptr;
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
drawable = ((PyUIFrameObject*)o)->data;
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
drawable = ((PyUICaptionObject*)o)->data;
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
drawable = ((PyUISpriteObject*)o)->data;
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
drawable = ((PyUIGridObject*)o)->data;
}
if (!drawable) {
PyErr_SetString(PyExc_TypeError,
"UICollection.insert requires a UI element (Frame, Caption, Sprite, Grid)");
return NULL;
}
// Handle negative indexing and clamping (Python list.insert behavior)
Py_ssize_t size = static_cast<Py_ssize_t>(vec->size());
if (index < 0) {
index += size;
if (index < 0) {
index = 0;
}
} else if (index > size) {
index = size;
}
// Insert at position
vec->insert(vec->begin() + index, drawable);
// release the shared pointer at self->data[index];
self->data->erase(self->data->begin() + index);
// Mark scene as needing resort after removing element
McRFPy_API::markSceneNeedsSort();
Py_RETURN_NONE;
Py_INCREF(Py_None);
return Py_None;
}
PyObject* UICollection::index_method(PyUICollectionObject* self, PyObject* value) {
@ -1026,173 +905,12 @@ PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) {
return PyLong_FromSsize_t(count);
}
// Helper function to match names with optional wildcard support
static bool matchName(const std::string& name, const std::string& pattern) {
// Check for wildcard pattern
if (pattern.find('*') != std::string::npos) {
// Simple wildcard matching: only support * at start, end, or both
if (pattern == "*") {
return true; // Match everything
} else if (pattern.front() == '*' && pattern.back() == '*' && pattern.length() > 2) {
// *substring* - contains match
std::string substring = pattern.substr(1, pattern.length() - 2);
return name.find(substring) != std::string::npos;
} else if (pattern.front() == '*') {
// *suffix - ends with
std::string suffix = pattern.substr(1);
return name.length() >= suffix.length() &&
name.compare(name.length() - suffix.length(), suffix.length(), suffix) == 0;
} else if (pattern.back() == '*') {
// prefix* - starts with
std::string prefix = pattern.substr(0, pattern.length() - 1);
return name.compare(0, prefix.length(), prefix) == 0;
}
// For more complex patterns, fall back to exact match
return name == pattern;
}
// Exact match
return name == pattern;
}
PyObject* UICollection::find(PyUICollectionObject* self, PyObject* args, PyObject* kwds) {
const char* name = nullptr;
int recursive = 0;
static const char* kwlist[] = {"name", "recursive", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|p", const_cast<char**>(kwlist),
&name, &recursive)) {
return NULL;
}
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
return NULL;
}
std::string pattern(name);
bool has_wildcard = (pattern.find('*') != std::string::npos);
if (has_wildcard) {
// Return list of all matches
PyObject* results = PyList_New(0);
if (!results) return NULL;
for (auto& drawable : *vec) {
if (matchName(drawable->name, pattern)) {
PyObject* py_drawable = convertDrawableToPython(drawable);
if (!py_drawable) {
Py_DECREF(results);
return NULL;
}
if (PyList_Append(results, py_drawable) < 0) {
Py_DECREF(py_drawable);
Py_DECREF(results);
return NULL;
}
Py_DECREF(py_drawable); // PyList_Append increfs
}
// Recursive search into Frame children
if (recursive && drawable->derived_type() == PyObjectsEnum::UIFRAME) {
auto frame = std::static_pointer_cast<UIFrame>(drawable);
// Create temporary collection object for recursive call
PyTypeObject* collType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection");
if (collType) {
PyUICollectionObject* child_coll = (PyUICollectionObject*)collType->tp_alloc(collType, 0);
if (child_coll) {
child_coll->data = frame->children;
PyObject* child_results = find(child_coll, args, kwds);
if (child_results && PyList_Check(child_results)) {
// Extend results with child results
for (Py_ssize_t i = 0; i < PyList_Size(child_results); i++) {
PyObject* item = PyList_GetItem(child_results, i);
Py_INCREF(item);
PyList_Append(results, item);
Py_DECREF(item);
}
Py_DECREF(child_results);
}
Py_DECREF(child_coll);
}
Py_DECREF(collType);
}
}
}
return results;
} else {
// Return first exact match or None
for (auto& drawable : *vec) {
if (drawable->name == pattern) {
return convertDrawableToPython(drawable);
}
// Recursive search into Frame children
if (recursive && drawable->derived_type() == PyObjectsEnum::UIFRAME) {
auto frame = std::static_pointer_cast<UIFrame>(drawable);
PyTypeObject* collType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection");
if (collType) {
PyUICollectionObject* child_coll = (PyUICollectionObject*)collType->tp_alloc(collType, 0);
if (child_coll) {
child_coll->data = frame->children;
PyObject* result = find(child_coll, args, kwds);
Py_DECREF(child_coll);
Py_DECREF(collType);
if (result && result != Py_None) {
return result;
}
Py_XDECREF(result);
} else {
Py_DECREF(collType);
}
}
}
}
Py_RETURN_NONE;
}
}
PyMethodDef UICollection::methods[] = {
{"append", (PyCFunction)UICollection::append, METH_O,
"append(element)\n\n"
"Add an element to the end of the collection."},
{"extend", (PyCFunction)UICollection::extend, METH_O,
"extend(iterable)\n\n"
"Add all elements from an iterable to the collection."},
{"insert", (PyCFunction)UICollection::insert, METH_VARARGS,
"insert(index, element)\n\n"
"Insert element at index. Like list.insert(), indices past the end append.\n\n"
"Note: If using z_index for sorting, insertion order may not persist after\n"
"the next render. Use name-based .find() for stable element access."},
{"remove", (PyCFunction)UICollection::remove, METH_O,
"remove(element)\n\n"
"Remove first occurrence of element. Raises ValueError if not found."},
{"pop", (PyCFunction)UICollection::pop, METH_VARARGS,
"pop([index]) -> element\n\n"
"Remove and return element at index (default: last element).\n\n"
"Note: If using z_index for sorting, indices may shift after render.\n"
"Use name-based .find() for stable element access."},
{"index", (PyCFunction)UICollection::index_method, METH_O,
"index(element) -> int\n\n"
"Return index of first occurrence of element. Raises ValueError if not found."},
{"count", (PyCFunction)UICollection::count, METH_O,
"count(element) -> int\n\n"
"Count occurrences of element in the collection."},
{"find", (PyCFunction)UICollection::find, METH_VARARGS | METH_KEYWORDS,
"find(name, recursive=False) -> element or list\n\n"
"Find elements by name.\n\n"
"Args:\n"
" name (str): Name to search for. Supports wildcards:\n"
" - 'exact' for exact match (returns single element or None)\n"
" - 'prefix*' for starts-with match (returns list)\n"
" - '*suffix' for ends-with match (returns list)\n"
" - '*substring*' for contains match (returns list)\n"
" recursive (bool): If True, search in Frame children recursively.\n\n"
"Returns:\n"
" Single element if exact match, list if wildcard, None if not found."},
{"append", (PyCFunction)UICollection::append, METH_O},
{"extend", (PyCFunction)UICollection::extend, METH_O},
{"remove", (PyCFunction)UICollection::remove, METH_O},
{"index", (PyCFunction)UICollection::index_method, METH_O},
{"count", (PyCFunction)UICollection::count, METH_O},
{NULL, NULL, 0, NULL}
};

View file

@ -30,11 +30,8 @@ public:
static PyObject* append(PyUICollectionObject* self, PyObject* o);
static PyObject* extend(PyUICollectionObject* self, PyObject* iterable);
static PyObject* remove(PyUICollectionObject* self, PyObject* o);
static PyObject* pop(PyUICollectionObject* self, PyObject* args);
static PyObject* insert(PyUICollectionObject* self, PyObject* args);
static PyObject* index_method(PyUICollectionObject* self, PyObject* value);
static PyObject* count(PyUICollectionObject* self, PyObject* value);
static PyObject* find(PyUICollectionObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
static PyObject* repr(PyUICollectionObject* self);
static int init(PyUICollectionObject* self, PyObject* args, PyObject* kwds);

View file

@ -1953,125 +1953,6 @@ PyObject* UIEntityCollection::extend(PyUIEntityCollectionObject* self, PyObject*
return Py_None;
}
PyObject* UIEntityCollection::pop(PyUIEntityCollectionObject* self, PyObject* args)
{
Py_ssize_t index = -1; // Default to last element
if (!PyArg_ParseTuple(args, "|n", &index)) {
return NULL;
}
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
return NULL;
}
if (list->empty()) {
PyErr_SetString(PyExc_IndexError, "pop from empty EntityCollection");
return NULL;
}
// Handle negative indexing
Py_ssize_t size = static_cast<Py_ssize_t>(list->size());
if (index < 0) {
index += size;
}
if (index < 0 || index >= size) {
PyErr_SetString(PyExc_IndexError, "pop index out of range");
return NULL;
}
// Navigate to the element (std::list requires iteration)
auto it = list->begin();
std::advance(it, index);
// Get the entity before removing
std::shared_ptr<UIEntity> entity = *it;
// Clear grid reference and remove from list
entity->grid = nullptr;
list->erase(it);
// Create Python object for the entity
PyTypeObject* entityType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
if (!entityType) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Entity type");
return NULL;
}
PyUIEntityObject* py_entity = (PyUIEntityObject*)entityType->tp_alloc(entityType, 0);
Py_DECREF(entityType);
if (!py_entity) {
return NULL;
}
py_entity->data = entity;
py_entity->weakreflist = NULL;
return (PyObject*)py_entity;
}
PyObject* UIEntityCollection::insert(PyUIEntityCollectionObject* self, PyObject* args)
{
Py_ssize_t index;
PyObject* o;
if (!PyArg_ParseTuple(args, "nO", &index, &o)) {
return NULL;
}
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
return NULL;
}
// Type checking - must be an Entity
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
PyErr_SetString(PyExc_TypeError, "EntityCollection.insert requires an Entity object");
return NULL;
}
PyUIEntityObject* entity = (PyUIEntityObject*)o;
if (!entity->data) {
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
return NULL;
}
// Handle negative indexing and clamping (Python list.insert behavior)
Py_ssize_t size = static_cast<Py_ssize_t>(list->size());
if (index < 0) {
index += size;
if (index < 0) {
index = 0;
}
} else if (index > size) {
index = size;
}
// Navigate to insert position
auto it = list->begin();
std::advance(it, index);
// Insert and set grid reference
list->insert(it, entity->data);
entity->data->grid = self->grid;
// Initialize gridstate if needed
if (entity->data->gridstate.size() == 0 && self->grid) {
entity->data->gridstate.resize(self->grid->grid_x * self->grid->grid_y);
for (auto& state : entity->data->gridstate) {
state.visible = false;
state.discovered = false;
}
}
Py_RETURN_NONE;
}
PyObject* UIEntityCollection::index_method(PyUIEntityCollectionObject* self, PyObject* value) {
auto list = self->data.get();
if (!list) {
@ -2335,138 +2216,12 @@ PyMappingMethods UIEntityCollection::mpmethods = {
.mp_ass_subscript = (objobjargproc)UIEntityCollection::ass_subscript
};
// Helper function for entity name matching with wildcards
static bool matchEntityName(const std::string& name, const std::string& pattern) {
if (pattern.find('*') != std::string::npos) {
if (pattern == "*") {
return true;
} else if (pattern.front() == '*' && pattern.back() == '*' && pattern.length() > 2) {
std::string substring = pattern.substr(1, pattern.length() - 2);
return name.find(substring) != std::string::npos;
} else if (pattern.front() == '*') {
std::string suffix = pattern.substr(1);
return name.length() >= suffix.length() &&
name.compare(name.length() - suffix.length(), suffix.length(), suffix) == 0;
} else if (pattern.back() == '*') {
std::string prefix = pattern.substr(0, pattern.length() - 1);
return name.compare(0, prefix.length(), prefix) == 0;
}
return name == pattern;
}
return name == pattern;
}
PyObject* UIEntityCollection::find(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds) {
const char* name = nullptr;
static const char* kwlist[] = {"name", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(kwlist), &name)) {
return NULL;
}
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
return NULL;
}
std::string pattern(name);
bool has_wildcard = (pattern.find('*') != std::string::npos);
// Get the Entity type for creating Python objects
PyTypeObject* entityType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
if (!entityType) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Entity type");
return NULL;
}
if (has_wildcard) {
// Return list of all matches
PyObject* results = PyList_New(0);
if (!results) {
Py_DECREF(entityType);
return NULL;
}
for (auto& entity : *list) {
// Entity name is stored in sprite.name
if (matchEntityName(entity->sprite.name, pattern)) {
PyUIEntityObject* py_entity = (PyUIEntityObject*)entityType->tp_alloc(entityType, 0);
if (!py_entity) {
Py_DECREF(results);
Py_DECREF(entityType);
return NULL;
}
py_entity->data = entity;
py_entity->weakreflist = NULL;
if (PyList_Append(results, (PyObject*)py_entity) < 0) {
Py_DECREF(py_entity);
Py_DECREF(results);
Py_DECREF(entityType);
return NULL;
}
Py_DECREF(py_entity); // PyList_Append increfs
}
}
Py_DECREF(entityType);
return results;
} else {
// Return first exact match or None
for (auto& entity : *list) {
if (entity->sprite.name == pattern) {
PyUIEntityObject* py_entity = (PyUIEntityObject*)entityType->tp_alloc(entityType, 0);
if (!py_entity) {
Py_DECREF(entityType);
return NULL;
}
py_entity->data = entity;
py_entity->weakreflist = NULL;
Py_DECREF(entityType);
return (PyObject*)py_entity;
}
}
Py_DECREF(entityType);
Py_RETURN_NONE;
}
}
PyMethodDef UIEntityCollection::methods[] = {
{"append", (PyCFunction)UIEntityCollection::append, METH_O,
"append(entity)\n\n"
"Add an entity to the end of the collection."},
{"extend", (PyCFunction)UIEntityCollection::extend, METH_O,
"extend(iterable)\n\n"
"Add all entities from an iterable to the collection."},
{"insert", (PyCFunction)UIEntityCollection::insert, METH_VARARGS,
"insert(index, entity)\n\n"
"Insert entity at index. Like list.insert(), indices past the end append."},
{"remove", (PyCFunction)UIEntityCollection::remove, METH_O,
"remove(entity)\n\n"
"Remove first occurrence of entity. Raises ValueError if not found."},
{"pop", (PyCFunction)UIEntityCollection::pop, METH_VARARGS,
"pop([index]) -> entity\n\n"
"Remove and return entity at index (default: last entity)."},
{"index", (PyCFunction)UIEntityCollection::index_method, METH_O,
"index(entity) -> int\n\n"
"Return index of first occurrence of entity. Raises ValueError if not found."},
{"count", (PyCFunction)UIEntityCollection::count, METH_O,
"count(entity) -> int\n\n"
"Count occurrences of entity in the collection."},
{"find", (PyCFunction)UIEntityCollection::find, METH_VARARGS | METH_KEYWORDS,
"find(name) -> entity or list\n\n"
"Find entities by name.\n\n"
"Args:\n"
" name (str): Name to search for. Supports wildcards:\n"
" - 'exact' for exact match (returns single entity or None)\n"
" - 'prefix*' for starts-with match (returns list)\n"
" - '*suffix' for ends-with match (returns list)\n"
" - '*substring*' for contains match (returns list)\n\n"
"Returns:\n"
" Single entity if exact match, list if wildcard, None if not found."},
{"append", (PyCFunction)UIEntityCollection::append, METH_O},
{"extend", (PyCFunction)UIEntityCollection::extend, METH_O},
{"remove", (PyCFunction)UIEntityCollection::remove, METH_O},
{"index", (PyCFunction)UIEntityCollection::index_method, METH_O},
{"count", (PyCFunction)UIEntityCollection::count, METH_O},
{NULL, NULL, 0, NULL}
};

View file

@ -141,11 +141,8 @@ public:
static PyObject* append(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* extend(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* remove(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* pop(PyUIEntityCollectionObject* self, PyObject* args);
static PyObject* insert(PyUIEntityCollectionObject* self, PyObject* args);
static PyObject* index_method(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* count(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* find(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
static PyObject* repr(PyUIEntityCollectionObject* self);
static int init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds);

View file

@ -44,11 +44,6 @@ int run_game_engine(const McRogueFaceConfig& config)
if (Py_IsInitialized()) {
McRFPy_API::api_shutdown();
}
// Return exception exit code if a Python exception signaled exit
if (McRFPy_API::shouldExit()) {
return McRFPy_API::exit_code.load();
}
return 0;
}
@ -189,10 +184,6 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
McRFPy_API::api_shutdown();
delete engine;
// Return exception exit code if signaled
if (McRFPy_API::shouldExit()) {
return McRFPy_API::exit_code.load();
}
return result;
}
else if (config.interactive_mode) {
@ -216,10 +207,6 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
engine->run();
McRFPy_API::api_shutdown();
delete engine;
// Return exception exit code if signaled
if (McRFPy_API::shouldExit()) {
return McRFPy_API::exit_code.load();
}
return 0;
}

View file

@ -1,31 +0,0 @@
#!/usr/bin/env python3
"""Test for --continue-after-exceptions behavior (Issue #133)
This test verifies that:
1. By default, unhandled exceptions in timer callbacks cause immediate exit with code 1
2. With --continue-after-exceptions, exceptions are logged but execution continues
"""
import mcrfpy
import sys
def timer_that_raises(runtime):
"""A timer callback that raises an exception"""
raise ValueError("Intentional test exception")
# Create a test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Schedule the timer - it will fire after 50ms
mcrfpy.setTimer("raise_exception", timer_that_raises, 50)
# This test expects:
# - Default behavior: exit with code 1 after first exception
# - With --continue-after-exceptions: continue running (would need timeout or explicit exit)
#
# The test runner should:
# 1. Run without --continue-after-exceptions and expect exit code 1
# 2. Run with --continue-after-exceptions and expect it to not exit immediately
print("Test initialized - timer will raise exception in 50ms")

View file

@ -1,38 +0,0 @@
#!/bin/bash
# Manual test for --continue-after-exceptions feature (Issue #133)
#
# This test must be run manually because it verifies exit codes
# rather than test output.
echo "Testing --continue-after-exceptions feature..."
echo
cd "$(dirname "$0")/../../build"
# Test 1: Default behavior - should exit with code 1 on first exception
echo "Test 1: Default behavior (exit on first exception)"
timeout 5 ./mcrogueface --headless --exec ../tests/notes/test_exception_exit.py 2>&1
EXIT_CODE=$?
echo "Exit code: $EXIT_CODE"
if [ $EXIT_CODE -eq 1 ]; then
echo "[PASS] Exit code is 1 as expected"
else
echo "[FAIL] Expected exit code 1, got $EXIT_CODE"
exit 1
fi
echo
# Test 2: --continue-after-exceptions - should keep running until timeout
echo "Test 2: --continue-after-exceptions (continue after exception)"
timeout 1 ./mcrogueface --headless --continue-after-exceptions --exec ../tests/notes/test_exception_exit.py 2>&1 | tail -5
EXIT_CODE=${PIPESTATUS[0]}
echo "Exit code: $EXIT_CODE"
if [ $EXIT_CODE -eq 124 ]; then
echo "[PASS] Timeout killed it (exit code 124) - continued running as expected"
else
echo "[FAIL] Expected exit code 124 (timeout), got $EXIT_CODE"
exit 1
fi
echo
echo "All tests PASSED!"

View file

@ -1,209 +0,0 @@
#!/usr/bin/env python3
"""Test for UICollection.find() and EntityCollection.find() methods.
Tests issue #40 (search and replace by name) and #41 (.find on collections).
"""
import mcrfpy
import sys
def test_uicollection_find():
"""Test UICollection.find() with exact and wildcard matches."""
print("Testing UICollection.find()...")
# Create a scene with named elements
mcrfpy.createScene("test_find")
ui = mcrfpy.sceneUI("test_find")
# Create frames with names
frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame1.name = "main_frame"
ui.append(frame1)
frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100))
frame2.name = "sidebar_frame"
ui.append(frame2)
frame3 = mcrfpy.Frame(pos=(200, 0), size=(100, 100))
frame3.name = "player_status"
ui.append(frame3)
caption1 = mcrfpy.Caption(text="Hello", pos=(0, 200))
caption1.name = "player_name"
ui.append(caption1)
# Create an unnamed element
unnamed = mcrfpy.Caption(text="Unnamed", pos=(0, 250))
ui.append(unnamed)
# Test exact match - found
result = ui.find("main_frame")
assert result is not None, "Exact match should find element"
assert result.name == "main_frame", f"Found wrong element: {result.name}"
print(" [PASS] Exact match found")
# Test exact match - not found
result = ui.find("nonexistent")
assert result is None, "Should return None when not found"
print(" [PASS] Not found returns None")
# Test prefix wildcard (starts with)
results = ui.find("player*")
assert isinstance(results, list), "Wildcard should return list"
assert len(results) == 2, f"Expected 2 matches, got {len(results)}"
names = [r.name for r in results]
assert "player_status" in names, "player_status should match player*"
assert "player_name" in names, "player_name should match player*"
print(" [PASS] Prefix wildcard works")
# Test suffix wildcard (ends with)
results = ui.find("*_frame")
assert isinstance(results, list), "Wildcard should return list"
assert len(results) == 2, f"Expected 2 matches, got {len(results)}"
names = [r.name for r in results]
assert "main_frame" in names
assert "sidebar_frame" in names
print(" [PASS] Suffix wildcard works")
# Test contains wildcard
results = ui.find("*bar*")
assert isinstance(results, list), "Wildcard should return list"
assert len(results) == 1, f"Expected 1 match, got {len(results)}"
assert results[0].name == "sidebar_frame"
print(" [PASS] Contains wildcard works")
# Test match all
results = ui.find("*")
# Should match all named elements (4 named + 1 unnamed with empty name)
assert isinstance(results, list), "Match all should return list"
assert len(results) == 5, f"Expected 5 matches, got {len(results)}"
print(" [PASS] Match all wildcard works")
# Test empty pattern matches elements with empty names (unnamed elements)
result = ui.find("")
# The unnamed caption has an empty name, so exact match should find it
assert result is not None, "Empty name exact match should find the unnamed element"
print(" [PASS] Empty pattern finds unnamed elements")
print("UICollection.find() tests passed!")
return True
def test_entitycollection_find():
"""Test EntityCollection.find() with exact and wildcard matches."""
print("\nTesting EntityCollection.find()...")
# Create a grid with entities
mcrfpy.createScene("test_entity_find")
ui = mcrfpy.sceneUI("test_entity_find")
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(400, 400))
ui.append(grid)
# Add named entities
player = mcrfpy.Entity(grid_pos=(1, 1))
player.name = "player"
grid.entities.append(player)
enemy1 = mcrfpy.Entity(grid_pos=(2, 2))
enemy1.name = "enemy_goblin"
grid.entities.append(enemy1)
enemy2 = mcrfpy.Entity(grid_pos=(3, 3))
enemy2.name = "enemy_orc"
grid.entities.append(enemy2)
item = mcrfpy.Entity(grid_pos=(4, 4))
item.name = "item_sword"
grid.entities.append(item)
# Test exact match
result = grid.entities.find("player")
assert result is not None, "Should find player"
assert result.name == "player"
print(" [PASS] Entity exact match works")
# Test not found
result = grid.entities.find("boss")
assert result is None, "Should return None when not found"
print(" [PASS] Entity not found returns None")
# Test prefix wildcard
results = grid.entities.find("enemy*")
assert isinstance(results, list)
assert len(results) == 2, f"Expected 2 enemies, got {len(results)}"
print(" [PASS] Entity prefix wildcard works")
# Test suffix wildcard
results = grid.entities.find("*_orc")
assert isinstance(results, list)
assert len(results) == 1
assert results[0].name == "enemy_orc"
print(" [PASS] Entity suffix wildcard works")
print("EntityCollection.find() tests passed!")
return True
def test_recursive_find():
"""Test recursive find in nested Frame children."""
print("\nTesting recursive find in nested frames...")
mcrfpy.createScene("test_recursive")
ui = mcrfpy.sceneUI("test_recursive")
# Create nested structure
parent = mcrfpy.Frame(pos=(0, 0), size=(400, 400))
parent.name = "parent"
ui.append(parent)
child = mcrfpy.Frame(pos=(10, 10), size=(200, 200))
child.name = "child_frame"
parent.children.append(child)
grandchild = mcrfpy.Caption(text="Deep", pos=(5, 5))
grandchild.name = "deep_caption"
child.children.append(grandchild)
# Non-recursive find should not find nested elements
result = ui.find("deep_caption")
assert result is None, "Non-recursive find should not find nested element"
print(" [PASS] Non-recursive doesn't find nested elements")
# Recursive find should find nested elements
result = ui.find("deep_caption", recursive=True)
assert result is not None, "Recursive find should find nested element"
assert result.name == "deep_caption"
print(" [PASS] Recursive find locates nested elements")
# Recursive wildcard should find all matches
results = ui.find("*_frame", recursive=True)
assert isinstance(results, list)
names = [r.name for r in results]
assert "child_frame" in names, "Should find child_frame"
print(" [PASS] Recursive wildcard finds nested matches")
print("Recursive find tests passed!")
return True
if __name__ == "__main__":
try:
all_passed = True
all_passed &= test_uicollection_find()
all_passed &= test_entitycollection_find()
all_passed &= test_recursive_find()
if all_passed:
print("\n" + "="*50)
print("All find() tests PASSED!")
print("="*50)
sys.exit(0)
else:
print("\nSome tests FAILED!")
sys.exit(1)
except Exception as e:
print(f"\nTest failed with exception: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View file

@ -1,263 +0,0 @@
#!/usr/bin/env python3
"""Test for Python list-like methods on UICollection and EntityCollection.
Tests that remove(), pop(), insert(), index(), count() match Python list semantics.
"""
import mcrfpy
import sys
def test_uicollection_remove():
"""Test UICollection.remove() takes a value, not an index."""
print("Testing UICollection.remove()...")
mcrfpy.createScene("test_remove")
ui = mcrfpy.sceneUI("test_remove")
frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100))
frame3 = mcrfpy.Frame(pos=(200, 0), size=(100, 100))
ui.append(frame1)
ui.append(frame2)
ui.append(frame3)
assert len(ui) == 3
# Remove by value (like Python list)
ui.remove(frame2)
assert len(ui) == 2
print(" [PASS] remove(element) works")
# Verify frame2 is gone, but frame1 and frame3 remain
assert ui[0] is not None
assert ui[1] is not None
# Try to remove something not in the list
try:
frame4 = mcrfpy.Frame(pos=(300, 0), size=(100, 100))
ui.remove(frame4)
assert False, "Should have raised ValueError"
except ValueError as e:
assert "not in" in str(e).lower()
print(" [PASS] remove() raises ValueError when not found")
# Try to pass an integer (should fail - no longer takes index)
try:
ui.remove(0)
assert False, "Should have raised TypeError"
except TypeError:
print(" [PASS] remove(int) raises TypeError (correct - takes element, not index)")
print("UICollection.remove() tests passed!")
return True
def test_uicollection_pop():
"""Test UICollection.pop() removes and returns element at index."""
print("\nTesting UICollection.pop()...")
mcrfpy.createScene("test_pop")
ui = mcrfpy.sceneUI("test_pop")
frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame1.name = "first"
frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100))
frame2.name = "second"
frame3 = mcrfpy.Frame(pos=(200, 0), size=(100, 100))
frame3.name = "third"
ui.append(frame1)
ui.append(frame2)
ui.append(frame3)
# pop() with no args removes last
popped = ui.pop()
assert popped.name == "third", f"Expected 'third', got '{popped.name}'"
assert len(ui) == 2
print(" [PASS] pop() removes last element")
# pop(0) removes first
popped = ui.pop(0)
assert popped.name == "first", f"Expected 'first', got '{popped.name}'"
assert len(ui) == 1
print(" [PASS] pop(0) removes first element")
# pop(-1) is same as pop()
ui.append(mcrfpy.Frame(pos=(0, 0), size=(10, 10)))
ui[-1].name = "new_last"
popped = ui.pop(-1)
assert popped.name == "new_last"
print(" [PASS] pop(-1) removes last element")
# pop from empty collection
ui.pop() # Remove last remaining element
try:
ui.pop()
assert False, "Should have raised IndexError"
except IndexError as e:
assert "empty" in str(e).lower()
print(" [PASS] pop() from empty raises IndexError")
print("UICollection.pop() tests passed!")
return True
def test_uicollection_insert():
"""Test UICollection.insert() inserts at given index."""
print("\nTesting UICollection.insert()...")
mcrfpy.createScene("test_insert")
ui = mcrfpy.sceneUI("test_insert")
frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame1.name = "first"
frame3 = mcrfpy.Frame(pos=(200, 0), size=(100, 100))
frame3.name = "third"
ui.append(frame1)
ui.append(frame3)
# Insert in middle
frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100))
frame2.name = "second"
ui.insert(1, frame2)
assert len(ui) == 3
assert ui[0].name == "first"
assert ui[1].name == "second"
assert ui[2].name == "third"
print(" [PASS] insert(1, element) inserts at index 1")
# Insert at beginning
frame0 = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
frame0.name = "zero"
ui.insert(0, frame0)
assert ui[0].name == "zero"
print(" [PASS] insert(0, element) inserts at beginning")
# Insert at end (index > len)
frame_end = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
frame_end.name = "end"
ui.insert(100, frame_end) # Way past end
assert ui[-1].name == "end"
print(" [PASS] insert(100, element) appends when index > len")
# Negative index
frame_neg = mcrfpy.Frame(pos=(0, 0), size=(50, 50))
frame_neg.name = "negative"
current_len = len(ui)
ui.insert(-1, frame_neg) # Insert before last
assert ui[-2].name == "negative"
print(" [PASS] insert(-1, element) inserts before last")
print("UICollection.insert() tests passed!")
return True
def test_entitycollection_pop_insert():
"""Test EntityCollection.pop() and insert()."""
print("\nTesting EntityCollection.pop() and insert()...")
mcrfpy.createScene("test_entity_pop")
ui = mcrfpy.sceneUI("test_entity_pop")
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(400, 400))
ui.append(grid)
e1 = mcrfpy.Entity(grid_pos=(1, 1))
e1.name = "first"
e2 = mcrfpy.Entity(grid_pos=(2, 2))
e2.name = "second"
e3 = mcrfpy.Entity(grid_pos=(3, 3))
e3.name = "third"
grid.entities.append(e1)
grid.entities.append(e2)
grid.entities.append(e3)
# Test pop()
popped = grid.entities.pop()
assert popped.name == "third"
assert len(grid.entities) == 2
print(" [PASS] EntityCollection.pop() works")
# Test pop(0)
popped = grid.entities.pop(0)
assert popped.name == "first"
assert len(grid.entities) == 1
print(" [PASS] EntityCollection.pop(0) works")
# Test insert
e_new = mcrfpy.Entity(grid_pos=(5, 5))
e_new.name = "new"
grid.entities.insert(0, e_new)
assert grid.entities[0].name == "new"
print(" [PASS] EntityCollection.insert() works")
print("EntityCollection pop/insert tests passed!")
return True
def test_index_and_count():
"""Test index() and count() methods."""
print("\nTesting index() and count()...")
mcrfpy.createScene("test_index_count")
ui = mcrfpy.sceneUI("test_index_count")
frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100))
ui.append(frame1)
ui.append(frame2)
# index() returns integer
idx = ui.index(frame1)
assert idx == 0, f"Expected 0, got {idx}"
assert isinstance(idx, int)
print(" [PASS] index() returns integer")
idx = ui.index(frame2)
assert idx == 1
print(" [PASS] index() finds correct position")
# count() returns integer
cnt = ui.count(frame1)
assert cnt == 1
assert isinstance(cnt, int)
print(" [PASS] count() returns integer")
# count of element not in collection
frame3 = mcrfpy.Frame(pos=(200, 0), size=(100, 100))
cnt = ui.count(frame3)
assert cnt == 0
print(" [PASS] count() returns 0 for element not in collection")
print("index() and count() tests passed!")
return True
if __name__ == "__main__":
try:
all_passed = True
all_passed &= test_uicollection_remove()
all_passed &= test_uicollection_pop()
all_passed &= test_uicollection_insert()
all_passed &= test_entitycollection_pop_insert()
all_passed &= test_index_and_count()
if all_passed:
print("\n" + "="*50)
print("All list-like method tests PASSED!")
print("="*50)
sys.exit(0)
else:
print("\nSome tests FAILED!")
sys.exit(1)
except Exception as e:
print(f"\nTest failed with exception: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View file

@ -92,9 +92,9 @@ def run_tests(runtime):
test(retrieved_caption.caption_id == "test_caption", "Caption custom data preserved")
# Test 8: Test removal and re-addition
# Use del to remove by index (Python standard), or .remove(element) to remove by value
#scene_ui.remove(frame) # TypeError: UICollection.remove requires an integer index to remove - seems like a C++ bug in the remove() implementation
print(f"before remove: {len(scene_ui)=}")
del scene_ui[-1] # Remove last element by index
scene_ui.remove(-1)
print(f"after remove: {len(scene_ui)=}")
scene_ui.append(frame)

View file

@ -1,203 +0,0 @@
#!/usr/bin/env python3
"""Unit tests for Vector convenience features (Issue #109)
Tests:
- Sequence protocol: indexing, negative indexing, iteration, unpacking
- Tuple comparison: Vector == tuple, Vector != tuple
- Integer conversion: .floor() method, .int property
- Boolean check: falsey for (0, 0)
"""
import mcrfpy
import sys
def approx(a, b, epsilon=1e-5):
"""Check if two floats are approximately equal (handles float32 precision)"""
return abs(a - b) < epsilon
def test_indexing():
"""Test sequence protocol indexing"""
# Use values that are exact in float32: 3.5 = 7/2, 7.5 = 15/2
v = mcrfpy.Vector(3.5, 7.5)
# Positive indices
assert v[0] == 3.5, f"v[0] should be 3.5, got {v[0]}"
assert v[1] == 7.5, f"v[1] should be 7.5, got {v[1]}"
# Negative indices
assert v[-1] == 7.5, f"v[-1] should be 7.5, got {v[-1]}"
assert v[-2] == 3.5, f"v[-2] should be 3.5, got {v[-2]}"
# Out of bounds
try:
_ = v[2]
assert False, "v[2] should raise IndexError"
except IndexError:
pass
try:
_ = v[-3]
assert False, "v[-3] should raise IndexError"
except IndexError:
pass
print(" [PASS] Indexing")
def test_length():
"""Test len() on Vector"""
v = mcrfpy.Vector(1, 2)
assert len(v) == 2, f"len(Vector) should be 2, got {len(v)}"
print(" [PASS] Length")
def test_iteration():
"""Test iteration and unpacking"""
# Use values that are exact in float32
v = mcrfpy.Vector(10.5, 20.5)
# Iteration - use approximate comparison for float32 precision
values = list(v)
assert len(values) == 2, f"list(v) should have 2 elements"
assert approx(values[0], 10.5), f"list(v)[0] should be ~10.5, got {values[0]}"
assert approx(values[1], 20.5), f"list(v)[1] should be ~20.5, got {values[1]}"
# Unpacking
x, y = v
assert approx(x, 10.5), f"Unpacked x should be ~10.5, got {x}"
assert approx(y, 20.5), f"Unpacked y should be ~20.5, got {y}"
# tuple() conversion
t = tuple(v)
assert len(t) == 2 and approx(t[0], 10.5) and approx(t[1], 20.5), f"tuple(v) should be ~(10.5, 20.5), got {t}"
print(" [PASS] Iteration and unpacking")
def test_tuple_comparison():
"""Test comparison with tuples"""
# Use integer values which are exact in float32
v = mcrfpy.Vector(5, 6)
# Vector == tuple (integers are exact)
assert v == (5, 6), "Vector(5, 6) should equal (5, 6)"
assert v == (5.0, 6.0), "Vector(5, 6) should equal (5.0, 6.0)"
# Vector != tuple
assert v != (5, 7), "Vector(5, 6) should not equal (5, 7)"
assert v != (4, 6), "Vector(5, 6) should not equal (4, 6)"
# Tuple == Vector (reverse comparison)
assert (5, 6) == v, "(5, 6) should equal Vector(5, 6)"
assert (5, 7) != v, "(5, 7) should not equal Vector(5, 6)"
# Edge cases
v_zero = mcrfpy.Vector(0, 0)
assert v_zero == (0, 0), "Vector(0, 0) should equal (0, 0)"
assert v_zero == (0.0, 0.0), "Vector(0, 0) should equal (0.0, 0.0)"
# Negative values - use exact float32 values (x.5 are exact)
v_neg = mcrfpy.Vector(-3.5, -7.5)
assert v_neg == (-3.5, -7.5), "Vector(-3.5, -7.5) should equal (-3.5, -7.5)"
print(" [PASS] Tuple comparison")
def test_floor_method():
"""Test .floor() method"""
# Use values that clearly floor to different integers
v = mcrfpy.Vector(3.75, -2.25) # exact in float32
floored = v.floor()
assert isinstance(floored, mcrfpy.Vector), ".floor() should return a Vector"
assert floored.x == 3.0, f"floor(3.75) should be 3.0, got {floored.x}"
assert floored.y == -3.0, f"floor(-2.25) should be -3.0, got {floored.y}"
# Positive values (use exact float32 values)
v2 = mcrfpy.Vector(5.875, 0.125) # exact in float32
f2 = v2.floor()
assert f2 == (5.0, 0.0), f"floor(5.875, 0.125) should be (5.0, 0.0), got ({f2.x}, {f2.y})"
# Already integers
v3 = mcrfpy.Vector(10.0, 20.0)
f3 = v3.floor()
assert f3 == (10.0, 20.0), f"floor(10.0, 20.0) should be (10.0, 20.0)"
print(" [PASS] .floor() method")
def test_int_property():
"""Test .int property"""
# Use exact float32 values
v = mcrfpy.Vector(3.75, -2.25)
int_tuple = v.int
assert isinstance(int_tuple, tuple), ".int should return a tuple"
assert len(int_tuple) == 2, ".int tuple should have 2 elements"
assert int_tuple == (3, -3), f".int should be (3, -3), got {int_tuple}"
# Check it's hashable (can be used as dict key)
d = {}
d[v.int] = "test"
assert d[(3, -3)] == "test", ".int tuple should work as dict key"
# Positive values (use exact float32 values)
v2 = mcrfpy.Vector(5.875, 0.125)
assert v2.int == (5, 0), f".int should be (5, 0), got {v2.int}"
print(" [PASS] .int property")
def test_bool_check():
"""Test boolean conversion (already implemented, verify it works)"""
v_zero = mcrfpy.Vector(0, 0)
v_nonzero = mcrfpy.Vector(1, 0)
v_nonzero2 = mcrfpy.Vector(0, 1)
assert not bool(v_zero), "Vector(0, 0) should be falsey"
assert bool(v_nonzero), "Vector(1, 0) should be truthy"
assert bool(v_nonzero2), "Vector(0, 1) should be truthy"
# In if statement
if v_zero:
assert False, "Vector(0, 0) should not pass if check"
if not v_nonzero:
assert False, "Vector(1, 0) should pass if check"
print(" [PASS] Boolean check")
def test_combined_operations():
"""Test that new features work together with existing operations"""
# Use exact float32 values
v1 = mcrfpy.Vector(3.5, 4.5)
v2 = mcrfpy.Vector(1.5, 2.5)
# Arithmetic then tuple comparison (sums are exact)
result = v1 + v2
assert result == (5.0, 7.0), f"(3.5+1.5, 4.5+2.5) should equal (5.0, 7.0), got ({result.x}, {result.y})"
# Floor then use as dict key
floored = v1.floor()
positions = {floored.int: "player"}
assert (3, 4) in positions, "floored.int should work as dict key"
# Unpack, modify, compare (products are exact)
x, y = v1
v3 = mcrfpy.Vector(x * 2, y * 2)
assert v3 == (7.0, 9.0), f"Unpacking and creating new vector should work, got ({v3.x}, {v3.y})"
print(" [PASS] Combined operations")
def run_tests():
"""Run all tests"""
print("Testing Vector convenience features (Issue #109)...")
test_indexing()
test_length()
test_iteration()
test_tuple_comparison()
test_floor_method()
test_int_property()
test_bool_check()
test_combined_operations()
print("\n[ALL TESTS PASSED]")
sys.exit(0)
# Run tests immediately (no game loop needed)
run_tests()