Compare commits

..

3 commits

40 changed files with 1883 additions and 128 deletions

1
.gitignore vendored
View file

@ -16,6 +16,7 @@ CMakeFiles/
Makefile
*.zip
__lib/
__lib_windows/
_oldscripts/
assets/
cellular_automata_fire/

View file

@ -8,14 +8,33 @@ project(McRogueFace)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# Detect cross-compilation for Windows (MinGW)
if(CMAKE_CROSSCOMPILING AND WIN32)
set(MCRF_CROSS_WINDOWS TRUE)
message(STATUS "Cross-compiling for Windows using MinGW")
endif()
# Add include directories
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux)
include_directories(${CMAKE_SOURCE_DIR}/deps)
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux/Python-3.11.1)
include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
# Python includes: use different paths for Windows vs Linux
if(MCRF_CROSS_WINDOWS)
# Windows cross-compilation: use cpython headers with PC/pyconfig.h
# Problem: Python.h uses #include "pyconfig.h" which finds Include/pyconfig.h (Linux) first
# Solution: Use -include to force Windows pyconfig.h to be included first
# This defines MS_WINDOWS before Python.h is processed, ensuring correct struct layouts
add_compile_options(-include ${CMAKE_SOURCE_DIR}/deps/cpython/PC/pyconfig.h)
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include)
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/PC) # For other Windows-specific headers
# Also include SFML and libtcod Windows headers
include_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/include)
include_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/include)
else()
# Native builds (Linux/Windows): use existing Python setup
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
endif()
# ImGui and ImGui-SFML include directories
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui)
@ -37,35 +56,71 @@ file(GLOB_RECURSE SOURCES "src/*.cpp")
list(APPEND SOURCES ${IMGUI_SOURCES})
# Find OpenGL (required by ImGui-SFML)
find_package(OpenGL REQUIRED)
# Create a list of libraries to link against
set(LINK_LIBS
sfml-graphics
sfml-window
sfml-system
sfml-audio
tcod
OpenGL::GL)
# On Windows, add any additional libs and include directories
if(WIN32)
# Windows-specific Python library name (no dots)
list(APPEND LINK_LIBS python314)
# Add the necessary Windows-specific libraries and include directories
# include_directories(path_to_additional_includes)
# link_directories(path_to_additional_libs)
# list(APPEND LINK_LIBS additional_windows_libs)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
if(MCRF_CROSS_WINDOWS)
# For cross-compilation, OpenGL is provided by MinGW
set(OPENGL_LIBRARIES opengl32)
else()
# Unix/Linux specific libraries
list(APPEND LINK_LIBS python3.14 m dl util pthread)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
find_package(OpenGL REQUIRED)
set(OPENGL_LIBRARIES OpenGL::GL)
endif()
# Add the directory where the linker should look for the libraries
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
link_directories(${CMAKE_SOURCE_DIR}/__lib)
# Create a list of libraries to link against
if(MCRF_CROSS_WINDOWS)
# MinGW cross-compilation: use full library names
set(LINK_LIBS
sfml-graphics
sfml-window
sfml-system
sfml-audio
libtcod
python314
${OPENGL_LIBRARIES})
# Add Windows system libraries needed by SFML and MinGW
list(APPEND LINK_LIBS
winmm # Windows multimedia (for audio)
gdi32 # Graphics Device Interface
ws2_32 # Winsock (networking, used by some deps)
ole32 # OLE support
oleaut32 # OLE automation
uuid # UUID library
comdlg32 # Common dialogs
imm32 # Input Method Manager
version # Version info
)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
# Link directories for cross-compiled Windows libs
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/lib)
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
elseif(WIN32)
# Native Windows build (MSVC)
set(LINK_LIBS
sfml-graphics
sfml-window
sfml-system
sfml-audio
tcod
python314
${OPENGL_LIBRARIES})
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
link_directories(${CMAKE_SOURCE_DIR}/__lib)
else()
# Unix/Linux build
set(LINK_LIBS
sfml-graphics
sfml-window
sfml-system
sfml-audio
tcod
python3.14
m dl util pthread
${OPENGL_LIBRARIES})
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
link_directories(${CMAKE_SOURCE_DIR}/__lib)
endif()
# Define the executable target before linking libraries
add_executable(mcrogueface ${SOURCES})
@ -73,11 +128,35 @@ add_executable(mcrogueface ${SOURCES})
# Define NO_SDL for libtcod-headless headers (excludes SDL-dependent code)
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
# On Windows, set subsystem to WINDOWS to hide console
if(WIN32)
set_target_properties(mcrogueface PROPERTIES
WIN32_EXECUTABLE TRUE
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")
# On Windows, define Py_ENABLE_SHARED for proper Python DLL imports
# Py_PYCONFIG_H prevents Include/pyconfig.h (Linux config) from being included
# (PC/pyconfig.h already defines HAVE_DECLSPEC_DLL and MS_WINDOWS)
if(WIN32 OR MCRF_CROSS_WINDOWS)
target_compile_definitions(mcrogueface PRIVATE Py_ENABLE_SHARED Py_PYCONFIG_H)
endif()
# On Windows, set subsystem to WINDOWS to hide console (release builds only)
# Use -DMCRF_WINDOWS_CONSOLE=ON for debug builds with console output
option(MCRF_WINDOWS_CONSOLE "Keep console window visible for debugging" OFF)
if(WIN32 AND NOT MCRF_CROSS_WINDOWS)
# MSVC-specific flags
if(NOT MCRF_WINDOWS_CONSOLE)
set_target_properties(mcrogueface PROPERTIES
WIN32_EXECUTABLE TRUE
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")
endif()
elseif(MCRF_CROSS_WINDOWS)
# MinGW cross-compilation
if(NOT MCRF_WINDOWS_CONSOLE)
# Release: use -mwindows to hide console
set_target_properties(mcrogueface PROPERTIES
WIN32_EXECUTABLE TRUE
LINK_FLAGS "-mwindows")
else()
# Debug: keep console for stdout/stderr output
message(STATUS "Windows console enabled for debugging")
endif()
endif()
# Now the linker will find the libraries in the specified directory
@ -99,20 +178,36 @@ add_custom_command(TARGET mcrogueface POST_BUILD
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
# On Windows, copy DLLs to executable directory
if(WIN32)
# Copy all DLL files from lib to the executable directory
if(MCRF_CROSS_WINDOWS)
# Cross-compilation: copy DLLs from __lib_windows
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib_windows/sfml/bin $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/bin $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/python314.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/python3.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140_1.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
/usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E echo "Copied Windows DLLs to executable directory")
# Copy Python standard library zip
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/python314.zip $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E echo "Copied Python stdlib")
elseif(WIN32)
# Native Windows build: copy DLLs from __lib
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
# Alternative: Copy specific DLLs if you want more control
# file(GLOB DLLS "${CMAKE_SOURCE_DIR}/__lib/*.dll")
# foreach(DLL ${DLLS})
# add_custom_command(TARGET mcrogueface POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E copy_if_different
# ${DLL} $<TARGET_FILE_DIR:mcrogueface>)
# endforeach()
endif()
# rpath for including shared libraries (Linux/Unix only)

View file

@ -0,0 +1,34 @@
# CMake toolchain file for cross-compiling to Windows using MinGW-w64
# Usage: cmake -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64-x86_64.cmake ..
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# Specify the cross-compiler (use posix variant for std::mutex support)
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++-posix)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
# Target environment location
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
# Add MinGW system include directories for Windows headers
include_directories(SYSTEM /usr/x86_64-w64-mingw32/include)
# Adjust search behavior
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# Static linking of libgcc and libstdc++ to avoid runtime dependency issues
# Enable auto-import for Python DLL data symbols
set(CMAKE_EXE_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
# Windows-specific defines
add_definitions(-DWIN32 -D_WIN32 -D_WINDOWS)
add_definitions(-DMINGW_HAS_SECURE_API)
# Disable console window for GUI applications (optional, can be overridden)
# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -mwindows")

View file

@ -1,12 +1,12 @@
#ifndef __PLATFORM
#define __PLATFORM
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 0
#include <Windows.h>
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
#include <windows.h>
std::wstring executable_path()
{
wchar_t buffer[MAX_PATH];
GetModuleFileName(NULL, buffer, MAX_PATH);
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
std::wstring exec_path = buffer;
size_t path_index = exec_path.find_last_of(L"\\/");
return exec_path.substr(0, path_index);
@ -15,7 +15,7 @@ std::wstring executable_path()
std::wstring executable_filename()
{
wchar_t buffer[MAX_PATH];
GetModuleFileName(NULL, buffer, MAX_PATH);
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
std::wstring exec_path = buffer;
return exec_path;
}

View file

@ -767,7 +767,7 @@ void AnimationManager::addAnimation(std::shared_ptr<Animation> animation,
}
return; // Don't add to active animations yet
case AnimationConflictMode::ERROR:
case AnimationConflictMode::RAISE_ERROR:
// Raise Python exception
PyGILState_STATE gstate = PyGILState_Ensure();
PyErr_Format(PyExc_RuntimeError,

View file

@ -16,9 +16,10 @@ class UIEntity;
* ConflictMode - How to handle multiple animations on the same property (#120)
*/
enum class AnimationConflictMode {
REPLACE, // Stop/complete existing animation, start new one (default)
QUEUE, // Queue new animation to run after existing one completes
ERROR // Raise an error if property is already being animated
REPLACE, // Stop/complete existing animation, start new one (default)
QUEUE, // Queue new animation to run after existing one completes
RAISE_ERROR // Raise an error if property is already being animated
// Note: Can't use ERROR as it conflicts with Windows macro
};
// Forward declare namespace

View file

@ -65,11 +65,21 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
!config.python_mode;
if (should_load_game) {
std::cerr << "[DEBUG] GameEngine: loading default game.py" << std::endl;
std::cerr.flush();
if (!Py_IsInitialized()) {
std::cerr << "[DEBUG] GameEngine: initializing Python API" << std::endl;
std::cerr.flush();
McRFPy_API::api_init();
}
std::cerr << "[DEBUG] GameEngine: importing mcrfpy" << std::endl;
std::cerr.flush();
McRFPy_API::executePyString("import mcrfpy");
std::cerr << "[DEBUG] GameEngine: executing scripts/game.py" << std::endl;
std::cerr.flush();
McRFPy_API::executeScript("scripts/game.py");
std::cerr << "[DEBUG] GameEngine: game.py execution complete" << std::endl;
std::cerr.flush();
}
// Note: --exec scripts are NOT executed here.

View file

@ -28,6 +28,7 @@
#include "PyScene.h"
#include "PythonObjectCache.h"
#include <filesystem>
#include <fstream>
#include <cstring>
#include <libtcod.h>
@ -806,13 +807,26 @@ void McRFPy_API::executeScript(std::string filename)
}
}
FILE* PScriptFile = fopen(script_path.string().c_str(), "r");
if(PScriptFile) {
PyRun_SimpleFile(PScriptFile, script_path.string().c_str());
fclose(PScriptFile);
} else {
// Use std::ifstream + PyRun_SimpleString instead of PyRun_SimpleFile
// PyRun_SimpleFile has compatibility issues with MinGW-compiled code
std::ifstream file(script_path);
if (!file.is_open()) {
std::cout << "Failed to open script: " << script_path.string() << std::endl;
return;
}
std::string script_content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
// Set __file__ before execution
PyObject* main_module = PyImport_AddModule("__main__");
PyObject* main_dict = PyModule_GetDict(main_module);
PyObject* py_filename = PyUnicode_FromString(script_path.string().c_str());
PyDict_SetItemString(main_dict, "__file__", py_filename);
Py_DECREF(py_filename);
PyRun_SimpleString(script_content.c_str());
}
void McRFPy_API::api_shutdown()

View file

@ -186,7 +186,7 @@ static bool parseConflictMode(const char* mode_str, AnimationConflictMode& mode)
} else if (strcmp(mode_str, "queue") == 0) {
mode = AnimationConflictMode::QUEUE;
} else if (strcmp(mode_str, "error") == 0) {
mode = AnimationConflictMode::ERROR;
mode = AnimationConflictMode::RAISE_ERROR;
} else {
PyErr_Format(PyExc_ValueError,
"Invalid conflict_mode '%s'. Must be 'replace', 'queue', or 'error'.", mode_str);

View file

@ -172,7 +172,7 @@ PyObject* PyColor::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
PyObject* PyColor::get_member(PyObject* obj, void* closure)
{
PyColorObject* self = (PyColorObject*)obj;
long member = (long)closure;
intptr_t member = (intptr_t)closure;
switch (member) {
case 0: // r
@ -192,7 +192,7 @@ PyObject* PyColor::get_member(PyObject* obj, void* closure)
int PyColor::set_member(PyObject* obj, PyObject* value, void* closure)
{
PyColorObject* self = (PyColorObject*)obj;
long member = (long)closure;
intptr_t member = (intptr_t)closure;
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "Color values must be integers");

View file

@ -227,7 +227,7 @@ PyObject* PyVector::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
PyObject* PyVector::get_member(PyObject* obj, void* closure)
{
PyVectorObject* self = (PyVectorObject*)obj;
if (reinterpret_cast<long>(closure) == 0) {
if (reinterpret_cast<intptr_t>(closure) == 0) {
// x
return PyFloat_FromDouble(self->data.x);
} else {
@ -250,7 +250,7 @@ int PyVector::set_member(PyObject* obj, PyObject* value, void* closure)
return -1;
}
if (reinterpret_cast<long>(closure) == 0) {
if (reinterpret_cast<intptr_t>(closure) == 0) {
// x
self->data.x = val;
} else {

View file

@ -96,7 +96,7 @@ void UICaption::onPositionChanged()
PyObject* UICaption::get_float_member(PyUICaptionObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr == 0)
return PyFloat_FromDouble(self->data->text.getPosition().x);
else if (member_ptr == 1)
@ -115,7 +115,7 @@ PyObject* UICaption::get_float_member(PyUICaptionObject* self, void* closure)
int UICaption::set_float_member(PyUICaptionObject* self, PyObject* value, void* closure)
{
float val;
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (PyFloat_Check(value))
{
val = PyFloat_AsDouble(value);
@ -156,7 +156,7 @@ PyObject* UICaption::get_color_member(PyUICaptionObject* self, void* closure)
// TODO: migrate this code to a switch statement - validate closure & return values in one tighter, more extensible structure
// validate closure (should be impossible to be wrong, but it's thorough)
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr != 0 && member_ptr != 1)
{
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
@ -181,7 +181,7 @@ PyObject* UICaption::get_color_member(PyUICaptionObject* self, void* closure)
int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
//TODO: this logic of (PyColor instance OR tuple -> sf::color) should be encapsulated for reuse
int r, g, b, a;
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color") /*(PyObject*)&mcrfpydef::PyColorType)*/))

View file

@ -170,7 +170,7 @@ void UIDrawable::render()
}
PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure)); // trust me bro, it's an Enum
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure)); // trust me bro, it's an Enum
PyObject* ptr;
switch (objtype)
@ -228,7 +228,7 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
}
int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure)); // trust me bro, it's an Enum
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure)); // trust me bro, it's an Enum
UIDrawable* target;
switch (objtype)
{
@ -305,7 +305,7 @@ void UIDrawable::on_move_unregister()
}
PyObject* UIDrawable::get_int(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -339,7 +339,7 @@ PyObject* UIDrawable::get_int(PyObject* self, void* closure) {
}
int UIDrawable::set_int(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -405,7 +405,7 @@ void UIDrawable::notifyZIndexChanged() {
}
PyObject* UIDrawable::get_name(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -439,7 +439,7 @@ PyObject* UIDrawable::get_name(PyObject* self, void* closure) {
}
int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -639,7 +639,7 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure)
}
PyObject* UIDrawable::get_pos(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -686,7 +686,7 @@ PyObject* UIDrawable::get_pos(PyObject* self, void* closure) {
}
int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -892,7 +892,7 @@ void UIDrawable::markDirty() {
// Python API - get parent drawable
PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -1003,7 +1003,7 @@ PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
// Python API - set parent drawable (or None to remove from parent)
int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
std::shared_ptr<UIDrawable> drawable = nullptr;
// Get the shared_ptr for self
@ -1125,7 +1125,7 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
// Python API - get global position (read-only)
PyObject* UIDrawable::get_global_pos(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -1175,7 +1175,7 @@ PyObject* UIDrawable::get_global_pos(PyObject* self, void* closure) {
// #138, #188 - Python API for bounds property - returns (pos, size) as pair of Vectors
PyObject* UIDrawable::get_bounds_py(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -1236,7 +1236,7 @@ PyObject* UIDrawable::get_bounds_py(PyObject* self, void* closure) {
// #138, #188 - Python API for global_bounds property - returns (pos, size) as pair of Vectors
PyObject* UIDrawable::get_global_bounds_py(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -1297,7 +1297,7 @@ PyObject* UIDrawable::get_global_bounds_py(PyObject* self, void* closure) {
// #140 - Python API for on_enter property
PyObject* UIDrawable::get_on_enter(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
PyObject* ptr = nullptr;
switch (objtype) {
@ -1340,7 +1340,7 @@ PyObject* UIDrawable::get_on_enter(PyObject* self, void* closure) {
}
int UIDrawable::set_on_enter(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* target = nullptr;
switch (objtype) {
@ -1380,7 +1380,7 @@ int UIDrawable::set_on_enter(PyObject* self, PyObject* value, void* closure) {
// #140 - Python API for on_exit property
PyObject* UIDrawable::get_on_exit(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
PyObject* ptr = nullptr;
switch (objtype) {
@ -1423,7 +1423,7 @@ PyObject* UIDrawable::get_on_exit(PyObject* self, void* closure) {
}
int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* target = nullptr;
switch (objtype) {
@ -1463,7 +1463,7 @@ int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) {
// #140 - Python API for hovered property (read-only)
PyObject* UIDrawable::get_hovered(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
@ -1498,7 +1498,7 @@ PyObject* UIDrawable::get_hovered(PyObject* self, void* closure) {
// #141 - Python API for on_move property
PyObject* UIDrawable::get_on_move(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
PyObject* ptr = nullptr;
switch (objtype) {
@ -1541,7 +1541,7 @@ PyObject* UIDrawable::get_on_move(PyObject* self, void* closure) {
}
int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* target = nullptr;
switch (objtype) {
@ -1689,7 +1689,7 @@ PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> self, PyObject* ar
} else if (strcmp(conflict_mode_str, "queue") == 0) {
conflict_mode = AnimationConflictMode::QUEUE;
} else if (strcmp(conflict_mode_str, "error") == 0) {
conflict_mode = AnimationConflictMode::ERROR;
conflict_mode = AnimationConflictMode::RAISE_ERROR;
} else {
PyErr_Format(PyExc_ValueError,
"Invalid conflict_mode '%s'. Must be 'replace', 'queue', or 'error'.", conflict_mode_str);

View file

@ -358,7 +358,7 @@ PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>&
}
PyObject* UIEntity::get_position(PyUIEntityObject* self, void* closure) {
if (reinterpret_cast<long>(closure) == 0) {
if (reinterpret_cast<intptr_t>(closure) == 0) {
return sfVector2f_to_PyObject(self->data->position);
} else {
// Return integer-cast position for grid coordinates
@ -373,7 +373,7 @@ int UIEntity::set_position(PyUIEntityObject* self, PyObject* value, void* closur
float old_x = self->data->position.x;
float old_y = self->data->position.y;
if (reinterpret_cast<long>(closure) == 0) {
if (reinterpret_cast<intptr_t>(closure) == 0) {
sf::Vector2f vec = PyObject_to_sfVector2f(value);
if (PyErr_Occurred()) {
return -1; // Error already set by PyObject_to_sfVector2f
@ -418,7 +418,7 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl
PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr == 0) // x
return PyFloat_FromDouble(self->data->position.x);
else if (member_ptr == 1) // y
@ -433,7 +433,7 @@ PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure)
int UIEntity::set_float_member(PyUIEntityObject* self, PyObject* value, void* closure)
{
float val;
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (PyFloat_Check(value))
{
val = PyFloat_AsDouble(value);
@ -540,7 +540,7 @@ PyObject* UIEntity::get_pixel_member(PyUIEntityObject* self, void* closure) {
float cell_width, cell_height;
get_cell_dimensions(self->data.get(), cell_width, cell_height);
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr == 0) // x
return PyFloat_FromDouble(self->data->position.x * cell_width);
else // y
@ -570,7 +570,7 @@ int UIEntity::set_pixel_member(PyUIEntityObject* self, PyObject* value, void* cl
float old_x = self->data->position.x;
float old_y = self->data->position.y;
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr == 0) // x
self->data->position.x = val / cell_width;
else // y
@ -584,7 +584,7 @@ int UIEntity::set_pixel_member(PyUIEntityObject* self, PyObject* value, void* cl
// #176 - Integer grid position (grid_x, grid_y)
PyObject* UIEntity::get_grid_int_member(PyUIEntityObject* self, void* closure) {
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr == 0) // grid_x
return PyLong_FromLong(static_cast<int>(self->data->position.x));
else // grid_y
@ -606,7 +606,7 @@ int UIEntity::set_grid_int_member(PyUIEntityObject* self, PyObject* value, void*
float old_x = self->data->position.x;
float old_y = self->data->position.y;
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr == 0) // grid_x
self->data->position.x = static_cast<float>(val);
else // grid_y
@ -1172,7 +1172,7 @@ PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kw
} else if (strcmp(conflict_mode_str, "queue") == 0) {
conflict_mode = AnimationConflictMode::QUEUE;
} else if (strcmp(conflict_mode_str, "error") == 0) {
conflict_mode = AnimationConflictMode::ERROR;
conflict_mode = AnimationConflictMode::RAISE_ERROR;
} else {
PyErr_Format(PyExc_ValueError,
"Invalid conflict_mode '%s'. Must be 'replace', 'queue', or 'error'.", conflict_mode_str);

View file

@ -187,7 +187,7 @@ PyObject* UIFrame::get_children(PyUIFrameObject* self, void* closure)
PyObject* UIFrame::get_float_member(PyUIFrameObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr == 0)
return PyFloat_FromDouble(self->data->box.getPosition().x);
else if (member_ptr == 1)
@ -208,7 +208,7 @@ PyObject* UIFrame::get_float_member(PyUIFrameObject* self, void* closure)
int UIFrame::set_float_member(PyUIFrameObject* self, PyObject* value, void* closure)
{
float val;
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (PyFloat_Check(value))
{
val = PyFloat_AsDouble(value);
@ -258,7 +258,7 @@ int UIFrame::set_float_member(PyUIFrameObject* self, PyObject* value, void* clos
PyObject* UIFrame::get_color_member(PyUIFrameObject* self, void* closure)
{
// validate closure (should be impossible to be wrong, but it's thorough)
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr != 0 && member_ptr != 1)
{
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
@ -293,7 +293,7 @@ PyObject* UIFrame::get_color_member(PyUIFrameObject* self, void* closure)
int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* closure)
{
//TODO: this logic of (PyColor instance OR tuple -> sf::color) should be encapsulated for reuse
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
int r, g, b, a;
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color")))
{

View file

@ -1076,7 +1076,7 @@ int UIGrid::set_center(PyUIGridObject* self, PyObject* value, void* closure) {
PyObject* UIGrid::get_float_member(PyUIGridObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr == 0) // x
return PyFloat_FromDouble(self->data->box.getPosition().x);
else if (member_ptr == 1) // y
@ -1101,7 +1101,7 @@ PyObject* UIGrid::get_float_member(PyUIGridObject* self, void* closure)
int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closure)
{
float val;
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (PyFloat_Check(value))
{
val = PyFloat_AsDouble(value);

View file

@ -57,7 +57,7 @@ sf::Color PyObject_to_sfColor(PyObject* obj) {
// #150 - Removed get_color/set_color - now handled by layers
PyObject* UIGridPoint::get_bool_member(PyUIGridPointObject* self, void* closure) {
if (reinterpret_cast<long>(closure) == 0) { // walkable
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
return PyBool_FromLong(self->data->walkable);
} else { // transparent
return PyBool_FromLong(self->data->transparent);
@ -66,13 +66,13 @@ PyObject* UIGridPoint::get_bool_member(PyUIGridPointObject* self, void* closure)
int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure) {
if (value == Py_True) {
if (reinterpret_cast<long>(closure) == 0) { // walkable
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
self->data->walkable = true;
} else { // transparent
self->data->transparent = true;
}
} else if (value == Py_False) {
if (reinterpret_cast<long>(closure) == 0) { // walkable
if (reinterpret_cast<intptr_t>(closure) == 0) { // walkable
self->data->walkable = false;
} else { // transparent
self->data->transparent = false;
@ -162,7 +162,7 @@ PyObject* UIGridPoint::repr(PyUIGridPointObject* self) {
}
PyObject* UIGridPointState::get_bool_member(PyUIGridPointStateObject* self, void* closure) {
if (reinterpret_cast<long>(closure) == 0) { // visible
if (reinterpret_cast<intptr_t>(closure) == 0) { // visible
return PyBool_FromLong(self->data->visible);
} else { // discovered
return PyBool_FromLong(self->data->discovered);
@ -180,7 +180,7 @@ int UIGridPointState::set_bool_member(PyUIGridPointStateObject* self, PyObject*
return -1; // PyObject_IsTrue returns -1 on error
}
if (reinterpret_cast<long>(closure) == 0) { // visible
if (reinterpret_cast<intptr_t>(closure) == 0) { // visible
self->data->visible = truthValue;
} else { // discovered
self->data->discovered = truthValue;

View file

@ -182,7 +182,7 @@ void UISprite::onPositionChanged()
PyObject* UISprite::get_float_member(PyUISpriteObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (member_ptr == 0)
return PyFloat_FromDouble(self->data->getPosition().x);
else if (member_ptr == 1)
@ -203,7 +203,7 @@ PyObject* UISprite::get_float_member(PyUISpriteObject* self, void* closure)
int UISprite::set_float_member(PyUISpriteObject* self, PyObject* value, void* closure)
{
float val;
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (PyFloat_Check(value))
{
val = PyFloat_AsDouble(value);
@ -232,7 +232,7 @@ int UISprite::set_float_member(PyUISpriteObject* self, PyObject* value, void* cl
PyObject* UISprite::get_int_member(PyUISpriteObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (true) {}
else
{
@ -246,7 +246,7 @@ PyObject* UISprite::get_int_member(PyUISpriteObject* self, void* closure)
int UISprite::set_int_member(PyUISpriteObject* self, PyObject* value, void* closure)
{
int val;
auto member_ptr = reinterpret_cast<long>(closure);
auto member_ptr = reinterpret_cast<intptr_t>(closure);
if (PyLong_Check(value))
{
val = PyLong_AsLong(value);

View file

@ -7,6 +7,7 @@
#include "PyTexture.h"
#include <Python.h>
#include <iostream>
#include <fstream>
#include <filesystem>
// Forward declarations
@ -18,7 +19,6 @@ int main(int argc, char* argv[])
McRogueFaceConfig config;
CommandLineParser parser(argc, argv);
// Parse arguments
auto parse_result = parser.parse(config);
if (parse_result.should_exit) {
return parse_result.exit_code;
@ -59,7 +59,7 @@ int run_python_interpreter(const McRogueFaceConfig& config)
// Initialize Python with configuration (argv is constructed from config)
McRFPy_API::init_python_with_config(config);
// Import mcrfpy module and store reference
McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
if (!McRFPy_API::mcrf_module) {
@ -74,7 +74,7 @@ int run_python_interpreter(const McRogueFaceConfig& config)
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
}
// Handle different Python modes
if (!config.python_command.empty()) {
// Execute command from -c
@ -82,15 +82,15 @@ int run_python_interpreter(const McRogueFaceConfig& config)
// Use PyRun_String to catch SystemExit
PyObject* main_module = PyImport_AddModule("__main__");
PyObject* main_dict = PyModule_GetDict(main_module);
PyObject* result_obj = PyRun_String(config.python_command.c_str(),
PyObject* result_obj = PyRun_String(config.python_command.c_str(),
Py_file_input, main_dict, main_dict);
if (result_obj == NULL) {
// Check if it's SystemExit
if (PyErr_Occurred()) {
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
// If it's SystemExit and we're in interactive mode, clear it
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
PyErr_Clear();
@ -99,7 +99,7 @@ int run_python_interpreter(const McRogueFaceConfig& config)
PyErr_Restore(type, value, traceback);
PyErr_Print();
}
Py_XDECREF(type);
Py_XDECREF(value);
Py_XDECREF(traceback);
@ -128,16 +128,46 @@ int run_python_interpreter(const McRogueFaceConfig& config)
}
else if (!config.script_path.empty()) {
// Execute script file (sys.argv already set at init time)
FILE* fp = fopen(config.script_path.string().c_str(), "r");
if (!fp) {
// Note: Using PyRun_SimpleString instead of PyRun_SimpleFile for better
// cross-platform compatibility (PyRun_SimpleFile has issues with MinGW/Wine)
// Read file contents
std::ifstream file(config.script_path);
if (!file.is_open()) {
std::cerr << "mcrogueface: can't open file '" << config.script_path << "': ";
std::cerr << "[Errno " << errno << "] " << strerror(errno) << std::endl;
delete engine;
return 1;
}
std::string script_content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
// Set __file__ before execution
PyObject* main_module = PyImport_AddModule("__main__");
PyObject* main_dict = PyModule_GetDict(main_module);
PyObject* py_filename = PyUnicode_FromString(config.script_path.string().c_str());
PyDict_SetItemString(main_dict, "__file__", py_filename);
Py_DECREF(py_filename);
int result = PyRun_SimpleString(script_content.c_str());
// Flush Python stdout/stderr to ensure output appears before engine->run() blocks
PyObject* sys_module = PyImport_ImportModule("sys");
if (sys_module) {
PyObject* stdout_obj = PyObject_GetAttrString(sys_module, "stdout");
if (stdout_obj) {
PyObject_CallMethod(stdout_obj, "flush", NULL);
Py_DECREF(stdout_obj);
}
PyObject* stderr_obj = PyObject_GetAttrString(sys_module, "stderr");
if (stderr_obj) {
PyObject_CallMethod(stderr_obj, "flush", NULL);
Py_DECREF(stderr_obj);
}
Py_DECREF(sys_module);
}
int result = PyRun_SimpleFile(fp, config.script_path.string().c_str());
fclose(fp);
if (config.interactive_mode) {
// Even if script had SystemExit, continue to interactive mode
if (result != 0) {
@ -145,7 +175,7 @@ int run_python_interpreter(const McRogueFaceConfig& config)
if (PyErr_Occurred()) {
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
PyErr_Clear();
result = 0; // Don't exit with error
@ -153,7 +183,7 @@ int run_python_interpreter(const McRogueFaceConfig& config)
PyErr_Restore(type, value, traceback);
PyErr_Print();
}
Py_XDECREF(type);
Py_XDECREF(value);
Py_XDECREF(traceback);
@ -162,7 +192,7 @@ int run_python_interpreter(const McRogueFaceConfig& config)
// Run interactive mode after script
PyRun_InteractiveLoop(stdin, "<stdin>");
}
// Run the game engine after script execution
engine->run();
@ -197,7 +227,7 @@ int run_python_interpreter(const McRogueFaceConfig& config)
}
return 0;
}
delete engine;
return 0;
}

View file

@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""Test batch of API changes for issues #177, #179, #181, #182, #184, #185, #188, #189, #190"""
import sys
import mcrfpy
def test_issue_177_gridpoint_grid_pos():
"""Test GridPoint.grid_pos property returns tuple"""
print("Testing #177: GridPoint.grid_pos property...")
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture, pos=(0, 0), size=(160, 160))
# Get a grid point
point = grid.at(3, 5)
# Test grid_pos property exists and returns tuple
grid_pos = point.grid_pos
assert isinstance(grid_pos, tuple), f"grid_pos should be tuple, got {type(grid_pos)}"
assert len(grid_pos) == 2, f"grid_pos should have 2 elements, got {len(grid_pos)}"
assert grid_pos == (3, 5), f"grid_pos should be (3, 5), got {grid_pos}"
# Test another position
point2 = grid.at(7, 2)
assert point2.grid_pos == (7, 2), f"grid_pos should be (7, 2), got {point2.grid_pos}"
print(" PASS: GridPoint.grid_pos works correctly")
return True
def test_issue_179_181_grid_vectors():
"""Test Grid properties return Vectors instead of tuples"""
print("Testing #179, #181: Grid Vector returns and grid_w/grid_h rename...")
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(15, 20), texture=texture, pos=(50, 100), size=(240, 320))
# Test center returns Vector
center = grid.center
assert hasattr(center, 'x') and hasattr(center, 'y'), f"center should be Vector, got {type(center)}"
# Test grid_size returns Vector
grid_size = grid.grid_size
assert hasattr(grid_size, 'x') and hasattr(grid_size, 'y'), f"grid_size should be Vector, got {type(grid_size)}"
assert grid_size.x == 15 and grid_size.y == 20, f"grid_size should be (15, 20), got ({grid_size.x}, {grid_size.y})"
# Test pos returns Vector
pos = grid.pos
assert hasattr(pos, 'x') and hasattr(pos, 'y'), f"pos should be Vector, got {type(pos)}"
print(" PASS: Grid properties return Vectors correctly")
return True
def test_issue_182_caption_size():
"""Test Caption read-only size, w, h properties"""
print("Testing #182: Caption read-only size/w/h properties...")
font = mcrfpy.Font("assets/JetbrainsMono.ttf")
caption = mcrfpy.Caption(text="Test Caption", pos=(100, 100), font=font)
# Test size property
size = caption.size
assert hasattr(size, 'x') and hasattr(size, 'y'), f"size should be Vector, got {type(size)}"
assert size.x > 0, f"width should be positive, got {size.x}"
assert size.y > 0, f"height should be positive, got {size.y}"
# Test w property
w = caption.w
assert isinstance(w, float), f"w should be float, got {type(w)}"
assert w > 0, f"w should be positive, got {w}"
# Test h property
h = caption.h
assert isinstance(h, float), f"h should be float, got {type(h)}"
assert h > 0, f"h should be positive, got {h}"
# Verify w and h match size
assert abs(w - size.x) < 0.001, f"w ({w}) should match size.x ({size.x})"
assert abs(h - size.y) < 0.001, f"h ({h}) should match size.y ({size.y})"
# Verify read-only
try:
caption.size = mcrfpy.Vector(100, 100)
print(" FAIL: size should be read-only")
return False
except AttributeError:
pass # Expected
try:
caption.w = 100
print(" FAIL: w should be read-only")
return False
except AttributeError:
pass # Expected
try:
caption.h = 100
print(" FAIL: h should be read-only")
return False
except AttributeError:
pass # Expected
print(" PASS: Caption size/w/h properties work correctly")
return True
def test_issue_184_189_module_namespace():
"""Test window singleton and hidden internal types"""
print("Testing #184, #189: window singleton + hide classes...")
# Test window singleton exists
assert hasattr(mcrfpy, 'window'), "mcrfpy.window should exist"
window = mcrfpy.window
assert window is not None, "window should not be None"
# Verify window properties
assert hasattr(window, 'resolution'), "window should have resolution property"
# Test that internal types are hidden from module namespace
assert not hasattr(mcrfpy, 'UICollectionIter'), "UICollectionIter should be hidden from module namespace"
assert not hasattr(mcrfpy, 'UIEntityCollectionIter'), "UIEntityCollectionIter should be hidden from module namespace"
# But iteration should still work - test UICollection iteration
scene = mcrfpy.Scene("test_scene")
ui = scene.children
ui.append(mcrfpy.Frame(pos=(0,0), size=(50,50)))
ui.append(mcrfpy.Caption(text="hi", pos=(0,0)))
count = 0
for item in ui:
count += 1
assert count == 2, f"Should iterate over 2 items, got {count}"
print(" PASS: window singleton and hidden types work correctly")
return True
def test_issue_185_188_bounds_vectors():
"""Test bounds returns Vector pair, get_bounds() removed"""
print("Testing #185, #188: Remove get_bounds(), bounds as Vector pair...")
frame = mcrfpy.Frame(pos=(50, 100), size=(200, 150))
# Test bounds returns tuple of Vectors
bounds = frame.bounds
assert isinstance(bounds, tuple), f"bounds should be tuple, got {type(bounds)}"
assert len(bounds) == 2, f"bounds should have 2 elements, got {len(bounds)}"
pos, size = bounds
assert hasattr(pos, 'x') and hasattr(pos, 'y'), f"pos should be Vector, got {type(pos)}"
assert hasattr(size, 'x') and hasattr(size, 'y'), f"size should be Vector, got {type(size)}"
assert pos.x == 50 and pos.y == 100, f"pos should be (50, 100), got ({pos.x}, {pos.y})"
assert size.x == 200 and size.y == 150, f"size should be (200, 150), got ({size.x}, {size.y})"
# Test global_bounds also returns Vector pair
global_bounds = frame.global_bounds
assert isinstance(global_bounds, tuple), f"global_bounds should be tuple, got {type(global_bounds)}"
assert len(global_bounds) == 2, f"global_bounds should have 2 elements"
# Test get_bounds() method is removed (#185)
assert not hasattr(frame, 'get_bounds'), "get_bounds() method should be removed"
print(" PASS: bounds returns Vector pairs, get_bounds() removed")
return True
def test_issue_190_layer_documentation():
"""Test that layer types have documentation"""
print("Testing #190: TileLayer/ColorLayer documentation...")
# Verify layer types exist and have docstrings
assert hasattr(mcrfpy, 'TileLayer'), "TileLayer should exist"
assert hasattr(mcrfpy, 'ColorLayer'), "ColorLayer should exist"
# Check that docstrings exist and contain useful info
tile_doc = mcrfpy.TileLayer.__doc__
color_doc = mcrfpy.ColorLayer.__doc__
assert tile_doc is not None and len(tile_doc) > 50, f"TileLayer should have substantial docstring, got: {tile_doc}"
assert color_doc is not None and len(color_doc) > 50, f"ColorLayer should have substantial docstring, got: {color_doc}"
# Check for key documentation elements
assert "layer" in tile_doc.lower() or "tile" in tile_doc.lower(), "TileLayer doc should mention layer or tile"
assert "layer" in color_doc.lower() or "color" in color_doc.lower(), "ColorLayer doc should mention layer or color"
print(" PASS: Layer documentation exists")
return True
def run_all_tests():
"""Run all tests and report results"""
print("=" * 60)
print("API Changes Batch Test - Issues #177, #179, #181, #182, #184, #185, #188, #189, #190")
print("=" * 60)
tests = [
("Issue #177 GridPoint.grid_pos", test_issue_177_gridpoint_grid_pos),
("Issue #179, #181 Grid Vectors", test_issue_179_181_grid_vectors),
("Issue #182 Caption size/w/h", test_issue_182_caption_size),
("Issue #184, #189 Module namespace", test_issue_184_189_module_namespace),
("Issue #185, #188 Bounds Vectors", test_issue_185_188_bounds_vectors),
("Issue #190 Layer documentation", test_issue_190_layer_documentation),
]
passed = 0
failed = 0
for name, test_func in tests:
try:
if test_func():
passed += 1
else:
failed += 1
print(f" FAILED: {name}")
except Exception as e:
failed += 1
print(f" ERROR in {name}: {e}")
print("=" * 60)
print(f"Results: {passed} passed, {failed} failed")
print("=" * 60)
if failed == 0:
print("ALL TESTS PASSED")
sys.exit(0)
else:
print("SOME TESTS FAILED")
sys.exit(1)
# Run tests
run_all_tests()

View file

@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Test for issue #176: Entity position naming consistency.
Tests the new Entity position properties:
- pos, x, y: pixel coordinates (requires grid attachment)
- grid_pos, grid_x, grid_y: integer tile coordinates
- draw_pos: fractional tile coordinates for animation
"""
import mcrfpy
import sys
def test_entity_positions():
"""Test Entity position properties with grid attachment."""
errors = []
# Create a texture with 16x16 sprites (standard tile size)
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create a grid (10x10 tiles, 16x16 pixels each)
grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture, pos=(0, 0), size=(160, 160))
# Create entity at tile position (3, 5)
entity = mcrfpy.Entity(grid_pos=(3, 5), texture=texture, grid=grid)
# Test 1: grid_pos should return integer tile coordinates
gpos = entity.grid_pos
if gpos.x != 3 or gpos.y != 5:
errors.append(f"grid_pos: expected (3, 5), got ({gpos.x}, {gpos.y})")
# Test 2: grid_x and grid_y should return integers
if entity.grid_x != 3:
errors.append(f"grid_x: expected 3, got {entity.grid_x}")
if entity.grid_y != 5:
errors.append(f"grid_y: expected 5, got {entity.grid_y}")
# Test 3: draw_pos should return float tile coordinates
dpos = entity.draw_pos
if abs(dpos.x - 3.0) > 0.001 or abs(dpos.y - 5.0) > 0.001:
errors.append(f"draw_pos: expected (3.0, 5.0), got ({dpos.x}, {dpos.y})")
# Test 4: pos should return pixel coordinates (tile * tile_size)
# With 16x16 tiles: (3, 5) tiles = (48, 80) pixels
ppos = entity.pos
if abs(ppos.x - 48.0) > 0.001 or abs(ppos.y - 80.0) > 0.001:
errors.append(f"pos: expected (48.0, 80.0), got ({ppos.x}, {ppos.y})")
# Test 5: x and y should return pixel coordinates
if abs(entity.x - 48.0) > 0.001:
errors.append(f"x: expected 48.0, got {entity.x}")
if abs(entity.y - 80.0) > 0.001:
errors.append(f"y: expected 80.0, got {entity.y}")
# Test 6: Setting grid_x/grid_y should update position
entity.grid_x = 7
entity.grid_y = 2
if entity.grid_x != 7 or entity.grid_y != 2:
errors.append(f"After setting grid_x/y: expected (7, 2), got ({entity.grid_x}, {entity.grid_y})")
# Pixel should update too: (7, 2) * 16 = (112, 32)
if abs(entity.x - 112.0) > 0.001 or abs(entity.y - 32.0) > 0.001:
errors.append(f"After grid_x/y set, pixel pos: expected (112, 32), got ({entity.x}, {entity.y})")
# Test 7: Setting pos (pixels) should update grid position
entity.pos = mcrfpy.Vector(64, 96) # (64, 96) / 16 = (4, 6) tiles
if abs(entity.draw_pos.x - 4.0) > 0.001 or abs(entity.draw_pos.y - 6.0) > 0.001:
errors.append(f"After setting pos, draw_pos: expected (4, 6), got ({entity.draw_pos.x}, {entity.draw_pos.y})")
if entity.grid_x != 4 or entity.grid_y != 6:
errors.append(f"After setting pos, grid_x/y: expected (4, 6), got ({entity.grid_x}, {entity.grid_y})")
# Test 8: repr should show grid_x/grid_y
repr_str = repr(entity)
if "grid_x=" not in repr_str or "grid_y=" not in repr_str:
errors.append(f"repr should contain grid_x/grid_y: {repr_str}")
return errors
def test_entity_without_grid():
"""Test that pixel positions require grid attachment."""
errors = []
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
entity = mcrfpy.Entity(grid_pos=(3, 5), texture=texture) # No grid
# grid_pos should work without grid
if entity.grid_x != 3 or entity.grid_y != 5:
errors.append(f"grid_x/y without grid: expected (3, 5), got ({entity.grid_x}, {entity.grid_y})")
# pos should raise RuntimeError without grid
try:
_ = entity.pos
errors.append("entity.pos should raise RuntimeError without grid")
except RuntimeError as e:
if "not attached to a Grid" not in str(e):
errors.append(f"Wrong error message for pos: {e}")
# x should raise RuntimeError without grid
try:
_ = entity.x
errors.append("entity.x should raise RuntimeError without grid")
except RuntimeError as e:
if "not attached to a Grid" not in str(e):
errors.append(f"Wrong error message for x: {e}")
# Setting pos should raise RuntimeError without grid
try:
entity.pos = mcrfpy.Vector(100, 100)
errors.append("setting entity.pos should raise RuntimeError without grid")
except RuntimeError as e:
if "not attached to a Grid" not in str(e):
errors.append(f"Wrong error message for setting pos: {e}")
return errors
def test_animation_properties():
"""Test that animation properties work correctly."""
errors = []
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture, pos=(0, 0), size=(160, 160))
entity = mcrfpy.Entity(grid_pos=(0, 0), texture=texture, grid=grid)
# Test draw_x/draw_y animation properties exist
try:
# hasProperty should accept draw_x and draw_y
# We can't call hasProperty directly, but we can try to animate
# and check if it raises ValueError for invalid property
pass # Animation tested implicitly through animate() error handling
except Exception as e:
errors.append(f"Animation property test failed: {e}")
return errors
def main():
print("Testing issue #176: Entity position naming consistency")
print("=" * 60)
all_errors = []
# Test 1: Entity with grid
print("\n1. Testing entity positions with grid attachment...")
errors = test_entity_positions()
if errors:
for e in errors:
print(f" FAIL: {e}")
all_errors.extend(errors)
else:
print(" PASS")
# Test 2: Entity without grid
print("\n2. Testing entity positions without grid...")
errors = test_entity_without_grid()
if errors:
for e in errors:
print(f" FAIL: {e}")
all_errors.extend(errors)
else:
print(" PASS")
# Test 3: Animation properties
print("\n3. Testing animation properties...")
errors = test_animation_properties()
if errors:
for e in errors:
print(f" FAIL: {e}")
all_errors.extend(errors)
else:
print(" PASS")
print("\n" + "=" * 60)
if all_errors:
print(f"FAILED: {len(all_errors)} error(s)")
sys.exit(1)
else:
print("All tests passed!")
sys.exit(0)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Test for issue #177: GridPoint.grid_pos property
Verifies that GridPoint objects have a grid_pos property that returns
the (grid_x, grid_y) coordinates as a tuple.
"""
import mcrfpy
import sys
print("Starting test...")
# Create a simple grid without texture (should work in headless mode)
grid = mcrfpy.Grid(grid_x=10, grid_y=8)
print(f"Created grid: {grid}")
# Test various grid positions
test_cases = [
(0, 0),
(5, 3),
(9, 7),
(0, 7),
(9, 0),
]
all_passed = True
for x, y in test_cases:
point = grid.at(x, y)
print(f"Got point at ({x}, {y}): {point}")
# Check that grid_pos property exists and returns correct value
if not hasattr(point, 'grid_pos'):
print(f"FAIL: GridPoint at ({x}, {y}) has no 'grid_pos' attribute")
all_passed = False
continue
grid_pos = point.grid_pos
# Verify it's a tuple
if not isinstance(grid_pos, tuple):
print(f"FAIL: grid_pos is {type(grid_pos).__name__}, expected tuple")
all_passed = False
continue
# Verify it has correct length
if len(grid_pos) != 2:
print(f"FAIL: grid_pos has length {len(grid_pos)}, expected 2")
all_passed = False
continue
# Verify correct values
if grid_pos != (x, y):
print(f"FAIL: grid_pos = {grid_pos}, expected ({x}, {y})")
all_passed = False
continue
print(f"OK: GridPoint at ({x}, {y}) has grid_pos = {grid_pos}")
# Test that grid_pos is read-only (should raise AttributeError)
point = grid.at(2, 3)
try:
point.grid_pos = (5, 5)
print("FAIL: grid_pos should be read-only but allowed assignment")
all_passed = False
except AttributeError:
print("OK: grid_pos is read-only (raises AttributeError on assignment)")
except Exception as e:
print(f"FAIL: Unexpected exception on assignment: {type(e).__name__}: {e}")
all_passed = False
# Verify the repr includes the coordinates
point = grid.at(4, 6)
repr_str = repr(point)
if "(4, 6)" in repr_str:
print(f"OK: repr includes coordinates: {repr_str}")
else:
print(f"Note: repr format: {repr_str}")
if all_passed:
print("PASS")
sys.exit(0)
else:
print("FAIL")
sys.exit(1)

View file

@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Test for issues #179 and #181: Grid attributes return Vectors and grid_x/grid_y renamed to grid_w/grid_h"""
import mcrfpy
import sys
def test_grid_vectors():
"""Test that Grid attributes return Vector objects instead of tuples."""
print("Testing issue #179: Grid attributes should return Vectors...")
# Create a Grid for testing
grid = mcrfpy.Grid(pos=(100, 150), size=(400, 300), grid_size=(20, 15))
# Test grid.size returns a Vector
size = grid.size
print(f" grid.size = {size}")
assert hasattr(size, 'x'), f"grid.size should have .x attribute, got {type(size)}"
assert hasattr(size, 'y'), f"grid.size should have .y attribute, got {type(size)}"
assert size.x == 400.0, f"grid.size.x should be 400.0, got {size.x}"
assert size.y == 300.0, f"grid.size.y should be 300.0, got {size.y}"
print(" PASS: grid.size returns Vector")
# Test grid.grid_size returns a Vector
grid_size = grid.grid_size
print(f" grid.grid_size = {grid_size}")
assert hasattr(grid_size, 'x'), f"grid.grid_size should have .x attribute, got {type(grid_size)}"
assert hasattr(grid_size, 'y'), f"grid.grid_size should have .y attribute, got {type(grid_size)}"
assert grid_size.x == 20.0, f"grid.grid_size.x should be 20.0, got {grid_size.x}"
assert grid_size.y == 15.0, f"grid.grid_size.y should be 15.0, got {grid_size.y}"
print(" PASS: grid.grid_size returns Vector")
# Test grid.center returns a Vector
grid.center = (50.0, 75.0) # Set center first
center = grid.center
print(f" grid.center = {center}")
assert hasattr(center, 'x'), f"grid.center should have .x attribute, got {type(center)}"
assert hasattr(center, 'y'), f"grid.center should have .y attribute, got {type(center)}"
assert center.x == 50.0, f"grid.center.x should be 50.0, got {center.x}"
assert center.y == 75.0, f"grid.center.y should be 75.0, got {center.y}"
print(" PASS: grid.center returns Vector")
# Test grid.position returns a Vector
position = grid.position
print(f" grid.position = {position}")
assert hasattr(position, 'x'), f"grid.position should have .x attribute, got {type(position)}"
assert hasattr(position, 'y'), f"grid.position should have .y attribute, got {type(position)}"
assert position.x == 100.0, f"grid.position.x should be 100.0, got {position.x}"
assert position.y == 150.0, f"grid.position.y should be 150.0, got {position.y}"
print(" PASS: grid.position returns Vector")
print("Issue #179 tests PASSED!")
def test_grid_w_h():
"""Test that grid_w and grid_h exist and grid_x/grid_y do not."""
print("\nTesting issue #181: grid_x/grid_y renamed to grid_w/grid_h...")
grid = mcrfpy.Grid(grid_size=(25, 18))
# Test grid_w and grid_h exist and return correct values
grid_w = grid.grid_w
grid_h = grid.grid_h
print(f" grid.grid_w = {grid_w}")
print(f" grid.grid_h = {grid_h}")
assert grid_w == 25, f"grid.grid_w should be 25, got {grid_w}"
assert grid_h == 18, f"grid.grid_h should be 18, got {grid_h}"
print(" PASS: grid.grid_w and grid.grid_h exist and return correct values")
# Test grid_x and grid_y do NOT exist (AttributeError expected)
try:
_ = grid.grid_x
print(" FAIL: grid.grid_x should not exist but it does!")
sys.exit(1)
except AttributeError:
print(" PASS: grid.grid_x correctly raises AttributeError")
try:
_ = grid.grid_y
print(" FAIL: grid.grid_y should not exist but it does!")
sys.exit(1)
except AttributeError:
print(" PASS: grid.grid_y correctly raises AttributeError")
print("Issue #181 tests PASSED!")
def main():
"""Run all tests."""
print("=" * 60)
print("Testing Grid Vector attributes and grid_w/grid_h rename")
print("=" * 60)
try:
test_grid_vectors()
test_grid_w_h()
print("\n" + "=" * 60)
print("ALL TESTS PASSED!")
print("=" * 60)
sys.exit(0)
except AssertionError as e:
print(f"\nFAIL: {e}")
sys.exit(1)
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,75 @@
"""Test for issue #180: Timers without a user-stored reference should still fire.
This test verifies that timers continue running even when the Python object
goes out of scope, and that they can be accessed via mcrfpy.timers.
"""
import mcrfpy
import gc
import sys
# Track timer fires
fire_count = 0
def on_timer(timer, runtime):
"""Timer callback that increments fire count."""
global fire_count
fire_count += 1
print(f"Timer fired! count={fire_count}, runtime={runtime}")
def create_orphan_timer():
"""Create a timer without storing a reference."""
# This timer should keep running even though we don't store the reference
mcrfpy.Timer("orphan_timer", on_timer, 50) # 50ms interval
print("Created orphan timer (no reference stored)")
# Set up test scene
scene = mcrfpy.Scene("test")
mcrfpy.current_scene = scene
# Create the orphan timer (no reference stored)
create_orphan_timer()
# Force garbage collection to ensure the Python wrapper is collected
gc.collect()
print("Forced garbage collection")
# Check timers immediately after GC
timers = mcrfpy.timers
print(f"Timers after GC: {len(timers)}")
for t in timers:
print(f" - {t.name}")
# In headless mode, use step() to advance time and process timers
print("\nAdvancing time with step()...")
for i in range(6):
mcrfpy.step(50) # Advance 50ms per step = 300ms total
print(f" Step {i+1}: fire_count={fire_count}")
# Now check results
print(f"\n=== Final Results ===")
print(f"Fire count: {fire_count}")
# Check that we can still find the timer in mcrfpy.timers
timers = mcrfpy.timers
print(f"Number of timers: {len(timers)}")
orphan_found = False
for t in timers:
print(f" - Timer: name={t.name}, interval={t.interval}")
if t.name == "orphan_timer":
orphan_found = True
# Stop it now that we've verified it exists
t.stop()
print(f" -> Stopped orphan timer")
# Verify the orphan timer was found and fired
if not orphan_found:
print("FAIL: Orphan timer not found in mcrfpy.timers")
sys.exit(1)
if fire_count == 0:
print("FAIL: Orphan timer never fired")
sys.exit(1)
print(f"\nPASS: Orphan timer fired {fire_count} times and was accessible via mcrfpy.timers")
sys.exit(0)

View file

@ -0,0 +1,83 @@
"""Test for issue #180: Stopped timers with user reference should stay alive.
This test verifies that:
1. A stopped timer with a user reference remains accessible
2. A stopped timer can be restarted
3. A stopped timer without references is properly cleaned up
"""
import mcrfpy
import gc
import sys
fire_count = 0
def on_timer(timer, runtime):
"""Timer callback."""
global fire_count
fire_count += 1
print(f"Timer fired! count={fire_count}")
# Set up test scene
scene = mcrfpy.Scene("test")
mcrfpy.current_scene = scene
print("=== Test 1: Stopped timer with reference stays alive ===")
# Create timer and keep reference
my_timer = mcrfpy.Timer("kept_timer", on_timer, 50)
print(f"Created timer: {my_timer.name}, active={my_timer.active}")
# Advance time to fire once
mcrfpy.step(60)
print(f"After step: fire_count={fire_count}")
# Stop the timer
my_timer.stop()
print(f"Stopped timer: active={my_timer.active}, stopped={my_timer.stopped}")
# Timer should NOT be in mcrfpy.timers (it's stopped)
timers = mcrfpy.timers
timer_names = [t.name for t in timers]
print(f"Timers after stop: {timer_names}")
if "kept_timer" in timer_names:
print("Note: Stopped timer still in mcrfpy.timers (expected - it was accessed)")
# But we should still have our reference and can restart
print(f"Our reference still valid: {my_timer.name}")
my_timer.restart()
print(f"Restarted timer: active={my_timer.active}")
# Advance time and verify it fires again
old_count = fire_count
mcrfpy.step(60)
print(f"After restart step: fire_count={fire_count}")
if fire_count <= old_count:
print("FAIL: Restarted timer didn't fire")
sys.exit(1)
print("\n=== Test 2: Stopped timer without reference is cleaned up ===")
# Create another timer, stop it, and lose the reference
temp_timer = mcrfpy.Timer("temp_timer", on_timer, 50)
temp_timer.stop()
print(f"Created and stopped temp_timer")
# Clear reference and GC
del temp_timer
gc.collect()
# The timer should be gone (stopped + no reference = GC'd)
timers = mcrfpy.timers
timer_names = [t.name for t in timers]
print(f"Timers after del+GC: {timer_names}")
# Note: temp_timer might still be there if it was retrieved before - that's OK
# The key test is that it WON'T fire since it's stopped
# Clean up
my_timer.stop()
print("\nPASS: Timer lifecycle works correctly")
sys.exit(0)

View file

@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Test for issue #182: Caption size, w, and h read-only properties.
This test verifies that:
1. Caption.size returns a Vector with the text dimensions
2. Caption.w and Caption.h return float values matching size.x and size.y
3. All three properties are read-only (setting raises AttributeError)
"""
import mcrfpy
import sys
def test_caption_size_properties():
"""Test Caption size, w, and h properties."""
# Create a caption with some text
caption = mcrfpy.Caption(text="Hello World", pos=(100, 100), font_size=24)
# Test 1: size property exists and returns a Vector
size = caption.size
print(f"caption.size = {size}")
# Verify it's a Vector
assert hasattr(size, 'x') and hasattr(size, 'y'), "size should be a Vector with x and y attributes"
print(f" size.x = {size.x}, size.y = {size.y}")
# Test 2: size values are positive (text has non-zero dimensions)
assert size.x > 0, f"size.x should be positive, got {size.x}"
assert size.y > 0, f"size.y should be positive, got {size.y}"
print(" size values are positive: PASS")
# Test 3: w and h properties exist and return floats
w = caption.w
h = caption.h
print(f"caption.w = {w}, caption.h = {h}")
assert isinstance(w, float), f"w should be a float, got {type(w)}"
assert isinstance(h, float), f"h should be a float, got {type(h)}"
print(" w and h are floats: PASS")
# Test 4: w and h match size.x and size.y
assert abs(w - size.x) < 0.001, f"w ({w}) should match size.x ({size.x})"
assert abs(h - size.y) < 0.001, f"h ({h}) should match size.y ({size.y})"
print(" w/h match size.x/size.y: PASS")
# Test 5: size is read-only
try:
caption.size = mcrfpy.Vector(50, 50)
print(" ERROR: setting size should raise AttributeError")
sys.exit(1)
except AttributeError:
print(" size is read-only: PASS")
# Test 6: w is read-only
try:
caption.w = 100.0
print(" ERROR: setting w should raise AttributeError")
sys.exit(1)
except AttributeError:
print(" w is read-only: PASS")
# Test 7: h is read-only
try:
caption.h = 50.0
print(" ERROR: setting h should raise AttributeError")
sys.exit(1)
except AttributeError:
print(" h is read-only: PASS")
# Test 8: Changing text changes the size
old_w = caption.w
caption.text = "Hello World! This is a much longer text."
new_w = caption.w
print(f"After changing text: old_w = {old_w}, new_w = {new_w}")
assert new_w > old_w, f"Longer text should have larger width: {new_w} > {old_w}"
print(" Changing text updates size: PASS")
# Test 9: Empty caption has zero or near-zero size
empty_caption = mcrfpy.Caption(text="", pos=(0, 0))
print(f"Empty caption: w={empty_caption.w}, h={empty_caption.h}")
# Note: Even empty text might have some height due to font metrics
assert empty_caption.w == 0 or empty_caption.w < 1, f"Empty text should have ~zero width, got {empty_caption.w}"
print(" Empty caption has minimal size: PASS")
print("\nAll tests passed!")
return True
if __name__ == "__main__":
try:
test_caption_size_properties()
print("PASS")
sys.exit(0)
except Exception as e:
print(f"FAIL: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View file

@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Test for issues #184 (mcrfpy.window singleton) and #189 (hide non-instantiable classes)"""
import mcrfpy
import sys
errors = []
# Test #184: mcrfpy.window singleton exists
print("Testing #184: mcrfpy.window singleton...")
try:
window = mcrfpy.window
print(f" mcrfpy.window exists: {window}")
except AttributeError as e:
errors.append(f"#184 FAIL: mcrfpy.window not found: {e}")
# Check window has expected attributes
if hasattr(mcrfpy, 'window'):
window = mcrfpy.window
# Check for expected properties
expected_attrs = ['resolution', 'fullscreen', 'vsync', 'title', 'visible']
for attr in expected_attrs:
if hasattr(window, attr):
print(f" window.{attr} = {getattr(window, attr)}")
else:
errors.append(f"#184 FAIL: mcrfpy.window missing attribute '{attr}'")
# Check that Window TYPE still exists (for isinstance checks)
if hasattr(mcrfpy, 'Window'):
print(f" mcrfpy.Window type exists: {mcrfpy.Window}")
# Verify window is an instance of Window
if isinstance(mcrfpy.window, mcrfpy.Window):
print(" isinstance(mcrfpy.window, mcrfpy.Window) = True")
else:
errors.append("#184 FAIL: mcrfpy.window is not an instance of mcrfpy.Window")
else:
errors.append("#184 FAIL: mcrfpy.Window type not found")
# Test #189: Hidden classes should NOT be accessible
print("\nTesting #189: Hidden classes should raise AttributeError...")
hidden_classes = [
'EntityCollection',
'UICollection',
'UICollectionIter',
'UIEntityCollectionIter',
'GridPoint',
'GridPointState'
]
for class_name in hidden_classes:
try:
cls = getattr(mcrfpy, class_name)
errors.append(f"#189 FAIL: mcrfpy.{class_name} should be hidden but is accessible: {cls}")
except AttributeError:
print(f" mcrfpy.{class_name} correctly raises AttributeError")
# Test that hidden classes still WORK (just not exported)
print("\nTesting that internal types still function correctly...")
# Create a scene to test UICollection
scene = mcrfpy.Scene("test_scene")
scene.activate()
# Test UICollection via .children
print(" Getting scene.children...")
children = scene.children
print(f" scene.children works: {children}")
children_type = type(children)
print(f" type(scene.children) = {children_type}")
if 'UICollection' in str(children_type):
print(" UICollection type works correctly (internal use)")
else:
errors.append(f"#189 FAIL: scene.children returned unexpected type: {children_type}")
# Test that Drawable IS still exported (should NOT be hidden)
print("\nTesting that Drawable is still exported...")
if hasattr(mcrfpy, 'Drawable'):
print(f" mcrfpy.Drawable exists: {mcrfpy.Drawable}")
else:
errors.append("FAIL: mcrfpy.Drawable should still be exported but is missing")
# Summary
print("\n" + "="*60)
if errors:
print("FAILURES:")
for e in errors:
print(f" {e}")
print(f"\nFAIL: {len(errors)} error(s)")
sys.exit(1)
else:
print("PASS: All tests passed for issues #184 and #189")
sys.exit(0)

View file

@ -0,0 +1,122 @@
"""Test for issues #185 and #188: bounds handling changes.
Issue #185: Remove .get_bounds() method - redundant with .bounds property
Issue #188: Change .bounds and .global_bounds to return (pos, size) as pair of Vectors
"""
import mcrfpy
import sys
def test_bounds():
"""Test that bounds returns (Vector, Vector) tuple."""
print("Testing bounds format...")
# Create a Frame with known position and size
frame = mcrfpy.Frame(pos=(100, 200), size=(300, 400))
bounds = frame.bounds
# Should be a tuple of 2 elements
assert isinstance(bounds, tuple), f"Expected tuple, got {type(bounds)}"
assert len(bounds) == 2, f"Expected 2 elements, got {len(bounds)}"
pos, size = bounds
# Check that pos is a Vector with correct values
assert isinstance(pos, mcrfpy.Vector), f"Expected Vector for pos, got {type(pos)}"
assert pos.x == 100, f"Expected pos.x=100, got {pos.x}"
assert pos.y == 200, f"Expected pos.y=200, got {pos.y}"
# Check that size is a Vector with correct values
assert isinstance(size, mcrfpy.Vector), f"Expected Vector for size, got {type(size)}"
assert size.x == 300, f"Expected size.x=300, got {size.x}"
assert size.y == 400, f"Expected size.y=400, got {size.y}"
print(" Frame bounds: PASS")
def test_global_bounds():
"""Test that global_bounds returns (Vector, Vector) tuple."""
print("Testing global_bounds format...")
frame = mcrfpy.Frame(pos=(50, 75), size=(150, 250))
global_bounds = frame.global_bounds
# Should be a tuple of 2 elements
assert isinstance(global_bounds, tuple), f"Expected tuple, got {type(global_bounds)}"
assert len(global_bounds) == 2, f"Expected 2 elements, got {len(global_bounds)}"
pos, size = global_bounds
assert isinstance(pos, mcrfpy.Vector), f"Expected Vector for pos, got {type(pos)}"
assert isinstance(size, mcrfpy.Vector), f"Expected Vector for size, got {type(size)}"
print(" Frame global_bounds: PASS")
def test_get_bounds_removed():
"""Test that get_bounds() method has been removed."""
print("Testing get_bounds removal...")
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
assert not hasattr(frame, 'get_bounds'), "get_bounds method should be removed"
print(" get_bounds removed: PASS")
def test_caption_bounds():
"""Test bounds on Caption."""
print("Testing Caption bounds...")
caption = mcrfpy.Caption(text="Test", pos=(25, 50))
bounds = caption.bounds
assert isinstance(bounds, tuple), f"Expected tuple, got {type(bounds)}"
assert len(bounds) == 2, f"Expected 2 elements, got {len(bounds)}"
pos, size = bounds
assert isinstance(pos, mcrfpy.Vector), f"Expected Vector for pos, got {type(pos)}"
assert isinstance(size, mcrfpy.Vector), f"Expected Vector for size, got {type(size)}"
# Caption should not have get_bounds
assert not hasattr(caption, 'get_bounds'), "get_bounds method should be removed from Caption"
print(" Caption bounds: PASS")
def test_sprite_bounds():
"""Test bounds on Sprite."""
print("Testing Sprite bounds...")
sprite = mcrfpy.Sprite(pos=(10, 20))
bounds = sprite.bounds
assert isinstance(bounds, tuple), f"Expected tuple, got {type(bounds)}"
assert len(bounds) == 2, f"Expected 2 elements, got {len(bounds)}"
pos, size = bounds
assert isinstance(pos, mcrfpy.Vector), f"Expected Vector for pos, got {type(pos)}"
assert isinstance(size, mcrfpy.Vector), f"Expected Vector for size, got {type(size)}"
# Sprite should not have get_bounds
assert not hasattr(sprite, 'get_bounds'), "get_bounds method should be removed from Sprite"
print(" Sprite bounds: PASS")
# Run tests
print("=" * 60)
print("Testing Issues #185 and #188: Bounds Handling")
print("=" * 60)
try:
test_bounds()
test_global_bounds()
test_get_bounds_removed()
test_caption_bounds()
test_sprite_bounds()
print("=" * 60)
print("All tests PASSED!")
print("=" * 60)
sys.exit(0)
except AssertionError as e:
print(f"FAILED: {e}")
sys.exit(1)
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

14
tests/minimal_reparent.py Normal file
View file

@ -0,0 +1,14 @@
import mcrfpy
scene = mcrfpy.Scene("test")
scene.activate()
f1 = mcrfpy.Frame((10,10), (100,100), fill_color = (255, 0, 0, 64))
f2 = mcrfpy.Frame((200,10), (100,100), fill_color = (0, 255, 0, 64))
f_child = mcrfpy.Frame((25,25), (50,50), fill_color = (0, 0, 255, 64))
scene.children.append(f1)
scene.children.append(f2)
f1.children.append(f_child)
f_child.parent = f2
print(f1.children)
print(f2.children)

9
tests/minimal_test.py Normal file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env python3
"""Minimal test to check if module works"""
import sys
import mcrfpy
print("Module loaded successfully")
print(f"mcrfpy has window: {hasattr(mcrfpy, 'window')}")
print(f"mcrfpy.Frame exists: {hasattr(mcrfpy, 'Frame')}")
sys.exit(0)

View file

@ -0,0 +1,285 @@
"""Regression test for issue #183: .parent quirks
Tests:
1. Newly-created drawable has parent None
2. Setting same parent twice doesn't duplicate children
3. Setting parent removes from old collection
4. Setting parent to None removes from parent collection
5. Grid parent handling works correctly
6. Moving from Frame to Grid works
7. Scene-level elements return Scene object from .parent
8. Setting parent to None removes from scene's children
9. Moving from scene to Frame works
10. Moving from Frame to scene works (via scene.children.append)
11. Direct .parent = scene assignment works
12. .parent = scene removes from Frame and adds to Scene
Note: Parent comparison uses repr() or child containment checking
since child.parent returns a new Python wrapper object each time.
"""
import mcrfpy
import sys
def same_drawable(a, b):
"""Check if two drawable wrappers point to the same C++ object.
Since get_parent creates new Python wrappers, we can't use == or is.
Instead, verify by checking that modifications to one affect the other.
"""
if a is None or b is None:
return a is None and b is None
# Modify a property on 'a' and verify it changes on 'b'
original_x = b.x
test_value = original_x + 12345.0
a.x = test_value
result = b.x == test_value
a.x = original_x # Restore
return result
def test_new_drawable_parent_none():
"""Newly-created drawable has parent None"""
frame = mcrfpy.Frame(pos=(0,0), size=(100,100))
assert frame.parent is None, f"Expected None, got {frame.parent}"
print("PASS: New drawable has parent None")
def test_no_duplicate_on_same_parent():
"""Setting same parent twice doesn't duplicate children"""
parent = mcrfpy.Frame(pos=(0,0), size=(200,200))
child = mcrfpy.Frame(pos=(10,10), size=(50,50))
# Add child to parent
parent.children.append(child)
initial_count = len(parent.children)
# Set same parent again via property
child.parent = parent
# Should not duplicate
assert len(parent.children) == initial_count, \
f"Expected {initial_count} children, got {len(parent.children)} - duplicate was added!"
print("PASS: Setting same parent twice doesn't duplicate")
def test_parent_removes_from_old_collection():
"""Setting parent removes from old collection"""
parent1 = mcrfpy.Frame(pos=(0,0), size=(200,200))
parent2 = mcrfpy.Frame(pos=(100,0), size=(200,200)) # Different x to distinguish
child = mcrfpy.Frame(pos=(10,10), size=(50,50))
# Add to parent1
parent1.children.append(child)
assert len(parent1.children) == 1, "Child should be in parent1"
assert child.parent is not None, "parent should not be None"
assert same_drawable(child.parent, parent1), "parent should be parent1"
# Move to parent2
child.parent = parent2
# Should be removed from parent1 and added to parent2
assert len(parent1.children) == 0, f"Expected 0 children in parent1, got {len(parent1.children)}"
assert len(parent2.children) == 1, f"Expected 1 child in parent2, got {len(parent2.children)}"
assert same_drawable(child.parent, parent2), "parent should be parent2"
print("PASS: Setting parent removes from old collection")
def test_parent_none_removes_from_collection():
"""Setting parent to None removes from parent's collection"""
parent = mcrfpy.Frame(pos=(0,0), size=(200,200))
child = mcrfpy.Frame(pos=(10,10), size=(50,50))
# Add child to parent
parent.children.append(child)
assert len(parent.children) == 1, "Child should be in parent"
# Set parent to None
child.parent = None
# Should be removed from parent
assert len(parent.children) == 0, f"Expected 0 children, got {len(parent.children)}"
assert child.parent is None, "parent should be None"
print("PASS: Setting parent to None removes from collection")
def test_grid_parent_handling():
"""Grid parent handling works correctly"""
grid = mcrfpy.Grid(grid_size=(10,10), pos=(0,0), size=(200,200))
child = mcrfpy.Frame(pos=(10,10), size=(50,50))
# Add child to grid
grid.children.append(child)
assert len(grid.children) == 1, "Child should be in grid"
assert child.parent is not None, "parent should not be None"
# Set same parent again (should not duplicate)
child.parent = grid
assert len(grid.children) == 1, f"Expected 1 child, got {len(grid.children)} - duplicate was added!"
# Move to a frame
frame = mcrfpy.Frame(pos=(0,0), size=(200,200))
child.parent = frame
# Should be removed from grid and added to frame
assert len(grid.children) == 0, f"Expected 0 children in grid, got {len(grid.children)}"
assert len(frame.children) == 1, f"Expected 1 child in frame, got {len(frame.children)}"
assert same_drawable(child.parent, frame), "parent should be frame"
print("PASS: Grid parent handling works correctly")
def test_move_from_frame_to_grid():
"""Moving from Frame parent to Grid parent works"""
frame = mcrfpy.Frame(pos=(0,0), size=(200,200))
grid = mcrfpy.Grid(grid_size=(10,10), pos=(100,0), size=(200,200)) # Different x
child = mcrfpy.Caption(text="Test", pos=(10,10))
# Add to frame
frame.children.append(child)
assert len(frame.children) == 1
# Move to grid
child.parent = grid
assert len(frame.children) == 0, f"Expected 0 children in frame, got {len(frame.children)}"
assert len(grid.children) == 1, f"Expected 1 child in grid, got {len(grid.children)}"
# Note: Caption doesn't have x property, so just check parent is not None
assert child.parent is not None, "parent should not be None"
print("PASS: Moving from Frame to Grid works")
def test_scene_parent_returns_scene_object():
"""Scene-level elements return Scene object from .parent"""
scene = mcrfpy.Scene('test_scene_parent_return')
child = mcrfpy.Frame(pos=(10,10), size=(50,50))
scene.children.append(child)
# .parent should return a Scene object, not None
parent = child.parent
assert parent is not None, "parent should not be None for scene-level element"
assert type(parent).__name__ == 'Scene', f"Expected Scene, got {type(parent).__name__}"
assert parent.name == 'test_scene_parent_return', f"Expected scene name 'test_scene_parent_return', got '{parent.name}'"
print("PASS: Scene-level elements return Scene object from .parent")
def test_scene_parent_none_removes():
"""Setting parent to None removes from scene's children"""
scene = mcrfpy.Scene('test_scene_remove')
mcrfpy.current_scene = scene
child = mcrfpy.Frame(pos=(10,10), size=(50,50))
scene.children.append(child)
assert len(scene.children) == 1, "Child should be in scene"
# Set parent to None - should remove from scene
child.parent = None
assert len(scene.children) == 0, f"Expected 0 children in scene, got {len(scene.children)}"
assert child.parent is None, "parent should be None"
print("PASS: Scene parent=None removes from scene")
def test_scene_to_frame():
"""Moving from scene to Frame removes from scene, adds to Frame"""
scene = mcrfpy.Scene('test_scene_to_frame')
mcrfpy.current_scene = scene
child = mcrfpy.Frame(pos=(10,10), size=(50,50))
scene.children.append(child)
assert len(scene.children) == 1
# Move to a Frame
parent_frame = mcrfpy.Frame(pos=(0,0), size=(200,200))
child.parent = parent_frame
assert len(scene.children) == 0, f"Expected 0 children in scene, got {len(scene.children)}"
assert len(parent_frame.children) == 1, f"Expected 1 child in frame, got {len(parent_frame.children)}"
print("PASS: Scene -> Frame movement works")
def test_frame_to_scene():
"""Moving from Frame to scene removes from Frame, adds to scene"""
scene = mcrfpy.Scene('test_frame_to_scene')
mcrfpy.current_scene = scene
parent_frame = mcrfpy.Frame(pos=(0,0), size=(200,200))
child = mcrfpy.Frame(pos=(10,10), size=(50,50))
parent_frame.children.append(child)
assert len(parent_frame.children) == 1
# Move to scene via scene.children.append()
scene.children.append(child)
assert len(parent_frame.children) == 0, f"Expected 0 children in frame, got {len(parent_frame.children)}"
assert len(scene.children) == 1, f"Expected 1 child in scene, got {len(scene.children)}"
print("PASS: Frame -> Scene movement works")
def test_parent_assign_scene():
"""Setting .parent = scene directly adds to scene's children"""
scene = mcrfpy.Scene('test_parent_assign_scene')
frame = mcrfpy.Frame(pos=(10,10), size=(50,50))
# Direct assignment: frame.parent = scene
frame.parent = scene
assert len(scene.children) == 1, f"Expected 1 child in scene, got {len(scene.children)}"
assert frame.parent is not None, "parent should not be None"
assert frame.parent.name == 'test_parent_assign_scene', f"Expected scene name, got '{frame.parent.name}'"
print("PASS: Direct .parent = scene assignment works")
def test_parent_assign_scene_from_frame():
"""Setting .parent = scene removes from Frame and adds to Scene"""
scene = mcrfpy.Scene('test_assign_scene_from_frame')
parent_frame = mcrfpy.Frame(pos=(0,0), size=(200,200))
child = mcrfpy.Frame(pos=(10,10), size=(50,50))
parent_frame.children.append(child)
assert len(parent_frame.children) == 1
# Move via direct assignment
child.parent = scene
assert len(parent_frame.children) == 0, f"Expected 0 children in frame, got {len(parent_frame.children)}"
assert len(scene.children) == 1, f"Expected 1 child in scene, got {len(scene.children)}"
assert child.parent.name == 'test_assign_scene_from_frame'
print("PASS: .parent = scene from Frame works")
def run_tests():
"""Run all tests"""
print("Issue #183: .parent quirks regression test")
print("=" * 50)
try:
test_new_drawable_parent_none()
test_no_duplicate_on_same_parent()
test_parent_removes_from_old_collection()
test_parent_none_removes_from_collection()
test_grid_parent_handling()
test_move_from_frame_to_grid()
# Scene parent tracking tests
test_scene_parent_returns_scene_object()
test_scene_parent_none_removes()
test_scene_to_frame()
test_frame_to_scene()
test_parent_assign_scene()
test_parent_assign_scene_from_frame()
print("=" * 50)
print("All tests PASSED!")
sys.exit(0)
except AssertionError as e:
print(f"FAIL: {e}")
sys.exit(1)
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
# Run tests immediately (no game loop needed)
run_tests()

15
tests/test_append.py Normal file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import sys
import mcrfpy
print("1 - Creating scene")
scene = mcrfpy.Scene("test")
print("2 - Getting children")
ui = scene.children
print("3 - Creating frame")
frame = mcrfpy.Frame(pos=(0,0), size=(50,50))
print("4 - Appending frame")
ui.append(frame)
print("5 - Length check")
print(f"len: {len(ui)}")
print("DONE")
sys.exit(0)

11
tests/test_children.py Normal file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env python3
import sys
import mcrfpy
print("1")
scene = mcrfpy.Scene("test")
print("2")
ui = scene.children
print("3")
print(f"children: {ui}")
print("4")
sys.exit(0)

View file

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import sys
import mcrfpy
print("1 - Loading texture", flush=True)
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
print("2 - Creating grid", flush=True)
grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture, pos=(0, 0), size=(160, 160))
print("3 - Getting grid point at (3, 5)", flush=True)
point = grid.at(3, 5)
print(f"4 - Point: {point}", flush=True)
print("5 - Getting grid_pos", flush=True)
grid_pos = point.grid_pos
print(f"6 - grid_pos: {grid_pos}", flush=True)
print("PASS", flush=True)
sys.exit(0)

View file

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Test GridPoint.grid_pos property"""
import sys
import mcrfpy
print("Testing GridPoint.grid_pos...")
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture, pos=(0, 0), size=(160, 160))
# Get a grid point
print("Getting grid point at (3, 5)...")
point = grid.at(3, 5)
print(f"Point: {point}")
# Test grid_pos property exists and returns tuple
print("Checking grid_pos property...")
grid_pos = point.grid_pos
print(f"grid_pos type: {type(grid_pos)}")
print(f"grid_pos value: {grid_pos}")
if not isinstance(grid_pos, tuple):
print(f"FAIL: grid_pos should be tuple, got {type(grid_pos)}")
sys.exit(1)
if len(grid_pos) != 2:
print(f"FAIL: grid_pos should have 2 elements, got {len(grid_pos)}")
sys.exit(1)
if grid_pos != (3, 5):
print(f"FAIL: grid_pos should be (3, 5), got {grid_pos}")
sys.exit(1)
# Test another position
print("Getting grid point at (7, 2)...")
point2 = grid.at(7, 2)
if point2.grid_pos != (7, 2):
print(f"FAIL: grid_pos should be (7, 2), got {point2.grid_pos}")
sys.exit(1)
print("PASS: GridPoint.grid_pos works correctly!")
sys.exit(0)

16
tests/test_iter_flush.py Normal file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env python3
import sys
import mcrfpy
print("1 - Creating scene", flush=True)
scene = mcrfpy.Scene("test")
print("2 - Getting children", flush=True)
ui = scene.children
print("3 - Creating frame", flush=True)
frame = mcrfpy.Frame(pos=(0,0), size=(50,50))
print("4 - Appending frame", flush=True)
ui.append(frame)
print("5 - Starting iteration", flush=True)
for item in ui:
print(f"Item: {item}", flush=True)
print("6 - Iteration done", flush=True)
sys.exit(0)

16
tests/test_iter_only.py Normal file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env python3
import sys
import mcrfpy
print("1 - Creating scene")
scene = mcrfpy.Scene("test")
print("2 - Getting children")
ui = scene.children
print("3 - Creating frame")
frame = mcrfpy.Frame(pos=(0,0), size=(50,50))
print("4 - Appending frame")
ui.append(frame)
print("5 - Starting iteration")
for item in ui:
print(f"Item: {item}")
print("6 - Iteration done")
sys.exit(0)

43
tests/test_iteration.py Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Test iteration works with hidden types"""
import sys
import mcrfpy
print("Step 1: Creating scene...")
scene = mcrfpy.Scene("test_scene")
print(f" scene: {scene}")
print("Step 2: Getting children...")
ui = scene.children
print(f" children: {ui}")
print("Step 3: Creating Frame...")
frame = mcrfpy.Frame(pos=(0,0), size=(50,50))
print(f" frame: {frame}")
print("Step 4: Appending Frame...")
ui.append(frame)
print(f" append succeeded, len={len(ui)}")
print("Step 5: Creating Caption...")
caption = mcrfpy.Caption(text="hi", pos=(0,0))
print(f" caption: {caption}")
print("Step 6: Appending Caption...")
ui.append(caption)
print(f" append succeeded, len={len(ui)}")
print("Step 7: Starting iteration...")
count = 0
for item in ui:
count += 1
print(f" Item {count}: {item}")
print(f"Step 8: Iteration complete, {count} items")
if count == 2:
print("PASS")
sys.exit(0)
else:
print(f"FAIL: expected 2 items, got {count}")
sys.exit(1)

View file

@ -0,0 +1,22 @@
#!/usr/bin/env python3
"""Simple module test"""
import sys
import mcrfpy
print("Step 1: Module loaded")
# Test window singleton exists (#184)
print("Step 2: Checking window...")
has_window = hasattr(mcrfpy, 'window')
print(f" has window: {has_window}")
if has_window:
print("Step 3: Getting window...")
window = mcrfpy.window
print(f" window: {window}")
print("Step 4: Checking resolution...")
res = window.resolution
print(f" resolution: {res}")
print("PASS")
sys.exit(0)

View file

@ -0,0 +1,7 @@
#!/usr/bin/env python3
import sys
import mcrfpy
print("Creating scene...")
scene = mcrfpy.Scene("test")
print("Scene created")
sys.exit(0)