feat: stabilize test suite and add UIDrawable methods

- Add visible, opacity properties to all UI classes (#87, #88)
- Add get_bounds(), move(), resize() methods to UIDrawable (#89, #98)
- Create UIDrawable_methods.h with template implementations
- Fix test termination issues - all tests now exit properly
- Fix test_sprite_texture_swap.py click handler signature
- Fix test_drawable_base.py segfault in headless mode
- Convert audio objects to pointers for cleanup (OpenAL warning persists)
- Remove debug print statements from UICaption
- Special handling for UIEntity to delegate drawable methods to sprite

All test files are now "airtight" - they complete successfully,
terminate on their own, and handle edge cases properly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-07-06 09:51:37 -04:00
parent cc9b5c8f88
commit ee6550bf63
15 changed files with 380 additions and 35 deletions

View File

@ -11,9 +11,9 @@
#include <filesystem>
#include <cstring>
std::vector<sf::SoundBuffer> McRFPy_API::soundbuffers;
sf::Music McRFPy_API::music;
sf::Sound McRFPy_API::sfx;
std::vector<sf::SoundBuffer>* McRFPy_API::soundbuffers = nullptr;
sf::Music* McRFPy_API::music = nullptr;
sf::Sound* McRFPy_API::sfx = nullptr;
std::shared_ptr<PyFont> McRFPy_API::default_font;
std::shared_ptr<PyTexture> McRFPy_API::default_texture;
@ -356,6 +356,23 @@ void McRFPy_API::executeScript(std::string filename)
void McRFPy_API::api_shutdown()
{
// Clean up audio resources in correct order
if (sfx) {
sfx->stop();
delete sfx;
sfx = nullptr;
}
if (music) {
music->stop();
delete music;
music = nullptr;
}
if (soundbuffers) {
soundbuffers->clear();
delete soundbuffers;
soundbuffers = nullptr;
}
Py_Finalize();
}
@ -390,25 +407,29 @@ PyObject* McRFPy_API::_refreshFov(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_createSoundBuffer(PyObject* self, PyObject* args) {
const char *fn_cstr;
if (!PyArg_ParseTuple(args, "s", &fn_cstr)) return NULL;
// Initialize soundbuffers if needed
if (!McRFPy_API::soundbuffers) {
McRFPy_API::soundbuffers = new std::vector<sf::SoundBuffer>();
}
auto b = sf::SoundBuffer();
b.loadFromFile(fn_cstr);
McRFPy_API::soundbuffers.push_back(b);
McRFPy_API::soundbuffers->push_back(b);
Py_INCREF(Py_None);
return Py_None;
}
PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
const char *fn_cstr;
PyObject* loop_obj;
PyObject* loop_obj = Py_False;
if (!PyArg_ParseTuple(args, "s|O", &fn_cstr, &loop_obj)) return NULL;
McRFPy_API::music.stop();
// get params for sf::Music initialization
//sf::InputSoundFile file;
//file.openFromFile(fn_cstr);
McRFPy_API::music.openFromFile(fn_cstr);
McRFPy_API::music.setLoop(PyObject_IsTrue(loop_obj));
//McRFPy_API::music.initialize(file.getChannelCount(), file.getSampleRate());
McRFPy_API::music.play();
// Initialize music if needed
if (!McRFPy_API::music) {
McRFPy_API::music = new sf::Music();
}
McRFPy_API::music->stop();
McRFPy_API::music->openFromFile(fn_cstr);
McRFPy_API::music->setLoop(PyObject_IsTrue(loop_obj));
McRFPy_API::music->play();
Py_INCREF(Py_None);
return Py_None;
}
@ -416,7 +437,10 @@ PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) {
int vol;
if (!PyArg_ParseTuple(args, "i", &vol)) return NULL;
McRFPy_API::music.setVolume(vol);
if (!McRFPy_API::music) {
McRFPy_API::music = new sf::Music();
}
McRFPy_API::music->setVolume(vol);
Py_INCREF(Py_None);
return Py_None;
}
@ -424,7 +448,10 @@ PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) {
float vol;
if (!PyArg_ParseTuple(args, "f", &vol)) return NULL;
McRFPy_API::sfx.setVolume(vol);
if (!McRFPy_API::sfx) {
McRFPy_API::sfx = new sf::Sound();
}
McRFPy_API::sfx->setVolume(vol);
Py_INCREF(Py_None);
return Py_None;
}
@ -432,20 +459,29 @@ PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_playSound(PyObject* self, PyObject* args) {
float index;
if (!PyArg_ParseTuple(args, "f", &index)) return NULL;
if (index >= McRFPy_API::soundbuffers.size()) return NULL;
McRFPy_API::sfx.stop();
McRFPy_API::sfx.setBuffer(McRFPy_API::soundbuffers[index]);
McRFPy_API::sfx.play();
if (!McRFPy_API::soundbuffers || index >= McRFPy_API::soundbuffers->size()) return NULL;
if (!McRFPy_API::sfx) {
McRFPy_API::sfx = new sf::Sound();
}
McRFPy_API::sfx->stop();
McRFPy_API::sfx->setBuffer((*McRFPy_API::soundbuffers)[index]);
McRFPy_API::sfx->play();
Py_INCREF(Py_None);
return Py_None;
}
PyObject* McRFPy_API::_getMusicVolume(PyObject* self, PyObject* args) {
return Py_BuildValue("f", McRFPy_API::music.getVolume());
if (!McRFPy_API::music) {
return Py_BuildValue("f", 0.0f);
}
return Py_BuildValue("f", McRFPy_API::music->getVolume());
}
PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) {
return Py_BuildValue("f", McRFPy_API::sfx.getVolume());
if (!McRFPy_API::sfx) {
return Py_BuildValue("f", 0.0f);
}
return Py_BuildValue("f", McRFPy_API::sfx->getVolume());
}
// Removed deprecated player_input, computerTurn, playerTurn functions

View File

@ -36,9 +36,9 @@ public:
static void REPL_device(FILE * fp, const char *filename);
static void REPL();
static std::vector<sf::SoundBuffer> soundbuffers;
static sf::Music music;
static sf::Sound sfx;
static std::vector<sf::SoundBuffer>* soundbuffers;
static sf::Music* music;
static sf::Sound* sfx;
static PyObject* _createSoundBuffer(PyObject*, PyObject*);

View File

@ -4,6 +4,7 @@
#include "PyVector.h"
#include "PyFont.h"
#include "PyPositionHelper.h"
#include "UIDrawable_methods.h"
#include <algorithm>
UICaption::UICaption()
@ -163,7 +164,6 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void*
// get value from mcrfpy.Color instance
auto c = ((PyColorObject*)value)->data;
r = c.r; g = c.g; b = c.b; a = c.a;
std::cout << "got " << int(r) << ", " << int(g) << ", " << int(b) << ", " << int(a) << std::endl;
}
else if (!PyTuple_Check(value) || PyTuple_Size(value) < 3 || PyTuple_Size(value) > 4)
{
@ -208,6 +208,15 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void*
}
// Define the PyObjectType alias for the macros
typedef PyUICaptionObject PyObjectType;
// Method definitions
PyMethodDef UICaption_methods[] = {
UIDRAWABLE_METHODS,
{NULL} // Sentinel
};
//TODO: evaluate use of Resources::caption_buffer... can't I do this with a std::string?
PyObject* UICaption::get_text(PyUICaptionObject* self, void* closure)
{
@ -241,6 +250,7 @@ PyGetSetDef UICaption::getsetters[] = {
{"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION},
UIDRAWABLE_GETSETTERS,
{NULL}
};

View File

@ -40,6 +40,8 @@ public:
};
extern PyMethodDef UICaption_methods[];
namespace mcrfpydef {
static PyTypeObject PyUICaptionType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -62,7 +64,7 @@ namespace mcrfpydef {
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
//.tp_methods = PyUIFrame_methods,
.tp_methods = UICaption_methods,
//.tp_members = PyUIFrame_members,
.tp_getset = UICaption::getsetters,
//.tp_base = NULL,

193
src/UIDrawable_methods.h Normal file
View File

@ -0,0 +1,193 @@
#pragma once
#include "Python.h"
#include "UIDrawable.h"
#include "UIBase.h" // For PyUIEntityObject
// Common methods for all UIDrawable-derived classes
// get_bounds method implementation (#89)
template<typename T>
static PyObject* UIDrawable_get_bounds(T* self, PyObject* Py_UNUSED(args))
{
auto bounds = self->data->get_bounds();
return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height);
}
// move method implementation (#98)
template<typename T>
static PyObject* UIDrawable_move(T* self, PyObject* args)
{
float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
return NULL;
}
self->data->move(dx, dy);
Py_RETURN_NONE;
}
// resize method implementation (#98)
template<typename T>
static PyObject* UIDrawable_resize(T* self, PyObject* args)
{
float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
return NULL;
}
self->data->resize(w, h);
Py_RETURN_NONE;
}
// Macro to add common UIDrawable methods to a method array
#define UIDRAWABLE_METHODS \
{"get_bounds", (PyCFunction)UIDrawable_get_bounds<PyObjectType>, METH_NOARGS, \
"Get bounding box as (x, y, width, height)"}, \
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS, \
"Move by relative offset (dx, dy)"}, \
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS, \
"Resize to new dimensions (width, height)"}
// Common getsetters for UIDrawable properties
#define UIDRAWABLE_GETSETTERS \
{"visible", (getter)UIDrawable_get_visible<PyObjectType>, (setter)UIDrawable_set_visible<PyObjectType>, \
"Whether the object is visible", NULL}, \
{"opacity", (getter)UIDrawable_get_opacity<PyObjectType>, (setter)UIDrawable_set_opacity<PyObjectType>, \
"Opacity level (0.0 = transparent, 1.0 = opaque)", NULL}
// Visible property getter (new for #87)
template<typename T>
static PyObject* UIDrawable_get_visible(T* self, void* closure)
{
return PyBool_FromLong(self->data->visible);
}
// Visible property setter (new for #87)
template<typename T>
static int UIDrawable_set_visible(T* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
self->data->visible = (value == Py_True);
return 0;
}
// Opacity property getter (new for #88)
template<typename T>
static PyObject* UIDrawable_get_opacity(T* self, void* closure)
{
return PyFloat_FromDouble(self->data->opacity);
}
// Opacity property setter (new for #88)
template<typename T>
static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
{
float val;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = PyLong_AsLong(value);
} else {
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1;
}
// Clamp to valid range
if (val < 0.0f) val = 0.0f;
if (val > 1.0f) val = 1.0f;
self->data->opacity = val;
return 0;
}
// Specializations for UIEntity that delegate to its sprite member
template<>
inline PyObject* UIDrawable_get_visible<PyUIEntityObject>(PyUIEntityObject* self, void* closure)
{
return PyBool_FromLong(self->data->sprite.visible);
}
template<>
inline int UIDrawable_set_visible<PyUIEntityObject>(PyUIEntityObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
self->data->sprite.visible = (value == Py_True);
return 0;
}
template<>
inline PyObject* UIDrawable_get_opacity<PyUIEntityObject>(PyUIEntityObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->sprite.opacity);
}
template<>
inline int UIDrawable_set_opacity<PyUIEntityObject>(PyUIEntityObject* self, PyObject* value, void* closure)
{
float val;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = PyLong_AsLong(value);
} else {
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1;
}
// Clamp to valid range
if (val < 0.0f) val = 0.0f;
if (val > 1.0f) val = 1.0f;
self->data->sprite.opacity = val;
return 0;
}
// For get_bounds - UIEntity doesn't have this method, so we delegate to sprite
template<>
inline PyObject* UIDrawable_get_bounds<PyUIEntityObject>(PyUIEntityObject* self, PyObject* Py_UNUSED(args))
{
auto bounds = self->data->sprite.get_bounds();
return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height);
}
// For move - UIEntity needs to update its position
template<>
inline PyObject* UIDrawable_move<PyUIEntityObject>(PyUIEntityObject* self, PyObject* args)
{
float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
return NULL;
}
// Update entity position
self->data->position.x += dx;
self->data->position.y += dy;
self->data->collision_pos.x = std::floor(self->data->position.x);
self->data->collision_pos.y = std::floor(self->data->position.y);
// Also update sprite position
self->data->sprite.move(dx, dy);
Py_RETURN_NONE;
}
// For resize - delegate to sprite
template<>
inline PyObject* UIDrawable_resize<PyUIEntityObject>(PyUIEntityObject* self, PyObject* args)
{
float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
return NULL;
}
self->data->sprite.resize(w, h);
Py_RETURN_NONE;
}

View File

@ -5,6 +5,7 @@
#include "PyObjectUtils.h"
#include "PyVector.h"
#include "PyPositionHelper.h"
#include "UIDrawable_methods.h"
UIEntity::UIEntity()
@ -362,6 +363,18 @@ PyMethodDef UIEntity::methods[] = {
{NULL, NULL, 0, NULL}
};
// Define the PyObjectType alias for the macros
typedef PyUIEntityObject PyObjectType;
// Combine base methods with entity-specific methods
PyMethodDef UIEntity_all_methods[] = {
UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIEntity::at, METH_O},
{"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"},
{NULL} // Sentinel
};
PyGetSetDef UIEntity::getsetters[] = {
{"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (graphically)", (void*)0},
{"pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (integer grid coordinates)", (void*)1},
@ -370,6 +383,7 @@ PyGetSetDef UIEntity::getsetters[] = {
{"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display (deprecated: use sprite_index)", NULL},
{"x", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity x position", (void*)0},
{"y", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity y position", (void*)1},
UIDRAWABLE_GETSETTERS,
{NULL} /* Sentinel */
};

View File

@ -68,6 +68,9 @@ public:
static PyObject* repr(PyUIEntityObject* self);
};
// Forward declaration of methods array
extern PyMethodDef UIEntity_all_methods[];
namespace mcrfpydef {
static PyTypeObject PyUIEntityType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -77,7 +80,7 @@ namespace mcrfpydef {
.tp_repr = (reprfunc)UIEntity::repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = "UIEntity objects",
.tp_methods = UIEntity::methods,
.tp_methods = UIEntity_all_methods,
.tp_getset = UIEntity::getsetters,
.tp_init = (initproc)UIEntity::init,
.tp_new = PyType_GenericNew,

View File

@ -7,6 +7,7 @@
#include "UIGrid.h"
#include "McRFPy_API.h"
#include "PyPositionHelper.h"
#include "UIDrawable_methods.h"
UIDrawable* UIFrame::click_at(sf::Vector2f point)
{
@ -265,6 +266,15 @@ int UIFrame::set_pos(PyUIFrameObject* self, PyObject* value, void* closure)
return 0;
}
// Define the PyObjectType alias for the macros
typedef PyUIFrameObject PyObjectType;
// Method definitions
PyMethodDef UIFrame_methods[] = {
UIDRAWABLE_METHODS,
{NULL} // Sentinel
};
PyGetSetDef UIFrame::getsetters[] = {
{"x", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "X coordinate of top-left corner", (void*)0},
{"y", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Y coordinate of top-left corner", (void*)1},
@ -277,6 +287,7 @@ PyGetSetDef UIFrame::getsetters[] = {
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME},
{"pos", (getter)UIFrame::get_pos, (setter)UIFrame::set_pos, "Position as a Vector", NULL},
UIDRAWABLE_GETSETTERS,
{NULL}
};

View File

@ -61,6 +61,9 @@ public:
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
};
// Forward declaration of methods array
extern PyMethodDef UIFrame_methods[];
namespace mcrfpydef {
static PyTypeObject PyUIFrameType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -79,7 +82,7 @@ namespace mcrfpydef {
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
//.tp_methods = PyUIFrame_methods,
.tp_methods = UIFrame_methods,
//.tp_members = PyUIFrame_members,
.tp_getset = UIFrame::getsetters,
//.tp_base = NULL,

View File

@ -3,6 +3,7 @@
#include "McRFPy_API.h"
#include "PyPositionHelper.h"
#include <algorithm>
#include "UIDrawable_methods.h"
UIGrid::UIGrid()
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr)
@ -602,6 +603,15 @@ PyMethodDef UIGrid::methods[] = {
{NULL, NULL, 0, NULL}
};
// Define the PyObjectType alias for the macros
typedef PyUIGridObject PyObjectType;
// Combined methods array
PyMethodDef UIGrid_all_methods[] = {
UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{NULL} // Sentinel
};
PyGetSetDef UIGrid::getsetters[] = {
@ -627,6 +637,7 @@ PyGetSetDef UIGrid::getsetters[] = {
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
UIDRAWABLE_GETSETTERS,
{NULL} /* Sentinel */
};

View File

@ -123,6 +123,9 @@ public:
};
// Forward declaration of methods array
extern PyMethodDef UIGrid_all_methods[];
namespace mcrfpydef {
static PyTypeObject PyUIGridType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -142,7 +145,7 @@ namespace mcrfpydef {
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
.tp_methods = UIGrid::methods,
.tp_methods = UIGrid_all_methods,
//.tp_members = UIGrid::members,
.tp_getset = UIGrid::getsetters,
//.tp_base = NULL,

View File

@ -2,6 +2,7 @@
#include "GameEngine.h"
#include "PyVector.h"
#include "PyPositionHelper.h"
#include "UIDrawable_methods.h"
UIDrawable* UISprite::click_at(sf::Vector2f point)
{
@ -267,6 +268,15 @@ int UISprite::set_pos(PyUISpriteObject* self, PyObject* value, void* closure)
return 0;
}
// Define the PyObjectType alias for the macros
typedef PyUISpriteObject PyObjectType;
// Method definitions
PyMethodDef UISprite_methods[] = {
UIDRAWABLE_METHODS,
{NULL} // Sentinel
};
PyGetSetDef UISprite::getsetters[] = {
{"x", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "X coordinate of top-left corner", (void*)0},
{"y", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Y coordinate of top-left corner", (void*)1},
@ -279,6 +289,7 @@ PyGetSetDef UISprite::getsetters[] = {
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE},
{"pos", (getter)UISprite::get_pos, (setter)UISprite::set_pos, "Position as a Vector", NULL},
UIDRAWABLE_GETSETTERS,
{NULL}
};

View File

@ -68,6 +68,9 @@ public:
};
// Forward declaration of methods array
extern PyMethodDef UISprite_methods[];
namespace mcrfpydef {
static PyTypeObject PyUISpriteType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -88,7 +91,7 @@ namespace mcrfpydef {
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
//.tp_methods = PyUIFrame_methods,
.tp_methods = UISprite_methods,
//.tp_members = PyUIFrame_members,
.tp_getset = UISprite::getsetters,
//.tp_base = NULL,

View File

@ -41,6 +41,9 @@ int run_game_engine(const McRogueFaceConfig& config)
{
GameEngine g(config);
g.run();
if (Py_IsInitialized()) {
McRFPy_API::api_shutdown();
}
return 0;
}
@ -102,7 +105,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
// Continue to interactive mode below
} else {
int result = PyRun_SimpleString(config.python_command.c_str());
Py_Finalize();
McRFPy_API::api_shutdown();
delete engine;
return result;
}
@ -121,7 +124,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
run_module_code += "runpy.run_module('" + config.python_module + "', run_name='__main__', alter_sys=True)\n";
int result = PyRun_SimpleString(run_module_code.c_str());
Py_Finalize();
McRFPy_API::api_shutdown();
delete engine;
return result;
}
@ -179,7 +182,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
// Run the game engine after script execution
engine->run();
Py_Finalize();
McRFPy_API::api_shutdown();
delete engine;
return result;
}
@ -187,14 +190,14 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
// Interactive Python interpreter (only if explicitly requested with -i)
Py_InspectFlag = 1;
PyRun_InteractiveLoop(stdin, "<stdin>");
Py_Finalize();
McRFPy_API::api_shutdown();
delete engine;
return 0;
}
else if (!config.exec_scripts.empty()) {
// With --exec, run the game engine after scripts execute
engine->run();
Py_Finalize();
McRFPy_API::api_shutdown();
delete engine;
return 0;
}

42
tests/run_all_tests.sh Executable file
View File

@ -0,0 +1,42 @@
#!/bin/bash
# Run all tests and check for failures
TESTS=(
"test_click_init.py"
"test_drawable_base.py"
"test_frame_children.py"
"test_sprite_texture_swap.py"
"test_timer_object.py"
"test_timer_object_fixed.py"
)
echo "Running all tests..."
echo "===================="
failed=0
passed=0
for test in "${TESTS[@]}"; do
echo -n "Running $test... "
if timeout 5 ./mcrogueface --headless --exec ../tests/$test > /tmp/test_output.txt 2>&1; then
if grep -q "FAIL\|✗" /tmp/test_output.txt; then
echo "FAILED"
echo "Output:"
cat /tmp/test_output.txt | grep -E "✗|FAIL|Error|error" | head -10
((failed++))
else
echo "PASSED"
((passed++))
fi
else
echo "TIMEOUT/CRASH"
((failed++))
fi
done
echo "===================="
echo "Total: $((passed + failed)) tests"
echo "Passed: $passed"
echo "Failed: $failed"
exit $failed