Compare commits

..

No commits in common. "93256b96c6551c63dd575f3cd9e888fccf654b57" and "cd0bd5468beda8edaacef937463b216a79493fc5" have entirely different histories.

88 changed files with 382 additions and 9142 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,99 +0,0 @@
#!/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)

View File

@ -1,61 +0,0 @@
#!/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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,105 +0,0 @@
#!/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)

View File

@ -1,111 +0,0 @@
#!/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)

View File

@ -1,101 +0,0 @@
#!/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)

View File

@ -1,77 +0,0 @@
#!/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)

View File

@ -1,60 +0,0 @@
#!/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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,87 +0,0 @@
#!/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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,73 +0,0 @@
#!/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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,93 +0,0 @@
# Phase 1-3 Completion Summary
## Overview
Successfully completed all tasks in Phases 1, 2, and 3 of the alpha_streamline_2 branch. This represents a major architectural improvement to McRogueFace's Python API, making it more consistent, safer, and feature-rich.
## Phase 1: Architecture Stabilization (Completed)
- ✅ #7 - Audited and fixed unsafe constructors across all UI classes
- ✅ #71 - Implemented _Drawable base class properties at C++ level
- ✅ #87 - Added visible property for show/hide functionality
- ✅ #88 - Added opacity property for transparency control
- ✅ #89 - Added get_bounds() method returning (x, y, width, height)
- ✅ #98 - Added move()/resize() methods for dynamic UI manipulation
## Phase 2: API Enhancements (Completed)
- ✅ #101 - Standardized default positions (all UI elements default to 0,0)
- ✅ #38 - Frame accepts children parameter in constructor
- ✅ #42 - All UI elements accept click handler in __init__
- ✅ #90 - Grid accepts size as tuple: Grid((20, 15))
- ✅ #19 - Sprite texture swapping via texture property
- ✅ #52 - Grid rendering skips out-of-bounds entities
## Phase 3: Game-Ready Features (Completed)
- ✅ #30 - Entity.die() method for proper cleanup
- ✅ #93 - Vector arithmetic operators (+, -, *, /, ==, bool, abs, neg)
- ✅ #94 - Color helper methods (from_hex, to_hex, lerp)
- ✅ #103 - Timer objects with pause/resume/cancel functionality
## Additional Improvements
- ✅ Standardized position arguments across all UI classes
- Created PyPositionHelper for consistent argument parsing
- All classes now accept: (x, y), pos=(x,y), x=x, y=y formats
- ✅ Fixed UTF-8 encoding configuration for Python output
- Configured PyConfig.stdio_encoding during initialization
- Resolved unicode character printing issues
## Technical Achievements
### Architecture
- Safe two-phase initialization for all Python objects
- Consistent constructor patterns across UI hierarchy
- Proper shared_ptr lifetime management
- Clean separation between C++ implementation and Python API
### API Consistency
- All UI elements follow same initialization patterns
- Position arguments work uniformly across all classes
- Properties accessible via standard Python attribute access
- Methods follow Python naming conventions
### Developer Experience
- Intuitive object construction with sensible defaults
- Flexible argument formats reduce boilerplate
- Clear error messages for invalid inputs
- Comprehensive test coverage for all features
## Impact on Game Development
### Before
```python
# Inconsistent, error-prone API
frame = mcrfpy.Frame()
frame.x = 100 # Had to set position after creation
frame.y = 50
caption = mcrfpy.Caption(mcrfpy.default_font, "Hello", 20, 20) # Different argument order
grid = mcrfpy.Grid(10, 10, 32, 32, 0, 0) # Confusing parameter order
```
### After
```python
# Clean, consistent API
frame = mcrfpy.Frame(x=100, y=50, children=[
mcrfpy.Caption("Hello", pos=(20, 20)),
mcrfpy.Sprite("icon.png", (10, 10))
])
grid = mcrfpy.Grid(size=(10, 10), pos=(0, 0))
# Advanced features
timer = mcrfpy.Timer("animation", update_frame, 16)
timer.pause() # Pause during menu
timer.resume() # Resume when gameplay continues
player.move(velocity * delta_time) # Vector math works naturally
ui_theme = mcrfpy.Color.from_hex("#2D3436")
```
## Next Steps
With Phases 1-3 complete, the codebase is ready for:
- Phase 4: Event System & Animations (advanced interactivity)
- Phase 5: Scene Management (transitions, lifecycle)
- Phase 6: Audio System (procedural generation, effects)
- Phase 7: Optimization (sprite batching, profiling)
The foundation is now solid for building sophisticated roguelike games with McRogueFace.

View File

@ -1,167 +0,0 @@
# RenderTexture Overhaul Design Document
## Overview
This document outlines the design for implementing RenderTexture support across all UIDrawable classes in McRogueFace. This is Issue #6 and represents a major architectural change to the rendering system.
## Goals
1. **Automatic Clipping**: Children rendered outside parent bounds should be clipped
2. **Off-screen Rendering**: Enable post-processing effects and complex compositing
3. **Performance**: Cache static content, only re-render when changed
4. **Backward Compatibility**: Existing code should continue to work
## Current State
### Classes Already Using RenderTexture:
- **UIGrid**: Uses a 1920x1080 RenderTexture for compositing grid view
- **SceneTransition**: Uses two 1024x768 RenderTextures for transitions
- **HeadlessRenderer**: Uses RenderTexture for headless mode
### Classes Using Direct Rendering:
- **UIFrame**: Renders box and children directly
- **UICaption**: Renders text directly
- **UISprite**: Renders sprite directly
## Design Decisions
### 1. Opt-in Architecture
Not all UIDrawables need RenderTextures. We'll use an opt-in approach:
```cpp
class UIDrawable {
protected:
// RenderTexture support (opt-in)
std::unique_ptr<sf::RenderTexture> render_texture;
sf::Sprite render_sprite;
bool use_render_texture = false;
bool render_dirty = true;
// Enable RenderTexture for this drawable
void enableRenderTexture(unsigned int width, unsigned int height);
void updateRenderTexture();
};
```
### 2. When to Use RenderTexture
RenderTextures will be enabled for:
1. **UIFrame with clipping enabled** (new property: `clip_children = true`)
2. **UIDrawables with effects** (future: shaders, blend modes)
3. **Complex composites** (many children that rarely change)
### 3. Render Flow
```
Standard Flow:
render() → render directly to target
RenderTexture Flow:
render() → if dirty → clear RT → render to RT → dirty = false
→ draw RT sprite to target
```
### 4. Dirty Flag Management
Mark as dirty when:
- Properties change (position, size, color, etc.)
- Children added/removed
- Child marked as dirty (propagate up)
- Animation frame
### 5. Size Management
RenderTexture size options:
1. **Fixed Size**: Set at creation (current UIGrid approach)
2. **Dynamic Size**: Match bounds, recreate on resize
3. **Pooled Sizes**: Use standard sizes from pool
We'll use **Dynamic Size** with lazy creation.
## Implementation Plan
### Phase 1: Base Infrastructure (This PR)
1. Add RenderTexture members to UIDrawable
2. Add `enableRenderTexture()` method
3. Implement dirty flag system
4. Add `clip_children` property to UIFrame
### Phase 2: UIFrame Implementation
1. Update UIFrame::render() to use RenderTexture when clipping
2. Test with nested frames
3. Verify clipping works correctly
### Phase 3: Performance Optimization
1. Implement texture pooling
2. Add dirty flag propagation
3. Profile and optimize
### Phase 4: Extended Features
1. Blur/glow effects using RenderTexture
2. Viewport-based rendering (#8)
3. Screenshot improvements
## API Changes
### Python API:
```python
# Enable clipping on frames
frame.clip_children = True # New property
# Future: effects
frame.blur_amount = 5.0
sprite.glow_color = Color(255, 200, 100)
```
### C++ API:
```cpp
// Enable RenderTexture
frame->enableRenderTexture(width, height);
frame->setClipChildren(true);
// Mark dirty
frame->markDirty();
```
## Performance Considerations
1. **Memory**: Each RenderTexture uses GPU memory (width * height * 4 bytes)
2. **Creation Cost**: Creating RenderTextures is expensive, use pooling
3. **Clear Cost**: Clearing large RenderTextures each frame is costly
4. **Bandwidth**: Drawing to RenderTexture then to screen doubles bandwidth
## Migration Strategy
1. All existing code continues to work (direct rendering by default)
2. Gradually enable RenderTexture for specific use cases
3. Profile before/after to ensure performance gains
4. Document best practices
## Risks and Mitigation
| Risk | Mitigation |
|------|------------|
| Performance regression | Opt-in design, profile extensively |
| Memory usage increase | Texture pooling, size limits |
| Complexity increase | Clear documentation, examples |
| Integration issues | Extensive testing with SceneTransition |
## Success Criteria
1. ✓ Frames can clip children to bounds
2. ✓ No performance regression for direct rendering
3. ✓ Scene transitions continue to work
4. ✓ Memory usage is reasonable
5. ✓ API is intuitive and documented
## Future Extensions
1. **Shader Support** (#106): RenderTextures enable post-processing shaders
2. **Particle Systems** (#107): Render particles to texture for effects
3. **Caching**: Static UI elements cached in RenderTextures
4. **Resolution Independence**: RenderTextures for DPI scaling
## Conclusion
This design provides a foundation for professional rendering capabilities while maintaining backward compatibility and performance. The opt-in approach allows gradual adoption and testing.

View File

@ -1,524 +0,0 @@
# McRogueFace - Development Roadmap
## Project Status: 🎉 ALPHA 0.1 RELEASE! 🎉
**Current State**: Alpha release achieved! All critical blockers resolved!
**Latest Update**: Moved RenderTexture (#6) to Beta - Alpha is READY! (2025-07-05)
**Branch**: interpreter_mode (ready for alpha release merge)
**Open Issues**: ~46 remaining (non-blocking quality-of-life improvements)
---
## Recent Achievements
### 2025-07-05: ALPHA 0.1 ACHIEVED! 🎊🍾
**All Alpha Blockers Resolved!**
- Z-order rendering with performance optimization (Issue #63)
- Python Sequence Protocol for collections (Issue #69)
- Comprehensive Animation System (Issue #59)
- Moved RenderTexture to Beta (not needed for Alpha)
- **McRogueFace is ready for Alpha release!**
### 2025-07-05: Z-order Rendering Complete! 🎉
**Issue #63 Resolved**: Consistent z-order rendering with performance optimization
- Dirty flag pattern prevents unnecessary per-frame sorting
- Lazy sorting for both Scene elements and Frame children
- Frame children now respect z_index (fixed inconsistency)
- Automatic dirty marking on z_index changes and collection modifications
- Performance: O(1) check for static scenes vs O(n log n) every frame
### 2025-07-05: Python Sequence Protocol Complete! 🎉
**Issue #69 Resolved**: Full sequence protocol implementation for collections
- Complete __setitem__, __delitem__, __contains__ support
- Slice operations with extended slice support (step != 1)
- Concatenation (+) and in-place concatenation (+=) with validation
- Negative indexing throughout, index() and count() methods
- Type safety: UICollection (Frame/Caption/Sprite/Grid), EntityCollection (Entity only)
- Default value support: None for texture/font parameters uses engine defaults
### 2025-07-05: Animation System Complete! 🎉
**Issue #59 Resolved**: Comprehensive animation system with 30+ easing functions
- Property-based animations for all UI classes (Frame, Caption, Sprite, Grid, Entity)
- Individual color component animation (r/g/b/a)
- Sprite sequence animation and text typewriter effects
- Pure C++ execution without Python callbacks
- Delta animation support for relative values
### 2025-01-03: Major Stability Update
**Major Cleanup**: Removed deprecated registerPyAction system (-180 lines)
**Bug Fixes**: 12 critical issues including Grid segfault, Issue #78 (middle click), Entity setters
**New Features**: Entity.index() (#73), EntityCollection.extend() (#27), Sprite validation (#33)
**Test Coverage**: Comprehensive test suite with timer callback pattern established
---
## 🔧 CURRENT WORK: Alpha Streamline 2 - Major Architecture Improvements
### Recent Completions:
- ✅ **Phase 1-4 Complete** - Foundation, API Polish, Entity Lifecycle, Visibility/Performance
- ✅ **Phase 5 Complete** - Window/Scene Architecture fully implemented!
- Window singleton with properties (#34)
- OOP Scene support with lifecycle methods (#61)
- Window resize events (#1)
- Scene transitions with animations (#105)
- 🚧 **Phase 6 Started** - Rendering Revolution in progress!
- Grid background colors (#50) ✅
- RenderTexture base infrastructure ✅
- UIFrame clipping support ✅
### Active Development:
- **Branch**: alpha_streamline_2
- **Current Phase**: Phase 6 - Rendering Revolution (IN PROGRESS)
- **Timeline**: 3-4 weeks for Phase 6 implementation
- **Strategic Vision**: See STRATEGIC_VISION.md for platform roadmap
- **Latest**: RenderTexture base infrastructure complete, UIFrame clipping working!
### 🏗️ Architectural Dependencies Map
```
Foundation Layer:
├── #71 Base Class (_Drawable)
│ ├── #10 Visibility System (needs AABB from base)
│ ├── #87 visible property
│ └── #88 opacity property
├── #7 Safe Constructors (affects all classes)
│ └── Blocks any new class creation until resolved
└── #30 Entity/Grid Integration (lifecycle management)
└── Enables reliable entity management
Window/Scene Layer:
├── #34 Window Object
│ ├── #61 Scene Object (depends on Window)
│ ├── #14 SFML Exposure (helps implement Window)
│ └── Future: Multi-window support
Rendering Layer:
└── #6 RenderTexture Overhaul
├── Enables clipping
├── Off-screen rendering
└── Post-processing effects
```
## 🚀 Alpha Streamline 2 - Comprehensive Phase Plan
### Phase 1: Foundation Stabilization (1-2 weeks)
**Goal**: Safe, predictable base for all future work
```
1. #7 - Audit and fix unsafe constructors (CRITICAL - do first!)
- Find all manually implemented no-arg constructors
- Verify map compatibility requirements
- Make pointer-safe or remove
2. #71 - _Drawable base class implementation
- Common properties: x, y, w, h, visible, opacity
- Virtual methods: get_bounds(), render()
- Proper Python inheritance setup
3. #87 - visible property
- Add to base class
- Update all render methods to check
4. #88 - opacity property (depends on #87)
- 0.0-1.0 float range
- Apply in render methods
5. #89 - get_bounds() method
- Virtual method returning (x, y, w, h)
- Override in each UI class
6. #98 - move()/resize() convenience methods
- move(dx, dy) - relative movement
- resize(w, h) - absolute sizing
```
*Rationale*: Can't build on unsafe foundations. Base class enables all UI improvements.
### Phase 2: Constructor & API Polish (1 week)
**Goal**: Pythonic, intuitive API
```
1. #101 - Standardize (0,0) defaults for all positions
2. #38 - Frame children parameter: Frame(children=[...])
3. #42 - Click handler in __init__: Button(click=callback)
4. #90 - Grid size tuple: Grid(grid_size=(10, 10))
5. #19 - Sprite texture swapping: sprite.texture = new_texture
6. #52 - Grid skip out-of-bounds entities (performance)
```
*Rationale*: Quick wins that make the API more pleasant before bigger changes.
### Phase 3: Entity Lifecycle Management (1 week)
**Goal**: Bulletproof entity/grid relationships
```
1. #30 - Entity.die() and grid association
- Grid.entities.append(e) sets e.grid = self
- Grid.entities.remove(e) sets e.grid = None
- Entity.die() calls self.grid.remove(self)
- Entity can only be in 0 or 1 grid
2. #93 - Vector arithmetic methods
- add, subtract, multiply, divide
- distance, normalize, dot product
3. #94 - Color helper methods
- from_hex("#FF0000"), to_hex()
- lerp(other_color, t) for interpolation
4. #103 - Timer objects
timer = mcrfpy.Timer("my_timer", callback, 1000)
timer.pause()
timer.resume()
timer.cancel()
```
*Rationale*: Games need reliable entity management. Timer objects enable entity AI.
### Phase 4: Visibility & Performance (1-2 weeks)
**Goal**: Only render/process what's needed
```
1. #10 - [UNSCHEDULED] Full visibility system with AABB
- Postponed: UIDrawables can exist in multiple collections
- Cannot reliably determine screen position due to multiple render contexts
- Needs architectural solution for parent-child relationships
2. #52 - Grid culling (COMPLETED in Phase 2)
3. #39/40/41 - Name system for finding elements
- name="button1" property on all UIDrawables
- only_one=True for unique names
- scene.find("button1") returns element
- collection.find("enemy*") returns list
4. #104 - Basic profiling/metrics
- Frame time tracking
- Draw call counting
- Python vs C++ time split
```
*Rationale*: Performance is feature. Finding elements by name is huge QoL.
### Phase 5: Window/Scene Architecture ✅ COMPLETE! (2025-07-06)
**Goal**: Modern, flexible architecture
```
1. ✅ #34 - Window object (singleton first)
window = mcrfpy.Window.get()
window.resolution = (1920, 1080)
window.fullscreen = True
window.vsync = True
2. ✅ #1 - Window resize events
scene.on_resize(self, width, height) callback implemented
3. ✅ #61 - Scene object (OOP scenes)
class MenuScene(mcrfpy.Scene):
def on_keypress(self, key, state):
# handle input
def on_enter(self):
# setup UI
def on_exit(self):
# cleanup
def update(self, dt):
# frame update
4. ✅ #14 - SFML exposure research
- Completed comprehensive analysis
- Recommendation: Direct integration as mcrfpy.sfml
- SFML 3.0 migration deferred to late 2025
5. ✅ #105 - Scene transitions
mcrfpy.setScene("menu", "fade", 1.0)
# Supports: fade, slide_left, slide_right, slide_up, slide_down
```
*Result*: Entire window/scene system modernized with OOP design!
### Phase 6: Rendering Revolution (3-4 weeks) 🚧 IN PROGRESS!
**Goal**: Professional rendering capabilities
```
1. ✅ #50 - Grid background colors [COMPLETED]
grid.background_color = mcrfpy.Color(50, 50, 50)
- Added background_color property with animation support
- Default dark gray background (8, 8, 8, 255)
2. 🚧 #6 - RenderTexture overhaul [PARTIALLY COMPLETE]
✅ Base infrastructure in UIDrawable
✅ UIFrame clip_children property
✅ Dirty flag optimization system
✅ Nested clipping support
⏳ Extend to other UI classes
⏳ Effects (blur, glow, etc.)
3. #8 - Viewport-based rendering [NEXT PRIORITY]
- RenderTexture matches viewport
- Proper scaling/letterboxing
- Coordinate system transformations
4. #106 - Shader support [STRETCH GOAL]
sprite.shader = mcrfpy.Shader.load("glow.frag")
frame.shader_params = {"intensity": 0.5}
5. #107 - Particle system [STRETCH GOAL]
emitter = mcrfpy.ParticleEmitter()
emitter.texture = spark_texture
emitter.emission_rate = 100
emitter.lifetime = (0.5, 2.0)
```
**Phase 6 Technical Notes**:
- RenderTexture is the foundation - everything else depends on it
- Grid backgrounds (#50) ✅ completed as warm-up task
- RenderTexture implementation uses opt-in architecture to preserve backward compatibility
- Dirty flag system crucial for performance - only re-render when properties change
- Nested clipping works correctly with proper coordinate transformations
- Scene transitions already use RenderTextures - good integration test
- Next: Viewport rendering (#8) will build on RenderTexture foundation
- Shader/Particle systems might be deferred to Phase 7 or Gamma
*Rationale*: This unlocks professional visual effects but is complex.
### Phase 7: Documentation & Distribution (1-2 weeks)
**Goal**: Ready for the world
```
1. #85 - Replace all "docstring" placeholders
2. #86 - Add parameter documentation
3. #108 - Generate .pyi type stubs for IDE support
4. #70 - PyPI wheel preparation
5. API reference generator tool
```
## 📋 Critical Path & Parallel Tracks
### 🔴 **Critical Path** (Must do in order)
**Safe Constructors (#7)** → **Base Class (#71)****Visibility (#10)****Window (#34)** → **Scene (#61)**
### 🟡 **Parallel Tracks** (Can be done alongside critical path)
**Track A: Entity Systems**
- Entity/Grid integration (#30)
- Timer objects (#103)
- Vector/Color helpers (#93, #94)
**Track B: API Polish**
- Constructor improvements (#101, #38, #42, #90)
- Sprite texture swap (#19)
- Name/search system (#39/40/41)
**Track C: Performance**
- Grid culling (#52)
- Visibility culling (part of #10)
- Profiling tools (#104)
### 💎 **Quick Wins to Sprinkle Throughout**
1. Color helpers (#94) - 1 hour
2. Vector methods (#93) - 1 hour
3. Grid backgrounds (#50) - 30 minutes
4. Default positions (#101) - 30 minutes
### 🎯 **Recommended Execution Order**
**Week 1-2**: Foundation (Critical constructors + base class)
**Week 3**: Entity lifecycle + API polish
**Week 4**: Visibility system + performance
**Week 5-6**: Window/Scene architecture
**Week 7-9**: Rendering revolution (or defer to gamma)
**Week 10**: Documentation + release prep
### 🆕 **New Issues to Create/Track**
1. [x] **Timer Objects** - Pythonic timer management (#103) - *Completed Phase 3*
2. [ ] **Event System Enhancement** - Mouse enter/leave, drag, right-click
3. [ ] **Resource Manager** - Centralized asset loading
4. [ ] **Serialization System** - Save/load game state
5. [x] **Scene Transitions** - Fade, slide, custom effects (#105) - *Completed Phase 5*
6. [x] **Profiling Tools** - Performance metrics (#104) - *Completed Phase 4*
7. [ ] **Particle System** - Visual effects framework (#107)
8. [ ] **Shader Support** - Custom rendering effects (#106)
---
## 📋 Phase 6 Implementation Strategy
### RenderTexture Overhaul (#6) - Technical Approach
**Current State**:
- UIGrid already uses RenderTexture for entity rendering
- Scene transitions use RenderTextures for smooth animations
- Direct rendering to window for Frame, Caption, Sprite
**Implementation Plan**:
1. **Base Infrastructure**:
- Add `sf::RenderTexture* target` to UIDrawable base
- Modify `render()` to check if target exists
- If target: render to texture, then draw texture to parent
- If no target: render directly (backward compatible)
2. **Clipping Support**:
- Frame enforces bounds on children via RenderTexture
- Children outside bounds are automatically clipped
- Nested frames create render texture hierarchy
3. **Performance Optimization**:
- Lazy RenderTexture creation (only when needed)
- Dirty flag system (only re-render when changed)
- Texture pooling for commonly used sizes
4. **Integration Points**:
- Scene transitions already working with RenderTextures
- UIGrid can be reference implementation
- Test with deeply nested UI structures
**Quick Wins Before Core Work**:
1. **Grid Background (#50)** - 30 min implementation
- Add `background_color` and `background_texture` properties
- Render before entities in UIGrid::render()
- Good warm-up before tackling RenderTexture
2. **Research Tasks**:
- Study UIGrid's current RenderTexture usage
- Profile scene transition performance
- Identify potential texture size limits
---
## 🚀 NEXT PHASE: Beta Features & Polish
### Alpha Complete! Moving to Beta Priorities:
1. ~~**#69** - Python Sequence Protocol for collections~~ - *Completed! (2025-07-05)*
2. ~~**#63** - Z-order rendering for UIDrawables~~ - *Completed! (2025-07-05)*
3. ~~**#59** - Animation system~~ - *Completed! (2025-07-05)*
4. **#6** - RenderTexture concept - *Extensive Overhaul*
5. ~~**#47** - New README.md for Alpha release~~ - *Completed*
- [x] **#78** - Middle Mouse Click sends "C" keyboard event - *Fixed*
- [x] **#77** - Fix error message copy/paste bug - *Fixed*
- [x] **#74** - Add missing `Grid.grid_y` property - *Fixed*
- [ ] **#37** - Fix Windows build module import from "scripts" directory - *Isolated Fix*
Issue #37 is **on hold** until we have a Windows build environment available. I actually suspect this is already fixed by the updates to the makefile, anyway.
- [x] **Entity Property Setters** - Fix "new style getargs format" error - *Fixed*
- [x] **Sprite Texture Setter** - Fix "error return without exception set" - *Fixed*
- [x] **keypressScene() Validation** - Add proper error handling - *Fixed*
### 🔄 Complete Iterator System
**Status**: Core iterators complete (#72 closed), Grid point iterators still pending
- [ ] **Grid Point Iterator Implementation** - Complete the remaining grid iteration work
- [x] **#73** - Add `entity.index()` method for collection removal - *Fixed*
- [x] **#69** ⚠️ **Alpha Blocker** - Refactor all collections to use Python Sequence Protocol - *Completed! (2025-07-05)*
**Dependencies**: Grid point iterators → #73 entity.index() → #69 Sequence Protocol overhaul
---
## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues)
### 🎮 Core Engine Systems
#### Iterator/Collection System (2 issues)
- [x] **#73** - Entity index() method for removal - *Fixed*
- [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)*
#### Python/C++ Integration (7 issues)
- [x] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations*
- [ ] **#71** - Drawable base class hierarchy - *Extensive Overhaul*
- [ ] **#70** - PyPI wheel distribution - *Extensive Overhaul*
- [~] **#32** - Executable behave like `python` command - *Extensive Overhaul* *(90% Complete: -h, -V, -c, -m, -i, script execution, sys.argv, --exec all implemented. Only stdin (-) support missing)*
- [ ] **#35** - TCOD as built-in module - *Extensive Overhaul*
- [~] **#14** - Expose SFML as built-in module - *Research Complete, Implementation Pending*
- [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations*
#### UI/Rendering System (12 issues)
- [x] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations*
- [x] **#59** ⚠️ **Alpha Blocker** - Animation system - *Completed! (2025-07-05)*
- [ ] **#6** ⚠️ **Alpha Blocker** - RenderTexture for all UIDrawables - *Extensive Overhaul*
- [ ] **#10** - UIDrawable visibility/AABB system - *Extensive Overhaul*
- [ ] **#8** - UIGrid RenderTexture viewport sizing - *Multiple Integrations*
- [x] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations*
- [ ] **#52** - UIGrid skip out-of-bounds entities - *Isolated Fix*
- [ ] **#50** - UIGrid background color field - *Isolated Fix*
- [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations*
- [ ] **#17** - Move UISprite position into sf::Sprite - *Isolated Fix*
- [x] **#33** - Sprite index validation against texture range - *Fixed*
#### Grid/Entity System (6 issues)
- [ ] **#30** - Entity/Grid association management (.die() method) - *Extensive Overhaul*
- [ ] **#16** - Grid strict mode for entity knowledge/visibility - *Extensive Overhaul*
- [ ] **#67** - Grid stitching for infinite worlds - *Extensive Overhaul*
- [ ] **#15** - UIGridPointState cleanup and standardization - *Multiple Integrations*
- [ ] **#20** - UIGrid get_grid_size standardization - *Multiple Integrations*
- [x] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix*
#### Scene/Window Management (5 issues)
- [x] **#61** - Scene object encapsulating key callbacks - *Completed Phase 5*
- [x] **#34** - Window object for resolution/scaling - *Completed Phase 5*
- [ ] **#62** - Multiple windows support - *Extensive Overhaul*
- [ ] **#49** - Window resolution & viewport controls - *Multiple Integrations*
- [x] **#1** - Scene resize event handling - *Completed Phase 5*
### 🔧 Quality of Life Features
#### UI Enhancement Features (8 issues)
- [ ] **#39** - Name field on UIDrawables - *Multiple Integrations*
- [ ] **#40** - `only_one` arg for unique naming - *Multiple Integrations*
- [ ] **#41** - `.find(name)` method for collections - *Multiple Integrations*
- [ ] **#38** - `children` arg for Frame initialization - *Isolated Fix*
- [ ] **#42** - Click callback arg for UIDrawable init - *Isolated Fix*
- [x] **#27** - UIEntityCollection.extend() method - *Fixed*
- [ ] **#28** - UICollectionIter for scene ui iteration - *Isolated Fix*
- [ ] **#26** - UIEntityCollectionIter implementation - *Isolated Fix*
### 🧹 Refactoring & Cleanup
#### Code Cleanup (7 issues)
- [x] **#3** ⚠️ **Alpha Blocker** - Remove `McRFPy_API::player_input` - *Completed*
- [x] **#2** ⚠️ **Alpha Blocker** - Review `registerPyAction` necessity - *Completed*
- [ ] **#7** - Remove unsafe no-argument constructors - *Multiple Integrations*
- [ ] **#21** - PyUIGrid dealloc cleanup - *Isolated Fix*
- [ ] **#75** - REPL thread separation from SFML window - *Multiple Integrations*
### 📚 Demo & Documentation
#### Documentation (2 issues)
- [x] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix*
- [ ] **#48** - Dependency compilation documentation - *Isolated Fix*
#### Demo Projects (6 issues)
- [ ] **#54** - Jupyter notebook integration demo - *Multiple Integrations*
- [ ] **#55** - Hunt the Wumpus AI demo - *Multiple Integrations*
- [ ] **#53** - Web interface input demo - *Multiple Integrations* *(New automation API could help)*
- [ ] **#45** - Accessibility mode demos - *Multiple Integrations* *(New automation API could help test)*
- [ ] **#36** - Dear ImGui integration tests - *Extensive Overhaul*
- [ ] **#65** - Python Explorer scene (replaces uitest) - *Extensive Overhaul*
---
## 🎮 STRATEGIC DIRECTION
### Engine Philosophy Maintained
- **C++ First**: Performance-critical code stays in C++
- **Python Close Behind**: Rich scripting without frame-rate impact
- **Game-Ready**: Each improvement should benefit actual game development
### Architecture Goals
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
3. **Resource Management**: RAII everywhere, proper lifecycle handling
4. **Multi-Platform**: Windows/Linux feature parity maintained
---
## 📚 REFERENCES & CONTEXT
**Issue Dependencies** (Key Chains):
- Iterator System: Grid points → #73#69 (Alpha Blocker)
- UI Hierarchy: #71#63 (Alpha Blocker)
- Rendering: #6 (Alpha Blocker) → #8, #9#10
- Entity System: #30#16#67
- Window Management: #34#49, #61#62
**Commit References**:
- 167636c: Iterator improvements (UICollection/UIEntityCollection complete)
- Recent work: 7DRL 2025 completion, RPATH updates, console improvements
**Architecture Files**:
- Iterator patterns: src/UICollection.cpp, src/UIGrid.cpp
- Python integration: src/McRFPy_API.cpp, src/PyObjectUtils.h
- Game implementation: src/scripts/ (Crypt of Sokoban complete game)
---
*Last Updated: 2025-07-05*

View File

@ -1,257 +0,0 @@
# SFML 3.0 Migration Research for McRogueFace
## Executive Summary
SFML 3.0 was released on December 21, 2024, marking the first major version in 12 years. While it offers significant improvements in type safety, modern C++ features, and API consistency, migrating McRogueFace would require substantial effort. Given our plans for `mcrfpy.sfml`, I recommend **deferring migration to SFML 3.0** until after implementing the initial `mcrfpy.sfml` module with SFML 2.6.1.
## SFML 3.0 Overview
### Release Highlights
- **Release Date**: December 21, 2024
- **Development**: 3 years, 1,100+ commits, 41 new contributors
- **Major Feature**: C++17 support (now required)
- **Audio Backend**: Replaced OpenAL with miniaudio
- **Test Coverage**: Expanded to 57%
- **New Features**: Scissor and stencil testing
### Key Breaking Changes
#### 1. C++ Standard Requirements
- **Minimum**: C++17 (was C++03)
- **Compilers**: MSVC 16 (VS 2019), GCC 9, Clang 9, AppleClang 12
#### 2. Event System Overhaul
```cpp
// SFML 2.x
sf::Event event;
while (window.pollEvent(event)) {
switch (event.type) {
case sf::Event::Closed:
window.close();
break;
case sf::Event::KeyPressed:
handleKey(event.key.code);
break;
}
}
// SFML 3.0
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) {
window.close();
}
else if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>()) {
handleKey(keyPressed->code);
}
}
```
#### 3. Scoped Enumerations
```cpp
// SFML 2.x
sf::Keyboard::A
sf::Mouse::Left
// SFML 3.0
sf::Keyboard::Key::A
sf::Mouse::Button::Left
```
#### 4. Resource Loading
```cpp
// SFML 2.x
sf::Texture texture;
if (!texture.loadFromFile("image.png")) {
// Handle error
}
// SFML 3.0
try {
sf::Texture texture("image.png");
} catch (const std::exception& e) {
// Handle error
}
```
#### 5. Geometry Changes
```cpp
// SFML 2.x
sf::FloatRect rect(left, top, width, height);
// SFML 3.0
sf::FloatRect rect({left, top}, {width, height});
// Now uses position and size vectors
```
#### 6. CMake Changes
```cmake
# SFML 2.x
find_package(SFML 2.6 COMPONENTS graphics window system audio REQUIRED)
target_link_libraries(app sfml-graphics sfml-window sfml-system sfml-audio)
# SFML 3.0
find_package(SFML 3.0 COMPONENTS Graphics Window System Audio REQUIRED)
target_link_libraries(app SFML::Graphics SFML::Window SFML::System SFML::Audio)
```
## McRogueFace SFML Usage Analysis
### Current Usage Statistics
- **SFML Version**: 2.6.1
- **Integration Level**: Moderate to Heavy
- **Affected Files**: ~40+ source files
### Major Areas Requiring Changes
#### 1. Event Handling (High Impact)
- **Files**: `GameEngine.cpp`, `PyScene.cpp`
- **Changes**: Complete rewrite of event loops
- **Effort**: High
#### 2. Enumerations (Medium Impact)
- **Files**: `ActionCode.h`, all input handling
- **Changes**: Update all keyboard/mouse enum references
- **Effort**: Medium (mostly find/replace)
#### 3. Resource Loading (Medium Impact)
- **Files**: `PyTexture.cpp`, `PyFont.cpp`, `McRFPy_API.cpp`
- **Changes**: Constructor-based loading with exception handling
- **Effort**: Medium
#### 4. Geometry (Low Impact)
- **Files**: Various UI classes
- **Changes**: Update Rect construction
- **Effort**: Low
#### 5. CMake Build System (Low Impact)
- **Files**: `CMakeLists.txt`
- **Changes**: Update find_package and target names
- **Effort**: Low
### Code Examples from McRogueFace
#### Current Event Loop (GameEngine.cpp)
```cpp
sf::Event event;
while (window && window->pollEvent(event)) {
processEvent(event);
if (event.type == sf::Event::Closed) {
running = false;
}
}
```
#### Current Key Mapping (ActionCode.h)
```cpp
{sf::Keyboard::Key::A, KEY_A},
{sf::Keyboard::Key::Left, KEY_LEFT},
{sf::Mouse::Left, MOUSEBUTTON_LEFT}
```
## Impact on mcrfpy.sfml Module Plans
### Option 1: Implement with SFML 2.6.1 First (Recommended)
**Pros**:
- Faster initial implementation
- Stable, well-tested SFML version
- Can provide value immediately
- Migration can be done later
**Cons**:
- Will require migration work later
- API might need changes for SFML 3.0
### Option 2: Wait and Implement with SFML 3.0
**Pros**:
- Future-proof implementation
- Modern C++ features
- No migration needed later
**Cons**:
- Delays `mcrfpy.sfml` implementation
- SFML 3.0 is very new (potential bugs)
- Less documentation/examples available
### Option 3: Dual Support
**Pros**:
- Maximum flexibility
- Gradual migration path
**Cons**:
- Significant additional complexity
- Maintenance burden
- Conditional compilation complexity
## Migration Strategy Recommendation
### Phase 1: Current State (Now)
1. Continue with SFML 2.6.1
2. Implement `mcrfpy.sfml` module as planned
3. Design module API to minimize future breaking changes
### Phase 2: Preparation (3-6 months)
1. Monitor SFML 3.0 stability and adoption
2. Create migration branch for testing
3. Update development environment to C++17
### Phase 3: Migration (6-12 months)
1. Migrate McRogueFace core to SFML 3.0
2. Update `mcrfpy.sfml` to match
3. Provide migration guide for users
### Phase 4: Deprecation (12-18 months)
1. Deprecate SFML 2.6.1 support
2. Focus on SFML 3.0 features
## Specific Migration Tasks
### Prerequisites
- [ ] Update to C++17 compatible compiler
- [ ] Update CMake to 3.16+
- [ ] Review all SFML usage locations
### Core Changes
- [ ] Rewrite all event handling loops
- [ ] Update all enum references
- [ ] Convert resource loading to constructors
- [ ] Update geometry construction
- [ ] Update CMake configuration
### mcrfpy.sfml Considerations
- [ ] Design API to be version-agnostic where possible
- [ ] Use abstraction layer for version-specific code
- [ ] Document version requirements clearly
## Risk Assessment
### High Risk Areas
1. **Event System**: Complete paradigm shift
2. **Exception Handling**: New resource loading model
3. **Third-party Dependencies**: May not support SFML 3.0 yet
### Medium Risk Areas
1. **Performance**: New implementations may differ
2. **Platform Support**: New version may have issues
3. **Documentation**: Less community knowledge
### Low Risk Areas
1. **Basic Rendering**: Core concepts unchanged
2. **CMake**: Straightforward updates
3. **Enums**: Mechanical changes
## Conclusion
While SFML 3.0 offers significant improvements, the migration effort is substantial. Given that:
1. SFML 3.0 is very new (released December 2024)
2. McRogueFace has heavy SFML integration
3. We plan to implement `mcrfpy.sfml` soon
4. The event system requires complete rewriting
**I recommend deferring SFML 3.0 migration** until after successfully implementing `mcrfpy.sfml` with SFML 2.6.1. This allows us to:
- Deliver value sooner with `mcrfpy.sfml`
- Learn from early adopters of SFML 3.0
- Design our module API with migration in mind
- Migrate when SFML 3.0 is more mature
The migration should be revisited in 6-12 months when SFML 3.0 has proven stability and wider adoption.

View File

@ -1,200 +0,0 @@
# SFML Exposure Research (#14)
## Executive Summary
After thorough research, I recommend **Option 3: Direct Integration** - implementing our own `mcrfpy.sfml` module with API compatibility to existing python-sfml bindings. This approach gives us full control while maintaining familiarity for developers who have used python-sfml.
## Current State Analysis
### McRogueFace SFML Usage
**Version**: SFML 2.6.1 (confirmed in `modules/SFML/include/SFML/Config.hpp`)
**Integration Level**: Moderate to Heavy
- SFML types appear in most header files
- Core rendering depends on `sf::RenderTarget`
- Event system uses `sf::Event` directly
- Input mapping uses SFML enums
**SFML Modules Used**:
- Graphics (sprites, textures, fonts, shapes)
- Window (events, keyboard, mouse)
- System (vectors, time, clocks)
- Audio (sound effects, music)
**Already Exposed to Python**:
- `mcrfpy.Color``sf::Color`
- `mcrfpy.Vector``sf::Vector2f`
- `mcrfpy.Font``sf::Font`
- `mcrfpy.Texture``sf::Texture`
### Python-SFML Status
**Official python-sfml (pysfml)**:
- Last version: 2.3.2 (supports SFML 2.3.2)
- Last meaningful update: ~2019
- Not compatible with SFML 2.6.1
- Project appears abandoned (domain redirects elsewhere)
- GitHub repo has 43 forks but no active maintained fork
**Alternatives**:
- No other major Python SFML bindings found
- Most alternatives were archived by 2021
## Option Analysis
### Option 1: Use Existing python-sfml
**Pros**:
- No development work needed
- Established API
**Cons**:
- Incompatible with SFML 2.6.1
- Would require downgrading to SFML 2.3.2
- Abandoned project (security/bug risks)
- Installation issues reported
**Verdict**: Not viable due to version incompatibility and abandonment
### Option 2: Fork and Update python-sfml
**Pros**:
- Leverage existing codebase
- Maintain API compatibility
**Cons**:
- Significant work to update from 2.3.2 to 2.6.1
- Cython complexity
- Maintenance burden of external codebase
- Still requires users to pip install separately
**Verdict**: High effort with limited benefit
### Option 3: Direct Integration (Recommended)
**Pros**:
- Full control over implementation
- Tight integration with McRogueFace
- No external dependencies
- Can expose exactly what we need
- Built-in module (no pip install)
- Can maintain API compatibility with python-sfml
**Cons**:
- Development effort required
- Need to maintain bindings
**Verdict**: Best long-term solution
## Implementation Plan for Direct Integration
### 1. Module Structure
```python
# Built-in module: mcrfpy.sfml
import mcrfpy.sfml as sf
# Maintain compatibility with python-sfml API
window = sf.RenderWindow(sf.VideoMode(800, 600), "My Window")
sprite = sf.Sprite()
texture = sf.Texture()
```
### 2. Priority Classes to Expose
**Phase 1 - Core Types** (Already partially done):
- [x] `sf::Vector2f`, `sf::Vector2i`
- [x] `sf::Color`
- [ ] `sf::Rect` (FloatRect, IntRect)
- [ ] `sf::VideoMode`
- [ ] `sf::Time`, `sf::Clock`
**Phase 2 - Graphics**:
- [x] `sf::Texture` (partial)
- [x] `sf::Font` (partial)
- [ ] `sf::Sprite` (full exposure)
- [ ] `sf::Text`
- [ ] `sf::Shape` hierarchy
- [ ] `sf::View`
- [ ] `sf::RenderWindow` (carefully managed)
**Phase 3 - Window/Input**:
- [ ] `sf::Event` and event types
- [ ] `sf::Keyboard` enums
- [ ] `sf::Mouse` enums
- [ ] `sf::Joystick`
**Phase 4 - Audio** (lower priority):
- [ ] `sf::SoundBuffer`
- [ ] `sf::Sound`
- [ ] `sf::Music`
### 3. Design Principles
1. **API Compatibility**: Match python-sfml's API where possible
2. **Memory Safety**: Use shared_ptr for resource management
3. **Thread Safety**: Consider GIL implications
4. **Integration**: Allow mixing with existing mcrfpy types
5. **Documentation**: Comprehensive docstrings
### 4. Technical Considerations
**Resource Sharing**:
- McRogueFace already manages SFML resources
- Need to share textures/fonts between mcrfpy and sfml modules
- Use the same underlying SFML objects
**Window Management**:
- McRogueFace owns the main window
- Expose read-only access or controlled modification
- Prevent users from closing/destroying the game window
**Event Handling**:
- Game engine processes events in main loop
- Need mechanism to expose events to Python safely
- Consider callback system or event queue
### 5. Implementation Phases
**Phase 1** (1-2 weeks):
- Create `mcrfpy.sfml` module structure
- Implement basic types (Vector, Color, Rect)
- Add comprehensive tests
**Phase 2** (2-3 weeks):
- Expose graphics classes
- Implement resource sharing with mcrfpy
- Create example scripts
**Phase 3** (2-3 weeks):
- Add window/input functionality
- Integrate with game event loop
- Performance optimization
**Phase 4** (1 week):
- Audio support
- Documentation
- PyPI packaging of mcrfpy.sfml separately
## Benefits of Direct Integration
1. **No Version Conflicts**: Always in sync with our SFML version
2. **Better Performance**: Direct C++ bindings without Cython overhead
3. **Selective Exposure**: Only expose what makes sense for game scripting
4. **Integrated Documentation**: Part of McRogueFace docs
5. **Future-Proof**: We control the implementation
## Migration Path for Users
Users familiar with python-sfml can easily migrate:
```python
# Old python-sfml code
import sfml as sf
# New McRogueFace code
import mcrfpy.sfml as sf
# Most code remains the same!
```
## Conclusion
Direct integration as `mcrfpy.sfml` provides the best balance of control, compatibility, and user experience. While it requires development effort, it ensures long-term maintainability and tight integration with McRogueFace's architecture.
The abandoned state of python-sfml actually presents an opportunity: we can provide a modern, maintained SFML binding for Python as part of McRogueFace, potentially attracting users who need SFML 2.6+ support.

View File

@ -1,226 +0,0 @@
# McRogueFace Strategic Vision: Beyond Alpha
## 🎯 Three Transformative Directions
### 1. **The Roguelike Operating System** 🖥️
Transform McRogueFace into a platform where games are apps:
#### Core Platform Features
- **Game Package Manager**: `mcrf install dungeon-crawler`
- **Hot-swappable Game Modules**: Switch between games without restarting
- **Shared Asset Library**: Common sprites, sounds, and UI components
- **Cross-Game Saves**: Universal character/inventory system
- **Multi-Game Sessions**: Run multiple roguelikes simultaneously in tabs
#### Technical Implementation
```python
# Future API Example
import mcrfpy.platform as platform
# Install and launch games
platform.install("nethack-remake")
platform.install("pixel-dungeon-port")
# Create multi-game session
session = platform.MultiGameSession()
session.add_tab("nethack-remake", save_file="warrior_lvl_15.sav")
session.add_tab("pixel-dungeon-port", new_game=True)
session.run()
```
### 2. **AI-Native Game Development** 🤖
Position McRogueFace as the first **AI-first roguelike engine**:
#### Integrated AI Features
- **GPT-Powered NPCs**: Dynamic dialogue and quest generation
- **Procedural Content via LLMs**: Describe a dungeon, AI generates it
- **AI Dungeon Master**: Adaptive difficulty and narrative
- **Code Assistant Integration**: Built-in AI helps write game logic
#### Revolutionary Possibilities
```python
# AI-Assisted Game Creation
from mcrfpy import ai_tools
# Natural language level design
dungeon = ai_tools.generate_dungeon("""
Create a haunted library with 3 floors.
First floor: Reading rooms with ghost librarians
Second floor: Restricted section with magical traps
Third floor: Ancient archive with boss encounter
""")
# AI-driven NPCs
npc = ai_tools.create_npc(
personality="Grumpy dwarf merchant who secretly loves poetry",
knowledge=["local rumors", "item prices", "hidden treasures"],
dynamic_dialogue=True
)
```
### 3. **Web-Native Multiplayer Platform** 🌐
Make McRogueFace the **Discord of Roguelikes**:
#### Multiplayer Revolution
- **Seamless Co-op**: Drop-in/drop-out multiplayer
- **Competitive Modes**: Racing, PvP arenas, daily challenges
- **Spectator System**: Watch and learn from others
- **Cloud Saves**: Play anywhere, sync everywhere
- **Social Features**: Guilds, tournaments, leaderboards
#### WebAssembly Future
```python
# Future Web API
import mcrfpy.web as web
# Host a game room
room = web.create_room("Epic Dungeon Run", max_players=4)
room.set_rules(friendly_fire=False, shared_loot=True)
room.open_to_public()
# Stream gameplay
stream = web.GameStream(room)
stream.to_twitch(channel="awesome_roguelike")
```
## 🏗️ Architecture Evolution Roadmap
### Phase 1: Beta Foundation (3-4 months)
**Focus**: Stability and Polish
- Complete RenderTexture system (#6)
- Implement save/load system
- Add audio mixing and 3D sound
- Create plugin architecture
- **Deliverable**: Beta release with plugin support
### Phase 2: Platform Infrastructure (6-8 months)
**Focus**: Multi-game Support
- Game package format specification
- Resource sharing system
- Inter-game communication API
- Cloud save infrastructure
- **Deliverable**: McRogueFace Platform 1.0
### Phase 3: AI Integration (8-12 months)
**Focus**: AI-Native Features
- LLM integration framework
- Procedural content pipelines
- Natural language game scripting
- AI behavior trees
- **Deliverable**: McRogueFace AI Studio
### Phase 4: Web Deployment (12-18 months)
**Focus**: Browser-based Gaming
- WebAssembly compilation
- WebRTC multiplayer
- Cloud computation for AI
- Mobile touch controls
- **Deliverable**: play.mcrogueface.com
## 🎮 Killer App Ideas
### 1. **Roguelike Maker** (Like Mario Maker)
- Visual dungeon editor
- Share levels online
- Play-test with AI
- Community ratings
### 2. **The Infinite Dungeon**
- Persistent world all players explore
- Procedurally expands based on player actions
- AI Dungeon Master creates personalized quests
- Cross-platform play
### 3. **Roguelike Battle Royale**
- 100 players start in connected dungeons
- Dungeons collapse, forcing encounters
- Last adventurer standing wins
- AI-generated commentary
## 🛠️ Technical Innovations to Pursue
### 1. **Temporal Debugging**
- Rewind game state
- Fork timelines for "what-if" scenarios
- Visual debugging of entity histories
### 2. **Neural Tileset Generation**
- Train on existing tilesets
- Generate infinite variations
- Style transfer between games
### 3. **Quantum Roguelike Mechanics**
- Superposition states for entities
- Probability-based combat
- Observer-effect puzzles
## 🌍 Community Building Strategy
### 1. **Education First**
- University partnerships
- Free curriculum: "Learn Python with Roguelikes"
- Summer of Code participation
- Student game jams
### 2. **Open Core Model**
- Core engine: MIT licensed
- Premium platforms: Cloud, AI, multiplayer
- Revenue sharing for content creators
- Sponsored tournaments
### 3. **Developer Ecosystem**
- Comprehensive API documentation
- Example games and tutorials
- Asset marketplace
- GitHub integration for mods
## 🎯 Success Metrics
### Year 1 Goals
- 1,000+ games created on platform
- 10,000+ monthly active developers
- 3 AAA-quality showcase games
- University curriculum adoption
### Year 2 Goals
- 100,000+ monthly active players
- $1M in platform transactions
- Major game studio partnership
- Native VR support
### Year 3 Goals
- #1 roguelike development platform
- IPO or acquisition readiness
- 1M+ monthly active players
- Industry standard for roguelikes
## 🚀 Next Immediate Actions
1. **Finish Beta Polish**
- Merge alpha_streamline_2 → master
- Complete RenderTexture (#6)
- Implement basic save/load
2. **Build Community**
- Launch Discord server
- Create YouTube tutorials
- Host first game jam
3. **Prototype AI Features**
- Simple GPT integration
- Procedural room descriptions
- Dynamic NPC dialogue
4. **Plan Platform Architecture**
- Design plugin system
- Spec game package format
- Cloud infrastructure research
---
*"McRogueFace: Not just an engine, but a universe of infinite dungeons."*
Remember: The best platforms create possibilities their creators never imagined. Build for the community you want to see, and they will create wonders.

View File

@ -1,16 +0,0 @@
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")

View File

@ -1,127 +0,0 @@
#!/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!")

View File

@ -1,336 +0,0 @@
#!/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")

View File

@ -1,33 +0,0 @@
#!/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}"

View File

@ -1,63 +0,0 @@
#!/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")

View File

@ -1,53 +0,0 @@
#!/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")

View File

@ -1,69 +0,0 @@
#!/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")

View File

@ -1,189 +0,0 @@
// 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<std::filesystem::path> 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

View File

@ -1,102 +0,0 @@
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()

View File

@ -26,7 +26,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
render_target = &headless_renderer->getRenderTarget();
} else {
window = std::make_unique<sf::RenderWindow>();
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize);
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close);
window->setFramerateLimit(60);
render_target = window.get();
}
@ -73,81 +73,19 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
GameEngine::~GameEngine()
{
cleanup();
for (auto& [name, scene] : scenes) {
delete scene;
}
}
void GameEngine::cleanup()
{
if (cleaned_up) return;
cleaned_up = true;
// Clear Python references before destroying C++ objects
// Clear all timers (they hold Python callables)
timers.clear();
// Clear McRFPy_API's reference to this game engine
if (McRFPy_API::game == this) {
McRFPy_API::game = nullptr;
}
// Force close the window if it's still open
if (window && window->isOpen()) {
window->close();
}
}
Scene* GameEngine::currentScene() { return scenes[scene]; }
void GameEngine::changeScene(std::string s)
{
changeScene(s, TransitionType::None, 0.0f);
}
void GameEngine::changeScene(std::string sceneName, TransitionType transitionType, float duration)
{
if (scenes.find(sceneName) == scenes.end())
{
std::cout << "Attempted to change to a scene that doesn't exist (`" << sceneName << "`)" << std::endl;
return;
}
if (transitionType == TransitionType::None || duration <= 0.0f)
{
// Immediate scene change
std::string old_scene = scene;
scene = sceneName;
// Trigger Python scene lifecycle events
McRFPy_API::triggerSceneChange(old_scene, sceneName);
}
/*std::cout << "Current scene is now '" << s << "'\n";*/
if (scenes.find(s) != scenes.end())
scene = s;
else
{
// Start transition
transition.start(transitionType, scene, sceneName, duration);
// Render current scene to texture
sf::RenderTarget* original_target = render_target;
render_target = transition.oldSceneTexture.get();
transition.oldSceneTexture->clear();
currentScene()->render();
transition.oldSceneTexture->display();
// Change to new scene
std::string old_scene = scene;
scene = sceneName;
// Render new scene to texture
render_target = transition.newSceneTexture.get();
transition.newSceneTexture->clear();
currentScene()->render();
transition.newSceneTexture->display();
// Restore original render target and scene
render_target = original_target;
scene = old_scene;
}
std::cout << "Attempted to change to a scene that doesn't exist (`" << s << "`)" << std::endl;
}
void GameEngine::quit() { running = false; }
void GameEngine::setPause(bool p) { paused = p; }
@ -181,15 +119,9 @@ void GameEngine::run()
clock.restart();
while (running)
{
// Reset per-frame metrics
metrics.resetPerFrame();
currentScene()->update();
testTimers();
// Update Python scenes
McRFPy_API::updatePythonScenes(frameTime);
// Update animations (only if frameTime is valid)
if (frameTime > 0.0f && frameTime < 1.0f) {
AnimationManager::getInstance().update(frameTime);
@ -201,33 +133,7 @@ void GameEngine::run()
if (!paused)
{
}
// Handle scene transitions
if (transition.type != TransitionType::None)
{
transition.update(frameTime);
if (transition.isComplete())
{
// Transition complete - finalize scene change
scene = transition.toScene;
transition.type = TransitionType::None;
// Trigger Python scene lifecycle events
McRFPy_API::triggerSceneChange(transition.fromScene, transition.toScene);
}
else
{
// Render transition
render_target->clear();
transition.render(*render_target);
}
}
else
{
// Normal scene rendering
currentScene()->render();
}
currentScene()->render();
// Display the frame
if (headless) {
@ -244,12 +150,8 @@ void GameEngine::run()
currentFrame++;
frameTime = clock.restart().asSeconds();
fps = 1 / frameTime;
// Update profiling metrics
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
int whole_fps = metrics.fps;
int tenth_fps = (metrics.fps * 10) % 10;
int whole_fps = (int)fps;
int tenth_fps = int(fps * 100) % 10;
if (!headless && window) {
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
@ -260,18 +162,6 @@ void GameEngine::run()
running = false;
}
}
// Clean up before exiting the run loop
cleanup();
}
std::shared_ptr<PyTimerCallable> GameEngine::getTimer(const std::string& name)
{
auto it = timers.find(name);
if (it != timers.end()) {
return it->second;
}
return nullptr;
}
void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
@ -318,15 +208,9 @@ void GameEngine::processEvent(const sf::Event& event)
int actionCode = 0;
if (event.type == sf::Event::Closed) { running = false; return; }
// Handle window resize events
// TODO: add resize event to Scene to react; call it after constructor too, maybe
else if (event.type == sf::Event::Resized) {
// Update the view to match the new window size
sf::FloatRect visibleArea(0, 0, event.size.width, event.size.height);
visible = sf::View(visibleArea);
render_target->setView(visible);
// Notify Python scenes about the resize
McRFPy_API::triggerResize(event.size.width, event.size.height);
return; // 7DRL short circuit. Resizing manually disabled
}
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
@ -386,27 +270,3 @@ std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(s
if (scenes.count(target) == 0) return NULL;
return scenes[target]->ui_elements;
}
void GameEngine::setWindowTitle(const std::string& title)
{
window_title = title;
if (!headless && window) {
window->setTitle(title);
}
}
void GameEngine::setVSync(bool enabled)
{
vsync_enabled = enabled;
if (!headless && window) {
window->setVerticalSyncEnabled(enabled);
}
}
void GameEngine::setFramerateLimit(unsigned int limit)
{
framerate_limit = limit;
if (!headless && window) {
window->setFramerateLimit(limit);
}
}

View File

@ -8,7 +8,6 @@
#include "PyCallable.h"
#include "McRogueFaceConfig.h"
#include "HeadlessRenderer.h"
#include "SceneTransition.h"
#include <memory>
class GameEngine
@ -29,63 +28,19 @@ class GameEngine
bool headless = false;
McRogueFaceConfig config;
bool cleaned_up = false;
// Window state tracking
bool vsync_enabled = false;
unsigned int framerate_limit = 60;
// Scene transition state
SceneTransition transition;
void testTimers();
public:
sf::Clock runtime;
//std::map<std::string, Timer> timers;
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
void testTimers();
public:
std::string scene;
// Profiling metrics
struct ProfilingMetrics {
float frameTime = 0.0f; // Current frame time in milliseconds
float avgFrameTime = 0.0f; // Average frame time over last N frames
int fps = 0; // Frames per second
int drawCalls = 0; // Draw calls per frame
int uiElements = 0; // Number of UI elements rendered
int visibleElements = 0; // Number of visible elements
// Frame time history for averaging
static constexpr int HISTORY_SIZE = 60;
float frameTimeHistory[HISTORY_SIZE] = {0};
int historyIndex = 0;
void updateFrameTime(float deltaMs) {
frameTime = deltaMs;
frameTimeHistory[historyIndex] = deltaMs;
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
// Calculate average
float sum = 0.0f;
for (int i = 0; i < HISTORY_SIZE; ++i) {
sum += frameTimeHistory[i];
}
avgFrameTime = sum / HISTORY_SIZE;
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
}
void resetPerFrame() {
drawCalls = 0;
uiElements = 0;
visibleElements = 0;
}
} metrics;
GameEngine();
GameEngine(const McRogueFaceConfig& cfg);
~GameEngine();
Scene* currentScene();
void changeScene(std::string);
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
void createScene(std::string);
void quit();
void setPause(bool);
@ -95,24 +50,14 @@ public:
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
void run();
void sUserInput();
void cleanup(); // Clean up Python references before destruction
int getFrame() { return currentFrame; }
float getFrameTime() { return frameTime; }
sf::View getView() { return visible; }
void manageTimer(std::string, PyObject*, int);
std::shared_ptr<PyTimerCallable> getTimer(const std::string& name);
void setWindowScale(float);
bool isHeadless() const { return headless; }
void processEvent(const sf::Event& event);
// Window property accessors
const std::string& getWindowTitle() const { return window_title; }
void setWindowTitle(const std::string& title);
bool getVSync() const { return vsync_enabled; }
void setVSync(bool enabled);
unsigned int getFramerateLimit() const { return framerate_limit; }
void setFramerateLimit(unsigned int limit);
// global textures for scripts to access
std::vector<IndexTexture> textures;

View File

@ -2,10 +2,6 @@
#include "McRFPy_Automation.h"
#include "platform.h"
#include "PyAnimation.h"
#include "PyDrawable.h"
#include "PyTimer.h"
#include "PyWindow.h"
#include "PySceneObject.h"
#include "GameEngine.h"
#include "UI.h"
#include "Resources.h"
@ -13,9 +9,9 @@
#include <filesystem>
#include <cstring>
std::vector<sf::SoundBuffer>* McRFPy_API::soundbuffers = nullptr;
sf::Music* McRFPy_API::music = nullptr;
sf::Sound* McRFPy_API::sfx = nullptr;
std::vector<sf::SoundBuffer> McRFPy_API::soundbuffers;
sf::Music McRFPy_API::music;
sf::Sound McRFPy_API::sfx;
std::shared_ptr<PyFont> McRFPy_API::default_font;
std::shared_ptr<PyTexture> McRFPy_API::default_texture;
@ -35,7 +31,7 @@ static PyMethodDef mcrfpyMethods[] = {
{"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS, "sceneUI(scene) - Returns a list of UI elements"},
{"currentScene", McRFPy_API::_currentScene, METH_VARARGS, "currentScene() - Current scene's name. Returns a string"},
{"setScene", McRFPy_API::_setScene, METH_VARARGS, "setScene(scene, transition=None, duration=0.0) - transition to a different scene. Transition can be 'fade', 'slide_left', 'slide_right', 'slide_up', or 'slide_down'"},
{"setScene", McRFPy_API::_setScene, METH_VARARGS, "setScene(scene) - transition to a different scene"},
{"createScene", McRFPy_API::_createScene, METH_VARARGS, "createScene(scene) - create a new blank scene with given name"},
{"keypressScene", McRFPy_API::_keypressScene, METH_VARARGS, "keypressScene(callable) - assign a callable object to the current scene receive keypress events"},
@ -43,12 +39,6 @@ static PyMethodDef mcrfpyMethods[] = {
{"delTimer", McRFPy_API::_delTimer, METH_VARARGS, "delTimer(name:str) - stop calling the timer labelled with `name`"},
{"exit", McRFPy_API::_exit, METH_VARARGS, "exit() - close down the game engine"},
{"setScale", McRFPy_API::_setScale, METH_VARARGS, "setScale(multiplier:float) - resize the window (still 1024x768, but bigger)"},
{"find", McRFPy_API::_find, METH_VARARGS, "find(name, scene=None) - find first UI element with given name"},
{"findAll", McRFPy_API::_findAll, METH_VARARGS, "findAll(pattern, scene=None) - find all UI elements matching name pattern (supports * wildcards)"},
{"getMetrics", McRFPy_API::_getMetrics, METH_VARARGS, "getMetrics() - get performance metrics (returns dict)"},
{NULL, NULL, 0, NULL}
};
@ -79,9 +69,6 @@ PyObject* PyInit_mcrfpy()
/*SFML exposed types*/
&PyColorType, /*&PyLinkedColorType,*/ &PyFontType, &PyTextureType, &PyVectorType,
/*Base classes*/
&PyDrawableType,
/*UI widgets*/
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
@ -94,26 +81,7 @@ PyObject* PyInit_mcrfpy()
/*animation*/
&PyAnimationType,
/*timer*/
&PyTimerType,
/*window singleton*/
&PyWindowType,
/*scene class*/
&PySceneType,
nullptr};
// Set up PyWindowType methods and getsetters before PyType_Ready
PyWindowType.tp_methods = PyWindow::methods;
PyWindowType.tp_getset = PyWindow::getsetters;
// Set up PySceneType methods and getsetters
PySceneType.tp_methods = PySceneClass::methods;
PySceneType.tp_getset = PySceneClass::getsetters;
int i = 0;
auto t = pytypes[i];
while (t != nullptr)
@ -132,7 +100,8 @@ PyObject* PyInit_mcrfpy()
// Add default_font and default_texture to module
McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf");
McRFPy_API::default_texture = std::make_shared<PyTexture>("assets/kenney_tinydungeon.png", 16, 16);
// These will be set later when the window is created
//PyModule_AddObject(m, "default_font", McRFPy_API::default_font->pyObject());
//PyModule_AddObject(m, "default_texture", McRFPy_API::default_texture->pyObject());
PyModule_AddObject(m, "default_font", Py_None);
PyModule_AddObject(m, "default_texture", Py_None);
@ -169,11 +138,6 @@ PyStatus init_python(const char *program_name)
PyConfig_InitIsolatedConfig(&config);
config.dev_mode = 0;
// Configure UTF-8 for stdio
PyConfig_SetString(&config, &config.stdio_encoding, L"UTF-8");
PyConfig_SetString(&config, &config.stdio_errors, L"surrogateescape");
config.configure_c_stdio = 1;
PyConfig_SetBytesString(&config, &config.home,
narrow_string(executable_path() + L"/lib/Python").c_str());
@ -220,11 +184,6 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, in
PyConfig pyconfig;
PyConfig_InitIsolatedConfig(&pyconfig);
// Configure UTF-8 for stdio
PyConfig_SetString(&pyconfig, &pyconfig.stdio_encoding, L"UTF-8");
PyConfig_SetString(&pyconfig, &pyconfig.stdio_errors, L"surrogateescape");
pyconfig.configure_c_stdio = 1;
// CRITICAL: Pass actual command line arguments to Python
status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
if (PyStatus_Exception(status)) {
@ -380,23 +339,6 @@ void McRFPy_API::executeScript(std::string filename)
void McRFPy_API::api_shutdown()
{
// Clean up audio resources in correct order
if (sfx) {
sfx->stop();
delete sfx;
sfx = nullptr;
}
if (music) {
music->stop();
delete music;
music = nullptr;
}
if (soundbuffers) {
soundbuffers->clear();
delete soundbuffers;
soundbuffers = nullptr;
}
Py_Finalize();
}
@ -431,29 +373,25 @@ PyObject* McRFPy_API::_refreshFov(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_createSoundBuffer(PyObject* self, PyObject* args) {
const char *fn_cstr;
if (!PyArg_ParseTuple(args, "s", &fn_cstr)) return NULL;
// Initialize soundbuffers if needed
if (!McRFPy_API::soundbuffers) {
McRFPy_API::soundbuffers = new std::vector<sf::SoundBuffer>();
}
auto b = sf::SoundBuffer();
b.loadFromFile(fn_cstr);
McRFPy_API::soundbuffers->push_back(b);
McRFPy_API::soundbuffers.push_back(b);
Py_INCREF(Py_None);
return Py_None;
}
PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
const char *fn_cstr;
PyObject* loop_obj = Py_False;
PyObject* loop_obj;
if (!PyArg_ParseTuple(args, "s|O", &fn_cstr, &loop_obj)) return NULL;
// Initialize music if needed
if (!McRFPy_API::music) {
McRFPy_API::music = new sf::Music();
}
McRFPy_API::music->stop();
McRFPy_API::music->openFromFile(fn_cstr);
McRFPy_API::music->setLoop(PyObject_IsTrue(loop_obj));
McRFPy_API::music->play();
McRFPy_API::music.stop();
// get params for sf::Music initialization
//sf::InputSoundFile file;
//file.openFromFile(fn_cstr);
McRFPy_API::music.openFromFile(fn_cstr);
McRFPy_API::music.setLoop(PyObject_IsTrue(loop_obj));
//McRFPy_API::music.initialize(file.getChannelCount(), file.getSampleRate());
McRFPy_API::music.play();
Py_INCREF(Py_None);
return Py_None;
}
@ -461,10 +399,7 @@ PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) {
int vol;
if (!PyArg_ParseTuple(args, "i", &vol)) return NULL;
if (!McRFPy_API::music) {
McRFPy_API::music = new sf::Music();
}
McRFPy_API::music->setVolume(vol);
McRFPy_API::music.setVolume(vol);
Py_INCREF(Py_None);
return Py_None;
}
@ -472,10 +407,7 @@ PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) {
float vol;
if (!PyArg_ParseTuple(args, "f", &vol)) return NULL;
if (!McRFPy_API::sfx) {
McRFPy_API::sfx = new sf::Sound();
}
McRFPy_API::sfx->setVolume(vol);
McRFPy_API::sfx.setVolume(vol);
Py_INCREF(Py_None);
return Py_None;
}
@ -483,29 +415,20 @@ PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_playSound(PyObject* self, PyObject* args) {
float index;
if (!PyArg_ParseTuple(args, "f", &index)) return NULL;
if (!McRFPy_API::soundbuffers || index >= McRFPy_API::soundbuffers->size()) return NULL;
if (!McRFPy_API::sfx) {
McRFPy_API::sfx = new sf::Sound();
}
McRFPy_API::sfx->stop();
McRFPy_API::sfx->setBuffer((*McRFPy_API::soundbuffers)[index]);
McRFPy_API::sfx->play();
if (index >= McRFPy_API::soundbuffers.size()) return NULL;
McRFPy_API::sfx.stop();
McRFPy_API::sfx.setBuffer(McRFPy_API::soundbuffers[index]);
McRFPy_API::sfx.play();
Py_INCREF(Py_None);
return Py_None;
}
PyObject* McRFPy_API::_getMusicVolume(PyObject* self, PyObject* args) {
if (!McRFPy_API::music) {
return Py_BuildValue("f", 0.0f);
}
return Py_BuildValue("f", McRFPy_API::music->getVolume());
return Py_BuildValue("f", McRFPy_API::music.getVolume());
}
PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) {
if (!McRFPy_API::sfx) {
return Py_BuildValue("f", 0.0f);
}
return Py_BuildValue("f", McRFPy_API::sfx->getVolume());
return Py_BuildValue("f", McRFPy_API::sfx.getVolume());
}
// Removed deprecated player_input, computerTurn, playerTurn functions
@ -558,24 +481,8 @@ PyObject* McRFPy_API::_currentScene(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_setScene(PyObject* self, PyObject* args) {
const char* newscene;
const char* transition_str = nullptr;
float duration = 0.0f;
// Parse arguments: scene name, optional transition type, optional duration
if (!PyArg_ParseTuple(args, "s|sf", &newscene, &transition_str, &duration)) return NULL;
// Map transition string to enum
TransitionType transition_type = TransitionType::None;
if (transition_str) {
std::string trans(transition_str);
if (trans == "fade") transition_type = TransitionType::Fade;
else if (trans == "slide_left") transition_type = TransitionType::SlideLeft;
else if (trans == "slide_right") transition_type = TransitionType::SlideRight;
else if (trans == "slide_up") transition_type = TransitionType::SlideUp;
else if (trans == "slide_down") transition_type = TransitionType::SlideDown;
}
game->changeScene(newscene, transition_type, duration);
if (!PyArg_ParseTuple(args, "s", &newscene)) return NULL;
game->changeScene(newscene);
Py_INCREF(Py_None);
return Py_None;
}
@ -660,283 +567,3 @@ void McRFPy_API::markSceneNeedsSort() {
}
}
}
// Helper function to check if a name matches a pattern with wildcards
static bool name_matches_pattern(const std::string& name, const std::string& pattern) {
if (pattern.find('*') == std::string::npos) {
// No wildcards, exact match
return name == pattern;
}
// Simple wildcard matching - * matches any sequence
size_t name_pos = 0;
size_t pattern_pos = 0;
while (pattern_pos < pattern.length() && name_pos < name.length()) {
if (pattern[pattern_pos] == '*') {
// Skip consecutive stars
while (pattern_pos < pattern.length() && pattern[pattern_pos] == '*') {
pattern_pos++;
}
if (pattern_pos == pattern.length()) {
// Pattern ends with *, matches rest of name
return true;
}
// Find next non-star character in pattern
char next_char = pattern[pattern_pos];
while (name_pos < name.length() && name[name_pos] != next_char) {
name_pos++;
}
} else if (pattern[pattern_pos] == name[name_pos]) {
pattern_pos++;
name_pos++;
} else {
return false;
}
}
// Skip trailing stars in pattern
while (pattern_pos < pattern.length() && pattern[pattern_pos] == '*') {
pattern_pos++;
}
return pattern_pos == pattern.length() && name_pos == name.length();
}
// Helper to recursively search a collection for named elements
static void find_in_collection(std::vector<std::shared_ptr<UIDrawable>>* collection, const std::string& pattern,
bool find_all, PyObject* results) {
if (!collection) return;
for (auto& drawable : *collection) {
if (!drawable) continue;
// Check this element's name
if (name_matches_pattern(drawable->name, pattern)) {
// Convert to Python object using RET_PY_INSTANCE logic
PyObject* py_obj = nullptr;
switch (drawable->derived_type()) {
case PyObjectsEnum::UIFRAME: {
auto frame = std::static_pointer_cast<UIFrame>(drawable);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (o) {
o->data = frame;
py_obj = (PyObject*)o;
}
break;
}
case PyObjectsEnum::UICAPTION: {
auto caption = std::static_pointer_cast<UICaption>(drawable);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
auto o = (PyUICaptionObject*)type->tp_alloc(type, 0);
if (o) {
o->data = caption;
py_obj = (PyObject*)o;
}
break;
}
case PyObjectsEnum::UISPRITE: {
auto sprite = std::static_pointer_cast<UISprite>(drawable);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
auto o = (PyUISpriteObject*)type->tp_alloc(type, 0);
if (o) {
o->data = sprite;
py_obj = (PyObject*)o;
}
break;
}
case PyObjectsEnum::UIGRID: {
auto grid = std::static_pointer_cast<UIGrid>(drawable);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
auto o = (PyUIGridObject*)type->tp_alloc(type, 0);
if (o) {
o->data = grid;
py_obj = (PyObject*)o;
}
break;
}
default:
break;
}
if (py_obj) {
if (find_all) {
PyList_Append(results, py_obj);
Py_DECREF(py_obj);
} else {
// For find (not findAll), we store in results and return early
PyList_Append(results, py_obj);
Py_DECREF(py_obj);
return;
}
}
}
// Recursively search in Frame children
if (drawable->derived_type() == PyObjectsEnum::UIFRAME) {
auto frame = std::static_pointer_cast<UIFrame>(drawable);
find_in_collection(frame->children.get(), pattern, find_all, results);
if (!find_all && PyList_Size(results) > 0) {
return; // Found one, stop searching
}
}
}
}
// Also search Grid entities
static void find_in_grid_entities(UIGrid* grid, const std::string& pattern,
bool find_all, PyObject* results) {
if (!grid || !grid->entities) return;
for (auto& entity : *grid->entities) {
if (!entity) continue;
// Entities delegate name to their sprite
if (name_matches_pattern(entity->sprite.name, pattern)) {
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
if (o) {
o->data = entity;
PyObject* py_obj = (PyObject*)o;
if (find_all) {
PyList_Append(results, py_obj);
Py_DECREF(py_obj);
} else {
PyList_Append(results, py_obj);
Py_DECREF(py_obj);
return;
}
}
}
}
}
PyObject* McRFPy_API::_find(PyObject* self, PyObject* args) {
const char* name;
const char* scene_name = nullptr;
if (!PyArg_ParseTuple(args, "s|s", &name, &scene_name)) {
return NULL;
}
PyObject* results = PyList_New(0);
// Get the UI elements to search
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;
if (scene_name) {
// Search specific scene
ui_elements = game->scene_ui(scene_name);
if (!ui_elements) {
PyErr_Format(PyExc_ValueError, "Scene '%s' not found", scene_name);
Py_DECREF(results);
return NULL;
}
} else {
// Search current scene
Scene* current = game->currentScene();
if (!current) {
PyErr_SetString(PyExc_RuntimeError, "No current scene");
Py_DECREF(results);
return NULL;
}
ui_elements = current->ui_elements;
}
// Search the scene's UI elements
find_in_collection(ui_elements.get(), name, false, results);
// Also search all grids in the scene for entities
if (PyList_Size(results) == 0 && ui_elements) {
for (auto& drawable : *ui_elements) {
if (drawable && drawable->derived_type() == PyObjectsEnum::UIGRID) {
auto grid = std::static_pointer_cast<UIGrid>(drawable);
find_in_grid_entities(grid.get(), name, false, results);
if (PyList_Size(results) > 0) break;
}
}
}
// Return the first result or None
if (PyList_Size(results) > 0) {
PyObject* result = PyList_GetItem(results, 0);
Py_INCREF(result);
Py_DECREF(results);
return result;
}
Py_DECREF(results);
Py_RETURN_NONE;
}
PyObject* McRFPy_API::_findAll(PyObject* self, PyObject* args) {
const char* pattern;
const char* scene_name = nullptr;
if (!PyArg_ParseTuple(args, "s|s", &pattern, &scene_name)) {
return NULL;
}
PyObject* results = PyList_New(0);
// Get the UI elements to search
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;
if (scene_name) {
// Search specific scene
ui_elements = game->scene_ui(scene_name);
if (!ui_elements) {
PyErr_Format(PyExc_ValueError, "Scene '%s' not found", scene_name);
Py_DECREF(results);
return NULL;
}
} else {
// Search current scene
Scene* current = game->currentScene();
if (!current) {
PyErr_SetString(PyExc_RuntimeError, "No current scene");
Py_DECREF(results);
return NULL;
}
ui_elements = current->ui_elements;
}
// Search the scene's UI elements
find_in_collection(ui_elements.get(), pattern, true, results);
// Also search all grids in the scene for entities
if (ui_elements) {
for (auto& drawable : *ui_elements) {
if (drawable && drawable->derived_type() == PyObjectsEnum::UIGRID) {
auto grid = std::static_pointer_cast<UIGrid>(drawable);
find_in_grid_entities(grid.get(), pattern, true, results);
}
}
}
return results;
}
PyObject* McRFPy_API::_getMetrics(PyObject* self, PyObject* args) {
// Create a dictionary with metrics
PyObject* dict = PyDict_New();
if (!dict) return NULL;
// Add frame time metrics
PyDict_SetItemString(dict, "frame_time", PyFloat_FromDouble(game->metrics.frameTime));
PyDict_SetItemString(dict, "avg_frame_time", PyFloat_FromDouble(game->metrics.avgFrameTime));
PyDict_SetItemString(dict, "fps", PyLong_FromLong(game->metrics.fps));
// Add draw call metrics
PyDict_SetItemString(dict, "draw_calls", PyLong_FromLong(game->metrics.drawCalls));
PyDict_SetItemString(dict, "ui_elements", PyLong_FromLong(game->metrics.uiElements));
PyDict_SetItemString(dict, "visible_elements", PyLong_FromLong(game->metrics.visibleElements));
// Add general metrics
PyDict_SetItemString(dict, "current_frame", PyLong_FromLong(game->getFrame()));
PyDict_SetItemString(dict, "runtime", PyFloat_FromDouble(game->runtime.getElapsedTime().asSeconds()));
return dict;
}

View File

@ -36,9 +36,9 @@ public:
static void REPL_device(FILE * fp, const char *filename);
static void REPL();
static std::vector<sf::SoundBuffer>* soundbuffers;
static sf::Music* music;
static sf::Sound* sfx;
static std::vector<sf::SoundBuffer> soundbuffers;
static sf::Music music;
static sf::Sound sfx;
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
@ -73,16 +73,4 @@ public:
// Helper to mark scenes as needing z_index resort
static void markSceneNeedsSort();
// Name-based finding methods
static PyObject* _find(PyObject*, PyObject*);
static PyObject* _findAll(PyObject*, PyObject*);
// Profiling/metrics
static PyObject* _getMetrics(PyObject*, PyObject*);
// Scene lifecycle management for Python Scene objects
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
static void updatePythonScenes(float dt);
static void triggerResize(int width, int height);
};

View File

@ -16,24 +16,21 @@ PyObject* PyCallable::call(PyObject* args, PyObject* kwargs)
return PyObject_Call(target, args, kwargs);
}
bool PyCallable::isNone() const
bool PyCallable::isNone()
{
return (target == Py_None || target == NULL);
}
PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now)
: PyCallable(_target), interval(_interval), last_ran(now),
paused(false), pause_start_time(0), total_paused_time(0)
: PyCallable(_target), interval(_interval), last_ran(now)
{}
PyTimerCallable::PyTimerCallable()
: PyCallable(Py_None), interval(0), last_ran(0),
paused(false), pause_start_time(0), total_paused_time(0)
: PyCallable(Py_None), interval(0), last_ran(0)
{}
bool PyTimerCallable::hasElapsed(int now)
{
if (paused) return false;
return now >= last_ran + interval;
}
@ -63,62 +60,6 @@ bool PyTimerCallable::test(int now)
return false;
}
void PyTimerCallable::pause(int current_time)
{
if (!paused) {
paused = true;
pause_start_time = current_time;
}
}
void PyTimerCallable::resume(int current_time)
{
if (paused) {
paused = false;
int paused_duration = current_time - pause_start_time;
total_paused_time += paused_duration;
// Adjust last_ran to account for the pause
last_ran += paused_duration;
}
}
void PyTimerCallable::restart(int current_time)
{
last_ran = current_time;
paused = false;
pause_start_time = 0;
total_paused_time = 0;
}
void PyTimerCallable::cancel()
{
// Cancel by setting target to None
if (target && target != Py_None) {
Py_DECREF(target);
}
target = Py_None;
Py_INCREF(Py_None);
}
int PyTimerCallable::getRemaining(int current_time) const
{
if (paused) {
// When paused, calculate time remaining from when it was paused
int elapsed_when_paused = pause_start_time - last_ran;
return interval - elapsed_when_paused;
}
int elapsed = current_time - last_ran;
return interval - elapsed;
}
void PyTimerCallable::setCallback(PyObject* new_callback)
{
if (target && target != Py_None) {
Py_DECREF(target);
}
target = Py_XNewRef(new_callback);
}
PyClickCallable::PyClickCallable(PyObject* _target)
: PyCallable(_target)
{}

View File

@ -10,7 +10,7 @@ protected:
~PyCallable();
PyObject* call(PyObject*, PyObject*);
public:
bool isNone() const;
bool isNone();
};
class PyTimerCallable: public PyCallable
@ -19,32 +19,11 @@ private:
int interval;
int last_ran;
void call(int);
// Pause/resume support
bool paused;
int pause_start_time;
int total_paused_time;
public:
bool hasElapsed(int);
bool test(int);
PyTimerCallable(PyObject*, int, int);
PyTimerCallable();
// Timer control methods
void pause(int current_time);
void resume(int current_time);
void restart(int current_time);
void cancel();
// Timer state queries
bool isPaused() const { return paused; }
bool isActive() const { return !isNone() && !paused; }
int getInterval() const { return interval; }
void setInterval(int new_interval) { interval = new_interval; }
int getRemaining(int current_time) const;
PyObject* getCallback() { return target; }
void setCallback(PyObject* new_callback);
};
class PyClickCallable: public PyCallable

View File

@ -2,8 +2,6 @@
#include "McRFPy_API.h"
#include "PyObjectUtils.h"
#include "PyRAII.h"
#include <string>
#include <cstdio>
PyGetSetDef PyColor::getsetters[] = {
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0},
@ -13,13 +11,6 @@ PyGetSetDef PyColor::getsetters[] = {
{NULL}
};
PyMethodDef PyColor::methods[] = {
{"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS, "Create Color from hex string (e.g., '#FF0000' or 'FF0000')"},
{"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS, "Convert Color to hex string"},
{"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS, "Linearly interpolate between this color and another"},
{NULL}
};
PyColor::PyColor(sf::Color target)
:data(target) {}
@ -226,105 +217,3 @@ PyColorObject* PyColor::from_arg(PyObject* args)
// Release ownership and return
return (PyColorObject*)obj.release();
}
// Color helper method implementations
PyObject* PyColor::from_hex(PyObject* cls, PyObject* args)
{
const char* hex_str;
if (!PyArg_ParseTuple(args, "s", &hex_str)) {
return NULL;
}
std::string hex(hex_str);
// Remove # if present
if (hex.length() > 0 && hex[0] == '#') {
hex = hex.substr(1);
}
// Validate hex string
if (hex.length() != 6 && hex.length() != 8) {
PyErr_SetString(PyExc_ValueError, "Hex string must be 6 or 8 characters (RGB or RGBA)");
return NULL;
}
// Parse hex values
try {
unsigned int r = std::stoul(hex.substr(0, 2), nullptr, 16);
unsigned int g = std::stoul(hex.substr(2, 2), nullptr, 16);
unsigned int b = std::stoul(hex.substr(4, 2), nullptr, 16);
unsigned int a = 255;
if (hex.length() == 8) {
a = std::stoul(hex.substr(6, 2), nullptr, 16);
}
// Create new Color object
PyTypeObject* type = (PyTypeObject*)cls;
PyColorObject* color = (PyColorObject*)type->tp_alloc(type, 0);
if (color) {
color->data = sf::Color(r, g, b, a);
}
return (PyObject*)color;
} catch (const std::exception& e) {
PyErr_SetString(PyExc_ValueError, "Invalid hex string");
return NULL;
}
}
PyObject* PyColor::to_hex(PyColorObject* self, PyObject* Py_UNUSED(ignored))
{
char hex[10]; // #RRGGBBAA + null terminator
// Include alpha only if not fully opaque
if (self->data.a < 255) {
snprintf(hex, sizeof(hex), "#%02X%02X%02X%02X",
self->data.r, self->data.g, self->data.b, self->data.a);
} else {
snprintf(hex, sizeof(hex), "#%02X%02X%02X",
self->data.r, self->data.g, self->data.b);
}
return PyUnicode_FromString(hex);
}
PyObject* PyColor::lerp(PyColorObject* self, PyObject* args)
{
PyObject* other_obj;
float t;
if (!PyArg_ParseTuple(args, "Of", &other_obj, &t)) {
return NULL;
}
// Validate other color
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
if (!PyObject_IsInstance(other_obj, (PyObject*)type)) {
Py_DECREF(type);
PyErr_SetString(PyExc_TypeError, "First argument must be a Color");
return NULL;
}
PyColorObject* other = (PyColorObject*)other_obj;
// Clamp t to [0, 1]
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
// Perform linear interpolation
sf::Uint8 r = static_cast<sf::Uint8>(self->data.r + (other->data.r - self->data.r) * t);
sf::Uint8 g = static_cast<sf::Uint8>(self->data.g + (other->data.g - self->data.g) * t);
sf::Uint8 b = static_cast<sf::Uint8>(self->data.b + (other->data.b - self->data.b) * t);
sf::Uint8 a = static_cast<sf::Uint8>(self->data.a + (other->data.a - self->data.a) * t);
// Create new Color object
PyColorObject* result = (PyColorObject*)type->tp_alloc(type, 0);
Py_DECREF(type);
if (result) {
result->data = sf::Color(r, g, b, a);
}
return (PyObject*)result;
}

View File

@ -28,13 +28,7 @@ public:
static PyObject* get_member(PyObject*, void*);
static int set_member(PyObject*, PyObject*, void*);
// Color helper methods
static PyObject* from_hex(PyObject* cls, PyObject* args);
static PyObject* to_hex(PyColorObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* lerp(PyColorObject* self, PyObject* args);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
static PyColorObject* from_arg(PyObject*);
};
@ -48,7 +42,6 @@ namespace mcrfpydef {
.tp_hash = PyColor::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Color Object"),
.tp_methods = PyColor::methods,
.tp_getset = PyColor::getsetters,
.tp_init = (initproc)PyColor::init,
.tp_new = PyColor::pynew,

View File

@ -1,179 +0,0 @@
#include "PyDrawable.h"
#include "McRFPy_API.h"
// Click property getter
static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
{
if (!self->data->click_callable)
Py_RETURN_NONE;
PyObject* ptr = self->data->click_callable->borrow();
if (ptr && ptr != Py_None)
return ptr;
else
Py_RETURN_NONE;
}
// Click property setter
static int PyDrawable_set_click(PyDrawableObject* self, PyObject* value, void* closure)
{
if (value == Py_None) {
self->data->click_unregister();
} else if (PyCallable_Check(value)) {
self->data->click_register(value);
} else {
PyErr_SetString(PyExc_TypeError, "click must be callable or None");
return -1;
}
return 0;
}
// Z-index property getter
static PyObject* PyDrawable_get_z_index(PyDrawableObject* self, void* closure)
{
return PyLong_FromLong(self->data->z_index);
}
// Z-index property setter
static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure)
{
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
return -1;
}
int val = PyLong_AsLong(value);
self->data->z_index = val;
// Mark scene as needing resort
self->data->notifyZIndexChanged();
return 0;
}
// Visible property getter (new for #87)
static PyObject* PyDrawable_get_visible(PyDrawableObject* self, void* closure)
{
return PyBool_FromLong(self->data->visible);
}
// Visible property setter (new for #87)
static int PyDrawable_set_visible(PyDrawableObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
self->data->visible = (value == Py_True);
return 0;
}
// Opacity property getter (new for #88)
static PyObject* PyDrawable_get_opacity(PyDrawableObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->opacity);
}
// Opacity property setter (new for #88)
static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void* closure)
{
float val;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = PyLong_AsLong(value);
} else {
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1;
}
// Clamp to valid range
if (val < 0.0f) val = 0.0f;
if (val > 1.0f) val = 1.0f;
self->data->opacity = val;
return 0;
}
// GetSetDef array for properties
static PyGetSetDef PyDrawable_getsetters[] = {
{"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
"Callable executed when object is clicked", NULL},
{"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index,
"Z-order for rendering (lower values rendered first)", NULL},
{"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible,
"Whether the object is visible", NULL},
{"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity,
"Opacity level (0.0 = transparent, 1.0 = opaque)", NULL},
{NULL} // Sentinel
};
// get_bounds method implementation (#89)
static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUSED(args))
{
auto bounds = self->data->get_bounds();
return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height);
}
// move method implementation (#98)
static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args)
{
float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
return NULL;
}
self->data->move(dx, dy);
Py_RETURN_NONE;
}
// resize method implementation (#98)
static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args)
{
float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
return NULL;
}
self->data->resize(w, h);
Py_RETURN_NONE;
}
// Method definitions
static PyMethodDef PyDrawable_methods[] = {
{"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS,
"Get bounding box as (x, y, width, height)"},
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS,
"Move by relative offset (dx, dy)"},
{"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS,
"Resize to new dimensions (width, height)"},
{NULL} // Sentinel
};
// Type initialization
static int PyDrawable_init(PyDrawableObject* self, PyObject* args, PyObject* kwds)
{
PyErr_SetString(PyExc_TypeError, "_Drawable is an abstract base class and cannot be instantiated directly");
return -1;
}
namespace mcrfpydef {
PyTypeObject PyDrawableType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy._Drawable",
.tp_basicsize = sizeof(PyDrawableObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) {
PyDrawableObject* obj = (PyDrawableObject*)self;
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_flags = Py_TPFLAGS_DEFAULT, // | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Base class for all drawable UI elements"),
.tp_methods = PyDrawable_methods,
.tp_getset = PyDrawable_getsetters,
.tp_init = (initproc)PyDrawable_init,
.tp_new = PyType_GenericNew,
};
}

View File

@ -1,15 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "UIDrawable.h"
// Python object structure for UIDrawable base class
typedef struct {
PyObject_HEAD
std::shared_ptr<UIDrawable> data;
} PyDrawableObject;
// Declare the Python type for _Drawable base class
namespace mcrfpydef {
extern PyTypeObject PyDrawableType;
}

View File

@ -1,164 +0,0 @@
#pragma once
#include "Python.h"
#include "PyVector.h"
#include "McRFPy_API.h"
// Helper class for standardized position argument parsing across UI classes
class PyPositionHelper {
public:
// Template structure for parsing results
struct ParseResult {
float x = 0.0f;
float y = 0.0f;
bool has_position = false;
};
struct ParseResultInt {
int x = 0;
int y = 0;
bool has_position = false;
};
// Parse position from multiple formats for UI class constructors
// Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector
static ParseResult parse_position(PyObject* args, PyObject* kwds,
int* arg_index = nullptr)
{
ParseResult result;
float x = 0.0f, y = 0.0f;
PyObject* pos_obj = nullptr;
int start_index = arg_index ? *arg_index : 0;
// Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) > start_index + 1) {
PyObject* first = PyTuple_GetItem(args, start_index);
PyObject* second = PyTuple_GetItem(args, start_index + 1);
// Check if both are numbers
if ((PyFloat_Check(first) || PyLong_Check(first)) &&
(PyFloat_Check(second) || PyLong_Check(second))) {
x = PyFloat_Check(first) ? PyFloat_AsDouble(first) : PyLong_AsLong(first);
y = PyFloat_Check(second) ? PyFloat_AsDouble(second) : PyLong_AsLong(second);
result.x = x;
result.y = y;
result.has_position = true;
if (arg_index) *arg_index += 2;
return result;
}
}
// Check for single positional argument that might be tuple or Vector
if (!kwds && PyTuple_Size(args) > start_index) {
PyObject* first = PyTuple_GetItem(args, start_index);
PyVectorObject* vec = PyVector::from_arg(first);
if (vec) {
result.x = vec->data.x;
result.y = vec->data.y;
result.has_position = true;
if (arg_index) *arg_index += 1;
return result;
}
}
// Try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_kw = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj) {
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
}
if (pos_kw) {
PyVectorObject* vec = PyVector::from_arg(pos_kw);
if (vec) {
result.x = vec->data.x;
result.y = vec->data.y;
result.has_position = true;
return result;
}
}
}
return result;
}
// Parse integer position for Grid.at() and similar
static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds)
{
ParseResultInt result;
// Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) >= 2) {
PyObject* first = PyTuple_GetItem(args, 0);
PyObject* second = PyTuple_GetItem(args, 1);
if (PyLong_Check(first) && PyLong_Check(second)) {
result.x = PyLong_AsLong(first);
result.y = PyLong_AsLong(second);
result.has_position = true;
return result;
}
}
// Check for single tuple argument
if (!kwds && PyTuple_Size(args) == 1) {
PyObject* first = PyTuple_GetItem(args, 0);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* x_obj = PyTuple_GetItem(first, 0);
PyObject* y_obj = PyTuple_GetItem(first, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
}
}
// Try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj && PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
if (pos_obj && PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if (PyLong_Check(x_val) && PyLong_Check(y_val)) {
result.x = PyLong_AsLong(x_val);
result.y = PyLong_AsLong(y_val);
result.has_position = true;
return result;
}
}
}
return result;
}
// Error message helper
static void set_position_error() {
PyErr_SetString(PyExc_TypeError,
"Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), or pos=Vector");
}
static void set_position_int_error() {
PyErr_SetString(PyExc_TypeError,
"Position must be specified as: (x, y), x=x, y=y, ((x,y)), or pos=(x,y) with integer values");
}
};

View File

@ -29,19 +29,26 @@ void PyScene::do_mouse_input(std::string button, std::string type)
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos);
// Create a sorted copy by z-index (highest first)
std::vector<std::shared_ptr<UIDrawable>> sorted_elements(*ui_elements);
std::sort(sorted_elements.begin(), sorted_elements.end(),
[](const auto& a, const auto& b) { return a->z_index > b->z_index; });
// Check elements in z-order (top to bottom)
for (const auto& element : sorted_elements) {
if (!element->visible) continue;
if (auto target = element->click_at(sf::Vector2f(mousepos))) {
UIDrawable* target;
for (auto d: *ui_elements)
{
target = d->click_at(sf::Vector2f(mousepos));
if (target)
{
/*
PyObject* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), type.c_str());
PyObject* retval = PyObject_Call(target->click_callable, args, NULL);
if (!retval)
{
std::cout << "click_callable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else if (retval != Py_None)
{
std::cout << "click_callable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
}
*/
target->click_callable->call(mousepos, button, type);
return; // Stop after first handler
}
}
}
@ -72,16 +79,8 @@ void PyScene::render()
// Render in sorted order (no need to copy anymore)
for (auto e: *ui_elements)
{
if (e) {
// Track metrics
game->metrics.uiElements++;
if (e->visible) {
game->metrics.visibleElements++;
// Count this as a draw call (each visible element = 1+ draw calls)
game->metrics.drawCalls++;
}
if (e)
e->render();
}
}
// Display is handled by GameEngine

View File

@ -1,268 +0,0 @@
#include "PySceneObject.h"
#include "PyScene.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include <iostream>
// Static map to store Python scene objects by name
static std::map<std::string, PySceneObject*> python_scenes;
PyObject* PySceneClass::__new__(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
PySceneObject* self = (PySceneObject*)type->tp_alloc(type, 0);
if (self) {
self->initialized = false;
// Don't create C++ scene yet - wait for __init__
}
return (PyObject*)self;
}
int PySceneClass::__init__(PySceneObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = {"name", nullptr};
const char* name = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(keywords), &name)) {
return -1;
}
// Check if scene with this name already exists
if (python_scenes.count(name) > 0) {
PyErr_Format(PyExc_ValueError, "Scene with name '%s' already exists", name);
return -1;
}
self->name = name;
// Create the C++ PyScene
McRFPy_API::game->createScene(name);
// Get reference to the created scene
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
// Store this Python object in our registry
python_scenes[name] = self;
Py_INCREF(self); // Keep a reference
// Create a Python function that routes to on_keypress
// We'll register this after the object is fully initialized
self->initialized = true;
return 0;
}
void PySceneClass::__dealloc(PyObject* self_obj)
{
PySceneObject* self = (PySceneObject*)self_obj;
// Remove from registry
if (python_scenes.count(self->name) > 0 && python_scenes[self->name] == self) {
python_scenes.erase(self->name);
}
// Call Python object destructor
Py_TYPE(self)->tp_free(self);
}
PyObject* PySceneClass::__repr__(PySceneObject* self)
{
return PyUnicode_FromFormat("<Scene '%s'>", self->name.c_str());
}
PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args)
{
// Call the static method from McRFPy_API
PyObject* py_args = Py_BuildValue("(s)", self->name.c_str());
PyObject* result = McRFPy_API::_setScene(NULL, py_args);
Py_DECREF(py_args);
return result;
}
PyObject* PySceneClass::get_ui(PySceneObject* self, PyObject* args)
{
// Call the static method from McRFPy_API
PyObject* py_args = Py_BuildValue("(s)", self->name.c_str());
PyObject* result = McRFPy_API::_sceneUI(NULL, py_args);
Py_DECREF(py_args);
return result;
}
PyObject* PySceneClass::register_keyboard(PySceneObject* self, PyObject* args)
{
PyObject* callable;
if (!PyArg_ParseTuple(args, "O", &callable)) {
return NULL;
}
if (!PyCallable_Check(callable)) {
PyErr_SetString(PyExc_TypeError, "Argument must be callable");
return NULL;
}
// Store the callable
Py_INCREF(callable);
// Get the current scene and set its key_callable
GameEngine* game = McRFPy_API::game;
if (game) {
// We need to be on the right scene first
std::string old_scene = game->scene;
game->scene = self->name;
game->currentScene()->key_callable = std::make_unique<PyKeyCallable>(callable);
game->scene = old_scene;
}
Py_DECREF(callable);
Py_RETURN_NONE;
}
PyObject* PySceneClass::get_name(PySceneObject* self, void* closure)
{
return PyUnicode_FromString(self->name.c_str());
}
PyObject* PySceneClass::get_active(PySceneObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
Py_RETURN_FALSE;
}
return PyBool_FromLong(game->scene == self->name);
}
// Lifecycle callbacks
void PySceneClass::call_on_enter(PySceneObject* self)
{
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_enter");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallNoArgs(method);
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
}
Py_XDECREF(method);
}
void PySceneClass::call_on_exit(PySceneObject* self)
{
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_exit");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallNoArgs(method);
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
}
Py_XDECREF(method);
}
void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action)
{
PyGILState_STATE gstate = PyGILState_Ensure();
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_keypress");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallFunction(method, "ss", key.c_str(), action.c_str());
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
}
Py_XDECREF(method);
PyGILState_Release(gstate);
}
void PySceneClass::call_update(PySceneObject* self, float dt)
{
PyObject* method = PyObject_GetAttrString((PyObject*)self, "update");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallFunction(method, "f", dt);
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
}
Py_XDECREF(method);
}
void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
{
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_resize");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallFunction(method, "ii", width, height);
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
}
Py_XDECREF(method);
}
// Properties
PyGetSetDef PySceneClass::getsetters[] = {
{"name", (getter)get_name, NULL, "Scene name", NULL},
{"active", (getter)get_active, NULL, "Whether this scene is currently active", NULL},
{NULL}
};
// Methods
PyMethodDef PySceneClass::methods[] = {
{"activate", (PyCFunction)activate, METH_NOARGS,
"Make this the active scene"},
{"get_ui", (PyCFunction)get_ui, METH_NOARGS,
"Get the UI element collection for this scene"},
{"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS,
"Register a keyboard handler function (alternative to overriding on_keypress)"},
{NULL}
};
// Helper function to trigger lifecycle events
void McRFPy_API::triggerSceneChange(const std::string& from_scene, const std::string& to_scene)
{
// Call on_exit for the old scene
if (!from_scene.empty() && python_scenes.count(from_scene) > 0) {
PySceneClass::call_on_exit(python_scenes[from_scene]);
}
// Call on_enter for the new scene
if (!to_scene.empty() && python_scenes.count(to_scene) > 0) {
PySceneClass::call_on_enter(python_scenes[to_scene]);
}
}
// Helper function to update Python scenes
void McRFPy_API::updatePythonScenes(float dt)
{
GameEngine* game = McRFPy_API::game;
if (!game) return;
// Only update the active scene
if (python_scenes.count(game->scene) > 0) {
PySceneClass::call_update(python_scenes[game->scene], dt);
}
}
// Helper function to trigger resize events on Python scenes
void McRFPy_API::triggerResize(int width, int height)
{
GameEngine* game = McRFPy_API::game;
if (!game) return;
// Only notify the active scene
if (python_scenes.count(game->scene) > 0) {
PySceneClass::call_on_resize(python_scenes[game->scene], width, height);
}
}

View File

@ -1,63 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <string>
#include <memory>
// Forward declarations
class PyScene;
// Python object structure for Scene
typedef struct {
PyObject_HEAD
std::string name;
std::shared_ptr<PyScene> scene; // Reference to the C++ scene
bool initialized;
} PySceneObject;
// C++ interface for Python Scene class
class PySceneClass
{
public:
// Type methods
static PyObject* __new__(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int __init__(PySceneObject* self, PyObject* args, PyObject* kwds);
static void __dealloc(PyObject* self);
static PyObject* __repr__(PySceneObject* self);
// Scene methods
static PyObject* activate(PySceneObject* self, PyObject* args);
static PyObject* get_ui(PySceneObject* self, PyObject* args);
static PyObject* register_keyboard(PySceneObject* self, PyObject* args);
// Properties
static PyObject* get_name(PySceneObject* self, void* closure);
static PyObject* get_active(PySceneObject* self, void* closure);
// Lifecycle callbacks (called from C++)
static void call_on_enter(PySceneObject* self);
static void call_on_exit(PySceneObject* self);
static void call_on_keypress(PySceneObject* self, std::string key, std::string action);
static void call_update(PySceneObject* self, float dt);
static void call_on_resize(PySceneObject* self, int width, int height);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PySceneType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Scene",
.tp_basicsize = sizeof(PySceneObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PySceneClass::__dealloc,
.tp_repr = (reprfunc)PySceneClass::__repr__,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // Allow subclassing
.tp_doc = PyDoc_STR("Base class for object-oriented scenes"),
.tp_methods = nullptr, // Set in McRFPy_API.cpp
.tp_getset = nullptr, // Set in McRFPy_API.cpp
.tp_init = (initproc)PySceneClass::__init__,
.tp_new = PySceneClass::__new__,
};
}

View File

@ -1,271 +0,0 @@
#include "PyTimer.h"
#include "PyCallable.h"
#include "GameEngine.h"
#include "Resources.h"
#include <sstream>
PyObject* PyTimer::repr(PyObject* self) {
PyTimerObject* timer = (PyTimerObject*)self;
std::ostringstream oss;
oss << "<Timer name='" << timer->name << "' ";
if (timer->data) {
oss << "interval=" << timer->data->getInterval() << "ms ";
oss << (timer->data->isPaused() ? "paused" : "active");
} else {
oss << "uninitialized";
}
oss << ">";
return PyUnicode_FromString(oss.str().c_str());
}
PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyTimerObject* self = (PyTimerObject*)type->tp_alloc(type, 0);
if (self) {
new(&self->name) std::string(); // Placement new for std::string
self->data = nullptr;
}
return (PyObject*)self;
}
int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {"name", "callback", "interval", NULL};
const char* name = nullptr;
PyObject* callback = nullptr;
int interval = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", kwlist,
&name, &callback, &interval)) {
return -1;
}
if (!PyCallable_Check(callback)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable");
return -1;
}
if (interval <= 0) {
PyErr_SetString(PyExc_ValueError, "interval must be positive");
return -1;
}
self->name = name;
// Get current time from game engine
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
// Create the timer callable
self->data = std::make_shared<PyTimerCallable>(callback, interval, current_time);
// Register with game engine
if (Resources::game) {
Resources::game->timers[self->name] = self->data;
}
return 0;
}
void PyTimer::dealloc(PyTimerObject* self) {
// Remove from game engine if still registered
if (Resources::game && !self->name.empty()) {
auto it = Resources::game->timers.find(self->name);
if (it != Resources::game->timers.end() && it->second == self->data) {
Resources::game->timers.erase(it);
}
}
// Explicitly destroy std::string
self->name.~basic_string();
// Clear shared_ptr
self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
// Timer control methods
PyObject* PyTimer::pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
self->data->pause(current_time);
Py_RETURN_NONE;
}
PyObject* PyTimer::resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
self->data->resume(current_time);
Py_RETURN_NONE;
}
PyObject* PyTimer::cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
// Remove from game engine
if (Resources::game && !self->name.empty()) {
auto it = Resources::game->timers.find(self->name);
if (it != Resources::game->timers.end() && it->second == self->data) {
Resources::game->timers.erase(it);
}
}
self->data->cancel();
self->data.reset();
Py_RETURN_NONE;
}
PyObject* PyTimer::restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
self->data->restart(current_time);
Py_RETURN_NONE;
}
// Property getters/setters
PyObject* PyTimer::get_interval(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
return PyLong_FromLong(self->data->getInterval());
}
int PyTimer::set_interval(PyTimerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return -1;
}
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "interval must be an integer");
return -1;
}
long interval = PyLong_AsLong(value);
if (interval <= 0) {
PyErr_SetString(PyExc_ValueError, "interval must be positive");
return -1;
}
self->data->setInterval(interval);
return 0;
}
PyObject* PyTimer::get_remaining(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
return PyLong_FromLong(self->data->getRemaining(current_time));
}
PyObject* PyTimer::get_paused(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
return PyBool_FromLong(self->data->isPaused());
}
PyObject* PyTimer::get_active(PyTimerObject* self, void* closure) {
if (!self->data) {
return Py_False;
}
return PyBool_FromLong(self->data->isActive());
}
PyObject* PyTimer::get_callback(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
PyObject* callback = self->data->getCallback();
if (!callback) {
Py_RETURN_NONE;
}
Py_INCREF(callback);
return callback;
}
int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return -1;
}
if (!PyCallable_Check(value)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable");
return -1;
}
self->data->setCallback(value);
return 0;
}
PyGetSetDef PyTimer::getsetters[] = {
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
"Timer interval in milliseconds", NULL},
{"remaining", (getter)PyTimer::get_remaining, NULL,
"Time remaining until next trigger in milliseconds", NULL},
{"paused", (getter)PyTimer::get_paused, NULL,
"Whether the timer is paused", NULL},
{"active", (getter)PyTimer::get_active, NULL,
"Whether the timer is active and not paused", NULL},
{"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback,
"The callback function to be called", NULL},
{NULL}
};
PyMethodDef PyTimer::methods[] = {
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
"Pause the timer"},
{"resume", (PyCFunction)PyTimer::resume, METH_NOARGS,
"Resume a paused timer"},
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
"Cancel the timer and remove it from the system"},
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
"Restart the timer from the current time"},
{NULL}
};

View File

@ -1,58 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <memory>
#include <string>
class PyTimerCallable;
typedef struct {
PyObject_HEAD
std::shared_ptr<PyTimerCallable> data;
std::string name;
} PyTimerObject;
class PyTimer
{
public:
// Python type methods
static PyObject* repr(PyObject* self);
static int init(PyTimerObject* self, PyObject* args, PyObject* kwds);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
static void dealloc(PyTimerObject* self);
// Timer control methods
static PyObject* pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
// Timer property getters
static PyObject* get_interval(PyTimerObject* self, void* closure);
static int set_interval(PyTimerObject* self, PyObject* value, void* closure);
static PyObject* get_remaining(PyTimerObject* self, void* closure);
static PyObject* get_paused(PyTimerObject* self, void* closure);
static PyObject* get_active(PyTimerObject* self, void* closure);
static PyObject* get_callback(PyTimerObject* self, void* closure);
static int set_callback(PyTimerObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PyTimerType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Timer",
.tp_basicsize = sizeof(PyTimerObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyTimer::dealloc,
.tp_repr = PyTimer::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Timer object for scheduled callbacks"),
.tp_methods = PyTimer::methods,
.tp_getset = PyTimer::getsetters,
.tp_init = (initproc)PyTimer::init,
.tp_new = PyTimer::pynew,
};
}

View File

@ -1,6 +1,5 @@
#include "PyVector.h"
#include "PyObjectUtils.h"
#include <cmath>
PyGetSetDef PyVector::getsetters[] = {
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member, "X/horizontal component", (void*)0},
@ -8,58 +7,6 @@ PyGetSetDef PyVector::getsetters[] = {
{NULL}
};
PyMethodDef PyVector::methods[] = {
{"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS, "Return the length of the vector"},
{"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS, "Return the squared length of the vector"},
{"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS, "Return a unit vector in the same direction"},
{"dot", (PyCFunction)PyVector::dot, METH_O, "Return the dot product with another vector"},
{"distance_to", (PyCFunction)PyVector::distance_to, METH_O, "Return the distance to another vector"},
{"angle", (PyCFunction)PyVector::angle, METH_NOARGS, "Return the angle in radians from the positive X axis"},
{"copy", (PyCFunction)PyVector::copy, METH_NOARGS, "Return a copy of this vector"},
{NULL}
};
namespace mcrfpydef {
PyNumberMethods PyVector_as_number = {
.nb_add = PyVector::add,
.nb_subtract = PyVector::subtract,
.nb_multiply = PyVector::multiply,
.nb_remainder = 0,
.nb_divmod = 0,
.nb_power = 0,
.nb_negative = PyVector::negative,
.nb_positive = 0,
.nb_absolute = PyVector::absolute,
.nb_bool = PyVector::bool_check,
.nb_invert = 0,
.nb_lshift = 0,
.nb_rshift = 0,
.nb_and = 0,
.nb_xor = 0,
.nb_or = 0,
.nb_int = 0,
.nb_reserved = 0,
.nb_float = 0,
.nb_inplace_add = 0,
.nb_inplace_subtract = 0,
.nb_inplace_multiply = 0,
.nb_inplace_remainder = 0,
.nb_inplace_power = 0,
.nb_inplace_lshift = 0,
.nb_inplace_rshift = 0,
.nb_inplace_and = 0,
.nb_inplace_xor = 0,
.nb_inplace_or = 0,
.nb_floor_divide = 0,
.nb_true_divide = PyVector::divide,
.nb_inplace_floor_divide = 0,
.nb_inplace_true_divide = 0,
.nb_index = 0,
.nb_matrix_multiply = 0,
.nb_inplace_matrix_multiply = 0
};
}
PyVector::PyVector(sf::Vector2f target)
:data(target) {}
@ -225,241 +172,3 @@ PyVectorObject* PyVector::from_arg(PyObject* args)
return obj;
}
// Arithmetic operations
PyObject* PyVector::add(PyObject* left, PyObject* right)
{
// Check if both operands are vectors
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
PyVectorObject* vec1 = nullptr;
PyVectorObject* vec2 = nullptr;
if (PyObject_IsInstance(left, (PyObject*)type) && PyObject_IsInstance(right, (PyObject*)type)) {
vec1 = (PyVectorObject*)left;
vec2 = (PyVectorObject*)right;
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(vec1->data.x + vec2->data.x, vec1->data.y + vec2->data.y);
}
return (PyObject*)result;
}
PyObject* PyVector::subtract(PyObject* left, PyObject* right)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
PyVectorObject* vec1 = nullptr;
PyVectorObject* vec2 = nullptr;
if (PyObject_IsInstance(left, (PyObject*)type) && PyObject_IsInstance(right, (PyObject*)type)) {
vec1 = (PyVectorObject*)left;
vec2 = (PyVectorObject*)right;
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(vec1->data.x - vec2->data.x, vec1->data.y - vec2->data.y);
}
return (PyObject*)result;
}
PyObject* PyVector::multiply(PyObject* left, PyObject* right)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
PyVectorObject* vec = nullptr;
double scalar = 0.0;
// Check for Vector * scalar
if (PyObject_IsInstance(left, (PyObject*)type) && (PyFloat_Check(right) || PyLong_Check(right))) {
vec = (PyVectorObject*)left;
scalar = PyFloat_AsDouble(right);
}
// Check for scalar * Vector
else if ((PyFloat_Check(left) || PyLong_Check(left)) && PyObject_IsInstance(right, (PyObject*)type)) {
scalar = PyFloat_AsDouble(left);
vec = (PyVectorObject*)right;
}
else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(vec->data.x * scalar, vec->data.y * scalar);
}
return (PyObject*)result;
}
PyObject* PyVector::divide(PyObject* left, PyObject* right)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
// Only support Vector / scalar
if (!PyObject_IsInstance(left, (PyObject*)type) || (!PyFloat_Check(right) && !PyLong_Check(right))) {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
PyVectorObject* vec = (PyVectorObject*)left;
double scalar = PyFloat_AsDouble(right);
if (scalar == 0.0) {
PyErr_SetString(PyExc_ZeroDivisionError, "Vector division by zero");
return NULL;
}
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(vec->data.x / scalar, vec->data.y / scalar);
}
return (PyObject*)result;
}
PyObject* PyVector::negative(PyObject* self)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
PyVectorObject* vec = (PyVectorObject*)self;
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(-vec->data.x, -vec->data.y);
}
return (PyObject*)result;
}
PyObject* PyVector::absolute(PyObject* self)
{
PyVectorObject* vec = (PyVectorObject*)self;
return PyFloat_FromDouble(std::sqrt(vec->data.x * vec->data.x + vec->data.y * vec->data.y));
}
int PyVector::bool_check(PyObject* self)
{
PyVectorObject* vec = (PyVectorObject*)self;
return (vec->data.x != 0.0f || vec->data.y != 0.0f) ? 1 : 0;
}
PyObject* PyVector::richcompare(PyObject* left, PyObject* right, int op)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (!PyObject_IsInstance(left, (PyObject*)type) || !PyObject_IsInstance(right, (PyObject*)type)) {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
PyVectorObject* vec1 = (PyVectorObject*)left;
PyVectorObject* vec2 = (PyVectorObject*)right;
bool result = false;
switch (op) {
case Py_EQ:
result = (vec1->data.x == vec2->data.x && vec1->data.y == vec2->data.y);
break;
case Py_NE:
result = (vec1->data.x != vec2->data.x || vec1->data.y != vec2->data.y);
break;
default:
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
if (result)
Py_RETURN_TRUE;
else
Py_RETURN_FALSE;
}
// Vector-specific methods
PyObject* PyVector::magnitude(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
float mag = std::sqrt(self->data.x * self->data.x + self->data.y * self->data.y);
return PyFloat_FromDouble(mag);
}
PyObject* PyVector::magnitude_squared(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
float mag_sq = self->data.x * self->data.x + self->data.y * self->data.y;
return PyFloat_FromDouble(mag_sq);
}
PyObject* PyVector::normalize(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
float mag = std::sqrt(self->data.x * self->data.x + self->data.y * self->data.y);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
if (mag > 0.0f) {
result->data = sf::Vector2f(self->data.x / mag, self->data.y / mag);
} else {
// Zero vector remains zero
result->data = sf::Vector2f(0.0f, 0.0f);
}
}
return (PyObject*)result;
}
PyObject* PyVector::dot(PyVectorObject* self, PyObject* other)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (!PyObject_IsInstance(other, (PyObject*)type)) {
PyErr_SetString(PyExc_TypeError, "Argument must be a Vector");
return NULL;
}
PyVectorObject* vec2 = (PyVectorObject*)other;
float dot_product = self->data.x * vec2->data.x + self->data.y * vec2->data.y;
return PyFloat_FromDouble(dot_product);
}
PyObject* PyVector::distance_to(PyVectorObject* self, PyObject* other)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (!PyObject_IsInstance(other, (PyObject*)type)) {
PyErr_SetString(PyExc_TypeError, "Argument must be a Vector");
return NULL;
}
PyVectorObject* vec2 = (PyVectorObject*)other;
float dx = self->data.x - vec2->data.x;
float dy = self->data.y - vec2->data.y;
float distance = std::sqrt(dx * dx + dy * dy);
return PyFloat_FromDouble(distance);
}
PyObject* PyVector::angle(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
float angle_rad = std::atan2(self->data.y, self->data.x);
return PyFloat_FromDouble(angle_rad);
}
PyObject* PyVector::copy(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = self->data;
}
return (PyObject*)result;
}

View File

@ -25,47 +25,19 @@ public:
static int set_member(PyObject*, PyObject*, void*);
static PyVectorObject* from_arg(PyObject*);
// Arithmetic operations
static PyObject* add(PyObject*, PyObject*);
static PyObject* subtract(PyObject*, PyObject*);
static PyObject* multiply(PyObject*, PyObject*);
static PyObject* divide(PyObject*, PyObject*);
static PyObject* negative(PyObject*);
static PyObject* absolute(PyObject*);
static int bool_check(PyObject*);
// Comparison operations
static PyObject* richcompare(PyObject*, PyObject*, int);
// Vector operations
static PyObject* magnitude(PyVectorObject*, PyObject*);
static PyObject* magnitude_squared(PyVectorObject*, PyObject*);
static PyObject* normalize(PyVectorObject*, PyObject*);
static PyObject* dot(PyVectorObject*, PyObject*);
static PyObject* distance_to(PyVectorObject*, PyObject*);
static PyObject* angle(PyVectorObject*, PyObject*);
static PyObject* copy(PyVectorObject*, PyObject*);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
// Forward declare the PyNumberMethods structure
extern PyNumberMethods PyVector_as_number;
static PyTypeObject PyVectorType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Vector",
.tp_basicsize = sizeof(PyVectorObject),
.tp_itemsize = 0,
.tp_repr = PyVector::repr,
.tp_as_number = &PyVector_as_number,
.tp_hash = PyVector::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Vector Object"),
.tp_richcompare = PyVector::richcompare,
.tp_methods = PyVector::methods,
.tp_getset = PyVector::getsetters,
.tp_init = (initproc)PyVector::init,
.tp_new = PyVector::pynew,

View File

@ -1,433 +0,0 @@
#include "PyWindow.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include <SFML/Graphics.hpp>
// Singleton instance - static variable, not a class member
static PyWindowObject* window_instance = nullptr;
PyObject* PyWindow::get(PyObject* cls, PyObject* args)
{
// Create singleton instance if it doesn't exist
if (!window_instance) {
// Use the class object passed as first argument
PyTypeObject* type = (PyTypeObject*)cls;
if (!type->tp_alloc) {
PyErr_SetString(PyExc_RuntimeError, "Window type not properly initialized");
return NULL;
}
window_instance = (PyWindowObject*)type->tp_alloc(type, 0);
if (!window_instance) {
return NULL;
}
}
Py_INCREF(window_instance);
return (PyObject*)window_instance;
}
PyObject* PyWindow::repr(PyWindowObject* self)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
return PyUnicode_FromString("<Window [no game engine]>");
}
if (game->isHeadless()) {
return PyUnicode_FromString("<Window [headless mode]>");
}
auto& window = game->getWindow();
auto size = window.getSize();
return PyUnicode_FromFormat("<Window %dx%d>", size.x, size.y);
}
// Property getters and setters
PyObject* PyWindow::get_resolution(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
if (game->isHeadless()) {
// Return headless renderer size
return Py_BuildValue("(ii)", 1024, 768); // Default headless size
}
auto& window = game->getWindow();
auto size = window.getSize();
return Py_BuildValue("(ii)", size.x, size.y);
}
int PyWindow::set_resolution(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
PyErr_SetString(PyExc_RuntimeError, "Cannot change resolution in headless mode");
return -1;
}
int width, height;
if (!PyArg_ParseTuple(value, "ii", &width, &height)) {
PyErr_SetString(PyExc_TypeError, "Resolution must be a tuple of two integers (width, height)");
return -1;
}
if (width <= 0 || height <= 0) {
PyErr_SetString(PyExc_ValueError, "Resolution dimensions must be positive");
return -1;
}
auto& window = game->getWindow();
// Get current window settings
auto style = sf::Style::Titlebar | sf::Style::Close;
if (window.getSize() == sf::Vector2u(sf::VideoMode::getDesktopMode().width,
sf::VideoMode::getDesktopMode().height)) {
style = sf::Style::Fullscreen;
}
// Recreate window with new size
window.create(sf::VideoMode(width, height), game->getWindowTitle(), style);
// Restore vsync and framerate settings
// Note: We'll need to store these settings in GameEngine
window.setFramerateLimit(60); // Default for now
return 0;
}
PyObject* PyWindow::get_fullscreen(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
if (game->isHeadless()) {
Py_RETURN_FALSE;
}
auto& window = game->getWindow();
auto size = window.getSize();
auto desktop = sf::VideoMode::getDesktopMode();
// Check if window size matches desktop size (rough fullscreen check)
bool fullscreen = (size.x == desktop.width && size.y == desktop.height);
return PyBool_FromLong(fullscreen);
}
int PyWindow::set_fullscreen(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
PyErr_SetString(PyExc_RuntimeError, "Cannot change fullscreen in headless mode");
return -1;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "Fullscreen must be a boolean");
return -1;
}
bool fullscreen = PyObject_IsTrue(value);
auto& window = game->getWindow();
if (fullscreen) {
// Switch to fullscreen
auto desktop = sf::VideoMode::getDesktopMode();
window.create(desktop, game->getWindowTitle(), sf::Style::Fullscreen);
} else {
// Switch to windowed mode
window.create(sf::VideoMode(1024, 768), game->getWindowTitle(),
sf::Style::Titlebar | sf::Style::Close);
}
// Restore settings
window.setFramerateLimit(60);
return 0;
}
PyObject* PyWindow::get_vsync(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
return PyBool_FromLong(game->getVSync());
}
int PyWindow::set_vsync(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
PyErr_SetString(PyExc_RuntimeError, "Cannot change vsync in headless mode");
return -1;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "vsync must be a boolean");
return -1;
}
bool vsync = PyObject_IsTrue(value);
game->setVSync(vsync);
return 0;
}
PyObject* PyWindow::get_title(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
return PyUnicode_FromString(game->getWindowTitle().c_str());
}
int PyWindow::set_title(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
// Silently ignore in headless mode
return 0;
}
const char* title = PyUnicode_AsUTF8(value);
if (!title) {
PyErr_SetString(PyExc_TypeError, "Title must be a string");
return -1;
}
game->setWindowTitle(title);
return 0;
}
PyObject* PyWindow::get_visible(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
if (game->isHeadless()) {
Py_RETURN_FALSE;
}
auto& window = game->getWindow();
bool visible = window.isOpen(); // Best approximation
return PyBool_FromLong(visible);
}
int PyWindow::set_visible(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
// Silently ignore in headless mode
return 0;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
bool visible = PyObject_IsTrue(value);
auto& window = game->getWindow();
window.setVisible(visible);
return 0;
}
PyObject* PyWindow::get_framerate_limit(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
return PyLong_FromLong(game->getFramerateLimit());
}
int PyWindow::set_framerate_limit(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
// Silently ignore in headless mode
return 0;
}
long limit = PyLong_AsLong(value);
if (PyErr_Occurred()) {
PyErr_SetString(PyExc_TypeError, "framerate_limit must be an integer");
return -1;
}
if (limit < 0) {
PyErr_SetString(PyExc_ValueError, "framerate_limit must be non-negative (0 for unlimited)");
return -1;
}
game->setFramerateLimit(limit);
return 0;
}
// Methods
PyObject* PyWindow::center(PyWindowObject* self, PyObject* args)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
if (game->isHeadless()) {
PyErr_SetString(PyExc_RuntimeError, "Cannot center window in headless mode");
return NULL;
}
auto& window = game->getWindow();
auto size = window.getSize();
auto desktop = sf::VideoMode::getDesktopMode();
int x = (desktop.width - size.x) / 2;
int y = (desktop.height - size.y) / 2;
window.setPosition(sf::Vector2i(x, y));
Py_RETURN_NONE;
}
PyObject* PyWindow::screenshot(PyWindowObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = {"filename", NULL};
const char* filename = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s", const_cast<char**>(keywords), &filename)) {
return NULL;
}
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
// Get the render target pointer
sf::RenderTarget* target = game->getRenderTargetPtr();
if (!target) {
PyErr_SetString(PyExc_RuntimeError, "No render target available");
return NULL;
}
sf::Image screenshot;
// For RenderWindow
if (auto* window = dynamic_cast<sf::RenderWindow*>(target)) {
sf::Vector2u windowSize = window->getSize();
sf::Texture texture;
texture.create(windowSize.x, windowSize.y);
texture.update(*window);
screenshot = texture.copyToImage();
}
// For RenderTexture (headless mode)
else if (auto* renderTexture = dynamic_cast<sf::RenderTexture*>(target)) {
screenshot = renderTexture->getTexture().copyToImage();
}
else {
PyErr_SetString(PyExc_RuntimeError, "Unknown render target type");
return NULL;
}
// Save to file if filename provided
if (filename) {
if (!screenshot.saveToFile(filename)) {
PyErr_SetString(PyExc_IOError, "Failed to save screenshot");
return NULL;
}
Py_RETURN_NONE;
}
// Otherwise return as bytes
auto pixels = screenshot.getPixelsPtr();
auto size = screenshot.getSize();
return PyBytes_FromStringAndSize((const char*)pixels, size.x * size.y * 4);
}
// Property definitions
PyGetSetDef PyWindow::getsetters[] = {
{"resolution", (getter)get_resolution, (setter)set_resolution,
"Window resolution as (width, height) tuple", NULL},
{"fullscreen", (getter)get_fullscreen, (setter)set_fullscreen,
"Window fullscreen state", NULL},
{"vsync", (getter)get_vsync, (setter)set_vsync,
"Vertical sync enabled state", NULL},
{"title", (getter)get_title, (setter)set_title,
"Window title string", NULL},
{"visible", (getter)get_visible, (setter)set_visible,
"Window visibility state", NULL},
{"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit,
"Frame rate limit (0 for unlimited)", NULL},
{NULL}
};
// Method definitions
PyMethodDef PyWindow::methods[] = {
{"get", (PyCFunction)PyWindow::get, METH_VARARGS | METH_CLASS,
"Get the Window singleton instance"},
{"center", (PyCFunction)PyWindow::center, METH_NOARGS,
"Center the window on the screen"},
{"screenshot", (PyCFunction)PyWindow::screenshot, METH_VARARGS | METH_KEYWORDS,
"Take a screenshot. Pass filename to save to file, or get raw bytes if no filename."},
{NULL}
};

View File

@ -1,65 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
// Forward declarations
class GameEngine;
// Python object structure for Window singleton
typedef struct {
PyObject_HEAD
// No data - Window is a singleton that accesses GameEngine
} PyWindowObject;
// C++ interface for the Window singleton
class PyWindow
{
public:
// Static methods for Python type
static PyObject* get(PyObject* cls, PyObject* args);
static PyObject* repr(PyWindowObject* self);
// Getters and setters for window properties
static PyObject* get_resolution(PyWindowObject* self, void* closure);
static int set_resolution(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_fullscreen(PyWindowObject* self, void* closure);
static int set_fullscreen(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_vsync(PyWindowObject* self, void* closure);
static int set_vsync(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_title(PyWindowObject* self, void* closure);
static int set_title(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_visible(PyWindowObject* self, void* closure);
static int set_visible(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_framerate_limit(PyWindowObject* self, void* closure);
static int set_framerate_limit(PyWindowObject* self, PyObject* value, void* closure);
// Methods
static PyObject* center(PyWindowObject* self, PyObject* args);
static PyObject* screenshot(PyWindowObject* self, PyObject* args, PyObject* kwds);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PyWindowType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Window",
.tp_basicsize = sizeof(PyWindowObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) {
// Don't delete the singleton instance
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)PyWindow::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Window singleton for accessing and modifying the game window properties"),
.tp_methods = nullptr, // Set in McRFPy_API.cpp after definition
.tp_getset = nullptr, // Set in McRFPy_API.cpp after definition
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
PyErr_SetString(PyExc_TypeError, "Cannot instantiate Window. Use Window.get() to access the singleton.");
return NULL;
}
};
}

View File

@ -1,85 +0,0 @@
#include "SceneTransition.h"
void SceneTransition::start(TransitionType t, const std::string& from, const std::string& to, float dur) {
type = t;
fromScene = from;
toScene = to;
duration = dur;
elapsed = 0.0f;
// Initialize render textures if needed
if (!oldSceneTexture) {
oldSceneTexture = std::make_unique<sf::RenderTexture>();
oldSceneTexture->create(1024, 768);
}
if (!newSceneTexture) {
newSceneTexture = std::make_unique<sf::RenderTexture>();
newSceneTexture->create(1024, 768);
}
}
void SceneTransition::update(float dt) {
if (type == TransitionType::None) return;
elapsed += dt;
}
void SceneTransition::render(sf::RenderTarget& target) {
if (type == TransitionType::None) return;
float progress = getProgress();
float easedProgress = easeInOut(progress);
// Update sprites with current textures
oldSprite.setTexture(oldSceneTexture->getTexture());
newSprite.setTexture(newSceneTexture->getTexture());
switch (type) {
case TransitionType::Fade:
// Fade out old scene, fade in new scene
oldSprite.setColor(sf::Color(255, 255, 255, 255 * (1.0f - easedProgress)));
newSprite.setColor(sf::Color(255, 255, 255, 255 * easedProgress));
target.draw(oldSprite);
target.draw(newSprite);
break;
case TransitionType::SlideLeft:
// Old scene slides out to left, new scene slides in from right
oldSprite.setPosition(-1024 * easedProgress, 0);
newSprite.setPosition(1024 * (1.0f - easedProgress), 0);
target.draw(oldSprite);
target.draw(newSprite);
break;
case TransitionType::SlideRight:
// Old scene slides out to right, new scene slides in from left
oldSprite.setPosition(1024 * easedProgress, 0);
newSprite.setPosition(-1024 * (1.0f - easedProgress), 0);
target.draw(oldSprite);
target.draw(newSprite);
break;
case TransitionType::SlideUp:
// Old scene slides up, new scene slides in from bottom
oldSprite.setPosition(0, -768 * easedProgress);
newSprite.setPosition(0, 768 * (1.0f - easedProgress));
target.draw(oldSprite);
target.draw(newSprite);
break;
case TransitionType::SlideDown:
// Old scene slides down, new scene slides in from top
oldSprite.setPosition(0, 768 * easedProgress);
newSprite.setPosition(0, -768 * (1.0f - easedProgress));
target.draw(oldSprite);
target.draw(newSprite);
break;
default:
break;
}
}
float SceneTransition::easeInOut(float t) {
// Smooth ease-in-out curve
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}

View File

@ -1,42 +0,0 @@
#pragma once
#include "Common.h"
#include <SFML/Graphics.hpp>
#include <string>
#include <memory>
enum class TransitionType {
None,
Fade,
SlideLeft,
SlideRight,
SlideUp,
SlideDown
};
class SceneTransition {
public:
TransitionType type = TransitionType::None;
float duration = 0.0f;
float elapsed = 0.0f;
std::string fromScene;
std::string toScene;
// Render textures for transition
std::unique_ptr<sf::RenderTexture> oldSceneTexture;
std::unique_ptr<sf::RenderTexture> newSceneTexture;
// Sprites for rendering textures
sf::Sprite oldSprite;
sf::Sprite newSprite;
SceneTransition() = default;
void start(TransitionType t, const std::string& from, const std::string& to, float dur);
void update(float dt);
void render(sf::RenderTarget& target);
bool isComplete() const { return elapsed >= duration; }
float getProgress() const { return duration > 0 ? std::min(elapsed / duration, 1.0f) : 1.0f; }
// Easing function for smooth transitions
static float easeInOut(float t);
};

View File

@ -1,6 +1,4 @@
#pragma once
#include "Python.h"
#include <memory>
class UIEntity;
typedef struct {
@ -32,103 +30,3 @@ typedef struct {
PyObject_HEAD
std::shared_ptr<UISprite> data;
} PyUISpriteObject;
// Common Python method implementations for UIDrawable-derived classes
// These template functions provide shared functionality for Python bindings
// get_bounds method implementation (#89)
template<typename T>
static PyObject* UIDrawable_get_bounds(T* self, PyObject* Py_UNUSED(args))
{
auto bounds = self->data->get_bounds();
return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height);
}
// move method implementation (#98)
template<typename T>
static PyObject* UIDrawable_move(T* self, PyObject* args)
{
float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
return NULL;
}
self->data->move(dx, dy);
Py_RETURN_NONE;
}
// resize method implementation (#98)
template<typename T>
static PyObject* UIDrawable_resize(T* self, PyObject* args)
{
float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
return NULL;
}
self->data->resize(w, h);
Py_RETURN_NONE;
}
// Macro to add common UIDrawable methods to a method array
#define UIDRAWABLE_METHODS \
{"get_bounds", (PyCFunction)UIDrawable_get_bounds<PyObjectType>, METH_NOARGS, \
"Get bounding box as (x, y, width, height)"}, \
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS, \
"Move by relative offset (dx, dy)"}, \
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS, \
"Resize to new dimensions (width, height)"}
// Property getters/setters for visible and opacity
template<typename T>
static PyObject* UIDrawable_get_visible(T* self, void* closure)
{
return PyBool_FromLong(self->data->visible);
}
template<typename T>
static int UIDrawable_set_visible(T* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
self->data->visible = PyObject_IsTrue(value);
return 0;
}
template<typename T>
static PyObject* UIDrawable_get_opacity(T* self, void* closure)
{
return PyFloat_FromDouble(self->data->opacity);
}
template<typename T>
static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
{
float opacity;
if (PyFloat_Check(value)) {
opacity = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
opacity = PyLong_AsDouble(value);
} else {
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1;
}
// Clamp to valid range
if (opacity < 0.0f) opacity = 0.0f;
if (opacity > 1.0f) opacity = 1.0f;
self->data->opacity = opacity;
return 0;
}
// Macro to add common UIDrawable properties to a getsetters array
#define UIDRAWABLE_GETSETTERS \
{"visible", (getter)UIDrawable_get_visible<PyObjectType>, (setter)UIDrawable_set_visible<PyObjectType>, \
"Visibility flag", NULL}, \
{"opacity", (getter)UIDrawable_get_opacity<PyObjectType>, (setter)UIDrawable_set_opacity<PyObjectType>, \
"Opacity (0.0 = transparent, 1.0 = opaque)", NULL}
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete

View File

@ -3,21 +3,8 @@
#include "PyColor.h"
#include "PyVector.h"
#include "PyFont.h"
#include "PyPositionHelper.h"
// UIDrawable methods now in UIBase.h
#include <algorithm>
UICaption::UICaption()
{
// Initialize text with safe defaults
text.setString("");
text.setPosition(0.0f, 0.0f);
text.setCharacterSize(12);
text.setFillColor(sf::Color::White);
text.setOutlineColor(sf::Color::Black);
text.setOutlineThickness(0.0f);
}
UIDrawable* UICaption::click_at(sf::Vector2f point)
{
if (click_callable)
@ -29,22 +16,10 @@ UIDrawable* UICaption::click_at(sf::Vector2f point)
void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
{
// Check visibility
if (!visible) return;
// Apply opacity
auto color = text.getFillColor();
color.a = static_cast<sf::Uint8>(255 * opacity);
text.setFillColor(color);
text.move(offset);
//Resources::game->getWindow().draw(text);
target.draw(text);
text.move(-offset);
// Restore original alpha
color.a = 255;
text.setFillColor(color);
}
PyObjectsEnum UICaption::derived_type()
@ -52,23 +27,6 @@ PyObjectsEnum UICaption::derived_type()
return PyObjectsEnum::UICAPTION;
}
// Phase 1 implementations
sf::FloatRect UICaption::get_bounds() const
{
return text.getGlobalBounds();
}
void UICaption::move(float dx, float dy)
{
text.move(dx, dy);
}
void UICaption::resize(float w, float h)
{
// Caption doesn't support direct resizing - size is controlled by font size
// This is a no-op but required by the interface
}
PyObject* UICaption::get_float_member(PyUICaptionObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
@ -164,6 +122,7 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void*
// get value from mcrfpy.Color instance
auto c = ((PyColorObject*)value)->data;
r = c.r; g = c.g; b = c.b; a = c.a;
std::cout << "got " << int(r) << ", " << int(g) << ", " << int(b) << ", " << int(a) << std::endl;
}
else if (!PyTuple_Check(value) || PyTuple_Size(value) < 3 || PyTuple_Size(value) > 4)
{
@ -208,15 +167,6 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void*
}
// Define the PyObjectType alias for the macros
typedef PyUICaptionObject PyObjectType;
// Method definitions
PyMethodDef UICaption_methods[] = {
UIDRAWABLE_METHODS,
{NULL} // Sentinel
};
//TODO: evaluate use of Resources::caption_buffer... can't I do this with a std::string?
PyObject* UICaption::get_text(PyUICaptionObject* self, void* closure)
{
@ -250,8 +200,6 @@ PyGetSetDef UICaption::getsetters[] = {
{"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION},
UIDRAWABLE_GETSETTERS,
{NULL}
};
@ -277,92 +225,30 @@ PyObject* UICaption::repr(PyUICaptionObject* self)
int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
{
using namespace mcrfpydef;
// Constructor switch to Vector position
//static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", nullptr };
//float x = 0.0f, y = 0.0f, outline = 0.0f;
static const char* keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", nullptr };
PyObject* pos;
float outline = 0.0f;
char* text;
PyObject* font=NULL, *fill_color=NULL, *outline_color=NULL;
static const char* keywords[] = { "text", "x", "y", "font", "fill_color", "outline_color", "outline", "click", "pos", nullptr };
float x = 0.0f, y = 0.0f, outline = 0.0f;
char* text = NULL;
PyObject* font = NULL;
PyObject* fill_color = NULL;
PyObject* outline_color = NULL;
PyObject* click_handler = NULL;
PyObject* pos_obj = NULL;
// Handle different argument patterns
Py_ssize_t args_size = PyTuple_Size(args);
if (args_size >= 2 && !PyUnicode_Check(PyTuple_GetItem(args, 0))) {
// Pattern 1: (x, y, text, ...) or ((x,y), text, ...)
PyObject* first_arg = PyTuple_GetItem(args, 0);
// Check if first arg is a tuple/Vector (pos format)
if (PyTuple_Check(first_arg) || PyObject_IsInstance(first_arg, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"))) {
// Pattern: ((x,y), text, ...)
static const char* pos_keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", "click", "x", "y", nullptr };
PyObject* pos = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOfOff",
const_cast<char**>(pos_keywords),
&pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &x, &y))
{
return -1;
}
// Parse position
if (pos && pos != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
}
else {
// Pattern: (x, y, text, ...)
static const char* xy_keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", "click", "pos", nullptr };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO",
const_cast<char**>(xy_keywords),
&x, &y, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj))
{
return -1;
}
// If pos was provided, it overrides x,y
if (pos_obj && pos_obj != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
}
}
else {
// Pattern 2: (text, ...) with x, y as keywords
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO",
const_cast<char**>(keywords),
&text, &x, &y, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj))
{
return -1;
}
// If pos was provided, it overrides x,y
if (pos_obj && pos_obj != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf",
// const_cast<char**>(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oz|OOOf",
const_cast<char**>(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline))
{
return -1;
}
self->data->text.setPosition(x, y);
PyVectorObject* pos_result = PyVector::from_arg(pos);
if (!pos_result)
{
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
}
self->data->text.setPosition(pos_result->data);
// check types for font, fill_color, outline_color
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
@ -389,12 +275,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
}
}
// Handle text - default to empty string if not provided
if (text && text != NULL) {
self->data->text.setString((std::string)text);
} else {
self->data->text.setString("");
}
self->data->text.setString((std::string)text);
self->data->text.setOutlineThickness(outline);
if (fill_color) {
auto fc = PyColor::from_arg(fill_color);
@ -420,15 +301,6 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
self->data->text.setOutlineColor(sf::Color(128,128,128,255));
}
// Process click handler if provided
if (click_handler && click_handler != Py_None) {
if (!PyCallable_Check(click_handler)) {
PyErr_SetString(PyExc_TypeError, "click must be callable");
return -1;
}
self->data->click_register(click_handler);
}
return 0;
}

View File

@ -7,16 +7,10 @@ class UICaption: public UIDrawable
{
public:
sf::Text text;
UICaption(); // Default constructor with safe initialization
void render(sf::Vector2f, sf::RenderTarget&) override final;
PyObjectsEnum derived_type() override final;
virtual UIDrawable* click_at(sf::Vector2f point) override final;
// Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override;
void resize(float w, float h) override;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Color& value) override;
@ -40,8 +34,6 @@ public:
};
extern PyMethodDef UICaption_methods[];
namespace mcrfpydef {
static PyTypeObject PyUICaptionType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -64,7 +56,7 @@ namespace mcrfpydef {
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
.tp_methods = UICaption_methods,
//.tp_methods = PyUIFrame_methods,
//.tp_members = PyUIFrame_members,
.tp_getset = UICaption::getsetters,
//.tp_base = NULL,

View File

@ -1,82 +0,0 @@
#pragma once
#include "UIDrawable.h"
#include <vector>
#include <memory>
// Base class for UI containers that provides common click handling logic
class UIContainerBase {
protected:
// Transform a point from parent coordinates to this container's local coordinates
virtual sf::Vector2f toLocalCoordinates(sf::Vector2f point) const = 0;
// Transform a point from this container's local coordinates to child coordinates
virtual sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const = 0;
// Get the bounds of this container in parent coordinates
virtual sf::FloatRect getBounds() const = 0;
// Check if a local point is within this container's bounds
virtual bool containsPoint(sf::Vector2f localPoint) const = 0;
// Get click handler if this container has one
virtual UIDrawable* getClickHandler() = 0;
// Get children to check for clicks (can be empty)
virtual std::vector<UIDrawable*> getClickableChildren() = 0;
public:
// Standard click handling algorithm for all containers
// Returns the deepest UIDrawable that has a click handler and contains the point
UIDrawable* handleClick(sf::Vector2f point) {
// Transform to local coordinates
sf::Vector2f localPoint = toLocalCoordinates(point);
// Check if point is within our bounds
if (!containsPoint(localPoint)) {
return nullptr;
}
// Check children in reverse z-order (top-most first)
// This ensures that elements rendered on top get first chance at clicks
auto children = getClickableChildren();
// TODO: Sort by z-index if not already sorted
// std::sort(children.begin(), children.end(),
// [](UIDrawable* a, UIDrawable* b) { return a->z_index > b->z_index; });
for (int i = children.size() - 1; i >= 0; --i) {
if (!children[i]->visible) continue;
sf::Vector2f childPoint = toChildCoordinates(localPoint, i);
if (auto target = children[i]->click_at(childPoint)) {
// Child (or its descendant) handled the click
return target;
}
// If child didn't handle it, continue checking other children
// This allows click-through for elements without handlers
}
// No child consumed the click
// Now check if WE have a click handler
return getClickHandler();
}
};
// Helper for containers with simple box bounds
class RectangularContainer : public UIContainerBase {
protected:
sf::FloatRect bounds;
sf::Vector2f toLocalCoordinates(sf::Vector2f point) const override {
return point - sf::Vector2f(bounds.left, bounds.top);
}
bool containsPoint(sf::Vector2f localPoint) const override {
return localPoint.x >= 0 && localPoint.y >= 0 &&
localPoint.x < bounds.width && localPoint.y < bounds.height;
}
sf::FloatRect getBounds() const override {
return bounds;
}
};

View File

@ -25,28 +25,16 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
switch (objtype)
{
case PyObjectsEnum::UIFRAME:
if (((PyUIFrameObject*)self)->data->click_callable)
ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow();
break;
case PyObjectsEnum::UICAPTION:
if (((PyUICaptionObject*)self)->data->click_callable)
ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow();
break;
case PyObjectsEnum::UISPRITE:
if (((PyUISpriteObject*)self)->data->click_callable)
ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow();
break;
case PyObjectsEnum::UIGRID:
if (((PyUIGridObject*)self)->data->click_callable)
ptr = ((PyUIGridObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
ptr = ((PyUIGridObject*)self)->data->click_callable->borrow();
break;
default:
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click");
@ -175,102 +163,3 @@ void UIDrawable::notifyZIndexChanged() {
// For now, Frame children will need manual sorting or collection modification
// to trigger a resort
}
PyObject* UIDrawable::get_name(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return NULL;
}
return PyUnicode_FromString(drawable->name.c_str());
}
int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return -1;
}
if (value == NULL || value == Py_None) {
drawable->name = "";
return 0;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "name must be a string");
return -1;
}
const char* name_str = PyUnicode_AsUTF8(value);
if (!name_str) {
return -1;
}
drawable->name = name_str;
return 0;
}
void UIDrawable::enableRenderTexture(unsigned int width, unsigned int height) {
// Create or recreate RenderTexture if size changed
if (!render_texture || render_texture->getSize().x != width || render_texture->getSize().y != height) {
render_texture = std::make_unique<sf::RenderTexture>();
if (!render_texture->create(width, height)) {
render_texture.reset();
use_render_texture = false;
return;
}
render_sprite.setTexture(render_texture->getTexture());
}
use_render_texture = true;
render_dirty = true;
}
void UIDrawable::updateRenderTexture() {
if (!use_render_texture || !render_texture) {
return;
}
// Clear the RenderTexture
render_texture->clear(sf::Color::Transparent);
// Render content to RenderTexture
// This will be overridden by derived classes
// For now, just display the texture
render_texture->display();
// Update the sprite
render_sprite.setTexture(render_texture->getTexture());
}

View File

@ -44,8 +44,6 @@ public:
static int set_click(PyObject* self, PyObject* value, void* closure);
static PyObject* get_int(PyObject* self, void* closure);
static int set_int(PyObject* self, PyObject* value, void* closure);
static PyObject* get_name(PyObject* self, void* closure);
static int set_name(PyObject* self, PyObject* value, void* closure);
// Z-order for rendering (lower values rendered first, higher values on top)
int z_index = 0;
@ -53,18 +51,6 @@ public:
// Notification for z_index changes
void notifyZIndexChanged();
// Name for finding elements
std::string name;
// New properties for Phase 1
bool visible = true; // #87 - visibility flag
float opacity = 1.0f; // #88 - opacity (0.0 = transparent, 1.0 = opaque)
// New virtual methods for Phase 1
virtual sf::FloatRect get_bounds() const = 0; // #89 - get bounding box
virtual void move(float dx, float dy) = 0; // #98 - move by offset
virtual void resize(float w, float h) = 0; // #98 - resize to dimensions
// Animation support
virtual bool setProperty(const std::string& name, float value) { return false; }
virtual bool setProperty(const std::string& name, int value) { return false; }
@ -77,21 +63,6 @@ public:
virtual bool getProperty(const std::string& name, sf::Color& value) const { return false; }
virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; }
virtual bool getProperty(const std::string& name, std::string& value) const { return false; }
protected:
// RenderTexture support (opt-in)
std::unique_ptr<sf::RenderTexture> render_texture;
sf::Sprite render_sprite;
bool use_render_texture = false;
bool render_dirty = true;
// Enable RenderTexture for this drawable
void enableRenderTexture(unsigned int width, unsigned int height);
void updateRenderTexture();
public:
// Mark this drawable as needing redraw
void markDirty() { render_dirty = true; }
};
typedef struct {

View File

@ -1,20 +1,11 @@
#include "UIEntity.h"
#include "UIGrid.h"
#include "McRFPy_API.h"
#include <algorithm>
#include "PyObjectUtils.h"
#include "PyVector.h"
#include "PyPositionHelper.h"
// UIDrawable methods now in UIBase.h
#include "UIEntityPyMethods.h"
UIEntity::UIEntity()
: self(nullptr), grid(nullptr), position(0.0f, 0.0f), collision_pos(0, 0)
{
// Initialize sprite with safe defaults (sprite has its own safe constructor now)
// gridstate vector starts empty since we don't know grid dimensions
}
UIEntity::UIEntity() {} // this will not work lol. TODO remove default constructor by finding the shared pointer inits that use it
UIEntity::UIEntity(UIGrid& grid)
: gridstate(grid.grid_x * grid.grid_y)
@ -73,52 +64,28 @@ PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
}
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", "pos", nullptr };
float x = 0.0f, y = 0.0f;
int sprite_index = 0; // Default to sprite index 0
//static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr };
//float x = 0.0f, y = 0.0f, scale = 1.0f;
static const char* keywords[] = { "pos", "texture", "sprite_index", "grid", nullptr };
PyObject* pos;
float scale = 1.0f;
int sprite_index = -1;
PyObject* texture = NULL;
PyObject* grid = NULL;
PyObject* pos_obj = NULL;
// Try to parse all arguments with keywords
if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO",
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &grid, &pos_obj))
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O",
// const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &grid))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OiO",
const_cast<char**>(keywords), &pos, &texture, &sprite_index, &grid))
{
// If pos was provided, it overrides x,y
if (pos_obj && pos_obj != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
return -1;
}
else
PyVectorObject* pos_result = PyVector::from_arg(pos);
if (!pos_result)
{
PyErr_Clear();
// Try alternative: pos as first argument
static const char* alt_keywords[] = { "pos", "texture", "sprite_index", "grid", nullptr };
PyObject* pos = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiO",
const_cast<char**>(alt_keywords), &pos, &texture, &sprite_index, &grid))
{
return -1;
}
// Parse position
if (pos && pos != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
}
// check types for texture
@ -137,11 +104,10 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
texture_ptr = McRFPy_API::default_texture;
}
// Allow creation without texture for testing purposes
// if (!texture_ptr) {
// PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
// return -1;
// }
if (!texture_ptr) {
PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
return -1;
}
if (grid != NULL && !PyObject_IsInstance(grid, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
@ -158,17 +124,8 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
Py_INCREF(self);
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
if (texture_ptr) {
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
} else {
// Create an empty sprite for testing
self->data->sprite = UISprite();
}
// Set position
self->data->position = sf::Vector2f(x, y);
self->data->collision_pos = sf::Vector2i(static_cast<int>(x), static_cast<int>(y));
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
self->data->position = pos_result->data;
if (grid != NULL) {
PyUIGridObject* pygrid = (PyUIGridObject*)grid;
self->data->grid = pygrid->data;
@ -287,106 +244,18 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl
return 0;
}
PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
if (member_ptr == 0) // x
return PyFloat_FromDouble(self->data->position.x);
else if (member_ptr == 1) // y
return PyFloat_FromDouble(self->data->position.y);
else
{
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
return nullptr;
}
}
int UIEntity::set_float_member(PyUIEntityObject* self, PyObject* value, void* closure)
{
float val;
auto member_ptr = reinterpret_cast<long>(closure);
if (PyFloat_Check(value))
{
val = PyFloat_AsDouble(value);
}
else if (PyLong_Check(value))
{
val = PyLong_AsLong(value);
}
else
{
PyErr_SetString(PyExc_TypeError, "Value must be a floating point number.");
return -1;
}
if (member_ptr == 0) // x
{
self->data->position.x = val;
self->data->collision_pos.x = static_cast<int>(val);
}
else if (member_ptr == 1) // y
{
self->data->position.y = val;
self->data->collision_pos.y = static_cast<int>(val);
}
return 0;
}
PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
{
// Check if entity has a grid
if (!self->data || !self->data->grid) {
Py_RETURN_NONE; // Entity not on a grid, nothing to do
}
// Remove entity from grid's entity list
auto grid = self->data->grid;
auto& entities = grid->entities;
// Find and remove this entity from the list
auto it = std::find_if(entities->begin(), entities->end(),
[self](const std::shared_ptr<UIEntity>& e) {
return e.get() == self->data.get();
});
if (it != entities->end()) {
entities->erase(it);
// Clear the grid reference
self->data->grid.reset();
}
Py_RETURN_NONE;
}
PyMethodDef UIEntity::methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
{NULL, NULL, 0, NULL}
};
// Define the PyObjectType alias for the macros
typedef PyUIEntityObject PyObjectType;
// Combine base methods with entity-specific methods
PyMethodDef UIEntity_all_methods[] = {
UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
{NULL} // Sentinel
};
PyGetSetDef UIEntity::getsetters[] = {
{"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (graphically)", (void*)0},
{"pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (integer grid coordinates)", (void*)1},
{"gridstate", (getter)UIEntity::get_gridstate, NULL, "Grid point states for the entity", NULL},
{"sprite_index", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display", NULL},
{"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display (deprecated: use sprite_index)", NULL},
{"x", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity x position", (void*)0},
{"y", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity y position", (void*)1},
{"visible", (getter)UIEntity_get_visible, (setter)UIEntity_set_visible, "Visibility flag", NULL},
{"opacity", (getter)UIEntity_get_opacity, (setter)UIEntity_set_opacity, "Opacity (0.0 = transparent, 1.0 = opaque)", NULL},
{"name", (getter)UIEntity_get_name, (setter)UIEntity_set_name, "Name for finding elements", NULL},
{NULL} /* Sentinel */
};

View File

@ -51,14 +51,8 @@ public:
bool setProperty(const std::string& name, int value);
bool getProperty(const std::string& name, float& value) const;
// Methods that delegate to sprite
sf::FloatRect get_bounds() const { return sprite.get_bounds(); }
void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; }
void resize(float w, float h) { /* Entities don't support direct resizing */ }
static PyObject* at(PyUIEntityObject* self, PyObject* o);
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* get_position(PyUIEntityObject* self, void* closure);
@ -66,16 +60,11 @@ public:
static PyObject* get_gridstate(PyUIEntityObject* self, void* closure);
static PyObject* get_spritenumber(PyUIEntityObject* self, void* closure);
static int set_spritenumber(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_float_member(PyUIEntityObject* self, void* closure);
static int set_float_member(PyUIEntityObject* self, PyObject* value, void* closure);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIEntityObject* self);
};
// Forward declaration of methods array
extern PyMethodDef UIEntity_all_methods[];
namespace mcrfpydef {
static PyTypeObject PyUIEntityType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -85,7 +74,7 @@ namespace mcrfpydef {
.tp_repr = (reprfunc)UIEntity::repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = "UIEntity objects",
.tp_methods = UIEntity_all_methods,
.tp_methods = UIEntity::methods,
.tp_getset = UIEntity::getsetters,
.tp_init = (initproc)UIEntity::init,
.tp_new = PyType_GenericNew,

View File

@ -1,75 +0,0 @@
#pragma once
#include "UIEntity.h"
#include "UIBase.h"
// UIEntity-specific property implementations
// These delegate to the wrapped sprite member
// Visible property
static PyObject* UIEntity_get_visible(PyUIEntityObject* self, void* closure)
{
return PyBool_FromLong(self->data->sprite.visible);
}
static int UIEntity_set_visible(PyUIEntityObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
self->data->sprite.visible = PyObject_IsTrue(value);
return 0;
}
// Opacity property
static PyObject* UIEntity_get_opacity(PyUIEntityObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->sprite.opacity);
}
static int UIEntity_set_opacity(PyUIEntityObject* self, PyObject* value, void* closure)
{
float opacity;
if (PyFloat_Check(value)) {
opacity = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
opacity = PyLong_AsDouble(value);
} else {
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1;
}
// Clamp to valid range
if (opacity < 0.0f) opacity = 0.0f;
if (opacity > 1.0f) opacity = 1.0f;
self->data->sprite.opacity = opacity;
return 0;
}
// Name property - delegate to sprite
static PyObject* UIEntity_get_name(PyUIEntityObject* self, void* closure)
{
return PyUnicode_FromString(self->data->sprite.name.c_str());
}
static int UIEntity_set_name(PyUIEntityObject* self, PyObject* value, void* closure)
{
if (value == NULL || value == Py_None) {
self->data->sprite.name = "";
return 0;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "name must be a string");
return -1;
}
const char* name_str = PyUnicode_AsUTF8(value);
if (!name_str) {
return -1;
}
self->data->sprite.name = name_str;
return 0;
}

View File

@ -2,40 +2,21 @@
#include "UICollection.h"
#include "GameEngine.h"
#include "PyVector.h"
#include "UICaption.h"
#include "UISprite.h"
#include "UIGrid.h"
#include "McRFPy_API.h"
#include "PyPositionHelper.h"
// UIDrawable methods now in UIBase.h
UIDrawable* UIFrame::click_at(sf::Vector2f point)
{
// Check bounds first (optimization)
float x = box.getPosition().x, y = box.getPosition().y, w = box.getSize().x, h = box.getSize().y;
if (point.x < x || point.y < y || point.x >= x+w || point.y >= y+h) {
return nullptr;
for (auto e: *children)
{
auto p = e->click_at(point + box.getPosition());
if (p)
return p;
}
// Transform to local coordinates for children
sf::Vector2f localPoint = point - box.getPosition();
// Check children in reverse order (top to bottom, highest z-index first)
for (auto it = children->rbegin(); it != children->rend(); ++it) {
auto& child = *it;
if (!child->visible) continue;
if (auto target = child->click_at(localPoint)) {
return target;
}
if (click_callable)
{
float x = box.getPosition().x, y = box.getPosition().y, w = box.getSize().x, h = box.getSize().y;
if (point.x > x && point.y > y && point.x < x+w && point.y < y+h) return this;
}
// No child handled it, check if we have a handler
if (click_callable) {
return this;
}
return nullptr;
return NULL;
}
UIFrame::UIFrame()
@ -64,95 +45,24 @@ PyObjectsEnum UIFrame::derived_type()
return PyObjectsEnum::UIFRAME;
}
// Phase 1 implementations
sf::FloatRect UIFrame::get_bounds() const
{
auto pos = box.getPosition();
auto size = box.getSize();
return sf::FloatRect(pos.x, pos.y, size.x, size.y);
}
void UIFrame::move(float dx, float dy)
{
box.move(dx, dy);
}
void UIFrame::resize(float w, float h)
{
box.setSize(sf::Vector2f(w, h));
}
void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
{
// Check visibility
if (!visible) return;
box.move(offset);
//Resources::game->getWindow().draw(box);
target.draw(box);
box.move(-offset);
// TODO: Apply opacity when SFML supports it on shapes
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
// Check if we need to use RenderTexture for clipping
if (clip_children && !children->empty()) {
// Enable RenderTexture if not already enabled
if (!use_render_texture) {
auto size = box.getSize();
enableRenderTexture(static_cast<unsigned int>(size.x),
static_cast<unsigned int>(size.y));
}
// Update RenderTexture if dirty
if (use_render_texture && render_dirty) {
// Clear the RenderTexture
render_texture->clear(sf::Color::Transparent);
// Draw the frame box to RenderTexture
box.setPosition(0, 0); // Render at origin in texture
render_texture->draw(box);
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
// Render children to RenderTexture at local coordinates
for (auto drawable : *children) {
drawable->render(sf::Vector2f(0, 0), *render_texture);
}
// Finalize the RenderTexture
render_texture->display();
// Update sprite
render_sprite.setTexture(render_texture->getTexture());
render_dirty = false;
}
// Draw the RenderTexture sprite
if (use_render_texture) {
render_sprite.setPosition(offset + box.getPosition());
target.draw(render_sprite);
}
} else {
// Standard rendering without clipping
box.move(offset);
target.draw(box);
box.move(-offset);
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
for (auto drawable : *children) {
drawable->render(offset + box.getPosition(), target);
}
for (auto drawable : *children) {
drawable->render(offset + box.getPosition(), target);
}
}
@ -205,36 +115,16 @@ int UIFrame::set_float_member(PyUIFrameObject* self, PyObject* value, void* clos
PyErr_SetString(PyExc_TypeError, "Value must be an integer.");
return -1;
}
if (member_ptr == 0) { //x
if (member_ptr == 0) //x
self->data->box.setPosition(val, self->data->box.getPosition().y);
self->data->markDirty();
}
else if (member_ptr == 1) { //y
else if (member_ptr == 1) //y
self->data->box.setPosition(self->data->box.getPosition().x, val);
self->data->markDirty();
}
else if (member_ptr == 2) { //w
else if (member_ptr == 2) //w
self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y));
if (self->data->use_render_texture) {
// Need to recreate RenderTexture with new size
self->data->enableRenderTexture(static_cast<unsigned int>(self->data->box.getSize().x),
static_cast<unsigned int>(self->data->box.getSize().y));
}
self->data->markDirty();
}
else if (member_ptr == 3) { //h
else if (member_ptr == 3) //h
self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val));
if (self->data->use_render_texture) {
// Need to recreate RenderTexture with new size
self->data->enableRenderTexture(static_cast<unsigned int>(self->data->box.getSize().x),
static_cast<unsigned int>(self->data->box.getSize().y));
}
self->data->markDirty();
}
else if (member_ptr == 4) { //outline
else if (member_ptr == 4) //outline
self->data->box.setOutlineThickness(val);
self->data->markDirty();
}
return 0;
}
@ -311,12 +201,10 @@ int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* clos
if (member_ptr == 0)
{
self->data->box.setFillColor(sf::Color(r, g, b, a));
self->data->markDirty();
}
else if (member_ptr == 1)
{
self->data->box.setOutlineColor(sf::Color(r, g, b, a));
self->data->markDirty();
}
else
{
@ -346,40 +234,9 @@ int UIFrame::set_pos(PyUIFrameObject* self, PyObject* value, void* closure)
return -1;
}
self->data->box.setPosition(vec->data);
self->data->markDirty();
return 0;
}
PyObject* UIFrame::get_clip_children(PyUIFrameObject* self, void* closure)
{
return PyBool_FromLong(self->data->clip_children);
}
int UIFrame::set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "clip_children must be a boolean");
return -1;
}
bool new_clip = PyObject_IsTrue(value);
if (new_clip != self->data->clip_children) {
self->data->clip_children = new_clip;
self->data->markDirty(); // Mark as needing redraw
}
return 0;
}
// Define the PyObjectType alias for the macros
typedef PyUIFrameObject PyObjectType;
// Method definitions
PyMethodDef UIFrame_methods[] = {
UIDRAWABLE_METHODS,
{NULL} // Sentinel
};
PyGetSetDef UIFrame::getsetters[] = {
{"x", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "X coordinate of top-left corner", (void*)0},
{"y", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Y coordinate of top-left corner", (void*)1},
@ -391,10 +248,7 @@ PyGetSetDef UIFrame::getsetters[] = {
{"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIFRAME},
{"pos", (getter)UIFrame::get_pos, (setter)UIFrame::set_pos, "Position as a Vector", NULL},
{"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL},
UIDRAWABLE_GETSETTERS,
{NULL}
};
@ -420,56 +274,35 @@ PyObject* UIFrame::repr(PyUIFrameObject* self)
int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
{
// Parse position using the standardized helper
auto pos_result = PyPositionHelper::parse_position(args, kwds);
const char* keywords[] = { "x", "y", "w", "h", "fill_color", "outline_color", "outline", "children", "click", "pos", nullptr };
//std::cout << "Init called\n";
const char* keywords[] = { "x", "y", "w", "h", "fill_color", "outline_color", "outline", nullptr };
float x = 0.0f, y = 0.0f, w = 0.0f, h=0.0f, outline=0.0f;
PyObject* fill_color = 0;
PyObject* outline_color = 0;
PyObject* children_arg = 0;
PyObject* click_handler = 0;
PyObject* pos_obj = 0;
// Try to parse all arguments including x, y
if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOO", const_cast<char**>(keywords),
&x, &y, &w, &h, &fill_color, &outline_color, &outline, &children_arg, &click_handler, &pos_obj))
{
// If pos was provided, it overrides x,y
if (pos_obj && pos_obj != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
}
else
// First try to parse as (x, y, w, h, ...)
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffff|OOf", const_cast<char**>(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline))
{
PyErr_Clear(); // Clear the error
// Try to parse as ((x,y), w, h, ...) or (Vector, w, h, ...)
const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", "children", "click", nullptr };
PyObject* pos_arg = nullptr;
PyObject* pos_obj = nullptr;
const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", nullptr };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OffOOfOO", const_cast<char**>(alt_keywords),
&pos_arg, &w, &h, &fill_color, &outline_color, &outline, &children_arg, &click_handler))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Off|OOf", const_cast<char**>(alt_keywords),
&pos_obj, &w, &h, &fill_color, &outline_color, &outline))
{
return -1;
}
// Convert position argument to x, y if provided
if (pos_arg && pos_arg != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos_arg);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
return -1;
}
x = vec->data.x;
y = vec->data.y;
// Convert position argument to x, y
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
self->data->box.setPosition(sf::Vector2f(x, y));
@ -483,70 +316,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1);
else self->data->box.setOutlineColor(sf::Color(128,128,128,255));
if (err_val) return err_val;
// Process children argument if provided
if (children_arg && children_arg != Py_None) {
if (!PySequence_Check(children_arg)) {
PyErr_SetString(PyExc_TypeError, "children must be a sequence");
return -1;
}
Py_ssize_t len = PySequence_Length(children_arg);
for (Py_ssize_t i = 0; i < len; i++) {
PyObject* child = PySequence_GetItem(children_arg, i);
if (!child) return -1;
// Check if it's a UIDrawable (Frame, Caption, Sprite, or Grid)
PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
if (!PyObject_IsInstance(child, frame_type) &&
!PyObject_IsInstance(child, caption_type) &&
!PyObject_IsInstance(child, sprite_type) &&
!PyObject_IsInstance(child, grid_type)) {
Py_DECREF(child);
PyErr_SetString(PyExc_TypeError, "children must contain only Frame, Caption, Sprite, or Grid objects");
return -1;
}
// Get the shared_ptr and add to children
std::shared_ptr<UIDrawable> drawable = nullptr;
if (PyObject_IsInstance(child, frame_type)) {
drawable = ((PyUIFrameObject*)child)->data;
} else if (PyObject_IsInstance(child, caption_type)) {
drawable = ((PyUICaptionObject*)child)->data;
} else if (PyObject_IsInstance(child, sprite_type)) {
drawable = ((PyUISpriteObject*)child)->data;
} else if (PyObject_IsInstance(child, grid_type)) {
drawable = ((PyUIGridObject*)child)->data;
}
// Clean up type references
Py_DECREF(frame_type);
Py_DECREF(caption_type);
Py_DECREF(sprite_type);
Py_DECREF(grid_type);
if (drawable) {
self->data->children->push_back(drawable);
self->data->children_need_sort = true;
}
Py_DECREF(child);
}
}
// Process click handler if provided
if (click_handler && click_handler != Py_None) {
if (!PyCallable_Check(click_handler)) {
PyErr_SetString(PyExc_TypeError, "click must be callable");
return -1;
}
self->data->click_register(click_handler);
}
return 0;
}
@ -554,81 +323,58 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
bool UIFrame::setProperty(const std::string& name, float value) {
if (name == "x") {
box.setPosition(sf::Vector2f(value, box.getPosition().y));
markDirty();
return true;
} else if (name == "y") {
box.setPosition(sf::Vector2f(box.getPosition().x, value));
markDirty();
return true;
} else if (name == "w") {
box.setSize(sf::Vector2f(value, box.getSize().y));
if (use_render_texture) {
// Need to recreate RenderTexture with new size
enableRenderTexture(static_cast<unsigned int>(box.getSize().x),
static_cast<unsigned int>(box.getSize().y));
}
markDirty();
return true;
} else if (name == "h") {
box.setSize(sf::Vector2f(box.getSize().x, value));
if (use_render_texture) {
// Need to recreate RenderTexture with new size
enableRenderTexture(static_cast<unsigned int>(box.getSize().x),
static_cast<unsigned int>(box.getSize().y));
}
markDirty();
return true;
} else if (name == "outline") {
box.setOutlineThickness(value);
markDirty();
return true;
} else if (name == "fill_color.r") {
auto color = box.getFillColor();
color.r = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "fill_color.g") {
auto color = box.getFillColor();
color.g = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "fill_color.b") {
auto color = box.getFillColor();
color.b = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "fill_color.a") {
auto color = box.getFillColor();
color.a = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "outline_color.r") {
auto color = box.getOutlineColor();
color.r = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
} else if (name == "outline_color.g") {
auto color = box.getOutlineColor();
color.g = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
} else if (name == "outline_color.b") {
auto color = box.getOutlineColor();
color.b = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
} else if (name == "outline_color.a") {
auto color = box.getOutlineColor();
color.a = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
}
return false;
@ -637,11 +383,9 @@ bool UIFrame::setProperty(const std::string& name, float value) {
bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
if (name == "fill_color") {
box.setFillColor(value);
markDirty();
return true;
} else if (name == "outline_color") {
box.setOutlineColor(value);
markDirty();
return true;
}
return false;
@ -650,16 +394,9 @@ bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "position") {
box.setPosition(value);
markDirty();
return true;
} else if (name == "size") {
box.setSize(value);
if (use_render_texture) {
// Need to recreate RenderTexture with new size
enableRenderTexture(static_cast<unsigned int>(value.x),
static_cast<unsigned int>(value.y));
}
markDirty();
return true;
}
return false;

View File

@ -29,17 +29,11 @@ public:
float outline;
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
bool children_need_sort = true; // Dirty flag for z_index sorting optimization
bool clip_children = false; // Whether to clip children to frame bounds
void render(sf::Vector2f, sf::RenderTarget&) override final;
void move(sf::Vector2f);
PyObjectsEnum derived_type() override final;
virtual UIDrawable* click_at(sf::Vector2f point) override final;
// Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override;
void resize(float w, float h) override;
static PyObject* get_children(PyUIFrameObject* self, void* closure);
static PyObject* get_float_member(PyUIFrameObject* self, void* closure);
@ -48,8 +42,6 @@ public:
static int set_color_member(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_pos(PyUIFrameObject* self, void* closure);
static int set_pos(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_clip_children(PyUIFrameObject* self, void* closure);
static int set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIFrameObject* self);
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);
@ -64,9 +56,6 @@ public:
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
};
// Forward declaration of methods array
extern PyMethodDef UIFrame_methods[];
namespace mcrfpydef {
static PyTypeObject PyUIFrameType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -85,7 +74,7 @@ namespace mcrfpydef {
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
.tp_methods = UIFrame_methods,
//.tp_methods = PyUIFrame_methods,
//.tp_members = PyUIFrame_members,
.tp_getset = UIFrame::getsetters,
//.tp_base = NULL,

View File

@ -1,38 +1,14 @@
#include "UIGrid.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "PyPositionHelper.h"
#include <algorithm>
// UIDrawable methods now in UIBase.h
UIGrid::UIGrid()
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
background_color(8, 8, 8, 255) // Default dark gray background
{
// Initialize entities list
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
// Initialize box with safe defaults
box.setSize(sf::Vector2f(0, 0));
box.setPosition(sf::Vector2f(0, 0));
box.setFillColor(sf::Color(0, 0, 0, 0));
// Initialize render texture (small default size)
renderTexture.create(1, 1);
// Initialize output sprite
output.setTextureRect(sf::IntRect(0, 0, 0, 0));
output.setPosition(0, 0);
output.setTexture(renderTexture.getTexture());
// Points vector starts empty (grid_x * grid_y = 0)
}
UIGrid::UIGrid() {}
UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _xy, sf::Vector2f _wh)
: grid_x(gx), grid_y(gy),
zoom(1.0f),
ptex(_ptex), points(gx * gy),
background_color(8, 8, 8, 255) // Default dark gray background
ptex(_ptex), points(gx * gy)
{
// Use texture dimensions if available, otherwise use defaults
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
@ -68,17 +44,12 @@ void UIGrid::update() {}
void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
{
// Check visibility
if (!visible) return;
// TODO: Apply opacity to output sprite
output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing
// output size can change; update size when drawing
output.setTextureRect(
sf::IntRect(0, 0,
box.getSize().x, box.getSize().y));
renderTexture.clear(background_color);
renderTexture.clear(sf::Color(8, 8, 8, 255)); // TODO - UIGrid needs a "background color" field
// Get cell dimensions - use texture if available, otherwise defaults
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
@ -142,13 +113,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
// middle layer - entities
// disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window)
for (auto e : *entities) {
// Skip out-of-bounds entities for performance
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 ||
e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) {
continue; // Skip this entity as it's not visible
}
// TODO skip out-of-bounds entities (grid square not visible at all, check for partially on visible grid squares / floating point grid position)
//auto drawent = e->cGrid->indexsprite.drawable();
auto& drawent = e->sprite;
//drawent.setScale(zoom, zoom);
@ -237,29 +202,6 @@ PyObjectsEnum UIGrid::derived_type()
return PyObjectsEnum::UIGRID;
}
// Phase 1 implementations
sf::FloatRect UIGrid::get_bounds() const
{
auto pos = box.getPosition();
auto size = box.getSize();
return sf::FloatRect(pos.x, pos.y, size.x, size.y);
}
void UIGrid::move(float dx, float dy)
{
box.move(dx, dy);
}
void UIGrid::resize(float w, float h)
{
box.setSize(sf::Vector2f(w, h));
// Recreate render texture with new size
if (w > 0 && h > 0) {
renderTexture.create(static_cast<unsigned int>(w), static_cast<unsigned int>(h));
output.setTexture(renderTexture.getTexture());
}
}
std::shared_ptr<PyTexture> UIGrid::getTexture()
{
return ptex;
@ -267,110 +209,24 @@ std::shared_ptr<PyTexture> UIGrid::getTexture()
UIDrawable* UIGrid::click_at(sf::Vector2f point)
{
// Check grid bounds first
if (!box.getGlobalBounds().contains(point)) {
return nullptr;
if (click_callable)
{
if(box.getGlobalBounds().contains(point)) return this;
}
// Transform to local coordinates
sf::Vector2f localPoint = point - box.getPosition();
// Get cell dimensions
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
// Calculate visible area parameters (from render function)
float center_x_sq = center_x / cell_width;
float center_y_sq = center_y / cell_height;
float width_sq = box.getSize().x / (cell_width * zoom);
float height_sq = box.getSize().y / (cell_height * zoom);
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
// Convert click position to grid coordinates
float grid_x = (localPoint.x / zoom + left_spritepixels) / cell_width;
float grid_y = (localPoint.y / zoom + top_spritepixels) / cell_height;
// Check entities in reverse order (assuming they should be checked top to bottom)
// Note: entities list is not sorted by z-index currently, but we iterate in reverse
// to match the render order assumption
if (entities) {
for (auto it = entities->rbegin(); it != entities->rend(); ++it) {
auto& entity = *it;
if (!entity || !entity->sprite.visible) continue;
// Check if click is within entity's grid cell
// Entities occupy a 1x1 grid cell centered on their position
float dx = grid_x - entity->position.x;
float dy = grid_y - entity->position.y;
if (dx >= -0.5f && dx < 0.5f && dy >= -0.5f && dy < 0.5f) {
// Click is within the entity's cell
// Check if entity sprite has a click handler
// For now, we return the entity's sprite as the click target
// Note: UIEntity doesn't derive from UIDrawable, so we check its sprite
if (entity->sprite.click_callable) {
return &entity->sprite;
}
}
}
}
// No entity handled it, check if grid itself has handler
if (click_callable) {
return this;
}
return nullptr;
return NULL;
}
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
int grid_x = 0, grid_y = 0; // Default to 0x0 grid
int grid_x, grid_y;
PyObject* textureObj = Py_None;
//float box_x, box_y, box_w, box_h;
PyObject* pos = NULL;
PyObject* size = NULL;
PyObject* grid_size_obj = NULL;
static const char* keywords[] = {"grid_x", "grid_y", "texture", "pos", "size", "grid_size", NULL};
// First try parsing with keywords
if (PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO", const_cast<char**>(keywords),
&grid_x, &grid_y, &textureObj, &pos, &size, &grid_size_obj)) {
// If grid_size is provided, use it to override grid_x and grid_y
if (grid_size_obj && grid_size_obj != Py_None) {
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
PyObject* x_obj = PyTuple_GetItem(grid_size_obj, 0);
PyObject* y_obj = PyTuple_GetItem(grid_size_obj, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
grid_x = PyLong_AsLong(x_obj);
grid_y = PyLong_AsLong(y_obj);
} else {
PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers");
return -1;
}
} else if (PyList_Check(grid_size_obj) && PyList_Size(grid_size_obj) == 2) {
PyObject* x_obj = PyList_GetItem(grid_size_obj, 0);
PyObject* y_obj = PyList_GetItem(grid_size_obj, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
grid_x = PyLong_AsLong(x_obj);
grid_y = PyLong_AsLong(y_obj);
} else {
PyErr_SetString(PyExc_TypeError, "grid_size list must contain integers");
return -1;
}
} else {
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple or list of two integers");
return -1;
}
}
} else {
// Clear error and try parsing without keywords (backward compatibility)
PyErr_Clear();
if (!PyArg_ParseTuple(args, "|iiOOO", &grid_x, &grid_y, &textureObj, &pos, &size)) {
return -1; // If parsing fails, return an error
}
//if (!PyArg_ParseTuple(args, "iiOffff", &grid_x, &grid_y, &textureObj, &box_x, &box_y, &box_w, &box_h)) {
if (!PyArg_ParseTuple(args, "ii|OOO", &grid_x, &grid_y, &textureObj, &pos, &size)) {
return -1; // If parsing fails, return an error
}
// Default position and size if not provided
@ -619,20 +475,13 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
return (PyObject*)obj;
}
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o)
{
// Use the standardized position parser
auto result = PyPositionHelper::parse_position_int(args, kwds);
if (!result.has_position) {
PyPositionHelper::set_position_int_error();
int x, y;
if (!PyArg_ParseTuple(o, "ii", &x, &y)) {
PyErr_SetString(PyExc_TypeError, "UIGrid.at requires two integer arguments: (x, y)");
return NULL;
}
int x = result.x;
int y = result.y;
// 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;
@ -651,43 +500,11 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
return (PyObject*)obj;
}
PyObject* UIGrid::get_background_color(PyUIGridObject* self, void* closure)
{
auto& color = self->data->background_color;
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
PyObject* args = Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a);
PyObject* obj = PyObject_CallObject((PyObject*)type, args);
Py_DECREF(args);
Py_DECREF(type);
return obj;
}
int UIGrid::set_background_color(PyUIGridObject* self, PyObject* value, void* closure)
{
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) {
PyErr_SetString(PyExc_TypeError, "background_color must be a Color object");
return -1;
}
PyColorObject* color = (PyColorObject*)value;
self->data->background_color = color->data;
return 0;
}
PyMethodDef UIGrid::methods[] = {
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS},
{NULL, NULL, 0, NULL}
};
// Define the PyObjectType alias for the macros
typedef PyUIGridObject PyObjectType;
// Combined methods array
PyMethodDef UIGrid_all_methods[] = {
UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{NULL} // Sentinel
};
PyGetSetDef UIGrid::getsetters[] = {
@ -712,10 +529,7 @@ PyGetSetDef UIGrid::getsetters[] = {
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID},
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
{"background_color", (getter)UIGrid::get_background_color, (setter)UIGrid::set_background_color, "Background color of the grid", NULL},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
UIDRAWABLE_GETSETTERS,
{NULL} /* Sentinel */
};
@ -1026,6 +840,184 @@ 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,
@ -1481,22 +1473,6 @@ bool UIGrid::setProperty(const std::string& name, float value) {
z_index = static_cast<int>(value);
return true;
}
else if (name == "background_color.r") {
background_color.r = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
return true;
}
else if (name == "background_color.g") {
background_color.g = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
return true;
}
else if (name == "background_color.b") {
background_color.b = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
return true;
}
else if (name == "background_color.a") {
background_color.a = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
return true;
}
return false;
}
@ -1552,22 +1528,6 @@ bool UIGrid::getProperty(const std::string& name, float& value) const {
value = static_cast<float>(z_index);
return true;
}
else if (name == "background_color.r") {
value = static_cast<float>(background_color.r);
return true;
}
else if (name == "background_color.g") {
value = static_cast<float>(background_color.g);
return true;
}
else if (name == "background_color.b") {
value = static_cast<float>(background_color.b);
return true;
}
else if (name == "background_color.a") {
value = static_cast<float>(background_color.a);
return true;
}
return false;
}

View File

@ -35,11 +35,6 @@ public:
//void setSprite(int);
virtual UIDrawable* click_at(sf::Vector2f point) override final;
// Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override;
void resize(float w, float h) override;
int grid_x, grid_y;
//int grid_size; // grid sizes are implied by IndexTexture now
sf::RectangleShape box;
@ -51,9 +46,6 @@ public:
std::vector<UIGridPoint> points;
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
// Background rendering
sf::Color background_color;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
@ -73,9 +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* get_background_color(PyUIGridObject* self, void* closure);
static int set_background_color(PyUIGridObject* self, PyObject* value, void* closure);
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_at(PyUIGridObject* self, PyObject* o);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
static PyObject* get_children(PyUIGridObject* self, void* closure);
@ -128,9 +118,6 @@ public:
};
// Forward declaration of methods array
extern PyMethodDef UIGrid_all_methods[];
namespace mcrfpydef {
static PyTypeObject PyUIGridType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -150,7 +137,7 @@ namespace mcrfpydef {
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
.tp_methods = UIGrid_all_methods,
.tp_methods = UIGrid::methods,
//.tp_members = UIGrid::members,
.tp_getset = UIGrid::getsetters,
//.tp_base = NULL,

View File

@ -1,8 +1,6 @@
#include "UISprite.h"
#include "GameEngine.h"
#include "PyVector.h"
#include "PyPositionHelper.h"
// UIDrawable methods now in UIBase.h
UIDrawable* UISprite::click_at(sf::Vector2f point)
{
@ -13,13 +11,7 @@ UIDrawable* UISprite::click_at(sf::Vector2f point)
return NULL;
}
UISprite::UISprite()
: sprite_index(0), ptex(nullptr)
{
// Initialize sprite to safe defaults
sprite.setPosition(0.0f, 0.0f);
sprite.setScale(1.0f, 1.0f);
}
UISprite::UISprite() {}
UISprite::UISprite(std::shared_ptr<PyTexture> _ptex, int _sprite_index, sf::Vector2f _pos, float _scale)
: ptex(_ptex), sprite_index(_sprite_index)
@ -38,21 +30,9 @@ void UISprite::render(sf::Vector2f offset)
void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target)
{
// Check visibility
if (!visible) return;
// Apply opacity
auto color = sprite.getColor();
color.a = static_cast<sf::Uint8>(255 * opacity);
sprite.setColor(color);
sprite.move(offset);
target.draw(sprite);
sprite.move(-offset);
// Restore original alpha
color.a = 255;
sprite.setColor(color);
}
void UISprite::setPosition(sf::Vector2f pos)
@ -104,28 +84,6 @@ PyObjectsEnum UISprite::derived_type()
return PyObjectsEnum::UISPRITE;
}
// Phase 1 implementations
sf::FloatRect UISprite::get_bounds() const
{
return sprite.getGlobalBounds();
}
void UISprite::move(float dx, float dy)
{
sprite.move(dx, dy);
}
void UISprite::resize(float w, float h)
{
// Calculate scale factors to achieve target size
auto bounds = sprite.getLocalBounds();
if (bounds.width > 0 && bounds.height > 0) {
float scaleX = w / bounds.width;
float scaleY = h / bounds.height;
sprite.setScale(scaleX, scaleY);
}
}
PyObject* UISprite::get_float_member(PyUISpriteObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
@ -268,15 +226,6 @@ int UISprite::set_pos(PyUISpriteObject* self, PyObject* value, void* closure)
return 0;
}
// Define the PyObjectType alias for the macros
typedef PyUISpriteObject PyObjectType;
// Method definitions
PyMethodDef UISprite_methods[] = {
UIDRAWABLE_METHODS,
{NULL} // Sentinel
};
PyGetSetDef UISprite::getsetters[] = {
{"x", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "X coordinate of top-left corner", (void*)0},
{"y", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Y coordinate of top-left corner", (void*)1},
@ -288,9 +237,7 @@ PyGetSetDef UISprite::getsetters[] = {
{"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UISPRITE},
{"pos", (getter)UISprite::get_pos, (setter)UISprite::set_pos, "Position as a Vector", NULL},
UIDRAWABLE_GETSETTERS,
{NULL}
};
@ -310,47 +257,33 @@ PyObject* UISprite::repr(PyUISpriteObject* self)
int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = { "x", "y", "texture", "sprite_index", "scale", "click", "pos", nullptr };
//std::cout << "Init called\n";
static const char* keywords[] = { "x", "y", "texture", "sprite_index", "scale", nullptr };
float x = 0.0f, y = 0.0f, scale = 1.0f;
int sprite_index = 0;
PyObject* texture = NULL;
PyObject* click_handler = NULL;
PyObject* pos_obj = NULL;
// Try to parse all arguments with keywords
if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifOO",
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &scale, &click_handler, &pos_obj))
{
// If pos was provided, it overrides x,y
if (pos_obj && pos_obj != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
return -1;
}
x = vec->data.x;
y = vec->data.y;
}
}
else
// First try to parse as (x, y, texture, ...)
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOif",
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &scale))
{
PyErr_Clear(); // Clear the error
// Try alternative: first arg is pos tuple/Vector
const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", "click", nullptr };
PyObject* pos = NULL;
// Try to parse as ((x,y), texture, ...) or (Vector, texture, ...)
PyObject* pos_obj = nullptr;
const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", nullptr };
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifO", const_cast<char**>(alt_keywords),
&pos, &texture, &sprite_index, &scale, &click_handler))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOif", const_cast<char**>(alt_keywords),
&pos_obj, &texture, &sprite_index, &scale))
{
return -1;
}
// Convert position argument to x, y
if (pos && pos != Py_None) {
PyVectorObject* vec = PyVector::from_arg(pos);
if (pos_obj) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately");
return -1;
}
x = vec->data.x;
@ -379,15 +312,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
self->data->setPosition(sf::Vector2f(x, y));
// Process click handler if provided
if (click_handler && click_handler != Py_None) {
if (!PyCallable_Check(click_handler)) {
PyErr_SetString(PyExc_TypeError, "click must be callable");
return -1;
}
self->data->click_register(click_handler);
}
return 0;
}

View File

@ -42,11 +42,6 @@ public:
PyObjectsEnum derived_type() override final;
// Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override;
void resize(float w, float h) override;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, int value) override;
@ -68,9 +63,6 @@ public:
};
// Forward declaration of methods array
extern PyMethodDef UISprite_methods[];
namespace mcrfpydef {
static PyTypeObject PyUISpriteType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -91,7 +83,7 @@ namespace mcrfpydef {
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
.tp_methods = UISprite_methods,
//.tp_methods = PyUIFrame_methods,
//.tp_members = PyUIFrame_members,
.tp_getset = UISprite::getsetters,
//.tp_base = NULL,

View File

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

View File

@ -1,100 +0,0 @@
#!/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)

View File

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

View File

@ -1,134 +0,0 @@
#!/usr/bin/env python3
"""Test UIFrame clipping functionality"""
import mcrfpy
from mcrfpy import Color, Frame, Caption, Vector
import sys
def test_clipping(runtime):
"""Test that clip_children property works correctly"""
mcrfpy.delTimer("test_clipping")
print("Testing UIFrame clipping functionality...")
# Create test scene
scene = mcrfpy.sceneUI("test")
# Create parent frame with clipping disabled (default)
parent1 = Frame(50, 50, 200, 150,
fill_color=Color(100, 100, 200),
outline_color=Color(255, 255, 255),
outline=2)
parent1.name = "parent1"
scene.append(parent1)
# Create parent frame with clipping enabled
parent2 = Frame(300, 50, 200, 150,
fill_color=Color(200, 100, 100),
outline_color=Color(255, 255, 255),
outline=2)
parent2.name = "parent2"
parent2.clip_children = True
scene.append(parent2)
# Add captions to both frames
caption1 = Caption(10, 10, "This text should overflow the frame bounds")
caption1.font_size = 16
caption1.fill_color = Color(255, 255, 255)
parent1.children.append(caption1)
caption2 = Caption(10, 10, "This text should be clipped to frame bounds")
caption2.font_size = 16
caption2.fill_color = Color(255, 255, 255)
parent2.children.append(caption2)
# Add child frames that extend beyond parent bounds
child1 = Frame(150, 100, 100, 100,
fill_color=Color(50, 255, 50),
outline_color=Color(0, 0, 0),
outline=1)
parent1.children.append(child1)
child2 = Frame(150, 100, 100, 100,
fill_color=Color(50, 255, 50),
outline_color=Color(0, 0, 0),
outline=1)
parent2.children.append(child2)
# Add caption to show clip state
status = Caption(50, 250,
f"Left frame: clip_children={parent1.clip_children}\n"
f"Right frame: clip_children={parent2.clip_children}")
status.font_size = 14
status.fill_color = Color(255, 255, 255)
scene.append(status)
# Add instructions
instructions = Caption(50, 300,
"Left: Children should overflow (no clipping)\n"
"Right: Children should be clipped to frame bounds\n"
"Press 'c' to toggle clipping on left frame")
instructions.font_size = 12
instructions.fill_color = Color(200, 200, 200)
scene.append(instructions)
# Take screenshot
from mcrfpy import Window, automation
automation.screenshot("frame_clipping_test.png")
print(f"Parent1 clip_children: {parent1.clip_children}")
print(f"Parent2 clip_children: {parent2.clip_children}")
# Test toggling clip_children
parent1.clip_children = True
print(f"After toggle - Parent1 clip_children: {parent1.clip_children}")
# Verify the property setter works
try:
parent1.clip_children = "not a bool" # Should raise TypeError
print("ERROR: clip_children accepted non-boolean value")
except TypeError as e:
print(f"PASS: clip_children correctly rejected non-boolean: {e}")
# Test with animations
def animate_frames(runtime):
mcrfpy.delTimer("animate")
# Animate child frames to show clipping in action
# Note: For now, just move the frames manually to demonstrate clipping
parent1.children[1].x = 50 # Move child frame
parent2.children[1].x = 50 # Move child frame
# Take another screenshot after starting animation
mcrfpy.setTimer("screenshot2", take_second_screenshot, 500)
def take_second_screenshot(runtime):
mcrfpy.delTimer("screenshot2")
automation.screenshot("frame_clipping_animated.png")
print("\nTest completed successfully!")
print("Screenshots saved:")
print(" - frame_clipping_test.png (initial state)")
print(" - frame_clipping_animated.png (with animation)")
sys.exit(0)
# Start animation after a short delay
mcrfpy.setTimer("animate", animate_frames, 100)
# Main execution
print("Creating test scene...")
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Set up keyboard handler to toggle clipping
def handle_keypress(key, modifiers):
if key == "c":
scene = mcrfpy.sceneUI("test")
parent1 = scene[0] # First frame
parent1.clip_children = not parent1.clip_children
print(f"Toggled parent1 clip_children to: {parent1.clip_children}")
mcrfpy.keypressScene(handle_keypress)
# Schedule the test
mcrfpy.setTimer("test_clipping", test_clipping, 100)
print("Test scheduled, running...")

View File

@ -1,103 +0,0 @@
#!/usr/bin/env python3
"""Advanced test for UIFrame clipping with nested frames"""
import mcrfpy
from mcrfpy import Color, Frame, Caption, Vector
import sys
def test_nested_clipping(runtime):
"""Test nested frames with clipping"""
mcrfpy.delTimer("test_nested_clipping")
print("Testing advanced UIFrame clipping with nested frames...")
# Create test scene
scene = mcrfpy.sceneUI("test")
# Create outer frame with clipping enabled
outer = Frame(50, 50, 400, 300,
fill_color=Color(50, 50, 150),
outline_color=Color(255, 255, 255),
outline=3)
outer.name = "outer"
outer.clip_children = True
scene.append(outer)
# Create inner frame that extends beyond outer bounds
inner = Frame(200, 150, 300, 200,
fill_color=Color(150, 50, 50),
outline_color=Color(255, 255, 0),
outline=2)
inner.name = "inner"
inner.clip_children = True # Also enable clipping on inner frame
outer.children.append(inner)
# Add content to inner frame that extends beyond its bounds
for i in range(5):
caption = Caption(10, 30 * i, f"Line {i+1}: This text should be double-clipped")
caption.font_size = 14
caption.fill_color = Color(255, 255, 255)
inner.children.append(caption)
# Add a child frame to inner that extends way out
deeply_nested = Frame(250, 100, 200, 150,
fill_color=Color(50, 150, 50),
outline_color=Color(255, 0, 255),
outline=2)
deeply_nested.name = "deeply_nested"
inner.children.append(deeply_nested)
# Add status text
status = Caption(50, 380,
"Nested clipping test:\n"
"- Blue outer frame clips red inner frame\n"
"- Red inner frame clips green deeply nested frame\n"
"- All text should be clipped to frame bounds")
status.font_size = 12
status.fill_color = Color(200, 200, 200)
scene.append(status)
# Test render texture size handling
print(f"Outer frame size: {outer.w}x{outer.h}")
print(f"Inner frame size: {inner.w}x{inner.h}")
# Dynamically resize frames to test RenderTexture recreation
def resize_test(runtime):
mcrfpy.delTimer("resize_test")
print("Resizing frames to test RenderTexture recreation...")
outer.w = 450
outer.h = 350
inner.w = 350
inner.h = 250
print(f"New outer frame size: {outer.w}x{outer.h}")
print(f"New inner frame size: {inner.w}x{inner.h}")
# Take screenshot after resize
mcrfpy.setTimer("screenshot_resize", take_resize_screenshot, 500)
def take_resize_screenshot(runtime):
mcrfpy.delTimer("screenshot_resize")
from mcrfpy import automation
automation.screenshot("frame_clipping_resized.png")
print("\nAdvanced test completed!")
print("Screenshots saved:")
print(" - frame_clipping_resized.png (after resize)")
sys.exit(0)
# Take initial screenshot
from mcrfpy import automation
automation.screenshot("frame_clipping_nested.png")
print("Initial screenshot saved: frame_clipping_nested.png")
# Schedule resize test
mcrfpy.setTimer("resize_test", resize_test, 1000)
# Main execution
print("Creating advanced test scene...")
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Schedule the test
mcrfpy.setTimer("test_nested_clipping", test_nested_clipping, 100)
print("Advanced test scheduled, running...")

View File

@ -1,126 +0,0 @@
#!/usr/bin/env python3
"""Test Grid background color functionality"""
import mcrfpy
import sys
def test_grid_background():
"""Test Grid background color property"""
print("Testing Grid Background Color...")
# Create a test scene
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create a grid with default background
grid = mcrfpy.Grid(20, 15, grid_size=(20, 15))
grid.x = 50
grid.y = 50
grid.w = 400
grid.h = 300
ui.append(grid)
# Add some tiles to see the background better
for x in range(5, 15):
for y in range(5, 10):
point = grid.at(x, y)
point.color = mcrfpy.Color(100, 150, 100)
# Add UI to show current background color
info_frame = mcrfpy.Frame(500, 50, 200, 150,
fill_color=mcrfpy.Color(40, 40, 40),
outline_color=mcrfpy.Color(200, 200, 200),
outline=2)
ui.append(info_frame)
color_caption = mcrfpy.Caption(510, 60, "Background Color:")
color_caption.font_size = 14
color_caption.fill_color = mcrfpy.Color(255, 255, 255)
info_frame.children.append(color_caption)
color_display = mcrfpy.Caption(510, 80, "")
color_display.font_size = 12
color_display.fill_color = mcrfpy.Color(200, 200, 200)
info_frame.children.append(color_display)
# Activate the scene
mcrfpy.setScene("test")
def run_tests(dt):
"""Run background color tests"""
mcrfpy.delTimer("run_tests")
print("\nTest 1: Default background color")
default_color = grid.background_color
print(f"Default: R={default_color.r}, G={default_color.g}, B={default_color.b}, A={default_color.a}")
color_display.text = f"R:{default_color.r} G:{default_color.g} B:{default_color.b}"
def test_set_color(dt):
mcrfpy.delTimer("test_set")
print("\nTest 2: Set background to blue")
grid.background_color = mcrfpy.Color(20, 40, 100)
new_color = grid.background_color
print(f"✓ Set to: R={new_color.r}, G={new_color.g}, B={new_color.b}")
color_display.text = f"R:{new_color.r} G:{new_color.g} B:{new_color.b}"
def test_animation(dt):
mcrfpy.delTimer("test_anim")
print("\nTest 3: Manual color cycling")
# Manually change color to test property is working
colors = [
mcrfpy.Color(200, 20, 20), # Red
mcrfpy.Color(20, 200, 20), # Green
mcrfpy.Color(20, 20, 200), # Blue
]
color_index = [0] # Use list to allow modification in nested function
def cycle_red(dt):
mcrfpy.delTimer("cycle_0")
grid.background_color = colors[0]
c = grid.background_color
color_display.text = f"R:{c.r} G:{c.g} B:{c.b}"
print(f"✓ Set to Red: R={c.r}, G={c.g}, B={c.b}")
def cycle_green(dt):
mcrfpy.delTimer("cycle_1")
grid.background_color = colors[1]
c = grid.background_color
color_display.text = f"R:{c.r} G:{c.g} B:{c.b}"
print(f"✓ Set to Green: R={c.r}, G={c.g}, B={c.b}")
def cycle_blue(dt):
mcrfpy.delTimer("cycle_2")
grid.background_color = colors[2]
c = grid.background_color
color_display.text = f"R:{c.r} G:{c.g} B:{c.b}"
print(f"✓ Set to Blue: R={c.r}, G={c.g}, B={c.b}")
# Cycle through colors
mcrfpy.setTimer("cycle_0", cycle_red, 100)
mcrfpy.setTimer("cycle_1", cycle_green, 400)
mcrfpy.setTimer("cycle_2", cycle_blue, 700)
def test_complete(dt):
mcrfpy.delTimer("complete")
print("\nTest 4: Final color check")
final_color = grid.background_color
print(f"Final: R={final_color.r}, G={final_color.g}, B={final_color.b}")
print("\n✓ Grid background color tests completed!")
print("- Default background color works")
print("- Setting background color works")
print("- Color cycling works")
sys.exit(0)
# Schedule tests
mcrfpy.setTimer("test_set", test_set_color, 1000)
mcrfpy.setTimer("test_anim", test_animation, 2000)
mcrfpy.setTimer("complete", test_complete, 4500)
# Start tests
mcrfpy.setTimer("run_tests", run_tests, 100)
if __name__ == "__main__":
test_grid_background()

View File

@ -1,101 +0,0 @@
// Example of how UIFrame would implement unified click handling
//
// Click Priority Example:
// - Dialog Frame (has click handler to drag window)
// - Title Caption (no click handler)
// - Button Frame (has click handler)
// - Button Caption "OK" (no click handler)
// - Close X Sprite (has click handler)
//
// Clicking on:
// - "OK" text -> Button Frame gets the click (deepest parent with handler)
// - Close X -> Close sprite gets the click
// - Title bar -> Dialog Frame gets the click (no child has handler there)
// - Outside dialog -> nullptr (bounds check fails)
class UIFrame : public UIDrawable, protected RectangularContainer {
private:
// Implementation of container interface
sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const override {
// Children use same coordinate system as frame's local coordinates
return localPoint;
}
UIDrawable* getClickHandler() override {
return click_callable ? this : nullptr;
}
std::vector<UIDrawable*> getClickableChildren() override {
std::vector<UIDrawable*> result;
for (auto& child : *children) {
result.push_back(child.get());
}
return result;
}
public:
UIDrawable* click_at(sf::Vector2f point) override {
// Update bounds from box
bounds = sf::FloatRect(box.getPosition().x, box.getPosition().y,
box.getSize().x, box.getSize().y);
// Use unified handler
return handleClick(point);
}
};
// Example for UIGrid with entity coordinate transformation
class UIGrid : public UIDrawable, protected RectangularContainer {
private:
sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const override {
// For entities, we need to transform from pixel coordinates to grid coordinates
// This is where the grid's special coordinate system is handled
// Assuming entity positions are in grid cells, not pixels
// We pass pixel coordinates relative to the grid's rendering area
return localPoint; // Entities will handle their own sprite positioning
}
std::vector<UIDrawable*> getClickableChildren() override {
std::vector<UIDrawable*> result;
// Only check entities that are visible on screen
float left_edge = center_x - (box.getSize().x / 2.0f) / (grid_size * zoom);
float top_edge = center_y - (box.getSize().y / 2.0f) / (grid_size * zoom);
float right_edge = left_edge + (box.getSize().x / (grid_size * zoom));
float bottom_edge = top_edge + (box.getSize().y / (grid_size * zoom));
for (auto& entity : entities) {
// Check if entity is within visible bounds
if (entity->position.x >= left_edge - 1 && entity->position.x < right_edge + 1 &&
entity->position.y >= top_edge - 1 && entity->position.y < bottom_edge + 1) {
result.push_back(&entity->sprite);
}
}
return result;
}
};
// For Scene, which has no coordinate transformation
class PyScene : protected UIContainerBase {
private:
sf::Vector2f toLocalCoordinates(sf::Vector2f point) const override {
// Scene uses window coordinates directly
return point;
}
sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const override {
// Top-level drawables use window coordinates
return localPoint;
}
bool containsPoint(sf::Vector2f localPoint) const override {
// Scene contains all points (full window)
return true;
}
UIDrawable* getClickHandler() override {
// Scene itself doesn't handle clicks
return nullptr;
}
};