Compare commits

...

30 Commits

Author SHA1 Message Date
John McCardle 9bd1561bfc 🎉 ALPHA 0.1 ACHIEVED! Update ROADMAP to reflect alpha release
- Mark project as Alpha 0.1 complete
- Move RenderTexture (#6) to Beta (not essential for Alpha)
- All 6 original alpha blockers resolved:
  * Animation system (#59)
  * Z-order rendering (#63)
  * Python Sequence Protocol (#69)
  * New README (#47)
  * Removed deprecated methods (#2, #3)
- Ready for alpha release and merge to main!

The engine now has:
- Full Python scripting with game loop integration
- Complete UI system with animations
- Proper z-order rendering
- Python sequence protocol for collections
- Automation API for testing
- Headless mode support
- Cross-platform CMake build

🍾 Time to celebrate - McRogueFace Alpha 0.1 is ready!
2025-07-05 11:20:07 -04:00
John McCardle 43321487eb Update ROADMAP.md: Mark Issue #63 (z-order rendering) as complete
- Add z-order rendering to recent achievements
- Update alpha blocker count from 3 to 2
- Archive z-order test files
- Next priority: RenderTexture concept (#6) - last major alpha blocker
2025-07-05 10:36:09 -04:00
John McCardle 90c318104b Fix Issue #63: Implement z-order rendering with dirty flag optimization
- Add dirty flags to PyScene and UIFrame to track when sorting is needed
- Implement lazy sorting - only sort when z_index changes or elements are added/removed
- Make Frame children respect z_index (previously rendered in insertion order only)
- Update UIDrawable::set_int to notify when z_index changes
- Mark collections dirty on append, remove, setitem, and slice operations
- Remove per-frame vector copy in PyScene::render for better performance

Performance improvement: Static scenes now use O(1) check instead of O(n log n) sort every frame

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 10:34:06 -04:00
John McCardle 2a48138011 Update ROADMAP.md to reflect completion of Issue #69 (Sequence Protocol)
- Mark Issue #69 as complete in all sections
- Add achievement entry for Python Sequence Protocol implementation
- Update alpha blockers count: 3 remaining (was 4)
- Update total open issues: 62 (was 63)
- Next priorities: Z-order rendering (#63) or RenderTexture (#6)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 02:00:12 -04:00
John McCardle e4482e7189 Implement complete Python Sequence Protocol for collections (closes #69)
Major implementation of the full sequence protocol for both UICollection
and UIEntityCollection, making them behave like proper Python sequences.

Core Features Implemented:
- __setitem__ (collection[i] = value) with type validation
- __delitem__ (del collection[i]) with proper cleanup
- __contains__ (item in collection) by C++ pointer comparison
- __add__ (collection + other) returns Python list
- __iadd__ (collection += other) with full validation before modification
- Negative indexing support throughout
- Complete slice support (getting, setting, deletion)
- Extended slices with step \!= 1
- index() and count() methods
- Type safety enforced for all operations

UICollection specifics:
- Accepts Frame, Caption, Sprite, and Grid objects only
- Preserves z_index when replacing items
- Auto-assigns z_index on append (existing behavior maintained)

UIEntityCollection specifics:
- Accepts Entity objects only
- Manages grid references on add/remove/replace
- Uses std::list iteration with std::advance()

Also includes:
- Default value support for constructors:
  - Caption accepts None for font (uses default_font)
  - Grid accepts None for texture (uses default_texture)
  - Sprite accepts None for texture (uses default_texture)
  - Entity accepts None for texture (uses default_texture)

This completes Issue #69, removing it as an Alpha Blocker.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 01:58:03 -04:00
John McCardle 38d44777f5 Update ROADMAP.md to reflect completion of Issue #59 (Animation System)
- Mark Animation system as complete in all relevant sections
- Update alpha blockers count from 7 to 4
- Add animation system architectural decisions
- Update project status and next priorities

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 00:58:41 -04:00
John McCardle 70cf44f8f0 Implement comprehensive animation system (closes #59)
- Add Animation class with 30+ easing functions (linear, ease in/out, quad, cubic, elastic, bounce, etc.)
- Add property system to all UI classes for animation support:
  - UIFrame: position, size, colors (including individual r/g/b/a components)
  - UICaption: position, size, text, colors
  - UISprite: position, scale, sprite_number (with sequence support)
  - UIGrid: position, size, camera center, zoom
  - UIEntity: position, sprite properties
- Create AnimationManager singleton for frame-based updates
- Add Python bindings through PyAnimation wrapper
- Support for delta animations (relative values)
- Fix segfault when running scripts directly (mcrf_module initialization)
- Fix headless/windowed mode behavior to respect --headless flag
- Animations run purely in C++ without Python callbacks per frame

All UI properties are now animatable with smooth interpolation and professional easing curves.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 00:56:42 -04:00
John McCardle dd3c64784d Mark Issue #47 (Alpha README) as completed in ROADMAP
Documentation has been comprehensively updated for the Alpha release.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 06:59:29 -04:00
John McCardle 05bddae511 Update comprehensive documentation for Alpha release (Issue #47)
- Completely rewrote README.md to reflect 7DRL 2025 success and current features
- Updated GitHub Pages documentation site with:
  - Modern landing page highlighting Crypt of Sokoban
  - Comprehensive API reference (2700+ lines) with exhaustive examples
  - Updated getting-started guide with installation and first game tutorial
  - 8 detailed tutorials covering all major game systems
  - Quick reference cheat sheet for common operations
- Generated documentation screenshots showing UI elements
- Fixed deprecated API references and added new features
- Added automation API documentation
- Included Python 3.12 requirement and platform-specific instructions

Note: Text rendering in headless mode has limitations for screenshots

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 06:59:02 -04:00
John McCardle 0d26d51bc3 Compress ROADMAP.md and archive completed test files
- Condensed 'Today's Achievements' section for clarity
- Archived 9 completed test files from bug fixing session
- Updated task completion status for issues fixed today
- Identified 5 remaining Alpha blockers as next priority

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 23:05:30 -04:00
John McCardle af6a5e090b Update ROADMAP.md to reflect completion of Issues #2 and #3
- Marked both issues as completed with the removal of deprecated action system
- Updated open issue count from ~50 to ~48
- These were both Alpha blockers, bringing us closer to release

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 21:43:58 -04:00
John McCardle 281800cd23 Remove deprecated registerPyAction/registerInputAction system (closes #2, closes #3)
This is our largest net-negative commit yet\! Removed the entire deprecated
action registration system that provided unnecessary two-step indirection:
keyboard → action string → Python callback

Removed components:
- McRFPy_API::_registerPyAction() and _registerInputAction() methods
- McRFPy_API::callbacks map for storing Python callables
- McRFPy_API::doAction() method for executing callbacks
- ACTIONPY macro from Scene.h for detecting "_py" suffixed actions
- Scene::registerActionInjected() and unregisterActionInjected() methods
- tests/api_registerPyAction_issue2_test.py (tested deprecated functionality)

The game now exclusively uses keypressScene() for keyboard input handling,
which is simpler and more direct. Also commented out the unused _camFollow
function that referenced non-existent do_camfollow variable.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 21:43:22 -04:00
John McCardle cc8a7d20e8 Clean up temporary test files 2025-07-03 21:13:59 -04:00
John McCardle ff83fd8bb1 Update ROADMAP.md to reflect massive progress today
- Fixed 12+ critical bugs in a single session
- Implemented 3 missing features (Entity.index, EntityCollection.extend, sprite validation)
- Updated Phase 1 progress showing 11 of 12 items complete
- Added detailed summary of today's achievements with issue numbers
- Emphasized test-driven development approach used throughout
2025-07-03 21:13:46 -04:00
John McCardle dae400031f Remove deprecated player_input and turn-based functions for Issue #3
Removed the commented-out player_input(), computerTurn(), and playerTurn()
functions that were part of the old turn-based system. These are no longer
needed as input is now handled through Scene callbacks.

Partial fix for #3
2025-07-03 21:12:29 -04:00
John McCardle cb0130b46e Implement sprite index validation for Issue #33
Added validation to prevent setting sprite indices outside the valid
range for a texture. The implementation:
- Adds getSpriteCount() method to PyTexture to expose total sprites
- Validates sprite_number setter to ensure index is within bounds
- Provides clear error messages showing valid range
- Works for both Sprite and Entity objects

closes #33
2025-07-03 21:09:06 -04:00
John McCardle 1e7f5e9e7e Implement EntityCollection.extend() method for Issue #27
Added extend() method to EntityCollection that accepts any iterable
of Entity objects and adds them all to the collection. The method:
- Accepts lists, tuples, generators, or any iterable
- Validates all items are Entity objects
- Sets the grid association for each added entity
- Properly handles errors and empty iterables

closes #27
2025-07-03 21:05:47 -04:00
John McCardle 923350137d Implement Entity.index() method for Issue #73
Added index() method to Entity class that returns the entity's
position in its parent grid's entity collection. This enables
proper entity removal patterns using entity.index().
2025-07-03 21:02:14 -04:00
John McCardle 6134869371 Add validation to keypressScene() for non-callable arguments
Added PyCallable_Check validation to ensure keypressScene() only
accepts callable objects. Now properly raises TypeError with a
clear error message when passed non-callable arguments like
strings, numbers, None, or dicts.
2025-07-03 20:41:03 -04:00
John McCardle 4715356b5e Fix Sprite texture setter 'error return without exception set'
Implemented the missing UISprite::set_texture method to properly:
- Validate the input is a Texture instance
- Update the sprite's texture using setTexture()
- Return appropriate error messages for invalid inputs

The setter now works correctly and no longer returns -1 without
setting an exception.
2025-07-03 20:31:36 -04:00
John McCardle 6dd1cec600 Fix Entity property setters and PyVector implementation
Fixed the 'new style getargs format' error in Entity property setters by:
- Implementing PyObject_to_sfVector2f/2i using PyVector::from_arg
- Adding proper error checking in Entity::set_position
- Implementing PyVector get_member/set_member for x/y properties
- Fixing PyVector::from_arg to handle non-tuple arguments correctly

Now Entity.pos and Entity.sprite_number setters work correctly with
proper type validation.
2025-07-03 20:27:32 -04:00
John McCardle f82b861bcd Fix Issue #74: Add missing Grid.grid_y property
Added individual grid_x and grid_y getter properties to the Grid class
to complement the existing grid_size property. This allows direct access
to grid dimensions and fixes error messages that referenced these
properties before they existed.

closes #74
2025-07-03 19:48:33 -04:00
John McCardle 59e6f8d53d Fix Issue #78: Middle mouse click no longer sends 'C' keyboard event
The bug was caused by accessing event.key.code on a mouse event without
checking the event type first. Since SFML uses a union for events, this
read garbage data. The middle mouse button value (2) coincidentally matched
the keyboard 'C' value (2), causing the spurious keyboard event.

Fixed by adding event type check before accessing key-specific fields.
Only keyboard events (KeyPressed/KeyReleased) now trigger key callbacks.

Test added to verify middle clicks no longer generate keyboard events.

Closes #78
2025-07-03 19:42:32 -04:00
John McCardle 1c71d8d4f7 Fix Grid to support None/null texture and fix error message bug
- Allow Grid to be created with None as texture parameter
- Use default cell dimensions (16x16) when no texture provided
- Skip sprite rendering when texture is null, but still render colors
- Fix issue #77: Corrected copy/paste error in Grid.at() error messages
- Grid now functional for color-only rendering and entity positioning

Test created to verify Grid works without texture, showing colored cells.

Closes #77
2025-07-03 19:40:42 -04:00
John McCardle 18cfe93a44 Fix --exec interactive prompt bug and create comprehensive test suite
Major fixes:
- Fixed --exec entering Python REPL instead of game loop
- Resolved screenshot transparency issue (requires timer callbacks)
- Added debug output to trace Python initialization

Test suite created:
- 13 comprehensive tests covering all Python-exposed methods
- Tests use timer callback pattern for proper game loop interaction
- Discovered multiple critical bugs and missing features

Critical bugs found:
- Grid class segfaults on instantiation (blocks all Grid functionality)
- Issue #78 confirmed: Middle mouse click sends 'C' keyboard event
- Entity property setters have argument parsing errors
- Sprite texture setter returns improper error
- keypressScene() segfaults on non-callable arguments

Documentation updates:
- Updated CLAUDE.md with testing guidelines and TDD practices
- Created test reports documenting all findings
- Updated ROADMAP.md with test results and new priorities

The Grid segfault is now the highest priority as it blocks all Grid-based functionality.
2025-07-03 19:25:49 -04:00
John McCardle 9ad0b6850d Update ROADMAP.md to reflect Python interpreter and automation API progress
- Mark #32 (Python interpreter behavior) as 90% complete
  - All major Python flags implemented: -h, -V, -c, -m, -i
  - Script execution with proper sys.argv handling works
  - Only stdin (-) support missing

- Note that new automation API enables:
  - Automated UI testing capabilities
  - Demo recording and playback
  - Accessibility testing support

- Flag issues #53 and #45 as potentially aided by automation API
2025-07-03 15:55:24 -04:00
John McCardle 7ec4698653 Update ROADMAP.md to remove closed issues
- Remove #72 (iterator improvements - closed)
- Remove #51 (UIEntity derive from UIDrawable - closed)
- Update issue counts: 64 open issues from original 78
- Update dependencies and references to reflect closed issues
- Clarify that core iterators are complete, only grid points remain
2025-07-03 14:57:59 -04:00
John McCardle 68c1a016b0 Implement --exec flag and PyAutoGUI-compatible automation API
- Add --exec flag to execute multiple scripts before main program
- Scripts are executed in order and share Python interpreter state
- Implement full PyAutoGUI-compatible automation API in McRFPy_Automation
- Add screenshot, mouse control, keyboard input capabilities
- Fix Python initialization issues when multiple scripts are loaded
- Update CommandLineParser to handle --exec with proper sys.argv management
- Add comprehensive examples and documentation

This enables automation testing by allowing test scripts to run alongside
games using the same Python environment. The automation API provides
event injection into the SFML render loop for UI testing.

Closes #32 partially (Python interpreter emulation)
References automation testing requirements
2025-07-03 14:27:01 -04:00
John McCardle 763fa201f0 Python command emulation 2025-07-03 10:46:21 -04:00
John McCardle a44b8c93e9 Prep: Cleanup for interpreter mode 2025-07-03 09:42:46 -04:00
115 changed files with 11066 additions and 399 deletions

View File

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
Test for Entity property setters - fixing "new style getargs format" error
Verifies that Entity position and sprite_number setters work correctly.
"""
def test_entity_setters(timer_name):
"""Test that Entity property setters work correctly"""
import mcrfpy
print("Testing Entity property setters...")
# Create test scene and grid
mcrfpy.createScene("entity_test")
ui = mcrfpy.sceneUI("entity_test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Create entity
initial_pos = mcrfpy.Vector(2.5, 3.5)
entity = mcrfpy.Entity(initial_pos, texture, 5, grid)
grid.entities.append(entity)
print(f"✓ Created entity at position {entity.pos}")
# Test position setter with Vector
new_pos = mcrfpy.Vector(4.0, 5.0)
try:
entity.pos = new_pos
assert entity.pos.x == 4.0, f"Expected x=4.0, got {entity.pos.x}"
assert entity.pos.y == 5.0, f"Expected y=5.0, got {entity.pos.y}"
print(f"✓ Position setter works with Vector: {entity.pos}")
except Exception as e:
print(f"✗ Position setter failed: {e}")
raise
# Test position setter with tuple (should also work via PyVector::from_arg)
try:
entity.pos = (7.5, 8.5)
assert entity.pos.x == 7.5, f"Expected x=7.5, got {entity.pos.x}"
assert entity.pos.y == 8.5, f"Expected y=8.5, got {entity.pos.y}"
print(f"✓ Position setter works with tuple: {entity.pos}")
except Exception as e:
print(f"✗ Position setter with tuple failed: {e}")
raise
# Test draw_pos setter (collision position)
try:
entity.draw_pos = mcrfpy.Vector(3, 4)
assert entity.draw_pos.x == 3, f"Expected x=3, got {entity.draw_pos.x}"
assert entity.draw_pos.y == 4, f"Expected y=4, got {entity.draw_pos.y}"
print(f"✓ Draw position setter works: {entity.draw_pos}")
except Exception as e:
print(f"✗ Draw position setter failed: {e}")
raise
# Test sprite_number setter
try:
entity.sprite_number = 10
assert entity.sprite_number == 10, f"Expected sprite_number=10, got {entity.sprite_number}"
print(f"✓ Sprite number setter works: {entity.sprite_number}")
except Exception as e:
print(f"✗ Sprite number setter failed: {e}")
raise
# Test invalid position setter (should raise TypeError)
try:
entity.pos = "invalid"
print("✗ Position setter should have raised TypeError for string")
assert False, "Should have raised TypeError"
except TypeError as e:
print(f"✓ Position setter correctly rejects invalid type: {e}")
except Exception as e:
print(f"✗ Unexpected error: {e}")
raise
# Test invalid sprite number (should raise TypeError)
try:
entity.sprite_number = "invalid"
print("✗ Sprite number setter should have raised TypeError for string")
assert False, "Should have raised TypeError"
except TypeError as e:
print(f"✓ Sprite number setter correctly rejects invalid type: {e}")
except Exception as e:
print(f"✗ Unexpected error: {e}")
raise
# Cleanup timer
mcrfpy.delTimer("test_timer")
print("\n✅ Entity property setters test PASSED - All setters work correctly")
# Execute the test after a short delay to ensure window is ready
import mcrfpy
mcrfpy.setTimer("test_timer", test_entity_setters, 100)

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
Simple test for Entity property setters
"""
def test_entity_setters(timer_name):
"""Test Entity property setters"""
import mcrfpy
import sys
print("Testing Entity property setters...")
# Create test scene and grid
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Create entity
entity = mcrfpy.Entity((2.5, 3.5), texture, 5, grid)
grid.entities.append(entity)
# Test 1: Initial position
print(f"Initial position: {entity.pos}")
print(f"Initial position x={entity.pos.x}, y={entity.pos.y}")
# Test 2: Set position with Vector
entity.pos = mcrfpy.Vector(4.0, 5.0)
print(f"After Vector setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}")
# Test 3: Set position with tuple
entity.pos = (7.5, 8.5)
print(f"After tuple setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}")
# Test 4: sprite_number
print(f"Initial sprite_number: {entity.sprite_number}")
entity.sprite_number = 10
print(f"After setter: sprite_number={entity.sprite_number}")
# Test 5: Invalid types
try:
entity.pos = "invalid"
print("ERROR: Should have raised TypeError")
except TypeError as e:
print(f"✓ Correctly rejected invalid position: {e}")
try:
entity.sprite_number = "invalid"
print("ERROR: Should have raised TypeError")
except TypeError as e:
print(f"✓ Correctly rejected invalid sprite_number: {e}")
print("\n✅ Entity property setters test completed")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_entity_setters, 100)

View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Test for Issue #27: EntityCollection.extend() method
Verifies that EntityCollection can extend with multiple entities at once.
"""
def test_entity_extend(timer_name):
"""Test that EntityCollection.extend() method works correctly"""
import mcrfpy
import sys
print("Issue #27 test: EntityCollection.extend() method")
# Create test scene and grid
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Add some initial entities
entity1 = mcrfpy.Entity((1, 1), texture, 1, grid)
entity2 = mcrfpy.Entity((2, 2), texture, 2, grid)
grid.entities.append(entity1)
grid.entities.append(entity2)
print(f"✓ Initial entities: {len(grid.entities)}")
# Test 1: Extend with a list of entities
new_entities = [
mcrfpy.Entity((3, 3), texture, 3, grid),
mcrfpy.Entity((4, 4), texture, 4, grid),
mcrfpy.Entity((5, 5), texture, 5, grid)
]
try:
grid.entities.extend(new_entities)
assert len(grid.entities) == 5, f"Expected 5 entities, got {len(grid.entities)}"
print(f"✓ Extended with list: now {len(grid.entities)} entities")
except Exception as e:
print(f"✗ Failed to extend with list: {e}")
raise
# Test 2: Extend with a tuple
more_entities = (
mcrfpy.Entity((6, 6), texture, 6, grid),
mcrfpy.Entity((7, 7), texture, 7, grid)
)
try:
grid.entities.extend(more_entities)
assert len(grid.entities) == 7, f"Expected 7 entities, got {len(grid.entities)}"
print(f"✓ Extended with tuple: now {len(grid.entities)} entities")
except Exception as e:
print(f"✗ Failed to extend with tuple: {e}")
raise
# Test 3: Extend with generator expression
try:
grid.entities.extend(mcrfpy.Entity((8, i), texture, 8+i, grid) for i in range(3))
assert len(grid.entities) == 10, f"Expected 10 entities, got {len(grid.entities)}"
print(f"✓ Extended with generator: now {len(grid.entities)} entities")
except Exception as e:
print(f"✗ Failed to extend with generator: {e}")
raise
# Test 4: Verify all entities have correct grid association
for i, entity in enumerate(grid.entities):
# Just checking that we can iterate and access them
assert entity.sprite_number >= 1, f"Entity {i} has invalid sprite number"
print("✓ All entities accessible and valid")
# Test 5: Invalid input - non-iterable
try:
grid.entities.extend(42)
print("✗ Should have raised TypeError for non-iterable")
except TypeError as e:
print(f"✓ Correctly rejected non-iterable: {e}")
# Test 6: Invalid input - iterable with non-Entity
try:
grid.entities.extend([entity1, "not an entity", entity2])
print("✗ Should have raised TypeError for non-Entity in iterable")
except TypeError as e:
print(f"✓ Correctly rejected non-Entity in iterable: {e}")
# Test 7: Empty iterable (should work)
initial_count = len(grid.entities)
try:
grid.entities.extend([])
assert len(grid.entities) == initial_count, "Empty extend changed count"
print("✓ Empty extend works correctly")
except Exception as e:
print(f"✗ Empty extend failed: {e}")
raise
print(f"\n✅ Issue #27 test PASSED - EntityCollection.extend() works correctly")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_entity_extend, 100)

View File

@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Test for Issue #33: Sprite index validation
Verifies that Sprite and Entity objects validate sprite indices
against the texture's actual sprite count.
"""
def test_sprite_index_validation(timer_name):
"""Test that sprite index validation works correctly"""
import mcrfpy
import sys
print("Issue #33 test: Sprite index validation")
# Create test scene
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create texture - kenney_ice.png is 11x12 sprites of 16x16 each
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
# Total sprites = 11 * 12 = 132 sprites (indices 0-131)
# Test 1: Create sprite with valid index
try:
sprite = mcrfpy.Sprite(100, 100, texture, 50) # Valid index
ui.append(sprite)
print(f"✓ Created sprite with valid index 50")
except Exception as e:
print(f"✗ Failed to create sprite with valid index: {e}")
raise
# Test 2: Set valid sprite index
try:
sprite.sprite_number = 100 # Still valid
assert sprite.sprite_number == 100
print(f"✓ Set sprite to valid index 100")
except Exception as e:
print(f"✗ Failed to set valid sprite index: {e}")
raise
# Test 3: Set maximum valid index
try:
sprite.sprite_number = 131 # Maximum valid index
assert sprite.sprite_number == 131
print(f"✓ Set sprite to maximum valid index 131")
except Exception as e:
print(f"✗ Failed to set maximum valid index: {e}")
raise
# Test 4: Invalid negative index
try:
sprite.sprite_number = -1
print("✗ Should have raised ValueError for negative index")
except ValueError as e:
print(f"✓ Correctly rejected negative index: {e}")
except Exception as e:
print(f"✗ Wrong exception type for negative index: {e}")
raise
# Test 5: Invalid index too large
try:
sprite.sprite_number = 132 # One past the maximum
print("✗ Should have raised ValueError for index 132")
except ValueError as e:
print(f"✓ Correctly rejected out-of-bounds index: {e}")
except Exception as e:
print(f"✗ Wrong exception type for out-of-bounds index: {e}")
raise
# Test 6: Very large invalid index
try:
sprite.sprite_number = 1000
print("✗ Should have raised ValueError for index 1000")
except ValueError as e:
print(f"✓ Correctly rejected large invalid index: {e}")
# Test 7: Entity sprite_number validation
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
entity = mcrfpy.Entity((5, 5), texture, 50, grid)
grid.entities.append(entity)
try:
entity.sprite_number = 200 # Out of bounds
print("✗ Entity should also validate sprite indices")
except ValueError as e:
print(f"✓ Entity also validates sprite indices: {e}")
except Exception as e:
# Entity might not have the same validation yet
print(f"Note: Entity validation not implemented yet: {e}")
# Test 8: Different texture sizes
# Create a smaller texture to test different bounds
small_texture = mcrfpy.Texture("assets/Sprite-0001.png", 32, 32)
small_sprite = mcrfpy.Sprite(200, 200, small_texture, 0)
# This texture might have fewer sprites, test accordingly
try:
small_sprite.sprite_number = 100 # Might be out of bounds
print("Note: Small texture accepted index 100")
except ValueError as e:
print(f"✓ Small texture has different bounds: {e}")
print(f"\n✅ Issue #33 test PASSED - Sprite index validation works correctly")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_sprite_index_validation, 100)

View File

@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Test for Issue #73: Entity.index() method for removal
Verifies that Entity objects can report their index in the grid's entity collection.
"""
def test_entity_index(timer_name):
"""Test that Entity.index() method works correctly"""
import mcrfpy
import sys
print("Issue #73 test: Entity.index() method")
# Create test scene and grid
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Create multiple entities
entities = []
for i in range(5):
entity = mcrfpy.Entity((i, i), texture, i, grid)
entities.append(entity)
grid.entities.append(entity)
print(f"✓ Created {len(entities)} entities")
# Test 1: Check each entity knows its index
for expected_idx, entity in enumerate(entities):
try:
actual_idx = entity.index()
assert actual_idx == expected_idx, f"Expected index {expected_idx}, got {actual_idx}"
print(f"✓ Entity {expected_idx} correctly reports index {actual_idx}")
except Exception as e:
print(f"✗ Entity {expected_idx} index() failed: {e}")
raise
# Test 2: Remove entity using index
entity_to_remove = entities[2]
remove_idx = entity_to_remove.index()
grid.entities.remove(remove_idx)
print(f"✓ Removed entity at index {remove_idx}")
# Test 3: Verify indices updated after removal
for i, entity in enumerate(entities):
if i == 2:
# This entity was removed, should raise error
try:
idx = entity.index()
print(f"✗ Removed entity still reports index {idx}")
except ValueError as e:
print(f"✓ Removed entity correctly raises error: {e}")
elif i < 2:
# These entities should keep their indices
idx = entity.index()
assert idx == i, f"Entity before removal has wrong index: {idx}"
else:
# These entities should have shifted down by 1
idx = entity.index()
assert idx == i - 1, f"Entity after removal has wrong index: {idx}"
# Test 4: Entity without grid
orphan_entity = mcrfpy.Entity((0, 0), texture, 0, None)
try:
idx = orphan_entity.index()
print(f"✗ Orphan entity should raise error but returned {idx}")
except RuntimeError as e:
print(f"✓ Orphan entity correctly raises error: {e}")
# Test 5: Use index() in practical removal pattern
# Add some new entities
for i in range(3):
entity = mcrfpy.Entity((7+i, 7+i), texture, 10+i, grid)
grid.entities.append(entity)
# Remove entities with sprite_number > 10
removed_count = 0
i = 0
while i < len(grid.entities):
entity = grid.entities[i]
if entity.sprite_number > 10:
grid.entities.remove(entity.index())
removed_count += 1
# Don't increment i, as entities shifted down
else:
i += 1
print(f"✓ Removed {removed_count} entities using index() in loop")
assert len(grid.entities) == 5, f"Expected 5 entities remaining, got {len(grid.entities)}"
print("\n✅ Issue #73 test PASSED - Entity.index() method works correctly")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_entity_index, 100)

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Simple test for Issue #73: Entity.index() method
"""
def test_entity_index(timer_name):
"""Test that Entity.index() method works correctly"""
import mcrfpy
import sys
print("Testing Entity.index() method...")
# Create test scene and grid
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Clear any existing entities
while len(grid.entities) > 0:
grid.entities.remove(0)
# Create entities
entity1 = mcrfpy.Entity((1, 1), texture, 1, grid)
entity2 = mcrfpy.Entity((2, 2), texture, 2, grid)
entity3 = mcrfpy.Entity((3, 3), texture, 3, grid)
grid.entities.append(entity1)
grid.entities.append(entity2)
grid.entities.append(entity3)
print(f"Created {len(grid.entities)} entities")
# Test index() method
idx1 = entity1.index()
idx2 = entity2.index()
idx3 = entity3.index()
print(f"Entity 1 index: {idx1}")
print(f"Entity 2 index: {idx2}")
print(f"Entity 3 index: {idx3}")
assert idx1 == 0, f"Entity 1 should be at index 0, got {idx1}"
assert idx2 == 1, f"Entity 2 should be at index 1, got {idx2}"
assert idx3 == 2, f"Entity 3 should be at index 2, got {idx3}"
print("✓ All entities report correct indices")
# Test removal using index
remove_idx = entity2.index()
grid.entities.remove(remove_idx)
print(f"✓ Removed entity at index {remove_idx}")
# Check remaining entities
assert len(grid.entities) == 2
assert entity1.index() == 0
assert entity3.index() == 1 # Should have shifted down
print("✓ Indices updated correctly after removal")
# Test entity not in grid
orphan = mcrfpy.Entity((5, 5), texture, 5, None)
try:
idx = orphan.index()
print(f"✗ Orphan entity should raise error but returned {idx}")
except RuntimeError as e:
print(f"✓ Orphan entity correctly raises error")
print("\n✅ Entity.index() test PASSED")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_entity_index, 100)

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
Test for Issue #74: Add missing Grid.grid_y property
Verifies that Grid objects expose grid_x and grid_y properties correctly.
"""
def test_grid_xy_properties(timer_name):
"""Test that Grid has grid_x and grid_y properties"""
import mcrfpy
# Test was run
print("Issue #74 test: Grid.grid_x and Grid.grid_y properties")
# Test with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(20, 15, texture, (0, 0), (800, 600))
# Test grid_x property
assert hasattr(grid, 'grid_x'), "Grid should have grid_x property"
assert grid.grid_x == 20, f"Expected grid_x=20, got {grid.grid_x}"
print(f"✓ grid.grid_x = {grid.grid_x}")
# Test grid_y property
assert hasattr(grid, 'grid_y'), "Grid should have grid_y property"
assert grid.grid_y == 15, f"Expected grid_y=15, got {grid.grid_y}"
print(f"✓ grid.grid_y = {grid.grid_y}")
# Test grid_size still works
assert hasattr(grid, 'grid_size'), "Grid should still have grid_size property"
assert grid.grid_size == (20, 15), f"Expected grid_size=(20, 15), got {grid.grid_size}"
print(f"✓ grid.grid_size = {grid.grid_size}")
# Test without texture
grid2 = mcrfpy.Grid(30, 25, None, (10, 10), (480, 400))
assert grid2.grid_x == 30, f"Expected grid_x=30, got {grid2.grid_x}"
assert grid2.grid_y == 25, f"Expected grid_y=25, got {grid2.grid_y}"
assert grid2.grid_size == (30, 25), f"Expected grid_size=(30, 25), got {grid2.grid_size}"
print("✓ Grid without texture also has correct grid_x and grid_y")
# Test using in error message context (original issue)
try:
grid.at((-1, 0)) # Should raise error
except ValueError as e:
error_msg = str(e)
assert "Grid.grid_x" in error_msg, f"Error message should reference Grid.grid_x: {error_msg}"
print(f"✓ Error message correctly references Grid.grid_x: {error_msg}")
try:
grid.at((0, -1)) # Should raise error
except ValueError as e:
error_msg = str(e)
assert "Grid.grid_y" in error_msg, f"Error message should reference Grid.grid_y: {error_msg}"
print(f"✓ Error message correctly references Grid.grid_y: {error_msg}")
print("\n✅ Issue #74 test PASSED - Grid.grid_x and Grid.grid_y properties work correctly")
# Execute the test after a short delay to ensure window is ready
import mcrfpy
mcrfpy.setTimer("test_timer", test_grid_xy_properties, 100)

View File

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Test that Issue #78 is fixed - Middle Mouse Click should NOT send 'C' keyboard event"""
import mcrfpy
from mcrfpy import automation
import sys
# Track events
keyboard_events = []
click_events = []
def keyboard_handler(key):
"""Track keyboard events"""
keyboard_events.append(key)
print(f"Keyboard event received: '{key}'")
def click_handler(x, y, button):
"""Track click events"""
click_events.append((x, y, button))
print(f"Click event received: ({x}, {y}, button={button})")
def test_middle_click_fix(runtime):
"""Test that middle click no longer sends 'C' key event"""
print(f"\n=== Testing Issue #78 Fix (runtime: {runtime}) ===")
# Simulate middle click
print("\nSimulating middle click at (200, 200)...")
automation.middleClick(200, 200)
# Also test other clicks for comparison
print("Simulating left click at (100, 100)...")
automation.click(100, 100)
print("Simulating right click at (300, 300)...")
automation.rightClick(300, 300)
# Wait a moment for events to process
mcrfpy.setTimer("check_results", check_results, 500)
def check_results(runtime):
"""Check if the bug is fixed"""
print(f"\n=== Results ===")
print(f"Keyboard events received: {len(keyboard_events)}")
print(f"Click events received: {len(click_events)}")
# Check if 'C' was incorrectly triggered
if 'C' in keyboard_events or 'c' in keyboard_events:
print("\n✗ FAIL - Issue #78 still exists: Middle click triggered 'C' keyboard event!")
print(f"Keyboard events: {keyboard_events}")
else:
print("\n✓ PASS - Issue #78 is FIXED: No spurious 'C' keyboard event from middle click!")
# Take screenshot
filename = f"issue78_fixed_{int(runtime)}.png"
automation.screenshot(filename)
print(f"\nScreenshot saved: {filename}")
# Cleanup and exit
mcrfpy.delTimer("check_results")
sys.exit(0)
# Set up test scene
print("Setting up test scene...")
mcrfpy.createScene("issue78_test")
mcrfpy.setScene("issue78_test")
ui = mcrfpy.sceneUI("issue78_test")
# Register keyboard handler
mcrfpy.keypressScene(keyboard_handler)
# Create a clickable frame
frame = mcrfpy.Frame(50, 50, 400, 400,
fill_color=mcrfpy.Color(100, 150, 200),
outline_color=mcrfpy.Color(255, 255, 255),
outline=3.0)
frame.click = click_handler
ui.append(frame)
# Add label
caption = mcrfpy.Caption(mcrfpy.Vector(100, 100),
text="Issue #78 Test - Middle Click",
fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 24
ui.append(caption)
# Schedule test
print("Scheduling test to run after render loop starts...")
mcrfpy.setTimer("test", test_middle_click_fix, 1000)

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
Test for Sprite texture setter - fixing "error return without exception set"
"""
def test_sprite_texture_setter(timer_name):
"""Test that Sprite texture setter works correctly"""
import mcrfpy
import sys
print("Testing Sprite texture setter...")
# Create test scene
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create textures
texture1 = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
texture2 = mcrfpy.Texture("assets/kenney_lava.png", 16, 16)
# Create sprite with first texture
sprite = mcrfpy.Sprite(100, 100, texture1, 5)
ui.append(sprite)
# Test getting texture
try:
current_texture = sprite.texture
print(f"✓ Got texture: {current_texture}")
except Exception as e:
print(f"✗ Failed to get texture: {e}")
raise
# Test setting new texture
try:
sprite.texture = texture2
print("✓ Set new texture successfully")
# Verify it changed
new_texture = sprite.texture
if new_texture != texture2:
print(f"✗ Texture didn't change properly")
else:
print("✓ Texture changed correctly")
except Exception as e:
print(f"✗ Failed to set texture: {e}")
raise
# Test invalid texture type
try:
sprite.texture = "invalid"
print("✗ Should have raised TypeError for invalid texture")
except TypeError as e:
print(f"✓ Correctly rejected invalid texture: {e}")
except Exception as e:
print(f"✗ Wrong exception type: {e}")
raise
# Test None texture
try:
sprite.texture = None
print("✗ Should have raised TypeError for None texture")
except TypeError as e:
print(f"✓ Correctly rejected None texture: {e}")
# Test that sprite still renders correctly
print("✓ Sprite still renders with new texture")
print("\n✅ Sprite texture setter test PASSED")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_sprite_texture_setter, 100)

17
.gitignore vendored
View File

@ -10,3 +10,20 @@ build
lib
obj
.cache/
7DRL2025 Release/
CMakeFiles/
Makefile
*.md
*.zip
__lib/
_oldscripts/
assets/
cellular_automata_fire/
*.txt
deps/
fetch_issues_txt.py
forest_fire_CA.py
mcrogueface.github.io
scripts/
test_*

283
CLAUDE.md Normal file
View File

@ -0,0 +1,283 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
```bash
# Build the project (compiles to ./build directory)
make
# Or use the build script directly
./build.sh
# Run the game
make run
# Clean build artifacts
make clean
# The executable and all assets are in ./build/
cd build
./mcrogueface
```
## Project Architecture
McRogueFace is a C++ game engine with Python scripting support, designed for creating roguelike games. The architecture consists of:
### Core Engine (C++)
- **Entry Point**: `src/main.cpp` initializes the game engine
- **Scene System**: `Scene.h/cpp` manages game states
- **Entity System**: `UIEntity.h/cpp` provides game objects
- **Python Integration**: `McRFPy_API.h/cpp` exposes engine functionality to Python
- **UI Components**: `UIFrame`, `UICaption`, `UISprite`, `UIGrid` for rendering
### Game Logic (Python)
- **Main Script**: `src/scripts/game.py` contains game initialization and scene setup
- **Entity System**: `src/scripts/cos_entities.py` implements game entities (Player, Enemy, Boulder, etc.)
- **Level Generation**: `src/scripts/cos_level.py` uses BSP for procedural dungeon generation
- **Tile System**: `src/scripts/cos_tiles.py` implements Wave Function Collapse for tile placement
### Key Python API (`mcrfpy` module)
The C++ engine exposes these primary functions to Python:
- Scene Management: `createScene()`, `setScene()`, `sceneUI()`
- Entity Creation: `Entity()` with position and sprite properties
- Grid Management: `Grid()` for tilemap rendering
- Input Handling: `keypressScene()` for keyboard events
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
- Timers: `setTimer()`, `delTimer()` for event scheduling
## Development Workflow
### Running the Game
After building, the executable expects:
- `assets/` directory with sprites, fonts, and audio
- `scripts/` directory with Python game files
- Python 3.12 shared libraries in `./lib/`
### Modifying Game Logic
- Game scripts are in `src/scripts/`
- Main game entry is `game.py`
- Entity behavior in `cos_entities.py`
- Level generation in `cos_level.py`
### Adding New Features
1. C++ API additions go in `src/McRFPy_API.cpp`
2. Expose to Python using the existing binding pattern
3. Update Python scripts to use new functionality
## Testing Game Changes
Currently no automated test suite. Manual testing workflow:
1. Build with `make`
2. Run `make run` or `cd build && ./mcrogueface`
3. Test specific features through gameplay
4. Check console output for Python errors
### Quick Testing Commands
```bash
# Test basic functionality
make test
# Run in Python interactive mode
make python
# Test headless mode
cd build
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
```
## Common Development Tasks
### Compiling McRogueFace
```bash
# Standard build (to ./build directory)
make
# Full rebuild
make clean && make
# Manual CMake build
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
# The library path issue: if linking fails, check that libraries are in __lib/
# CMakeLists.txt expects: link_directories(${CMAKE_SOURCE_DIR}/__lib)
```
### Running and Capturing Output
```bash
# Run with timeout and capture output
cd build
timeout 5 ./mcrogueface 2>&1 | tee output.log
# Run in background and kill after delay
./mcrogueface > output.txt 2>&1 & PID=$!; sleep 3; kill $PID 2>/dev/null
# Just capture first N lines (useful for crashes)
./mcrogueface 2>&1 | head -50
```
### Debugging with GDB
```bash
# Interactive debugging
gdb ./mcrogueface
(gdb) run
(gdb) bt # backtrace after crash
# Batch mode debugging (non-interactive)
gdb -batch -ex run -ex where -ex quit ./mcrogueface 2>&1
# Get just the backtrace after a crash
gdb -batch -ex "run" -ex "bt" ./mcrogueface 2>&1 | head -50
# Debug with specific commands
echo -e "run\nbt 5\nquit\ny" | gdb ./mcrogueface 2>&1
```
### Testing Different Python Scripts
```bash
# The game automatically runs build/scripts/game.py on startup
# To test different behavior:
# Option 1: Replace game.py temporarily
cd build
cp scripts/my_test_script.py scripts/game.py
./mcrogueface
# Option 2: Backup original and test
mv scripts/game.py scripts/game.py.bak
cp my_test.py scripts/game.py
./mcrogueface
mv scripts/game.py.bak scripts/game.py
# Option 3: For quick tests, create minimal game.py
echo 'import mcrfpy; print("Test"); mcrfpy.createScene("test")' > scripts/game.py
```
### Understanding Key Macros and Patterns
#### RET_PY_INSTANCE Macro (UIDrawable.h)
This macro handles converting C++ UI objects to their Python equivalents:
```cpp
RET_PY_INSTANCE(target);
// Expands to a switch on target->derived_type() that:
// 1. Allocates the correct Python object type (Frame, Caption, Sprite, Grid)
// 2. Sets the shared_ptr data member
// 3. Returns the PyObject*
```
#### Collection Patterns
- `UICollection` wraps `std::vector<std::shared_ptr<UIDrawable>>`
- `UIEntityCollection` wraps `std::list<std::shared_ptr<UIEntity>>`
- Different containers require different iteration code (vector vs list)
#### Python Object Creation Patterns
```cpp
// Pattern 1: Using tp_alloc (most common)
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
o->data = std::make_shared<UIFrame>();
// Pattern 2: Getting type from module
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
// Pattern 3: Direct shared_ptr assignment
iterObj->data = self->data; // Shares the C++ object
```
### Working Directory Structure
```
build/
├── mcrogueface # The executable
├── scripts/
│ └── game.py # Auto-loaded Python script
├── assets/ # Copied from source during build
└── lib/ # Python libraries (copied from __lib/)
```
### Quick Iteration Tips
- Keep a test script ready for quick experiments
- Use `timeout` to auto-kill hanging processes
- The game expects a window manager; use Xvfb for headless testing
- Python errors go to stderr, game output to stdout
- Segfaults usually mean Python type initialization issues
## Important Notes
- The project uses SFML for graphics/audio and libtcod for roguelike utilities
- Python scripts are loaded at runtime from the `scripts/` directory
- Asset loading expects specific paths relative to the executable
- The game was created for 7DRL 2025 as "Crypt of Sokoban"
- Iterator implementations require careful handling of C++/Python boundaries
## Testing Guidelines
### Test-Driven Development
- **Always write tests first**: Create automation tests in `./tests/` for all bugs and new features
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix is applied
- **Close the loop**: Reproduce issue → change code → recompile → verify behavior change
### Two Types of Tests
#### 1. Direct Execution Tests (No Game Loop)
For tests that only need class initialization or direct code execution:
```python
# These tests can treat McRogueFace like a Python interpreter
import mcrfpy
# Test code here
result = mcrfpy.some_function()
assert result == expected_value
print("PASS" if condition else "FAIL")
```
#### 2. Game Loop Tests (Timer-Based)
For tests requiring rendering, game state, or elapsed time:
```python
import mcrfpy
from mcrfpy import automation
import sys
def run_test(runtime):
"""Timer callback - runs after game loop starts"""
# Now rendering is active, screenshots will work
automation.screenshot("test_result.png")
# Run your tests here
automation.click(100, 100)
# Always exit at the end
print("PASS" if success else "FAIL")
sys.exit(0)
# Set up the test scene
mcrfpy.createScene("test")
# ... add UI elements ...
# Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100) # 0.1 seconds
```
### Key Testing Principles
- **Timer callbacks are essential**: Screenshots and UI interactions only work after the render loop starts
- **Use automation API**: Always create and examine screenshots when visual feedback is required
- **Exit properly**: Call `sys.exit()` at the end of timer-based tests to prevent hanging
- **Headless mode**: Use `--exec` flag for automated testing: `./mcrogueface --headless --exec tests/my_test.py`
### Example Test Pattern
```bash
# Run a test that requires game loop
./build/mcrogueface --headless --exec tests/issue_78_middle_click_test.py
# The test will:
# 1. Set up the scene during script execution
# 2. Register a timer callback
# 3. Game loop starts
# 4. Timer fires after 100ms
# 5. Test runs with full rendering available
# 6. Test takes screenshots and validates behavior
# 7. Test calls sys.exit() to terminate
```

54
GNUmakefile Normal file
View File

@ -0,0 +1,54 @@
# Convenience Makefile wrapper for McRogueFace
# This delegates to CMake build in the build directory
.PHONY: all build clean run test dist help
# Default target
all: build
# Build the project
build:
@./build.sh
# Clean build artifacts
clean:
@./clean.sh
# Run the game
run: build
@cd build && ./mcrogueface
# Run in Python mode
python: build
@cd build && ./mcrogueface -i
# Test basic functionality
test: build
@echo "Testing McRogueFace..."
@cd build && ./mcrogueface -V
@cd build && ./mcrogueface -c "print('Test passed')"
@cd build && ./mcrogueface --headless -c "import mcrfpy; print('mcrfpy imported successfully')"
# Create distribution archive
dist: build
@echo "Creating distribution archive..."
@cd build && zip -r ../McRogueFace-$$(date +%Y%m%d).zip . -x "*.o" "CMakeFiles/*" "Makefile" "*.cmake"
@echo "Distribution archive created: McRogueFace-$$(date +%Y%m%d).zip"
# Show help
help:
@echo "McRogueFace Build System"
@echo "======================="
@echo ""
@echo "Available targets:"
@echo " make - Build the project (default)"
@echo " make build - Build the project"
@echo " make clean - Remove all build artifacts"
@echo " make run - Build and run the game"
@echo " make python - Build and run in Python interactive mode"
@echo " make test - Run basic tests"
@echo " make dist - Create distribution archive"
@echo " make help - Show this help message"
@echo ""
@echo "Build output goes to: ./build/"
@echo "Distribution archives are created in project root"

View File

@ -1,30 +1,85 @@
# McRogueFace - 2D Game Engine
# McRogueFace
An experimental prototype game engine built for my own use in 7DRL 2023.
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
*Blame my wife for the name*
**Latest Release**: Successfully completed 7DRL 2025 with *"Crypt of Sokoban"* - a unique roguelike that blends Sokoban puzzle mechanics with dungeon crawling!
## Tenets:
## Features
* C++ first, Python close behind.
* Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
* Graphics, particles and shaders provided by SFML.
* Pathfinding, noise generation, and other Roguelike goodness provided by TCOD.
- **Python-First Design**: Write your game logic in Python while leveraging C++ performance
- **Rich UI System**: Sprites, Grids, Frames, and Captions with full animation support
- **Entity-Component Architecture**: Flexible game object system with Python integration
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
- **Automation API**: PyAutoGUI-compatible testing and demo recording
- **Interactive Development**: Python REPL integration for live game debugging
## Why?
## Quick Start
I did the r/RoguelikeDev TCOD tutorial in Python. I loved it, but I did not want to be limited to ASCII. I want to be able to draw pixels on top of my tiles (like lines or circles) and eventually incorporate even more polish.
```bash
# Clone and build
git clone https://github.com/jmcb/McRogueFace.git
cd McRogueFace
make
## To-do
# Run the example game
cd build
./mcrogueface
```
* ✅ Initial Commit
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
* ✅ Windows / Visual Studio project
* ✅ Draw Sprites
* ✅ Play Sounds
* ✅ Draw UI, spawn entity from Python code
* ❌ Python AI for entities (NPCs on set paths, enemies towards player)
* ✅ Walking / Collision
* ❌ "Boards" (stairs / doors / walk off edge of screen)
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
* ❌ Mouse integration - tooltips, zoom, click to select targets, cursors
## Example: Creating a Simple Scene
```python
import mcrfpy
# Create a new scene
mcrfpy.createScene("intro")
# Add a text caption
caption = mcrfpy.Caption(50, 50, "Welcome to McRogueFace!")
caption.font = mcrfpy.default_font
caption.font_color = (255, 255, 255)
# Add to scene
mcrfpy.sceneUI("intro").append(caption)
# Switch to the scene
mcrfpy.setScene("intro")
```
## Documentation
For comprehensive documentation, tutorials, and API reference, visit:
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
## Requirements
- C++17 compiler (GCC 7+ or Clang 5+)
- CMake 3.14+
- Python 3.12+
- SFML 2.5+
- Linux or Windows (macOS untested)
## Project Structure
```
McRogueFace/
├── src/ # C++ engine source
├── scripts/ # Python game scripts
├── assets/ # Sprites, fonts, audio
├── build/ # Build output directory
└── tests/ # Automated test suite
```
## Contributing
McRogueFace is under active development. Check the [ROADMAP.md](ROADMAP.md) for current priorities and open issues.
## License
This project is licensed under the MIT License - see LICENSE file for details.
## Acknowledgments
- Developed for 7-Day Roguelike Challenge 2025
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
- Inspired by David Churchill's COMP4300 game engine lectures

362
ROADMAP.md Normal file
View File

@ -0,0 +1,362 @@
# 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: Python Interpreter Mode & Automation API
### Branch: interpreter_mode
**Status**: Actively implementing Python interpreter emulation features
#### Completed Features:
- [x] **--exec flag implementation** - Execute multiple scripts before main program
- Scripts execute in order and share Python interpreter state
- Proper sys.argv handling for main script execution
- Compatible with -i (interactive), -c (command), and -m (module) modes
- [x] **PyAutoGUI-compatible Automation API** - Full automation testing capability
- Screenshot capture: `automation.screenshot(filename)`
- Mouse control: `click()`, `moveTo()`, `dragTo()`, `scroll()`
- Keyboard input: `typewrite()`, `hotkey()`, `keyDown()`, `keyUp()`
- Event injection into SFML render loop
- **Enables**: Automated UI testing, demo recording/playback, accessibility testing
#### Architectural Decisions:
1. **Single-threaded design maintained** - All Python runs in main thread between frames
2. **Honor system for scripts** - Scripts must return control to C++ render loop
3. **Shared Python state** - All --exec scripts share the same interpreter
4. **No threading complexity** - Chose simplicity over parallelism (see THREADING_FOOTGUNS.md)
5. **Animation system in pure C++** - All interpolation happens in C++ for performance
6. **Property-based animation** - Unified interface for all UI element properties
#### Key Files Created:
- `src/McRFPy_Automation.h/cpp` - Complete automation API implementation
- `EXEC_FLAG_DOCUMENTATION.md` - Usage guide and examples
- `AUTOMATION_ARCHITECTURE_REPORT.md` - Design analysis and alternatives
- Multiple example scripts demonstrating automation patterns
#### Addresses:
- **#32** - Executable behave like `python` command (90% complete - all major Python interpreter flags implemented)
#### Test Suite Results (2025-07-03):
Created comprehensive test suite with 13 tests covering all Python-exposed methods:
**✅ Fixed Issues:**
- Fixed `--exec` Python interactive prompt bug (was entering REPL instead of game loop)
- Resolved screenshot transparency issue (must use timer callbacks for rendered content)
- Updated CLAUDE.md with testing guidelines and patterns
**❌ Critical Bugs Found:**
1. **SEGFAULT**: Grid class crashes on instantiation (blocks all Grid functionality)
2. **#78 CONFIRMED**: Middle mouse click sends 'C' keyboard event
3. **Entity property setters**: "new style getargs format" error
4. **Sprite texture setter**: Returns "error return without exception set"
5. **keypressScene()**: Segfaults on non-callable arguments
**📋 Missing Features Confirmed:**
- #73: Entity.index() method
- #27: EntityCollection.extend() method
- #41: UICollection.find(name) method
- #38: Frame 'children' constructor parameter
- #33: Sprite index validation
---
## 🚀 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*
- [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
---
## ✅ ALPHA 0.1 RELEASE ACHIEVED! (All Blockers Complete)
### ✅ All Alpha Requirements Complete!
- [x] **#69** - Collections use Python Sequence Protocol - *Completed! (2025-07-05)*
- [x] **#63** - Z-order rendering for UIDrawables - *Completed! (2025-07-05)*
- [x] **#59** - Animation system for arbitrary UIDrawable fields - *Completed! (2025-07-05)*
- [x] **#47** - New README.md for Alpha release - *Completed*
- [x] **#3** - Remove deprecated `McRFPy_API::player_input` - *Completed*
- [x] **#2** - Remove `registerPyAction` system - *Completed*
### 📋 Moved to Beta:
- [ ] **#6** - RenderTexture concept - *Moved to Beta (not needed for Alpha)*
---
## 🗂 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)
- [ ] **#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 - *Extensive Overhaul*
- [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations*
#### UI/Rendering System (12 issues)
- [ ] **#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*
- [ ] **#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*
- [ ] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix*
#### Scene/Window Management (5 issues)
- [ ] **#61** - Scene object encapsulating key callbacks - *Extensive Overhaul*
- [ ] **#34** - Window object for resolution/scaling - *Extensive Overhaul*
- [ ] **#62** - Multiple windows support - *Extensive Overhaul*
- [ ] **#49** - Window resolution & viewport controls - *Multiple Integrations*
- [ ] **#1** - Scene resize event handling - *Isolated Fix*
### 🔧 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)
- [ ] **#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*
---
## 🎯 RECOMMENDED TRIAGE SEQUENCE
### Phase 1: Foundation Stabilization (1-2 weeks)
```
✅ COMPLETE AS OF 2025-01-03:
1. ✅ Fix Grid Segfault - Grid now supports None/null textures
2. ✅ Fix #78 Middle Mouse Click bug - Event type checking added
3. ✅ Fix Entity/Sprite property setters - PyVector conversion fixed
4. ✅ Fix #77 - Error message copy/paste bug fixed
5. ✅ Fix #74 - Grid.grid_y property added
6. ✅ Fix keypressScene() validation - Now rejects non-callable
7. ✅ Fix Sprite texture setter - No longer returns error without exception
8. ✅ Fix PyVector x/y properties - Were returning None
REMAINING IN PHASE 1:
9. ✅ Fix #73 - Entity.index() method for removal
10. ✅ Fix #27 - EntityCollection.extend() method
11. ✅ Fix #33 - Sprite index validation
12. Alpha Blockers (#3, #2) - Remove deprecated methods
```
### Phase 2: Alpha Release Preparation (4-6 weeks)
```
1. Collections Sequence Protocol (#69) - Major refactor, alpha blocker
2. Z-order rendering (#63) - Essential UI improvement, alpha blocker
3. RenderTexture overhaul (#6) - Core rendering improvement, alpha blocker
4. ✅ Animation system (#59) - COMPLETE! 30+ easing functions, all UI properties
5. ✅ Documentation (#47) - README.md complete, #48 dependency docs remaining
```
### Phase 3: Engine Architecture (6-8 weeks)
```
1. Drawable base class (#71) - Clean up inheritance patterns
2. Entity/Grid associations (#30) - Proper lifecycle management
3. Window object (#34) - Scene/window architecture
4. UIDrawable visibility (#10) - Rendering optimization
```
### Phase 4: Advanced Features (8-12 weeks)
```
1. Grid strict mode (#16) - Entity knowledge/visibility system
2. SFML/TCOD integration (#14, #35) - Expose native libraries
3. Scene object refactor (#61) - Better input handling
4. Name-based finding (#39, #40, #41) - UI element management
5. Demo projects (#54, #55, #36) - Showcase capabilities
```
### Ongoing/Low Priority
```
- PyPI distribution (#70) - Community access
- Multiple windows (#62) - Advanced use cases
- Grid stitching (#67) - Infinite world support
- Accessibility (#45) - Important but not blocking
- Subinterpreter tests (#46) - Performance research
```
---
## 📊 DIFFICULTY ASSESSMENT SUMMARY
**Isolated Fixes (24 issues)**: Single file/function changes
- Bugfixes: #77, #74, #37, #78
- Simple features: #73, #52, #50, #33, #17, #38, #42, #27, #28, #26, #12, #1
- Cleanup: #3, #2, #21, #47, #48
**Multiple Integrations (28 issues)**: Cross-system changes
- UI/Rendering: #63, #8, #9, #19, #39, #40, #41
- Grid/Entity: #15, #20, #76, #46, #49, #75
- Features: #54, #55, #53, #45, #7
**Extensive Overhauls (26 issues)**: Major architectural changes
- Core Systems: #69, #59, #6, #10, #30, #16, #67, #61, #34, #62
- Integration: #71, #70, #32, #35, #14
- Advanced: #36, #65
---
## 🎮 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
### Success Metrics for Alpha 0.1
- [ ] All Alpha Blocker issues resolved (5 of 7 complete: #69, #59, #47, #3, #2)
- [ ] Grid point iteration complete and tested
- [ ] Clean build on Windows and Linux
- [ ] Documentation sufficient for external developers
- [ ] At least one compelling demo (Wumpus or Jupyter integration)
---
## 📚 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*
*Total Open Issues: 62* (from original 78)
*Alpha Status: 🎉 COMPLETE! All blockers resolved!*
*Achievement Unlocked: Alpha 0.1 Release Ready*
*Next Phase: Beta features including RenderTexture (#6), advanced UI patterns, and platform polish*

127
automation_example.py Normal file
View File

@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""
McRogueFace Automation API Example
This demonstrates how to use the automation API for testing game UIs.
The API is PyAutoGUI-compatible for easy migration of existing tests.
"""
from mcrfpy import automation
import mcrfpy
import time
def automation_demo():
"""Demonstrate all automation API features"""
print("=== McRogueFace Automation API Demo ===\n")
# 1. Screen Information
print("1. Screen Information:")
screen_size = automation.size()
print(f" Screen size: {screen_size[0]}x{screen_size[1]}")
mouse_pos = automation.position()
print(f" Current mouse position: {mouse_pos}")
on_screen = automation.onScreen(100, 100)
print(f" Is (100, 100) on screen? {on_screen}")
print()
# 2. Mouse Movement
print("2. Mouse Movement:")
print(" Moving to center of screen...")
center_x, center_y = screen_size[0]//2, screen_size[1]//2
automation.moveTo(center_x, center_y, duration=0.5)
print(" Moving relative by (100, 100)...")
automation.moveRel(100, 100, duration=0.5)
print()
# 3. Mouse Clicks
print("3. Mouse Clicks:")
print(" Single click...")
automation.click()
time.sleep(0.2)
print(" Double click...")
automation.doubleClick()
time.sleep(0.2)
print(" Right click...")
automation.rightClick()
time.sleep(0.2)
print(" Triple click...")
automation.tripleClick()
print()
# 4. Keyboard Input
print("4. Keyboard Input:")
print(" Typing message...")
automation.typewrite("Hello from McRogueFace automation!", interval=0.05)
print(" Pressing Enter...")
automation.keyDown("enter")
automation.keyUp("enter")
print(" Hotkey Ctrl+A (select all)...")
automation.hotkey("ctrl", "a")
print()
# 5. Drag Operations
print("5. Drag Operations:")
print(" Dragging from current position to (500, 500)...")
automation.dragTo(500, 500, duration=1.0)
print(" Dragging relative by (-100, -100)...")
automation.dragRel(-100, -100, duration=0.5)
print()
# 6. Scroll Operations
print("6. Scroll Operations:")
print(" Scrolling up 5 clicks...")
automation.scroll(5)
time.sleep(0.5)
print(" Scrolling down 5 clicks...")
automation.scroll(-5)
print()
# 7. Screenshots
print("7. Screenshots:")
print(" Taking screenshot...")
success = automation.screenshot("automation_demo_screenshot.png")
print(f" Screenshot saved: {success}")
print()
print("=== Demo Complete ===")
def create_test_ui():
"""Create a simple UI for testing automation"""
print("Creating test UI...")
# Create a test scene
mcrfpy.createScene("automation_test")
mcrfpy.setScene("automation_test")
# Add some UI elements
ui = mcrfpy.sceneUI("automation_test")
# Add a frame
frame = mcrfpy.Frame(50, 50, 300, 200)
ui.append(frame)
# Add a caption
caption = mcrfpy.Caption(60, 60, "Automation Test UI")
ui.append(caption)
print("Test UI created!")
if __name__ == "__main__":
# Create test UI first
create_test_ui()
# Run automation demo
automation_demo()
print("\nYou can now use the automation API to test your game!")

336
automation_exec_examples.py Normal file
View File

@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""
Examples of automation patterns using the proposed --exec flag
Usage:
./mcrogueface game.py --exec automation_basic.py
./mcrogueface game.py --exec automation_stress.py --exec monitor.py
"""
# ===== automation_basic.py =====
# Basic automation that runs alongside the game
import mcrfpy
from mcrfpy import automation
import time
class GameAutomation:
"""Automated testing that runs periodically"""
def __init__(self):
self.test_count = 0
self.test_results = []
def run_test_suite(self):
"""Called by timer - runs one test per invocation"""
test_name = f"test_{self.test_count}"
try:
if self.test_count == 0:
# Test main menu
self.test_main_menu()
elif self.test_count == 1:
# Test inventory
self.test_inventory()
elif self.test_count == 2:
# Test combat
self.test_combat()
else:
# All tests complete
self.report_results()
return
self.test_results.append((test_name, "PASS"))
except Exception as e:
self.test_results.append((test_name, f"FAIL: {e}"))
self.test_count += 1
def test_main_menu(self):
"""Test main menu interactions"""
automation.screenshot("test_main_menu_before.png")
automation.click(400, 300) # New Game button
time.sleep(0.5)
automation.screenshot("test_main_menu_after.png")
def test_inventory(self):
"""Test inventory system"""
automation.hotkey("i") # Open inventory
time.sleep(0.5)
automation.screenshot("test_inventory_open.png")
# Drag item
automation.moveTo(100, 200)
automation.dragTo(200, 200, duration=0.5)
automation.hotkey("i") # Close inventory
def test_combat(self):
"""Test combat system"""
# Move character
automation.keyDown("w")
time.sleep(0.5)
automation.keyUp("w")
# Attack
automation.click(500, 400)
automation.screenshot("test_combat.png")
def report_results(self):
"""Generate test report"""
print("\n=== Automation Test Results ===")
for test, result in self.test_results:
print(f"{test}: {result}")
print(f"Total: {len(self.test_results)} tests")
# Stop the timer
mcrfpy.delTimer("automation_suite")
# Create automation instance and register timer
auto = GameAutomation()
mcrfpy.setTimer("automation_suite", auto.run_test_suite, 2000) # Run every 2 seconds
print("Game automation started - tests will run every 2 seconds")
# ===== automation_stress.py =====
# Stress testing with random inputs
import mcrfpy
from mcrfpy import automation
import random
class StressTester:
"""Randomly interact with the game to find edge cases"""
def __init__(self):
self.action_count = 0
self.errors = []
def random_action(self):
"""Perform a random UI action"""
try:
action = random.choice([
self.random_click,
self.random_key,
self.random_drag,
self.random_hotkey
])
action()
self.action_count += 1
# Periodic screenshot
if self.action_count % 50 == 0:
automation.screenshot(f"stress_test_{self.action_count}.png")
print(f"Stress test: {self.action_count} actions performed")
except Exception as e:
self.errors.append((self.action_count, str(e)))
def random_click(self):
x = random.randint(0, 1024)
y = random.randint(0, 768)
button = random.choice(["left", "right"])
automation.click(x, y, button=button)
def random_key(self):
key = random.choice([
"a", "b", "c", "d", "w", "s",
"space", "enter", "escape",
"1", "2", "3", "4", "5"
])
automation.keyDown(key)
automation.keyUp(key)
def random_drag(self):
x1 = random.randint(0, 1024)
y1 = random.randint(0, 768)
x2 = random.randint(0, 1024)
y2 = random.randint(0, 768)
automation.moveTo(x1, y1)
automation.dragTo(x2, y2, duration=0.2)
def random_hotkey(self):
modifier = random.choice(["ctrl", "alt", "shift"])
key = random.choice(["a", "s", "d", "f"])
automation.hotkey(modifier, key)
# Create stress tester and run frequently
stress = StressTester()
mcrfpy.setTimer("stress_test", stress.random_action, 100) # Every 100ms
print("Stress testing started - random actions every 100ms")
# ===== monitor.py =====
# Performance and state monitoring
import mcrfpy
from mcrfpy import automation
import json
import time
class PerformanceMonitor:
"""Monitor game performance and state"""
def __init__(self):
self.samples = []
self.start_time = time.time()
def collect_sample(self):
"""Collect performance data"""
sample = {
"timestamp": time.time() - self.start_time,
"fps": mcrfpy.getFPS() if hasattr(mcrfpy, 'getFPS') else 60,
"scene": mcrfpy.currentScene(),
"memory": self.estimate_memory_usage()
}
self.samples.append(sample)
# Log every 10 samples
if len(self.samples) % 10 == 0:
avg_fps = sum(s["fps"] for s in self.samples[-10:]) / 10
print(f"Average FPS (last 10 samples): {avg_fps:.1f}")
# Save data every 100 samples
if len(self.samples) % 100 == 0:
self.save_report()
def estimate_memory_usage(self):
"""Estimate memory usage based on scene complexity"""
# This is a placeholder - real implementation would use psutil
ui_count = len(mcrfpy.sceneUI(mcrfpy.currentScene()))
return ui_count * 1000 # Rough estimate in KB
def save_report(self):
"""Save performance report"""
with open("performance_report.json", "w") as f:
json.dump({
"samples": self.samples,
"summary": {
"total_samples": len(self.samples),
"duration": time.time() - self.start_time,
"avg_fps": sum(s["fps"] for s in self.samples) / len(self.samples)
}
}, f, indent=2)
print(f"Performance report saved ({len(self.samples)} samples)")
# Create monitor and start collecting
monitor = PerformanceMonitor()
mcrfpy.setTimer("performance_monitor", monitor.collect_sample, 1000) # Every second
print("Performance monitoring started - sampling every second")
# ===== automation_replay.py =====
# Record and replay user actions
import mcrfpy
from mcrfpy import automation
import json
import time
class ActionRecorder:
"""Record user actions for replay"""
def __init__(self):
self.recording = False
self.actions = []
self.start_time = None
def start_recording(self):
"""Start recording user actions"""
self.recording = True
self.actions = []
self.start_time = time.time()
print("Recording started - perform actions to record")
# Register callbacks for all input types
mcrfpy.registerPyAction("record_click", self.record_click)
mcrfpy.registerPyAction("record_key", self.record_key)
# Map all mouse buttons
for button in range(3):
mcrfpy.registerInputAction(8192 + button, "record_click")
# Map common keys
for key in range(256):
mcrfpy.registerInputAction(4096 + key, "record_key")
def record_click(self, action_type):
"""Record mouse click"""
if not self.recording or action_type != "start":
return
pos = automation.position()
self.actions.append({
"type": "click",
"time": time.time() - self.start_time,
"x": pos[0],
"y": pos[1]
})
def record_key(self, action_type):
"""Record key press"""
if not self.recording or action_type != "start":
return
# This is simplified - real implementation would decode the key
self.actions.append({
"type": "key",
"time": time.time() - self.start_time,
"key": "unknown"
})
def stop_recording(self):
"""Stop recording and save"""
self.recording = False
with open("recorded_actions.json", "w") as f:
json.dump(self.actions, f, indent=2)
print(f"Recording stopped - {len(self.actions)} actions saved")
def replay_actions(self):
"""Replay recorded actions"""
print("Replaying recorded actions...")
with open("recorded_actions.json", "r") as f:
actions = json.load(f)
start_time = time.time()
action_index = 0
def replay_next():
nonlocal action_index
if action_index >= len(actions):
print("Replay complete")
mcrfpy.delTimer("replay")
return
action = actions[action_index]
current_time = time.time() - start_time
# Wait until it's time for this action
if current_time >= action["time"]:
if action["type"] == "click":
automation.click(action["x"], action["y"])
elif action["type"] == "key":
automation.keyDown(action["key"])
automation.keyUp(action["key"])
action_index += 1
mcrfpy.setTimer("replay", replay_next, 10) # Check every 10ms
# Example usage - would be controlled by UI
recorder = ActionRecorder()
# To start recording:
# recorder.start_recording()
# To stop and save:
# recorder.stop_recording()
# To replay:
# recorder.replay_actions()
print("Action recorder ready - call recorder.start_recording() to begin")

54
build.sh Executable file
View File

@ -0,0 +1,54 @@
#!/bin/bash
# Build script for McRogueFace - compiles everything into ./build directory
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}McRogueFace Build Script${NC}"
echo "========================="
# Create build directory if it doesn't exist
if [ ! -d "build" ]; then
echo -e "${YELLOW}Creating build directory...${NC}"
mkdir build
fi
# Change to build directory
cd build
# Run CMake to generate build files
echo -e "${YELLOW}Running CMake...${NC}"
cmake .. -DCMAKE_BUILD_TYPE=Release
# Check if CMake succeeded
if [ $? -ne 0 ]; then
echo -e "${RED}CMake configuration failed!${NC}"
exit 1
fi
# Run make with parallel jobs
echo -e "${YELLOW}Building with make...${NC}"
make -j$(nproc)
# Check if make succeeded
if [ $? -ne 0 ]; then
echo -e "${RED}Build failed!${NC}"
exit 1
fi
echo -e "${GREEN}Build completed successfully!${NC}"
echo ""
echo "The build directory contains:"
ls -la
echo ""
echo -e "${GREEN}To run McRogueFace:${NC}"
echo " cd build"
echo " ./mcrogueface"
echo ""
echo -e "${GREEN}To create a distribution archive:${NC}"
echo " cd build"
echo " zip -r ../McRogueFace-$(date +%Y%m%d).zip ."

33
clean.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
# Clean script for McRogueFace - removes build artifacts
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}Cleaning McRogueFace build artifacts...${NC}"
# Remove build directory
if [ -d "build" ]; then
echo "Removing build directory..."
rm -rf build
fi
# Remove CMake artifacts from project root
echo "Removing CMake artifacts from project root..."
rm -f CMakeCache.txt
rm -f cmake_install.cmake
rm -f Makefile
rm -rf CMakeFiles
# Remove compiled executable from project root
rm -f mcrogueface
# Remove any test artifacts
rm -f test_script.py
rm -rf test_venv
rm -f python3 # symlink
echo -e "${GREEN}Clean complete!${NC}"

112
compile_commands.json Normal file
View File

@ -0,0 +1,112 @@
[
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/GameEngine.cpp.o -c /home/john/Development/McRogueFace/src/GameEngine.cpp",
"file": "/home/john/Development/McRogueFace/src/GameEngine.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/IndexTexture.cpp.o -c /home/john/Development/McRogueFace/src/IndexTexture.cpp",
"file": "/home/john/Development/McRogueFace/src/IndexTexture.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/McRFPy_API.cpp.o -c /home/john/Development/McRogueFace/src/McRFPy_API.cpp",
"file": "/home/john/Development/McRogueFace/src/McRFPy_API.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyCallable.cpp.o -c /home/john/Development/McRogueFace/src/PyCallable.cpp",
"file": "/home/john/Development/McRogueFace/src/PyCallable.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyColor.cpp.o -c /home/john/Development/McRogueFace/src/PyColor.cpp",
"file": "/home/john/Development/McRogueFace/src/PyColor.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyFont.cpp.o -c /home/john/Development/McRogueFace/src/PyFont.cpp",
"file": "/home/john/Development/McRogueFace/src/PyFont.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyScene.cpp.o -c /home/john/Development/McRogueFace/src/PyScene.cpp",
"file": "/home/john/Development/McRogueFace/src/PyScene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyTexture.cpp.o -c /home/john/Development/McRogueFace/src/PyTexture.cpp",
"file": "/home/john/Development/McRogueFace/src/PyTexture.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyVector.cpp.o -c /home/john/Development/McRogueFace/src/PyVector.cpp",
"file": "/home/john/Development/McRogueFace/src/PyVector.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Resources.cpp.o -c /home/john/Development/McRogueFace/src/Resources.cpp",
"file": "/home/john/Development/McRogueFace/src/Resources.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Scene.cpp.o -c /home/john/Development/McRogueFace/src/Scene.cpp",
"file": "/home/john/Development/McRogueFace/src/Scene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Timer.cpp.o -c /home/john/Development/McRogueFace/src/Timer.cpp",
"file": "/home/john/Development/McRogueFace/src/Timer.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICaption.cpp.o -c /home/john/Development/McRogueFace/src/UICaption.cpp",
"file": "/home/john/Development/McRogueFace/src/UICaption.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICollection.cpp.o -c /home/john/Development/McRogueFace/src/UICollection.cpp",
"file": "/home/john/Development/McRogueFace/src/UICollection.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIDrawable.cpp.o -c /home/john/Development/McRogueFace/src/UIDrawable.cpp",
"file": "/home/john/Development/McRogueFace/src/UIDrawable.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIEntity.cpp.o -c /home/john/Development/McRogueFace/src/UIEntity.cpp",
"file": "/home/john/Development/McRogueFace/src/UIEntity.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIFrame.cpp.o -c /home/john/Development/McRogueFace/src/UIFrame.cpp",
"file": "/home/john/Development/McRogueFace/src/UIFrame.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGrid.cpp.o -c /home/john/Development/McRogueFace/src/UIGrid.cpp",
"file": "/home/john/Development/McRogueFace/src/UIGrid.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGridPoint.cpp.o -c /home/john/Development/McRogueFace/src/UIGridPoint.cpp",
"file": "/home/john/Development/McRogueFace/src/UIGridPoint.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UISprite.cpp.o -c /home/john/Development/McRogueFace/src/UISprite.cpp",
"file": "/home/john/Development/McRogueFace/src/UISprite.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UITestScene.cpp.o -c /home/john/Development/McRogueFace/src/UITestScene.cpp",
"file": "/home/john/Development/McRogueFace/src/UITestScene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/main.cpp.o -c /home/john/Development/McRogueFace/src/main.cpp",
"file": "/home/john/Development/McRogueFace/src/main.cpp"
}
]

BIN
debug_immediate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
debug_multi_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
debug_multi_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
debug_multi_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

63
example_automation.py Normal file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
Example automation script using --exec flag
Usage: ./mcrogueface game.py --exec example_automation.py
"""
import mcrfpy
from mcrfpy import automation
class GameAutomation:
def __init__(self):
self.frame_count = 0
self.test_phase = 0
print("Automation: Initialized")
def periodic_test(self):
"""Called every second to perform automation tasks"""
self.frame_count = mcrfpy.getFrame()
print(f"Automation: Running test at frame {self.frame_count}")
# Take periodic screenshots
if self.test_phase % 5 == 0:
filename = f"automation_screenshot_{self.test_phase}.png"
automation.screenshot(filename)
print(f"Automation: Saved {filename}")
# Simulate user input based on current scene
scene = mcrfpy.currentScene()
print(f"Automation: Current scene is '{scene}'")
if scene == "main_menu" and self.test_phase < 5:
# Click start button
automation.click(512, 400)
print("Automation: Clicked start button")
elif scene == "game":
# Perform game actions
if self.test_phase % 3 == 0:
automation.hotkey("i") # Toggle inventory
print("Automation: Toggled inventory")
else:
# Random movement
import random
key = random.choice(["w", "a", "s", "d"])
automation.keyDown(key)
automation.keyUp(key)
print(f"Automation: Pressed '{key}' key")
self.test_phase += 1
# Stop after 20 tests
if self.test_phase >= 20:
print("Automation: Test suite complete")
mcrfpy.delTimer("automation_test")
# Could also call mcrfpy.quit() to exit the game
# Create automation instance
automation_instance = GameAutomation()
# Register periodic timer
mcrfpy.setTimer("automation_test", automation_instance.periodic_test, 1000)
print("Automation: Script loaded - tests will run every second")
print("Automation: The game and automation share the same Python environment")

53
example_config.py Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""
Example configuration script that sets up shared state for other scripts
Usage: ./mcrogueface --exec example_config.py --exec example_automation.py game.py
"""
import mcrfpy
# Create a shared configuration namespace
class AutomationConfig:
# Test settings
test_enabled = True
screenshot_interval = 5 # Take screenshot every N tests
max_test_count = 50
test_delay_ms = 1000
# Monitoring settings
monitor_enabled = True
monitor_interval_ms = 500
report_delay_seconds = 30
# Game-specific settings
start_button_pos = (512, 400)
inventory_key = "i"
movement_keys = ["w", "a", "s", "d"]
# Shared state
test_results = []
performance_data = []
@classmethod
def log_result(cls, test_name, success, details=""):
"""Log a test result"""
cls.test_results.append({
"test": test_name,
"success": success,
"details": details,
"frame": mcrfpy.getFrame()
})
@classmethod
def get_summary(cls):
"""Get test summary"""
total = len(cls.test_results)
passed = sum(1 for r in cls.test_results if r["success"])
return f"Tests: {passed}/{total} passed"
# Attach config to mcrfpy module so other scripts can access it
mcrfpy.automation_config = AutomationConfig
print("Config: Automation configuration loaded")
print(f"Config: Test delay = {AutomationConfig.test_delay_ms}ms")
print(f"Config: Max tests = {AutomationConfig.max_test_count}")
print("Config: Other scripts can access config via mcrfpy.automation_config")

69
example_monitoring.py Normal file
View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
Example monitoring script that works alongside automation
Usage: ./mcrogueface game.py --exec example_automation.py --exec example_monitoring.py
"""
import mcrfpy
import time
class PerformanceMonitor:
def __init__(self):
self.start_time = time.time()
self.frame_samples = []
self.scene_changes = []
self.last_scene = None
print("Monitor: Performance monitoring initialized")
def collect_metrics(self):
"""Collect performance and state metrics"""
current_frame = mcrfpy.getFrame()
current_time = time.time() - self.start_time
current_scene = mcrfpy.currentScene()
# Track frame rate
if len(self.frame_samples) > 0:
last_frame, last_time = self.frame_samples[-1]
fps = (current_frame - last_frame) / (current_time - last_time)
print(f"Monitor: FPS = {fps:.1f}")
self.frame_samples.append((current_frame, current_time))
# Track scene changes
if current_scene != self.last_scene:
print(f"Monitor: Scene changed from '{self.last_scene}' to '{current_scene}'")
self.scene_changes.append((current_time, self.last_scene, current_scene))
self.last_scene = current_scene
# Keep only last 100 samples
if len(self.frame_samples) > 100:
self.frame_samples = self.frame_samples[-100:]
def generate_report(self):
"""Generate a summary report"""
if len(self.frame_samples) < 2:
return
total_frames = self.frame_samples[-1][0] - self.frame_samples[0][0]
total_time = self.frame_samples[-1][1] - self.frame_samples[0][1]
avg_fps = total_frames / total_time
print("\n=== Performance Report ===")
print(f"Monitor: Total time: {total_time:.1f} seconds")
print(f"Monitor: Total frames: {total_frames}")
print(f"Monitor: Average FPS: {avg_fps:.1f}")
print(f"Monitor: Scene changes: {len(self.scene_changes)}")
# Stop monitoring
mcrfpy.delTimer("performance_monitor")
# Create monitor instance
monitor = PerformanceMonitor()
# Register monitoring timer (runs every 500ms)
mcrfpy.setTimer("performance_monitor", monitor.collect_metrics, 500)
# Register report generation (runs after 30 seconds)
mcrfpy.setTimer("performance_report", monitor.generate_report, 30000)
print("Monitor: Script loaded - collecting metrics every 500ms")
print("Monitor: Will generate report after 30 seconds")

View File

@ -0,0 +1,189 @@
// Example implementation of --exec flag for McRogueFace
// This shows the minimal changes needed to support multiple script execution
// === In McRogueFaceConfig.h ===
struct McRogueFaceConfig {
// ... existing fields ...
// Scripts to execute after main script (McRogueFace style)
std::vector<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

102
gitea_issues.py Normal file
View File

@ -0,0 +1,102 @@
import json
from time import time
#with open("/home/john/issues.json", "r") as f:
# data = json.loads(f.read())
#with open("/home/john/issues2.json", "r") as f:
# data.extend(json.loads(f.read()))
print("Fetching issues...", end='')
start = time()
from gitea import Gitea, Repository, Issue
g = Gitea("https://gamedev.ffwf.net/gitea", token_text="3b450f66e21d62c22bb9fa1c8b975049a5d0c38d")
repo = Repository.request(g, "john", "McRogueFace")
issues = repo.get_issues()
dur = time() - start
print(f"({dur:.1f}s)")
print("Gitea Version: " + g.get_version())
print("API-Token belongs to user: " + g.get_user().username)
data = [
{
"labels": i.labels,
"body": i.body,
"number": i.number,
}
for i in issues
]
input()
def front_number(txt):
if not txt[0].isdigit(): return None
number = ""
for c in txt:
if not c.isdigit():
break
number += c
return int(number)
def split_any(txt, splitters):
tokens = []
txt = [txt]
for s in splitters:
for t in txt:
tokens.extend(t.split(s))
txt = tokens
tokens = []
return txt
def find_refs(txt):
tokens = [tok for tok in split_any(txt, ' ,;\t\r\n') if tok.startswith('#')]
return [front_number(tok[1:]) for tok in tokens]
from collections import defaultdict
issue_relations = defaultdict(list)
nodes = set()
for issue in data:
#refs = issue['body'].split('#')[1::2]
#refs = [front_number(r) for r in refs if front_number(r) is not None]
refs = find_refs(issue['body'])
print(issue['number'], ':', refs)
issue_relations[issue['number']].extend(refs)
nodes.add(issue['number'])
for r in refs:
nodes.add(r)
issue_relations[r].append(issue['number'])
# Find issue labels
issue_labels = {}
for d in data:
labels = [l['name'] for l in d['labels']]
#print(d['number'], labels)
issue_labels[d['number']] = labels
import networkx as nx
import matplotlib.pyplot as plt
relations = nx.Graph()
for k in issue_relations:
relations.add_node(k)
for r in issue_relations[k]:
relations.add_edge(k, r)
relations.add_edge(r, k)
#nx.draw_networkx(relations)
pos = nx.spring_layout(relations)
nx.draw_networkx_nodes(relations, pos,
nodelist = [n for n in issue_labels if 'Alpha Release Requirement' in issue_labels[n]],
node_color="tab:red")
nx.draw_networkx_nodes(relations, pos,
nodelist = [n for n in issue_labels if 'Alpha Release Requirement' not in issue_labels[n]],
node_color="tab:blue")
nx.draw_networkx_edges(relations, pos,
edgelist = relations.edges()
)
nx.draw_networkx_labels(relations, pos, {i: str(i) for i in relations.nodes()})
plt.show()

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
issue78_fixed_1658.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -11,10 +11,10 @@ public:
const static int WHEEL_NUM = 4;
const static int WHEEL_NEG = 2;
const static int WHEEL_DEL = 1;
static int keycode(sf::Keyboard::Key& k) { return KEY + (int)k; }
static int keycode(sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
static int keycode(const sf::Keyboard::Key& k) { return KEY + (int)k; }
static int keycode(const sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
//static int keycode(sf::Mouse::Wheel& w, float d) { return MOUSEWHEEL + (((int)w)<<12) + int(d*16) + 512; }
static int keycode(sf::Mouse::Wheel& w, float d) {
static int keycode(const sf::Mouse::Wheel& w, float d) {
int neg = 0;
if (d < 0) { neg = 1; }
return MOUSEWHEEL + (w * WHEEL_NUM) + (neg * WHEEL_NEG) + 1;
@ -32,7 +32,7 @@ public:
return (a & WHEEL_DEL) * factor;
}
static std::string key_str(sf::Keyboard::Key& keycode)
static std::string key_str(const sf::Keyboard::Key& keycode)
{
switch(keycode)
{

527
src/Animation.cpp Normal file
View File

@ -0,0 +1,527 @@
#include "Animation.h"
#include "UIDrawable.h"
#include "UIEntity.h"
#include <cmath>
#include <algorithm>
#include <unordered_map>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Animation implementation
Animation::Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc,
bool delta)
: targetProperty(targetProperty)
, targetValue(targetValue)
, duration(duration)
, easingFunc(easingFunc)
, delta(delta)
{
}
void Animation::start(UIDrawable* target) {
currentTarget = target;
elapsed = 0.0f;
// Capture startValue from target based on targetProperty
if (!currentTarget) return;
// Try to get the current value based on the expected type
std::visit([this](const auto& targetVal) {
using T = std::decay_t<decltype(targetVal)>;
if constexpr (std::is_same_v<T, float>) {
float value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
int value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, get current sprite index
int value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Color>) {
sf::Color value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
sf::Vector2f value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::string>) {
std::string value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
}, targetValue);
}
void Animation::startEntity(UIEntity* target) {
currentEntityTarget = target;
currentTarget = nullptr; // Clear drawable target
elapsed = 0.0f;
// Capture the starting value from the entity
std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
float value = 0.0f;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
// For entities, we might need to handle sprite_number differently
if (targetProperty == "sprite_number") {
startValue = target->sprite.getSpriteIndex();
}
}
// Entities don't support other types yet
}, targetValue);
}
bool Animation::update(float deltaTime) {
if ((!currentTarget && !currentEntityTarget) || isComplete()) {
return false;
}
elapsed += deltaTime;
elapsed = std::min(elapsed, duration);
// Calculate easing value (0.0 to 1.0)
float t = duration > 0 ? elapsed / duration : 1.0f;
float easedT = easingFunc(t);
// Get interpolated value
AnimationValue currentValue = interpolate(easedT);
// Apply currentValue to target (either drawable or entity)
std::visit([this](const auto& value) {
using T = std::decay_t<decltype(value)>;
if (currentTarget) {
// Handle UIDrawable targets
if constexpr (std::is_same_v<T, float>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, int>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, std::string>) {
currentTarget->setProperty(targetProperty, value);
}
}
else if (currentEntityTarget) {
// Handle UIEntity targets
if constexpr (std::is_same_v<T, float>) {
currentEntityTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, int>) {
currentEntityTarget->setProperty(targetProperty, value);
}
// Entities don't support other types yet
}
}, currentValue);
return !isComplete();
}
AnimationValue Animation::getCurrentValue() const {
float t = duration > 0 ? elapsed / duration : 1.0f;
float easedT = easingFunc(t);
return interpolate(easedT);
}
AnimationValue Animation::interpolate(float t) const {
// Visit the variant to perform type-specific interpolation
return std::visit([this, t](const auto& target) -> AnimationValue {
using T = std::decay_t<decltype(target)>;
if constexpr (std::is_same_v<T, float>) {
// Interpolate float
const float* start = std::get_if<float>(&startValue);
if (!start) return target; // Type mismatch
if (delta) {
return *start + target * t;
} else {
return *start + (target - *start) * t;
}
}
else if constexpr (std::is_same_v<T, int>) {
// Interpolate integer
const int* start = std::get_if<int>(&startValue);
if (!start) return target;
float result;
if (delta) {
result = *start + target * t;
} else {
result = *start + (target - *start) * t;
}
return static_cast<int>(std::round(result));
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, interpolate through the list
if (target.empty()) return target;
// Map t to an index in the vector
size_t index = static_cast<size_t>(t * (target.size() - 1));
index = std::min(index, target.size() - 1);
return static_cast<int>(target[index]);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
// Interpolate color
const sf::Color* start = std::get_if<sf::Color>(&startValue);
if (!start) return target;
sf::Color result;
if (delta) {
result.r = std::clamp(start->r + target.r * t, 0.0f, 255.0f);
result.g = std::clamp(start->g + target.g * t, 0.0f, 255.0f);
result.b = std::clamp(start->b + target.b * t, 0.0f, 255.0f);
result.a = std::clamp(start->a + target.a * t, 0.0f, 255.0f);
} else {
result.r = start->r + (target.r - start->r) * t;
result.g = start->g + (target.g - start->g) * t;
result.b = start->b + (target.b - start->b) * t;
result.a = start->a + (target.a - start->a) * t;
}
return result;
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
// Interpolate vector
const sf::Vector2f* start = std::get_if<sf::Vector2f>(&startValue);
if (!start) return target;
if (delta) {
return sf::Vector2f(start->x + target.x * t,
start->y + target.y * t);
} else {
return sf::Vector2f(start->x + (target.x - start->x) * t,
start->y + (target.y - start->y) * t);
}
}
else if constexpr (std::is_same_v<T, std::string>) {
// For text, show characters based on t
const std::string* start = std::get_if<std::string>(&startValue);
if (!start) return target;
// If delta mode, append characters from target
if (delta) {
size_t chars = static_cast<size_t>(target.length() * t);
return *start + target.substr(0, chars);
} else {
// Transition from start text to target text
if (t < 0.5f) {
// First half: remove characters from start
size_t chars = static_cast<size_t>(start->length() * (1.0f - t * 2.0f));
return start->substr(0, chars);
} else {
// Second half: add characters to target
size_t chars = static_cast<size_t>(target.length() * ((t - 0.5f) * 2.0f));
return target.substr(0, chars);
}
}
}
return target; // Fallback
}, targetValue);
}
// Easing functions implementation
namespace EasingFunctions {
float linear(float t) {
return t;
}
float easeIn(float t) {
return t * t;
}
float easeOut(float t) {
return t * (2.0f - t);
}
float easeInOut(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
// Quadratic
float easeInQuad(float t) {
return t * t;
}
float easeOutQuad(float t) {
return t * (2.0f - t);
}
float easeInOutQuad(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
// Cubic
float easeInCubic(float t) {
return t * t * t;
}
float easeOutCubic(float t) {
float t1 = t - 1.0f;
return t1 * t1 * t1 + 1.0f;
}
float easeInOutCubic(float t) {
return t < 0.5f ? 4.0f * t * t * t : (t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f;
}
// Quartic
float easeInQuart(float t) {
return t * t * t * t;
}
float easeOutQuart(float t) {
float t1 = t - 1.0f;
return 1.0f - t1 * t1 * t1 * t1;
}
float easeInOutQuart(float t) {
return t < 0.5f ? 8.0f * t * t * t * t : 1.0f - 8.0f * (t - 1.0f) * (t - 1.0f) * (t - 1.0f) * (t - 1.0f);
}
// Sine
float easeInSine(float t) {
return 1.0f - std::cos(t * M_PI / 2.0f);
}
float easeOutSine(float t) {
return std::sin(t * M_PI / 2.0f);
}
float easeInOutSine(float t) {
return 0.5f * (1.0f - std::cos(M_PI * t));
}
// Exponential
float easeInExpo(float t) {
return t == 0.0f ? 0.0f : std::pow(2.0f, 10.0f * (t - 1.0f));
}
float easeOutExpo(float t) {
return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t);
}
float easeInOutExpo(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
if (t < 0.5f) {
return 0.5f * std::pow(2.0f, 20.0f * t - 10.0f);
} else {
return 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f);
}
}
// Circular
float easeInCirc(float t) {
return 1.0f - std::sqrt(1.0f - t * t);
}
float easeOutCirc(float t) {
float t1 = t - 1.0f;
return std::sqrt(1.0f - t1 * t1);
}
float easeInOutCirc(float t) {
if (t < 0.5f) {
return 0.5f * (1.0f - std::sqrt(1.0f - 4.0f * t * t));
} else {
return 0.5f * (std::sqrt(1.0f - (2.0f * t - 2.0f) * (2.0f * t - 2.0f)) + 1.0f);
}
}
// Elastic
float easeInElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.3f;
float a = 1.0f;
float s = p / 4.0f;
float t1 = t - 1.0f;
return -(a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
}
float easeOutElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.3f;
float a = 1.0f;
float s = p / 4.0f;
return a * std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * M_PI) / p) + 1.0f;
}
float easeInOutElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.45f;
float a = 1.0f;
float s = p / 4.0f;
if (t < 0.5f) {
float t1 = 2.0f * t - 1.0f;
return -0.5f * (a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
} else {
float t1 = 2.0f * t - 1.0f;
return a * std::pow(2.0f, -10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p) * 0.5f + 1.0f;
}
}
// Back (overshooting)
float easeInBack(float t) {
const float s = 1.70158f;
return t * t * ((s + 1.0f) * t - s);
}
float easeOutBack(float t) {
const float s = 1.70158f;
float t1 = t - 1.0f;
return t1 * t1 * ((s + 1.0f) * t1 + s) + 1.0f;
}
float easeInOutBack(float t) {
const float s = 1.70158f * 1.525f;
if (t < 0.5f) {
return 0.5f * (4.0f * t * t * ((s + 1.0f) * 2.0f * t - s));
} else {
float t1 = 2.0f * t - 2.0f;
return 0.5f * (t1 * t1 * ((s + 1.0f) * t1 + s) + 2.0f);
}
}
// Bounce
float easeOutBounce(float t) {
if (t < 1.0f / 2.75f) {
return 7.5625f * t * t;
} else if (t < 2.0f / 2.75f) {
float t1 = t - 1.5f / 2.75f;
return 7.5625f * t1 * t1 + 0.75f;
} else if (t < 2.5f / 2.75f) {
float t1 = t - 2.25f / 2.75f;
return 7.5625f * t1 * t1 + 0.9375f;
} else {
float t1 = t - 2.625f / 2.75f;
return 7.5625f * t1 * t1 + 0.984375f;
}
}
float easeInBounce(float t) {
return 1.0f - easeOutBounce(1.0f - t);
}
float easeInOutBounce(float t) {
if (t < 0.5f) {
return 0.5f * easeInBounce(2.0f * t);
} else {
return 0.5f * easeOutBounce(2.0f * t - 1.0f) + 0.5f;
}
}
// Get easing function by name
EasingFunction getByName(const std::string& name) {
static std::unordered_map<std::string, EasingFunction> easingMap = {
{"linear", linear},
{"easeIn", easeIn},
{"easeOut", easeOut},
{"easeInOut", easeInOut},
{"easeInQuad", easeInQuad},
{"easeOutQuad", easeOutQuad},
{"easeInOutQuad", easeInOutQuad},
{"easeInCubic", easeInCubic},
{"easeOutCubic", easeOutCubic},
{"easeInOutCubic", easeInOutCubic},
{"easeInQuart", easeInQuart},
{"easeOutQuart", easeOutQuart},
{"easeInOutQuart", easeInOutQuart},
{"easeInSine", easeInSine},
{"easeOutSine", easeOutSine},
{"easeInOutSine", easeInOutSine},
{"easeInExpo", easeInExpo},
{"easeOutExpo", easeOutExpo},
{"easeInOutExpo", easeInOutExpo},
{"easeInCirc", easeInCirc},
{"easeOutCirc", easeOutCirc},
{"easeInOutCirc", easeInOutCirc},
{"easeInElastic", easeInElastic},
{"easeOutElastic", easeOutElastic},
{"easeInOutElastic", easeInOutElastic},
{"easeInBack", easeInBack},
{"easeOutBack", easeOutBack},
{"easeInOutBack", easeInOutBack},
{"easeInBounce", easeInBounce},
{"easeOutBounce", easeOutBounce},
{"easeInOutBounce", easeInOutBounce}
};
auto it = easingMap.find(name);
if (it != easingMap.end()) {
return it->second;
}
return linear; // Default to linear
}
} // namespace EasingFunctions
// AnimationManager implementation
AnimationManager& AnimationManager::getInstance() {
static AnimationManager instance;
return instance;
}
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
activeAnimations.push_back(animation);
}
void AnimationManager::update(float deltaTime) {
for (auto& anim : activeAnimations) {
anim->update(deltaTime);
}
cleanup();
}
void AnimationManager::cleanup() {
activeAnimations.erase(
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
[](const std::shared_ptr<Animation>& anim) {
return anim->isComplete();
}),
activeAnimations.end()
);
}
void AnimationManager::clear() {
activeAnimations.clear();
}

146
src/Animation.h Normal file
View File

@ -0,0 +1,146 @@
#pragma once
#include <string>
#include <functional>
#include <memory>
#include <variant>
#include <vector>
#include <SFML/Graphics.hpp>
// Forward declarations
class UIDrawable;
class UIEntity;
// Forward declare namespace
namespace EasingFunctions {
float linear(float t);
}
// Easing function type
typedef std::function<float(float)> EasingFunction;
// Animation target value can be various types
typedef std::variant<
float, // Single float value
int, // Single integer value
std::vector<int>, // List of integers (for sprite animation)
sf::Color, // Color animation
sf::Vector2f, // Vector animation
std::string // String animation (for text)
> AnimationValue;
class Animation {
public:
// Constructor
Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc = EasingFunctions::linear,
bool delta = false);
// Apply this animation to a drawable
void start(UIDrawable* target);
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
void startEntity(UIEntity* target);
// Update animation (called each frame)
// Returns true if animation is still running, false if complete
bool update(float deltaTime);
// Get current interpolated value
AnimationValue getCurrentValue() const;
// Animation properties
std::string getTargetProperty() const { return targetProperty; }
float getDuration() const { return duration; }
float getElapsed() const { return elapsed; }
bool isComplete() const { return elapsed >= duration; }
bool isDelta() const { return delta; }
private:
std::string targetProperty; // Property name to animate (e.g., "x", "color.r", "sprite_number")
AnimationValue startValue; // Starting value (captured when animation starts)
AnimationValue targetValue; // Target value to animate to
float duration; // Animation duration in seconds
float elapsed = 0.0f; // Elapsed time
EasingFunction easingFunc; // Easing function to use
bool delta; // If true, targetValue is relative to start
UIDrawable* currentTarget = nullptr; // Current target being animated
UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable)
// Helper to interpolate between values
AnimationValue interpolate(float t) const;
};
// Easing functions library
namespace EasingFunctions {
// Basic easing functions
float linear(float t);
float easeIn(float t);
float easeOut(float t);
float easeInOut(float t);
// Advanced easing functions
float easeInQuad(float t);
float easeOutQuad(float t);
float easeInOutQuad(float t);
float easeInCubic(float t);
float easeOutCubic(float t);
float easeInOutCubic(float t);
float easeInQuart(float t);
float easeOutQuart(float t);
float easeInOutQuart(float t);
float easeInSine(float t);
float easeOutSine(float t);
float easeInOutSine(float t);
float easeInExpo(float t);
float easeOutExpo(float t);
float easeInOutExpo(float t);
float easeInCirc(float t);
float easeOutCirc(float t);
float easeInOutCirc(float t);
float easeInElastic(float t);
float easeOutElastic(float t);
float easeInOutElastic(float t);
float easeInBack(float t);
float easeOutBack(float t);
float easeInOutBack(float t);
float easeInBounce(float t);
float easeOutBounce(float t);
float easeInOutBounce(float t);
// Get easing function by name
EasingFunction getByName(const std::string& name);
}
// Animation manager to handle active animations
class AnimationManager {
public:
static AnimationManager& getInstance();
// Add an animation to be managed
void addAnimation(std::shared_ptr<Animation> animation);
// Update all animations
void update(float deltaTime);
// Remove completed animations
void cleanup();
// Clear all animations
void clear();
private:
AnimationManager() = default;
std::vector<std::shared_ptr<Animation>> activeAnimations;
};

172
src/CommandLineParser.cpp Normal file
View File

@ -0,0 +1,172 @@
#include "CommandLineParser.h"
#include <iostream>
#include <filesystem>
#include <algorithm>
CommandLineParser::CommandLineParser(int argc, char* argv[])
: argc(argc), argv(argv) {}
CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) {
ParseResult result;
current_arg = 1; // Reset for each parse
// Detect if running as Python interpreter
std::filesystem::path exec_name = std::filesystem::path(argv[0]).filename();
if (exec_name.string().find("python") == 0) {
config.headless = true;
config.python_mode = true;
}
while (current_arg < argc) {
std::string arg = argv[current_arg];
// Handle Python-style single-letter flags
if (arg == "-h" || arg == "--help") {
print_help();
result.should_exit = true;
result.exit_code = 0;
return result;
}
if (arg == "-V" || arg == "--version") {
print_version();
result.should_exit = true;
result.exit_code = 0;
return result;
}
// Python execution modes
if (arg == "-c") {
config.python_mode = true;
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the -c option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.python_command = argv[current_arg];
current_arg++;
continue;
}
if (arg == "-m") {
config.python_mode = true;
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the -m option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.python_module = argv[current_arg];
current_arg++;
// Collect remaining args as module args
while (current_arg < argc) {
config.script_args.push_back(argv[current_arg]);
current_arg++;
}
continue;
}
if (arg == "-i") {
config.interactive_mode = true;
config.python_mode = true;
current_arg++;
continue;
}
// McRogueFace specific flags
if (arg == "--headless") {
config.headless = true;
config.audio_enabled = false;
current_arg++;
continue;
}
if (arg == "--audio-off") {
config.audio_enabled = false;
current_arg++;
continue;
}
if (arg == "--audio-on") {
config.audio_enabled = true;
current_arg++;
continue;
}
if (arg == "--screenshot") {
config.take_screenshot = true;
current_arg++;
if (current_arg < argc && argv[current_arg][0] != '-') {
config.screenshot_path = argv[current_arg];
current_arg++;
} else {
config.screenshot_path = "screenshot.png";
}
continue;
}
if (arg == "--exec") {
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the --exec option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.exec_scripts.push_back(argv[current_arg]);
config.python_mode = true;
current_arg++;
continue;
}
// If no flags matched, treat as positional argument (script name)
if (arg[0] != '-') {
config.script_path = arg;
config.python_mode = true;
current_arg++;
// Remaining args are script args
while (current_arg < argc) {
config.script_args.push_back(argv[current_arg]);
current_arg++;
}
break;
}
// Unknown flag
std::cerr << "Unknown option: " << arg << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
return result;
}
void CommandLineParser::print_help() {
std::cout << "usage: mcrogueface [option] ... [-c cmd | -m mod | file | -] [arg] ...\n"
<< "Options:\n"
<< " -c cmd : program passed in as string (terminates option list)\n"
<< " -h : print this help message and exit (also --help)\n"
<< " -i : inspect interactively after running script\n"
<< " -m mod : run library module as a script (terminates option list)\n"
<< " -V : print the Python version number and exit (also --version)\n"
<< "\n"
<< "McRogueFace specific options:\n"
<< " --exec file : execute script before main program (can be used multiple times)\n"
<< " --headless : run without creating a window (implies --audio-off)\n"
<< " --audio-off : disable audio\n"
<< " --audio-on : enable audio (even in headless mode)\n"
<< " --screenshot [path] : take a screenshot in headless mode\n"
<< "\n"
<< "Arguments:\n"
<< " file : program read from script file\n"
<< " - : program read from stdin\n"
<< " arg ...: arguments passed to program in sys.argv[1:]\n";
}
void CommandLineParser::print_version() {
std::cout << "Python 3.12.0 (McRogueFace embedded)\n";
}

30
src/CommandLineParser.h Normal file
View File

@ -0,0 +1,30 @@
#ifndef COMMAND_LINE_PARSER_H
#define COMMAND_LINE_PARSER_H
#include <string>
#include <vector>
#include "McRogueFaceConfig.h"
class CommandLineParser {
public:
struct ParseResult {
bool should_exit = false;
int exit_code = 0;
};
CommandLineParser(int argc, char* argv[]);
ParseResult parse(McRogueFaceConfig& config);
private:
int argc;
char** argv;
int current_arg = 1; // Skip program name
bool has_flag(const std::string& short_flag, const std::string& long_flag = "");
std::string get_next_arg(const std::string& flag_name);
void parse_positional_args(McRogueFaceConfig& config);
void print_help();
void print_version();
};
#endif // COMMAND_LINE_PARSER_H

View File

@ -4,27 +4,80 @@
#include "PyScene.h"
#include "UITestScene.h"
#include "Resources.h"
#include "Animation.h"
GameEngine::GameEngine()
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
{
}
GameEngine::GameEngine(const McRogueFaceConfig& cfg)
: config(cfg), headless(cfg.headless)
{
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
Resources::game = this;
window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine";
window.create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close);
visible = window.getDefaultView();
window.setFramerateLimit(60);
// Initialize rendering based on headless mode
if (headless) {
headless_renderer = std::make_unique<HeadlessRenderer>();
if (!headless_renderer->init(1024, 768)) {
throw std::runtime_error("Failed to initialize headless renderer");
}
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);
window->setFramerateLimit(60);
render_target = window.get();
}
visible = render_target->getDefaultView();
scene = "uitest";
scenes["uitest"] = new UITestScene(this);
McRFPy_API::game = this;
// Only load game.py if no custom script/command/module/exec is specified
bool should_load_game = config.script_path.empty() &&
config.python_command.empty() &&
config.python_module.empty() &&
config.exec_scripts.empty() &&
!config.interactive_mode &&
!config.python_mode;
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 in order
if (!config.exec_scripts.empty()) {
if (!Py_IsInitialized()) {
McRFPy_API::api_init();
}
McRFPy_API::executePyString("import mcrfpy");
for (const auto& exec_script : config.exec_scripts) {
std::cout << "Executing script: " << exec_script << std::endl;
McRFPy_API::executeScript(exec_script.string());
}
std::cout << "All --exec scripts completed" << std::endl;
}
clock.restart();
runtime.restart();
}
GameEngine::~GameEngine()
{
for (auto& [name, scene] : scenes) {
delete scene;
}
}
Scene* GameEngine::currentScene() { return scenes[scene]; }
void GameEngine::changeScene(std::string s)
{
@ -37,36 +90,77 @@ void GameEngine::changeScene(std::string s)
void GameEngine::quit() { running = false; }
void GameEngine::setPause(bool p) { paused = p; }
sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; }
sf::RenderWindow & GameEngine::getWindow() { return window; }
sf::RenderWindow & GameEngine::getWindow() {
if (!window) {
throw std::runtime_error("Window not available in headless mode");
}
return *window;
}
sf::RenderTarget & GameEngine::getRenderTarget() {
return *render_target;
}
void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); }
void GameEngine::setWindowScale(float multiplier)
{
window.setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling
if (!headless && window) {
window->setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling
}
//window.create(sf::VideoMode(1024 * multiplier, 768 * multiplier), window_title, sf::Style::Titlebar | sf::Style::Close);
}
void GameEngine::run()
{
std::cout << "GameEngine::run() starting main loop..." << std::endl;
float fps = 0.0;
frameTime = 0.016f; // Initialize to ~60 FPS
clock.restart();
while (running)
{
currentScene()->update();
testTimers();
// Update animations (only if frameTime is valid)
if (frameTime > 0.0f && frameTime < 1.0f) {
AnimationManager::getInstance().update(frameTime);
}
if (!headless) {
sUserInput();
}
if (!paused)
{
}
currentScene()->render();
// Display the frame
if (headless) {
headless_renderer->display();
// Take screenshot if requested
if (config.take_screenshot) {
headless_renderer->saveScreenshot(config.screenshot_path.empty() ? "screenshot.png" : config.screenshot_path);
config.take_screenshot = false; // Only take one screenshot
}
} else {
window->display();
}
currentFrame++;
frameTime = clock.restart().asSeconds();
fps = 1 / frameTime;
int whole_fps = (int)fps;
int tenth_fps = int(fps * 100) % 10;
//window.setTitle(window_title + " " + std::to_string(fps) + " FPS");
window.setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
if (!headless && window) {
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
}
// In windowed mode, check if window was closed
if (!headless && window && !window->isOpen()) {
running = false;
}
}
}
@ -108,29 +202,15 @@ void GameEngine::testTimers()
}
}
void GameEngine::sUserInput()
{
sf::Event event;
while (window.pollEvent(event))
void GameEngine::processEvent(const sf::Event& event)
{
std::string actionType;
int actionCode = 0;
if (event.type == sf::Event::Closed) { running = false; continue; }
if (event.type == sf::Event::Closed) { running = false; return; }
// TODO: add resize event to Scene to react; call it after constructor too, maybe
else if (event.type == sf::Event::Resized) {
continue; // 7DRL short circuit. Resizing manually disabled
/*
sf::FloatRect area(0.f, 0.f, event.size.width, event.size.height);
//sf::FloatRect area(0.f, 0.f, 1024.f, 768.f); // 7DRL 2024: attempt to set scale appropriately
//sf::FloatRect area(0.f, 0.f, event.size.width, event.size.width * 0.75);
visible = sf::View(area);
window.setView(visible);
//window.setSize(sf::Vector2u(event.size.width, event.size.width * 0.75)); // 7DRL 2024: window scaling
std::cout << "Visible area set to (0, 0, " << event.size.width << ", " << event.size.height <<")"<<std::endl;
actionType = "resize";
//window.setSize(sf::Vector2u(event.size.width, event.size.width * 0.75)); // 7DRL 2024: window scaling
*/
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";
@ -142,52 +222,34 @@ void GameEngine::sUserInput()
actionCode = ActionCode::keycode(event.key.code);
else if (event.type == sf::Event::MouseWheelScrolled)
{
// //sf::Mouse::Wheel w = event.MouseWheelScrollEvent.wheel;
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
{
int delta = 1;
if (event.mouseWheelScroll.delta < 0) delta = -1;
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
/*
std::cout << "[GameEngine] Generated MouseWheel code w(" << (int)event.mouseWheelScroll.wheel << ") d(" << event.mouseWheelScroll.delta << ") D(" << delta << ") = " << actionCode << std::endl;
std::cout << " test decode: isMouseWheel=" << ActionCode::isMouseWheel(actionCode) << ", wheel=" << ActionCode::wheel(actionCode) << ", delta=" << ActionCode::delta(actionCode) << std::endl;
std::cout << " math test: actionCode && WHEEL_NEG -> " << (actionCode && ActionCode::WHEEL_NEG) << "; actionCode && WHEEL_DEL -> " << (actionCode && ActionCode::WHEEL_DEL) << ";" << std::endl;
*/
}
// float d = event.MouseWheelScrollEvent.delta;
// actionCode = ActionCode::keycode(0, d);
}
else
continue;
//std::cout << "Event produced action code " << actionCode << ": " << actionType << std::endl;
return;
if (currentScene()->hasAction(actionCode))
{
std::string name = currentScene()->action(actionCode);
currentScene()->doAction(name, actionType);
}
else if (currentScene()->key_callable)
else if (currentScene()->key_callable &&
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
{
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
/*
PyObject* args = Py_BuildValue("(ss)", ActionCode::key_str(event.key.code).c_str(), actionType.c_str());
PyObject* retval = PyObject_Call(currentScene()->key_callable, args, NULL);
if (!retval)
{
std::cout << "key_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 << "key_callable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
}
*/
}
else
void GameEngine::sUserInput()
{
//std::cout << "[GameEngine] Action not registered for input: " << actionCode << ": " << actionType << std::endl;
}
sf::Event event;
while (window && window->pollEvent(event))
{
processEvent(event);
}
}

View File

@ -6,10 +6,16 @@
#include "IndexTexture.h"
#include "Timer.h"
#include "PyCallable.h"
#include "McRogueFaceConfig.h"
#include "HeadlessRenderer.h"
#include <memory>
class GameEngine
{
sf::RenderWindow window;
std::unique_ptr<sf::RenderWindow> window;
std::unique_ptr<HeadlessRenderer> headless_renderer;
sf::RenderTarget* render_target;
sf::Font font;
std::map<std::string, Scene*> scenes;
bool running = true;
@ -20,6 +26,9 @@ class GameEngine
float frameTime;
std::string window_title;
bool headless = false;
McRogueFaceConfig config;
sf::Clock runtime;
//std::map<std::string, Timer> timers;
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
@ -28,6 +37,8 @@ class GameEngine
public:
std::string scene;
GameEngine();
GameEngine(const McRogueFaceConfig& cfg);
~GameEngine();
Scene* currentScene();
void changeScene(std::string);
void createScene(std::string);
@ -35,6 +46,8 @@ public:
void setPause(bool);
sf::Font & getFont();
sf::RenderWindow & getWindow();
sf::RenderTarget & getRenderTarget();
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
void run();
void sUserInput();
int getFrame() { return currentFrame; }
@ -42,6 +55,8 @@ public:
sf::View getView() { return visible; }
void manageTimer(std::string, PyObject*, int);
void setWindowScale(float);
bool isHeadless() const { return headless; }
void processEvent(const sf::Event& event);
// global textures for scripts to access
std::vector<IndexTexture> textures;

27
src/HeadlessRenderer.cpp Normal file
View File

@ -0,0 +1,27 @@
#include "HeadlessRenderer.h"
#include <iostream>
bool HeadlessRenderer::init(int width, int height) {
if (!render_texture.create(width, height)) {
std::cerr << "Failed to create headless render texture" << std::endl;
return false;
}
return true;
}
sf::RenderTarget& HeadlessRenderer::getRenderTarget() {
return render_texture;
}
void HeadlessRenderer::saveScreenshot(const std::string& path) {
sf::Image screenshot = render_texture.getTexture().copyToImage();
if (!screenshot.saveToFile(path)) {
std::cerr << "Failed to save screenshot to: " << path << std::endl;
} else {
std::cout << "Screenshot saved to: " << path << std::endl;
}
}
void HeadlessRenderer::display() {
render_texture.display();
}

20
src/HeadlessRenderer.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef HEADLESS_RENDERER_H
#define HEADLESS_RENDERER_H
#include <SFML/Graphics.hpp>
#include <memory>
#include <string>
class HeadlessRenderer {
private:
sf::RenderTexture render_texture;
public:
bool init(int width = 1024, int height = 768);
sf::RenderTarget& getRenderTarget();
void saveScreenshot(const std::string& path);
void display(); // Finalize the current frame
bool isOpen() const { return true; } // Always "open" in headless mode
};
#endif // HEADLESS_RENDERER_H

View File

@ -1,10 +1,14 @@
#include "McRFPy_API.h"
#include "McRFPy_Automation.h"
#include "platform.h"
#include "PyAnimation.h"
#include "GameEngine.h"
#include "UI.h"
#include "Resources.h"
#include "PyScene.h"
#include <filesystem>
#include <cstring>
std::map<std::string, PyObject*> McRFPy_API::callbacks;
std::vector<sf::SoundBuffer> McRFPy_API::soundbuffers;
sf::Music McRFPy_API::music;
sf::Sound McRFPy_API::sfx;
@ -15,11 +19,6 @@ PyObject* McRFPy_API::mcrf_module;
static PyMethodDef mcrfpyMethods[] = {
{"registerPyAction", McRFPy_API::_registerPyAction, METH_VARARGS,
"Register a callable Python object to correspond to an action string. (actionstr, callable)"},
{"registerInputAction", McRFPy_API::_registerInputAction, METH_VARARGS,
"Register a SFML input code to correspond to an action string. (input_code, actionstr)"},
{"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS, "(filename)"},
{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS, "(filename)"},
@ -79,17 +78,20 @@ PyObject* PyInit_mcrfpy()
/*collections & iterators*/
&PyUICollectionType, &PyUICollectionIterType,
&PyUIEntityCollectionType, &PyUIEntityCollectionIterType,
/*animation*/
&PyAnimationType,
nullptr};
int i = 0;
auto t = pytypes[i];
while (t != nullptr)
{
std::cout << "Registering type: " << t->tp_name << std::endl;
//std::cout << "Registering type: " << t->tp_name << std::endl;
if (PyType_Ready(t) < 0) {
std::cout << "ERROR: PyType_Ready failed for " << t->tp_name << std::endl;
return NULL;
}
std::cout << " tp_alloc after PyType_Ready: " << (void*)t->tp_alloc << std::endl;
//std::cout << " tp_alloc after PyType_Ready: " << (void*)t->tp_alloc << std::endl;
PyModule_AddType(m, t);
i++;
t = pytypes[i];
@ -102,6 +104,17 @@ PyObject* PyInit_mcrfpy()
//PyModule_AddObject(m, "default_texture", McRFPy_API::default_texture->pyObject());
PyModule_AddObject(m, "default_font", Py_None);
PyModule_AddObject(m, "default_texture", Py_None);
// Add automation submodule
PyObject* automation_module = McRFPy_Automation::init_automation_module();
if (automation_module != NULL) {
PyModule_AddObject(m, "automation", automation_module);
// Also add to sys.modules for proper import behavior
PyObject* sys_modules = PyImport_GetModuleDict();
PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module);
}
//McRFPy_API::mcrf_module = m;
return m;
}
@ -160,6 +173,75 @@ PyStatus init_python(const char *program_name)
return status;
}
PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv)
{
// If Python is already initialized, just return success
if (Py_IsInitialized()) {
return PyStatus_Ok();
}
PyStatus status;
PyConfig pyconfig;
PyConfig_InitIsolatedConfig(&pyconfig);
// CRITICAL: Pass actual command line arguments to Python
status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
if (PyStatus_Exception(status)) {
return status;
}
// Check if we're in a virtual environment
auto exe_path = std::filesystem::path(argv[0]);
auto exe_dir = exe_path.parent_path();
auto venv_root = exe_dir.parent_path();
if (std::filesystem::exists(venv_root / "pyvenv.cfg")) {
// We're running from within a venv!
// Add venv's site-packages to module search paths
auto site_packages = venv_root / "lib" / "python3.12" / "site-packages";
PyWideStringList_Append(&pyconfig.module_search_paths,
site_packages.wstring().c_str());
pyconfig.module_search_paths_set = 1;
}
// Set Python home to our bundled Python
auto python_home = executable_path() + L"/lib/Python";
PyConfig_SetString(&pyconfig, &pyconfig.home, python_home.c_str());
// Set up module search paths
#if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1
if (!pyconfig.module_search_paths_set) {
pyconfig.module_search_paths_set = 1;
}
// search paths for python libs/modules/scripts
const wchar_t* str_arr[] = {
L"/scripts",
L"/lib/Python/lib.linux-x86_64-3.12",
L"/lib/Python",
L"/lib/Python/Lib",
L"/venv/lib/python3.12/site-packages"
};
for(auto s : str_arr) {
status = PyWideStringList_Append(&pyconfig.module_search_paths, (executable_path() + s).c_str());
if (PyStatus_Exception(status)) {
continue;
}
}
#endif
// Register mcrfpy module before initialization
if (!Py_IsInitialized()) {
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
}
status = Py_InitializeFromConfig(&pyconfig);
PyConfig_Clear(&pyconfig);
return status;
}
/*
void McRFPy_API::setSpriteTexture(int ti)
{
@ -177,9 +259,11 @@ void McRFPy_API::setSpriteTexture(int ti)
void McRFPy_API::api_init() {
// build API exposure before python initialization
if (!Py_IsInitialized()) {
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
// use full path version of argv[0] from OS to init python
init_python(narrow_string(executable_filename()).c_str());
}
//texture.loadFromFile("./assets/kenney_tinydungeon.png");
//texture_size = 16, texture_width = 12, texture_height= 11;
@ -200,11 +284,40 @@ void McRFPy_API::api_init() {
//setSpriteTexture(0);
}
void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv) {
// Initialize Python with proper argv - this is CRITICAL
PyStatus status = init_python_with_config(config, argc, argv);
if (PyStatus_Exception(status)) {
Py_ExitStatusException(status);
}
McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
// For -m module execution, let Python handle it
if (!config.python_module.empty() && config.python_module != "venv") {
// Py_RunMain() will handle -m execution
return;
}
// Execute based on mode - this is handled in main.cpp now
// The actual execution logic is in run_python_interpreter()
// Set up default resources only if in game mode
if (!config.python_mode) {
//PyModule_AddObject(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
//PyModule_AddObject(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
}
}
void McRFPy_API::executeScript(std::string filename)
{
FILE* PScriptFile = fopen(filename.c_str(), "r");
if(PScriptFile) {
std::cout << "Before PyRun_SimpleFile" << std::endl;
PyRun_SimpleFile(PScriptFile, filename.c_str());
std::cout << "After PyRun_SimpleFile" << std::endl;
fclose(PScriptFile);
}
}
@ -230,63 +343,7 @@ void McRFPy_API::REPL_device(FILE * fp, const char *filename)
}
// python connection
PyObject* McRFPy_API::_registerPyAction(PyObject *self, PyObject *args)
{
PyObject* callable;
const char * actionstr;
if (!PyArg_ParseTuple(args, "sO", &actionstr, &callable)) return NULL;
//TODO: if the string already exists in the callbacks map,
// decrease our reference count so it can potentially be garbage collected
callbacks[std::string(actionstr)] = callable;
Py_INCREF(callable);
// return None correctly
Py_INCREF(Py_None);
return Py_None;
}
PyObject* McRFPy_API::_registerInputAction(PyObject *self, PyObject *args)
{
int action_code;
const char * actionstr;
if (!PyArg_ParseTuple(args, "iz", &action_code, &actionstr)) return NULL;
bool success;
if (actionstr == NULL) { // Action provided is None, i.e. unregister
std::cout << "Unregistering\n";
success = game->currentScene()->unregisterActionInjected(action_code, std::string(actionstr) + "_py");
} else {
std::cout << "Registering " << actionstr << "_py to " << action_code << "\n";
success = game->currentScene()->registerActionInjected(action_code, std::string(actionstr) + "_py");
}
success ? Py_INCREF(Py_True) : Py_INCREF(Py_False);
return success ? Py_True : Py_False;
}
void McRFPy_API::doAction(std::string actionstr) {
// hard coded actions that require no registration
//std::cout << "Calling Python Action: " << actionstr;
if (!actionstr.compare("startrepl")) return McRFPy_API::REPL();
if (callbacks.find(actionstr) == callbacks.end())
{
//std::cout << " (not found)" << std::endl;
return;
}
//std::cout << " (" << PyUnicode_AsUTF8(PyObject_Repr(callbacks[actionstr])) << ")" << std::endl;
PyObject* retval = PyObject_Call(callbacks[actionstr], PyTuple_New(0), NULL);
if (!retval)
{
std::cout << "doAction 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 << "doAction returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
}
}
/*
PyObject* McRFPy_API::_refreshFov(PyObject* self, PyObject* args) {
@ -359,73 +416,10 @@ PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) {
return Py_BuildValue("f", McRFPy_API::sfx.getVolume());
}
// Removed deprecated player_input, computerTurn, playerTurn functions
// These were part of the old turn-based system that is no longer used
/*
void McRFPy_API::player_input(int dx, int dy) {
//std::cout << "# entities tagged 'player': " << McRFPy_API::entities.getEntities("player").size() << std::endl;
auto player_entity = McRFPy_API::entities.getEntities("player")[0];
auto grid = player_entity->cGrid->grid;
//std::cout << "Grid pointed to: " << (long)player_entity->cGrid->grid << std::endl;
if (McRFPy_API::input_mode.compare("playerturn") != 0) {
// no input accepted while computer moving
//std::cout << "Can't move while it's not player's turn." << std::endl;
return;
}
// TODO: selection cursor via keyboard
// else if (!input_mode.compare("selectpoint") {}
// else if (!input_mode.compare("selectentity") {}
// grid bounds check
if (player_entity->cGrid->x + dx < 0 ||
player_entity->cGrid->y + dy < 0 ||
player_entity->cGrid->x + dx > grid->grid_x - 1 ||
player_entity->cGrid->y + dy > grid->grid_y - 1) {
//std::cout << "(" << player_entity->cGrid->x << ", " << player_entity->cGrid->y <<
// ") + (" << dx << ", " << dy << ") is OOB." << std::endl;
return;
}
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(player_entity->cBehavior->object)) << std::endl;
PyObject* move_fn = PyObject_GetAttrString(player_entity->cBehavior->object, "move");
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(move_fn)) << std::endl;
if (move_fn) {
//std::cout << "Calling `move`" << std::endl;
PyObject* move_args = Py_BuildValue("(ii)", dx, dy);
PyObject_CallObject((PyObject*) move_fn, move_args);
} else {
//std::cout << "player_input called on entity with no `move` method" << std::endl;
}
}
void McRFPy_API::computerTurn() {
McRFPy_API::input_mode = "computerturnrunning";
for (auto e : McRFPy_API::grids[McRFPy_API::active_grid]->entities) {
if (e->cBehavior) {
PyObject_Call(PyObject_GetAttrString(e->cBehavior->object, "ai_act"), PyTuple_New(0), NULL);
}
}
}
void McRFPy_API::playerTurn() {
McRFPy_API::input_mode = "playerturn";
for (auto e : McRFPy_API::entities.getEntities("player")) {
if (e->cBehavior) {
PyObject_Call(PyObject_GetAttrString(e->cBehavior->object, "player_act"), PyTuple_New(0), NULL);
}
}
}
void McRFPy_API::camFollow() {
if (!McRFPy_API::do_camfollow) return;
auto& ag = McRFPy_API::grids[McRFPy_API::active_grid];
for (auto e : McRFPy_API::entities.getEntities("player")) {
//std::cout << "grid center: " << ag->center_x << ", " << ag->center_y << std::endl <<
// "player grid pos: " << e->cGrid->x << ", " << e->cGrid->y << std::endl <<
// "player sprite pos: " << e->cGrid->indexsprite.x << ", " << e->cGrid->indexsprite.y << std::endl;
ag->center_x = e->cGrid->indexsprite.x * ag->grid_size + ag->grid_size * 0.5;
ag->center_y = e->cGrid->indexsprite.y * ag->grid_size + ag->grid_size * 0.5;
}
}
PyObject* McRFPy_API::_camFollow(PyObject* self, PyObject* args) {
PyObject* set_camfollow = NULL;
//std::cout << "camFollow Parse Args" << std::endl;
@ -489,6 +483,13 @@ PyObject* McRFPy_API::_createScene(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_keypressScene(PyObject* self, PyObject* args) {
PyObject* callable;
if (!PyArg_ParseTuple(args, "O", &callable)) return NULL;
// Validate that the argument is callable
if (!PyCallable_Check(callable)) {
PyErr_SetString(PyExc_TypeError, "keypressScene() argument must be callable");
return NULL;
}
/*
if (game->currentScene()->key_callable != NULL and game->currentScene()->key_callable != Py_None)
{
@ -499,6 +500,7 @@ PyObject* McRFPy_API::_keypressScene(PyObject* self, PyObject* args) {
Py_INCREF(Py_None);
*/
game->currentScene()->key_callable = std::make_unique<PyKeyCallable>(callable);
Py_INCREF(Py_None);
return Py_None;
}
@ -538,3 +540,15 @@ PyObject* McRFPy_API::_setScale(PyObject* self, PyObject* args) {
Py_INCREF(Py_None);
return Py_None;
}
void McRFPy_API::markSceneNeedsSort() {
// Mark the current scene as needing a z_index sort
auto scene = game->currentScene();
if (scene && scene->ui_elements) {
// Cast to PyScene to access ui_elements_need_sort
PyScene* pyscene = dynamic_cast<PyScene*>(scene);
if (pyscene) {
pyscene->ui_elements_need_sort = true;
}
}
}

View File

@ -5,6 +5,7 @@
#include "PyFont.h"
#include "PyTexture.h"
#include "McRogueFaceConfig.h"
class GameEngine; // forward declared (circular members)
@ -27,6 +28,8 @@ public:
//static void setSpriteTexture(int);
inline static GameEngine* game;
static void api_init();
static void api_init(const McRogueFaceConfig& config, int argc, char** argv);
static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv);
static void api_shutdown();
// Python API functionality - use mcrfpy.* in scripts
//static PyObject* _drawSprite(PyObject*, PyObject*);
@ -37,9 +40,6 @@ public:
static sf::Music music;
static sf::Sound sfx;
static std::map<std::string, PyObject*> callbacks;
static PyObject* _registerPyAction(PyObject*, PyObject*);
static PyObject* _registerInputAction(PyObject*, PyObject*);
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
static PyObject* _loadMusic(PyObject*, PyObject*);
@ -66,12 +66,11 @@ public:
// accept keyboard input from scene
static sf::Vector2i cursor_position;
static void player_input(int, int);
static void computerTurn();
static void playerTurn();
static void doAction(std::string);
static void executeScript(std::string);
static void executePyString(std::string);
// Helper to mark scenes as needing z_index resort
static void markSceneNeedsSort();
};

817
src/McRFPy_Automation.cpp Normal file
View File

@ -0,0 +1,817 @@
#include "McRFPy_Automation.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <unordered_map>
// Helper function to get game engine
GameEngine* McRFPy_Automation::getGameEngine() {
return McRFPy_API::game;
}
// Sleep helper
void McRFPy_Automation::sleep_ms(int milliseconds) {
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
}
// Convert string to SFML key code
sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
static const std::unordered_map<std::string, sf::Keyboard::Key> keyMap = {
// Letters
{"a", sf::Keyboard::A}, {"b", sf::Keyboard::B}, {"c", sf::Keyboard::C},
{"d", sf::Keyboard::D}, {"e", sf::Keyboard::E}, {"f", sf::Keyboard::F},
{"g", sf::Keyboard::G}, {"h", sf::Keyboard::H}, {"i", sf::Keyboard::I},
{"j", sf::Keyboard::J}, {"k", sf::Keyboard::K}, {"l", sf::Keyboard::L},
{"m", sf::Keyboard::M}, {"n", sf::Keyboard::N}, {"o", sf::Keyboard::O},
{"p", sf::Keyboard::P}, {"q", sf::Keyboard::Q}, {"r", sf::Keyboard::R},
{"s", sf::Keyboard::S}, {"t", sf::Keyboard::T}, {"u", sf::Keyboard::U},
{"v", sf::Keyboard::V}, {"w", sf::Keyboard::W}, {"x", sf::Keyboard::X},
{"y", sf::Keyboard::Y}, {"z", sf::Keyboard::Z},
// Numbers
{"0", sf::Keyboard::Num0}, {"1", sf::Keyboard::Num1}, {"2", sf::Keyboard::Num2},
{"3", sf::Keyboard::Num3}, {"4", sf::Keyboard::Num4}, {"5", sf::Keyboard::Num5},
{"6", sf::Keyboard::Num6}, {"7", sf::Keyboard::Num7}, {"8", sf::Keyboard::Num8},
{"9", sf::Keyboard::Num9},
// Function keys
{"f1", sf::Keyboard::F1}, {"f2", sf::Keyboard::F2}, {"f3", sf::Keyboard::F3},
{"f4", sf::Keyboard::F4}, {"f5", sf::Keyboard::F5}, {"f6", sf::Keyboard::F6},
{"f7", sf::Keyboard::F7}, {"f8", sf::Keyboard::F8}, {"f9", sf::Keyboard::F9},
{"f10", sf::Keyboard::F10}, {"f11", sf::Keyboard::F11}, {"f12", sf::Keyboard::F12},
{"f13", sf::Keyboard::F13}, {"f14", sf::Keyboard::F14}, {"f15", sf::Keyboard::F15},
// Special keys
{"escape", sf::Keyboard::Escape}, {"esc", sf::Keyboard::Escape},
{"enter", sf::Keyboard::Enter}, {"return", sf::Keyboard::Enter},
{"space", sf::Keyboard::Space}, {" ", sf::Keyboard::Space},
{"tab", sf::Keyboard::Tab}, {"\t", sf::Keyboard::Tab},
{"backspace", sf::Keyboard::BackSpace},
{"delete", sf::Keyboard::Delete}, {"del", sf::Keyboard::Delete},
{"insert", sf::Keyboard::Insert},
{"home", sf::Keyboard::Home},
{"end", sf::Keyboard::End},
{"pageup", sf::Keyboard::PageUp}, {"pgup", sf::Keyboard::PageUp},
{"pagedown", sf::Keyboard::PageDown}, {"pgdn", sf::Keyboard::PageDown},
// Arrow keys
{"left", sf::Keyboard::Left},
{"right", sf::Keyboard::Right},
{"up", sf::Keyboard::Up},
{"down", sf::Keyboard::Down},
// Modifiers
{"ctrl", sf::Keyboard::LControl}, {"ctrlleft", sf::Keyboard::LControl},
{"ctrlright", sf::Keyboard::RControl},
{"alt", sf::Keyboard::LAlt}, {"altleft", sf::Keyboard::LAlt},
{"altright", sf::Keyboard::RAlt},
{"shift", sf::Keyboard::LShift}, {"shiftleft", sf::Keyboard::LShift},
{"shiftright", sf::Keyboard::RShift},
{"win", sf::Keyboard::LSystem}, {"winleft", sf::Keyboard::LSystem},
{"winright", sf::Keyboard::RSystem}, {"command", sf::Keyboard::LSystem},
// Punctuation
{",", sf::Keyboard::Comma}, {".", sf::Keyboard::Period},
{"/", sf::Keyboard::Slash}, {"\\", sf::Keyboard::BackSlash},
{";", sf::Keyboard::SemiColon}, {"'", sf::Keyboard::Quote},
{"[", sf::Keyboard::LBracket}, {"]", sf::Keyboard::RBracket},
{"-", sf::Keyboard::Dash}, {"=", sf::Keyboard::Equal},
// Numpad
{"num0", sf::Keyboard::Numpad0}, {"num1", sf::Keyboard::Numpad1},
{"num2", sf::Keyboard::Numpad2}, {"num3", sf::Keyboard::Numpad3},
{"num4", sf::Keyboard::Numpad4}, {"num5", sf::Keyboard::Numpad5},
{"num6", sf::Keyboard::Numpad6}, {"num7", sf::Keyboard::Numpad7},
{"num8", sf::Keyboard::Numpad8}, {"num9", sf::Keyboard::Numpad9},
{"add", sf::Keyboard::Add}, {"subtract", sf::Keyboard::Subtract},
{"multiply", sf::Keyboard::Multiply}, {"divide", sf::Keyboard::Divide},
// Other
{"pause", sf::Keyboard::Pause},
{"capslock", sf::Keyboard::LControl}, // Note: SFML doesn't have CapsLock
{"numlock", sf::Keyboard::LControl}, // Note: SFML doesn't have NumLock
{"scrolllock", sf::Keyboard::LControl}, // Note: SFML doesn't have ScrollLock
};
auto it = keyMap.find(keyName);
if (it != keyMap.end()) {
return it->second;
}
return sf::Keyboard::Unknown;
}
// Inject mouse event into the game engine
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button) {
auto engine = getGameEngine();
if (!engine) return;
sf::Event event;
event.type = type;
switch (type) {
case sf::Event::MouseMoved:
event.mouseMove.x = x;
event.mouseMove.y = y;
break;
case sf::Event::MouseButtonPressed:
case sf::Event::MouseButtonReleased:
event.mouseButton.button = button;
event.mouseButton.x = x;
event.mouseButton.y = y;
break;
case sf::Event::MouseWheelScrolled:
event.mouseWheelScroll.wheel = sf::Mouse::VerticalWheel;
event.mouseWheelScroll.delta = static_cast<float>(x); // x is used for scroll amount
event.mouseWheelScroll.x = x;
event.mouseWheelScroll.y = y;
break;
default:
break;
}
engine->processEvent(event);
}
// Inject keyboard event into the game engine
void McRFPy_Automation::injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key) {
auto engine = getGameEngine();
if (!engine) return;
sf::Event event;
event.type = type;
if (type == sf::Event::KeyPressed || type == sf::Event::KeyReleased) {
event.key.code = key;
event.key.alt = sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RAlt);
event.key.control = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RControl);
event.key.shift = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RShift);
event.key.system = sf::Keyboard::isKeyPressed(sf::Keyboard::LSystem) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RSystem);
}
engine->processEvent(event);
}
// Inject text event for typing
void McRFPy_Automation::injectTextEvent(sf::Uint32 unicode) {
auto engine = getGameEngine();
if (!engine) return;
sf::Event event;
event.type = sf::Event::TextEntered;
event.text.unicode = unicode;
engine->processEvent(event);
}
// Screenshot implementation
PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) {
const char* filename;
if (!PyArg_ParseTuple(args, "s", &filename)) {
return NULL;
}
auto engine = getGameEngine();
if (!engine) {
PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
return NULL;
}
// Get the render target
sf::RenderTarget* target = engine->getRenderTargetPtr();
if (!target) {
PyErr_SetString(PyExc_RuntimeError, "No render target available");
return NULL;
}
// For RenderWindow, we can get a screenshot directly
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);
if (texture.copyToImage().saveToFile(filename)) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
// For RenderTexture (headless mode)
else if (auto* renderTexture = dynamic_cast<sf::RenderTexture*>(target)) {
if (renderTexture->getTexture().copyToImage().saveToFile(filename)) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
PyErr_SetString(PyExc_RuntimeError, "Unknown render target type");
return NULL;
}
// Get current mouse position
PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) {
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
return Py_BuildValue("(ii)", 0, 0);
}
// In headless mode, we'd need to track the simulated mouse position
// For now, return the actual mouse position relative to window if available
if (auto* window = dynamic_cast<sf::RenderWindow*>(engine->getRenderTargetPtr())) {
sf::Vector2i pos = sf::Mouse::getPosition(*window);
return Py_BuildValue("(ii)", pos.x, pos.y);
}
// In headless mode, return simulated position (TODO: track this)
return Py_BuildValue("(ii)", 0, 0);
}
// Get screen size
PyObject* McRFPy_Automation::_size(PyObject* self, PyObject* args) {
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
return Py_BuildValue("(ii)", 1024, 768); // Default size
}
sf::Vector2u size = engine->getRenderTarget().getSize();
return Py_BuildValue("(ii)", size.x, size.y);
}
// Check if coordinates are on screen
PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) {
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
return NULL;
}
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
Py_RETURN_FALSE;
}
sf::Vector2u size = engine->getRenderTarget().getSize();
if (x >= 0 && x < (int)size.x && y >= 0 && y < (int)size.y) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
// Move mouse to position
PyObject* McRFPy_Automation::_moveTo(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "duration", NULL};
int x, y;
float duration = 0.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
&x, &y, &duration)) {
return NULL;
}
// TODO: Implement smooth movement with duration
injectMouseEvent(sf::Event::MouseMoved, x, y);
if (duration > 0) {
sleep_ms(static_cast<int>(duration * 1000));
}
Py_RETURN_NONE;
}
// Move mouse relative
PyObject* McRFPy_Automation::_moveRel(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"xOffset", "yOffset", "duration", NULL};
int xOffset, yOffset;
float duration = 0.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
&xOffset, &yOffset, &duration)) {
return NULL;
}
// Get current position
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
int currentX, currentY;
if (!PyArg_ParseTuple(pos, "ii", &currentX, &currentY)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
// Move to new position
injectMouseEvent(sf::Event::MouseMoved, currentX + xOffset, currentY + yOffset);
if (duration > 0) {
sleep_ms(static_cast<int>(duration * 1000));
}
Py_RETURN_NONE;
}
// Click implementation
PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "clicks", "interval", "button", NULL};
int x = -1, y = -1;
int clicks = 1;
float interval = 0.0f;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iiifs", const_cast<char**>(kwlist),
&x, &y, &clicks, &interval, &button)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
// Determine button
sf::Mouse::Button sfButton = sf::Mouse::Left;
if (strcmp(button, "right") == 0) {
sfButton = sf::Mouse::Right;
} else if (strcmp(button, "middle") == 0) {
sfButton = sf::Mouse::Middle;
}
// Move to position first
injectMouseEvent(sf::Event::MouseMoved, x, y);
// Perform clicks
for (int i = 0; i < clicks; i++) {
if (i > 0 && interval > 0) {
sleep_ms(static_cast<int>(interval * 1000));
}
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
sleep_ms(10); // Small delay between press and release
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
}
Py_RETURN_NONE;
}
// Right click
PyObject* McRFPy_Automation::_rightClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
// Build new args with button="right"
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("right"));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
// Double click
PyObject* McRFPy_Automation::_doubleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(2));
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
// Type text
PyObject* McRFPy_Automation::_typewrite(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"message", "interval", NULL};
const char* message;
float interval = 0.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|f", const_cast<char**>(kwlist),
&message, &interval)) {
return NULL;
}
// Type each character
for (size_t i = 0; message[i] != '\0'; i++) {
if (i > 0 && interval > 0) {
sleep_ms(static_cast<int>(interval * 1000));
}
char c = message[i];
// Handle special characters
if (c == '\n') {
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Enter);
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Enter);
} else if (c == '\t') {
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Tab);
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Tab);
} else {
// For regular characters, send text event
injectTextEvent(static_cast<sf::Uint32>(c));
}
}
Py_RETURN_NONE;
}
// Press and hold key
PyObject* McRFPy_Automation::_keyDown(PyObject* self, PyObject* args) {
const char* keyName;
if (!PyArg_ParseTuple(args, "s", &keyName)) {
return NULL;
}
sf::Keyboard::Key key = stringToKey(keyName);
if (key == sf::Keyboard::Unknown) {
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
return NULL;
}
injectKeyEvent(sf::Event::KeyPressed, key);
Py_RETURN_NONE;
}
// Release key
PyObject* McRFPy_Automation::_keyUp(PyObject* self, PyObject* args) {
const char* keyName;
if (!PyArg_ParseTuple(args, "s", &keyName)) {
return NULL;
}
sf::Keyboard::Key key = stringToKey(keyName);
if (key == sf::Keyboard::Unknown) {
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
return NULL;
}
injectKeyEvent(sf::Event::KeyReleased, key);
Py_RETURN_NONE;
}
// Hotkey combination
PyObject* McRFPy_Automation::_hotkey(PyObject* self, PyObject* args) {
// Get all keys as separate arguments
Py_ssize_t numKeys = PyTuple_Size(args);
if (numKeys == 0) {
PyErr_SetString(PyExc_ValueError, "hotkey() requires at least one key");
return NULL;
}
// Press all keys
for (Py_ssize_t i = 0; i < numKeys; i++) {
PyObject* keyObj = PyTuple_GetItem(args, i);
const char* keyName = PyUnicode_AsUTF8(keyObj);
if (!keyName) {
return NULL;
}
sf::Keyboard::Key key = stringToKey(keyName);
if (key == sf::Keyboard::Unknown) {
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
return NULL;
}
injectKeyEvent(sf::Event::KeyPressed, key);
sleep_ms(10); // Small delay between key presses
}
// Release all keys in reverse order
for (Py_ssize_t i = numKeys - 1; i >= 0; i--) {
PyObject* keyObj = PyTuple_GetItem(args, i);
const char* keyName = PyUnicode_AsUTF8(keyObj);
sf::Keyboard::Key key = stringToKey(keyName);
injectKeyEvent(sf::Event::KeyReleased, key);
sleep_ms(10);
}
Py_RETURN_NONE;
}
// Scroll wheel
PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"clicks", "x", "y", NULL};
int clicks;
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", const_cast<char**>(kwlist),
&clicks, &x, &y)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
// Inject scroll event
injectMouseEvent(sf::Event::MouseWheelScrolled, clicks, y);
Py_RETURN_NONE;
}
// Other click types using the main click function
PyObject* McRFPy_Automation::_middleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("middle"));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
PyObject* McRFPy_Automation::_tripleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(3));
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
// Mouse button press/release
PyObject* McRFPy_Automation::_mouseDown(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "button", NULL};
int x = -1, y = -1;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
&x, &y, &button)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
sf::Mouse::Button sfButton = sf::Mouse::Left;
if (strcmp(button, "right") == 0) {
sfButton = sf::Mouse::Right;
} else if (strcmp(button, "middle") == 0) {
sfButton = sf::Mouse::Middle;
}
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
Py_RETURN_NONE;
}
PyObject* McRFPy_Automation::_mouseUp(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "button", NULL};
int x = -1, y = -1;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
&x, &y, &button)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
sf::Mouse::Button sfButton = sf::Mouse::Left;
if (strcmp(button, "right") == 0) {
sfButton = sf::Mouse::Right;
} else if (strcmp(button, "middle") == 0) {
sfButton = sf::Mouse::Middle;
}
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
Py_RETURN_NONE;
}
// Drag operations
PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "duration", "button", NULL};
int x, y;
float duration = 0.0f;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
&x, &y, &duration, &button)) {
return NULL;
}
// Get current position
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
int startX, startY;
if (!PyArg_ParseTuple(pos, "ii", &startX, &startY)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
// Mouse down at current position
PyObject* downArgs = Py_BuildValue("(ii)", startX, startY);
PyObject* downKwargs = PyDict_New();
PyDict_SetItemString(downKwargs, "button", PyUnicode_FromString(button));
PyObject* downResult = _mouseDown(self, downArgs, downKwargs);
Py_DECREF(downArgs);
Py_DECREF(downKwargs);
if (!downResult) return NULL;
Py_DECREF(downResult);
// Move to target position
if (duration > 0) {
// Smooth movement
int steps = static_cast<int>(duration * 60); // 60 FPS
for (int i = 1; i <= steps; i++) {
int currentX = startX + (x - startX) * i / steps;
int currentY = startY + (y - startY) * i / steps;
injectMouseEvent(sf::Event::MouseMoved, currentX, currentY);
sleep_ms(1000 / 60); // 60 FPS
}
} else {
injectMouseEvent(sf::Event::MouseMoved, x, y);
}
// Mouse up at target position
PyObject* upArgs = Py_BuildValue("(ii)", x, y);
PyObject* upKwargs = PyDict_New();
PyDict_SetItemString(upKwargs, "button", PyUnicode_FromString(button));
PyObject* upResult = _mouseUp(self, upArgs, upKwargs);
Py_DECREF(upArgs);
Py_DECREF(upKwargs);
if (!upResult) return NULL;
Py_DECREF(upResult);
Py_RETURN_NONE;
}
PyObject* McRFPy_Automation::_dragRel(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"xOffset", "yOffset", "duration", "button", NULL};
int xOffset, yOffset;
float duration = 0.0f;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
&xOffset, &yOffset, &duration, &button)) {
return NULL;
}
// Get current position
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
int currentX, currentY;
if (!PyArg_ParseTuple(pos, "ii", &currentX, &currentY)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
// Call dragTo with absolute position
PyObject* dragArgs = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset);
PyObject* dragKwargs = PyDict_New();
PyDict_SetItemString(dragKwargs, "duration", PyFloat_FromDouble(duration));
PyDict_SetItemString(dragKwargs, "button", PyUnicode_FromString(button));
PyObject* result = _dragTo(self, dragArgs, dragKwargs);
Py_DECREF(dragArgs);
Py_DECREF(dragKwargs);
return result;
}
// Method definitions for the automation module
static PyMethodDef automationMethods[] = {
{"screenshot", McRFPy_Automation::_screenshot, METH_VARARGS,
"screenshot(filename) - Save a screenshot to the specified file"},
{"position", McRFPy_Automation::_position, METH_NOARGS,
"position() - Get current mouse position as (x, y) tuple"},
{"size", McRFPy_Automation::_size, METH_NOARGS,
"size() - Get screen size as (width, height) tuple"},
{"onScreen", McRFPy_Automation::_onScreen, METH_VARARGS,
"onScreen(x, y) - Check if coordinates are within screen bounds"},
{"moveTo", (PyCFunction)McRFPy_Automation::_moveTo, METH_VARARGS | METH_KEYWORDS,
"moveTo(x, y, duration=0.0) - Move mouse to absolute position"},
{"moveRel", (PyCFunction)McRFPy_Automation::_moveRel, METH_VARARGS | METH_KEYWORDS,
"moveRel(xOffset, yOffset, duration=0.0) - Move mouse relative to current position"},
{"dragTo", (PyCFunction)McRFPy_Automation::_dragTo, METH_VARARGS | METH_KEYWORDS,
"dragTo(x, y, duration=0.0, button='left') - Drag mouse to position"},
{"dragRel", (PyCFunction)McRFPy_Automation::_dragRel, METH_VARARGS | METH_KEYWORDS,
"dragRel(xOffset, yOffset, duration=0.0, button='left') - Drag mouse relative to current position"},
{"click", (PyCFunction)McRFPy_Automation::_click, METH_VARARGS | METH_KEYWORDS,
"click(x=None, y=None, clicks=1, interval=0.0, button='left') - Click at position"},
{"rightClick", (PyCFunction)McRFPy_Automation::_rightClick, METH_VARARGS | METH_KEYWORDS,
"rightClick(x=None, y=None) - Right click at position"},
{"middleClick", (PyCFunction)McRFPy_Automation::_middleClick, METH_VARARGS | METH_KEYWORDS,
"middleClick(x=None, y=None) - Middle click at position"},
{"doubleClick", (PyCFunction)McRFPy_Automation::_doubleClick, METH_VARARGS | METH_KEYWORDS,
"doubleClick(x=None, y=None) - Double click at position"},
{"tripleClick", (PyCFunction)McRFPy_Automation::_tripleClick, METH_VARARGS | METH_KEYWORDS,
"tripleClick(x=None, y=None) - Triple click at position"},
{"scroll", (PyCFunction)McRFPy_Automation::_scroll, METH_VARARGS | METH_KEYWORDS,
"scroll(clicks, x=None, y=None) - Scroll wheel at position"},
{"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS,
"mouseDown(x=None, y=None, button='left') - Press mouse button"},
{"mouseUp", (PyCFunction)McRFPy_Automation::_mouseUp, METH_VARARGS | METH_KEYWORDS,
"mouseUp(x=None, y=None, button='left') - Release mouse button"},
{"typewrite", (PyCFunction)McRFPy_Automation::_typewrite, METH_VARARGS | METH_KEYWORDS,
"typewrite(message, interval=0.0) - Type text with optional interval between keystrokes"},
{"hotkey", McRFPy_Automation::_hotkey, METH_VARARGS,
"hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c'))"},
{"keyDown", McRFPy_Automation::_keyDown, METH_VARARGS,
"keyDown(key) - Press and hold a key"},
{"keyUp", McRFPy_Automation::_keyUp, METH_VARARGS,
"keyUp(key) - Release a key"},
{NULL, NULL, 0, NULL}
};
// Module definition for mcrfpy.automation
static PyModuleDef automationModule = {
PyModuleDef_HEAD_INIT,
"mcrfpy.automation",
"Automation API for McRogueFace - PyAutoGUI-compatible interface",
-1,
automationMethods
};
// Initialize automation submodule
PyObject* McRFPy_Automation::init_automation_module() {
PyObject* module = PyModule_Create(&automationModule);
if (module == NULL) {
return NULL;
}
return module;
}

56
src/McRFPy_Automation.h Normal file
View File

@ -0,0 +1,56 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <SFML/Graphics.hpp>
#include <SFML/Window.hpp>
#include <string>
#include <chrono>
#include <thread>
class GameEngine;
class McRFPy_Automation {
public:
// Initialize the automation submodule
static PyObject* init_automation_module();
// Screenshot functionality
static PyObject* _screenshot(PyObject* self, PyObject* args);
// Mouse position and screen info
static PyObject* _position(PyObject* self, PyObject* args);
static PyObject* _size(PyObject* self, PyObject* args);
static PyObject* _onScreen(PyObject* self, PyObject* args);
// Mouse movement
static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _moveRel(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _dragTo(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _dragRel(PyObject* self, PyObject* args, PyObject* kwargs);
// Mouse clicks
static PyObject* _click(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _rightClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _middleClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _doubleClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _tripleClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _scroll(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _mouseDown(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _mouseUp(PyObject* self, PyObject* args, PyObject* kwargs);
// Keyboard
static PyObject* _typewrite(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _hotkey(PyObject* self, PyObject* args);
static PyObject* _keyDown(PyObject* self, PyObject* args);
static PyObject* _keyUp(PyObject* self, PyObject* args);
// Helper functions
static void injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button = sf::Mouse::Left);
static void injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key);
static void injectTextEvent(sf::Uint32 unicode);
static sf::Keyboard::Key stringToKey(const std::string& keyName);
static void sleep_ms(int milliseconds);
private:
static GameEngine* getGameEngine();
};

33
src/McRogueFaceConfig.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef MCROGUEFACE_CONFIG_H
#define MCROGUEFACE_CONFIG_H
#include <string>
#include <vector>
#include <filesystem>
struct McRogueFaceConfig {
// McRogueFace specific
bool headless = false;
bool audio_enabled = true;
// Python interpreter emulation
bool python_mode = false;
std::string python_command; // -c command
std::string python_module; // -m module
bool interactive_mode = false; // -i flag
bool show_version = false; // -V flag
bool show_help = false; // -h flag
// Script execution
std::filesystem::path script_path;
std::vector<std::string> script_args;
// Scripts to execute before main script (--exec flag)
std::vector<std::filesystem::path> exec_scripts;
// Screenshot functionality for headless mode
std::string screenshot_path;
bool take_screenshot = false;
};
#endif // MCROGUEFACE_CONFIG_H

234
src/PyAnimation.cpp Normal file
View File

@ -0,0 +1,234 @@
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "UIDrawable.h"
#include "UIFrame.h"
#include "UICaption.h"
#include "UISprite.h"
#include "UIGrid.h"
#include "UIEntity.h"
#include "UI.h" // For the PyTypeObject definitions
#include <cstring>
PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyAnimationObject* self = (PyAnimationObject*)type->tp_alloc(type, 0);
if (self != NULL) {
// Will be initialized in init
}
return (PyObject*)self;
}
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
const char* easing_name = "linear";
int delta = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta)) {
return -1;
}
// Convert Python target value to AnimationValue
AnimationValue animValue;
if (PyFloat_Check(target_value)) {
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
}
else if (PyLong_Check(target_value)) {
animValue = static_cast<int>(PyLong_AsLong(target_value));
}
else if (PyList_Check(target_value)) {
// List of integers for sprite animation
std::vector<int> indices;
Py_ssize_t size = PyList_Size(target_value);
for (Py_ssize_t i = 0; i < size; i++) {
PyObject* item = PyList_GetItem(target_value, i);
if (PyLong_Check(item)) {
indices.push_back(PyLong_AsLong(item));
} else {
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
return -1;
}
}
animValue = indices;
}
else if (PyTuple_Check(target_value)) {
Py_ssize_t size = PyTuple_Size(target_value);
if (size == 2) {
// Vector2f
float x = PyFloat_AsDouble(PyTuple_GetItem(target_value, 0));
float y = PyFloat_AsDouble(PyTuple_GetItem(target_value, 1));
animValue = sf::Vector2f(x, y);
}
else if (size == 3 || size == 4) {
// Color (RGB or RGBA)
int r = PyLong_AsLong(PyTuple_GetItem(target_value, 0));
int g = PyLong_AsLong(PyTuple_GetItem(target_value, 1));
int b = PyLong_AsLong(PyTuple_GetItem(target_value, 2));
int a = size == 4 ? PyLong_AsLong(PyTuple_GetItem(target_value, 3)) : 255;
animValue = sf::Color(r, g, b, a);
}
else {
PyErr_SetString(PyExc_ValueError, "Tuple must have 2 elements (vector) or 3-4 elements (color)");
return -1;
}
}
else if (PyUnicode_Check(target_value)) {
// String for text animation
const char* str = PyUnicode_AsUTF8(target_value);
animValue = std::string(str);
}
else {
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
return -1;
}
// Get easing function
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
// Create the Animation
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0);
return 0;
}
void PyAnimation::dealloc(PyAnimationObject* self) {
self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyAnimation::get_property(PyAnimationObject* self, void* closure) {
return PyUnicode_FromString(self->data->getTargetProperty().c_str());
}
PyObject* PyAnimation::get_duration(PyAnimationObject* self, void* closure) {
return PyFloat_FromDouble(self->data->getDuration());
}
PyObject* PyAnimation::get_elapsed(PyAnimationObject* self, void* closure) {
return PyFloat_FromDouble(self->data->getElapsed());
}
PyObject* PyAnimation::get_is_complete(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isComplete());
}
PyObject* PyAnimation::get_is_delta(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isDelta());
}
PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
PyObject* target_obj;
if (!PyArg_ParseTuple(args, "O", &target_obj)) {
return NULL;
}
// Get the UIDrawable from the Python object
UIDrawable* drawable = nullptr;
// Check type by comparing type names
const char* type_name = Py_TYPE(target_obj)->tp_name;
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
drawable = frame->data.get();
}
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
drawable = caption->data.get();
}
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
drawable = sprite->data.get();
}
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
drawable = grid->data.get();
}
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
// Special handling for Entity since it doesn't inherit from UIDrawable
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
// Start the animation directly on the entity
self->data->startEntity(entity->data.get());
// Add to AnimationManager
AnimationManager::getInstance().addAnimation(self->data);
Py_RETURN_NONE;
}
else {
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
return NULL;
}
// Start the animation
self->data->start(drawable);
// Add to AnimationManager
AnimationManager::getInstance().addAnimation(self->data);
Py_RETURN_NONE;
}
PyObject* PyAnimation::update(PyAnimationObject* self, PyObject* args) {
float deltaTime;
if (!PyArg_ParseTuple(args, "f", &deltaTime)) {
return NULL;
}
bool still_running = self->data->update(deltaTime);
return PyBool_FromLong(still_running);
}
PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args) {
AnimationValue value = self->data->getCurrentValue();
// Convert AnimationValue back to Python
return std::visit([](const auto& val) -> PyObject* {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
return PyFloat_FromDouble(val);
}
else if constexpr (std::is_same_v<T, int>) {
return PyLong_FromLong(val);
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// This shouldn't happen as we interpolate to int
return PyLong_FromLong(0);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
return Py_BuildValue("(iiii)", val.r, val.g, val.b, val.a);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
return Py_BuildValue("(ff)", val.x, val.y);
}
else if constexpr (std::is_same_v<T, std::string>) {
return PyUnicode_FromString(val.c_str());
}
Py_RETURN_NONE;
}, value);
}
PyGetSetDef PyAnimation::getsetters[] = {
{"property", (getter)get_property, NULL, "Target property name", NULL},
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
{"elapsed", (getter)get_elapsed, NULL, "Elapsed time in seconds", NULL},
{"is_complete", (getter)get_is_complete, NULL, "Whether animation is complete", NULL},
{"is_delta", (getter)get_is_delta, NULL, "Whether animation uses delta mode", NULL},
{NULL}
};
PyMethodDef PyAnimation::methods[] = {
{"start", (PyCFunction)start, METH_VARARGS,
"Start the animation on a target UIDrawable"},
{"update", (PyCFunction)update, METH_VARARGS,
"Update the animation by deltaTime (returns True if still running)"},
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
"Get the current interpolated value"},
{NULL}
};

50
src/PyAnimation.h Normal file
View File

@ -0,0 +1,50 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include "Animation.h"
#include <memory>
typedef struct {
PyObject_HEAD
std::shared_ptr<Animation> data;
} PyAnimationObject;
class PyAnimation {
public:
static PyObject* create(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int init(PyAnimationObject* self, PyObject* args, PyObject* kwds);
static void dealloc(PyAnimationObject* self);
// Properties
static PyObject* get_property(PyAnimationObject* self, void* closure);
static PyObject* get_duration(PyAnimationObject* self, void* closure);
static PyObject* get_elapsed(PyAnimationObject* self, void* closure);
static PyObject* get_is_complete(PyAnimationObject* self, void* closure);
static PyObject* get_is_delta(PyAnimationObject* self, void* closure);
// Methods
static PyObject* start(PyAnimationObject* self, PyObject* args);
static PyObject* update(PyAnimationObject* self, PyObject* args);
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PyAnimationType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Animation",
.tp_basicsize = sizeof(PyAnimationObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyAnimation::dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Animation object for animating UI properties"),
.tp_methods = PyAnimation::methods,
.tp_getset = PyAnimation::getsetters,
.tp_init = (initproc)PyAnimation::init,
.tp_new = PyAnimation::create,
};
}

View File

@ -2,6 +2,7 @@
#include "ActionCode.h"
#include "Resources.h"
#include "PyCallable.h"
#include <algorithm>
PyScene::PyScene(GameEngine* g) : Scene(g)
{
@ -21,6 +22,11 @@ void PyScene::update()
void PyScene::do_mouse_input(std::string button, std::string type)
{
// In headless mode, mouse input is not available
if (game->isHeadless()) {
return;
}
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos);
UIDrawable* target;
@ -49,10 +55,7 @@ void PyScene::do_mouse_input(std::string button, std::string type)
void PyScene::doAction(std::string name, std::string type)
{
if (ACTIONPY) {
McRFPy_API::doAction(name.substr(0, name.size() - 3));
}
else if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) {
if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) {
do_mouse_input(name, type);
}
else if ACTIONONCE("debug_menu") {
@ -62,14 +65,23 @@ void PyScene::doAction(std::string name, std::string type)
void PyScene::render()
{
game->getWindow().clear();
game->getRenderTarget().clear();
auto vec = *ui_elements;
for (auto e: vec)
// Only sort if z_index values have changed
if (ui_elements_need_sort) {
std::sort(ui_elements->begin(), ui_elements->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
ui_elements_need_sort = false;
}
// Render in sorted order (no need to copy anymore)
for (auto e: *ui_elements)
{
if (e)
e->render();
}
game->getWindow().display();
// Display is handled by GameEngine
}

View File

@ -14,4 +14,7 @@ public:
void render() override final;
void do_mouse_input(std::string, std::string);
// Dirty flag for z_index sorting optimization
bool ui_elements_need_sort = true;
};

View File

@ -19,6 +19,7 @@ public:
int sprite_width, sprite_height; // just use them read only, OK?
PyTexture(std::string filename, int sprite_w, int sprite_h);
sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0));
int getSpriteCount() const { return sheet_width * sheet_height; }
PyObject* pyObject();
static PyObject* repr(PyObject*);

View File

@ -106,13 +106,37 @@ PyObject* PyVector::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
PyObject* PyVector::get_member(PyObject* obj, void* closure)
{
// TODO
return Py_None;
PyVectorObject* self = (PyVectorObject*)obj;
if (reinterpret_cast<long>(closure) == 0) {
// x
return PyFloat_FromDouble(self->data.x);
} else {
// y
return PyFloat_FromDouble(self->data.y);
}
}
int PyVector::set_member(PyObject* obj, PyObject* value, void* closure)
{
// TODO
PyVectorObject* self = (PyVectorObject*)obj;
float val;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = PyLong_AsDouble(value);
} else {
PyErr_SetString(PyExc_TypeError, "Vector members must be numeric");
return -1;
}
if (reinterpret_cast<long>(closure) == 0) {
// x
self->data.x = val;
} else {
// y
self->data.y = val;
}
return 0;
}
@ -120,11 +144,31 @@ PyVectorObject* PyVector::from_arg(PyObject* args)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (PyObject_IsInstance(args, (PyObject*)type)) return (PyVectorObject*)args;
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
// Handle different input types
if (PyTuple_Check(args)) {
// It's already a tuple, pass it directly to init
int err = init(obj, args, NULL);
if (err) {
Py_DECREF(obj);
return NULL;
}
} else {
// Wrap single argument in a tuple for init
PyObject* tuple = PyTuple_Pack(1, args);
if (!tuple) {
Py_DECREF(obj);
return NULL;
}
int err = init(obj, tuple, NULL);
Py_DECREF(tuple);
if (err) {
Py_DECREF(obj);
return NULL;
}
}
return obj;
}

View File

@ -30,16 +30,6 @@ std::string Scene::action(int code)
return actions[code];
}
bool Scene::registerActionInjected(int code, std::string name)
{
std::cout << "Inject registered action - default implementation\n";
return false;
}
bool Scene::unregisterActionInjected(int code, std::string name)
{
return false;
}
void Scene::key_register(PyObject* callable)
{

View File

@ -4,7 +4,6 @@
#define ACTION(X, Y) (name.compare(X) == 0 && type.compare(Y) == 0)
#define ACTIONONCE(X) ((name.compare(X) == 0 && type.compare("start") == 0 && !actionState[name]))
#define ACTIONAFTER(X) ((name.compare(X) == 0 && type.compare("end") == 0))
#define ACTIONPY ((name.size() > 3 && name.compare(name.size() - 3, 3, "_py") == 0))
#include "Common.h"
#include <list>
@ -37,8 +36,6 @@ public:
bool hasAction(int);
std::string action(int);
virtual bool registerActionInjected(int, std::string);
virtual bool unregisterActionInjected(int, std::string);
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;

View File

@ -3,6 +3,7 @@
#include "PyColor.h"
#include "PyVector.h"
#include "PyFont.h"
#include <algorithm>
UIDrawable* UICaption::click_at(sf::Vector2f point)
{
@ -198,6 +199,7 @@ PyGetSetDef UICaption::getsetters[] = {
{"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL},
{"size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Text 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},
{NULL}
};
@ -234,7 +236,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf",
// const_cast<char**>(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|zOOOf",
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oz|OOOf",
const_cast<char**>(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline))
{
return -1;
@ -250,10 +252,10 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
// check types for font, fill_color, outline_color
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
if (font != NULL && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance");
if (font != NULL && font != Py_None && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance or None");
return -1;
} else if (font != NULL)
} else if (font != NULL && font != Py_None)
{
auto font_obj = (PyFontObject*)font;
self->data->text.setFont(font_obj->data->font);
@ -261,8 +263,16 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
Py_INCREF(font);
} else
{
// default font
//self->data->text.setFont(Resources::game->getFont());
// Use default font when None or not provided
if (McRFPy_API::default_font) {
self->data->text.setFont(McRFPy_API::default_font->font);
// Store reference to default font
PyObject* default_font_obj = PyObject_GetAttrString(McRFPy_API::mcrf_module, "default_font");
if (default_font_obj) {
self->font = default_font_obj;
// Don't need to DECREF since we're storing it
}
}
}
self->data->text.setString((std::string)text);
@ -294,3 +304,172 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
return 0;
}
// Property system implementation for animations
bool UICaption::setProperty(const std::string& name, float value) {
if (name == "x") {
text.setPosition(sf::Vector2f(value, text.getPosition().y));
return true;
}
else if (name == "y") {
text.setPosition(sf::Vector2f(text.getPosition().x, value));
return true;
}
else if (name == "size") {
text.setCharacterSize(static_cast<unsigned int>(value));
return true;
}
else if (name == "outline") {
text.setOutlineThickness(value);
return true;
}
else if (name == "fill_color.r") {
auto color = text.getFillColor();
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
return true;
}
else if (name == "fill_color.g") {
auto color = text.getFillColor();
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
return true;
}
else if (name == "fill_color.b") {
auto color = text.getFillColor();
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
return true;
}
else if (name == "fill_color.a") {
auto color = text.getFillColor();
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
return true;
}
else if (name == "outline_color.r") {
auto color = text.getOutlineColor();
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
return true;
}
else if (name == "outline_color.g") {
auto color = text.getOutlineColor();
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
return true;
}
else if (name == "outline_color.b") {
auto color = text.getOutlineColor();
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
return true;
}
else if (name == "outline_color.a") {
auto color = text.getOutlineColor();
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
return true;
}
else if (name == "z_index") {
z_index = static_cast<int>(value);
return true;
}
return false;
}
bool UICaption::setProperty(const std::string& name, const sf::Color& value) {
if (name == "fill_color") {
text.setFillColor(value);
return true;
}
else if (name == "outline_color") {
text.setOutlineColor(value);
return true;
}
return false;
}
bool UICaption::setProperty(const std::string& name, const std::string& value) {
if (name == "text") {
text.setString(value);
return true;
}
return false;
}
bool UICaption::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = text.getPosition().x;
return true;
}
else if (name == "y") {
value = text.getPosition().y;
return true;
}
else if (name == "size") {
value = static_cast<float>(text.getCharacterSize());
return true;
}
else if (name == "outline") {
value = text.getOutlineThickness();
return true;
}
else if (name == "fill_color.r") {
value = text.getFillColor().r;
return true;
}
else if (name == "fill_color.g") {
value = text.getFillColor().g;
return true;
}
else if (name == "fill_color.b") {
value = text.getFillColor().b;
return true;
}
else if (name == "fill_color.a") {
value = text.getFillColor().a;
return true;
}
else if (name == "outline_color.r") {
value = text.getOutlineColor().r;
return true;
}
else if (name == "outline_color.g") {
value = text.getOutlineColor().g;
return true;
}
else if (name == "outline_color.b") {
value = text.getOutlineColor().b;
return true;
}
else if (name == "outline_color.a") {
value = text.getOutlineColor().a;
return true;
}
else if (name == "z_index") {
value = static_cast<float>(z_index);
return true;
}
return false;
}
bool UICaption::getProperty(const std::string& name, sf::Color& value) const {
if (name == "fill_color") {
value = text.getFillColor();
return true;
}
else if (name == "outline_color") {
value = text.getOutlineColor();
return true;
}
return false;
}
bool UICaption::getProperty(const std::string& name, std::string& value) const {
if (name == "text") {
value = text.getString();
return true;
}
return false;
}

View File

@ -11,6 +11,15 @@ public:
PyObjectsEnum derived_type() override final;
virtual UIDrawable* click_at(sf::Vector2f point) override final;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Color& value) override;
bool setProperty(const std::string& name, const std::string& value) override;
bool getProperty(const std::string& name, float& value) const override;
bool getProperty(const std::string& name, sf::Color& value) const override;
bool getProperty(const std::string& name, std::string& value) const override;
static PyObject* get_float_member(PyUICaptionObject* self, void* closure);
static int set_float_member(PyUICaptionObject* self, PyObject* value, void* closure);
static PyObject* get_vec_member(PyUICaptionObject* self, void* closure);

View File

@ -6,6 +6,8 @@
#include "UIGrid.h"
#include "McRFPy_API.h"
#include "PyObjectUtils.h"
#include <climits>
#include <algorithm>
using namespace mcrfpydef;
@ -148,15 +150,394 @@ PyObject* UICollection::getitem(PyUICollectionObject* self, Py_ssize_t index) {
}
int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject* value) {
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return -1;
}
// Handle negative indexing
while (index < 0) index += self->data->size();
// Bounds check
if (index >= self->data->size()) {
PyErr_SetString(PyExc_IndexError, "UICollection assignment index out of range");
return -1;
}
// Handle deletion
if (value == NULL) {
self->data->erase(self->data->begin() + index);
return 0;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "UICollection can only contain Frame, Caption, Sprite, and Grid objects");
return -1;
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> new_drawable = nullptr;
int old_z_index = (*vec)[index]->z_index; // Preserve the z_index
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
PyUIFrameObject* frame = (PyUIFrameObject*)value;
new_drawable = frame->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
PyUICaptionObject* caption = (PyUICaptionObject*)value;
new_drawable = caption->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
PyUISpriteObject* sprite = (PyUISpriteObject*)value;
new_drawable = sprite->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyUIGridObject* grid = (PyUIGridObject*)value;
new_drawable = grid->data;
}
if (!new_drawable) {
PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object");
return -1;
}
// Preserve the z_index of the replaced element
new_drawable->z_index = old_z_index;
// Replace the element
(*vec)[index] = new_drawable;
// Mark scene as needing resort after replacing element
McRFPy_API::markSceneNeedsSort();
return 0;
}
int UICollection::contains(PyUICollectionObject* self, PyObject* value) {
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return -1;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
// Not a valid type, so it can't be in the collection
return 0;
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> search_drawable = nullptr;
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
PyUIFrameObject* frame = (PyUIFrameObject*)value;
search_drawable = frame->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
PyUICaptionObject* caption = (PyUICaptionObject*)value;
search_drawable = caption->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
PyUISpriteObject* sprite = (PyUISpriteObject*)value;
search_drawable = sprite->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyUIGridObject* grid = (PyUIGridObject*)value;
search_drawable = grid->data;
}
if (!search_drawable) {
return 0;
}
// Search for the object by comparing C++ pointers
for (const auto& drawable : *vec) {
if (drawable.get() == search_drawable.get()) {
return 1; // Found
}
}
return 0; // Not found
}
PyObject* UICollection::concat(PyUICollectionObject* 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 UICollection");
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
for (Py_ssize_t i = 0; i < self_len; i++) {
PyObject* item = convertDrawableToPython((*self->data)[i]);
if (!item) {
Py_DECREF(result_list);
return NULL;
}
PyList_SET_ITEM(result_list, i, item); // Steals reference
}
// 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* UICollection::inplace_concat(PyUICollectionObject* self, PyObject* other) {
if (!PySequence_Check(other)) {
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to UICollection");
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, "Frame")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
Py_DECREF(item);
PyErr_Format(PyExc_TypeError,
"UICollection can only contain Frame, Caption, Sprite, and Grid 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 z_index assignment
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;
}
PyObject* UICollection::subscript(PyUICollectionObject* self, PyObject* key) {
if (PyLong_Check(key)) {
// Single index - delegate to sq_item
Py_ssize_t index = PyLong_AsSsize_t(key);
if (index == -1 && PyErr_Occurred()) {
return NULL;
}
return getitem(self, index);
} else if (PySlice_Check(key)) {
// Handle slice
Py_ssize_t start, stop, step, slicelength;
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
return NULL;
}
PyObject* result_list = PyList_New(slicelength);
if (!result_list) {
return NULL;
}
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
PyObject* item = convertDrawableToPython((*self->data)[cur]);
if (!item) {
Py_DECREF(result_list);
return NULL;
}
PyList_SET_ITEM(result_list, i, item); // Steals reference
}
return result_list;
} else {
PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s",
Py_TYPE(key)->tp_name);
return NULL;
}
}
int UICollection::ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value) {
if (PyLong_Check(key)) {
// Single index - delegate to sq_ass_item
Py_ssize_t index = PyLong_AsSsize_t(key);
if (index == -1 && PyErr_Occurred()) {
return -1;
}
return setitem(self, index, value);
} else if (PySlice_Check(key)) {
// Handle slice assignment/deletion
Py_ssize_t start, stop, step, slicelength;
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
return -1;
}
if (value == NULL) {
// Deletion
if (step != 1) {
// For non-contiguous slices, delete from highest to lowest to maintain indices
std::vector<Py_ssize_t> indices;
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
indices.push_back(cur);
}
// Sort in descending order and delete
std::sort(indices.begin(), indices.end(), std::greater<Py_ssize_t>());
for (Py_ssize_t idx : indices) {
self->data->erase(self->data->begin() + idx);
}
} else {
// Contiguous slice - can delete in one go
self->data->erase(self->data->begin() + start, self->data->begin() + stop);
}
// Mark scene as needing resort after slice deletion
McRFPy_API::markSceneNeedsSort();
return 0;
} else {
// Assignment
if (!PySequence_Check(value)) {
PyErr_SetString(PyExc_TypeError, "can only assign sequence to slice");
return -1;
}
Py_ssize_t value_len = PySequence_Length(value);
if (value_len == -1) {
return -1;
}
// Validate all items first
std::vector<std::shared_ptr<UIDrawable>> new_items;
for (Py_ssize_t i = 0; i < value_len; i++) {
PyObject* item = PySequence_GetItem(value, i);
if (!item) {
return -1;
}
// Type check and extract C++ object
std::shared_ptr<UIDrawable> drawable = nullptr;
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
drawable = ((PyUIFrameObject*)item)->data;
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
drawable = ((PyUICaptionObject*)item)->data;
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
drawable = ((PyUISpriteObject*)item)->data;
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
drawable = ((PyUIGridObject*)item)->data;
} else {
Py_DECREF(item);
PyErr_Format(PyExc_TypeError,
"UICollection can only contain Frame, Caption, Sprite, and Grid objects; "
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
return -1;
}
Py_DECREF(item);
new_items.push_back(drawable);
}
// Now perform the assignment
if (step == 1) {
// Contiguous slice
if (slicelength != value_len) {
// Need to resize
auto it_start = self->data->begin() + start;
auto it_stop = self->data->begin() + stop;
self->data->erase(it_start, it_stop);
self->data->insert(self->data->begin() + start, new_items.begin(), new_items.end());
} else {
// Same size, just replace
for (Py_ssize_t i = 0; i < slicelength; i++) {
// Preserve z_index
new_items[i]->z_index = (*self->data)[start + i]->z_index;
(*self->data)[start + i] = new_items[i];
}
}
} else {
// Extended slice
if (slicelength != value_len) {
PyErr_Format(PyExc_ValueError,
"attempt to assign sequence of size %zd to extended slice of size %zd",
value_len, slicelength);
return -1;
}
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
// Preserve z_index
new_items[i]->z_index = (*self->data)[cur]->z_index;
(*self->data)[cur] = new_items[i];
}
}
// Mark scene as needing resort after slice assignment
McRFPy_API::markSceneNeedsSort();
return 0;
}
} else {
PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s",
Py_TYPE(key)->tp_name);
return -1;
}
}
PyMappingMethods UICollection::mpmethods = {
.mp_length = (lenfunc)UICollection::len,
.mp_subscript = (binaryfunc)UICollection::subscript,
.mp_ass_subscript = (objobjargproc)UICollection::ass_subscript
};
PySequenceMethods UICollection::sqmethods = {
.sq_length = (lenfunc)UICollection::len,
.sq_concat = (binaryfunc)UICollection::concat,
.sq_repeat = NULL,
.sq_item = (ssizeargfunc)UICollection::getitem,
//.sq_item_by_index = PyUICollection_getitem
//.sq_slice - return a subset of the iterable
//.sq_ass_item - called when `o[x] = y` is executed (x is any object type)
//.sq_ass_slice - cool; no thanks, for now
//.sq_contains - called when `x in o` is executed
//.sq_ass_item_by_index - called when `o[x] = y` is executed (x is explictly an integer)
.was_sq_slice = NULL,
.sq_ass_item = (ssizeobjargproc)UICollection::setitem,
.was_sq_ass_slice = NULL,
.sq_contains = (objobjproc)UICollection::contains,
.sq_inplace_concat = (binaryfunc)UICollection::inplace_concat,
.sq_inplace_repeat = NULL
};
/* Idiomatic way to fetch complete types from the API rather than referencing their PyTypeObject struct
@ -173,6 +554,12 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
// if not UIDrawable subclass, reject it
// self->data->push_back( c++ object inside o );
// Ensure module is initialized
if (!McRFPy_API::mcrf_module) {
PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized");
return NULL;
}
// this would be a great use case for .tp_base
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
@ -184,27 +571,46 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
return NULL;
}
// Calculate z_index for the new element
int new_z_index = 0;
if (!self->data->empty()) {
// Get the z_index of the last element and add 10
int last_z = self->data->back()->z_index;
if (last_z <= INT_MAX - 10) {
new_z_index = last_z + 10;
} else {
new_z_index = INT_MAX;
}
}
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")))
{
PyUIFrameObject* frame = (PyUIFrameObject*)o;
frame->data->z_index = new_z_index;
self->data->push_back(frame->data);
}
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")))
{
PyUICaptionObject* caption = (PyUICaptionObject*)o;
caption->data->z_index = new_z_index;
self->data->push_back(caption->data);
}
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")))
{
PyUISpriteObject* sprite = (PyUISpriteObject*)o;
sprite->data->z_index = new_z_index;
self->data->push_back(sprite->data);
}
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
{
PyUIGridObject* grid = (PyUIGridObject*)o;
grid->data->z_index = new_z_index;
self->data->push_back(grid->data);
}
// Mark scene as needing resort after adding element
McRFPy_API::markSceneNeedsSort();
Py_INCREF(Py_None);
return Py_None;
}
@ -217,27 +623,121 @@ PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
return NULL;
}
long index = PyLong_AsLong(o);
// Handle negative indexing
while (index < 0) index += self->data->size();
if (index >= self->data->size())
{
PyErr_SetString(PyExc_ValueError, "Index out of range");
return NULL;
}
else if (index < 0)
{
PyErr_SetString(PyExc_NotImplementedError, "reverse indexing is not implemented.");
return NULL;
}
// release the shared pointer at self->data[index];
self->data->erase(self->data->begin() + index);
// Mark scene as needing resort after removing element
McRFPy_API::markSceneNeedsSort();
Py_INCREF(Py_None);
return Py_None;
}
PyObject* UICollection::index_method(PyUICollectionObject* self, PyObject* value) {
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return NULL;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "UICollection.index requires a Frame, Caption, Sprite, or Grid object");
return NULL;
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> search_drawable = nullptr;
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
search_drawable = ((PyUIFrameObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
search_drawable = ((PyUICaptionObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
search_drawable = ((PyUISpriteObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
search_drawable = ((PyUIGridObject*)value)->data;
}
if (!search_drawable) {
PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object");
return NULL;
}
// Search for the object
for (size_t i = 0; i < vec->size(); i++) {
if ((*vec)[i].get() == search_drawable.get()) {
return PyLong_FromSsize_t(i);
}
}
PyErr_SetString(PyExc_ValueError, "value not in UICollection");
return NULL;
}
PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) {
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return NULL;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
// Not a valid type, so count is 0
return PyLong_FromLong(0);
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> search_drawable = nullptr;
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
search_drawable = ((PyUIFrameObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
search_drawable = ((PyUICaptionObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
search_drawable = ((PyUISpriteObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
search_drawable = ((PyUIGridObject*)value)->data;
}
if (!search_drawable) {
return PyLong_FromLong(0);
}
// Count occurrences
Py_ssize_t count = 0;
for (const auto& drawable : *vec) {
if (drawable.get() == search_drawable.get()) {
count++;
}
}
return PyLong_FromSsize_t(count);
}
PyMethodDef UICollection::methods[] = {
{"append", (PyCFunction)UICollection::append, METH_O},
//{"extend", (PyCFunction)PyUICollection_extend, METH_O}, // TODO
{"remove", (PyCFunction)UICollection::remove, METH_O},
{"index", (PyCFunction)UICollection::index_method, METH_O},
{"count", (PyCFunction)UICollection::count, METH_O},
{NULL, NULL, 0, NULL}
};

View File

@ -19,9 +19,18 @@ class UICollection
public:
static Py_ssize_t len(PyUICollectionObject* self);
static PyObject* getitem(PyUICollectionObject* self, Py_ssize_t index);
static int setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject* value);
static int contains(PyUICollectionObject* self, PyObject* value);
static PyObject* concat(PyUICollectionObject* self, PyObject* other);
static PyObject* inplace_concat(PyUICollectionObject* self, PyObject* other);
static PySequenceMethods sqmethods;
static PyMappingMethods mpmethods;
static PyObject* subscript(PyUICollectionObject* self, PyObject* key);
static int ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value);
static PyObject* append(PyUICollectionObject* self, PyObject* o);
static PyObject* remove(PyUICollectionObject* self, PyObject* o);
static PyObject* index_method(PyUICollectionObject* self, PyObject* value);
static PyObject* count(PyUICollectionObject* self, PyObject* value);
static PyMethodDef methods[];
static PyObject* repr(PyUICollectionObject* self);
static int init(PyUICollectionObject* self, PyObject* args, PyObject* kwds);
@ -71,6 +80,7 @@ namespace mcrfpydef {
},
.tp_repr = (reprfunc)UICollection::repr,
.tp_as_sequence = &UICollection::sqmethods,
.tp_as_mapping = &UICollection::mpmethods,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Iterable, indexable collection of UI objects"),
.tp_iter = (getiterfunc)UICollection::iter,

View File

@ -4,6 +4,7 @@
#include "UISprite.h"
#include "UIGrid.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
UIDrawable::UIDrawable() { click_callable = NULL; }
@ -14,7 +15,7 @@ void UIDrawable::click_unregister()
void UIDrawable::render()
{
render(sf::Vector2f(), Resources::game->getWindow());
render(sf::Vector2f(), Resources::game->getRenderTarget());
}
PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
@ -80,3 +81,85 @@ void UIDrawable::click_register(PyObject* callable)
{
click_callable = std::make_unique<PyClickCallable>(callable);
}
PyObject* UIDrawable::get_int(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 PyLong_FromLong(drawable->z_index);
}
int UIDrawable::set_int(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 (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
return -1;
}
long z = PyLong_AsLong(value);
if (z == -1 && PyErr_Occurred()) {
return -1;
}
// Clamp to int range
if (z < INT_MIN) z = INT_MIN;
if (z > INT_MAX) z = INT_MAX;
int old_z_index = drawable->z_index;
drawable->z_index = static_cast<int>(z);
// Notify of z_index change
if (old_z_index != drawable->z_index) {
drawable->notifyZIndexChanged();
}
return 0;
}
void UIDrawable::notifyZIndexChanged() {
// Mark the current scene as needing sort
// This works for elements in the scene's ui_elements collection
McRFPy_API::markSceneNeedsSort();
// TODO: In the future, we could add parent tracking to handle Frame children
// For now, Frame children will need manual sorting or collection modification
// to trigger a resort
}

View File

@ -42,6 +42,27 @@ public:
static PyObject* get_click(PyObject* self, void* closure);
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);
// Z-order for rendering (lower values rendered first, higher values on top)
int z_index = 0;
// Notification for z_index changes
void notifyZIndexChanged();
// Animation support
virtual bool setProperty(const std::string& name, float value) { return false; }
virtual bool setProperty(const std::string& name, int value) { return false; }
virtual bool setProperty(const std::string& name, const sf::Color& value) { return false; }
virtual bool setProperty(const std::string& name, const sf::Vector2f& value) { return false; }
virtual bool setProperty(const std::string& name, const std::string& value) { return false; }
virtual bool getProperty(const std::string& name, float& value) const { return false; }
virtual bool getProperty(const std::string& name, int& value) const { return false; }
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; }
};
typedef struct {

View File

@ -2,6 +2,8 @@
#include "UIGrid.h"
#include "McRFPy_API.h"
#include "PyObjectUtils.h"
#include "PyVector.h"
UIEntity::UIEntity() {} // this will not work lol. TODO remove default constructor by finding the shared pointer inits that use it
@ -34,6 +36,33 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
}
PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) {
// Check if entity has an associated grid
if (!self->data || !self->data->grid) {
PyErr_SetString(PyExc_RuntimeError, "Entity is not associated with a grid");
return NULL;
}
// Get the grid's entity collection
auto entities = self->data->grid->entities;
if (!entities) {
PyErr_SetString(PyExc_RuntimeError, "Grid has no entity collection");
return NULL;
}
// Find this entity in the collection
int index = 0;
for (auto it = entities->begin(); it != entities->end(); ++it, ++index) {
if (it->get() == self->data.get()) {
return PyLong_FromLong(index);
}
}
// Entity not found in its grid's collection
PyErr_SetString(PyExc_ValueError, "Entity not found in its grid's entity collection");
return NULL;
}
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
//static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr };
//float x = 0.0f, y = 0.0f, scale = 1.0f;
@ -46,7 +75,7 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O",
// const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &grid))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi|O",
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OiO",
const_cast<char**>(keywords), &pos, &texture, &sprite_index, &grid))
{
return -1;
@ -61,33 +90,37 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
// check types for texture
//
// Set Texture
// Set Texture - allow None or use default
//
if (texture != NULL && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
std::shared_ptr<PyTexture> texture_ptr = nullptr;
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1;
} /*else if (texture != NULL) // this section needs to go; texture isn't optional and isn't managed by the UI objects anymore
{
self->texture = texture;
Py_INCREF(texture);
} else
{
// default tex?
}*/
} else if (texture != NULL && texture != Py_None) {
auto pytexture = (PyTextureObject*)texture;
texture_ptr = pytexture->data;
} else {
// Use default texture when None or not provided
texture_ptr = McRFPy_API::default_texture;
}
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");
return -1;
}
auto pytexture = (PyTextureObject*)texture;
if (grid == NULL)
self->data = std::make_shared<UIEntity>();
else
self->data = std::make_shared<UIEntity>(*((PyUIGridObject*)grid)->data);
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
self->data->sprite = UISprite(pytexture->data, sprite_index, sf::Vector2f(0,0), 1.0);
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;
@ -104,28 +137,40 @@ PyObject* UIEntity::get_spritenumber(PyUIEntityObject* self, void* closure) {
return PyLong_FromDouble(self->data->sprite.getSpriteIndex());
}
PyObject* sfVector2f_to_PyObject(sf::Vector2f vector) {
return Py_BuildValue("(ff)", vector.x, vector.y);
PyObject* sfVector2f_to_PyObject(sf::Vector2f vec) {
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
if (obj) {
obj->data = vec;
}
return (PyObject*)obj;
}
PyObject* sfVector2i_to_PyObject(sf::Vector2i vector) {
return Py_BuildValue("(ii)", vector.x, vector.y);
PyObject* sfVector2i_to_PyObject(sf::Vector2i vec) {
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
if (obj) {
obj->data = sf::Vector2f(static_cast<float>(vec.x), static_cast<float>(vec.y));
}
return (PyObject*)obj;
}
sf::Vector2f PyObject_to_sfVector2f(PyObject* obj) {
float x, y;
if (!PyArg_ParseTuple(obj, "ff", &x, &y)) {
return sf::Vector2f(); // TODO / reconsider this default: Return default vector on parse error
PyVectorObject* vec = PyVector::from_arg(obj);
if (!vec) {
// PyVector::from_arg already set the error
return sf::Vector2f(0, 0);
}
return sf::Vector2f(x, y);
return vec->data;
}
sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) {
int x, y;
if (!PyArg_ParseTuple(obj, "ii", &x, &y)) {
return sf::Vector2i(); // TODO / reconsider this default: Return default vector on parse error
PyVectorObject* vec = PyVector::from_arg(obj);
if (!vec) {
// PyVector::from_arg already set the error
return sf::Vector2i(0, 0);
}
return sf::Vector2i(x, y);
return sf::Vector2i(static_cast<int>(vec->data.x), static_cast<int>(vec->data.y));
}
// TODO - deprecate / remove this helper
@ -161,9 +206,17 @@ PyObject* UIEntity::get_position(PyUIEntityObject* self, void* closure) {
int UIEntity::set_position(PyUIEntityObject* self, PyObject* value, void* closure) {
if (reinterpret_cast<long>(closure) == 0) {
self->data->position = PyObject_to_sfVector2f(value);
sf::Vector2f vec = PyObject_to_sfVector2f(value);
if (PyErr_Occurred()) {
return -1; // Error already set by PyObject_to_sfVector2f
}
self->data->position = vec;
} else {
self->data->collision_pos = PyObject_to_sfVector2i(value);
sf::Vector2i vec = PyObject_to_sfVector2i(value);
if (PyErr_Occurred()) {
return -1; // Error already set by PyObject_to_sfVector2i
}
self->data->collision_pos = vec;
}
return 0;
}
@ -189,6 +242,7 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl
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"},
{NULL, NULL, 0, NULL}
};
@ -211,3 +265,51 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) {
std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
}
// Property system implementation for animations
bool UIEntity::setProperty(const std::string& name, float value) {
if (name == "x") {
position.x = value;
collision_pos.x = static_cast<int>(value);
// Update sprite position based on grid position
// Note: This is a simplified version - actual grid-to-pixel conversion depends on grid properties
sprite.setPosition(sf::Vector2f(position.x, position.y));
return true;
}
else if (name == "y") {
position.y = value;
collision_pos.y = static_cast<int>(value);
// Update sprite position based on grid position
sprite.setPosition(sf::Vector2f(position.x, position.y));
return true;
}
else if (name == "sprite_scale") {
sprite.setScale(sf::Vector2f(value, value));
return true;
}
return false;
}
bool UIEntity::setProperty(const std::string& name, int value) {
if (name == "sprite_number") {
sprite.setSpriteIndex(value);
return true;
}
return false;
}
bool UIEntity::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = position.x;
return true;
}
else if (name == "y") {
value = position.y;
return true;
}
else if (name == "sprite_scale") {
value = sprite.getScale().x; // Assuming uniform scale
return true;
}
return false;
}

View File

@ -46,7 +46,13 @@ public:
UIEntity();
UIEntity(UIGrid&);
// Property system for animations
bool setProperty(const std::string& name, float value);
bool setProperty(const std::string& name, int value);
bool getProperty(const std::string& name, float& value) const;
static PyObject* at(PyUIEntityObject* self, PyObject* o);
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* get_position(PyUIEntityObject* self, void* closure);

View File

@ -51,6 +51,15 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
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);
}
@ -215,6 +224,7 @@ PyGetSetDef UIFrame::getsetters[] = {
{"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1},
{"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},
{NULL}
};
@ -264,3 +274,152 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
if (err_val) return err_val;
return 0;
}
// Animation property system implementation
bool UIFrame::setProperty(const std::string& name, float value) {
if (name == "x") {
box.setPosition(sf::Vector2f(value, box.getPosition().y));
return true;
} else if (name == "y") {
box.setPosition(sf::Vector2f(box.getPosition().x, value));
return true;
} else if (name == "w") {
box.setSize(sf::Vector2f(value, box.getSize().y));
return true;
} else if (name == "h") {
box.setSize(sf::Vector2f(box.getSize().x, value));
return true;
} else if (name == "outline") {
box.setOutlineThickness(value);
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);
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);
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);
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);
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);
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);
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);
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);
return true;
}
return false;
}
bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
if (name == "fill_color") {
box.setFillColor(value);
return true;
} else if (name == "outline_color") {
box.setOutlineColor(value);
return true;
}
return false;
}
bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "position") {
box.setPosition(value);
return true;
} else if (name == "size") {
box.setSize(value);
return true;
}
return false;
}
bool UIFrame::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = box.getPosition().x;
return true;
} else if (name == "y") {
value = box.getPosition().y;
return true;
} else if (name == "w") {
value = box.getSize().x;
return true;
} else if (name == "h") {
value = box.getSize().y;
return true;
} else if (name == "outline") {
value = box.getOutlineThickness();
return true;
} else if (name == "fill_color.r") {
value = box.getFillColor().r;
return true;
} else if (name == "fill_color.g") {
value = box.getFillColor().g;
return true;
} else if (name == "fill_color.b") {
value = box.getFillColor().b;
return true;
} else if (name == "fill_color.a") {
value = box.getFillColor().a;
return true;
} else if (name == "outline_color.r") {
value = box.getOutlineColor().r;
return true;
} else if (name == "outline_color.g") {
value = box.getOutlineColor().g;
return true;
} else if (name == "outline_color.b") {
value = box.getOutlineColor().b;
return true;
} else if (name == "outline_color.a") {
value = box.getOutlineColor().a;
return true;
}
return false;
}
bool UIFrame::getProperty(const std::string& name, sf::Color& value) const {
if (name == "fill_color") {
value = box.getFillColor();
return true;
} else if (name == "outline_color") {
value = box.getOutlineColor();
return true;
}
return false;
}
bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const {
if (name == "position") {
value = box.getPosition();
return true;
} else if (name == "size") {
value = box.getSize();
return true;
}
return false;
}

View File

@ -28,6 +28,7 @@ public:
sf::RectangleShape box;
float outline;
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
bool children_need_sort = true; // Dirty flag for z_index sorting optimization
void render(sf::Vector2f, sf::RenderTarget&) override final;
void move(sf::Vector2f);
PyObjectsEnum derived_type() override final;
@ -42,6 +43,15 @@ public:
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIFrameObject* self);
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);
// Animation property system
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Color& value) override;
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
bool getProperty(const std::string& name, float& value) const override;
bool getProperty(const std::string& name, sf::Color& value) const override;
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
};
namespace mcrfpydef {

View File

@ -1,14 +1,21 @@
#include "UIGrid.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include <algorithm>
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), center_x((gx/2) * _ptex->sprite_width), center_y((gy/2) * _ptex->sprite_height),
zoom(1.0f),
ptex(_ptex), points(gx * gy)
{
// Use texture dimensions if available, otherwise use defaults
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
int cell_height = _ptex ? _ptex->sprite_height : DEFAULT_CELL_HEIGHT;
center_x = (gx/2) * cell_width;
center_y = (gy/2) * cell_height;
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
box.setSize(_wh);
@ -18,7 +25,10 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
// create renderTexture with maximum theoretical size; sprite can resize to show whatever amount needs to be rendered
renderTexture.create(1920, 1080); // TODO - renderTexture should be window size; above 1080p this will cause rendering errors
// Only initialize sprite if texture is available
if (ptex) {
sprite = ptex->sprite(0);
}
output.setTextureRect(
sf::IntRect(0, 0,
@ -40,12 +50,17 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
sf::IntRect(0, 0,
box.getSize().x, box.getSize().y));
renderTexture.clear(sf::Color(8, 8, 8, 255)); // TODO - UIGrid needs a "background color" field
// sprites that are visible according to zoom, center_x, center_y, and box width
float center_x_sq = center_x / ptex->sprite_width;
float center_y_sq = center_y / ptex->sprite_height;
float width_sq = box.getSize().x / (ptex->sprite_width * zoom);
float height_sq = box.getSize().y / (ptex->sprite_height * zoom);
// Get cell dimensions - use texture if available, otherwise defaults
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
// sprites that are visible according to zoom, center_x, center_y, and box width
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);
float left_edge = center_x_sq - (width_sq / 2.0);
float top_edge = center_y_sq - (height_sq / 2.0);
@ -54,7 +69,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
//sprite.setScale(sf::Vector2f(zoom, zoom));
sf::RectangleShape r; // for colors and overlays
r.setSize(sf::Vector2f(ptex->sprite_width * zoom, ptex->sprite_height * zoom));
r.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
r.setOutlineThickness(0);
int x_limit = left_edge + width_sq + 2;
@ -74,8 +89,8 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
y+=1)
{
auto pixel_pos = sf::Vector2f(
(x*ptex->sprite_width - left_spritepixels) * zoom,
(y*ptex->sprite_height - top_spritepixels) * zoom );
(x*cell_width - left_spritepixels) * zoom,
(y*cell_height - top_spritepixels) * zoom );
auto gridpoint = at(std::floor(x), std::floor(y));
@ -85,10 +100,10 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
r.setFillColor(gridpoint.color);
renderTexture.draw(r);
// tilesprite
// tilesprite - only draw if texture is available
// if discovered but not visible, set opacity to 90%
// if not discovered... just don't draw it?
if (gridpoint.tilesprite != -1) {
if (ptex && gridpoint.tilesprite != -1) {
sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);;
renderTexture.draw(sprite);
}
@ -104,8 +119,8 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
//drawent.setScale(zoom, zoom);
drawent.setScale(sf::Vector2f(zoom, zoom));
auto pixel_pos = sf::Vector2f(
(e->position.x*ptex->sprite_width - left_spritepixels) * zoom,
(e->position.y*ptex->sprite_height - top_spritepixels) * zoom );
(e->position.x*cell_width - left_spritepixels) * zoom,
(e->position.y*cell_height - top_spritepixels) * zoom );
//drawent.setPosition(pixel_pos);
//renderTexture.draw(drawent);
drawent.render(pixel_pos, renderTexture);
@ -204,46 +219,92 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
int grid_x, grid_y;
PyObject* textureObj;
PyObject* textureObj = Py_None;
//float box_x, box_y, box_w, box_h;
PyObject* pos, *size;
PyObject* pos = NULL;
PyObject* size = NULL;
//if (!PyArg_ParseTuple(args, "iiOffff", &grid_x, &grid_y, &textureObj, &box_x, &box_y, &box_w, &box_h)) {
if (!PyArg_ParseTuple(args, "iiOOO", &grid_x, &grid_y, &textureObj, &pos, &size)) {
if (!PyArg_ParseTuple(args, "ii|OOO", &grid_x, &grid_y, &textureObj, &pos, &size)) {
return -1; // If parsing fails, return an error
}
PyVectorObject* pos_result = PyVector::from_arg(pos);
// Default position and size if not provided
PyVectorObject* pos_result = NULL;
PyVectorObject* size_result = NULL;
if (pos) {
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;
}
} else {
// Default position (0, 0)
PyObject* vector_class = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_class) {
PyObject* pos_obj = PyObject_CallFunction(vector_class, "ff", 0.0f, 0.0f);
Py_DECREF(vector_class);
if (pos_obj) {
pos_result = (PyVectorObject*)pos_obj;
}
}
if (!pos_result) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create default position vector");
return -1;
}
}
PyVectorObject* size_result = PyVector::from_arg(size);
if (size) {
size_result = PyVector::from_arg(size);
if (!size_result)
{
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
PyErr_SetString(PyExc_TypeError, "size must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
}
} else {
// Default size based on grid dimensions
float default_w = grid_x * 16.0f; // Assuming 16 pixel tiles
float default_h = grid_y * 16.0f;
PyObject* vector_class = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_class) {
PyObject* size_obj = PyObject_CallFunction(vector_class, "ff", default_w, default_h);
Py_DECREF(vector_class);
if (size_obj) {
size_result = (PyVectorObject*)size_obj;
}
}
if (!size_result) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create default size vector");
return -1;
}
}
// Convert PyObject texture to IndexTexture*
// This requires the texture object to have been initialized similar to UISprite's texture handling
std::shared_ptr<PyTexture> texture_ptr = nullptr;
// Allow None for texture - use default texture in that case
if (textureObj != Py_None) {
//if (!PyObject_IsInstance(textureObj, (PyObject*)&PyTextureType)) {
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1;
}
PyTextureObject* pyTexture = reinterpret_cast<PyTextureObject*>(textureObj);
// TODO (7DRL day 2, item 4.) use shared_ptr / PyTextureObject on UIGrid
//IndexTexture* texture = pyTexture->data.get();
texture_ptr = pyTexture->data;
} else {
// Use default texture when None is provided
texture_ptr = McRFPy_API::default_texture;
}
// Initialize UIGrid
// Initialize UIGrid - texture_ptr will be nullptr if texture was None
//self->data = new UIGrid(grid_x, grid_y, texture, sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h));
//self->data = std::make_shared<UIGrid>(grid_x, grid_y, pyTexture->data,
// sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h));
self->data = std::make_shared<UIGrid>(grid_x, grid_y, pyTexture->data, pos_result->data, size_result->data);
self->data = std::make_shared<UIGrid>(grid_x, grid_y, texture_ptr, pos_result->data, size_result->data);
return 0; // Success
}
@ -251,6 +312,14 @@ PyObject* UIGrid::get_grid_size(PyUIGridObject* self, void* closure) {
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
}
PyObject* UIGrid::get_grid_x(PyUIGridObject* self, void* closure) {
return PyLong_FromLong(self->data->grid_x);
}
PyObject* UIGrid::get_grid_y(PyUIGridObject* self, void* closure) {
return PyLong_FromLong(self->data->grid_y);
}
PyObject* UIGrid::get_position(PyUIGridObject* self, void* closure) {
auto& box = self->data->box;
return Py_BuildValue("(ff)", box.getPosition().x, box.getPosition().y);
@ -365,9 +434,16 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
//return self->data->getTexture()->pyObject();
// PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState")
//PyTextureObject* obj = (PyTextureObject*)((&PyTextureType)->tp_alloc(&PyTextureType, 0));
// Return None if no texture
auto texture = self->data->getTexture();
if (!texture) {
Py_RETURN_NONE;
}
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture");
auto obj = (PyTextureObject*)type->tp_alloc(type, 0);
obj->data = self->data->getTexture();
obj->data = texture;
return (PyObject*)obj;
}
@ -379,7 +455,7 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o)
return NULL;
}
if (x < 0 || x >= self->data->grid_x) {
PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_y)");
PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_x)");
return NULL;
}
if (y < 0 || y >= self->data->grid_y) {
@ -406,6 +482,8 @@ PyGetSetDef UIGrid::getsetters[] = {
// TODO - refactor into get_vector_member with field identifier values `(void*)n`
{"grid_size", (getter)UIGrid::get_grid_size, NULL, "Grid dimensions (grid_x, grid_y)", NULL},
{"grid_x", (getter)UIGrid::get_grid_x, NULL, "Grid x dimension", NULL},
{"grid_y", (getter)UIGrid::get_grid_y, NULL, "Grid y dimension", NULL},
{"position", (getter)UIGrid::get_position, (setter)UIGrid::set_position, "Position of the grid (x, y)", NULL},
{"size", (getter)UIGrid::get_size, (setter)UIGrid::set_size, "Size of the grid (width, height)", NULL},
{"center", (getter)UIGrid::get_center, (setter)UIGrid::set_center, "Grid coordinate at the center of the Grid's view (pan)", NULL},
@ -423,6 +501,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
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
{NULL} /* Sentinel */
};
@ -546,15 +625,196 @@ return NULL;
}
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,
.sq_concat = (binaryfunc)UIEntityCollection::concat,
.sq_repeat = NULL,
.sq_item = (ssizeargfunc)UIEntityCollection::getitem,
//.sq_item_by_index = UIEntityCollection::getitem
//.sq_slice - return a subset of the iterable
//.sq_ass_item - called when `o[x] = y` is executed (x is any object type)
//.sq_ass_slice - cool; no thanks, for now
//.sq_contains - called when `x in o` is executed
//.sq_ass_item_by_index - called when `o[x] = y` is executed (x is explictly an integer)
.was_sq_slice = NULL,
.sq_ass_item = (ssizeobjargproc)UIEntityCollection::setitem,
.was_sq_ass_slice = NULL,
.sq_contains = (objobjproc)UIEntityCollection::contains,
.sq_inplace_concat = (binaryfunc)UIEntityCollection::inplace_concat,
.sq_inplace_repeat = NULL
};
PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject* o)
@ -581,31 +841,340 @@ PyObject* UIEntityCollection::remove(PyUIEntityCollectionObject* self, PyObject*
{
if (!PyLong_Check(o))
{
PyErr_SetString(PyExc_TypeError, "UICollection.remove requires an integer index to remove");
PyErr_SetString(PyExc_TypeError, "EntityCollection.remove requires an integer index to remove");
return NULL;
}
long index = PyLong_AsLong(o);
// Handle negative indexing
while (index < 0) index += self->data->size();
if (index >= self->data->size())
{
PyErr_SetString(PyExc_ValueError, "Index out of range");
return NULL;
}
else if (index < 0)
{
PyErr_SetString(PyExc_NotImplementedError, "reverse indexing is not implemented.");
return NULL;
}
// Get iterator to the entity to remove
auto it = self->data->begin();
std::advance(it, index);
// Clear grid reference before removing
(*it)->grid = nullptr;
// release the shared pointer at correct part of the list
self->data->erase(std::next(self->data->begin(), index));
self->data->erase(it);
Py_INCREF(Py_None);
return Py_None;
}
PyObject* UIEntityCollection::extend(PyUIEntityCollectionObject* self, PyObject* o)
{
// Accept any iterable of Entity objects
PyObject* iterator = PyObject_GetIter(o);
if (iterator == NULL) {
PyErr_SetString(PyExc_TypeError, "UIEntityCollection.extend requires an iterable");
return NULL;
}
PyObject* item;
while ((item = PyIter_Next(iterator)) != NULL) {
// Check if item is an Entity
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
Py_DECREF(item);
Py_DECREF(iterator);
PyErr_SetString(PyExc_TypeError, "All items in iterable must be Entity objects");
return NULL;
}
// Add the entity to the collection
PyUIEntityObject* entity = (PyUIEntityObject*)item;
self->data->push_back(entity->data);
entity->data->grid = self->grid;
Py_DECREF(item);
}
Py_DECREF(iterator);
// Check if iteration ended due to an error
if (PyErr_Occurred()) {
return NULL;
}
Py_INCREF(Py_None);
return Py_None;
}
PyObject* UIEntityCollection::index_method(PyUIEntityCollectionObject* self, PyObject* value) {
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return NULL;
}
// Type checking - must be an Entity
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
PyErr_SetString(PyExc_TypeError, "EntityCollection.index requires an Entity object");
return NULL;
}
// Get the C++ object from the Python object
PyUIEntityObject* entity = (PyUIEntityObject*)value;
if (!entity->data) {
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
return NULL;
}
// Search for the object
Py_ssize_t idx = 0;
for (const auto& ent : *list) {
if (ent.get() == entity->data.get()) {
return PyLong_FromSsize_t(idx);
}
idx++;
}
PyErr_SetString(PyExc_ValueError, "Entity not in EntityCollection");
return NULL;
}
PyObject* UIEntityCollection::count(PyUIEntityCollectionObject* self, PyObject* value) {
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return NULL;
}
// Type checking - must be an Entity
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
// Not an Entity, so count is 0
return PyLong_FromLong(0);
}
// Get the C++ object from the Python object
PyUIEntityObject* entity = (PyUIEntityObject*)value;
if (!entity->data) {
return PyLong_FromLong(0);
}
// Count occurrences
Py_ssize_t count = 0;
for (const auto& ent : *list) {
if (ent.get() == entity->data.get()) {
count++;
}
}
return PyLong_FromSsize_t(count);
}
PyObject* UIEntityCollection::subscript(PyUIEntityCollectionObject* self, PyObject* key) {
if (PyLong_Check(key)) {
// Single index - delegate to sq_item
Py_ssize_t index = PyLong_AsSsize_t(key);
if (index == -1 && PyErr_Occurred()) {
return NULL;
}
return getitem(self, index);
} else if (PySlice_Check(key)) {
// Handle slice
Py_ssize_t start, stop, step, slicelength;
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
return NULL;
}
PyObject* result_list = PyList_New(slicelength);
if (!result_list) {
return NULL;
}
// Iterate through the list with slice parameters
auto it = self->data->begin();
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
auto cur_it = it;
std::advance(cur_it, cur);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0);
if (obj) {
obj->data = *cur_it;
PyList_SET_ITEM(result_list, i, (PyObject*)obj); // Steals reference
} else {
Py_DECREF(result_list);
Py_DECREF(type);
return NULL;
}
Py_DECREF(type);
}
return result_list;
} else {
PyErr_Format(PyExc_TypeError, "EntityCollection indices must be integers or slices, not %.200s",
Py_TYPE(key)->tp_name);
return NULL;
}
}
int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value) {
if (PyLong_Check(key)) {
// Single index - delegate to sq_ass_item
Py_ssize_t index = PyLong_AsSsize_t(key);
if (index == -1 && PyErr_Occurred()) {
return -1;
}
return setitem(self, index, value);
} else if (PySlice_Check(key)) {
// Handle slice assignment/deletion
Py_ssize_t start, stop, step, slicelength;
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
return -1;
}
if (value == NULL) {
// Deletion
if (step != 1) {
// For non-contiguous slices, delete from highest to lowest to maintain indices
std::vector<Py_ssize_t> indices;
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
indices.push_back(cur);
}
// Sort in descending order
std::sort(indices.begin(), indices.end(), std::greater<Py_ssize_t>());
// Delete each index
for (Py_ssize_t idx : indices) {
auto it = self->data->begin();
std::advance(it, idx);
(*it)->grid = nullptr; // Clear grid reference
self->data->erase(it);
}
} else {
// Contiguous slice - delete range
auto it_start = self->data->begin();
auto it_stop = self->data->begin();
std::advance(it_start, start);
std::advance(it_stop, stop);
// Clear grid references
for (auto it = it_start; it != it_stop; ++it) {
(*it)->grid = nullptr;
}
self->data->erase(it_start, it_stop);
}
return 0;
} else {
// Assignment
if (!PySequence_Check(value)) {
PyErr_SetString(PyExc_TypeError, "can only assign sequence to slice");
return -1;
}
Py_ssize_t value_len = PySequence_Length(value);
if (value_len == -1) {
return -1;
}
// Validate all items first
std::vector<std::shared_ptr<UIEntity>> new_items;
for (Py_ssize_t i = 0; i < value_len; i++) {
PyObject* item = PySequence_GetItem(value, i);
if (!item) {
return -1;
}
// 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 -1;
}
PyUIEntityObject* entity = (PyUIEntityObject*)item;
Py_DECREF(item);
new_items.push_back(entity->data);
}
// Now perform the assignment
if (step == 1) {
// Contiguous slice
if (slicelength != value_len) {
// Need to resize - remove old items and insert new ones
auto it_start = self->data->begin();
auto it_stop = self->data->begin();
std::advance(it_start, start);
std::advance(it_stop, stop);
// Clear grid references from old items
for (auto it = it_start; it != it_stop; ++it) {
(*it)->grid = nullptr;
}
// Erase old range
it_start = self->data->erase(it_start, it_stop);
// Insert new items
for (const auto& entity : new_items) {
entity->grid = self->grid;
it_start = self->data->insert(it_start, entity);
++it_start;
}
} else {
// Same size, just replace
auto it = self->data->begin();
std::advance(it, start);
for (const auto& entity : new_items) {
(*it)->grid = nullptr; // Clear old grid ref
*it = entity;
entity->grid = self->grid; // Set new grid ref
++it;
}
}
} else {
// Extended slice
if (slicelength != value_len) {
PyErr_Format(PyExc_ValueError,
"attempt to assign sequence of size %zd to extended slice of size %zd",
value_len, slicelength);
return -1;
}
auto list_it = self->data->begin();
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
auto cur_it = list_it;
std::advance(cur_it, cur);
(*cur_it)->grid = nullptr; // Clear old grid ref
*cur_it = new_items[i];
new_items[i]->grid = self->grid; // Set new grid ref
}
}
return 0;
}
} else {
PyErr_Format(PyExc_TypeError, "EntityCollection indices must be integers or slices, not %.200s",
Py_TYPE(key)->tp_name);
return -1;
}
}
PyMappingMethods UIEntityCollection::mpmethods = {
.mp_length = (lenfunc)UIEntityCollection::len,
.mp_subscript = (binaryfunc)UIEntityCollection::subscript,
.mp_ass_subscript = (objobjargproc)UIEntityCollection::ass_subscript
};
PyMethodDef UIEntityCollection::methods[] = {
{"append", (PyCFunction)UIEntityCollection::append, METH_O},
//{"extend", (PyCFunction)UIEntityCollection::extend, METH_O}, // TODO
{"extend", (PyCFunction)UIEntityCollection::extend, METH_O},
{"remove", (PyCFunction)UIEntityCollection::remove, METH_O},
{"index", (PyCFunction)UIEntityCollection::index_method, METH_O},
{"count", (PyCFunction)UIEntityCollection::count, METH_O},
{NULL, NULL, 0, NULL}
};
@ -650,3 +1219,115 @@ PyObject* UIEntityCollection::iter(PyUIEntityCollectionObject* self)
Py_DECREF(iterType);
return (PyObject*)iterObj;
}
// Property system implementation for animations
bool UIGrid::setProperty(const std::string& name, float value) {
if (name == "x") {
box.setPosition(sf::Vector2f(value, box.getPosition().y));
output.setPosition(box.getPosition());
return true;
}
else if (name == "y") {
box.setPosition(sf::Vector2f(box.getPosition().x, value));
output.setPosition(box.getPosition());
return true;
}
else if (name == "w" || name == "width") {
box.setSize(sf::Vector2f(value, box.getSize().y));
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
return true;
}
else if (name == "h" || name == "height") {
box.setSize(sf::Vector2f(box.getSize().x, value));
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
return true;
}
else if (name == "center_x") {
center_x = value;
return true;
}
else if (name == "center_y") {
center_y = value;
return true;
}
else if (name == "zoom") {
zoom = value;
return true;
}
else if (name == "z_index") {
z_index = static_cast<int>(value);
return true;
}
return false;
}
bool UIGrid::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "position") {
box.setPosition(value);
output.setPosition(box.getPosition());
return true;
}
else if (name == "size") {
box.setSize(value);
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
return true;
}
else if (name == "center") {
center_x = value.x;
center_y = value.y;
return true;
}
return false;
}
bool UIGrid::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = box.getPosition().x;
return true;
}
else if (name == "y") {
value = box.getPosition().y;
return true;
}
else if (name == "w" || name == "width") {
value = box.getSize().x;
return true;
}
else if (name == "h" || name == "height") {
value = box.getSize().y;
return true;
}
else if (name == "center_x") {
value = center_x;
return true;
}
else if (name == "center_y") {
value = center_y;
return true;
}
else if (name == "zoom") {
value = zoom;
return true;
}
else if (name == "z_index") {
value = static_cast<float>(z_index);
return true;
}
return false;
}
bool UIGrid::getProperty(const std::string& name, sf::Vector2f& value) const {
if (name == "position") {
value = box.getPosition();
return true;
}
else if (name == "size") {
value = box.getSize();
return true;
}
else if (name == "center") {
value = sf::Vector2f(center_x, center_y);
return true;
}
return false;
}

View File

@ -21,6 +21,9 @@ class UIGrid: public UIDrawable
{
private:
std::shared_ptr<PyTexture> ptex;
// Default cell dimensions when no texture is provided
static constexpr int DEFAULT_CELL_WIDTH = 16;
static constexpr int DEFAULT_CELL_HEIGHT = 16;
public:
UIGrid();
//UIGrid(int, int, IndexTexture*, float, float, float, float);
@ -43,8 +46,16 @@ public:
std::vector<UIGridPoint> points;
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
bool getProperty(const std::string& name, float& value) const override;
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
static int init(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* get_grid_size(PyUIGridObject* self, void* closure);
static PyObject* get_grid_x(PyUIGridObject* self, void* closure);
static PyObject* get_grid_y(PyUIGridObject* self, void* closure);
static PyObject* get_position(PyUIGridObject* self, void* closure);
static int set_position(PyUIGridObject* self, PyObject* value, void* closure);
static PyObject* get_size(PyUIGridObject* self, void* closure);
@ -71,14 +82,24 @@ typedef struct {
class UIEntityCollection {
public:
static PySequenceMethods sqmethods;
static PyMappingMethods mpmethods;
static PyObject* append(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* extend(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* remove(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* index_method(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* count(PyUIEntityCollectionObject* self, PyObject* value);
static PyMethodDef methods[];
static PyObject* repr(PyUIEntityCollectionObject* self);
static int init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds);
static PyObject* iter(PyUIEntityCollectionObject* self);
static Py_ssize_t len(PyUIEntityCollectionObject* self);
static PyObject* getitem(PyUIEntityCollectionObject* self, Py_ssize_t index);
static int setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value);
static int contains(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* concat(PyUIEntityCollectionObject* self, PyObject* other);
static PyObject* inplace_concat(PyUIEntityCollectionObject* self, PyObject* other);
static PyObject* subscript(PyUIEntityCollectionObject* self, PyObject* key);
static int ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value);
};
typedef struct {
@ -168,6 +189,7 @@ namespace mcrfpydef {
},
.tp_repr = (reprfunc)UIEntityCollection::repr,
.tp_as_sequence = &UIEntityCollection::sqmethods,
.tp_as_mapping = &UIEntityCollection::mpmethods,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Iterable, indexable collection of Entities"),
.tp_iter = (getiterfunc)UIEntityCollection::iter,

View File

@ -58,7 +58,7 @@ void UISprite::setSpriteIndex(int _sprite_index)
sprite = ptex->sprite(sprite_index, sprite.getPosition(), sprite.getScale());
}
sf::Vector2f UISprite::getScale()
sf::Vector2f UISprite::getScale() const
{
return sprite.getScale();
}
@ -151,6 +151,20 @@ int UISprite::set_int_member(PyUISpriteObject* self, PyObject* value, void* clos
PyErr_SetString(PyExc_TypeError, "Value must be an integer.");
return -1;
}
// Validate sprite index is within texture bounds
auto texture = self->data->getTexture();
if (texture) {
int sprite_count = texture->getSpriteCount();
if (val < 0 || val >= sprite_count) {
PyErr_Format(PyExc_ValueError,
"Sprite index %d out of range. Texture has %d sprites (0-%d)",
val, sprite_count, sprite_count - 1);
return -1;
}
}
self->data->setSpriteIndex(val);
return 0;
}
@ -162,9 +176,25 @@ PyObject* UISprite::get_texture(PyUISpriteObject* self, void* closure)
int UISprite::set_texture(PyUISpriteObject* self, PyObject* value, void* closure)
{
// Check if value is a Texture instance
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
return -1;
}
// Get the texture from the Python object
auto pytexture = (PyTextureObject*)value;
if (!pytexture->data) {
PyErr_SetString(PyExc_ValueError, "Invalid texture object");
return -1;
}
// Update the sprite's texture
self->data->setTexture(pytexture->data);
return 0;
}
PyGetSetDef UISprite::getsetters[] = {
{"x", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "X coordinate of top-left corner", (void*)0},
{"y", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Y coordinate of top-left corner", (void*)1},
@ -172,6 +202,7 @@ PyGetSetDef UISprite::getsetters[] = {
{"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL},
{"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},
{NULL}
};
@ -194,8 +225,8 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
//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;
PyObject* texture;
int sprite_index = 0;
PyObject* texture = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOif",
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &scale))
@ -203,15 +234,107 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
return -1;
}
// check types for texture
//if (texture != NULL && !PyObject_IsInstance(texture, (PyObject*)&PyTextureType)){
if (texture != NULL && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
// Handle texture - allow None or use default
std::shared_ptr<PyTexture> texture_ptr = nullptr;
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1;
} else if (texture != NULL && texture != Py_None) {
auto pytexture = (PyTextureObject*)texture;
texture_ptr = pytexture->data;
} else {
// Use default texture when None or not provided
texture_ptr = McRFPy_API::default_texture;
}
if (!texture_ptr) {
PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
return -1;
}
auto pytexture = (PyTextureObject*)texture;
self->data = std::make_shared<UISprite>(pytexture->data, sprite_index, sf::Vector2f(x, y), scale);
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
self->data->setPosition(sf::Vector2f(x, y));
return 0;
}
// Property system implementation for animations
bool UISprite::setProperty(const std::string& name, float value) {
if (name == "x") {
sprite.setPosition(sf::Vector2f(value, sprite.getPosition().y));
return true;
}
else if (name == "y") {
sprite.setPosition(sf::Vector2f(sprite.getPosition().x, value));
return true;
}
else if (name == "scale") {
sprite.setScale(sf::Vector2f(value, value));
return true;
}
else if (name == "scale_x") {
sprite.setScale(sf::Vector2f(value, sprite.getScale().y));
return true;
}
else if (name == "scale_y") {
sprite.setScale(sf::Vector2f(sprite.getScale().x, value));
return true;
}
else if (name == "z_index") {
z_index = static_cast<int>(value);
return true;
}
return false;
}
bool UISprite::setProperty(const std::string& name, int value) {
if (name == "sprite_number") {
setSpriteIndex(value);
return true;
}
else if (name == "z_index") {
z_index = value;
return true;
}
return false;
}
bool UISprite::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = sprite.getPosition().x;
return true;
}
else if (name == "y") {
value = sprite.getPosition().y;
return true;
}
else if (name == "scale") {
value = sprite.getScale().x; // Assuming uniform scale
return true;
}
else if (name == "scale_x") {
value = sprite.getScale().x;
return true;
}
else if (name == "scale_y") {
value = sprite.getScale().y;
return true;
}
else if (name == "z_index") {
value = static_cast<float>(z_index);
return true;
}
return false;
}
bool UISprite::getProperty(const std::string& name, int& value) const {
if (name == "sprite_number") {
value = sprite_index;
return true;
}
else if (name == "z_index") {
value = z_index;
return true;
}
return false;
}

View File

@ -33,7 +33,7 @@ public:
void setPosition(sf::Vector2f);
sf::Vector2f getPosition();
void setScale(sf::Vector2f);
sf::Vector2f getScale();
sf::Vector2f getScale() const;
void setSpriteIndex(int);
int getSpriteIndex();
@ -42,6 +42,12 @@ public:
PyObjectsEnum derived_type() override final;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, int value) override;
bool getProperty(const std::string& name, float& value) const override;
bool getProperty(const std::string& name, int& value) const override;
static PyObject* get_float_member(PyUISpriteObject* self, void* closure);
static int set_float_member(PyUISpriteObject* self, PyObject* value, void* closure);

View File

@ -156,8 +156,8 @@ void UITestScene::doAction(std::string name, std::string type)
void UITestScene::render()
{
game->getWindow().clear();
game->getWindow().draw(text);
game->getRenderTarget().clear();
game->getRenderTarget().draw(text);
// draw all UI elements
//for (auto e: ui_elements)
@ -175,7 +175,7 @@ void UITestScene::render()
//e1.render(sf::Vector2f(-100, -100));
game->getWindow().display();
// Display is handled by GameEngine
//McRFPy_API::REPL();
}

View File

@ -1,8 +1,204 @@
#include <SFML/Graphics.hpp>
#include "GameEngine.h"
#include "CommandLineParser.h"
#include "McRogueFaceConfig.h"
#include "McRFPy_API.h"
#include "PyFont.h"
#include "PyTexture.h"
#include <Python.h>
#include <iostream>
#include <filesystem>
int main()
// Forward declarations
int run_game_engine(const McRogueFaceConfig& config);
int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[]);
int main(int argc, char* argv[])
{
GameEngine g;
g.run();
McRogueFaceConfig config;
CommandLineParser parser(argc, argv);
// Parse arguments
auto parse_result = parser.parse(config);
if (parse_result.should_exit) {
return parse_result.exit_code;
}
// Special handling for -m module: let Python handle modules properly
if (!config.python_module.empty()) {
config.python_mode = true;
}
// Initialize based on configuration
if (config.python_mode) {
return run_python_interpreter(config, argc, argv);
} else {
return run_game_engine(config);
}
}
int run_game_engine(const McRogueFaceConfig& config)
{
GameEngine g(config);
g.run();
return 0;
}
int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[])
{
// Create a game engine with the requested configuration
GameEngine* engine = new GameEngine(config);
// Initialize Python with configuration
McRFPy_API::init_python_with_config(config, argc, argv);
// Import mcrfpy module and store reference
McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
if (!McRFPy_API::mcrf_module) {
PyErr_Print();
std::cerr << "Failed to import mcrfpy module" << std::endl;
} else {
// Set up default_font and default_texture if not already done
if (!McRFPy_API::default_font) {
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);
}
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
}
// Handle different Python modes
if (!config.python_command.empty()) {
// Execute command from -c
if (config.interactive_mode) {
// Use PyRun_String to catch SystemExit
PyObject* main_module = PyImport_AddModule("__main__");
PyObject* main_dict = PyModule_GetDict(main_module);
PyObject* result_obj = PyRun_String(config.python_command.c_str(),
Py_file_input, main_dict, main_dict);
if (result_obj == NULL) {
// Check if it's SystemExit
if (PyErr_Occurred()) {
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
// If it's SystemExit and we're in interactive mode, clear it
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
PyErr_Clear();
} else {
// Re-raise other exceptions
PyErr_Restore(type, value, traceback);
PyErr_Print();
}
Py_XDECREF(type);
Py_XDECREF(value);
Py_XDECREF(traceback);
}
} else {
Py_DECREF(result_obj);
}
// Continue to interactive mode below
} else {
int result = PyRun_SimpleString(config.python_command.c_str());
Py_Finalize();
delete engine;
return result;
}
}
else if (!config.python_module.empty()) {
// Execute module using runpy
std::string run_module_code =
"import sys\n"
"import runpy\n"
"sys.argv = ['" + config.python_module + "'";
for (const auto& arg : config.script_args) {
run_module_code += ", '" + arg + "'";
}
run_module_code += "]\n";
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());
Py_Finalize();
delete engine;
return result;
}
else if (!config.script_path.empty()) {
// Execute script file
FILE* fp = fopen(config.script_path.string().c_str(), "r");
if (!fp) {
std::cerr << "mcrogueface: can't open file '" << config.script_path << "': ";
std::cerr << "[Errno " << errno << "] " << strerror(errno) << std::endl;
return 1;
}
// Set up sys.argv
wchar_t** python_argv = new wchar_t*[config.script_args.size() + 1];
python_argv[0] = Py_DecodeLocale(config.script_path.string().c_str(), nullptr);
for (size_t i = 0; i < config.script_args.size(); i++) {
python_argv[i + 1] = Py_DecodeLocale(config.script_args[i].c_str(), nullptr);
}
PySys_SetArgvEx(config.script_args.size() + 1, python_argv, 0);
int result = PyRun_SimpleFile(fp, config.script_path.string().c_str());
fclose(fp);
// Clean up
for (size_t i = 0; i <= config.script_args.size(); i++) {
PyMem_RawFree(python_argv[i]);
}
delete[] python_argv;
if (config.interactive_mode) {
// Even if script had SystemExit, continue to interactive mode
if (result != 0) {
// Check if it was SystemExit
if (PyErr_Occurred()) {
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
PyErr_Clear();
result = 0; // Don't exit with error
} else {
PyErr_Restore(type, value, traceback);
PyErr_Print();
}
Py_XDECREF(type);
Py_XDECREF(value);
Py_XDECREF(traceback);
}
}
// Run interactive mode after script
PyRun_InteractiveLoop(stdin, "<stdin>");
}
// Run the game engine after script execution
engine->run();
Py_Finalize();
delete engine;
return result;
}
else if (config.interactive_mode) {
// Interactive Python interpreter (only if explicitly requested with -i)
Py_InspectFlag = 1;
PyRun_InteractiveLoop(stdin, "<stdin>");
Py_Finalize();
delete engine;
return 0;
}
else if (!config.exec_scripts.empty()) {
// With --exec, run the game engine after scripts execute
engine->run();
Py_Finalize();
delete engine;
return 0;
}
delete engine;
return 0;
}

View File

@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Example of CORRECT test pattern using timer callbacks for automation"""
import mcrfpy
from mcrfpy import automation
from datetime import datetime
def run_automation_tests():
"""This runs AFTER the game loop has started and rendered frames"""
print("\n=== Automation Test Running (1 second after start) ===")
# NOW we can take screenshots that will show content!
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"WORKING_screenshot_{timestamp}.png"
# Take screenshot - this should now show our red frame
result = automation.screenshot(filename)
print(f"Screenshot taken: {filename} - Result: {result}")
# Test clicking on the frame
automation.click(200, 200) # Click in center of red frame
# Test keyboard input
automation.typewrite("Hello from timer callback!")
# Take another screenshot to show any changes
filename2 = f"WORKING_screenshot_after_click_{timestamp}.png"
automation.screenshot(filename2)
print(f"Second screenshot: {filename2}")
print("Test completed successfully!")
print("\nThis works because:")
print("1. The game loop has been running for 1 second")
print("2. The scene has been rendered multiple times")
print("3. The RenderTexture now contains actual rendered content")
# Cancel this timer so it doesn't repeat
mcrfpy.delTimer("automation_test")
# Optional: exit after a moment
def exit_game():
print("Exiting...")
mcrfpy.exit()
mcrfpy.setTimer("exit", exit_game, 500) # Exit 500ms later
# This code runs during --exec script execution
print("=== Setting Up Test Scene ===")
# Create scene with visible content
mcrfpy.createScene("timer_test_scene")
mcrfpy.setScene("timer_test_scene")
ui = mcrfpy.sceneUI("timer_test_scene")
# Add a bright red frame that should be visible
frame = mcrfpy.Frame(100, 100, 400, 300,
fill_color=mcrfpy.Color(255, 0, 0), # Bright red
outline_color=mcrfpy.Color(255, 255, 255), # White outline
outline=5.0)
ui.append(frame)
# Add text
caption = mcrfpy.Caption(mcrfpy.Vector(150, 150),
text="TIMER TEST - SHOULD BE VISIBLE",
fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 24
frame.children.append(caption)
# Add click handler to demonstrate interaction
def frame_clicked(x, y, button):
print(f"Frame clicked at ({x}, {y}) with button {button}")
frame.click = frame_clicked
print("Scene setup complete. Setting timer for automation tests...")
# THIS IS THE KEY: Set timer to run AFTER the game loop starts
mcrfpy.setTimer("automation_test", run_automation_tests, 1000)
print("Timer set. Game loop will start after this script completes.")
print("Automation tests will run 1 second later when content is visible.")
# Script ends here - game loop starts next

165
tests/animation_demo.py Normal file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""Animation System Demo - Shows all animation capabilities"""
import mcrfpy
import math
# Create main scene
mcrfpy.createScene("animation_demo")
ui = mcrfpy.sceneUI("animation_demo")
mcrfpy.setScene("animation_demo")
# Title
title = mcrfpy.Caption((400, 30), "McRogueFace Animation System Demo", mcrfpy.default_font)
title.size = 24
title.fill_color = (255, 255, 255)
# Note: centered property doesn't exist for Caption
ui.append(title)
# 1. Position Animation Demo
pos_frame = mcrfpy.Frame(50, 100, 80, 80)
pos_frame.fill_color = (255, 100, 100)
pos_frame.outline = 2
ui.append(pos_frame)
pos_label = mcrfpy.Caption((50, 80), "Position Animation", mcrfpy.default_font)
pos_label.fill_color = (200, 200, 200)
ui.append(pos_label)
# 2. Size Animation Demo
size_frame = mcrfpy.Frame(200, 100, 50, 50)
size_frame.fill_color = (100, 255, 100)
size_frame.outline = 2
ui.append(size_frame)
size_label = mcrfpy.Caption((200, 80), "Size Animation", mcrfpy.default_font)
size_label.fill_color = (200, 200, 200)
ui.append(size_label)
# 3. Color Animation Demo
color_frame = mcrfpy.Frame(350, 100, 80, 80)
color_frame.fill_color = (255, 0, 0)
ui.append(color_frame)
color_label = mcrfpy.Caption((350, 80), "Color Animation", mcrfpy.default_font)
color_label.fill_color = (200, 200, 200)
ui.append(color_label)
# 4. Easing Functions Demo
easing_y = 250
easing_frames = []
easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInElastic", "easeOutBounce"]
for i, easing in enumerate(easings):
x = 50 + i * 120
frame = mcrfpy.Frame(x, easing_y, 20, 20)
frame.fill_color = (100, 150, 255)
ui.append(frame)
easing_frames.append((frame, easing))
label = mcrfpy.Caption((x, easing_y - 20), easing, mcrfpy.default_font)
label.size = 12
label.fill_color = (200, 200, 200)
ui.append(label)
# 5. Complex Animation Demo
complex_frame = mcrfpy.Frame(300, 350, 100, 100)
complex_frame.fill_color = (128, 128, 255)
complex_frame.outline = 3
ui.append(complex_frame)
complex_label = mcrfpy.Caption((300, 330), "Complex Multi-Property", mcrfpy.default_font)
complex_label.fill_color = (200, 200, 200)
ui.append(complex_label)
# Start animations
def start_animations(runtime):
# 1. Position animation - back and forth
x_anim = mcrfpy.Animation("x", 500.0, 3.0, "easeInOut")
x_anim.start(pos_frame)
# 2. Size animation - pulsing
w_anim = mcrfpy.Animation("w", 150.0, 2.0, "easeInOut")
h_anim = mcrfpy.Animation("h", 150.0, 2.0, "easeInOut")
w_anim.start(size_frame)
h_anim.start(size_frame)
# 3. Color animation - rainbow cycle
color_anim = mcrfpy.Animation("fill_color", (0, 255, 255, 255), 2.0, "linear")
color_anim.start(color_frame)
# 4. Easing demos - all move up with different easings
for frame, easing in easing_frames:
y_anim = mcrfpy.Animation("y", 150.0, 2.0, easing)
y_anim.start(frame)
# 5. Complex animation - multiple properties
cx_anim = mcrfpy.Animation("x", 500.0, 4.0, "easeInOut")
cy_anim = mcrfpy.Animation("y", 400.0, 4.0, "easeOut")
cw_anim = mcrfpy.Animation("w", 150.0, 4.0, "easeInElastic")
ch_anim = mcrfpy.Animation("h", 150.0, 4.0, "easeInElastic")
outline_anim = mcrfpy.Animation("outline", 10.0, 4.0, "linear")
cx_anim.start(complex_frame)
cy_anim.start(complex_frame)
cw_anim.start(complex_frame)
ch_anim.start(complex_frame)
outline_anim.start(complex_frame)
# Individual color component animations
r_anim = mcrfpy.Animation("fill_color.r", 255.0, 4.0, "easeInOut")
g_anim = mcrfpy.Animation("fill_color.g", 100.0, 4.0, "easeInOut")
b_anim = mcrfpy.Animation("fill_color.b", 50.0, 4.0, "easeInOut")
r_anim.start(complex_frame)
g_anim.start(complex_frame)
b_anim.start(complex_frame)
print("All animations started!")
# Reverse some animations
def reverse_animations(runtime):
# Position back
x_anim = mcrfpy.Animation("x", 50.0, 3.0, "easeInOut")
x_anim.start(pos_frame)
# Size back
w_anim = mcrfpy.Animation("w", 50.0, 2.0, "easeInOut")
h_anim = mcrfpy.Animation("h", 50.0, 2.0, "easeInOut")
w_anim.start(size_frame)
h_anim.start(size_frame)
# Color cycle continues
color_anim = mcrfpy.Animation("fill_color", (255, 0, 255, 255), 2.0, "linear")
color_anim.start(color_frame)
# Easing frames back down
for frame, easing in easing_frames:
y_anim = mcrfpy.Animation("y", 250.0, 2.0, easing)
y_anim.start(frame)
# Continue color cycle
def cycle_colors(runtime):
color_anim = mcrfpy.Animation("fill_color", (255, 255, 0, 255), 2.0, "linear")
color_anim.start(color_frame)
# Info text
info = mcrfpy.Caption((400, 550), "Watch as different properties animate with various easing functions!", mcrfpy.default_font)
info.fill_color = (255, 255, 200)
# Note: centered property doesn't exist for Caption
ui.append(info)
# Schedule animations
mcrfpy.setTimer("start", start_animations, 500)
mcrfpy.setTimer("reverse", reverse_animations, 4000)
mcrfpy.setTimer("cycle", cycle_colors, 2500)
# Exit handler
def on_key(key):
if key == "Escape":
mcrfpy.exit()
mcrfpy.keypressScene(on_key)
print("Animation demo started! Press Escape to exit.")

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Test for mcrfpy.createScene() method"""
import mcrfpy
def test_createScene():
"""Test creating a new scene"""
# Test creating scenes
test_scenes = ["test_scene1", "test_scene2", "special_chars_!@#"]
for scene_name in test_scenes:
try:
mcrfpy.createScene(scene_name)
print(f"✓ Created scene: {scene_name}")
except Exception as e:
print(f"✗ Failed to create scene {scene_name}: {e}")
return
# Try to set scene to verify it was created
try:
mcrfpy.setScene("test_scene1")
current = mcrfpy.currentScene()
if current == "test_scene1":
print("✓ Scene switching works correctly")
else:
print(f"✗ Scene switch failed: expected 'test_scene1', got '{current}'")
except Exception as e:
print(f"✗ Scene switching error: {e}")
print("PASS")
# Run test immediately
print("Running createScene test...")
test_createScene()
print("Test completed.")

View File

@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Test for mcrfpy.keypressScene() - Related to issue #61"""
import mcrfpy
# Track keypresses for different scenes
scene1_presses = []
scene2_presses = []
def scene1_handler(key_code):
"""Handle keyboard events for scene 1"""
scene1_presses.append(key_code)
print(f"Scene 1 key pressed: {key_code}")
def scene2_handler(key_code):
"""Handle keyboard events for scene 2"""
scene2_presses.append(key_code)
print(f"Scene 2 key pressed: {key_code}")
def test_keypressScene():
"""Test keyboard event handling for scenes"""
print("=== Testing mcrfpy.keypressScene() ===")
# Test 1: Basic handler registration
print("\n1. Basic handler registration:")
mcrfpy.createScene("scene1")
mcrfpy.setScene("scene1")
try:
mcrfpy.keypressScene(scene1_handler)
print("✓ Keypress handler registered for scene1")
except Exception as e:
print(f"✗ Failed to register handler: {e}")
print("FAIL")
return
# Test 2: Handler persists across scene changes
print("\n2. Testing handler persistence:")
mcrfpy.createScene("scene2")
mcrfpy.setScene("scene2")
try:
mcrfpy.keypressScene(scene2_handler)
print("✓ Keypress handler registered for scene2")
except Exception as e:
print(f"✗ Failed to register handler for scene2: {e}")
# Switch back to scene1
mcrfpy.setScene("scene1")
current = mcrfpy.currentScene()
print(f"✓ Switched back to: {current}")
# Test 3: Clear handler
print("\n3. Testing handler clearing:")
try:
mcrfpy.keypressScene(None)
print("✓ Handler cleared with None")
except Exception as e:
print(f"✗ Failed to clear handler: {e}")
# Test 4: Re-register handler
print("\n4. Testing re-registration:")
try:
mcrfpy.keypressScene(scene1_handler)
print("✓ Handler re-registered successfully")
except Exception as e:
print(f"✗ Failed to re-register: {e}")
# Test 5: Lambda functions
print("\n5. Testing lambda functions:")
try:
mcrfpy.keypressScene(lambda k: print(f"Lambda key: {k}"))
print("✓ Lambda function accepted as handler")
except Exception as e:
print(f"✗ Failed with lambda: {e}")
# Known issues
print("\n⚠ Known Issues:")
print("- Invalid argument (non-callable) causes segfault")
print("- No way to query current handler")
print("- Handler is global, not per-scene (issue #61)")
# Summary related to issue #61
print("\n📋 Issue #61 Analysis:")
print("Current: mcrfpy.keypressScene() sets a global handler")
print("Proposed: Scene objects should encapsulate their own callbacks")
print("Impact: Currently only one keypress handler active at a time")
print("\n=== Test Complete ===")
print("PASS - API functions correctly within current limitations")
# Run test immediately
test_keypressScene()

80
tests/api_sceneUI_test.py Normal file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Test for mcrfpy.sceneUI() method - Related to issue #28"""
import mcrfpy
from mcrfpy import automation
from datetime import datetime
def test_sceneUI():
"""Test getting UI collection from scene"""
# Create a test scene
mcrfpy.createScene("ui_test_scene")
mcrfpy.setScene("ui_test_scene")
# Get initial UI collection (should be empty)
try:
ui_collection = mcrfpy.sceneUI("ui_test_scene")
print(f"✓ sceneUI returned collection with {len(ui_collection)} items")
except Exception as e:
print(f"✗ sceneUI failed: {e}")
print("FAIL")
return
# Add some UI elements to the scene
frame = mcrfpy.Frame(10, 10, 200, 150,
fill_color=mcrfpy.Color(100, 100, 200),
outline_color=mcrfpy.Color(255, 255, 255),
outline=2.0)
ui_collection.append(frame)
caption = mcrfpy.Caption(mcrfpy.Vector(220, 10),
text="Test Caption",
fill_color=mcrfpy.Color(255, 255, 0))
ui_collection.append(caption)
# Skip sprite for now since it requires a texture
# sprite = mcrfpy.Sprite(10, 170, scale=2.0)
# ui_collection.append(sprite)
# Get UI collection again
ui_collection2 = mcrfpy.sceneUI("ui_test_scene")
print(f"✓ After adding elements: {len(ui_collection2)} items")
# Test iteration (Issue #28 - UICollectionIter)
try:
item_types = []
for item in ui_collection2:
item_types.append(type(item).__name__)
print(f"✓ Iteration works, found types: {item_types}")
except Exception as e:
print(f"✗ Iteration failed (Issue #28): {e}")
# Test indexing
try:
first_item = ui_collection2[0]
print(f"✓ Indexing works, first item type: {type(first_item).__name__}")
except Exception as e:
print(f"✗ Indexing failed: {e}")
# Test invalid scene name
try:
invalid_ui = mcrfpy.sceneUI("nonexistent_scene")
print(f"✗ sceneUI should fail for nonexistent scene, got {len(invalid_ui)} items")
except Exception as e:
print(f"✓ sceneUI correctly fails for nonexistent scene: {e}")
# Take screenshot
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"test_sceneUI_{timestamp}.png"
automation.screenshot(filename)
print(f"Screenshot saved: {filename}")
print("PASS")
# Set up timer to run test
mcrfpy.setTimer("test", test_sceneUI, 1000)
# Cancel timer after running once
def cleanup():
mcrfpy.delTimer("test")
mcrfpy.delTimer("cleanup")
mcrfpy.setTimer("cleanup", cleanup, 1100)

View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Test for mcrfpy.setScene() and currentScene() methods"""
import mcrfpy
print("Starting setScene/currentScene test...")
# Create test scenes first
scenes = ["scene_A", "scene_B", "scene_C"]
for scene in scenes:
mcrfpy.createScene(scene)
print(f"Created scene: {scene}")
results = []
# Test switching between scenes
for scene in scenes:
try:
mcrfpy.setScene(scene)
current = mcrfpy.currentScene()
if current == scene:
results.append(f"✓ setScene/currentScene works for '{scene}'")
else:
results.append(f"✗ Scene mismatch: set '{scene}', got '{current}'")
except Exception as e:
results.append(f"✗ Error with scene '{scene}': {e}")
# Test invalid scene - it should not change the current scene
current_before = mcrfpy.currentScene()
mcrfpy.setScene("nonexistent_scene")
current_after = mcrfpy.currentScene()
if current_before == current_after:
results.append(f"✓ setScene correctly ignores nonexistent scene (stayed on '{current_after}')")
else:
results.append(f"✗ Scene changed unexpectedly from '{current_before}' to '{current_after}'")
# Print results
for result in results:
print(result)
# Determine pass/fail
if all("" in r for r in results):
print("PASS")
else:
print("FAIL")

70
tests/api_timer_test.py Normal file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Test for mcrfpy.setTimer() and delTimer() methods"""
import mcrfpy
import sys
def test_timers():
"""Test timer API methods"""
print("Testing mcrfpy timer methods...")
# Test 1: Create a simple timer
try:
call_count = [0]
def simple_callback(runtime):
call_count[0] += 1
print(f"Timer callback called, count={call_count[0]}, runtime={runtime}")
mcrfpy.setTimer("test_timer", simple_callback, 100)
print("✓ setTimer() called successfully")
except Exception as e:
print(f"✗ setTimer() failed: {e}")
print("FAIL")
return
# Test 2: Delete the timer
try:
mcrfpy.delTimer("test_timer")
print("✓ delTimer() called successfully")
except Exception as e:
print(f"✗ delTimer() failed: {e}")
print("FAIL")
return
# Test 3: Delete non-existent timer (should not crash)
try:
mcrfpy.delTimer("nonexistent_timer")
print("✓ delTimer() accepts non-existent timer names")
except Exception as e:
print(f"✗ delTimer() failed on non-existent timer: {e}")
print("FAIL")
return
# Test 4: Create multiple timers
try:
def callback1(rt): pass
def callback2(rt): pass
def callback3(rt): pass
mcrfpy.setTimer("timer1", callback1, 500)
mcrfpy.setTimer("timer2", callback2, 750)
mcrfpy.setTimer("timer3", callback3, 250)
print("✓ Multiple timers created successfully")
# Clean up
mcrfpy.delTimer("timer1")
mcrfpy.delTimer("timer2")
mcrfpy.delTimer("timer3")
print("✓ Multiple timers deleted successfully")
except Exception as e:
print(f"✗ Multiple timer test failed: {e}")
print("FAIL")
return
print("\nAll timer API tests passed")
print("PASS")
# Run the test
test_timers()
# Exit cleanly
sys.exit(0)

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
Analysis of Issue #78: Middle Mouse Click sends 'C' keyboard event
BUG FOUND in GameEngine::processEvent() at src/GameEngine.cpp
The bug occurs in this code section:
```cpp
if (currentScene()->hasAction(actionCode))
{
std::string name = currentScene()->action(actionCode);
currentScene()->doAction(name, actionType);
}
else if (currentScene()->key_callable)
{
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
}
```
ISSUE: When a middle mouse button event occurs and there's no registered action for it,
the code falls through to the key_callable branch. However, it then tries to access
`event.key.code` from what is actually a mouse button event!
Since it's a union, `event.key.code` reads garbage data from the mouse event structure.
The middle mouse button has value 2, which coincidentally matches sf::Keyboard::C (also value 2),
causing the spurious 'C' keyboard event.
SOLUTION: The code should check the event type before accessing event-specific fields:
```cpp
else if (currentScene()->key_callable &&
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
{
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
}
```
TEST STATUS:
- Test Name: automation_click_issue78_test.py
- Method Tested: Middle mouse click behavior
- Pass/Fail: FAIL - Issue #78 confirmed to exist
- Error: Middle mouse clicks incorrectly trigger 'C' keyboard events
- Modifications: None needed - bug is in C++ code, not the test
The test correctly identifies the issue but cannot run in headless mode due to
requiring actual event processing through the game loop.
"""
import mcrfpy
import sys
print(__doc__)
# Demonstrate the issue conceptually
print("\nDemonstration of the bug:")
print("1. Middle mouse button value in SFML: 2")
print("2. Keyboard 'C' value in SFML: 2")
print("3. When processEvent reads event.key.code from a mouse event,")
print(" it gets the value 2, which ActionCode::key_str interprets as 'C'")
print("\nThe fix is simple: add an event type check before accessing key.code")
sys.exit(0)

View File

@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Test for automation click methods - Related to issue #78 (Middle click sends 'C')"""
import mcrfpy
from datetime import datetime
# Try to import automation, but handle if it doesn't exist
try:
from mcrfpy import automation
HAS_AUTOMATION = True
print("SUCCESS: mcrfpy.automation module imported successfully")
except (ImportError, AttributeError) as e:
HAS_AUTOMATION = False
print(f"WARNING: mcrfpy.automation module not available - {e}")
print("The automation module may not be implemented yet")
# Track events
click_events = []
key_events = []
def click_handler(x, y, button):
"""Track click events"""
click_events.append((x, y, button))
print(f"Click received: ({x}, {y}, button={button})")
def key_handler(key, scancode=None):
"""Track keyboard events"""
key_events.append(key)
print(f"Key received: {key} (scancode: {scancode})")
def test_clicks():
"""Test various click types, especially middle click (Issue #78)"""
if not HAS_AUTOMATION:
print("SKIP - automation module not available")
print("The automation module may not be implemented yet")
return
# Create test scene
mcrfpy.createScene("click_test")
mcrfpy.setScene("click_test")
ui = mcrfpy.sceneUI("click_test")
# Set up keyboard handler to detect Issue #78
mcrfpy.keypressScene(key_handler)
# Create clickable frame
frame = mcrfpy.Frame(50, 50, 300, 200,
fill_color=mcrfpy.Color(100, 100, 200),
outline_color=mcrfpy.Color(255, 255, 255),
outline=2.0)
frame.click = click_handler
ui.append(frame)
caption = mcrfpy.Caption(mcrfpy.Vector(60, 60),
text="Click Test Area",
fill_color=mcrfpy.Color(255, 255, 255))
frame.children.append(caption)
# Test different click types
print("Testing click types...")
# Left click
try:
automation.click(200, 150)
print("✓ Left click sent")
except Exception as e:
print(f"✗ Left click failed: {e}")
# Right click
try:
automation.rightClick(200, 150)
print("✓ Right click sent")
except Exception as e:
print(f"✗ Right click failed: {e}")
# Middle click - This is Issue #78
try:
automation.middleClick(200, 150)
print("✓ Middle click sent")
except Exception as e:
print(f"✗ Middle click failed: {e}")
# Double click
try:
automation.doubleClick(200, 150)
print("✓ Double click sent")
except Exception as e:
print(f"✗ Double click failed: {e}")
# Triple click
try:
automation.tripleClick(200, 150)
print("✓ Triple click sent")
except Exception as e:
print(f"✗ Triple click failed: {e}")
# Click with specific button parameter
try:
automation.click(200, 150, button='middle')
print("✓ Click with button='middle' sent")
except Exception as e:
print(f"✗ Click with button parameter failed: {e}")
# Check results after a delay
def check_results(runtime):
print(f"\nClick events received: {len(click_events)}")
print(f"Keyboard events received: {len(key_events)}")
# Check for Issue #78
if any('C' in str(event) or ord('C') == event for event in key_events):
print("✗ ISSUE #78 CONFIRMED: Middle click sent 'C' keyboard event!")
else:
print("✓ No spurious 'C' keyboard events detected")
# Analyze click events
for event in click_events:
print(f" Click: {event}")
# Take screenshot
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"test_clicks_issue78_{timestamp}.png"
automation.screenshot(filename)
print(f"Screenshot saved: {filename}")
if len(click_events) > 0:
print("PASS - Clicks detected")
else:
print("FAIL - No clicks detected (may be headless limitation)")
mcrfpy.delTimer("check_results")
mcrfpy.setTimer("check_results", check_results, 2000)
# Set up timer to run test
print("Setting up test timer...")
mcrfpy.setTimer("test", test_clicks, 1000)
# Cancel timer after running once
def cleanup():
mcrfpy.delTimer("test")
mcrfpy.delTimer("cleanup")
mcrfpy.setTimer("cleanup", cleanup, 1100)
# Exit after test completes
def exit_test():
print("\nTest completed - exiting")
import sys
sys.exit(0)
mcrfpy.setTimer("exit", exit_test, 5000)
print("Test script initialized, waiting for timers...")

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Test for mcrfpy.automation.screenshot()"""
import mcrfpy
from mcrfpy import automation
from datetime import datetime
import os
import sys
import time
runs = 0
def test_screenshot(*args):
"""Test screenshot functionality"""
#global runs
#runs += 1
#if runs < 2:
# print("tick")
# return
#print("tock")
#mcrfpy.delTimer("timer1")
# Create a scene with some visual elements
mcrfpy.createScene("screenshot_test")
mcrfpy.setScene("screenshot_test")
ui = mcrfpy.sceneUI("screenshot_test")
# Add some colorful elements
frame1 = mcrfpy.Frame(10, 10, 200, 150,
fill_color=mcrfpy.Color(255, 0, 0),
outline_color=mcrfpy.Color(255, 255, 255),
outline=3.0)
ui.append(frame1)
frame2 = mcrfpy.Frame(220, 10, 200, 150,
fill_color=mcrfpy.Color(0, 255, 0),
outline_color=mcrfpy.Color(0, 0, 0),
outline=2.0)
ui.append(frame2)
caption = mcrfpy.Caption(mcrfpy.Vector(10, 170),
text="Screenshot Test Scene",
fill_color=mcrfpy.Color(255, 255, 0))
caption.size = 24
ui.append(caption)
# Test multiple screenshots
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filenames = []
# Test 1: Basic screenshot
try:
filename1 = f"test_screenshot_basic_{timestamp}.png"
result = automation.screenshot(filename1)
filenames.append(filename1)
print(f"✓ Basic screenshot saved: {filename1} (result: {result})")
except Exception as e:
print(f"✗ Basic screenshot failed: {e}")
print("FAIL")
sys.exit(1)
# Test 2: Screenshot with special characters in filename
try:
filename2 = f"test_screenshot_special_chars_{timestamp}_test.png"
result = automation.screenshot(filename2)
filenames.append(filename2)
print(f"✓ Screenshot with special filename saved: {filename2} (result: {result})")
except Exception as e:
print(f"✗ Special filename screenshot failed: {e}")
# Test 3: Invalid filename (if applicable)
try:
result = automation.screenshot("")
print(f"✗ Empty filename should fail but returned: {result}")
except Exception as e:
print(f"✓ Empty filename correctly rejected: {e}")
# Check files exist immediately
files_found = 0
for filename in filenames:
if os.path.exists(filename):
size = os.path.getsize(filename)
print(f"✓ File exists: {filename} ({size} bytes)")
files_found += 1
else:
print(f"✗ File not found: {filename}")
if files_found == len(filenames):
print("PASS")
sys.exit(0)
else:
print("FAIL")
sys.exit(1)
print("Set callback")
mcrfpy.setTimer("timer1", test_screenshot, 1000)
# Run the test immediately
#test_screenshot()

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Simple test for mcrfpy.automation.screenshot()"""
import mcrfpy
from mcrfpy import automation
import os
import sys
# Create a simple scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Take a screenshot immediately
try:
filename = "test_screenshot.png"
result = automation.screenshot(filename)
print(f"Screenshot result: {result}")
# Check if file exists
if os.path.exists(filename):
size = os.path.getsize(filename)
print(f"PASS - Screenshot saved: {filename} ({size} bytes)")
else:
print(f"FAIL - Screenshot file not created: {filename}")
except Exception as e:
print(f"FAIL - Screenshot error: {e}")
import traceback
traceback.print_exc()
# Exit immediately
sys.exit(0)

View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Debug rendering to find why screenshots are transparent"""
import mcrfpy
from mcrfpy import automation
import sys
# Check if we're in headless mode
print("=== Debug Render Test ===")
print(f"Module loaded: {mcrfpy}")
print(f"Automation available: {'automation' in dir(mcrfpy)}")
# Try to understand the scene state
print("\nCreating and checking scene...")
mcrfpy.createScene("debug_scene")
mcrfpy.setScene("debug_scene")
current = mcrfpy.currentScene()
print(f"Current scene: {current}")
# Get UI collection
ui = mcrfpy.sceneUI("debug_scene")
print(f"UI collection type: {type(ui)}")
print(f"Initial UI elements: {len(ui)}")
# Add a simple frame
frame = mcrfpy.Frame(0, 0, 100, 100,
fill_color=mcrfpy.Color(255, 255, 255))
ui.append(frame)
print(f"After adding frame: {len(ui)} elements")
# Check if the issue is with timing
print("\nTaking immediate screenshot...")
result1 = automation.screenshot("debug_immediate.png")
print(f"Immediate screenshot result: {result1}")
# Maybe we need to let the engine process the frame?
# In headless mode with --exec, the game loop might not be running
print("\nNote: In --exec mode, the game loop doesn't run continuously.")
print("This might prevent rendering from occurring.")
# Let's also check what happens with multiple screenshots
for i in range(3):
result = automation.screenshot(f"debug_multi_{i}.png")
print(f"Screenshot {i}: {result}")
print("\nConclusion: The issue appears to be that in --exec mode,")
print("the render loop never runs, so nothing is drawn to the RenderTexture.")
print("The screenshot captures an uninitialized/unrendered texture.")
sys.exit(0)

2
tests/empty_script.py Normal file
View File

@ -0,0 +1,2 @@
# This script is intentionally empty
pass

View File

@ -0,0 +1,7 @@
#!/usr/bin/env python3
"""Test if calling mcrfpy.exit() prevents the >>> prompt"""
import mcrfpy
print("Calling mcrfpy.exit() immediately...")
mcrfpy.exit()
print("This should not print if exit worked")

View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Force Python to be non-interactive"""
import sys
import os
print("Attempting to force non-interactive mode...")
# Remove ps1/ps2 if they exist
if hasattr(sys, 'ps1'):
delattr(sys, 'ps1')
if hasattr(sys, 'ps2'):
delattr(sys, 'ps2')
# Set environment variable
os.environ['PYTHONSTARTUP'] = ''
# Try to set stdin to non-interactive
try:
import fcntl
import termios
# Make stdin non-interactive by removing ICANON flag
attrs = termios.tcgetattr(0)
attrs[3] = attrs[3] & ~termios.ICANON
termios.tcsetattr(0, termios.TCSANOW, attrs)
print("Modified terminal attributes")
except:
print("Could not modify terminal attributes")
print("Script complete")

View File

@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Generate caption documentation screenshot with proper font"""
import mcrfpy
from mcrfpy import automation
import sys
def capture_caption(runtime):
"""Capture caption example after render loop starts"""
# Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_caption_example.png")
print("Caption screenshot saved!")
# Exit after capturing
sys.exit(0)
# Create scene
mcrfpy.createScene("captions")
# Title
title = mcrfpy.Caption(400, 30, "Caption Examples")
title.font = mcrfpy.default_font
title.font_size = 28
title.font_color = (255, 255, 255)
# Different sizes
size_label = mcrfpy.Caption(100, 100, "Different Sizes:")
size_label.font = mcrfpy.default_font
size_label.font_color = (200, 200, 200)
large = mcrfpy.Caption(300, 100, "Large Text (24pt)")
large.font = mcrfpy.default_font
large.font_size = 24
large.font_color = (255, 255, 255)
medium = mcrfpy.Caption(300, 140, "Medium Text (18pt)")
medium.font = mcrfpy.default_font
medium.font_size = 18
medium.font_color = (255, 255, 255)
small = mcrfpy.Caption(300, 170, "Small Text (14pt)")
small.font = mcrfpy.default_font
small.font_size = 14
small.font_color = (255, 255, 255)
# Different colors
color_label = mcrfpy.Caption(100, 230, "Different Colors:")
color_label.font = mcrfpy.default_font
color_label.font_color = (200, 200, 200)
white_text = mcrfpy.Caption(300, 230, "White Text")
white_text.font = mcrfpy.default_font
white_text.font_color = (255, 255, 255)
green_text = mcrfpy.Caption(300, 260, "Green Text")
green_text.font = mcrfpy.default_font
green_text.font_color = (100, 255, 100)
red_text = mcrfpy.Caption(300, 290, "Red Text")
red_text.font = mcrfpy.default_font
red_text.font_color = (255, 100, 100)
blue_text = mcrfpy.Caption(300, 320, "Blue Text")
blue_text.font = mcrfpy.default_font
blue_text.font_color = (100, 150, 255)
# Caption with background
bg_label = mcrfpy.Caption(100, 380, "With Background:")
bg_label.font = mcrfpy.default_font
bg_label.font_color = (200, 200, 200)
# Frame background
frame = mcrfpy.Frame(280, 370, 250, 50)
frame.bgcolor = (64, 64, 128)
frame.outline = 2
framed_text = mcrfpy.Caption(405, 395, "Caption on Frame")
framed_text.font = mcrfpy.default_font
framed_text.font_size = 18
framed_text.font_color = (255, 255, 255)
framed_text.centered = True
# Centered text example
center_label = mcrfpy.Caption(100, 460, "Centered Text:")
center_label.font = mcrfpy.default_font
center_label.font_color = (200, 200, 200)
centered = mcrfpy.Caption(400, 460, "This text is centered")
centered.font = mcrfpy.default_font
centered.font_size = 20
centered.font_color = (255, 255, 100)
centered.centered = True
# Multi-line example
multi_label = mcrfpy.Caption(100, 520, "Multi-line:")
multi_label.font = mcrfpy.default_font
multi_label.font_color = (200, 200, 200)
multiline = mcrfpy.Caption(300, 520, "Line 1: McRogueFace\nLine 2: Game Engine\nLine 3: Python API")
multiline.font = mcrfpy.default_font
multiline.font_size = 14
multiline.font_color = (255, 255, 255)
# Add all to scene
ui = mcrfpy.sceneUI("captions")
ui.append(title)
ui.append(size_label)
ui.append(large)
ui.append(medium)
ui.append(small)
ui.append(color_label)
ui.append(white_text)
ui.append(green_text)
ui.append(red_text)
ui.append(blue_text)
ui.append(bg_label)
ui.append(frame)
ui.append(framed_text)
ui.append(center_label)
ui.append(centered)
ui.append(multi_label)
ui.append(multiline)
# Switch to scene
mcrfpy.setScene("captions")
# Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_caption, 100)

View File

@ -0,0 +1,451 @@
#!/usr/bin/env python3
"""Generate documentation screenshots for McRogueFace UI elements"""
import mcrfpy
from mcrfpy import automation
import sys
import os
# Crypt of Sokoban color scheme
FRAME_COLOR = mcrfpy.Color(64, 64, 128)
SHADOW_COLOR = mcrfpy.Color(64, 64, 86)
BOX_COLOR = mcrfpy.Color(96, 96, 160)
WHITE = mcrfpy.Color(255, 255, 255)
BLACK = mcrfpy.Color(0, 0, 0)
GREEN = mcrfpy.Color(0, 255, 0)
RED = mcrfpy.Color(255, 0, 0)
# Create texture for sprites
sprite_texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Output directory - create it during setup
output_dir = "mcrogueface.github.io/images"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
def create_caption(x, y, text, font_size=16, text_color=WHITE, outline_color=BLACK):
"""Helper function to create captions with common settings"""
caption = mcrfpy.Caption(mcrfpy.Vector(x, y), text=text)
caption.size = font_size
caption.fill_color = text_color
caption.outline_color = outline_color
return caption
def create_caption_example():
"""Create a scene showing Caption UI element examples"""
mcrfpy.createScene("caption_example")
ui = mcrfpy.sceneUI("caption_example")
# Background frame
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
# Title caption
title = create_caption(200, 50, "Caption Examples", 32)
ui.append(title)
# Different sized captions
caption1 = create_caption(100, 150, "Large Caption (24pt)", 24)
ui.append(caption1)
caption2 = create_caption(100, 200, "Medium Caption (18pt)", 18, GREEN)
ui.append(caption2)
caption3 = create_caption(100, 240, "Small Caption (14pt)", 14, RED)
ui.append(caption3)
# Caption with background
caption_bg = mcrfpy.Frame(100, 300, 300, 50, fill_color=BOX_COLOR)
ui.append(caption_bg)
caption4 = create_caption(110, 315, "Caption with Background", 16)
ui.append(caption4)
def create_sprite_example():
"""Create a scene showing Sprite UI element examples"""
mcrfpy.createScene("sprite_example")
ui = mcrfpy.sceneUI("sprite_example")
# Background frame
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
# Title
title = create_caption(250, 50, "Sprite Examples", 32)
ui.append(title)
# Create a grid background for sprites
sprite_bg = mcrfpy.Frame(100, 150, 600, 300, fill_color=BOX_COLOR)
ui.append(sprite_bg)
# Player sprite (84)
player_label = create_caption(150, 180, "Player", 14)
ui.append(player_label)
player_sprite = mcrfpy.Sprite(150, 200, sprite_texture, 84, 3.0)
ui.append(player_sprite)
# Enemy sprites
enemy_label = create_caption(250, 180, "Enemies", 14)
ui.append(enemy_label)
enemy1 = mcrfpy.Sprite(250, 200, sprite_texture, 123, 3.0) # Basic enemy
ui.append(enemy1)
enemy2 = mcrfpy.Sprite(300, 200, sprite_texture, 107, 3.0) # Different enemy
ui.append(enemy2)
# Boulder sprite (66)
boulder_label = create_caption(400, 180, "Boulder", 14)
ui.append(boulder_label)
boulder_sprite = mcrfpy.Sprite(400, 200, sprite_texture, 66, 3.0)
ui.append(boulder_sprite)
# Exit sprites
exit_label = create_caption(500, 180, "Exit States", 14)
ui.append(exit_label)
exit_locked = mcrfpy.Sprite(500, 200, sprite_texture, 45, 3.0) # Locked
ui.append(exit_locked)
exit_open = mcrfpy.Sprite(550, 200, sprite_texture, 21, 3.0) # Open
ui.append(exit_open)
# Item sprites
item_label = create_caption(150, 300, "Items", 14)
ui.append(item_label)
treasure = mcrfpy.Sprite(150, 320, sprite_texture, 89, 3.0) # Treasure
ui.append(treasure)
sword = mcrfpy.Sprite(200, 320, sprite_texture, 222, 3.0) # Sword
ui.append(sword)
potion = mcrfpy.Sprite(250, 320, sprite_texture, 175, 3.0) # Potion
ui.append(potion)
# Button sprite
button_label = create_caption(350, 300, "Button", 14)
ui.append(button_label)
button = mcrfpy.Sprite(350, 320, sprite_texture, 250, 3.0)
ui.append(button)
def create_frame_example():
"""Create a scene showing Frame UI element examples"""
mcrfpy.createScene("frame_example")
ui = mcrfpy.sceneUI("frame_example")
# Background
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=SHADOW_COLOR)
ui.append(bg)
# Title
title = create_caption(250, 30, "Frame Examples", 32)
ui.append(title)
# Basic frame
frame1 = mcrfpy.Frame(50, 100, 200, 150, fill_color=FRAME_COLOR)
ui.append(frame1)
label1 = create_caption(60, 110, "Basic Frame", 16)
ui.append(label1)
# Frame with outline
frame2 = mcrfpy.Frame(300, 100, 200, 150, fill_color=BOX_COLOR,
outline_color=WHITE, outline=2.0)
ui.append(frame2)
label2 = create_caption(310, 110, "Frame with Outline", 16)
ui.append(label2)
# Nested frames
frame3 = mcrfpy.Frame(550, 100, 200, 150, fill_color=FRAME_COLOR,
outline_color=WHITE, outline=1)
ui.append(frame3)
inner_frame = mcrfpy.Frame(570, 130, 160, 90, fill_color=BOX_COLOR)
ui.append(inner_frame)
label3 = create_caption(560, 110, "Nested Frames", 16)
ui.append(label3)
# Complex layout with frames
main_frame = mcrfpy.Frame(50, 300, 700, 250, fill_color=FRAME_COLOR,
outline_color=WHITE, outline=2)
ui.append(main_frame)
# Add some UI elements inside
ui_label = create_caption(60, 310, "Complex UI Layout", 18)
ui.append(ui_label)
# Status panel
status_frame = mcrfpy.Frame(70, 350, 150, 180, fill_color=BOX_COLOR)
ui.append(status_frame)
status_label = create_caption(80, 360, "Status", 14)
ui.append(status_label)
# Inventory panel
inv_frame = mcrfpy.Frame(240, 350, 300, 180, fill_color=BOX_COLOR)
ui.append(inv_frame)
inv_label = create_caption(250, 360, "Inventory", 14)
ui.append(inv_label)
# Actions panel
action_frame = mcrfpy.Frame(560, 350, 170, 180, fill_color=BOX_COLOR)
ui.append(action_frame)
action_label = create_caption(570, 360, "Actions", 14)
ui.append(action_label)
def create_grid_example():
"""Create a scene showing Grid UI element examples"""
mcrfpy.createScene("grid_example")
ui = mcrfpy.sceneUI("grid_example")
# Background
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
# Title
title = create_caption(250, 30, "Grid Example", 32)
ui.append(title)
# Create a grid showing a small dungeon
grid = mcrfpy.Grid(20, 15, sprite_texture,
mcrfpy.Vector(100, 100), mcrfpy.Vector(320, 240))
# Set up dungeon tiles
# Floor tiles (index 48)
# Wall tiles (index 3)
for x in range(20):
for y in range(15):
if x == 0 or x == 19 or y == 0 or y == 14:
# Walls around edge
grid.at((x, y)).tilesprite = 3
grid.at((x, y)).walkable = False
else:
# Floor
grid.at((x, y)).tilesprite = 48
grid.at((x, y)).walkable = True
# Add some internal walls
for x in range(5, 15):
grid.at((x, 7)).tilesprite = 3
grid.at((x, 7)).walkable = False
for y in range(3, 8):
grid.at((10, y)).tilesprite = 3
grid.at((10, y)).walkable = False
# Add a door
grid.at((10, 7)).tilesprite = 131 # Door tile
grid.at((10, 7)).walkable = True
# Add to UI
ui.append(grid)
# Label
grid_label = create_caption(100, 480, "20x15 Grid with 2x scale - Simple Dungeon Layout", 16)
ui.append(grid_label)
def create_entity_example():
"""Create a scene showing Entity examples in a Grid"""
mcrfpy.createScene("entity_example")
ui = mcrfpy.sceneUI("entity_example")
# Background
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
# Title
title = create_caption(200, 30, "Entity Collection Example", 32)
ui.append(title)
# Create a grid for the entities
grid = mcrfpy.Grid(15, 10, sprite_texture,
mcrfpy.Vector(150, 100), mcrfpy.Vector(360, 240))
# Set all tiles to floor
for x in range(15):
for y in range(10):
grid.at((x, y)).tilesprite = 48
grid.at((x, y)).walkable = True
# Add walls
for x in range(15):
grid.at((x, 0)).tilesprite = 3
grid.at((x, 0)).walkable = False
grid.at((x, 9)).tilesprite = 3
grid.at((x, 9)).walkable = False
for y in range(10):
grid.at((0, y)).tilesprite = 3
grid.at((0, y)).walkable = False
grid.at((14, y)).tilesprite = 3
grid.at((14, y)).walkable = False
ui.append(grid)
# Add entities to the grid
# Player entity
player = mcrfpy.Entity(mcrfpy.Vector(3, 3), sprite_texture, 84, grid)
grid.entities.append(player)
# Enemy entities
enemy1 = mcrfpy.Entity(mcrfpy.Vector(7, 4), sprite_texture, 123, grid)
grid.entities.append(enemy1)
enemy2 = mcrfpy.Entity(mcrfpy.Vector(10, 6), sprite_texture, 107, grid)
grid.entities.append(enemy2)
# Boulder
boulder = mcrfpy.Entity(mcrfpy.Vector(5, 5), sprite_texture, 66, grid)
grid.entities.append(boulder)
# Treasure
treasure = mcrfpy.Entity(mcrfpy.Vector(12, 2), sprite_texture, 89, grid)
grid.entities.append(treasure)
# Exit (locked)
exit_door = mcrfpy.Entity(mcrfpy.Vector(12, 8), sprite_texture, 45, grid)
grid.entities.append(exit_door)
# Button
button = mcrfpy.Entity(mcrfpy.Vector(3, 7), sprite_texture, 250, grid)
grid.entities.append(button)
# Items
sword = mcrfpy.Entity(mcrfpy.Vector(8, 2), sprite_texture, 222, grid)
grid.entities.append(sword)
potion = mcrfpy.Entity(mcrfpy.Vector(6, 8), sprite_texture, 175, grid)
grid.entities.append(potion)
# Label
entity_label = create_caption(150, 500, "Grid with Entity Collection - Game Objects", 16)
ui.append(entity_label)
def create_combined_example():
"""Create a scene showing all UI elements combined"""
mcrfpy.createScene("combined_example")
ui = mcrfpy.sceneUI("combined_example")
# Background
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=SHADOW_COLOR)
ui.append(bg)
# Title
title = create_caption(200, 20, "McRogueFace UI Elements", 28)
ui.append(title)
# Main game area frame
game_frame = mcrfpy.Frame(20, 70, 500, 400, fill_color=FRAME_COLOR,
outline_color=WHITE, outline=2)
ui.append(game_frame)
# Grid inside game frame
grid = mcrfpy.Grid(12, 10, sprite_texture,
mcrfpy.Vector(30, 80), mcrfpy.Vector(480, 400))
for x in range(12):
for y in range(10):
if x == 0 or x == 11 or y == 0 or y == 9:
grid.at((x, y)).tilesprite = 3
grid.at((x, y)).walkable = False
else:
grid.at((x, y)).tilesprite = 48
grid.at((x, y)).walkable = True
# Add some entities
player = mcrfpy.Entity(mcrfpy.Vector(2, 2), sprite_texture, 84, grid)
grid.entities.append(player)
enemy = mcrfpy.Entity(mcrfpy.Vector(8, 6), sprite_texture, 123, grid)
grid.entities.append(enemy)
boulder = mcrfpy.Entity(mcrfpy.Vector(5, 4), sprite_texture, 66, grid)
grid.entities.append(boulder)
ui.append(grid)
# Status panel
status_frame = mcrfpy.Frame(540, 70, 240, 200, fill_color=BOX_COLOR,
outline_color=WHITE, outline=1)
ui.append(status_frame)
status_title = create_caption(550, 80, "Status", 20)
ui.append(status_title)
hp_label = create_caption(550, 120, "HP: 10/10", 16, GREEN)
ui.append(hp_label)
level_label = create_caption(550, 150, "Level: 1", 16)
ui.append(level_label)
# Inventory panel
inv_frame = mcrfpy.Frame(540, 290, 240, 180, fill_color=BOX_COLOR,
outline_color=WHITE, outline=1)
ui.append(inv_frame)
inv_title = create_caption(550, 300, "Inventory", 20)
ui.append(inv_title)
# Add some item sprites
item1 = mcrfpy.Sprite(560, 340, sprite_texture, 222, 2.0)
ui.append(item1)
item2 = mcrfpy.Sprite(610, 340, sprite_texture, 175, 2.0)
ui.append(item2)
# Message log
log_frame = mcrfpy.Frame(20, 490, 760, 90, fill_color=BOX_COLOR,
outline_color=WHITE, outline=1)
ui.append(log_frame)
log_msg = create_caption(30, 500, "Welcome to McRogueFace!", 14)
ui.append(log_msg)
# Set up all the scenes
print("Creating UI example scenes...")
create_caption_example()
create_sprite_example()
create_frame_example()
create_grid_example()
create_entity_example()
create_combined_example()
# Screenshot state
current_screenshot = 0
screenshots = [
("caption_example", "ui_caption_example.png"),
("sprite_example", "ui_sprite_example.png"),
("frame_example", "ui_frame_example.png"),
("grid_example", "ui_grid_example.png"),
("entity_example", "ui_entity_example.png"),
("combined_example", "ui_combined_example.png")
]
def take_screenshots(runtime):
"""Timer callback to take screenshots sequentially"""
global current_screenshot
if current_screenshot >= len(screenshots):
print("\nAll screenshots captured successfully!")
print(f"Screenshots saved to: {output_dir}/")
mcrfpy.exit()
return
scene_name, filename = screenshots[current_screenshot]
# Switch to the scene
mcrfpy.setScene(scene_name)
# Take screenshot after a short delay to ensure rendering
def capture():
global current_screenshot
full_path = f"{output_dir}/{filename}"
result = automation.screenshot(full_path)
print(f"Screenshot {current_screenshot + 1}/{len(screenshots)}: {filename} - {'Success' if result else 'Failed'}")
current_screenshot += 1
# Schedule next screenshot
mcrfpy.setTimer("next_screenshot", take_screenshots, 200)
# Give scene time to render
mcrfpy.setTimer("capture", lambda r: capture(), 100)
# Start with the first scene
mcrfpy.setScene("caption_example")
# Start the screenshot process
print(f"\nStarting screenshot capture of {len(screenshots)} scenes...")
mcrfpy.setTimer("start", take_screenshots, 500)
# Safety timeout
def safety_exit(runtime):
print("\nERROR: Safety timeout reached! Exiting...")
mcrfpy.exit()
mcrfpy.setTimer("safety", safety_exit, 30000)
print("Setup complete. Game loop starting...")

View File

@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Generate documentation screenshots for McRogueFace UI elements - Simple version"""
import mcrfpy
from mcrfpy import automation
import sys
import os
# Crypt of Sokoban color scheme
FRAME_COLOR = mcrfpy.Color(64, 64, 128)
SHADOW_COLOR = mcrfpy.Color(64, 64, 86)
BOX_COLOR = mcrfpy.Color(96, 96, 160)
WHITE = mcrfpy.Color(255, 255, 255)
BLACK = mcrfpy.Color(0, 0, 0)
GREEN = mcrfpy.Color(0, 255, 0)
RED = mcrfpy.Color(255, 0, 0)
# Create texture for sprites
sprite_texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Output directory
output_dir = "mcrogueface.github.io/images"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
def create_caption(x, y, text, font_size=16, text_color=WHITE, outline_color=BLACK):
"""Helper function to create captions with common settings"""
caption = mcrfpy.Caption(mcrfpy.Vector(x, y), text=text)
caption.size = font_size
caption.fill_color = text_color
caption.outline_color = outline_color
return caption
# Screenshot counter
screenshot_count = 0
total_screenshots = 4
def screenshot_and_continue(runtime):
"""Take a screenshot and move to the next scene"""
global screenshot_count
if screenshot_count == 0:
# Caption example
print("Creating Caption example...")
mcrfpy.createScene("caption_example")
ui = mcrfpy.sceneUI("caption_example")
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
title = create_caption(200, 50, "Caption Examples", 32)
ui.append(title)
caption1 = create_caption(100, 150, "Large Caption (24pt)", 24)
ui.append(caption1)
caption2 = create_caption(100, 200, "Medium Caption (18pt)", 18, GREEN)
ui.append(caption2)
caption3 = create_caption(100, 240, "Small Caption (14pt)", 14, RED)
ui.append(caption3)
caption_bg = mcrfpy.Frame(100, 300, 300, 50, fill_color=BOX_COLOR)
ui.append(caption_bg)
caption4 = create_caption(110, 315, "Caption with Background", 16)
ui.append(caption4)
mcrfpy.setScene("caption_example")
mcrfpy.setTimer("next1", lambda r: capture_screenshot("ui_caption_example.png"), 200)
elif screenshot_count == 1:
# Sprite example
print("Creating Sprite example...")
mcrfpy.createScene("sprite_example")
ui = mcrfpy.sceneUI("sprite_example")
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
title = create_caption(250, 50, "Sprite Examples", 32)
ui.append(title)
sprite_bg = mcrfpy.Frame(100, 150, 600, 300, fill_color=BOX_COLOR)
ui.append(sprite_bg)
player_label = create_caption(150, 180, "Player", 14)
ui.append(player_label)
player_sprite = mcrfpy.Sprite(150, 200, sprite_texture, 84, 3.0)
ui.append(player_sprite)
enemy_label = create_caption(250, 180, "Enemies", 14)
ui.append(enemy_label)
enemy1 = mcrfpy.Sprite(250, 200, sprite_texture, 123, 3.0)
ui.append(enemy1)
enemy2 = mcrfpy.Sprite(300, 200, sprite_texture, 107, 3.0)
ui.append(enemy2)
boulder_label = create_caption(400, 180, "Boulder", 14)
ui.append(boulder_label)
boulder_sprite = mcrfpy.Sprite(400, 200, sprite_texture, 66, 3.0)
ui.append(boulder_sprite)
exit_label = create_caption(500, 180, "Exit States", 14)
ui.append(exit_label)
exit_locked = mcrfpy.Sprite(500, 200, sprite_texture, 45, 3.0)
ui.append(exit_locked)
exit_open = mcrfpy.Sprite(550, 200, sprite_texture, 21, 3.0)
ui.append(exit_open)
mcrfpy.setScene("sprite_example")
mcrfpy.setTimer("next2", lambda r: capture_screenshot("ui_sprite_example.png"), 200)
elif screenshot_count == 2:
# Frame example
print("Creating Frame example...")
mcrfpy.createScene("frame_example")
ui = mcrfpy.sceneUI("frame_example")
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=SHADOW_COLOR)
ui.append(bg)
title = create_caption(250, 30, "Frame Examples", 32)
ui.append(title)
frame1 = mcrfpy.Frame(50, 100, 200, 150, fill_color=FRAME_COLOR)
ui.append(frame1)
label1 = create_caption(60, 110, "Basic Frame", 16)
ui.append(label1)
frame2 = mcrfpy.Frame(300, 100, 200, 150, fill_color=BOX_COLOR,
outline_color=WHITE, outline=2.0)
ui.append(frame2)
label2 = create_caption(310, 110, "Frame with Outline", 16)
ui.append(label2)
frame3 = mcrfpy.Frame(550, 100, 200, 150, fill_color=FRAME_COLOR,
outline_color=WHITE, outline=1)
ui.append(frame3)
inner_frame = mcrfpy.Frame(570, 130, 160, 90, fill_color=BOX_COLOR)
ui.append(inner_frame)
label3 = create_caption(560, 110, "Nested Frames", 16)
ui.append(label3)
mcrfpy.setScene("frame_example")
mcrfpy.setTimer("next3", lambda r: capture_screenshot("ui_frame_example.png"), 200)
elif screenshot_count == 3:
# Grid example
print("Creating Grid example...")
mcrfpy.createScene("grid_example")
ui = mcrfpy.sceneUI("grid_example")
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
title = create_caption(250, 30, "Grid Example", 32)
ui.append(title)
grid = mcrfpy.Grid(20, 15, sprite_texture,
mcrfpy.Vector(100, 100), mcrfpy.Vector(320, 240))
# Set up dungeon tiles
for x in range(20):
for y in range(15):
if x == 0 or x == 19 or y == 0 or y == 14:
# Walls
grid.at((x, y)).tilesprite = 3
grid.at((x, y)).walkable = False
else:
# Floor
grid.at((x, y)).tilesprite = 48
grid.at((x, y)).walkable = True
# Add some internal walls
for x in range(5, 15):
grid.at((x, 7)).tilesprite = 3
grid.at((x, 7)).walkable = False
for y in range(3, 8):
grid.at((10, y)).tilesprite = 3
grid.at((10, y)).walkable = False
# Add a door
grid.at((10, 7)).tilesprite = 131
grid.at((10, 7)).walkable = True
ui.append(grid)
grid_label = create_caption(100, 480, "20x15 Grid - Simple Dungeon Layout", 16)
ui.append(grid_label)
mcrfpy.setScene("grid_example")
mcrfpy.setTimer("next4", lambda r: capture_screenshot("ui_grid_example.png"), 200)
else:
print("\nAll screenshots captured successfully!")
print(f"Screenshots saved to: {output_dir}/")
mcrfpy.exit()
return
def capture_screenshot(filename):
"""Capture a screenshot"""
global screenshot_count
full_path = f"{output_dir}/{filename}"
result = automation.screenshot(full_path)
print(f"Screenshot {screenshot_count + 1}/{total_screenshots}: {filename} - {'Success' if result else 'Failed'}")
screenshot_count += 1
# Schedule next scene
mcrfpy.setTimer("continue", screenshot_and_continue, 300)
# Start the process
print("Starting screenshot generation...")
mcrfpy.setTimer("start", screenshot_and_continue, 500)
# Safety timeout
mcrfpy.setTimer("safety", lambda r: mcrfpy.exit(), 30000)
print("Setup complete. Game loop starting...")

View File

@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Generate entity documentation screenshot with proper font loading"""
import mcrfpy
from mcrfpy import automation
import sys
def capture_entity(runtime):
"""Capture entity example after render loop starts"""
# Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_entity_example.png")
print("Entity screenshot saved!")
# Exit after capturing
sys.exit(0)
# Create scene
mcrfpy.createScene("entities")
# Use the default font which is already loaded
# Instead of: font = mcrfpy.Font("assets/JetbrainsMono.ttf")
# We use: mcrfpy.default_font (which is already loaded by the engine)
# Title
title = mcrfpy.Caption((400, 30), "Entity Example - Roguelike Characters", font=mcrfpy.default_font)
#title.font = mcrfpy.default_font
#title.font_size = 24
title.size=24
#title.font_color = (255, 255, 255)
#title.text_color = (255,255,255)
# Create a grid background
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Create grid with entities - using 2x scale (32x32 pixel tiles)
#grid = mcrfpy.Grid((100, 100), (20, 15), texture, 16, 16) # I can never get the args right for this thing
t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
grid = mcrfpy.Grid(20, 15, t, (10, 10), (1014, 758))
grid.zoom = 2.0
#grid.texture = texture
# Define tile types
FLOOR = 58 # Stone floor
WALL = 11 # Stone wall
# Fill with floor
for x in range(20):
for y in range(15):
grid.at((x, y)).tilesprite = WALL
# Add walls around edges
for x in range(20):
grid.at((x, 0)).tilesprite = WALL
grid.at((x, 14)).tilesprite = WALL
for y in range(15):
grid.at((0, y)).tilesprite = WALL
grid.at((19, y)).tilesprite = WALL
# Create entities
# Player at center
player = mcrfpy.Entity((10, 7), t, 84)
#player.texture = texture
#player.sprite_index = 84 # Player sprite
# Enemies
rat1 = mcrfpy.Entity((5, 5), t, 123)
#rat1.texture = texture
#rat1.sprite_index = 123 # Rat
rat2 = mcrfpy.Entity((15, 5), t, 123)
#rat2.texture = texture
#rat2.sprite_index = 123 # Rat
big_rat = mcrfpy.Entity((7, 10), t, 130)
#big_rat.texture = texture
#big_rat.sprite_index = 130 # Big rat
cyclops = mcrfpy.Entity((13, 10), t, 109)
#cyclops.texture = texture
#cyclops.sprite_index = 109 # Cyclops
# Items
chest = mcrfpy.Entity((3, 3), t, 89)
#chest.texture = texture
#chest.sprite_index = 89 # Chest
boulder = mcrfpy.Entity((10, 5), t, 66)
#boulder.texture = texture
#boulder.sprite_index = 66 # Boulder
key = mcrfpy.Entity((17, 12), t, 384)
#key.texture = texture
#key.sprite_index = 384 # Key
# Add all entities to grid
grid.entities.append(player)
grid.entities.append(rat1)
grid.entities.append(rat2)
grid.entities.append(big_rat)
grid.entities.append(cyclops)
grid.entities.append(chest)
grid.entities.append(boulder)
grid.entities.append(key)
# Labels
entity_label = mcrfpy.Caption((100, 580), "Entities move independently on the grid. Grid scale: 2x (32x32 pixels)")
#entity_label.font = mcrfpy.default_font
#entity_label.font_color = (255, 255, 255)
info = mcrfpy.Caption((100, 600), "Player (center), Enemies (rats, cyclops), Items (chest, boulder, key)")
#info.font = mcrfpy.default_font
#info.font_size = 14
#info.font_color = (200, 200, 200)
# Legend frame
legend_frame = mcrfpy.Frame(50, 50, 200, 150)
#legend_frame.bgcolor = (64, 64, 128)
#legend_frame.outline = 2
legend_title = mcrfpy.Caption((150, 60), "Entity Types")
#legend_title.font = mcrfpy.default_font
#legend_title.font_color = (255, 255, 255)
#legend_title.centered = True
#legend_text = mcrfpy.Caption((60, 90), "Player: @\nRat: r\nBig Rat: R\nCyclops: C\nChest: $\nBoulder: O\nKey: k")
#legend_text.font = mcrfpy.default_font
#legend_text.font_size = 12
#legend_text.font_color = (255, 255, 255)
# Add all to scene
ui = mcrfpy.sceneUI("entities")
ui.append(grid)
ui.append(title)
ui.append(entity_label)
ui.append(info)
ui.append(legend_frame)
ui.append(legend_title)
#ui.append(legend_text)
# Switch to scene
mcrfpy.setScene("entities")
# Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_entity, 100)

View File

@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""Generate grid documentation screenshot for McRogueFace"""
import mcrfpy
from mcrfpy import automation
import sys
def capture_grid(runtime):
"""Capture grid example after render loop starts"""
# Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_grid_example.png")
print("Grid screenshot saved!")
# Exit after capturing
sys.exit(0)
# Create scene
mcrfpy.createScene("grid")
# Load texture
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Title
title = mcrfpy.Caption(400, 30, "Grid Example - Dungeon View")
title.font = mcrfpy.default_font
title.font_size = 24
title.font_color = (255, 255, 255)
# Create main grid (20x15 tiles, each 32x32 pixels)
grid = mcrfpy.Grid(100, 100, 20, 15, texture, 32, 32)
grid.texture = texture
# Define tile types from Crypt of Sokoban
FLOOR = 58 # Stone floor
WALL = 11 # Stone wall
DOOR = 28 # Closed door
CHEST = 89 # Treasure chest
BUTTON = 250 # Floor button
EXIT = 45 # Locked exit
BOULDER = 66 # Boulder
# Create a simple dungeon room layout
# Fill with walls first
for x in range(20):
for y in range(15):
grid.set_tile(x, y, WALL)
# Carve out room
for x in range(2, 18):
for y in range(2, 13):
grid.set_tile(x, y, FLOOR)
# Add door
grid.set_tile(10, 2, DOOR)
# Add some features
grid.set_tile(5, 5, CHEST)
grid.set_tile(15, 10, BUTTON)
grid.set_tile(10, 12, EXIT)
grid.set_tile(8, 8, BOULDER)
grid.set_tile(12, 8, BOULDER)
# Create some entities on the grid
# Player entity
player = mcrfpy.Entity(5, 7)
player.texture = texture
player.sprite_index = 84 # Player sprite
# Enemy entities
rat1 = mcrfpy.Entity(12, 5)
rat1.texture = texture
rat1.sprite_index = 123 # Rat
rat2 = mcrfpy.Entity(14, 9)
rat2.texture = texture
rat2.sprite_index = 123 # Rat
cyclops = mcrfpy.Entity(10, 10)
cyclops.texture = texture
cyclops.sprite_index = 109 # Cyclops
# Add entities to grid
grid.entities.append(player)
grid.entities.append(rat1)
grid.entities.append(rat2)
grid.entities.append(cyclops)
# Create a smaller grid showing tile palette
palette_label = mcrfpy.Caption(100, 600, "Tile Types:")
palette_label.font = mcrfpy.default_font
palette_label.font_color = (255, 255, 255)
palette = mcrfpy.Grid(250, 580, 7, 1, texture, 32, 32)
palette.texture = texture
palette.set_tile(0, 0, FLOOR)
palette.set_tile(1, 0, WALL)
palette.set_tile(2, 0, DOOR)
palette.set_tile(3, 0, CHEST)
palette.set_tile(4, 0, BUTTON)
palette.set_tile(5, 0, EXIT)
palette.set_tile(6, 0, BOULDER)
# Labels for palette
labels = ["Floor", "Wall", "Door", "Chest", "Button", "Exit", "Boulder"]
for i, label in enumerate(labels):
l = mcrfpy.Caption(250 + i * 32, 615, label)
l.font = mcrfpy.default_font
l.font_size = 10
l.font_color = (255, 255, 255)
mcrfpy.sceneUI("grid").append(l)
# Add info caption
info = mcrfpy.Caption(100, 680, "Grid supports tiles and entities. Entities can move independently of the tile grid.")
info.font = mcrfpy.default_font
info.font_size = 14
info.font_color = (200, 200, 200)
# Add all elements to scene
ui = mcrfpy.sceneUI("grid")
ui.append(title)
ui.append(grid)
ui.append(palette_label)
ui.append(palette)
ui.append(info)
# Switch to scene
mcrfpy.setScene("grid")
# Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_grid, 100)

View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Generate sprite documentation screenshots for McRogueFace"""
import mcrfpy
from mcrfpy import automation
import sys
def capture_sprites(runtime):
"""Capture sprite examples after render loop starts"""
# Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_sprite_example.png")
print("Sprite screenshot saved!")
# Exit after capturing
sys.exit(0)
# Create scene
mcrfpy.createScene("sprites")
# Load texture
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Title
title = mcrfpy.Caption(400, 30, "Sprite Examples")
title.font = mcrfpy.default_font
title.font_size = 24
title.font_color = (255, 255, 255)
# Create a frame background
frame = mcrfpy.Frame(50, 80, 700, 500)
frame.bgcolor = (64, 64, 128)
frame.outline = 2
# Player sprite
player_label = mcrfpy.Caption(100, 120, "Player")
player_label.font = mcrfpy.default_font
player_label.font_color = (255, 255, 255)
player = mcrfpy.Sprite(120, 150)
player.texture = texture
player.sprite_index = 84 # Player sprite
player.scale = (3.0, 3.0)
# Enemy sprites
enemy_label = mcrfpy.Caption(250, 120, "Enemies")
enemy_label.font = mcrfpy.default_font
enemy_label.font_color = (255, 255, 255)
rat = mcrfpy.Sprite(250, 150)
rat.texture = texture
rat.sprite_index = 123 # Rat
rat.scale = (3.0, 3.0)
big_rat = mcrfpy.Sprite(320, 150)
big_rat.texture = texture
big_rat.sprite_index = 130 # Big rat
big_rat.scale = (3.0, 3.0)
cyclops = mcrfpy.Sprite(390, 150)
cyclops.texture = texture
cyclops.sprite_index = 109 # Cyclops
cyclops.scale = (3.0, 3.0)
# Items row
items_label = mcrfpy.Caption(100, 250, "Items")
items_label.font = mcrfpy.default_font
items_label.font_color = (255, 255, 255)
# Boulder
boulder = mcrfpy.Sprite(100, 280)
boulder.texture = texture
boulder.sprite_index = 66 # Boulder
boulder.scale = (3.0, 3.0)
# Chest
chest = mcrfpy.Sprite(170, 280)
chest.texture = texture
chest.sprite_index = 89 # Closed chest
chest.scale = (3.0, 3.0)
# Key
key = mcrfpy.Sprite(240, 280)
key.texture = texture
key.sprite_index = 384 # Key
key.scale = (3.0, 3.0)
# Button
button = mcrfpy.Sprite(310, 280)
button.texture = texture
button.sprite_index = 250 # Button
button.scale = (3.0, 3.0)
# UI elements row
ui_label = mcrfpy.Caption(100, 380, "UI Elements")
ui_label.font = mcrfpy.default_font
ui_label.font_color = (255, 255, 255)
# Hearts
heart_full = mcrfpy.Sprite(100, 410)
heart_full.texture = texture
heart_full.sprite_index = 210 # Full heart
heart_full.scale = (3.0, 3.0)
heart_half = mcrfpy.Sprite(170, 410)
heart_half.texture = texture
heart_half.sprite_index = 209 # Half heart
heart_half.scale = (3.0, 3.0)
heart_empty = mcrfpy.Sprite(240, 410)
heart_empty.texture = texture
heart_empty.sprite_index = 208 # Empty heart
heart_empty.scale = (3.0, 3.0)
# Armor
armor = mcrfpy.Sprite(340, 410)
armor.texture = texture
armor.sprite_index = 211 # Armor
armor.scale = (3.0, 3.0)
# Scale demonstration
scale_label = mcrfpy.Caption(500, 120, "Scale Demo")
scale_label.font = mcrfpy.default_font
scale_label.font_color = (255, 255, 255)
# Same sprite at different scales
for i, scale in enumerate([1.0, 2.0, 3.0, 4.0]):
s = mcrfpy.Sprite(500 + i * 60, 150)
s.texture = texture
s.sprite_index = 84 # Player
s.scale = (scale, scale)
mcrfpy.sceneUI("sprites").append(s)
# Add all elements to scene
ui = mcrfpy.sceneUI("sprites")
ui.append(frame)
ui.append(title)
ui.append(player_label)
ui.append(player)
ui.append(enemy_label)
ui.append(rat)
ui.append(big_rat)
ui.append(cyclops)
ui.append(items_label)
ui.append(boulder)
ui.append(chest)
ui.append(key)
ui.append(button)
ui.append(ui_label)
ui.append(heart_full)
ui.append(heart_half)
ui.append(heart_empty)
ui.append(armor)
ui.append(scale_label)
# Switch to scene
mcrfpy.setScene("sprites")
# Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_sprites, 100)

View File

@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""
Test for keypressScene() validation - should reject non-callable arguments
"""
def test_keypress_validation(timer_name):
"""Test that keypressScene validates its argument is callable"""
import mcrfpy
import sys
print("Testing keypressScene() validation...")
# Create test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Test 1: Valid callable (function)
def key_handler(key, action):
print(f"Key pressed: {key}, action: {action}")
try:
mcrfpy.keypressScene(key_handler)
print("✓ Accepted valid function as key handler")
except Exception as e:
print(f"✗ Rejected valid function: {e}")
raise
# Test 2: Valid callable (lambda)
try:
mcrfpy.keypressScene(lambda k, a: None)
print("✓ Accepted valid lambda as key handler")
except Exception as e:
print(f"✗ Rejected valid lambda: {e}")
raise
# Test 3: Invalid - string
try:
mcrfpy.keypressScene("not callable")
print("✗ Should have rejected string as key handler")
except TypeError as e:
print(f"✓ Correctly rejected string: {e}")
except Exception as e:
print(f"✗ Wrong exception type for string: {e}")
raise
# Test 4: Invalid - number
try:
mcrfpy.keypressScene(42)
print("✗ Should have rejected number as key handler")
except TypeError as e:
print(f"✓ Correctly rejected number: {e}")
except Exception as e:
print(f"✗ Wrong exception type for number: {e}")
raise
# Test 5: Invalid - None
try:
mcrfpy.keypressScene(None)
print("✗ Should have rejected None as key handler")
except TypeError as e:
print(f"✓ Correctly rejected None: {e}")
except Exception as e:
print(f"✗ Wrong exception type for None: {e}")
raise
# Test 6: Invalid - dict
try:
mcrfpy.keypressScene({"not": "callable"})
print("✗ Should have rejected dict as key handler")
except TypeError as e:
print(f"✓ Correctly rejected dict: {e}")
except Exception as e:
print(f"✗ Wrong exception type for dict: {e}")
raise
# Test 7: Valid callable class instance
class KeyHandler:
def __call__(self, key, action):
print(f"Class handler: {key}, {action}")
try:
mcrfpy.keypressScene(KeyHandler())
print("✓ Accepted valid callable class instance")
except Exception as e:
print(f"✗ Rejected valid callable class: {e}")
raise
print("\n✅ keypressScene() validation test PASSED")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_keypress_validation, 100)

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Test and workaround for transparent screenshot issue"""
import mcrfpy
from mcrfpy import automation
from datetime import datetime
import sys
def test_transparency_workaround():
"""Create a full-window opaque background to fix transparency"""
print("=== Screenshot Transparency Fix Test ===\n")
# Create a scene
mcrfpy.createScene("opaque_test")
mcrfpy.setScene("opaque_test")
ui = mcrfpy.sceneUI("opaque_test")
# WORKAROUND: Create a full-window opaque frame as the first element
# This acts as an opaque background since the scene clears with transparent
print("Creating full-window opaque background...")
background = mcrfpy.Frame(0, 0, 1024, 768,
fill_color=mcrfpy.Color(50, 50, 50), # Dark gray
outline_color=None,
outline=0.0)
ui.append(background)
print("✓ Added opaque background frame")
# Now add normal content on top
print("\nAdding test content...")
# Red frame
frame1 = mcrfpy.Frame(100, 100, 200, 150,
fill_color=mcrfpy.Color(255, 0, 0),
outline_color=mcrfpy.Color(255, 255, 255),
outline=3.0)
ui.append(frame1)
# Green frame
frame2 = mcrfpy.Frame(350, 100, 200, 150,
fill_color=mcrfpy.Color(0, 255, 0),
outline_color=mcrfpy.Color(0, 0, 0),
outline=3.0)
ui.append(frame2)
# Blue frame
frame3 = mcrfpy.Frame(100, 300, 200, 150,
fill_color=mcrfpy.Color(0, 0, 255),
outline_color=mcrfpy.Color(255, 255, 0),
outline=3.0)
ui.append(frame3)
# Add text
caption = mcrfpy.Caption(mcrfpy.Vector(250, 50),
text="OPAQUE BACKGROUND TEST",
fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 32
ui.append(caption)
# Take screenshot
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"screenshot_opaque_fix_{timestamp}.png"
result = automation.screenshot(filename)
print(f"\nScreenshot taken: {filename}")
print(f"Result: {result}")
print("\n=== Analysis ===")
print("The issue is that PyScene::render() calls clear() without a color parameter.")
print("SFML's default clear color is transparent black (0,0,0,0).")
print("In windowed mode, the window provides an opaque background.")
print("In headless mode, the RenderTexture preserves the transparency.")
print("\nWORKAROUND: Always add a full-window opaque Frame as the first UI element.")
print("FIX: Modify PyScene.cpp and UITestScene.cpp to use clear(sf::Color::Black)")
sys.exit(0)
# Run immediately
test_transparency_workaround()

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Simple screenshot test to verify automation API"""
import mcrfpy
from mcrfpy import automation
import sys
import time
def take_screenshot(runtime):
"""Take screenshot after render starts"""
print(f"Timer callback fired at runtime: {runtime}")
# Try different paths
paths = [
"test_screenshot.png",
"./test_screenshot.png",
"mcrogueface.github.io/images/test_screenshot.png"
]
for path in paths:
try:
print(f"Trying to save to: {path}")
automation.screenshot(path)
print(f"Success: {path}")
except Exception as e:
print(f"Failed {path}: {e}")
sys.exit(0)
# Create minimal scene
mcrfpy.createScene("test")
# Add a visible element
caption = mcrfpy.Caption(100, 100, "Screenshot Test")
caption.font = mcrfpy.default_font
caption.font_color = (255, 255, 255)
caption.font_size = 24
mcrfpy.sceneUI("test").append(caption)
mcrfpy.setScene("test")
# Use timer to ensure rendering has started
print("Setting timer...")
mcrfpy.setTimer("screenshot", take_screenshot, 500) # Wait 0.5 seconds
print("Timer set, entering game loop...")

View File

@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Simplified test to verify timer-based screenshots work"""
import mcrfpy
from mcrfpy import automation
# Counter to track timer calls
call_count = 0
def take_screenshot_and_exit():
"""Timer callback that takes screenshot then exits"""
global call_count
call_count += 1
print(f"\nTimer callback fired! (call #{call_count})")
# Take screenshot
filename = f"timer_screenshot_test_{call_count}.png"
result = automation.screenshot(filename)
print(f"Screenshot result: {result} -> {filename}")
# Exit after first call
if call_count >= 1:
print("Exiting game...")
mcrfpy.exit()
# Set up a simple scene
print("Creating test scene...")
mcrfpy.createScene("test")
mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# Add visible content - a white frame on default background
frame = mcrfpy.Frame(100, 100, 200, 200,
fill_color=mcrfpy.Color(255, 255, 255))
ui.append(frame)
print("Setting timer to fire in 100ms...")
mcrfpy.setTimer("screenshot_timer", take_screenshot_and_exit, 100)
print("Setup complete. Game loop starting...")

View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Test if closing stdin prevents the >>> prompt"""
import mcrfpy
import sys
import os
print("=== Testing stdin theory ===")
print(f"stdin.isatty(): {sys.stdin.isatty()}")
print(f"stdin fileno: {sys.stdin.fileno()}")
# Set up a basic scene
mcrfpy.createScene("stdin_test")
mcrfpy.setScene("stdin_test")
# Try to prevent interactive mode by closing stdin
print("\nAttempting to prevent interactive mode...")
try:
# Method 1: Close stdin
sys.stdin.close()
print("Closed sys.stdin")
except:
print("Failed to close sys.stdin")
try:
# Method 2: Redirect stdin to /dev/null
devnull = open(os.devnull, 'r')
os.dup2(devnull.fileno(), 0)
print("Redirected stdin to /dev/null")
except:
print("Failed to redirect stdin")
print("\nScript complete. If >>> still appears, the issue is elsewhere.")

View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Trace execution behavior to understand the >>> prompt"""
import mcrfpy
import sys
import traceback
print("=== Tracing Execution ===")
print(f"Python version: {sys.version}")
print(f"sys.argv: {sys.argv}")
print(f"__name__: {__name__}")
# Check if we're in interactive mode
print(f"sys.flags.interactive: {sys.flags.interactive}")
print(f"sys.flags.inspect: {sys.flags.inspect}")
# Check sys.ps1 (interactive prompt)
if hasattr(sys, 'ps1'):
print(f"sys.ps1 exists: '{sys.ps1}'")
else:
print("sys.ps1 not set (not in interactive mode)")
# Create a simple scene
mcrfpy.createScene("trace_test")
mcrfpy.setScene("trace_test")
print(f"Current scene: {mcrfpy.currentScene()}")
# Set a timer that should fire
def timer_test():
print("\n!!! Timer fired successfully !!!")
mcrfpy.delTimer("trace_timer")
# Try to exit
print("Attempting to exit...")
mcrfpy.exit()
print("Setting timer...")
mcrfpy.setTimer("trace_timer", timer_test, 500)
print("\n=== Script execution complete ===")
print("If you see >>> after this, Python entered interactive mode")
print("The game loop should start now...")
# Try to ensure we don't enter interactive mode
if hasattr(sys, 'ps1'):
del sys.ps1
# Explicitly NOT calling sys.exit() to let the game loop run

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
"""Trace interactive mode by monkey-patching"""
import sys
import mcrfpy
# Monkey-patch to detect interactive mode
original_ps1 = None
if hasattr(sys, 'ps1'):
original_ps1 = sys.ps1
class PS1Detector:
def __repr__(self):
import traceback
print("\n!!! sys.ps1 accessed! Stack trace:")
traceback.print_stack()
return ">>> "
# Set our detector
sys.ps1 = PS1Detector()
print("Trace script loaded, ps1 detector installed")
# Do nothing else - let the game run

View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Test for Entity class - Related to issue #73 (index() method)"""
import mcrfpy
from datetime import datetime
print("Test script starting...")
def test_Entity():
"""Test Entity class and index() method for collection removal"""
# Create test scene with grid
mcrfpy.createScene("entity_test")
mcrfpy.setScene("entity_test")
ui = mcrfpy.sceneUI("entity_test")
# Create a grid
grid = mcrfpy.Grid(10, 10,
mcrfpy.default_texture,
mcrfpy.Vector(10, 10),
mcrfpy.Vector(400, 400))
ui.append(grid)
entities = grid.entities
# Create multiple entities
entity1 = mcrfpy.Entity(mcrfpy.Vector(2, 2), mcrfpy.default_texture, 0, grid)
entity2 = mcrfpy.Entity(mcrfpy.Vector(5, 5), mcrfpy.default_texture, 1, grid)
entity3 = mcrfpy.Entity(mcrfpy.Vector(7, 7), mcrfpy.default_texture, 2, grid)
entities.append(entity1)
entities.append(entity2)
entities.append(entity3)
print(f"Created {len(entities)} entities")
# Test entity properties
try:
print(f" Entity1 pos: {entity1.pos}")
print(f" Entity1 draw_pos: {entity1.draw_pos}")
print(f" Entity1 sprite_number: {entity1.sprite_number}")
# Modify properties
entity1.pos = mcrfpy.Vector(3, 3)
entity1.sprite_number = 5
print(" Entity properties modified")
except Exception as e:
print(f"X Entity property access failed: {e}")
# Test gridstate access
try:
gridstate = entity2.gridstate
print(" Entity gridstate accessible")
# Test at() method
point_state = entity2.at()#.at(0, 0)
print(" Entity at() method works")
except Exception as e:
print(f"X Entity gridstate/at() failed: {e}")
# Test index() method (Issue #73)
print("\nTesting index() method (Issue #73)...")
try:
# Try to find entity2's index
index = entity2.index()
print(f":) index() method works: entity2 is at index {index}")
# Verify by checking collection
if entities[index] == entity2:
print("✓ Index is correct")
else:
print("✗ Index mismatch")
# Remove using index
entities.remove(index)
print(f":) Removed entity using index, now {len(entities)} entities")
except AttributeError:
print("✗ index() method not implemented (Issue #73)")
# Try manual removal as workaround
try:
for i in range(len(entities)):
if entities[i] == entity2:
entities.remove(i)
print(":) Manual removal workaround succeeded")
break
except:
print("✗ Manual removal also failed")
except Exception as e:
print(f":) index() method error: {e}")
# Test EntityCollection iteration
try:
positions = []
for entity in entities:
positions.append(entity.pos)
print(f":) Entity iteration works: {len(positions)} entities")
except Exception as e:
print(f"X Entity iteration failed: {e}")
# Test EntityCollection extend (Issue #27)
try:
new_entities = [
mcrfpy.Entity(mcrfpy.Vector(1, 1), mcrfpy.default_texture, 3, grid),
mcrfpy.Entity(mcrfpy.Vector(9, 9), mcrfpy.default_texture, 4, grid)
]
entities.extend(new_entities)
print(f":) extend() method works: now {len(entities)} entities")
except AttributeError:
print("✗ extend() method not implemented (Issue #27)")
except Exception as e:
print(f"X extend() method error: {e}")
# Skip screenshot in headless mode
print("PASS")
# Run test immediately in headless mode
print("Running test immediately...")
test_Entity()
print("Test completed.")

Some files were not shown because too many files have changed in this diff Show More