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/_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/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/UIGrid.cpp b/src/UIGrid.cpp index 2a12531..e2ae8e5 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -475,13 +475,75 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) { return (PyObject*)obj; } -PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o) +PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - int x, y; - if (!PyArg_ParseTuple(o, "ii", &x, &y)) { - PyErr_SetString(PyExc_TypeError, "UIGrid.at requires two integer arguments: (x, y)"); + static const char* keywords[] = { "x", "y", "pos", nullptr }; + int x = -1, y = -1; + PyObject* pos = nullptr; + + // Try to parse with keywords first + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiO", const_cast(keywords), &x, &y, &pos)) { + PyErr_Clear(); // Clear the error and try different parsing + + // Check if we have a single tuple argument (x, y) + if (PyTuple_Size(args) == 1 && kwds == nullptr) { + PyObject* arg = PyTuple_GetItem(args, 0); + if (PyTuple_Check(arg) && PyTuple_Size(arg) == 2) { + // It's a tuple, extract x and y + PyObject* x_obj = PyTuple_GetItem(arg, 0); + PyObject* y_obj = PyTuple_GetItem(arg, 1); + if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { + x = PyLong_AsLong(x_obj); + y = PyLong_AsLong(y_obj); + } else { + PyErr_SetString(PyExc_TypeError, "Tuple elements must be integers"); + return NULL; + } + } else { + PyErr_SetString(PyExc_TypeError, "UIGrid.at accepts: (x, y), x, y, x=x, y=y, or pos=(x,y)"); + return NULL; + } + } else if (PyTuple_Size(args) == 2 && kwds == nullptr) { + // Two positional arguments + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + PyErr_SetString(PyExc_TypeError, "Arguments must be integers"); + return NULL; + } + } else { + PyErr_SetString(PyExc_TypeError, "UIGrid.at accepts: (x, y), x, y, x=x, y=y, or pos=(x,y)"); + return NULL; + } + } + + // Handle pos keyword argument + if (pos != nullptr) { + if (x != -1 || y != -1) { + PyErr_SetString(PyExc_TypeError, "Cannot specify both pos and x/y arguments"); + return NULL; + } + if (PyTuple_Check(pos) && PyTuple_Size(pos) == 2) { + PyObject* x_obj = PyTuple_GetItem(pos, 0); + PyObject* y_obj = PyTuple_GetItem(pos, 1); + if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { + x = PyLong_AsLong(x_obj); + y = PyLong_AsLong(y_obj); + } else { + PyErr_SetString(PyExc_TypeError, "pos tuple elements must be integers"); + return NULL; + } + } else { + PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two integers"); + return NULL; + } + } + + // Validate we have both x and y + if (x == -1 || y == -1) { + PyErr_SetString(PyExc_TypeError, "UIGrid.at requires both x and y coordinates"); return NULL; } + + // Range validation if (x < 0 || x >= self->data->grid_x) { PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_x)"); return NULL; @@ -501,7 +563,7 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o) } PyMethodDef UIGrid::methods[] = { - {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS}, + {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {NULL, NULL, 0, NULL} }; @@ -840,184 +902,6 @@ PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, P return (PyObject*)self; } -int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) { - auto list = self->data.get(); - if (!list) { - PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); - return -1; - } - - // Handle negative indexing - while (index < 0) index += list->size(); - - // Bounds check - if (index >= list->size()) { - PyErr_SetString(PyExc_IndexError, "EntityCollection assignment index out of range"); - return -1; - } - - // Get iterator to the target position - auto it = list->begin(); - std::advance(it, index); - - // Handle deletion - if (value == NULL) { - // Clear grid reference from the entity being removed - (*it)->grid = nullptr; - list->erase(it); - return 0; - } - - // Type checking - must be an Entity - if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { - PyErr_SetString(PyExc_TypeError, "EntityCollection can only contain Entity objects"); - return -1; - } - - // Get the C++ object from the Python object - PyUIEntityObject* entity = (PyUIEntityObject*)value; - if (!entity->data) { - PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); - return -1; - } - - // Clear grid reference from the old entity - (*it)->grid = nullptr; - - // Replace the element and set grid reference - *it = entity->data; - entity->data->grid = self->grid; - - return 0; -} - -int UIEntityCollection::contains(PyUIEntityCollectionObject* self, PyObject* value) { - auto list = self->data.get(); - if (!list) { - PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); - return -1; - } - - // Type checking - must be an Entity - if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { - // Not an Entity, so it can't be in the collection - return 0; - } - - // Get the C++ object from the Python object - PyUIEntityObject* entity = (PyUIEntityObject*)value; - if (!entity->data) { - return 0; - } - - // Search for the object by comparing C++ pointers - for (const auto& ent : *list) { - if (ent.get() == entity->data.get()) { - return 1; // Found - } - } - - return 0; // Not found -} - -PyObject* UIEntityCollection::concat(PyUIEntityCollectionObject* self, PyObject* other) { - // Create a new Python list containing elements from both collections - if (!PySequence_Check(other)) { - PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection"); - return NULL; - } - - Py_ssize_t self_len = self->data->size(); - Py_ssize_t other_len = PySequence_Length(other); - if (other_len == -1) { - return NULL; // Error already set - } - - PyObject* result_list = PyList_New(self_len + other_len); - if (!result_list) { - return NULL; - } - - // Add all elements from self - Py_ssize_t idx = 0; - for (const auto& entity : *self->data) { - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); - auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0); - if (obj) { - obj->data = entity; - PyList_SET_ITEM(result_list, idx, (PyObject*)obj); // Steals reference - } else { - Py_DECREF(result_list); - Py_DECREF(type); - return NULL; - } - Py_DECREF(type); - idx++; - } - - // Add all elements from other - for (Py_ssize_t i = 0; i < other_len; i++) { - PyObject* item = PySequence_GetItem(other, i); - if (!item) { - Py_DECREF(result_list); - return NULL; - } - PyList_SET_ITEM(result_list, self_len + i, item); // Steals reference - } - - return result_list; -} - -PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, PyObject* other) { - if (!PySequence_Check(other)) { - PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection"); - return NULL; - } - - // First, validate ALL items in the sequence before modifying anything - Py_ssize_t other_len = PySequence_Length(other); - if (other_len == -1) { - return NULL; // Error already set - } - - // Validate all items first - for (Py_ssize_t i = 0; i < other_len; i++) { - PyObject* item = PySequence_GetItem(other, i); - if (!item) { - return NULL; - } - - // Type check - if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { - Py_DECREF(item); - PyErr_Format(PyExc_TypeError, - "EntityCollection can only contain Entity objects; " - "got %s at index %zd", Py_TYPE(item)->tp_name, i); - return NULL; - } - Py_DECREF(item); - } - - // All items validated, now we can safely add them - for (Py_ssize_t i = 0; i < other_len; i++) { - PyObject* item = PySequence_GetItem(other, i); - if (!item) { - return NULL; // Shouldn't happen, but be safe - } - - // Use the existing append method which handles grid references - PyObject* result = append(self, item); - Py_DECREF(item); - - if (!result) { - return NULL; // append() failed - } - Py_DECREF(result); // append returns Py_None - } - - Py_INCREF(self); - return (PyObject*)self; -} PySequenceMethods UIEntityCollection::sqmethods = { .sq_length = (lenfunc)UIEntityCollection::len, diff --git a/src/UIGrid.h b/src/UIGrid.h index a167c0b..28aa174 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -65,7 +65,7 @@ public: static PyObject* get_float_member(PyUIGridObject* self, void* closure); static int set_float_member(PyUIGridObject* self, PyObject* value, void* closure); static PyObject* get_texture(PyUIGridObject* self, void* closure); - static PyObject* py_at(PyUIGridObject* self, PyObject* o); + static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyMethodDef methods[]; static PyGetSetDef getsetters[]; static PyObject* get_children(PyUIGridObject* self, void* closure); diff --git a/tests/grid_at_argument_test.py b/tests/grid_at_argument_test.py new file mode 100644 index 0000000..14e9485 --- /dev/null +++ b/tests/grid_at_argument_test.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Test Grid.at() method with various argument formats""" + +import mcrfpy +import sys + +def test_grid_at_arguments(): + """Test that Grid.at() accepts all required argument formats""" + print("Testing Grid.at() argument formats...") + + # Create a test scene + mcrfpy.createScene("test") + + # Create a grid + grid = mcrfpy.Grid(10, 10) + ui = mcrfpy.sceneUI("test") + ui.append(grid) + + success_count = 0 + total_tests = 4 + + # Test 1: Two positional arguments (x, y) + try: + point1 = grid.at(5, 5) + print("✓ Test 1 PASSED: grid.at(5, 5)") + success_count += 1 + except Exception as e: + print(f"✗ Test 1 FAILED: grid.at(5, 5) - {e}") + + # Test 2: Single tuple argument (x, y) + try: + point2 = grid.at((3, 3)) + print("✓ Test 2 PASSED: grid.at((3, 3))") + success_count += 1 + except Exception as e: + print(f"✗ Test 2 FAILED: grid.at((3, 3)) - {e}") + + # Test 3: Keyword arguments x=x, y=y + try: + point3 = grid.at(x=7, y=2) + print("✓ Test 3 PASSED: grid.at(x=7, y=2)") + success_count += 1 + except Exception as e: + print(f"✗ Test 3 FAILED: grid.at(x=7, y=2) - {e}") + + # Test 4: pos keyword argument pos=(x, y) + try: + point4 = grid.at(pos=(1, 8)) + print("✓ Test 4 PASSED: grid.at(pos=(1, 8))") + success_count += 1 + except Exception as e: + print(f"✗ Test 4 FAILED: grid.at(pos=(1, 8)) - {e}") + + # Test error cases + print("\nTesting error cases...") + + # Test 5: Invalid - mixing pos with x/y + try: + grid.at(x=1, pos=(2, 2)) + print("✗ Test 5 FAILED: Should have raised error for mixing pos and x/y") + except TypeError as e: + print(f"✓ Test 5 PASSED: Correctly rejected mixing pos and x/y - {e}") + + # Test 6: Invalid - out of range + try: + grid.at(15, 15) + print("✗ Test 6 FAILED: Should have raised error for out of range") + except ValueError as e: + print(f"✓ Test 6 PASSED: Correctly rejected out of range - {e}") + + # Test 7: Verify all points are valid GridPoint objects + try: + # Check that we can set walkable on all returned points + if 'point1' in locals(): + point1.walkable = True + if 'point2' in locals(): + point2.walkable = False + if 'point3' in locals(): + point3.color = mcrfpy.Color(255, 0, 0) + if 'point4' in locals(): + point4.tilesprite = 5 + print("✓ All returned GridPoint objects are valid") + except Exception as e: + print(f"✗ GridPoint objects validation failed: {e}") + + print(f"\nSummary: {success_count}/{total_tests} tests passed") + + if success_count == total_tests: + print("ALL TESTS PASSED!") + sys.exit(0) + else: + print("SOME TESTS FAILED!") + sys.exit(1) + +# Run timer callback to execute tests after render loop starts +def run_test(elapsed): + test_grid_at_arguments() + +# Set a timer to run the test +mcrfpy.setTimer("test", run_test, 100) \ 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