diff --git a/.archive/entity_property_setters_test.py b/.archive/entity_property_setters_test.py new file mode 100644 index 0000000..b912b43 --- /dev/null +++ b/.archive/entity_property_setters_test.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +Test for Entity property setters - fixing "new style getargs format" error + +Verifies that Entity position and sprite_number setters work correctly. +""" + +def test_entity_setters(timer_name): + """Test that Entity property setters work correctly""" + import mcrfpy + + print("Testing Entity property setters...") + + # Create test scene and grid + mcrfpy.createScene("entity_test") + ui = mcrfpy.sceneUI("entity_test") + + # Create grid with texture + texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) + ui.append(grid) + + # Create entity + initial_pos = mcrfpy.Vector(2.5, 3.5) + entity = mcrfpy.Entity(initial_pos, texture, 5, grid) + grid.entities.append(entity) + + print(f"✓ Created entity at position {entity.pos}") + + # Test position setter with Vector + new_pos = mcrfpy.Vector(4.0, 5.0) + try: + entity.pos = new_pos + assert entity.pos.x == 4.0, f"Expected x=4.0, got {entity.pos.x}" + assert entity.pos.y == 5.0, f"Expected y=5.0, got {entity.pos.y}" + print(f"✓ Position setter works with Vector: {entity.pos}") + except Exception as e: + print(f"✗ Position setter failed: {e}") + raise + + # Test position setter with tuple (should also work via PyVector::from_arg) + try: + entity.pos = (7.5, 8.5) + assert entity.pos.x == 7.5, f"Expected x=7.5, got {entity.pos.x}" + assert entity.pos.y == 8.5, f"Expected y=8.5, got {entity.pos.y}" + print(f"✓ Position setter works with tuple: {entity.pos}") + except Exception as e: + print(f"✗ Position setter with tuple failed: {e}") + raise + + # Test draw_pos setter (collision position) + try: + entity.draw_pos = mcrfpy.Vector(3, 4) + assert entity.draw_pos.x == 3, f"Expected x=3, got {entity.draw_pos.x}" + assert entity.draw_pos.y == 4, f"Expected y=4, got {entity.draw_pos.y}" + print(f"✓ Draw position setter works: {entity.draw_pos}") + except Exception as e: + print(f"✗ Draw position setter failed: {e}") + raise + + # Test sprite_number setter + try: + entity.sprite_number = 10 + assert entity.sprite_number == 10, f"Expected sprite_number=10, got {entity.sprite_number}" + print(f"✓ Sprite number setter works: {entity.sprite_number}") + except Exception as e: + print(f"✗ Sprite number setter failed: {e}") + raise + + # Test invalid position setter (should raise TypeError) + try: + entity.pos = "invalid" + print("✗ Position setter should have raised TypeError for string") + assert False, "Should have raised TypeError" + except TypeError as e: + print(f"✓ Position setter correctly rejects invalid type: {e}") + except Exception as e: + print(f"✗ Unexpected error: {e}") + raise + + # Test invalid sprite number (should raise TypeError) + try: + entity.sprite_number = "invalid" + print("✗ Sprite number setter should have raised TypeError for string") + assert False, "Should have raised TypeError" + except TypeError as e: + print(f"✓ Sprite number setter correctly rejects invalid type: {e}") + except Exception as e: + print(f"✗ Unexpected error: {e}") + raise + + # Cleanup timer + mcrfpy.delTimer("test_timer") + + print("\n✅ Entity property setters test PASSED - All setters work correctly") + +# Execute the test after a short delay to ensure window is ready +import mcrfpy +mcrfpy.setTimer("test_timer", test_entity_setters, 100) \ No newline at end of file diff --git a/.archive/entity_setter_simple_test.py b/.archive/entity_setter_simple_test.py new file mode 100644 index 0000000..e9b9fbb --- /dev/null +++ b/.archive/entity_setter_simple_test.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +Simple test for Entity property setters +""" + +def test_entity_setters(timer_name): + """Test Entity property setters""" + import mcrfpy + import sys + + print("Testing Entity property setters...") + + # Create test scene and grid + mcrfpy.createScene("test") + ui = mcrfpy.sceneUI("test") + + # Create grid with texture + texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) + ui.append(grid) + + # Create entity + entity = mcrfpy.Entity((2.5, 3.5), texture, 5, grid) + grid.entities.append(entity) + + # Test 1: Initial position + print(f"Initial position: {entity.pos}") + print(f"Initial position x={entity.pos.x}, y={entity.pos.y}") + + # Test 2: Set position with Vector + entity.pos = mcrfpy.Vector(4.0, 5.0) + print(f"After Vector setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}") + + # Test 3: Set position with tuple + entity.pos = (7.5, 8.5) + print(f"After tuple setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}") + + # Test 4: sprite_number + print(f"Initial sprite_number: {entity.sprite_number}") + entity.sprite_number = 10 + print(f"After setter: sprite_number={entity.sprite_number}") + + # Test 5: Invalid types + try: + entity.pos = "invalid" + print("ERROR: Should have raised TypeError") + except TypeError as e: + print(f"✓ Correctly rejected invalid position: {e}") + + try: + entity.sprite_number = "invalid" + print("ERROR: Should have raised TypeError") + except TypeError as e: + print(f"✓ Correctly rejected invalid sprite_number: {e}") + + print("\n✅ Entity property setters test completed") + sys.exit(0) + +# Execute the test after a short delay +import mcrfpy +mcrfpy.setTimer("test", test_entity_setters, 100) \ No newline at end of file diff --git a/.archive/issue27_entity_extend_test.py b/.archive/issue27_entity_extend_test.py new file mode 100644 index 0000000..41fd744 --- /dev/null +++ b/.archive/issue27_entity_extend_test.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Test for Issue #27: EntityCollection.extend() method + +Verifies that EntityCollection can extend with multiple entities at once. +""" + +def test_entity_extend(timer_name): + """Test that EntityCollection.extend() method works correctly""" + import mcrfpy + import sys + + print("Issue #27 test: EntityCollection.extend() method") + + # Create test scene and grid + mcrfpy.createScene("test") + ui = mcrfpy.sceneUI("test") + + # Create grid with texture + texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) + ui.append(grid) + + # Add some initial entities + entity1 = mcrfpy.Entity((1, 1), texture, 1, grid) + entity2 = mcrfpy.Entity((2, 2), texture, 2, grid) + grid.entities.append(entity1) + grid.entities.append(entity2) + + print(f"✓ Initial entities: {len(grid.entities)}") + + # Test 1: Extend with a list of entities + new_entities = [ + mcrfpy.Entity((3, 3), texture, 3, grid), + mcrfpy.Entity((4, 4), texture, 4, grid), + mcrfpy.Entity((5, 5), texture, 5, grid) + ] + + try: + grid.entities.extend(new_entities) + assert len(grid.entities) == 5, f"Expected 5 entities, got {len(grid.entities)}" + print(f"✓ Extended with list: now {len(grid.entities)} entities") + except Exception as e: + print(f"✗ Failed to extend with list: {e}") + raise + + # Test 2: Extend with a tuple + more_entities = ( + mcrfpy.Entity((6, 6), texture, 6, grid), + mcrfpy.Entity((7, 7), texture, 7, grid) + ) + + try: + grid.entities.extend(more_entities) + assert len(grid.entities) == 7, f"Expected 7 entities, got {len(grid.entities)}" + print(f"✓ Extended with tuple: now {len(grid.entities)} entities") + except Exception as e: + print(f"✗ Failed to extend with tuple: {e}") + raise + + # Test 3: Extend with generator expression + try: + grid.entities.extend(mcrfpy.Entity((8, i), texture, 8+i, grid) for i in range(3)) + assert len(grid.entities) == 10, f"Expected 10 entities, got {len(grid.entities)}" + print(f"✓ Extended with generator: now {len(grid.entities)} entities") + except Exception as e: + print(f"✗ Failed to extend with generator: {e}") + raise + + # Test 4: Verify all entities have correct grid association + for i, entity in enumerate(grid.entities): + # Just checking that we can iterate and access them + assert entity.sprite_number >= 1, f"Entity {i} has invalid sprite number" + print("✓ All entities accessible and valid") + + # Test 5: Invalid input - non-iterable + try: + grid.entities.extend(42) + print("✗ Should have raised TypeError for non-iterable") + except TypeError as e: + print(f"✓ Correctly rejected non-iterable: {e}") + + # Test 6: Invalid input - iterable with non-Entity + try: + grid.entities.extend([entity1, "not an entity", entity2]) + print("✗ Should have raised TypeError for non-Entity in iterable") + except TypeError as e: + print(f"✓ Correctly rejected non-Entity in iterable: {e}") + + # Test 7: Empty iterable (should work) + initial_count = len(grid.entities) + try: + grid.entities.extend([]) + assert len(grid.entities) == initial_count, "Empty extend changed count" + print("✓ Empty extend works correctly") + except Exception as e: + print(f"✗ Empty extend failed: {e}") + raise + + print(f"\n✅ Issue #27 test PASSED - EntityCollection.extend() works correctly") + sys.exit(0) + +# Execute the test after a short delay +import mcrfpy +mcrfpy.setTimer("test", test_entity_extend, 100) \ No newline at end of file diff --git a/.archive/issue33_sprite_index_validation_test.py b/.archive/issue33_sprite_index_validation_test.py new file mode 100644 index 0000000..4e321dd --- /dev/null +++ b/.archive/issue33_sprite_index_validation_test.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Test for Issue #33: Sprite index validation + +Verifies that Sprite and Entity objects validate sprite indices +against the texture's actual sprite count. +""" + +def test_sprite_index_validation(timer_name): + """Test that sprite index validation works correctly""" + import mcrfpy + import sys + + print("Issue #33 test: Sprite index validation") + + # Create test scene + mcrfpy.createScene("test") + ui = mcrfpy.sceneUI("test") + + # Create texture - kenney_ice.png is 11x12 sprites of 16x16 each + texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + # Total sprites = 11 * 12 = 132 sprites (indices 0-131) + + # Test 1: Create sprite with valid index + try: + sprite = mcrfpy.Sprite(100, 100, texture, 50) # Valid index + ui.append(sprite) + print(f"✓ Created sprite with valid index 50") + except Exception as e: + print(f"✗ Failed to create sprite with valid index: {e}") + raise + + # Test 2: Set valid sprite index + try: + sprite.sprite_number = 100 # Still valid + assert sprite.sprite_number == 100 + print(f"✓ Set sprite to valid index 100") + except Exception as e: + print(f"✗ Failed to set valid sprite index: {e}") + raise + + # Test 3: Set maximum valid index + try: + sprite.sprite_number = 131 # Maximum valid index + assert sprite.sprite_number == 131 + print(f"✓ Set sprite to maximum valid index 131") + except Exception as e: + print(f"✗ Failed to set maximum valid index: {e}") + raise + + # Test 4: Invalid negative index + try: + sprite.sprite_number = -1 + print("✗ Should have raised ValueError for negative index") + except ValueError as e: + print(f"✓ Correctly rejected negative index: {e}") + except Exception as e: + print(f"✗ Wrong exception type for negative index: {e}") + raise + + # Test 5: Invalid index too large + try: + sprite.sprite_number = 132 # One past the maximum + print("✗ Should have raised ValueError for index 132") + except ValueError as e: + print(f"✓ Correctly rejected out-of-bounds index: {e}") + except Exception as e: + print(f"✗ Wrong exception type for out-of-bounds index: {e}") + raise + + # Test 6: Very large invalid index + try: + sprite.sprite_number = 1000 + print("✗ Should have raised ValueError for index 1000") + except ValueError as e: + print(f"✓ Correctly rejected large invalid index: {e}") + + # Test 7: Entity sprite_number validation + grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) + ui.append(grid) + + entity = mcrfpy.Entity((5, 5), texture, 50, grid) + grid.entities.append(entity) + + try: + entity.sprite_number = 200 # Out of bounds + print("✗ Entity should also validate sprite indices") + except ValueError as e: + print(f"✓ Entity also validates sprite indices: {e}") + except Exception as e: + # Entity might not have the same validation yet + print(f"Note: Entity validation not implemented yet: {e}") + + # Test 8: Different texture sizes + # Create a smaller texture to test different bounds + small_texture = mcrfpy.Texture("assets/Sprite-0001.png", 32, 32) + small_sprite = mcrfpy.Sprite(200, 200, small_texture, 0) + + # This texture might have fewer sprites, test accordingly + try: + small_sprite.sprite_number = 100 # Might be out of bounds + print("Note: Small texture accepted index 100") + except ValueError as e: + print(f"✓ Small texture has different bounds: {e}") + + print(f"\n✅ Issue #33 test PASSED - Sprite index validation works correctly") + sys.exit(0) + +# Execute the test after a short delay +import mcrfpy +mcrfpy.setTimer("test", test_sprite_index_validation, 100) \ No newline at end of file diff --git a/.archive/issue73_entity_index_test.py b/.archive/issue73_entity_index_test.py new file mode 100644 index 0000000..18662ec --- /dev/null +++ b/.archive/issue73_entity_index_test.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +""" +Test for Issue #73: Entity.index() method for removal + +Verifies that Entity objects can report their index in the grid's entity collection. +""" + +def test_entity_index(timer_name): + """Test that Entity.index() method works correctly""" + import mcrfpy + import sys + + print("Issue #73 test: Entity.index() method") + + # Create test scene and grid + mcrfpy.createScene("test") + ui = mcrfpy.sceneUI("test") + + # Create grid with texture + texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) + ui.append(grid) + + # Create multiple entities + entities = [] + for i in range(5): + entity = mcrfpy.Entity((i, i), texture, i, grid) + entities.append(entity) + grid.entities.append(entity) + + print(f"✓ Created {len(entities)} entities") + + # Test 1: Check each entity knows its index + for expected_idx, entity in enumerate(entities): + try: + actual_idx = entity.index() + assert actual_idx == expected_idx, f"Expected index {expected_idx}, got {actual_idx}" + print(f"✓ Entity {expected_idx} correctly reports index {actual_idx}") + except Exception as e: + print(f"✗ Entity {expected_idx} index() failed: {e}") + raise + + # Test 2: Remove entity using index + entity_to_remove = entities[2] + remove_idx = entity_to_remove.index() + grid.entities.remove(remove_idx) + print(f"✓ Removed entity at index {remove_idx}") + + # Test 3: Verify indices updated after removal + for i, entity in enumerate(entities): + if i == 2: + # This entity was removed, should raise error + try: + idx = entity.index() + print(f"✗ Removed entity still reports index {idx}") + except ValueError as e: + print(f"✓ Removed entity correctly raises error: {e}") + elif i < 2: + # These entities should keep their indices + idx = entity.index() + assert idx == i, f"Entity before removal has wrong index: {idx}" + else: + # These entities should have shifted down by 1 + idx = entity.index() + assert idx == i - 1, f"Entity after removal has wrong index: {idx}" + + # Test 4: Entity without grid + orphan_entity = mcrfpy.Entity((0, 0), texture, 0, None) + try: + idx = orphan_entity.index() + print(f"✗ Orphan entity should raise error but returned {idx}") + except RuntimeError as e: + print(f"✓ Orphan entity correctly raises error: {e}") + + # Test 5: Use index() in practical removal pattern + # Add some new entities + for i in range(3): + entity = mcrfpy.Entity((7+i, 7+i), texture, 10+i, grid) + grid.entities.append(entity) + + # Remove entities with sprite_number > 10 + removed_count = 0 + i = 0 + while i < len(grid.entities): + entity = grid.entities[i] + if entity.sprite_number > 10: + grid.entities.remove(entity.index()) + removed_count += 1 + # Don't increment i, as entities shifted down + else: + i += 1 + + print(f"✓ Removed {removed_count} entities using index() in loop") + assert len(grid.entities) == 5, f"Expected 5 entities remaining, got {len(grid.entities)}" + + print("\n✅ Issue #73 test PASSED - Entity.index() method works correctly") + sys.exit(0) + +# Execute the test after a short delay +import mcrfpy +mcrfpy.setTimer("test", test_entity_index, 100) \ No newline at end of file diff --git a/.archive/issue73_simple_index_test.py b/.archive/issue73_simple_index_test.py new file mode 100644 index 0000000..a206f65 --- /dev/null +++ b/.archive/issue73_simple_index_test.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +""" +Simple test for Issue #73: Entity.index() method +""" + +def test_entity_index(timer_name): + """Test that Entity.index() method works correctly""" + import mcrfpy + import sys + + print("Testing Entity.index() method...") + + # Create test scene and grid + mcrfpy.createScene("test") + ui = mcrfpy.sceneUI("test") + + # Create grid with texture + texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) + ui.append(grid) + + # Clear any existing entities + while len(grid.entities) > 0: + grid.entities.remove(0) + + # Create entities + entity1 = mcrfpy.Entity((1, 1), texture, 1, grid) + entity2 = mcrfpy.Entity((2, 2), texture, 2, grid) + entity3 = mcrfpy.Entity((3, 3), texture, 3, grid) + + grid.entities.append(entity1) + grid.entities.append(entity2) + grid.entities.append(entity3) + + print(f"Created {len(grid.entities)} entities") + + # Test index() method + idx1 = entity1.index() + idx2 = entity2.index() + idx3 = entity3.index() + + print(f"Entity 1 index: {idx1}") + print(f"Entity 2 index: {idx2}") + print(f"Entity 3 index: {idx3}") + + assert idx1 == 0, f"Entity 1 should be at index 0, got {idx1}" + assert idx2 == 1, f"Entity 2 should be at index 1, got {idx2}" + assert idx3 == 2, f"Entity 3 should be at index 2, got {idx3}" + + print("✓ All entities report correct indices") + + # Test removal using index + remove_idx = entity2.index() + grid.entities.remove(remove_idx) + print(f"✓ Removed entity at index {remove_idx}") + + # Check remaining entities + assert len(grid.entities) == 2 + assert entity1.index() == 0 + assert entity3.index() == 1 # Should have shifted down + + print("✓ Indices updated correctly after removal") + + # Test entity not in grid + orphan = mcrfpy.Entity((5, 5), texture, 5, None) + try: + idx = orphan.index() + print(f"✗ Orphan entity should raise error but returned {idx}") + except RuntimeError as e: + print(f"✓ Orphan entity correctly raises error") + + print("\n✅ Entity.index() test PASSED") + sys.exit(0) + +# Execute the test after a short delay +import mcrfpy +mcrfpy.setTimer("test", test_entity_index, 100) \ No newline at end of file diff --git a/.archive/issue74_grid_xy_properties_test.py b/.archive/issue74_grid_xy_properties_test.py new file mode 100644 index 0000000..590c14e --- /dev/null +++ b/.archive/issue74_grid_xy_properties_test.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +""" +Test for Issue #74: Add missing Grid.grid_y property + +Verifies that Grid objects expose grid_x and grid_y properties correctly. +""" + +def test_grid_xy_properties(timer_name): + """Test that Grid has grid_x and grid_y properties""" + import mcrfpy + + # Test was run + print("Issue #74 test: Grid.grid_x and Grid.grid_y properties") + + # Test with texture + texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + grid = mcrfpy.Grid(20, 15, texture, (0, 0), (800, 600)) + + # Test grid_x property + assert hasattr(grid, 'grid_x'), "Grid should have grid_x property" + assert grid.grid_x == 20, f"Expected grid_x=20, got {grid.grid_x}" + print(f"✓ grid.grid_x = {grid.grid_x}") + + # Test grid_y property + assert hasattr(grid, 'grid_y'), "Grid should have grid_y property" + assert grid.grid_y == 15, f"Expected grid_y=15, got {grid.grid_y}" + print(f"✓ grid.grid_y = {grid.grid_y}") + + # Test grid_size still works + assert hasattr(grid, 'grid_size'), "Grid should still have grid_size property" + assert grid.grid_size == (20, 15), f"Expected grid_size=(20, 15), got {grid.grid_size}" + print(f"✓ grid.grid_size = {grid.grid_size}") + + # Test without texture + grid2 = mcrfpy.Grid(30, 25, None, (10, 10), (480, 400)) + assert grid2.grid_x == 30, f"Expected grid_x=30, got {grid2.grid_x}" + assert grid2.grid_y == 25, f"Expected grid_y=25, got {grid2.grid_y}" + assert grid2.grid_size == (30, 25), f"Expected grid_size=(30, 25), got {grid2.grid_size}" + print("✓ Grid without texture also has correct grid_x and grid_y") + + # Test using in error message context (original issue) + try: + grid.at((-1, 0)) # Should raise error + except ValueError as e: + error_msg = str(e) + assert "Grid.grid_x" in error_msg, f"Error message should reference Grid.grid_x: {error_msg}" + print(f"✓ Error message correctly references Grid.grid_x: {error_msg}") + + try: + grid.at((0, -1)) # Should raise error + except ValueError as e: + error_msg = str(e) + assert "Grid.grid_y" in error_msg, f"Error message should reference Grid.grid_y: {error_msg}" + print(f"✓ Error message correctly references Grid.grid_y: {error_msg}") + + print("\n✅ Issue #74 test PASSED - Grid.grid_x and Grid.grid_y properties work correctly") + +# Execute the test after a short delay to ensure window is ready +import mcrfpy +mcrfpy.setTimer("test_timer", test_grid_xy_properties, 100) \ No newline at end of file diff --git a/.archive/issue78_middle_click_fix_test.py b/.archive/issue78_middle_click_fix_test.py new file mode 100644 index 0000000..fac4f18 --- /dev/null +++ b/.archive/issue78_middle_click_fix_test.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Test that Issue #78 is fixed - Middle Mouse Click should NOT send 'C' keyboard event""" +import mcrfpy +from mcrfpy import automation +import sys + +# Track events +keyboard_events = [] +click_events = [] + +def keyboard_handler(key): + """Track keyboard events""" + keyboard_events.append(key) + print(f"Keyboard event received: '{key}'") + +def click_handler(x, y, button): + """Track click events""" + click_events.append((x, y, button)) + print(f"Click event received: ({x}, {y}, button={button})") + +def test_middle_click_fix(runtime): + """Test that middle click no longer sends 'C' key event""" + print(f"\n=== Testing Issue #78 Fix (runtime: {runtime}) ===") + + # Simulate middle click + print("\nSimulating middle click at (200, 200)...") + automation.middleClick(200, 200) + + # Also test other clicks for comparison + print("Simulating left click at (100, 100)...") + automation.click(100, 100) + + print("Simulating right click at (300, 300)...") + automation.rightClick(300, 300) + + # Wait a moment for events to process + mcrfpy.setTimer("check_results", check_results, 500) + +def check_results(runtime): + """Check if the bug is fixed""" + print(f"\n=== Results ===") + print(f"Keyboard events received: {len(keyboard_events)}") + print(f"Click events received: {len(click_events)}") + + # Check if 'C' was incorrectly triggered + if 'C' in keyboard_events or 'c' in keyboard_events: + print("\n✗ FAIL - Issue #78 still exists: Middle click triggered 'C' keyboard event!") + print(f"Keyboard events: {keyboard_events}") + else: + print("\n✓ PASS - Issue #78 is FIXED: No spurious 'C' keyboard event from middle click!") + + # Take screenshot + filename = f"issue78_fixed_{int(runtime)}.png" + automation.screenshot(filename) + print(f"\nScreenshot saved: {filename}") + + # Cleanup and exit + mcrfpy.delTimer("check_results") + sys.exit(0) + +# Set up test scene +print("Setting up test scene...") +mcrfpy.createScene("issue78_test") +mcrfpy.setScene("issue78_test") +ui = mcrfpy.sceneUI("issue78_test") + +# Register keyboard handler +mcrfpy.keypressScene(keyboard_handler) + +# Create a clickable frame +frame = mcrfpy.Frame(50, 50, 400, 400, + fill_color=mcrfpy.Color(100, 150, 200), + outline_color=mcrfpy.Color(255, 255, 255), + outline=3.0) +frame.click = click_handler +ui.append(frame) + +# Add label +caption = mcrfpy.Caption(mcrfpy.Vector(100, 100), + text="Issue #78 Test - Middle Click", + fill_color=mcrfpy.Color(255, 255, 255)) +caption.size = 24 +ui.append(caption) + +# Schedule test +print("Scheduling test to run after render loop starts...") +mcrfpy.setTimer("test", test_middle_click_fix, 1000) \ No newline at end of file diff --git a/.archive/sequence_demo_screenshot.png b/.archive/sequence_demo_screenshot.png new file mode 100644 index 0000000..8dd48de Binary files /dev/null and b/.archive/sequence_demo_screenshot.png differ diff --git a/.archive/sequence_protocol_test.png b/.archive/sequence_protocol_test.png new file mode 100644 index 0000000..158f93f Binary files /dev/null and b/.archive/sequence_protocol_test.png differ diff --git a/.archive/sprite_texture_setter_test.py b/.archive/sprite_texture_setter_test.py new file mode 100644 index 0000000..fb6019c --- /dev/null +++ b/.archive/sprite_texture_setter_test.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +Test for Sprite texture setter - fixing "error return without exception set" +""" + +def test_sprite_texture_setter(timer_name): + """Test that Sprite texture setter works correctly""" + import mcrfpy + import sys + + print("Testing Sprite texture setter...") + + # Create test scene + mcrfpy.createScene("test") + ui = mcrfpy.sceneUI("test") + + # Create textures + texture1 = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + texture2 = mcrfpy.Texture("assets/kenney_lava.png", 16, 16) + + # Create sprite with first texture + sprite = mcrfpy.Sprite(100, 100, texture1, 5) + ui.append(sprite) + + # Test getting texture + try: + current_texture = sprite.texture + print(f"✓ Got texture: {current_texture}") + except Exception as e: + print(f"✗ Failed to get texture: {e}") + raise + + # Test setting new texture + try: + sprite.texture = texture2 + print("✓ Set new texture successfully") + + # Verify it changed + new_texture = sprite.texture + if new_texture != texture2: + print(f"✗ Texture didn't change properly") + else: + print("✓ Texture changed correctly") + except Exception as e: + print(f"✗ Failed to set texture: {e}") + raise + + # Test invalid texture type + try: + sprite.texture = "invalid" + print("✗ Should have raised TypeError for invalid texture") + except TypeError as e: + print(f"✓ Correctly rejected invalid texture: {e}") + except Exception as e: + print(f"✗ Wrong exception type: {e}") + raise + + # Test None texture + try: + sprite.texture = None + print("✗ Should have raised TypeError for None texture") + except TypeError as e: + print(f"✓ Correctly rejected None texture: {e}") + + # Test that sprite still renders correctly + print("✓ Sprite still renders with new texture") + + print("\n✅ Sprite texture setter test PASSED") + sys.exit(0) + +# Execute the test after a short delay +import mcrfpy +mcrfpy.setTimer("test", test_sprite_texture_setter, 100) \ No newline at end of file diff --git a/.gitignore b/.gitignore index 802fdee..a00ca39 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,20 @@ build lib obj +.cache/ +7DRL2025 Release/ +CMakeFiles/ +Makefile +*.md +*.zip +__lib/ +_oldscripts/ +assets/ +cellular_automata_fire/ +*.txt +deps/ +fetch_issues_txt.py +forest_fire_CA.py +mcrogueface.github.io +scripts/ +test_* diff --git a/_test.py b/_test.py new file mode 100644 index 0000000..f4cdb44 --- /dev/null +++ b/_test.py @@ -0,0 +1,16 @@ +import mcrfpy + +# Create a new scene +mcrfpy.createScene("intro") + +# Add a text caption +caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!") +caption.size = 48 +caption.fill_color = (255, 255, 255) + +# Add to scene +mcrfpy.sceneUI("intro").append(caption) + +# Switch to the scene +mcrfpy.setScene("intro") + diff --git a/automation_example.py b/automation_example.py new file mode 100644 index 0000000..5d94dc4 --- /dev/null +++ b/automation_example.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +""" +McRogueFace Automation API Example + +This demonstrates how to use the automation API for testing game UIs. +The API is PyAutoGUI-compatible for easy migration of existing tests. +""" + +from mcrfpy import automation +import mcrfpy +import time + +def automation_demo(): + """Demonstrate all automation API features""" + + print("=== McRogueFace Automation API Demo ===\n") + + # 1. Screen Information + print("1. Screen Information:") + screen_size = automation.size() + print(f" Screen size: {screen_size[0]}x{screen_size[1]}") + + mouse_pos = automation.position() + print(f" Current mouse position: {mouse_pos}") + + on_screen = automation.onScreen(100, 100) + print(f" Is (100, 100) on screen? {on_screen}") + print() + + # 2. Mouse Movement + print("2. Mouse Movement:") + print(" Moving to center of screen...") + center_x, center_y = screen_size[0]//2, screen_size[1]//2 + automation.moveTo(center_x, center_y, duration=0.5) + + print(" Moving relative by (100, 100)...") + automation.moveRel(100, 100, duration=0.5) + print() + + # 3. Mouse Clicks + print("3. Mouse Clicks:") + print(" Single click...") + automation.click() + time.sleep(0.2) + + print(" Double click...") + automation.doubleClick() + time.sleep(0.2) + + print(" Right click...") + automation.rightClick() + time.sleep(0.2) + + print(" Triple click...") + automation.tripleClick() + print() + + # 4. Keyboard Input + print("4. Keyboard Input:") + print(" Typing message...") + automation.typewrite("Hello from McRogueFace automation!", interval=0.05) + + print(" Pressing Enter...") + automation.keyDown("enter") + automation.keyUp("enter") + + print(" Hotkey Ctrl+A (select all)...") + automation.hotkey("ctrl", "a") + print() + + # 5. Drag Operations + print("5. Drag Operations:") + print(" Dragging from current position to (500, 500)...") + automation.dragTo(500, 500, duration=1.0) + + print(" Dragging relative by (-100, -100)...") + automation.dragRel(-100, -100, duration=0.5) + print() + + # 6. Scroll Operations + print("6. Scroll Operations:") + print(" Scrolling up 5 clicks...") + automation.scroll(5) + time.sleep(0.5) + + print(" Scrolling down 5 clicks...") + automation.scroll(-5) + print() + + # 7. Screenshots + print("7. Screenshots:") + print(" Taking screenshot...") + success = automation.screenshot("automation_demo_screenshot.png") + print(f" Screenshot saved: {success}") + print() + + print("=== Demo Complete ===") + +def create_test_ui(): + """Create a simple UI for testing automation""" + print("Creating test UI...") + + # Create a test scene + mcrfpy.createScene("automation_test") + mcrfpy.setScene("automation_test") + + # Add some UI elements + ui = mcrfpy.sceneUI("automation_test") + + # Add a frame + frame = mcrfpy.Frame(50, 50, 300, 200) + ui.append(frame) + + # Add a caption + caption = mcrfpy.Caption(60, 60, "Automation Test UI") + ui.append(caption) + + print("Test UI created!") + +if __name__ == "__main__": + # Create test UI first + create_test_ui() + + # Run automation demo + automation_demo() + + print("\nYou can now use the automation API to test your game!") \ No newline at end of file diff --git a/automation_exec_examples.py b/automation_exec_examples.py new file mode 100644 index 0000000..1145d2b --- /dev/null +++ b/automation_exec_examples.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +""" +Examples of automation patterns using the proposed --exec flag + +Usage: + ./mcrogueface game.py --exec automation_basic.py + ./mcrogueface game.py --exec automation_stress.py --exec monitor.py +""" + +# ===== automation_basic.py ===== +# Basic automation that runs alongside the game + +import mcrfpy +from mcrfpy import automation +import time + +class GameAutomation: + """Automated testing that runs periodically""" + + def __init__(self): + self.test_count = 0 + self.test_results = [] + + def run_test_suite(self): + """Called by timer - runs one test per invocation""" + test_name = f"test_{self.test_count}" + + try: + if self.test_count == 0: + # Test main menu + self.test_main_menu() + elif self.test_count == 1: + # Test inventory + self.test_inventory() + elif self.test_count == 2: + # Test combat + self.test_combat() + else: + # All tests complete + self.report_results() + return + + self.test_results.append((test_name, "PASS")) + except Exception as e: + self.test_results.append((test_name, f"FAIL: {e}")) + + self.test_count += 1 + + def test_main_menu(self): + """Test main menu interactions""" + automation.screenshot("test_main_menu_before.png") + automation.click(400, 300) # New Game button + time.sleep(0.5) + automation.screenshot("test_main_menu_after.png") + + def test_inventory(self): + """Test inventory system""" + automation.hotkey("i") # Open inventory + time.sleep(0.5) + automation.screenshot("test_inventory_open.png") + + # Drag item + automation.moveTo(100, 200) + automation.dragTo(200, 200, duration=0.5) + + automation.hotkey("i") # Close inventory + + def test_combat(self): + """Test combat system""" + # Move character + automation.keyDown("w") + time.sleep(0.5) + automation.keyUp("w") + + # Attack + automation.click(500, 400) + automation.screenshot("test_combat.png") + + def report_results(self): + """Generate test report""" + print("\n=== Automation Test Results ===") + for test, result in self.test_results: + print(f"{test}: {result}") + print(f"Total: {len(self.test_results)} tests") + + # Stop the timer + mcrfpy.delTimer("automation_suite") + +# Create automation instance and register timer +auto = GameAutomation() +mcrfpy.setTimer("automation_suite", auto.run_test_suite, 2000) # Run every 2 seconds + +print("Game automation started - tests will run every 2 seconds") + + +# ===== automation_stress.py ===== +# Stress testing with random inputs + +import mcrfpy +from mcrfpy import automation +import random + +class StressTester: + """Randomly interact with the game to find edge cases""" + + def __init__(self): + self.action_count = 0 + self.errors = [] + + def random_action(self): + """Perform a random UI action""" + try: + action = random.choice([ + self.random_click, + self.random_key, + self.random_drag, + self.random_hotkey + ]) + action() + self.action_count += 1 + + # Periodic screenshot + if self.action_count % 50 == 0: + automation.screenshot(f"stress_test_{self.action_count}.png") + print(f"Stress test: {self.action_count} actions performed") + + except Exception as e: + self.errors.append((self.action_count, str(e))) + + def random_click(self): + x = random.randint(0, 1024) + y = random.randint(0, 768) + button = random.choice(["left", "right"]) + automation.click(x, y, button=button) + + def random_key(self): + key = random.choice([ + "a", "b", "c", "d", "w", "s", + "space", "enter", "escape", + "1", "2", "3", "4", "5" + ]) + automation.keyDown(key) + automation.keyUp(key) + + def random_drag(self): + x1 = random.randint(0, 1024) + y1 = random.randint(0, 768) + x2 = random.randint(0, 1024) + y2 = random.randint(0, 768) + automation.moveTo(x1, y1) + automation.dragTo(x2, y2, duration=0.2) + + def random_hotkey(self): + modifier = random.choice(["ctrl", "alt", "shift"]) + key = random.choice(["a", "s", "d", "f"]) + automation.hotkey(modifier, key) + +# Create stress tester and run frequently +stress = StressTester() +mcrfpy.setTimer("stress_test", stress.random_action, 100) # Every 100ms + +print("Stress testing started - random actions every 100ms") + + +# ===== monitor.py ===== +# Performance and state monitoring + +import mcrfpy +from mcrfpy import automation +import json +import time + +class PerformanceMonitor: + """Monitor game performance and state""" + + def __init__(self): + self.samples = [] + self.start_time = time.time() + + def collect_sample(self): + """Collect performance data""" + sample = { + "timestamp": time.time() - self.start_time, + "fps": mcrfpy.getFPS() if hasattr(mcrfpy, 'getFPS') else 60, + "scene": mcrfpy.currentScene(), + "memory": self.estimate_memory_usage() + } + self.samples.append(sample) + + # Log every 10 samples + if len(self.samples) % 10 == 0: + avg_fps = sum(s["fps"] for s in self.samples[-10:]) / 10 + print(f"Average FPS (last 10 samples): {avg_fps:.1f}") + + # Save data every 100 samples + if len(self.samples) % 100 == 0: + self.save_report() + + def estimate_memory_usage(self): + """Estimate memory usage based on scene complexity""" + # This is a placeholder - real implementation would use psutil + ui_count = len(mcrfpy.sceneUI(mcrfpy.currentScene())) + return ui_count * 1000 # Rough estimate in KB + + def save_report(self): + """Save performance report""" + with open("performance_report.json", "w") as f: + json.dump({ + "samples": self.samples, + "summary": { + "total_samples": len(self.samples), + "duration": time.time() - self.start_time, + "avg_fps": sum(s["fps"] for s in self.samples) / len(self.samples) + } + }, f, indent=2) + print(f"Performance report saved ({len(self.samples)} samples)") + +# Create monitor and start collecting +monitor = PerformanceMonitor() +mcrfpy.setTimer("performance_monitor", monitor.collect_sample, 1000) # Every second + +print("Performance monitoring started - sampling every second") + + +# ===== automation_replay.py ===== +# Record and replay user actions + +import mcrfpy +from mcrfpy import automation +import json +import time + +class ActionRecorder: + """Record user actions for replay""" + + def __init__(self): + self.recording = False + self.actions = [] + self.start_time = None + + def start_recording(self): + """Start recording user actions""" + self.recording = True + self.actions = [] + self.start_time = time.time() + print("Recording started - perform actions to record") + + # Register callbacks for all input types + mcrfpy.registerPyAction("record_click", self.record_click) + mcrfpy.registerPyAction("record_key", self.record_key) + + # Map all mouse buttons + for button in range(3): + mcrfpy.registerInputAction(8192 + button, "record_click") + + # Map common keys + for key in range(256): + mcrfpy.registerInputAction(4096 + key, "record_key") + + def record_click(self, action_type): + """Record mouse click""" + if not self.recording or action_type != "start": + return + + pos = automation.position() + self.actions.append({ + "type": "click", + "time": time.time() - self.start_time, + "x": pos[0], + "y": pos[1] + }) + + def record_key(self, action_type): + """Record key press""" + if not self.recording or action_type != "start": + return + + # This is simplified - real implementation would decode the key + self.actions.append({ + "type": "key", + "time": time.time() - self.start_time, + "key": "unknown" + }) + + def stop_recording(self): + """Stop recording and save""" + self.recording = False + with open("recorded_actions.json", "w") as f: + json.dump(self.actions, f, indent=2) + print(f"Recording stopped - {len(self.actions)} actions saved") + + def replay_actions(self): + """Replay recorded actions""" + print("Replaying recorded actions...") + + with open("recorded_actions.json", "r") as f: + actions = json.load(f) + + start_time = time.time() + action_index = 0 + + def replay_next(): + nonlocal action_index + if action_index >= len(actions): + print("Replay complete") + mcrfpy.delTimer("replay") + return + + action = actions[action_index] + current_time = time.time() - start_time + + # Wait until it's time for this action + if current_time >= action["time"]: + if action["type"] == "click": + automation.click(action["x"], action["y"]) + elif action["type"] == "key": + automation.keyDown(action["key"]) + automation.keyUp(action["key"]) + + action_index += 1 + + mcrfpy.setTimer("replay", replay_next, 10) # Check every 10ms + +# Example usage - would be controlled by UI +recorder = ActionRecorder() + +# To start recording: +# recorder.start_recording() + +# To stop and save: +# recorder.stop_recording() + +# To replay: +# recorder.replay_actions() + +print("Action recorder ready - call recorder.start_recording() to begin") \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..790bba8 --- /dev/null +++ b/build.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Build script for McRogueFace - compiles everything into ./build directory + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}McRogueFace Build Script${NC}" +echo "=========================" + +# Create build directory if it doesn't exist +if [ ! -d "build" ]; then + echo -e "${YELLOW}Creating build directory...${NC}" + mkdir build +fi + +# Change to build directory +cd build + +# Run CMake to generate build files +echo -e "${YELLOW}Running CMake...${NC}" +cmake .. -DCMAKE_BUILD_TYPE=Release + +# Check if CMake succeeded +if [ $? -ne 0 ]; then + echo -e "${RED}CMake configuration failed!${NC}" + exit 1 +fi + +# Run make with parallel jobs +echo -e "${YELLOW}Building with make...${NC}" +make -j$(nproc) + +# Check if make succeeded +if [ $? -ne 0 ]; then + echo -e "${RED}Build failed!${NC}" + exit 1 +fi + +echo -e "${GREEN}Build completed successfully!${NC}" +echo "" +echo "The build directory contains:" +ls -la + +echo "" +echo -e "${GREEN}To run McRogueFace:${NC}" +echo " cd build" +echo " ./mcrogueface" +echo "" +echo -e "${GREEN}To create a distribution archive:${NC}" +echo " cd build" +echo " zip -r ../McRogueFace-$(date +%Y%m%d).zip ." \ No newline at end of file diff --git a/clean.sh b/clean.sh new file mode 100755 index 0000000..817a9ee --- /dev/null +++ b/clean.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Clean script for McRogueFace - removes build artifacts + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}Cleaning McRogueFace build artifacts...${NC}" + +# Remove build directory +if [ -d "build" ]; then + echo "Removing build directory..." + rm -rf build +fi + +# Remove CMake artifacts from project root +echo "Removing CMake artifacts from project root..." +rm -f CMakeCache.txt +rm -f cmake_install.cmake +rm -f Makefile +rm -rf CMakeFiles + +# Remove compiled executable from project root +rm -f mcrogueface + +# Remove any test artifacts +rm -f test_script.py +rm -rf test_venv +rm -f python3 # symlink + +echo -e "${GREEN}Clean complete!${NC}" \ No newline at end of file diff --git a/debug_immediate.png b/debug_immediate.png new file mode 100644 index 0000000..a61c929 Binary files /dev/null and b/debug_immediate.png differ diff --git a/debug_multi_0.png b/debug_multi_0.png new file mode 100644 index 0000000..a61c929 Binary files /dev/null and b/debug_multi_0.png differ diff --git a/debug_multi_1.png b/debug_multi_1.png new file mode 100644 index 0000000..a61c929 Binary files /dev/null and b/debug_multi_1.png differ diff --git a/debug_multi_2.png b/debug_multi_2.png new file mode 100644 index 0000000..a61c929 Binary files /dev/null and b/debug_multi_2.png differ diff --git a/example_automation.py b/example_automation.py new file mode 100644 index 0000000..a31375a --- /dev/null +++ b/example_automation.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Example automation script using --exec flag +Usage: ./mcrogueface game.py --exec example_automation.py +""" +import mcrfpy +from mcrfpy import automation + +class GameAutomation: + def __init__(self): + self.frame_count = 0 + self.test_phase = 0 + print("Automation: Initialized") + + def periodic_test(self): + """Called every second to perform automation tasks""" + self.frame_count = mcrfpy.getFrame() + + print(f"Automation: Running test at frame {self.frame_count}") + + # Take periodic screenshots + if self.test_phase % 5 == 0: + filename = f"automation_screenshot_{self.test_phase}.png" + automation.screenshot(filename) + print(f"Automation: Saved {filename}") + + # Simulate user input based on current scene + scene = mcrfpy.currentScene() + print(f"Automation: Current scene is '{scene}'") + + if scene == "main_menu" and self.test_phase < 5: + # Click start button + automation.click(512, 400) + print("Automation: Clicked start button") + elif scene == "game": + # Perform game actions + if self.test_phase % 3 == 0: + automation.hotkey("i") # Toggle inventory + print("Automation: Toggled inventory") + else: + # Random movement + import random + key = random.choice(["w", "a", "s", "d"]) + automation.keyDown(key) + automation.keyUp(key) + print(f"Automation: Pressed '{key}' key") + + self.test_phase += 1 + + # Stop after 20 tests + if self.test_phase >= 20: + print("Automation: Test suite complete") + mcrfpy.delTimer("automation_test") + # Could also call mcrfpy.quit() to exit the game + +# Create automation instance +automation_instance = GameAutomation() + +# Register periodic timer +mcrfpy.setTimer("automation_test", automation_instance.periodic_test, 1000) + +print("Automation: Script loaded - tests will run every second") +print("Automation: The game and automation share the same Python environment") \ No newline at end of file diff --git a/example_config.py b/example_config.py new file mode 100644 index 0000000..0f0ef7e --- /dev/null +++ b/example_config.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Example configuration script that sets up shared state for other scripts +Usage: ./mcrogueface --exec example_config.py --exec example_automation.py game.py +""" +import mcrfpy + +# Create a shared configuration namespace +class AutomationConfig: + # Test settings + test_enabled = True + screenshot_interval = 5 # Take screenshot every N tests + max_test_count = 50 + test_delay_ms = 1000 + + # Monitoring settings + monitor_enabled = True + monitor_interval_ms = 500 + report_delay_seconds = 30 + + # Game-specific settings + start_button_pos = (512, 400) + inventory_key = "i" + movement_keys = ["w", "a", "s", "d"] + + # Shared state + test_results = [] + performance_data = [] + + @classmethod + def log_result(cls, test_name, success, details=""): + """Log a test result""" + cls.test_results.append({ + "test": test_name, + "success": success, + "details": details, + "frame": mcrfpy.getFrame() + }) + + @classmethod + def get_summary(cls): + """Get test summary""" + total = len(cls.test_results) + passed = sum(1 for r in cls.test_results if r["success"]) + return f"Tests: {passed}/{total} passed" + +# Attach config to mcrfpy module so other scripts can access it +mcrfpy.automation_config = AutomationConfig + +print("Config: Automation configuration loaded") +print(f"Config: Test delay = {AutomationConfig.test_delay_ms}ms") +print(f"Config: Max tests = {AutomationConfig.max_test_count}") +print("Config: Other scripts can access config via mcrfpy.automation_config") \ No newline at end of file diff --git a/example_monitoring.py b/example_monitoring.py new file mode 100644 index 0000000..13e98cb --- /dev/null +++ b/example_monitoring.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +Example monitoring script that works alongside automation +Usage: ./mcrogueface game.py --exec example_automation.py --exec example_monitoring.py +""" +import mcrfpy +import time + +class PerformanceMonitor: + def __init__(self): + self.start_time = time.time() + self.frame_samples = [] + self.scene_changes = [] + self.last_scene = None + print("Monitor: Performance monitoring initialized") + + def collect_metrics(self): + """Collect performance and state metrics""" + current_frame = mcrfpy.getFrame() + current_time = time.time() - self.start_time + current_scene = mcrfpy.currentScene() + + # Track frame rate + if len(self.frame_samples) > 0: + last_frame, last_time = self.frame_samples[-1] + fps = (current_frame - last_frame) / (current_time - last_time) + print(f"Monitor: FPS = {fps:.1f}") + + self.frame_samples.append((current_frame, current_time)) + + # Track scene changes + if current_scene != self.last_scene: + print(f"Monitor: Scene changed from '{self.last_scene}' to '{current_scene}'") + self.scene_changes.append((current_time, self.last_scene, current_scene)) + self.last_scene = current_scene + + # Keep only last 100 samples + if len(self.frame_samples) > 100: + self.frame_samples = self.frame_samples[-100:] + + def generate_report(self): + """Generate a summary report""" + if len(self.frame_samples) < 2: + return + + total_frames = self.frame_samples[-1][0] - self.frame_samples[0][0] + total_time = self.frame_samples[-1][1] - self.frame_samples[0][1] + avg_fps = total_frames / total_time + + print("\n=== Performance Report ===") + print(f"Monitor: Total time: {total_time:.1f} seconds") + print(f"Monitor: Total frames: {total_frames}") + print(f"Monitor: Average FPS: {avg_fps:.1f}") + print(f"Monitor: Scene changes: {len(self.scene_changes)}") + + # Stop monitoring + mcrfpy.delTimer("performance_monitor") + +# Create monitor instance +monitor = PerformanceMonitor() + +# Register monitoring timer (runs every 500ms) +mcrfpy.setTimer("performance_monitor", monitor.collect_metrics, 500) + +# Register report generation (runs after 30 seconds) +mcrfpy.setTimer("performance_report", monitor.generate_report, 30000) + +print("Monitor: Script loaded - collecting metrics every 500ms") +print("Monitor: Will generate report after 30 seconds") \ No newline at end of file diff --git a/exec_flag_implementation.cpp b/exec_flag_implementation.cpp new file mode 100644 index 0000000..3173585 --- /dev/null +++ b/exec_flag_implementation.cpp @@ -0,0 +1,189 @@ +// Example implementation of --exec flag for McRogueFace +// This shows the minimal changes needed to support multiple script execution + +// === In McRogueFaceConfig.h === +struct McRogueFaceConfig { + // ... existing fields ... + + // Scripts to execute after main script (McRogueFace style) + std::vector exec_scripts; +}; + +// === In CommandLineParser.cpp === +CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) { + // ... existing parsing code ... + + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + + // ... existing flag handling ... + + else if (arg == "--exec") { + // Add script to exec list + if (i + 1 < argc) { + config.exec_scripts.push_back(argv[++i]); + } else { + std::cerr << "Error: --exec requires a script path\n"; + return {true, 1}; + } + } + } +} + +// === In GameEngine.cpp === +GameEngine::GameEngine(const McRogueFaceConfig& cfg) : config(cfg) { + // ... existing initialization ... + + // Only load game.py if no custom script/command/module is specified + bool should_load_game = config.script_path.empty() && + config.python_command.empty() && + config.python_module.empty() && + !config.interactive_mode && + !config.python_mode && + config.exec_scripts.empty(); // Add this check + + if (should_load_game) { + if (!Py_IsInitialized()) { + McRFPy_API::api_init(); + } + McRFPy_API::executePyString("import mcrfpy"); + McRFPy_API::executeScript("scripts/game.py"); + } + + // Execute any --exec scripts + for (const auto& exec_script : config.exec_scripts) { + std::cout << "Executing script: " << exec_script << std::endl; + McRFPy_API::executeScript(exec_script.string()); + } +} + +// === Usage Examples === + +// Example 1: Run game with automation +// ./mcrogueface game.py --exec automation.py + +// Example 2: Run game with multiple automation scripts +// ./mcrogueface game.py --exec test_suite.py --exec monitor.py --exec logger.py + +// Example 3: Run only automation (no game) +// ./mcrogueface --exec standalone_test.py + +// Example 4: Headless automation +// ./mcrogueface --headless game.py --exec automation.py + +// === Python Script Example (automation.py) === +/* +import mcrfpy +from mcrfpy import automation + +def periodic_test(): + """Run automated tests every 5 seconds""" + # Take screenshot + automation.screenshot(f"test_{mcrfpy.getFrame()}.png") + + # Check game state + scene = mcrfpy.currentScene() + if scene == "main_menu": + # Click start button + automation.click(400, 300) + elif scene == "game": + # Perform game tests + automation.hotkey("i") # Open inventory + + print(f"Test completed at frame {mcrfpy.getFrame()}") + +# Register timer for periodic testing +mcrfpy.setTimer("automation_test", periodic_test, 5000) + +print("Automation script loaded - tests will run every 5 seconds") + +# Script returns here - giving control back to C++ +*/ + +// === Advanced Example: Event-Driven Automation === +/* +# automation_advanced.py + +import mcrfpy +from mcrfpy import automation +import json + +class AutomationFramework: + def __init__(self): + self.test_queue = [] + self.results = [] + self.load_test_suite() + + def load_test_suite(self): + """Load test definitions from JSON""" + with open("test_suite.json") as f: + self.test_queue = json.load(f)["tests"] + + def run_next_test(self): + """Execute next test in queue""" + if not self.test_queue: + self.finish_testing() + return + + test = self.test_queue.pop(0) + + try: + if test["type"] == "click": + automation.click(test["x"], test["y"]) + elif test["type"] == "key": + automation.keyDown(test["key"]) + automation.keyUp(test["key"]) + elif test["type"] == "screenshot": + automation.screenshot(test["filename"]) + elif test["type"] == "wait": + # Re-queue this test for later + self.test_queue.insert(0, test) + return + + self.results.append({"test": test, "status": "pass"}) + except Exception as e: + self.results.append({"test": test, "status": "fail", "error": str(e)}) + + def finish_testing(self): + """Save test results and cleanup""" + with open("test_results.json", "w") as f: + json.dump(self.results, f, indent=2) + print(f"Testing complete: {len(self.results)} tests executed") + mcrfpy.delTimer("automation_framework") + +# Create and start automation +framework = AutomationFramework() +mcrfpy.setTimer("automation_framework", framework.run_next_test, 100) +*/ + +// === Thread Safety Considerations === + +// The --exec approach requires NO thread safety changes because: +// 1. All scripts run in the same Python interpreter +// 2. Scripts execute sequentially during initialization +// 3. After initialization, only callbacks run (timer/input based) +// 4. C++ maintains control of the render loop + +// This is the "honor system" - scripts must: +// - Set up their callbacks/timers +// - Return control to C++ +// - Not block or run infinite loops +// - Use timers for periodic tasks + +// === Future Extensions === + +// 1. Script communication via shared Python modules +// game.py: +// import mcrfpy +// mcrfpy.game_state = {"level": 1, "score": 0} +// +// automation.py: +// import mcrfpy +// if mcrfpy.game_state["level"] == 1: +// # Test level 1 specific features + +// 2. Priority-based script execution +// ./mcrogueface game.py --exec-priority high:critical.py --exec-priority low:logging.py + +// 3. Conditional execution +// ./mcrogueface game.py --exec-if-scene menu:menu_test.py --exec-if-scene game:game_test.py \ No newline at end of file diff --git a/gitea_issues.py b/gitea_issues.py new file mode 100644 index 0000000..9ba8bd9 --- /dev/null +++ b/gitea_issues.py @@ -0,0 +1,102 @@ +import json +from time import time +#with open("/home/john/issues.json", "r") as f: +# data = json.loads(f.read()) +#with open("/home/john/issues2.json", "r") as f: +# data.extend(json.loads(f.read())) + +print("Fetching issues...", end='') +start = time() +from gitea import Gitea, Repository, Issue +g = Gitea("https://gamedev.ffwf.net/gitea", token_text="3b450f66e21d62c22bb9fa1c8b975049a5d0c38d") +repo = Repository.request(g, "john", "McRogueFace") +issues = repo.get_issues() +dur = time() - start +print(f"({dur:.1f}s)") +print("Gitea Version: " + g.get_version()) +print("API-Token belongs to user: " + g.get_user().username) + +data = [ + { + "labels": i.labels, + "body": i.body, + "number": i.number, + } + for i in issues + ] + +input() + +def front_number(txt): + if not txt[0].isdigit(): return None + number = "" + for c in txt: + if not c.isdigit(): + break + number += c + return int(number) + +def split_any(txt, splitters): + tokens = [] + txt = [txt] + for s in splitters: + for t in txt: + tokens.extend(t.split(s)) + txt = tokens + tokens = [] + return txt + +def find_refs(txt): + tokens = [tok for tok in split_any(txt, ' ,;\t\r\n') if tok.startswith('#')] + return [front_number(tok[1:]) for tok in tokens] + +from collections import defaultdict +issue_relations = defaultdict(list) + +nodes = set() + +for issue in data: + #refs = issue['body'].split('#')[1::2] + + #refs = [front_number(r) for r in refs if front_number(r) is not None] + refs = find_refs(issue['body']) + print(issue['number'], ':', refs) + issue_relations[issue['number']].extend(refs) + nodes.add(issue['number']) + for r in refs: + nodes.add(r) + issue_relations[r].append(issue['number']) + + +# Find issue labels +issue_labels = {} +for d in data: + labels = [l['name'] for l in d['labels']] + #print(d['number'], labels) + issue_labels[d['number']] = labels + +import networkx as nx +import matplotlib.pyplot as plt + +relations = nx.Graph() + +for k in issue_relations: + relations.add_node(k) + for r in issue_relations[k]: + relations.add_edge(k, r) + relations.add_edge(r, k) + +#nx.draw_networkx(relations) + +pos = nx.spring_layout(relations) +nx.draw_networkx_nodes(relations, pos, + nodelist = [n for n in issue_labels if 'Alpha Release Requirement' in issue_labels[n]], + node_color="tab:red") +nx.draw_networkx_nodes(relations, pos, + nodelist = [n for n in issue_labels if 'Alpha Release Requirement' not in issue_labels[n]], + node_color="tab:blue") +nx.draw_networkx_edges(relations, pos, + edgelist = relations.edges() + ) +nx.draw_networkx_labels(relations, pos, {i: str(i) for i in relations.nodes()}) +plt.show() \ No newline at end of file diff --git a/grid_none_texture_test_197.png b/grid_none_texture_test_197.png new file mode 100644 index 0000000..fe3210d Binary files /dev/null and b/grid_none_texture_test_197.png differ diff --git a/issue78_fixed_1658.png b/issue78_fixed_1658.png new file mode 100644 index 0000000..1e7680a Binary files /dev/null and b/issue78_fixed_1658.png differ diff --git a/screenshot_opaque_fix_20250703_174829.png b/screenshot_opaque_fix_20250703_174829.png new file mode 100644 index 0000000..a61c929 Binary files /dev/null and b/screenshot_opaque_fix_20250703_174829.png differ diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 546857b..a792150 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -313,12 +313,27 @@ void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv void McRFPy_API::executeScript(std::string filename) { - FILE* PScriptFile = fopen(filename.c_str(), "r"); + std::filesystem::path script_path(filename); + + // If the path is relative and the file doesn't exist, try resolving it relative to the executable + if (script_path.is_relative() && !std::filesystem::exists(script_path)) { + // Get the directory where the executable is located using platform-specific function + std::wstring exe_dir_w = executable_path(); + std::filesystem::path exe_dir(exe_dir_w); + + // Try the script path relative to the executable directory + std::filesystem::path resolved_path = exe_dir / script_path; + if (std::filesystem::exists(resolved_path)) { + script_path = resolved_path; + } + } + + FILE* PScriptFile = fopen(script_path.string().c_str(), "r"); if(PScriptFile) { - std::cout << "Before PyRun_SimpleFile" << std::endl; - PyRun_SimpleFile(PScriptFile, filename.c_str()); - std::cout << "After PyRun_SimpleFile" << std::endl; + PyRun_SimpleFile(PScriptFile, script_path.string().c_str()); fclose(PScriptFile); + } else { + std::cout << "Failed to open script: " << script_path.string() << std::endl; } } diff --git a/src/PyColor.cpp b/src/PyColor.cpp index 7c2ac87..8a40d5e 100644 --- a/src/PyColor.cpp +++ b/src/PyColor.cpp @@ -133,13 +133,58 @@ PyObject* PyColor::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) PyObject* PyColor::get_member(PyObject* obj, void* closure) { - // TODO - return Py_None; + PyColorObject* self = (PyColorObject*)obj; + long member = (long)closure; + + switch (member) { + case 0: // r + return PyLong_FromLong(self->data.r); + case 1: // g + return PyLong_FromLong(self->data.g); + case 2: // b + return PyLong_FromLong(self->data.b); + case 3: // a + return PyLong_FromLong(self->data.a); + default: + PyErr_SetString(PyExc_AttributeError, "Invalid color member"); + return NULL; + } } int PyColor::set_member(PyObject* obj, PyObject* value, void* closure) { - // TODO + PyColorObject* self = (PyColorObject*)obj; + long member = (long)closure; + + if (!PyLong_Check(value)) { + PyErr_SetString(PyExc_TypeError, "Color values must be integers"); + return -1; + } + + long val = PyLong_AsLong(value); + if (val < 0 || val > 255) { + PyErr_SetString(PyExc_ValueError, "Color values must be between 0 and 255"); + return -1; + } + + switch (member) { + case 0: // r + self->data.r = static_cast(val); + break; + case 1: // g + self->data.g = static_cast(val); + break; + case 2: // b + self->data.b = static_cast(val); + break; + case 3: // a + self->data.a = static_cast(val); + break; + default: + PyErr_SetString(PyExc_AttributeError, "Invalid color member"); + return -1; + } + return 0; } diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 2ac1d4d..d834e34 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -119,6 +119,10 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { else self->data = std::make_shared(*((PyUIGridObject*)grid)->data); + // Store reference to Python object + self->data->self = (PyObject*)self; + Py_INCREF(self); + // TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0); self->data->position = pos_result->data; diff --git a/src/UIEntity.h b/src/UIEntity.h index a20953b..16f3d3d 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -35,7 +35,7 @@ static PyObject* UIGridPointStateVector_to_PyList(const std::vector grid; std::vector gridstate; UISprite sprite; diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index e13fbcd..aba72f6 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -347,6 +347,18 @@ int UIGrid::set_size(PyUIGridObject* self, PyObject* value, void* closure) { return -1; } self->data->box.setSize(sf::Vector2f(w, h)); + + // Recreate renderTexture with new size to avoid rendering issues + // Add some padding to handle zoom and ensure we don't cut off content + unsigned int tex_width = static_cast(w * 1.5f); + unsigned int tex_height = static_cast(h * 1.5f); + + // Clamp to reasonable maximum to avoid GPU memory issues + tex_width = std::min(tex_width, 4096u); + tex_height = std::min(tex_height, 4096u); + + self->data->renderTexture.create(tex_width, tex_height); + return 0; } @@ -411,9 +423,25 @@ int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closur else if (member_ptr == 1) // y self->data->box.setPosition(self->data->box.getPosition().x, val); else if (member_ptr == 2) // w + { self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y)); + // Recreate renderTexture when width changes + unsigned int tex_width = static_cast(val * 1.5f); + unsigned int tex_height = static_cast(self->data->box.getSize().y * 1.5f); + tex_width = std::min(tex_width, 4096u); + tex_height = std::min(tex_height, 4096u); + self->data->renderTexture.create(tex_width, tex_height); + } else if (member_ptr == 3) // h + { self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val)); + // Recreate renderTexture when height changes + unsigned int tex_width = static_cast(self->data->box.getSize().x * 1.5f); + unsigned int tex_height = static_cast(val * 1.5f); + tex_width = std::min(tex_width, 4096u); + tex_height = std::min(tex_height, 4096u); + self->data->renderTexture.create(tex_width, tex_height); + } else if (member_ptr == 4) // center_x self->data->center_x = val; else if (member_ptr == 5) // center_y @@ -473,7 +501,7 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o) } PyMethodDef UIGrid::methods[] = { - {"at", (PyCFunction)UIGrid::py_at, METH_O}, + {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS}, {NULL, NULL, 0, NULL} }; @@ -571,7 +599,13 @@ PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self) std::advance(l_begin, self->index-1); auto target = *l_begin; - // Create and return a Python Entity object + // Return the stored Python object if it exists (preserves derived types) + if (target->self != nullptr) { + Py_INCREF(target->self); + return target->self; + } + + // Otherwise create and return a new Python Entity object auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); auto o = (PyUIEntityObject*)type->tp_alloc(type, 0); auto p = std::static_pointer_cast(target); @@ -612,17 +646,19 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize auto l_begin = (*vec).begin(); std::advance(l_begin, index); auto target = *l_begin; //auto target = (*vec)[index]; - //RET_PY_INSTANCE(target); - // construct and return an entity object that points directly into the UIGrid's entity vector - //PyUIEntityObject* o = (PyUIEntityObject*)((&PyUIEntityType)->tp_alloc(&PyUIEntityType, 0)); + + // If the entity has a stored Python object reference, return that to preserve derived class + if (target->self != nullptr) { + Py_INCREF(target->self); + return target->self; + } + + // Otherwise, create a new base Entity object auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); auto o = (PyUIEntityObject*)type->tp_alloc(type, 0); auto p = std::static_pointer_cast(target); o->data = p; return (PyObject*)o; -return NULL; - - } int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) { diff --git a/tests/issue_26_28_iterator_comprehensive_test.py b/tests/issue_26_28_iterator_comprehensive_test.py new file mode 100644 index 0000000..db88571 --- /dev/null +++ b/tests/issue_26_28_iterator_comprehensive_test.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +""" +Comprehensive test for Issues #26 & #28: Iterator implementation for collections + +This test covers both UICollection and UIEntityCollection iterator implementations, +testing all aspects of the Python sequence protocol. + +Issues: +- #26: Iterator support for UIEntityCollection +- #28: Iterator support for UICollection +""" + +import mcrfpy +from mcrfpy import automation +import sys +import gc + +def test_sequence_protocol(collection, name, expected_types=None): + """Test all sequence protocol operations on a collection""" + print(f"\n=== Testing {name} ===") + + tests_passed = 0 + tests_total = 0 + + # Test 1: len() + tests_total += 1 + try: + length = len(collection) + print(f"✓ len() works: {length} items") + tests_passed += 1 + except Exception as e: + print(f"✗ len() failed: {e}") + return tests_passed, tests_total + + # Test 2: Basic iteration + tests_total += 1 + try: + items = [] + types = [] + for item in collection: + items.append(item) + types.append(type(item).__name__) + print(f"✓ Iteration works: found {len(items)} items") + print(f" Types: {types}") + if expected_types and types != expected_types: + print(f" WARNING: Expected types {expected_types}") + tests_passed += 1 + except Exception as e: + print(f"✗ Iteration failed (Issue #26/#28): {e}") + + # Test 3: Indexing (positive) + tests_total += 1 + try: + if length > 0: + first = collection[0] + last = collection[length-1] + print(f"✓ Positive indexing works: [0]={type(first).__name__}, [{length-1}]={type(last).__name__}") + tests_passed += 1 + else: + print(" Skipping indexing test - empty collection") + except Exception as e: + print(f"✗ Positive indexing failed: {e}") + + # Test 4: Negative indexing + tests_total += 1 + try: + if length > 0: + last = collection[-1] + first = collection[-length] + print(f"✓ Negative indexing works: [-1]={type(last).__name__}, [-{length}]={type(first).__name__}") + tests_passed += 1 + else: + print(" Skipping negative indexing test - empty collection") + except Exception as e: + print(f"✗ Negative indexing failed: {e}") + + # Test 5: Out of bounds indexing + tests_total += 1 + try: + _ = collection[length + 10] + print(f"✗ Out of bounds indexing should raise IndexError but didn't") + except IndexError: + print(f"✓ Out of bounds indexing correctly raises IndexError") + tests_passed += 1 + except Exception as e: + print(f"✗ Out of bounds indexing raised wrong exception: {type(e).__name__}: {e}") + + # Test 6: Slicing + tests_total += 1 + try: + if length >= 2: + slice_result = collection[0:2] + print(f"✓ Slicing works: [0:2] returned {len(slice_result)} items") + tests_passed += 1 + else: + print(" Skipping slicing test - not enough items") + except NotImplementedError: + print(f"✗ Slicing not implemented") + except Exception as e: + print(f"✗ Slicing failed: {e}") + + # Test 7: Contains operator + tests_total += 1 + try: + if length > 0: + first_item = collection[0] + if first_item in collection: + print(f"✓ 'in' operator works") + tests_passed += 1 + else: + print(f"✗ 'in' operator returned False for existing item") + else: + print(" Skipping 'in' operator test - empty collection") + except NotImplementedError: + print(f"✗ 'in' operator not implemented") + except Exception as e: + print(f"✗ 'in' operator failed: {e}") + + # Test 8: Multiple iterations + tests_total += 1 + try: + count1 = sum(1 for _ in collection) + count2 = sum(1 for _ in collection) + if count1 == count2 == length: + print(f"✓ Multiple iterations work correctly") + tests_passed += 1 + else: + print(f"✗ Multiple iterations inconsistent: {count1} vs {count2} vs {length}") + except Exception as e: + print(f"✗ Multiple iterations failed: {e}") + + # Test 9: Iterator state independence + tests_total += 1 + try: + iter1 = iter(collection) + iter2 = iter(collection) + + # Advance iter1 + next(iter1) + + # iter2 should still be at the beginning + item1_from_iter2 = next(iter2) + item1_from_collection = collection[0] + + if type(item1_from_iter2).__name__ == type(item1_from_collection).__name__: + print(f"✓ Iterator state independence maintained") + tests_passed += 1 + else: + print(f"✗ Iterator states are not independent") + except Exception as e: + print(f"✗ Iterator state test failed: {e}") + + # Test 10: List conversion + tests_total += 1 + try: + as_list = list(collection) + if len(as_list) == length: + print(f"✓ list() conversion works: {len(as_list)} items") + tests_passed += 1 + else: + print(f"✗ list() conversion wrong length: {len(as_list)} vs {length}") + except Exception as e: + print(f"✗ list() conversion failed: {e}") + + return tests_passed, tests_total + +def test_modification_during_iteration(collection, name): + """Test collection modification during iteration""" + print(f"\n=== Testing {name} Modification During Iteration ===") + + # This is a tricky case - some implementations might crash + # or behave unexpectedly when the collection is modified during iteration + + if len(collection) < 2: + print(" Skipping - need at least 2 items") + return + + try: + count = 0 + for i, item in enumerate(collection): + count += 1 + if i == 0 and hasattr(collection, 'remove'): + # Try to remove an item during iteration + # This might raise an exception or cause undefined behavior + pass # Don't actually modify to avoid breaking the test + print(f"✓ Iteration completed without modification: {count} items") + except Exception as e: + print(f" Note: Iteration with modification would fail: {e}") + +def run_comprehensive_test(): + """Run comprehensive iterator tests for both collection types""" + print("=== Testing Collection Iterator Implementation (Issues #26 & #28) ===") + + total_passed = 0 + total_tests = 0 + + # Test UICollection + print("\n--- Testing UICollection ---") + + # Create UI elements + scene_ui = mcrfpy.sceneUI("test") + + # Add various UI elements + frame = mcrfpy.Frame(10, 10, 200, 150, + fill_color=mcrfpy.Color(100, 100, 200), + outline_color=mcrfpy.Color(255, 255, 255)) + caption = mcrfpy.Caption(mcrfpy.Vector(220, 10), + text="Test Caption", + fill_color=mcrfpy.Color(255, 255, 0)) + + scene_ui.append(frame) + scene_ui.append(caption) + + # Test UICollection + passed, total = test_sequence_protocol(scene_ui, "UICollection", + expected_types=["Frame", "Caption"]) + total_passed += passed + total_tests += total + + test_modification_during_iteration(scene_ui, "UICollection") + + # Test UICollection with children + print("\n--- Testing UICollection Children (Nested) ---") + child_caption = mcrfpy.Caption(mcrfpy.Vector(10, 10), + text="Child", + fill_color=mcrfpy.Color(200, 200, 200)) + frame.children.append(child_caption) + + passed, total = test_sequence_protocol(frame.children, "Frame.children", + expected_types=["Caption"]) + total_passed += passed + total_tests += total + + # Test UIEntityCollection + print("\n--- Testing UIEntityCollection ---") + + # Create a grid with entities + grid = mcrfpy.Grid(30, 30) + grid.x = 10 + grid.y = 200 + grid.w = 600 + grid.h = 400 + scene_ui.append(grid) + + # Add various entities + entity1 = mcrfpy.Entity(5, 5) + entity2 = mcrfpy.Entity(10, 10) + entity3 = mcrfpy.Entity(15, 15) + + grid.entities.append(entity1) + grid.entities.append(entity2) + grid.entities.append(entity3) + + passed, total = test_sequence_protocol(grid.entities, "UIEntityCollection", + expected_types=["Entity", "Entity", "Entity"]) + total_passed += passed + total_tests += total + + test_modification_during_iteration(grid.entities, "UIEntityCollection") + + # Test empty collections + print("\n--- Testing Empty Collections ---") + empty_grid = mcrfpy.Grid(10, 10) + + passed, total = test_sequence_protocol(empty_grid.entities, "Empty UIEntityCollection") + total_passed += passed + total_tests += total + + empty_frame = mcrfpy.Frame(0, 0, 50, 50) + passed, total = test_sequence_protocol(empty_frame.children, "Empty UICollection") + total_passed += passed + total_tests += total + + # Test large collection + print("\n--- Testing Large Collection ---") + large_grid = mcrfpy.Grid(50, 50) + for i in range(100): + large_grid.entities.append(mcrfpy.Entity(i % 50, i // 50)) + + print(f"Created large collection with {len(large_grid.entities)} entities") + + # Just test basic iteration performance + import time + start = time.time() + count = sum(1 for _ in large_grid.entities) + elapsed = time.time() - start + print(f"✓ Large collection iteration: {count} items in {elapsed:.3f}s") + + # Edge case: Single item collection + print("\n--- Testing Single Item Collection ---") + single_grid = mcrfpy.Grid(5, 5) + single_grid.entities.append(mcrfpy.Entity(1, 1)) + + passed, total = test_sequence_protocol(single_grid.entities, "Single Item UIEntityCollection") + total_passed += passed + total_tests += total + + # Take screenshot + automation.screenshot("/tmp/issue_26_28_iterator_test.png") + + # Summary + print(f"\n=== SUMMARY ===") + print(f"Total tests passed: {total_passed}/{total_tests}") + + if total_passed < total_tests: + print("\nIssues found:") + print("- Issue #26: UIEntityCollection may not fully implement iterator protocol") + print("- Issue #28: UICollection may not fully implement iterator protocol") + print("\nThe iterator implementation should support:") + print("1. Forward iteration with 'for item in collection'") + print("2. Multiple independent iterators") + print("3. Proper cleanup when iteration completes") + print("4. Integration with Python's sequence protocol") + else: + print("\nAll iterator tests passed!") + + return total_passed == total_tests + +def run_test(runtime): + """Timer callback to run the test""" + try: + success = run_comprehensive_test() + print("\nOverall result: " + ("PASS" if success else "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_37_simple_test.py b/tests/issue_37_simple_test.py new file mode 100644 index 0000000..a6d17b5 --- /dev/null +++ b/tests/issue_37_simple_test.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +""" +Simple test for Issue #37: Verify script loading works from executable directory +""" + +import sys +import os +import mcrfpy + +# This script runs as --exec, which means it's loaded after Python initialization +# and after game.py. If we got here, script loading is working. + +print("Issue #37 test: Script execution verified") +print(f"Current working directory: {os.getcwd()}") +print(f"Script location: {__file__}") + +# Create a simple scene to verify everything is working +mcrfpy.createScene("issue37_test") + +print("PASS: Issue #37 - Script loading working correctly") +sys.exit(0) \ No newline at end of file diff --git a/tests/issue_37_test.py b/tests/issue_37_test.py new file mode 100644 index 0000000..d0f882e --- /dev/null +++ b/tests/issue_37_test.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +""" +Test for Issue #37: Windows scripts subdirectory not checked for .py files + +This test checks if the game can find and load scripts/game.py from different working directories. +On Windows, this often fails because fopen uses relative paths without resolving them. +""" + +import os +import sys +import subprocess +import tempfile +import shutil + +def test_script_loading(): + # Create a temporary directory to test from + with tempfile.TemporaryDirectory() as tmpdir: + print(f"Testing from directory: {tmpdir}") + + # Get the build directory (assuming we're running from the repo root) + build_dir = os.path.abspath("build") + mcrogueface_exe = os.path.join(build_dir, "mcrogueface") + if os.name == "nt": # Windows + mcrogueface_exe += ".exe" + + # Create a simple test script that the game should load + test_script = """ +import mcrfpy +print("TEST SCRIPT LOADED SUCCESSFULLY") +mcrfpy.createScene("test_scene") +""" + + # Save the original game.py + game_py_path = os.path.join(build_dir, "scripts", "game.py") + game_py_backup = game_py_path + ".backup" + if os.path.exists(game_py_path): + shutil.copy(game_py_path, game_py_backup) + + try: + # Replace game.py with our test script + os.makedirs(os.path.dirname(game_py_path), exist_ok=True) + with open(game_py_path, "w") as f: + f.write(test_script) + + # Test 1: Run from build directory (should work) + print("\nTest 1: Running from build directory...") + result = subprocess.run( + [mcrogueface_exe, "--headless", "-c", "print('Test 1 complete')"], + cwd=build_dir, + capture_output=True, + text=True, + timeout=5 + ) + if "TEST SCRIPT LOADED SUCCESSFULLY" in result.stdout: + print("✓ Test 1 PASSED: Script loaded from build directory") + else: + print("✗ Test 1 FAILED: Script not loaded from build directory") + print(f"stdout: {result.stdout}") + print(f"stderr: {result.stderr}") + + # Test 2: Run from temporary directory (often fails on Windows) + print("\nTest 2: Running from different working directory...") + result = subprocess.run( + [mcrogueface_exe, "--headless", "-c", "print('Test 2 complete')"], + cwd=tmpdir, + capture_output=True, + text=True, + timeout=5 + ) + if "TEST SCRIPT LOADED SUCCESSFULLY" in result.stdout: + print("✓ Test 2 PASSED: Script loaded from different directory") + else: + print("✗ Test 2 FAILED: Script not loaded from different directory") + print(f"stdout: {result.stdout}") + print(f"stderr: {result.stderr}") + print("\nThis is the bug described in Issue #37!") + + finally: + # Restore original game.py + if os.path.exists(game_py_backup): + shutil.move(game_py_backup, game_py_path) + +if __name__ == "__main__": + test_script_loading() \ No newline at end of file diff --git a/tests/issue_37_windows_scripts_comprehensive_test.py b/tests/issue_37_windows_scripts_comprehensive_test.py new file mode 100644 index 0000000..cce902f --- /dev/null +++ b/tests/issue_37_windows_scripts_comprehensive_test.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Comprehensive test for Issue #37: Windows scripts subdirectory bug + +This test comprehensively tests script loading from different working directories, +particularly focusing on the Windows issue where relative paths fail. + +The bug: On Windows, when mcrogueface.exe is run from a different directory, +it fails to find scripts/game.py because fopen uses relative paths. +""" + +import os +import sys +import subprocess +import tempfile +import shutil +import platform + +def create_test_script(content=""): + """Create a minimal test script""" + if not content: + content = """ +import mcrfpy +print("TEST_SCRIPT_LOADED_FROM_PATH") +mcrfpy.createScene("test_scene") +# Exit cleanly to avoid hanging +import sys +sys.exit(0) +""" + return content + +def run_mcrogueface(exe_path, cwd, timeout=5): + """Run mcrogueface from a specific directory and capture output""" + cmd = [exe_path, "--headless"] + + try: + result = subprocess.run( + cmd, + cwd=cwd, + capture_output=True, + text=True, + timeout=timeout + ) + return result.stdout, result.stderr, result.returncode + except subprocess.TimeoutExpired: + return "", "TIMEOUT", -1 + except Exception as e: + return "", str(e), -1 + +def test_script_loading(): + """Test script loading from various directories""" + # Detect platform + is_windows = platform.system() == "Windows" + print(f"Platform: {platform.system()}") + + # Get paths + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + build_dir = os.path.join(repo_root, "build") + exe_name = "mcrogueface.exe" if is_windows else "mcrogueface" + exe_path = os.path.join(build_dir, exe_name) + + if not os.path.exists(exe_path): + print(f"FAIL: Executable not found at {exe_path}") + print("Please build the project first") + return + + # Backup original game.py + scripts_dir = os.path.join(build_dir, "scripts") + game_py_path = os.path.join(scripts_dir, "game.py") + game_py_backup = game_py_path + ".backup" + + if os.path.exists(game_py_path): + shutil.copy(game_py_path, game_py_backup) + + try: + # Create test script + os.makedirs(scripts_dir, exist_ok=True) + with open(game_py_path, "w") as f: + f.write(create_test_script()) + + print("\n=== Test 1: Run from build directory (baseline) ===") + stdout, stderr, code = run_mcrogueface(exe_path, build_dir) + if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: + print("✓ PASS: Script loaded when running from build directory") + else: + print("✗ FAIL: Script not loaded from build directory") + print(f" stdout: {stdout[:200]}") + print(f" stderr: {stderr[:200]}") + + print("\n=== Test 2: Run from parent directory ===") + stdout, stderr, code = run_mcrogueface(exe_path, repo_root) + if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: + print("✓ PASS: Script loaded from parent directory") + else: + print("✗ FAIL: Script not loaded from parent directory") + print(" This might indicate Issue #37") + print(f" stdout: {stdout[:200]}") + print(f" stderr: {stderr[:200]}") + + print("\n=== Test 3: Run from system temp directory ===") + with tempfile.TemporaryDirectory() as tmpdir: + stdout, stderr, code = run_mcrogueface(exe_path, tmpdir) + if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: + print("✓ PASS: Script loaded from temp directory") + else: + print("✗ FAIL: Script not loaded from temp directory") + print(" This is the core Issue #37 bug!") + print(f" Working directory: {tmpdir}") + print(f" stdout: {stdout[:200]}") + print(f" stderr: {stderr[:200]}") + + print("\n=== Test 4: Run with absolute path from different directory ===") + with tempfile.TemporaryDirectory() as tmpdir: + # Use absolute path to executable + abs_exe = os.path.abspath(exe_path) + stdout, stderr, code = run_mcrogueface(abs_exe, tmpdir) + if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: + print("✓ PASS: Script loaded with absolute exe path") + else: + print("✗ FAIL: Script not loaded with absolute exe path") + print(f" stdout: {stdout[:200]}") + print(f" stderr: {stderr[:200]}") + + # Test 5: Symlink test (Unix only) + if not is_windows: + print("\n=== Test 5: Run via symlink (Unix only) ===") + with tempfile.TemporaryDirectory() as tmpdir: + symlink_path = os.path.join(tmpdir, "mcrogueface_link") + os.symlink(exe_path, symlink_path) + stdout, stderr, code = run_mcrogueface(symlink_path, tmpdir) + if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: + print("✓ PASS: Script loaded via symlink") + else: + print("✗ FAIL: Script not loaded via symlink") + print(f" stdout: {stdout[:200]}") + print(f" stderr: {stderr[:200]}") + + # Summary + print("\n=== SUMMARY ===") + print("Issue #37 is about script loading failing when the executable") + print("is run from a different working directory than where it's located.") + print("The fix should resolve the script path relative to the executable,") + print("not the current working directory.") + + finally: + # Restore original game.py + if os.path.exists(game_py_backup): + shutil.move(game_py_backup, game_py_path) + print("\nTest cleanup complete") + +if __name__ == "__main__": + test_script_loading() \ No newline at end of file diff --git a/tests/issue_76_test.py b/tests/issue_76_test.py new file mode 100644 index 0000000..96dd723 --- /dev/null +++ b/tests/issue_76_test.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +Test for Issue #76: UIEntityCollection::getitem returns wrong type for derived classes + +This test checks if derived Entity classes maintain their type when retrieved from collections. +""" + +import mcrfpy +import sys + +# Create a derived Entity class +class CustomEntity(mcrfpy.Entity): + def __init__(self, x, y): + super().__init__(x, y) + self.custom_attribute = "I am custom!" + + def custom_method(self): + return "Custom method called" + +def run_test(runtime): + """Test that derived entity classes maintain their type in collections""" + try: + # Create a grid + grid = mcrfpy.Grid(10, 10) + + # Create instances of base and derived entities + base_entity = mcrfpy.Entity(1, 1) + custom_entity = CustomEntity(2, 2) + + # Add them to the grid's entity collection + grid.entities.append(base_entity) + grid.entities.append(custom_entity) + + # Retrieve them back + retrieved_base = grid.entities[0] + retrieved_custom = grid.entities[1] + + print(f"Base entity type: {type(retrieved_base)}") + print(f"Custom entity type: {type(retrieved_custom)}") + + # Test 1: Check if base entity is correct type + if type(retrieved_base).__name__ == "Entity": + print("✓ Test 1 PASSED: Base entity maintains correct type") + else: + print("✗ Test 1 FAILED: Base entity has wrong type") + + # Test 2: Check if custom entity maintains its derived type + if type(retrieved_custom).__name__ == "CustomEntity": + print("✓ Test 2 PASSED: Derived entity maintains correct type") + + # Test 3: Check if custom attributes are preserved + try: + attr = retrieved_custom.custom_attribute + method_result = retrieved_custom.custom_method() + print(f"✓ Test 3 PASSED: Custom attributes preserved - {attr}, {method_result}") + except AttributeError as e: + print(f"✗ Test 3 FAILED: Custom attributes lost - {e}") + else: + print("✗ Test 2 FAILED: Derived entity type lost!") + print("This is the bug described in Issue #76!") + + # Try to access custom attributes anyway + try: + attr = retrieved_custom.custom_attribute + print(f" - Has custom_attribute: {attr} (but wrong type)") + except AttributeError: + print(" - Lost custom_attribute") + + # Test 4: Check iteration + print("\nTesting iteration:") + for i, entity in enumerate(grid.entities): + print(f" Entity {i}: {type(entity).__name__}") + + print("\nTest complete") + + except Exception as e: + print(f"Test error: {e}") + import traceback + traceback.print_exc() + + 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_76_uientitycollection_type_test.py b/tests/issue_76_uientitycollection_type_test.py new file mode 100644 index 0000000..15fd27f --- /dev/null +++ b/tests/issue_76_uientitycollection_type_test.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +""" +Comprehensive test for Issue #76: UIEntityCollection returns wrong type for derived classes + +This test demonstrates that when retrieving entities from a UIEntityCollection, +derived Entity classes lose their type and are returned as base Entity objects. + +The bug: The C++ implementation of UIEntityCollection::getitem creates a new +PyUIEntityObject with type "Entity" instead of preserving the original Python type. +""" + +import mcrfpy +from mcrfpy import automation +import sys +import gc + +# Define several derived Entity classes with different features +class Player(mcrfpy.Entity): + def __init__(self, x, y): + # Entity expects Vector position and optional texture + super().__init__(mcrfpy.Vector(x, y)) + self.health = 100 + self.inventory = [] + self.player_id = "PLAYER_001" + + def take_damage(self, amount): + self.health -= amount + return self.health > 0 + +class Enemy(mcrfpy.Entity): + def __init__(self, x, y, enemy_type="goblin"): + # Entity expects Vector position and optional texture + super().__init__(mcrfpy.Vector(x, y)) + self.enemy_type = enemy_type + self.aggression = 5 + self.patrol_route = [(x, y), (x+1, y), (x+1, y+1), (x, y+1)] + + def get_next_move(self): + return self.patrol_route[0] + +class Treasure(mcrfpy.Entity): + def __init__(self, x, y, value=100): + # Entity expects Vector position and optional texture + super().__init__(mcrfpy.Vector(x, y)) + self.value = value + self.collected = False + + def collect(self): + if not self.collected: + self.collected = True + return self.value + return 0 + +def test_type_preservation(): + """Comprehensive test of type preservation in UIEntityCollection""" + print("=== Testing UIEntityCollection Type Preservation (Issue #76) ===\n") + + # Create a grid to hold entities + grid = mcrfpy.Grid(30, 30) + grid.x = 10 + grid.y = 10 + grid.w = 600 + grid.h = 600 + + # Add grid to scene + scene_ui = mcrfpy.sceneUI("test") + scene_ui.append(grid) + + # Create various entity instances + player = Player(5, 5) + enemy1 = Enemy(10, 10, "orc") + enemy2 = Enemy(15, 15, "skeleton") + treasure = Treasure(20, 20, 500) + base_entity = mcrfpy.Entity(mcrfpy.Vector(25, 25)) + + print("Created entities:") + print(f" - Player at (5,5): type={type(player).__name__}, health={player.health}") + print(f" - Enemy at (10,10): type={type(enemy1).__name__}, enemy_type={enemy1.enemy_type}") + print(f" - Enemy at (15,15): type={type(enemy2).__name__}, enemy_type={enemy2.enemy_type}") + print(f" - Treasure at (20,20): type={type(treasure).__name__}, value={treasure.value}") + print(f" - Base Entity at (25,25): type={type(base_entity).__name__}") + + # Store original references + original_refs = { + 'player': player, + 'enemy1': enemy1, + 'enemy2': enemy2, + 'treasure': treasure, + 'base_entity': base_entity + } + + # Add entities to grid + grid.entities.append(player) + grid.entities.append(enemy1) + grid.entities.append(enemy2) + grid.entities.append(treasure) + grid.entities.append(base_entity) + + print(f"\nAdded {len(grid.entities)} entities to grid") + + # Test 1: Direct indexing + print("\n--- Test 1: Direct Indexing ---") + retrieved_entities = [] + for i in range(len(grid.entities)): + entity = grid.entities[i] + retrieved_entities.append(entity) + print(f"grid.entities[{i}]: type={type(entity).__name__}, id={id(entity)}") + + # Test 2: Check type preservation + print("\n--- Test 2: Type Preservation Check ---") + r_player = grid.entities[0] + r_enemy1 = grid.entities[1] + r_treasure = grid.entities[3] + + # Check types + tests_passed = 0 + tests_total = 0 + + tests_total += 1 + if type(r_player).__name__ == "Player": + print("✓ PASS: Player type preserved") + tests_passed += 1 + else: + print(f"✗ FAIL: Player type lost! Got {type(r_player).__name__} instead of Player") + print(" This is the core Issue #76 bug!") + + tests_total += 1 + if type(r_enemy1).__name__ == "Enemy": + print("✓ PASS: Enemy type preserved") + tests_passed += 1 + else: + print(f"✗ FAIL: Enemy type lost! Got {type(r_enemy1).__name__} instead of Enemy") + + tests_total += 1 + if type(r_treasure).__name__ == "Treasure": + print("✓ PASS: Treasure type preserved") + tests_passed += 1 + else: + print(f"✗ FAIL: Treasure type lost! Got {type(r_treasure).__name__} instead of Treasure") + + # Test 3: Check attribute preservation + print("\n--- Test 3: Attribute Preservation ---") + + # Test Player attributes + try: + tests_total += 1 + health = r_player.health + inv = r_player.inventory + pid = r_player.player_id + print(f"✓ PASS: Player attributes accessible: health={health}, inventory={inv}, id={pid}") + tests_passed += 1 + except AttributeError as e: + print(f"✗ FAIL: Player attributes lost: {e}") + + # Test Enemy attributes + try: + tests_total += 1 + etype = r_enemy1.enemy_type + aggr = r_enemy1.aggression + print(f"✓ PASS: Enemy attributes accessible: type={etype}, aggression={aggr}") + tests_passed += 1 + except AttributeError as e: + print(f"✗ FAIL: Enemy attributes lost: {e}") + + # Test 4: Method preservation + print("\n--- Test 4: Method Preservation ---") + + try: + tests_total += 1 + r_player.take_damage(10) + print(f"✓ PASS: Player method callable, health now: {r_player.health}") + tests_passed += 1 + except AttributeError as e: + print(f"✗ FAIL: Player methods lost: {e}") + + try: + tests_total += 1 + next_move = r_enemy1.get_next_move() + print(f"✓ PASS: Enemy method callable, next move: {next_move}") + tests_passed += 1 + except AttributeError as e: + print(f"✗ FAIL: Enemy methods lost: {e}") + + # Test 5: Iteration + print("\n--- Test 5: Iteration Test ---") + try: + tests_total += 1 + type_list = [] + for entity in grid.entities: + type_list.append(type(entity).__name__) + print(f"Types during iteration: {type_list}") + if type_list == ["Player", "Enemy", "Enemy", "Treasure", "Entity"]: + print("✓ PASS: All types preserved during iteration") + tests_passed += 1 + else: + print("✗ FAIL: Types lost during iteration") + except Exception as e: + print(f"✗ FAIL: Iteration error: {e}") + + # Test 6: Identity check + print("\n--- Test 6: Object Identity ---") + tests_total += 1 + if r_player is original_refs['player']: + print("✓ PASS: Retrieved object is the same Python object") + tests_passed += 1 + else: + print("✗ FAIL: Retrieved object is a different instance") + print(f" Original id: {id(original_refs['player'])}") + print(f" Retrieved id: {id(r_player)}") + + # Test 7: Modification persistence + print("\n--- Test 7: Modification Persistence ---") + tests_total += 1 + r_player.x = 50 + r_player.y = 50 + + # Retrieve again + r_player2 = grid.entities[0] + if r_player2.x == 50 and r_player2.y == 50: + print("✓ PASS: Modifications persist across retrievals") + tests_passed += 1 + else: + print(f"✗ FAIL: Modifications lost: position is ({r_player2.x}, {r_player2.y})") + + # Take screenshot + automation.screenshot("/tmp/issue_76_test.png") + + # Summary + print(f"\n=== SUMMARY ===") + print(f"Tests passed: {tests_passed}/{tests_total}") + + if tests_passed < tests_total: + print("\nIssue #76: The C++ implementation creates new PyUIEntityObject instances") + print("with type 'Entity' instead of preserving the original Python type.") + print("This causes derived classes to lose their type, attributes, and methods.") + print("\nThe fix requires storing and restoring the original Python type") + print("when creating objects in UIEntityCollection::getitem.") + + return tests_passed == tests_total + +def run_test(runtime): + """Timer callback to run the test""" + try: + success = test_type_preservation() + print("\nOverall result: " + ("PASS" if success else "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_79_color_properties_test.py b/tests/issue_79_color_properties_test.py new file mode 100644 index 0000000..05233b2 --- /dev/null +++ b/tests/issue_79_color_properties_test.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +""" +Test for Issue #79: Color r, g, b, a properties return None + +This test verifies that Color object properties (r, g, b, a) work correctly. +""" + +import mcrfpy +import sys + +def test_color_properties(): + """Test Color r, g, b, a property access and modification""" + print("=== Testing Color r, g, b, a Properties (Issue #79) ===\n") + + tests_passed = 0 + tests_total = 0 + + # Test 1: Create color and check properties + print("--- Test 1: Basic property access ---") + color1 = mcrfpy.Color(255, 128, 64, 32) + + tests_total += 1 + if color1.r == 255: + print("✓ PASS: color.r returns correct value (255)") + tests_passed += 1 + else: + print(f"✗ FAIL: color.r returned {color1.r} instead of 255") + + tests_total += 1 + if color1.g == 128: + print("✓ PASS: color.g returns correct value (128)") + tests_passed += 1 + else: + print(f"✗ FAIL: color.g returned {color1.g} instead of 128") + + tests_total += 1 + if color1.b == 64: + print("✓ PASS: color.b returns correct value (64)") + tests_passed += 1 + else: + print(f"✗ FAIL: color.b returned {color1.b} instead of 64") + + tests_total += 1 + if color1.a == 32: + print("✓ PASS: color.a returns correct value (32)") + tests_passed += 1 + else: + print(f"✗ FAIL: color.a returned {color1.a} instead of 32") + + # Test 2: Modify properties + print("\n--- Test 2: Property modification ---") + color1.r = 200 + color1.g = 100 + color1.b = 50 + color1.a = 25 + + tests_total += 1 + if color1.r == 200: + print("✓ PASS: color.r set successfully") + tests_passed += 1 + else: + print(f"✗ FAIL: color.r is {color1.r} after setting to 200") + + tests_total += 1 + if color1.g == 100: + print("✓ PASS: color.g set successfully") + tests_passed += 1 + else: + print(f"✗ FAIL: color.g is {color1.g} after setting to 100") + + tests_total += 1 + if color1.b == 50: + print("✓ PASS: color.b set successfully") + tests_passed += 1 + else: + print(f"✗ FAIL: color.b is {color1.b} after setting to 50") + + tests_total += 1 + if color1.a == 25: + print("✓ PASS: color.a set successfully") + tests_passed += 1 + else: + print(f"✗ FAIL: color.a is {color1.a} after setting to 25") + + # Test 3: Boundary values + print("\n--- Test 3: Boundary value tests ---") + color2 = mcrfpy.Color(0, 0, 0, 0) + + tests_total += 1 + if color2.r == 0 and color2.g == 0 and color2.b == 0 and color2.a == 0: + print("✓ PASS: Minimum values (0) work correctly") + tests_passed += 1 + else: + print("✗ FAIL: Minimum values not working") + + color3 = mcrfpy.Color(255, 255, 255, 255) + tests_total += 1 + if color3.r == 255 and color3.g == 255 and color3.b == 255 and color3.a == 255: + print("✓ PASS: Maximum values (255) work correctly") + tests_passed += 1 + else: + print("✗ FAIL: Maximum values not working") + + # Test 4: Invalid value handling + print("\n--- Test 4: Invalid value handling ---") + tests_total += 1 + try: + color3.r = 256 # Out of range + print("✗ FAIL: Should have raised ValueError for value > 255") + except ValueError as e: + print(f"✓ PASS: Correctly raised ValueError: {e}") + tests_passed += 1 + + tests_total += 1 + try: + color3.g = -1 # Out of range + print("✗ FAIL: Should have raised ValueError for value < 0") + except ValueError as e: + print(f"✓ PASS: Correctly raised ValueError: {e}") + tests_passed += 1 + + tests_total += 1 + try: + color3.b = "red" # Wrong type + print("✗ FAIL: Should have raised TypeError for string value") + except TypeError as e: + print(f"✓ PASS: Correctly raised TypeError: {e}") + tests_passed += 1 + + # Test 5: Verify __repr__ shows correct values + print("\n--- Test 5: String representation ---") + color4 = mcrfpy.Color(10, 20, 30, 40) + repr_str = repr(color4) + tests_total += 1 + if "(10, 20, 30, 40)" in repr_str: + print(f"✓ PASS: __repr__ shows correct values: {repr_str}") + tests_passed += 1 + else: + print(f"✗ FAIL: __repr__ incorrect: {repr_str}") + + # Summary + print(f"\n=== SUMMARY ===") + print(f"Tests passed: {tests_passed}/{tests_total}") + + if tests_passed == tests_total: + print("\nIssue #79 FIXED: Color properties now work correctly!") + else: + print("\nIssue #79: Some tests failed") + + return tests_passed == tests_total + +def run_test(runtime): + """Timer callback to run the test""" + try: + success = test_color_properties() + print("\nOverall result: " + ("PASS" if success else "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_9_minimal_test.py b/tests/issue_9_minimal_test.py new file mode 100644 index 0000000..09eb9c6 --- /dev/null +++ b/tests/issue_9_minimal_test.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +Minimal test for Issue #9: RenderTexture resize +""" + +import mcrfpy +from mcrfpy import automation +import sys + +def run_test(runtime): + """Test RenderTexture resizing""" + print("Testing Issue #9: RenderTexture resize (minimal)") + + try: + # Create a grid + print("Creating grid...") + grid = mcrfpy.Grid(30, 30) + grid.x = 10 + grid.y = 10 + grid.w = 300 + grid.h = 300 + + # Add to scene + scene_ui = mcrfpy.sceneUI("test") + scene_ui.append(grid) + + # Test accessing grid points + print("Testing grid.at()...") + point = grid.at(5, 5) + print(f"Got grid point: {point}") + + # Test color creation + print("Testing Color creation...") + red = mcrfpy.Color(255, 0, 0, 255) + print(f"Created color: {red}") + + # Set color + print("Setting grid point color...") + point.color = red + + print("Taking screenshot before resize...") + automation.screenshot("/tmp/issue_9_minimal_before.png") + + # Resize grid + print("Resizing grid to 2500x2500...") + grid.w = 2500 + grid.h = 2500 + + print("Taking screenshot after resize...") + automation.screenshot("/tmp/issue_9_minimal_after.png") + + print("\nTest complete - check screenshots") + print("If RenderTexture is recreated properly, grid should render correctly at large size") + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + + sys.exit(0) + +# Create and set scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/issue_9_rendertexture_resize_test.py b/tests/issue_9_rendertexture_resize_test.py new file mode 100644 index 0000000..8d643b5 --- /dev/null +++ b/tests/issue_9_rendertexture_resize_test.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +Comprehensive test for Issue #9: Recreate RenderTexture when UIGrid is resized + +This test demonstrates that UIGrid has a hardcoded RenderTexture size of 1920x1080, +which causes rendering issues when the grid is resized beyond these dimensions. + +The bug: UIGrid::render() creates a RenderTexture with fixed size (1920x1080) once, +but never recreates it when the grid is resized, causing clipping and rendering artifacts. +""" + +import mcrfpy +from mcrfpy import automation +import sys +import os + +def create_checkerboard_pattern(grid, grid_width, grid_height, cell_size=2): + """Create a checkerboard pattern on the grid for visibility""" + for x in range(grid_width): + for y in range(grid_height): + if (x // cell_size + y // cell_size) % 2 == 0: + grid.at(x, y).color = mcrfpy.Color(255, 255, 255, 255) # White + else: + grid.at(x, y).color = mcrfpy.Color(100, 100, 100, 255) # Gray + +def add_border_markers(grid, grid_width, grid_height): + """Add colored markers at the borders to test rendering limits""" + # Red border on top + for x in range(grid_width): + grid.at(x, 0).color = mcrfpy.Color(255, 0, 0, 255) + + # Green border on right + for y in range(grid_height): + grid.at(grid_width-1, y).color = mcrfpy.Color(0, 255, 0, 255) + + # Blue border on bottom + for x in range(grid_width): + grid.at(x, grid_height-1).color = mcrfpy.Color(0, 0, 255, 255) + + # Yellow border on left + for y in range(grid_height): + grid.at(0, y).color = mcrfpy.Color(255, 255, 0, 255) + +def test_rendertexture_resize(): + """Test RenderTexture behavior with various grid sizes""" + print("=== Testing UIGrid RenderTexture Resize (Issue #9) ===\n") + + scene_ui = mcrfpy.sceneUI("test") + + # Test 1: Small grid (should work fine) + print("--- Test 1: Small Grid (400x300) ---") + grid1 = mcrfpy.Grid(20, 15) # 20x15 tiles + grid1.x = 10 + grid1.y = 10 + grid1.w = 400 + grid1.h = 300 + scene_ui.append(grid1) + + create_checkerboard_pattern(grid1, 20, 15) + add_border_markers(grid1, 20, 15) + + automation.screenshot("/tmp/issue_9_small_grid.png") + print("✓ Small grid created and rendered") + + # Test 2: Medium grid at 1920x1080 limit + print("\n--- Test 2: Medium Grid at 1920x1080 Limit ---") + grid2 = mcrfpy.Grid(64, 36) # 64x36 tiles at 30px each = 1920x1080 + grid2.x = 10 + grid2.y = 320 + grid2.w = 1920 + grid2.h = 1080 + scene_ui.append(grid2) + + create_checkerboard_pattern(grid2, 64, 36, 4) + add_border_markers(grid2, 64, 36) + + automation.screenshot("/tmp/issue_9_limit_grid.png") + print("✓ Grid at RenderTexture limit created") + + # Test 3: Resize grid1 beyond limits + print("\n--- Test 3: Resizing Small Grid Beyond 1920x1080 ---") + print("Original size: 400x300") + grid1.w = 2400 + grid1.h = 1400 + print(f"Resized to: {grid1.w}x{grid1.h}") + + # The content should still be visible but may be clipped + automation.screenshot("/tmp/issue_9_resized_beyond_limit.png") + print("✗ EXPECTED ISSUE: Grid resized beyond RenderTexture limits") + print(" Content beyond 1920x1080 will be clipped!") + + # Test 4: Create large grid from start + print("\n--- Test 4: Large Grid from Start (2400x1400) ---") + # Clear previous grids + while len(scene_ui) > 0: + scene_ui.remove(0) + + grid3 = mcrfpy.Grid(80, 50) # Large tile count + grid3.x = 10 + grid3.y = 10 + grid3.w = 2400 + grid3.h = 1400 + scene_ui.append(grid3) + + create_checkerboard_pattern(grid3, 80, 50, 5) + add_border_markers(grid3, 80, 50) + + # Add markers at specific positions to test rendering + # Mark the center + center_x, center_y = 40, 25 + for dx in range(-2, 3): + for dy in range(-2, 3): + grid3.at(center_x + dx, center_y + dy).color = mcrfpy.Color(255, 0, 255, 255) # Magenta + + # Mark position at 1920 pixel boundary (64 tiles * 30 pixels/tile = 1920) + if 64 < 80: # Only if within grid bounds + for y in range(min(50, 10)): + grid3.at(64, y).color = mcrfpy.Color(255, 128, 0, 255) # Orange + + automation.screenshot("/tmp/issue_9_large_grid.png") + print("✗ EXPECTED ISSUE: Large grid created") + print(" Content beyond 1920x1080 will not render!") + print(" Look for missing orange line at x=1920 boundary") + + # Test 5: Dynamic resize test + print("\n--- Test 5: Dynamic Resize Test ---") + scene_ui.remove(0) + + grid4 = mcrfpy.Grid(100, 100) + grid4.x = 10 + grid4.y = 10 + scene_ui.append(grid4) + + sizes = [(500, 500), (1000, 1000), (1500, 1500), (2000, 2000), (2500, 2500)] + + for i, (w, h) in enumerate(sizes): + grid4.w = w + grid4.h = h + + # Add pattern at current size + visible_tiles_x = min(100, w // 30) + visible_tiles_y = min(100, h // 30) + + # Clear and create new pattern + for x in range(visible_tiles_x): + for y in range(visible_tiles_y): + if x == visible_tiles_x - 1 or y == visible_tiles_y - 1: + # Edge markers + grid4.at(x, y).color = mcrfpy.Color(255, 255, 0, 255) + elif (x + y) % 10 == 0: + # Diagonal lines + grid4.at(x, y).color = mcrfpy.Color(0, 255, 255, 255) + + automation.screenshot(f"/tmp/issue_9_resize_{w}x{h}.png") + + if w > 1920 or h > 1080: + print(f"✗ Size {w}x{h}: Content clipped at 1920x1080") + else: + print(f"✓ Size {w}x{h}: Rendered correctly") + + # Test 6: Verify exact clipping boundary + print("\n--- Test 6: Exact Clipping Boundary Test ---") + scene_ui.remove(0) + + grid5 = mcrfpy.Grid(70, 40) + grid5.x = 0 + grid5.y = 0 + grid5.w = 2100 # 70 * 30 = 2100 pixels + grid5.h = 1200 # 40 * 30 = 1200 pixels + scene_ui.append(grid5) + + # Create a pattern that shows the boundary clearly + for x in range(70): + for y in range(40): + pixel_x = x * 30 + pixel_y = y * 30 + + if pixel_x == 1920 - 30: # Last tile before boundary + grid5.at(x, y).color = mcrfpy.Color(255, 0, 0, 255) # Red + elif pixel_x == 1920: # First tile after boundary + grid5.at(x, y).color = mcrfpy.Color(0, 255, 0, 255) # Green + elif pixel_y == 1080 - 30: # Last row before boundary + grid5.at(x, y).color = mcrfpy.Color(0, 0, 255, 255) # Blue + elif pixel_y == 1080: # First row after boundary + grid5.at(x, y).color = mcrfpy.Color(255, 255, 0, 255) # Yellow + else: + # Normal checkerboard + if (x + y) % 2 == 0: + grid5.at(x, y).color = mcrfpy.Color(200, 200, 200, 255) + + automation.screenshot("/tmp/issue_9_boundary_test.png") + print("Screenshot saved showing clipping boundary") + print("- Red tiles: Last visible column (x=1890-1919)") + print("- Green tiles: First clipped column (x=1920+)") + print("- Blue tiles: Last visible row (y=1050-1079)") + print("- Yellow tiles: First clipped row (y=1080+)") + + # Summary + print("\n=== SUMMARY ===") + print("Issue #9: UIGrid uses a hardcoded RenderTexture size of 1920x1080") + print("Problems demonstrated:") + print("1. Grids larger than 1920x1080 are clipped") + print("2. Resizing grids doesn't recreate the RenderTexture") + print("3. Content beyond the boundary is not rendered") + print("\nThe fix should:") + print("1. Recreate RenderTexture when grid size changes") + print("2. Use the actual grid dimensions instead of hardcoded values") + print("3. Consider memory limits for very large grids") + + print(f"\nScreenshots saved to /tmp/issue_9_*.png") + +def run_test(runtime): + """Timer callback to run the test""" + try: + test_rendertexture_resize() + print("\nTest complete - check screenshots for visual verification") + except Exception as e: + print(f"\nTest error: {e}") + import traceback + traceback.print_exc() + + 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_9_simple_test.py b/tests/issue_9_simple_test.py new file mode 100644 index 0000000..2db3806 --- /dev/null +++ b/tests/issue_9_simple_test.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +""" +Simple test for Issue #9: RenderTexture resize +""" + +import mcrfpy +from mcrfpy import automation +import sys + +def run_test(runtime): + """Test RenderTexture resizing""" + print("Testing Issue #9: RenderTexture resize") + + # Create a scene + scene_ui = mcrfpy.sceneUI("test") + + # Create a small grid + print("Creating 50x50 grid with initial size 500x500") + grid = mcrfpy.Grid(50, 50) + grid.x = 10 + grid.y = 10 + grid.w = 500 + grid.h = 500 + scene_ui.append(grid) + + # Color some tiles to make it visible + print("Coloring tiles...") + for i in range(50): + # Diagonal line + grid.at(i, i).color = mcrfpy.Color(255, 0, 0, 255) + # Borders + grid.at(i, 0).color = mcrfpy.Color(0, 255, 0, 255) + grid.at(0, i).color = mcrfpy.Color(0, 0, 255, 255) + grid.at(i, 49).color = mcrfpy.Color(255, 255, 0, 255) + grid.at(49, i).color = mcrfpy.Color(255, 0, 255, 255) + + # Take initial screenshot + automation.screenshot("/tmp/issue_9_before_resize.png") + print("Screenshot saved: /tmp/issue_9_before_resize.png") + + # Resize to larger than 1920x1080 + print("\nResizing grid to 2500x2500...") + grid.w = 2500 + grid.h = 2500 + + # Take screenshot after resize + automation.screenshot("/tmp/issue_9_after_resize.png") + print("Screenshot saved: /tmp/issue_9_after_resize.png") + + # Test individual dimension changes + print("\nTesting individual dimension changes...") + grid.w = 3000 + automation.screenshot("/tmp/issue_9_width_3000.png") + print("Width set to 3000, screenshot: /tmp/issue_9_width_3000.png") + + grid.h = 3000 + automation.screenshot("/tmp/issue_9_both_3000.png") + print("Height set to 3000, screenshot: /tmp/issue_9_both_3000.png") + + print("\nIf the RenderTexture is properly recreated, all colored tiles") + print("should be visible in all screenshots, not clipped at 1920x1080.") + + print("\nTest complete - PASS") + sys.exit(0) + +# Create and set scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/issue_9_test.py b/tests/issue_9_test.py new file mode 100644 index 0000000..39a1f22 --- /dev/null +++ b/tests/issue_9_test.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +""" +Test for Issue #9: Recreate RenderTexture when UIGrid is resized + +This test checks if resizing a UIGrid properly recreates its RenderTexture. +""" + +import mcrfpy +from mcrfpy import automation +import sys + +def run_test(runtime): + """Test that UIGrid properly handles resizing""" + try: + # Create a grid with initial size + grid = mcrfpy.Grid(20, 20) + grid.x = 50 + grid.y = 50 + grid.w = 200 + grid.h = 200 + + # Add grid to scene + scene_ui = mcrfpy.sceneUI("test") + scene_ui.append(grid) + + # Take initial screenshot + automation.screenshot("/tmp/grid_initial.png") + print("Initial grid created at 200x200") + + # Add some visible content to the grid + for x in range(5): + for y in range(5): + grid.at(x, y).color = mcrfpy.Color(255, 0, 0, 255) # Red squares + + automation.screenshot("/tmp/grid_with_content.png") + print("Added red squares to grid") + + # Test 1: Resize the grid smaller + print("\nTest 1: Resizing grid to 100x100...") + grid.w = 100 + grid.h = 100 + + automation.screenshot("/tmp/grid_resized_small.png") + + # The grid should still render correctly + print("✓ Test 1: Grid resized to 100x100") + + # Test 2: Resize the grid larger than initial + print("\nTest 2: Resizing grid to 400x400...") + grid.w = 400 + grid.h = 400 + + automation.screenshot("/tmp/grid_resized_large.png") + + # Add content at the edges to test if render texture is big enough + for x in range(15, 20): + for y in range(15, 20): + grid.at(x, y).color = mcrfpy.Color(0, 255, 0, 255) # Green squares + + automation.screenshot("/tmp/grid_resized_with_edge_content.png") + print("✓ Test 2: Grid resized to 400x400 with edge content") + + # Test 3: Resize beyond the hardcoded 1920x1080 limit + print("\nTest 3: Resizing grid beyond 1920x1080...") + grid.w = 2000 + grid.h = 1200 + + automation.screenshot("/tmp/grid_resized_huge.png") + + # This should fail with the current implementation + print("✗ Test 3: This likely shows rendering errors due to fixed RenderTexture size") + print("This is the bug described in Issue #9!") + + print("\nScreenshots saved to /tmp/grid_*.png") + print("Check grid_resized_huge.png for rendering artifacts") + + except Exception as e: + print(f"Test error: {e}") + import traceback + traceback.print_exc() + + 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/run_issue_tests.py b/tests/run_issue_tests.py new file mode 100755 index 0000000..b8ea601 --- /dev/null +++ b/tests/run_issue_tests.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Test runner for high-priority McRogueFace issues + +This script runs comprehensive tests for the highest priority bugs that can be fixed rapidly. +Each test is designed to fail initially (demonstrating the bug) and pass after the fix. +""" + +import os +import sys +import subprocess +import time + +# Test configurations +TESTS = [ + { + "issue": "37", + "name": "Windows scripts subdirectory bug", + "script": "issue_37_windows_scripts_comprehensive_test.py", + "needs_game_loop": False, + "description": "Tests script loading from different working directories" + }, + { + "issue": "76", + "name": "UIEntityCollection returns wrong type", + "script": "issue_76_uientitycollection_type_test.py", + "needs_game_loop": True, + "description": "Tests type preservation for derived Entity classes in collections" + }, + { + "issue": "9", + "name": "RenderTexture resize bug", + "script": "issue_9_rendertexture_resize_test.py", + "needs_game_loop": True, + "description": "Tests UIGrid rendering with sizes beyond 1920x1080" + }, + { + "issue": "26/28", + "name": "Iterator implementation for collections", + "script": "issue_26_28_iterator_comprehensive_test.py", + "needs_game_loop": True, + "description": "Tests Python sequence protocol for UI collections" + } +] + +def run_test(test_config, mcrogueface_path): + """Run a single test and return the result""" + script_path = os.path.join(os.path.dirname(__file__), test_config["script"]) + + if not os.path.exists(script_path): + return f"SKIP - Test script not found: {script_path}" + + print(f"\n{'='*60}") + print(f"Running test for Issue #{test_config['issue']}: {test_config['name']}") + print(f"Description: {test_config['description']}") + print(f"Script: {test_config['script']}") + print(f"{'='*60}\n") + + if test_config["needs_game_loop"]: + # Run with game loop using --exec + cmd = [mcrogueface_path, "--headless", "--exec", script_path] + else: + # Run directly as Python script + cmd = [sys.executable, script_path] + + try: + start_time = time.time() + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30 # 30 second timeout + ) + elapsed = time.time() - start_time + + # Check for pass/fail in output + output = result.stdout + result.stderr + + if "PASS" in output and "FAIL" not in output: + status = "PASS" + elif "FAIL" in output: + status = "FAIL" + else: + status = "UNKNOWN" + + # Look for specific bug indicators + bug_found = False + if test_config["issue"] == "37" and "Script not loaded from different directory" in output: + bug_found = True + elif test_config["issue"] == "76" and "type lost!" in output: + bug_found = True + elif test_config["issue"] == "9" and "clipped at 1920x1080" in output: + bug_found = True + elif test_config["issue"] == "26/28" and "not implemented" in output: + bug_found = True + + return { + "status": status, + "bug_found": bug_found, + "elapsed": elapsed, + "output": output if len(output) < 1000 else output[:1000] + "\n... (truncated)" + } + + except subprocess.TimeoutExpired: + return { + "status": "TIMEOUT", + "bug_found": False, + "elapsed": 30, + "output": "Test timed out after 30 seconds" + } + except Exception as e: + return { + "status": "ERROR", + "bug_found": False, + "elapsed": 0, + "output": str(e) + } + +def main(): + """Run all tests and provide summary""" + # Find mcrogueface executable + build_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "build") + mcrogueface_path = os.path.join(build_dir, "mcrogueface") + + if not os.path.exists(mcrogueface_path): + print(f"ERROR: mcrogueface executable not found at {mcrogueface_path}") + print("Please build the project first with 'make'") + return 1 + + print("McRogueFace Issue Test Suite") + print(f"Executable: {mcrogueface_path}") + print(f"Running {len(TESTS)} tests...\n") + + results = [] + + for test in TESTS: + result = run_test(test, mcrogueface_path) + results.append((test, result)) + + # Summary + print(f"\n{'='*60}") + print("TEST SUMMARY") + print(f"{'='*60}\n") + + bugs_found = 0 + tests_passed = 0 + + for test, result in results: + if isinstance(result, str): + print(f"Issue #{test['issue']}: {result}") + else: + status_str = result['status'] + if result['bug_found']: + status_str += " (BUG CONFIRMED)" + bugs_found += 1 + elif result['status'] == 'PASS': + tests_passed += 1 + + print(f"Issue #{test['issue']}: {status_str} ({result['elapsed']:.2f}s)") + + if result['status'] not in ['PASS', 'UNKNOWN']: + print(f" Details: {result['output'].splitlines()[0] if result['output'] else 'No output'}") + + print(f"\nBugs confirmed: {bugs_found}/{len(TESTS)}") + print(f"Tests passed: {tests_passed}/{len(TESTS)}") + + if bugs_found > 0: + print("\nThese tests demonstrate bugs that need fixing.") + print("After fixing, the tests should pass instead of confirming bugs.") + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/timer_success_1086.png b/timer_success_1086.png new file mode 100644 index 0000000..a09f8d5 Binary files /dev/null and b/timer_success_1086.png differ diff --git a/validate_screenshot_basic_20250703_174532.png b/validate_screenshot_basic_20250703_174532.png new file mode 100644 index 0000000..a61c929 Binary files /dev/null and b/validate_screenshot_basic_20250703_174532.png differ diff --git a/validate_screenshot_final_20250703_174532.png b/validate_screenshot_final_20250703_174532.png new file mode 100644 index 0000000..a61c929 Binary files /dev/null and b/validate_screenshot_final_20250703_174532.png differ diff --git a/validate_screenshot_with_spaces 20250703_174532.png b/validate_screenshot_with_spaces 20250703_174532.png new file mode 100644 index 0000000..a61c929 Binary files /dev/null and b/validate_screenshot_with_spaces 20250703_174532.png differ