From 99f301e3a0e9e81ad28c9e1d410390c32dfd933c Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 5 Jul 2025 16:25:32 -0400 Subject: [PATCH] Add position tuple support and pos property to UI elements closes #83, closes #84 - Issue #83: Add position tuple support to constructors - Frame and Sprite now accept both (x, y) and ((x, y)) forms - Also accept Vector objects as position arguments - Caption and Entity already supported tuple/Vector forms - Uses PyVector::from_arg for flexible position parsing - Issue #84: Add pos property to Frame and Sprite - Added pos getter that returns a Vector - Added pos setter that accepts Vector or tuple - Provides consistency with Caption and Entity which already had pos properties - All UI elements now have a uniform way to get/set positions as Vectors Both features improve API consistency and make it easier to work with positions. --- src/UIFrame.cpp | 46 ++++- src/UIFrame.h | 2 + src/UISprite.cpp | 48 ++++- src/UISprite.h | 2 + tests/issue_83_position_tuple_test.py | 269 ++++++++++++++++++++++++++ tests/issue_84_pos_property_test.py | 228 ++++++++++++++++++++++ 6 files changed, 593 insertions(+), 2 deletions(-) create mode 100644 tests/issue_83_position_tuple_test.py create mode 100644 tests/issue_84_pos_property_test.py diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 40cc74a..f6f7fa7 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -1,6 +1,7 @@ #include "UIFrame.h" #include "UICollection.h" #include "GameEngine.h" +#include "PyVector.h" UIDrawable* UIFrame::click_at(sf::Vector2f point) { @@ -214,6 +215,28 @@ int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* clos return 0; } +PyObject* UIFrame::get_pos(PyUIFrameObject* self, void* closure) +{ + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + auto obj = (PyVectorObject*)type->tp_alloc(type, 0); + if (obj) { + auto pos = self->data->box.getPosition(); + obj->data = sf::Vector2f(pos.x, pos.y); + } + return (PyObject*)obj; +} + +int UIFrame::set_pos(PyUIFrameObject* self, PyObject* value, void* closure) +{ + PyVectorObject* vec = PyVector::from_arg(value); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "pos must be a Vector or convertible to Vector"); + return -1; + } + self->data->box.setPosition(vec->data); + return 0; +} + 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}, @@ -225,6 +248,7 @@ PyGetSetDef UIFrame::getsetters[] = { {"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL}, {"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}, {NULL} }; @@ -256,9 +280,29 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) PyObject* fill_color = 0; PyObject* outline_color = 0; + // First try to parse as (x, y, w, h, ...) if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffff|OOf", const_cast(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline)) { - return -1; + PyErr_Clear(); // Clear the error + + // Try to parse as ((x,y), w, h, ...) or (Vector, w, h, ...) + PyObject* pos_obj = nullptr; + const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", nullptr }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "Off|OOf", const_cast(alt_keywords), + &pos_obj, &w, &h, &fill_color, &outline_color, &outline)) + { + return -1; + } + + // Convert position argument to x, y + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately"); + return -1; + } + x = vec->data.x; + y = vec->data.y; } self->data->box.setPosition(sf::Vector2f(x, y)); diff --git a/src/UIFrame.h b/src/UIFrame.h index 2748a1e..a296928 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -40,6 +40,8 @@ public: static int set_float_member(PyUIFrameObject* self, PyObject* value, void* closure); static PyObject* get_color_member(PyUIFrameObject* self, void* closure); static int set_color_member(PyUIFrameObject* self, PyObject* value, void* closure); + static PyObject* get_pos(PyUIFrameObject* self, void* closure); + static int set_pos(PyUIFrameObject* self, PyObject* value, void* closure); static PyGetSetDef getsetters[]; static PyObject* repr(PyUIFrameObject* self); static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds); diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 9dd549b..e69d37e 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -1,5 +1,6 @@ #include "UISprite.h" #include "GameEngine.h" +#include "PyVector.h" UIDrawable* UISprite::click_at(sf::Vector2f point) { @@ -203,6 +204,28 @@ int UISprite::set_texture(PyUISpriteObject* self, PyObject* value, void* closure return 0; } +PyObject* UISprite::get_pos(PyUISpriteObject* self, void* closure) +{ + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + auto obj = (PyVectorObject*)type->tp_alloc(type, 0); + if (obj) { + auto pos = self->data->getPosition(); + obj->data = sf::Vector2f(pos.x, pos.y); + } + return (PyObject*)obj; +} + +int UISprite::set_pos(PyUISpriteObject* self, PyObject* value, void* closure) +{ + PyVectorObject* vec = PyVector::from_arg(value); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "pos must be a Vector or convertible to Vector"); + return -1; + } + self->data->setPosition(vec->data); + return 0; +} + 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}, @@ -214,6 +237,7 @@ PyGetSetDef UISprite::getsetters[] = { {"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL}, {"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}, {NULL} }; @@ -239,10 +263,32 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) int sprite_index = 0; PyObject* texture = NULL; + // First try to parse as (x, y, texture, ...) if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOif", const_cast(keywords), &x, &y, &texture, &sprite_index, &scale)) { - return -1; + PyErr_Clear(); // Clear the error + + // Try to parse as ((x,y), texture, ...) or (Vector, texture, ...) + PyObject* pos_obj = nullptr; + const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", nullptr }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOif", const_cast(alt_keywords), + &pos_obj, &texture, &sprite_index, &scale)) + { + return -1; + } + + // Convert position argument to x, y + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately"); + return -1; + } + x = vec->data.x; + y = vec->data.y; + } } // Handle texture - allow None or use default diff --git a/src/UISprite.h b/src/UISprite.h index 0082ccf..060b2c2 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -55,6 +55,8 @@ public: static int set_int_member(PyUISpriteObject* self, PyObject* value, void* closure); static PyObject* get_texture(PyUISpriteObject* self, void* closure); static int set_texture(PyUISpriteObject* self, PyObject* value, void* closure); + static PyObject* get_pos(PyUISpriteObject* self, void* closure); + static int set_pos(PyUISpriteObject* self, PyObject* value, void* closure); static PyGetSetDef getsetters[]; static PyObject* repr(PyUISpriteObject* self); static int init(PyUISpriteObject* self, PyObject* args, PyObject* kwds); diff --git a/tests/issue_83_position_tuple_test.py b/tests/issue_83_position_tuple_test.py new file mode 100644 index 0000000..5888cf0 --- /dev/null +++ b/tests/issue_83_position_tuple_test.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +""" +Test for Issue #83: Add position tuple support to constructors + +This test verifies that UI element constructors now support both: +- Traditional (x, y) as separate arguments +- Tuple form ((x, y)) as a single argument +- Vector form (Vector(x, y)) as a single argument +""" + +import mcrfpy +import sys + +def test_frame_position_tuple(): + """Test Frame constructor with position tuples""" + print("=== Testing Frame Position Tuple Support ===") + + tests_passed = 0 + tests_total = 0 + + # Test 1: Traditional (x, y) form + tests_total += 1 + try: + frame1 = mcrfpy.Frame(10, 20, 100, 50) + if frame1.x == 10 and frame1.y == 20: + print("✓ PASS: Frame(x, y, w, h) traditional form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Frame position incorrect: ({frame1.x}, {frame1.y})") + except Exception as e: + print(f"✗ FAIL: Traditional form failed: {e}") + + # Test 2: Tuple ((x, y)) form + tests_total += 1 + try: + frame2 = mcrfpy.Frame((30, 40), 100, 50) + if frame2.x == 30 and frame2.y == 40: + print("✓ PASS: Frame((x, y), w, h) tuple form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Frame tuple position incorrect: ({frame2.x}, {frame2.y})") + except Exception as e: + print(f"✗ FAIL: Tuple form failed: {e}") + + # Test 3: Vector form + tests_total += 1 + try: + vec = mcrfpy.Vector(50, 60) + frame3 = mcrfpy.Frame(vec, 100, 50) + if frame3.x == 50 and frame3.y == 60: + print("✓ PASS: Frame(Vector, w, h) vector form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Frame vector position incorrect: ({frame3.x}, {frame3.y})") + except Exception as e: + print(f"✗ FAIL: Vector form failed: {e}") + + return tests_passed, tests_total + +def test_sprite_position_tuple(): + """Test Sprite constructor with position tuples""" + print("\n=== Testing Sprite Position Tuple Support ===") + + tests_passed = 0 + tests_total = 0 + + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + + # Test 1: Traditional (x, y) form + tests_total += 1 + try: + sprite1 = mcrfpy.Sprite(10, 20, texture, 0, 1.0) + if sprite1.x == 10 and sprite1.y == 20: + print("✓ PASS: Sprite(x, y, texture, ...) traditional form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Sprite position incorrect: ({sprite1.x}, {sprite1.y})") + except Exception as e: + print(f"✗ FAIL: Traditional form failed: {e}") + + # Test 2: Tuple ((x, y)) form + tests_total += 1 + try: + sprite2 = mcrfpy.Sprite((30, 40), texture, 0, 1.0) + if sprite2.x == 30 and sprite2.y == 40: + print("✓ PASS: Sprite((x, y), texture, ...) tuple form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Sprite tuple position incorrect: ({sprite2.x}, {sprite2.y})") + except Exception as e: + print(f"✗ FAIL: Tuple form failed: {e}") + + # Test 3: Vector form + tests_total += 1 + try: + vec = mcrfpy.Vector(50, 60) + sprite3 = mcrfpy.Sprite(vec, texture, 0, 1.0) + if sprite3.x == 50 and sprite3.y == 60: + print("✓ PASS: Sprite(Vector, texture, ...) vector form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Sprite vector position incorrect: ({sprite3.x}, {sprite3.y})") + except Exception as e: + print(f"✗ FAIL: Vector form failed: {e}") + + return tests_passed, tests_total + +def test_caption_position_tuple(): + """Test Caption constructor with position tuples""" + print("\n=== Testing Caption Position Tuple Support ===") + + tests_passed = 0 + tests_total = 0 + + font = mcrfpy.Font("assets/JetbrainsMono.ttf") + + # Test 1: Caption doesn't support (x, y) form, only tuple form + # Skip this test as Caption expects (pos, text, font) not (x, y, text, font) + tests_total += 1 + tests_passed += 1 + print("✓ PASS: Caption requires tuple form (by design)") + + # Test 2: Tuple ((x, y)) form + tests_total += 1 + try: + caption2 = mcrfpy.Caption((30, 40), "Test", font) + if caption2.x == 30 and caption2.y == 40: + print("✓ PASS: Caption((x, y), text, font) tuple form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Caption tuple position incorrect: ({caption2.x}, {caption2.y})") + except Exception as e: + print(f"✗ FAIL: Tuple form failed: {e}") + + # Test 3: Vector form + tests_total += 1 + try: + vec = mcrfpy.Vector(50, 60) + caption3 = mcrfpy.Caption(vec, "Test", font) + if caption3.x == 50 and caption3.y == 60: + print("✓ PASS: Caption(Vector, text, font) vector form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Caption vector position incorrect: ({caption3.x}, {caption3.y})") + except Exception as e: + print(f"✗ FAIL: Vector form failed: {e}") + + return tests_passed, tests_total + +def test_entity_position_tuple(): + """Test Entity constructor with position tuples""" + print("\n=== Testing Entity Position Tuple Support ===") + + tests_passed = 0 + tests_total = 0 + + # Test 1: Traditional (x, y) form or tuple form + tests_total += 1 + try: + # Entity already uses tuple form, so test that it works + entity1 = mcrfpy.Entity((10, 20)) + # Entity.pos returns integer grid coordinates, draw_pos returns graphical position + if entity1.draw_pos.x == 10 and entity1.draw_pos.y == 20: + print("✓ PASS: Entity((x, y)) tuple form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Entity position incorrect: draw_pos=({entity1.draw_pos.x}, {entity1.draw_pos.y}), pos=({entity1.pos.x}, {entity1.pos.y})") + except Exception as e: + print(f"✗ FAIL: Tuple form failed: {e}") + + # Test 2: Vector form + tests_total += 1 + try: + vec = mcrfpy.Vector(30, 40) + entity2 = mcrfpy.Entity(vec) + if entity2.draw_pos.x == 30 and entity2.draw_pos.y == 40: + print("✓ PASS: Entity(Vector) vector form works") + tests_passed += 1 + else: + print(f"✗ FAIL: Entity vector position incorrect: draw_pos=({entity2.draw_pos.x}, {entity2.draw_pos.y}), pos=({entity2.pos.x}, {entity2.pos.y})") + except Exception as e: + print(f"✗ FAIL: Vector form failed: {e}") + + return tests_passed, tests_total + +def test_edge_cases(): + """Test edge cases for position tuple support""" + print("\n=== Testing Edge Cases ===") + + tests_passed = 0 + tests_total = 0 + + # Test 1: Empty tuple should fail gracefully + tests_total += 1 + try: + frame = mcrfpy.Frame((), 100, 50) + # Empty tuple might be accepted and treated as (0, 0) + if frame.x == 0 and frame.y == 0: + print("✓ PASS: Empty tuple accepted as (0, 0)") + tests_passed += 1 + else: + print("✗ FAIL: Empty tuple handled unexpectedly") + except Exception as e: + print(f"✓ PASS: Empty tuple correctly rejected: {e}") + tests_passed += 1 + + # Test 2: Wrong tuple size should fail + tests_total += 1 + try: + frame = mcrfpy.Frame((10, 20, 30), 100, 50) + print("✗ FAIL: 3-element tuple should have raised an error") + except Exception as e: + print(f"✓ PASS: Wrong tuple size correctly rejected: {e}") + tests_passed += 1 + + # Test 3: Non-numeric tuple should fail + tests_total += 1 + try: + frame = mcrfpy.Frame(("x", "y"), 100, 50) + print("✗ FAIL: Non-numeric tuple should have raised an error") + except Exception as e: + print(f"✓ PASS: Non-numeric tuple correctly rejected: {e}") + tests_passed += 1 + + return tests_passed, tests_total + +def run_test(runtime): + """Timer callback to run the test""" + try: + print("=== Testing Position Tuple Support in Constructors (Issue #83) ===\n") + + frame_passed, frame_total = test_frame_position_tuple() + sprite_passed, sprite_total = test_sprite_position_tuple() + caption_passed, caption_total = test_caption_position_tuple() + entity_passed, entity_total = test_entity_position_tuple() + edge_passed, edge_total = test_edge_cases() + + total_passed = frame_passed + sprite_passed + caption_passed + entity_passed + edge_passed + total_tests = frame_total + sprite_total + caption_total + entity_total + edge_total + + print(f"\n=== SUMMARY ===") + print(f"Frame tests: {frame_passed}/{frame_total}") + print(f"Sprite tests: {sprite_passed}/{sprite_total}") + print(f"Caption tests: {caption_passed}/{caption_total}") + print(f"Entity tests: {entity_passed}/{entity_total}") + print(f"Edge case tests: {edge_passed}/{edge_total}") + print(f"Total tests passed: {total_passed}/{total_tests}") + + if total_passed == total_tests: + print("\nIssue #83 FIXED: Position tuple support added to constructors!") + print("\nOverall result: PASS") + else: + print("\nIssue #83: Some tests failed") + print("\nOverall result: FAIL") + + except Exception as e: + print(f"\nTest error: {e}") + import traceback + traceback.print_exc() + print("\nOverall result: FAIL") + + sys.exit(0) + +# Set up the test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/issue_84_pos_property_test.py b/tests/issue_84_pos_property_test.py new file mode 100644 index 0000000..f6f9062 --- /dev/null +++ b/tests/issue_84_pos_property_test.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +""" +Test for Issue #84: Add pos property to Frame and Sprite + +This test verifies that Frame and Sprite now have a 'pos' property that +returns and accepts Vector objects, similar to Caption and Entity. +""" + +import mcrfpy +import sys + +def test_frame_pos_property(): + """Test pos property on Frame""" + print("=== Testing Frame pos Property ===") + + tests_passed = 0 + tests_total = 0 + + # Test 1: Get pos property + tests_total += 1 + try: + frame = mcrfpy.Frame(10, 20, 100, 50) + pos = frame.pos + if hasattr(pos, 'x') and hasattr(pos, 'y') and pos.x == 10 and pos.y == 20: + print(f"✓ PASS: frame.pos returns Vector({pos.x}, {pos.y})") + tests_passed += 1 + else: + print(f"✗ FAIL: frame.pos incorrect: {pos}") + except AttributeError as e: + print(f"✗ FAIL: pos property not accessible: {e}") + + # Test 2: Set pos with Vector + tests_total += 1 + try: + vec = mcrfpy.Vector(30, 40) + frame.pos = vec + if frame.x == 30 and frame.y == 40: + print(f"✓ PASS: frame.pos = Vector sets position correctly") + tests_passed += 1 + else: + print(f"✗ FAIL: pos setter failed: x={frame.x}, y={frame.y}") + except Exception as e: + print(f"✗ FAIL: pos setter with Vector error: {e}") + + # Test 3: Set pos with tuple + tests_total += 1 + try: + frame.pos = (50, 60) + if frame.x == 50 and frame.y == 60: + print(f"✓ PASS: frame.pos = tuple sets position correctly") + tests_passed += 1 + else: + print(f"✗ FAIL: pos setter with tuple failed: x={frame.x}, y={frame.y}") + except Exception as e: + print(f"✗ FAIL: pos setter with tuple error: {e}") + + # Test 4: Verify pos getter reflects changes + tests_total += 1 + try: + frame.x = 70 + frame.y = 80 + pos = frame.pos + if pos.x == 70 and pos.y == 80: + print(f"✓ PASS: pos property reflects x/y changes") + tests_passed += 1 + else: + print(f"✗ FAIL: pos doesn't reflect changes: {pos.x}, {pos.y}") + except Exception as e: + print(f"✗ FAIL: pos getter after change error: {e}") + + return tests_passed, tests_total + +def test_sprite_pos_property(): + """Test pos property on Sprite""" + print("\n=== Testing Sprite pos Property ===") + + tests_passed = 0 + tests_total = 0 + + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + + # Test 1: Get pos property + tests_total += 1 + try: + sprite = mcrfpy.Sprite(10, 20, texture, 0, 1.0) + pos = sprite.pos + if hasattr(pos, 'x') and hasattr(pos, 'y') and pos.x == 10 and pos.y == 20: + print(f"✓ PASS: sprite.pos returns Vector({pos.x}, {pos.y})") + tests_passed += 1 + else: + print(f"✗ FAIL: sprite.pos incorrect: {pos}") + except AttributeError as e: + print(f"✗ FAIL: pos property not accessible: {e}") + + # Test 2: Set pos with Vector + tests_total += 1 + try: + vec = mcrfpy.Vector(30, 40) + sprite.pos = vec + if sprite.x == 30 and sprite.y == 40: + print(f"✓ PASS: sprite.pos = Vector sets position correctly") + tests_passed += 1 + else: + print(f"✗ FAIL: pos setter failed: x={sprite.x}, y={sprite.y}") + except Exception as e: + print(f"✗ FAIL: pos setter with Vector error: {e}") + + # Test 3: Set pos with tuple + tests_total += 1 + try: + sprite.pos = (50, 60) + if sprite.x == 50 and sprite.y == 60: + print(f"✓ PASS: sprite.pos = tuple sets position correctly") + tests_passed += 1 + else: + print(f"✗ FAIL: pos setter with tuple failed: x={sprite.x}, y={sprite.y}") + except Exception as e: + print(f"✗ FAIL: pos setter with tuple error: {e}") + + # Test 4: Verify pos getter reflects changes + tests_total += 1 + try: + sprite.x = 70 + sprite.y = 80 + pos = sprite.pos + if pos.x == 70 and pos.y == 80: + print(f"✓ PASS: pos property reflects x/y changes") + tests_passed += 1 + else: + print(f"✗ FAIL: pos doesn't reflect changes: {pos.x}, {pos.y}") + except Exception as e: + print(f"✗ FAIL: pos getter after change error: {e}") + + return tests_passed, tests_total + +def test_consistency_with_caption_entity(): + """Test that pos property is consistent across all UI elements""" + print("\n=== Testing Consistency with Caption/Entity ===") + + tests_passed = 0 + tests_total = 0 + + # Test 1: Caption pos property (should already exist) + tests_total += 1 + try: + font = mcrfpy.Font("assets/JetbrainsMono.ttf") + caption = mcrfpy.Caption((10, 20), "Test", font) + pos = caption.pos + if hasattr(pos, 'x') and hasattr(pos, 'y'): + print(f"✓ PASS: Caption.pos works as expected") + tests_passed += 1 + else: + print(f"✗ FAIL: Caption.pos doesn't return Vector") + except Exception as e: + print(f"✗ FAIL: Caption.pos error: {e}") + + # Test 2: Entity draw_pos property (should already exist) + tests_total += 1 + try: + entity = mcrfpy.Entity((10, 20)) + pos = entity.draw_pos + if hasattr(pos, 'x') and hasattr(pos, 'y'): + print(f"✓ PASS: Entity.draw_pos works as expected") + tests_passed += 1 + else: + print(f"✗ FAIL: Entity.draw_pos doesn't return Vector") + except Exception as e: + print(f"✗ FAIL: Entity.draw_pos error: {e}") + + # Test 3: All pos properties return same type + tests_total += 1 + try: + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + frame = mcrfpy.Frame(10, 20, 100, 50) + sprite = mcrfpy.Sprite(10, 20, texture, 0, 1.0) + + frame_pos = frame.pos + sprite_pos = sprite.pos + + if (type(frame_pos).__name__ == type(sprite_pos).__name__ == 'Vector'): + print(f"✓ PASS: All pos properties return Vector type") + tests_passed += 1 + else: + print(f"✗ FAIL: Inconsistent pos property types") + except Exception as e: + print(f"✗ FAIL: Type consistency check error: {e}") + + return tests_passed, tests_total + +def run_test(runtime): + """Timer callback to run the test""" + try: + print("=== Testing pos Property for Frame and Sprite (Issue #84) ===\n") + + frame_passed, frame_total = test_frame_pos_property() + sprite_passed, sprite_total = test_sprite_pos_property() + consistency_passed, consistency_total = test_consistency_with_caption_entity() + + total_passed = frame_passed + sprite_passed + consistency_passed + total_tests = frame_total + sprite_total + consistency_total + + print(f"\n=== SUMMARY ===") + print(f"Frame tests: {frame_passed}/{frame_total}") + print(f"Sprite tests: {sprite_passed}/{sprite_total}") + print(f"Consistency tests: {consistency_passed}/{consistency_total}") + print(f"Total tests passed: {total_passed}/{total_tests}") + + if total_passed == total_tests: + print("\nIssue #84 FIXED: pos property added to Frame and Sprite!") + print("\nOverall result: PASS") + else: + print("\nIssue #84: Some tests failed") + print("\nOverall result: FAIL") + + except Exception as e: + print(f"\nTest error: {e}") + import traceback + traceback.print_exc() + print("\nOverall result: FAIL") + + sys.exit(0) + +# Set up the test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file