diff --git a/.gitignore b/.gitignore index aaee9ad..4bd78c1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,26 +8,26 @@ PCbuild obj build lib -obj __pycache__ .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_* - tcod_reference .archive + +# Keep important documentation and tests +!CLAUDE.md +!README.md +!tests/ diff --git a/CLAUDE.md b/CLAUDE.md index 73c46f4..b9e553c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -238,25 +238,72 @@ After building, the executable expects: 2. Expose to Python using the existing binding pattern 3. Update Python scripts to use new functionality -## Testing Game Changes +## Testing -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 +### Test Suite Structure + +The `tests/` directory contains the comprehensive test suite: + +``` +tests/ +├── run_tests.py # Test runner - executes all tests with timeout +├── unit/ # Unit tests for individual components (105+ tests) +├── integration/ # Integration tests for system interactions +├── regression/ # Bug regression tests (issue_XX_*.py) +├── benchmarks/ # Performance benchmarks +├── demo/ # Feature demonstration system +│ ├── demo_main.py # Interactive demo runner +│ ├── screens/ # Per-feature demo screens +│ └── screenshots/ # Generated demo screenshots +└── notes/ # Analysis files and documentation +``` + +### Running Tests + +```bash +# Run the full test suite (from tests/ directory) +cd tests && python3 run_tests.py + +# Run a specific test +cd build && ./mcrogueface --headless --exec ../tests/unit/some_test.py + +# Run the demo system interactively +cd build && ./mcrogueface ../tests/demo/demo_main.py + +# Generate demo screenshots (headless) +cd build && ./mcrogueface --headless --exec ../tests/demo/demo_main.py +``` + +### Reading Tests as Examples + +**IMPORTANT**: Before implementing a feature or fixing a bug, check existing tests for API usage examples: + +- `tests/unit/` - Shows correct usage of individual mcrfpy classes and functions +- `tests/demo/screens/` - Complete working examples of UI components +- `tests/regression/` - Documents edge cases and bug scenarios + +Example: To understand Animation API: +```bash +grep -r "Animation" tests/unit/ +cat tests/demo/screens/animation_demo.py +``` + +### Writing Tests + +**Always write tests when adding features or fixing bugs:** + +1. **For new features**: Create `tests/unit/feature_name_test.py` +2. **For bug fixes**: Create `tests/regression/issue_XX_description_test.py` +3. **For demos**: Add to `tests/demo/screens/` if it showcases a feature ### Quick Testing Commands ```bash -# Test basic functionality -make test - -# Run in Python interactive mode -make python - -# Test headless mode +# Test headless mode with inline Python cd build ./mcrogueface --headless -c "import mcrfpy; print('Headless test')" + +# Run specific test with output +./mcrogueface --headless --exec ../tests/unit/my_test.py 2>&1 ``` ## Common Development Tasks @@ -387,76 +434,82 @@ build/ ## 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 +- **Always write tests first**: Create tests in `./tests/` for all bugs and new features +- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix +- **Read existing tests**: Check `tests/unit/` and `tests/demo/screens/` for API examples before writing code +- **Close the loop**: Reproduce issue → change code → recompile → run test → verify ### 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 +# tests/unit/my_feature_test.py import mcrfpy +import sys -# Test code here -result = mcrfpy.some_function() -assert result == expected_value -print("PASS" if condition else "FAIL") +# Test code - runs immediately +frame = mcrfpy.Frame(pos=(0,0), size=(100,100)) +assert frame.x == 0 +assert frame.w == 100 + +print("PASS") +sys.exit(0) ``` #### 2. Game Loop Tests (Timer-Based) -For tests requiring rendering, game state, or elapsed time: +For tests requiring rendering, screenshots, or elapsed time: ```python +# tests/unit/my_visual_test.py 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") + # Validate results... + print("PASS") 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 +ui = mcrfpy.sceneUI("test") +ui.append(mcrfpy.Frame(pos=(50,50), size=(100,100))) +mcrfpy.setScene("test") +mcrfpy.setTimer("test", run_test, 100) ``` ### 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` +- **Timer callbacks are essential**: Screenshots only work after the render loop starts +- **Use automation API**: `automation.screenshot()`, `automation.click()` for visual testing +- **Exit properly**: Always call `sys.exit(0)` for PASS or `sys.exit(1)` for FAIL +- **Headless mode**: Use `--headless --exec` for CI/automated testing +- **Check examples first**: Read `tests/demo/screens/*.py` for correct API usage -### Example Test Pattern -```bash -# Run a test that requires game loop -./build/mcrogueface --headless --exec tests/issue_78_middle_click_test.py +### API Quick Reference (from tests) +```python +# Animation: (property, target_value, duration, easing) +anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut") +anim.start(frame) -# 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 +# Caption: use keyword arguments to avoid positional conflicts +cap = mcrfpy.Caption(text="Hello", pos=(100, 100)) + +# Grid center: uses pixel coordinates, not cell coordinates +grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 50), size=(400, 300)) +grid.center = (120, 80) # pixels: (cells * cell_size / 2) + +# Keyboard handler: key names are "Num1", "Num2", "Escape", "Q", etc. +def on_key(key, state): + if key == "Num1" and state == "start": + mcrfpy.setScene("demo_1") ``` ## Development Best Practices ### Testing and Deployment -- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, and tests shouldn't be included +- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, tests shouldn't be included +- **Run full suite before commits**: `cd tests && python3 run_tests.py` ## Documentation Guidelines diff --git a/tests/archive/generate_caption_screenshot_fixed.py b/tests/archive/generate_caption_screenshot_fixed.py deleted file mode 100644 index 66234cb..0000000 --- a/tests/archive/generate_caption_screenshot_fixed.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/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) \ No newline at end of file diff --git a/tests/archive/generate_docs_screenshots_simple.py b/tests/archive/generate_docs_screenshots_simple.py deleted file mode 100755 index 75712f4..0000000 --- a/tests/archive/generate_docs_screenshots_simple.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/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...") \ No newline at end of file diff --git a/tests/archive/generate_entity_screenshot_fixed.py b/tests/archive/generate_entity_screenshot_fixed.py deleted file mode 100644 index 4855319..0000000 --- a/tests/archive/generate_entity_screenshot_fixed.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/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) diff --git a/tests/archive/path_vision_fixed.py b/tests/archive/path_vision_fixed.py deleted file mode 100644 index ee4c804..0000000 --- a/tests/archive/path_vision_fixed.py +++ /dev/null @@ -1,375 +0,0 @@ -#!/usr/bin/env python3 -""" -Path & Vision Sizzle Reel (Fixed) -================================= - -Fixed version with proper animation chaining to prevent glitches. -""" - -import mcrfpy -import sys - -class PathAnimator: - """Handles step-by-step animation with proper completion tracking""" - - def __init__(self, entity, name="animator"): - self.entity = entity - self.name = name - self.path = [] - self.current_index = 0 - self.step_duration = 0.4 - self.animating = False - self.on_step = None - self.on_complete = None - - def set_path(self, path): - """Set the path to animate along""" - self.path = path - self.current_index = 0 - - def start(self): - """Start animating""" - if not self.path: - return - - self.animating = True - self.current_index = 0 - self._move_to_next() - - def stop(self): - """Stop animating""" - self.animating = False - mcrfpy.delTimer(f"{self.name}_check") - - def _move_to_next(self): - """Move to next position in path""" - if not self.animating or self.current_index >= len(self.path): - self.animating = False - if self.on_complete: - self.on_complete() - return - - # Get next position - x, y = self.path[self.current_index] - - # Create animations - anim_x = mcrfpy.Animation("x", float(x), self.step_duration, "easeInOut") - anim_y = mcrfpy.Animation("y", float(y), self.step_duration, "easeInOut") - - anim_x.start(self.entity) - anim_y.start(self.entity) - - # Update visibility - self.entity.update_visibility() - - # Callback for each step - if self.on_step: - self.on_step(self.current_index, x, y) - - # Schedule next move - delay = int(self.step_duration * 1000) + 50 # Add small buffer - mcrfpy.setTimer(f"{self.name}_next", self._handle_next, delay) - - def _handle_next(self, dt): - """Timer callback to move to next position""" - self.current_index += 1 - mcrfpy.delTimer(f"{self.name}_next") - self._move_to_next() - -# Global state -grid = None -player = None -enemy = None -player_animator = None -enemy_animator = None -demo_phase = 0 - -def create_scene(): - """Create the demo environment""" - global grid, player, enemy - - mcrfpy.createScene("fixed_demo") - - # Create grid - grid = mcrfpy.Grid(grid_x=30, grid_y=20) - grid.fill_color = mcrfpy.Color(20, 20, 30) - - # Simple dungeon layout - map_layout = [ - "##############################", - "#......#########.....#########", - "#......#########.....#########", - "#......#.........#...#########", - "#......#.........#...#########", - "####.###.........#.###########", - "####.............#.###########", - "####.............#.###########", - "####.###.........#.###########", - "#......#.........#...#########", - "#......#.........#...#########", - "#......#########.#...........#", - "#......#########.#...........#", - "#......#########.#...........#", - "#......#########.#############", - "####.###########.............#", - "####.........................#", - "####.###########.............#", - "#......#########.............#", - "##############################", - ] - - # Build map - for y, row in enumerate(map_layout): - for x, char in enumerate(row): - cell = grid.at(x, y) - if char == '#': - cell.walkable = False - cell.transparent = False - cell.color = mcrfpy.Color(40, 30, 30) - else: - cell.walkable = True - cell.transparent = True - cell.color = mcrfpy.Color(80, 80, 100) - - # Create entities - player = mcrfpy.Entity(3, 3, grid=grid) - player.sprite_index = 64 # @ - - enemy = mcrfpy.Entity(26, 16, grid=grid) - enemy.sprite_index = 69 # E - - # Initial visibility - player.update_visibility() - enemy.update_visibility() - - # Set initial perspective - grid.perspective = 0 - -def setup_ui(): - """Create UI elements""" - ui = mcrfpy.sceneUI("fixed_demo") - ui.append(grid) - - grid.position = (50, 80) - grid.size = (700, 500) - - title = mcrfpy.Caption("Path & Vision Demo (Fixed)", 300, 20) - title.fill_color = mcrfpy.Color(255, 255, 255) - ui.append(title) - - global status_text, perspective_text - status_text = mcrfpy.Caption("Initializing...", 50, 50) - status_text.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(status_text) - - perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50) - perspective_text.fill_color = mcrfpy.Color(100, 255, 100) - ui.append(perspective_text) - - controls = mcrfpy.Caption("Space: Start/Pause | R: Restart | Q: Quit", 250, 600) - controls.fill_color = mcrfpy.Color(150, 150, 150) - ui.append(controls) - -def update_camera_smooth(target, duration=0.3): - """Smoothly move camera to entity""" - center_x = target.x * 23 # Approximate pixel size - center_y = target.y * 23 - - cam_anim = mcrfpy.Animation("center", (center_x, center_y), duration, "easeOut") - cam_anim.start(grid) - -def start_demo(): - """Start the demo sequence""" - global demo_phase, player_animator, enemy_animator - - demo_phase = 1 - status_text.text = "Phase 1: Player movement with camera follow" - - # Player path - player_path = [ - (3, 3), (3, 6), (4, 6), (7, 6), (7, 8), - (10, 8), (13, 8), (16, 8), (16, 10), - (16, 13), (16, 16), (20, 16), (24, 16) - ] - - # Setup player animator - player_animator = PathAnimator(player, "player") - player_animator.set_path(player_path) - player_animator.step_duration = 0.5 - - def on_player_step(index, x, y): - """Called for each player step""" - status_text.text = f"Player step {index+1}/{len(player_path)}" - if grid.perspective == 0: - update_camera_smooth(player, 0.4) - - def on_player_complete(): - """Called when player path is complete""" - start_phase_2() - - player_animator.on_step = on_player_step - player_animator.on_complete = on_player_complete - player_animator.start() - -def start_phase_2(): - """Start enemy movement phase""" - global demo_phase - - demo_phase = 2 - status_text.text = "Phase 2: Enemy movement (may enter player's view)" - - # Enemy path - enemy_path = [ - (26, 16), (22, 16), (18, 16), (16, 16), - (16, 13), (16, 10), (16, 8), (13, 8), - (10, 8), (7, 8), (7, 6), (4, 6) - ] - - # Setup enemy animator - enemy_animator.set_path(enemy_path) - enemy_animator.step_duration = 0.4 - - def on_enemy_step(index, x, y): - """Check if enemy is visible to player""" - if grid.perspective == 0: - # Check if enemy is in player's view - enemy_idx = int(y) * grid.grid_x + int(x) - if enemy_idx < len(player.gridstate) and player.gridstate[enemy_idx].visible: - status_text.text = "Enemy spotted in player's view!" - - def on_enemy_complete(): - """Start perspective transition""" - start_phase_3() - - enemy_animator.on_step = on_enemy_step - enemy_animator.on_complete = on_enemy_complete - enemy_animator.start() - -def start_phase_3(): - """Dramatic perspective shift""" - global demo_phase - - demo_phase = 3 - status_text.text = "Phase 3: Perspective shift..." - - # Stop any ongoing animations - player_animator.stop() - enemy_animator.stop() - - # Zoom out - zoom_out = mcrfpy.Animation("zoom", 0.6, 2.0, "easeInExpo") - zoom_out.start(grid) - - # Schedule perspective switch - mcrfpy.setTimer("switch_persp", switch_perspective, 2100) - -def switch_perspective(dt): - """Switch to enemy perspective""" - grid.perspective = 1 - perspective_text.text = "Perspective: Enemy" - perspective_text.fill_color = mcrfpy.Color(255, 100, 100) - - # Update camera - update_camera_smooth(enemy, 0.5) - - # Zoom back in - zoom_in = mcrfpy.Animation("zoom", 1.0, 2.0, "easeOutExpo") - zoom_in.start(grid) - - status_text.text = "Now following enemy perspective" - - # Clean up timer - mcrfpy.delTimer("switch_persp") - - # Continue enemy movement after transition - mcrfpy.setTimer("continue_enemy", continue_enemy_movement, 2500) - -def continue_enemy_movement(dt): - """Continue enemy movement after perspective shift""" - mcrfpy.delTimer("continue_enemy") - - # Continue path - enemy_path_2 = [ - (4, 6), (3, 6), (3, 3), (3, 2), (3, 1) - ] - - enemy_animator.set_path(enemy_path_2) - - def on_step(index, x, y): - update_camera_smooth(enemy, 0.4) - status_text.text = f"Following enemy: step {index+1}" - - def on_complete(): - status_text.text = "Demo complete! Press R to restart" - - enemy_animator.on_step = on_step - enemy_animator.on_complete = on_complete - enemy_animator.start() - -# Control state -running = False - -def handle_keys(key, state): - """Handle keyboard input""" - global running - - if state != "start": - return - - key = key.lower() - - if key == "q": - sys.exit(0) - elif key == "space": - if not running: - running = True - start_demo() - else: - running = False - player_animator.stop() - enemy_animator.stop() - status_text.text = "Paused" - elif key == "r": - # Reset everything - player.x, player.y = 3, 3 - enemy.x, enemy.y = 26, 16 - grid.perspective = 0 - perspective_text.text = "Perspective: Player" - perspective_text.fill_color = mcrfpy.Color(100, 255, 100) - grid.zoom = 1.0 - update_camera_smooth(player, 0.5) - - if running: - player_animator.stop() - enemy_animator.stop() - running = False - - status_text.text = "Reset - Press SPACE to start" - -# Initialize -create_scene() -setup_ui() - -# Setup animators -player_animator = PathAnimator(player, "player") -enemy_animator = PathAnimator(enemy, "enemy") - -# Set scene -mcrfpy.setScene("fixed_demo") -mcrfpy.keypressScene(handle_keys) - -# Initial camera -grid.zoom = 1.0 -update_camera_smooth(player, 0.5) - -print("Path & Vision Demo (Fixed)") -print("==========================") -print("This version properly chains animations to prevent glitches.") -print() -print("The demo will:") -print("1. Move player with camera following") -print("2. Move enemy (may enter player's view)") -print("3. Dramatic perspective shift to enemy") -print("4. Continue following enemy") -print() -print("Press SPACE to start, Q to quit") \ No newline at end of file diff --git a/tests/archive/ui_Grid_test_simple.py b/tests/archive/ui_Grid_test_simple.py deleted file mode 100644 index d7897bc..0000000 --- a/tests/archive/ui_Grid_test_simple.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -"""Simple test for mcrfpy.Grid""" -import mcrfpy - -print("Starting Grid test...") - -# Create test scene -print("[DEBUG] Creating scene...") -mcrfpy.createScene("grid_test") -print("[DEBUG] Setting scene...") -mcrfpy.setScene("grid_test") -print("[DEBUG] Getting UI...") -ui = mcrfpy.sceneUI("grid_test") -print("[DEBUG] UI retrieved") - -# Test grid creation -try: - # Texture constructor: filename, sprite_width, sprite_height - # kenney_ice.png is 192x176, so 16x16 would give us 12x11 sprites - texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - print("[INFO] Texture created successfully") -except Exception as e: - print(f"[FAIL] Texture creation failed: {e}") - exit(1) -grid = None - -try: - # Try with just 2 args - grid = mcrfpy.Grid(20, 15) # Just grid dimensions - print("[INFO] Grid created with 2 args") -except Exception as e: - print(f"[FAIL] 2 args failed: {e}") - -if not grid: - try: - # Try with 3 args - grid = mcrfpy.Grid(20, 15, texture) - print("[INFO] Grid created with 3 args") - except Exception as e: - print(f"[FAIL] 3 args failed: {e}") - -# If we got here, add to UI -try: - ui.append(grid) - print("[PASS] Grid created and added to UI successfully") -except Exception as e: - print(f"[FAIL] Failed to add Grid to UI: {e}") - exit(1) - -# Test grid properties -try: - print(f"Grid size: {grid.grid_size}") - print(f"Position: {grid.position}") - print(f"Size: {grid.size}") -except Exception as e: - print(f"[FAIL] Property access failed: {e}") - -print("Test complete!") \ No newline at end of file diff --git a/tests/automation/automation_click_issue78_test.py b/tests/automation/automation_click_issue78_test.py deleted file mode 100644 index 159c30e..0000000 --- a/tests/automation/automation_click_issue78_test.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/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...") \ No newline at end of file diff --git a/tests/automation/automation_screenshot_test.py b/tests/automation/automation_screenshot_test.py deleted file mode 100644 index c0c1d2f..0000000 --- a/tests/automation/automation_screenshot_test.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/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() - diff --git a/tests/benchmark_static_grid.py b/tests/benchmark_static_grid.py deleted file mode 100644 index 5307232..0000000 --- a/tests/benchmark_static_grid.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -Benchmark: Static Grid Performance Test - -This benchmark measures McRogueFace's grid rendering performance with a static -100x100 grid. The goal is 60 FPS with minimal CPU usage. - -Expected results: -- 60 FPS (16.6ms per frame) -- Grid render time should be <2ms after dirty flag optimization -- Currently will be higher (likely 8-12ms) - this establishes baseline - -Usage: - ./build/mcrogueface --exec tests/benchmark_static_grid.py - -Press F3 to toggle performance overlay -Press ESC to exit -""" - -import mcrfpy -import sys - -# Create the benchmark scene -mcrfpy.createScene("benchmark") -mcrfpy.setScene("benchmark") - -# Get scene UI -ui = mcrfpy.sceneUI("benchmark") - -# Create a 100x100 grid with default texture -grid = mcrfpy.Grid( - grid_size=(100, 100), - pos=(0, 0), - size=(1024, 768) -) - -# Fill grid with varied tile patterns to ensure realistic rendering -for x in range(100): - for y in range(100): - cell = grid.at((x, y)) - # Checkerboard pattern with different sprites - if (x + y) % 2 == 0: - cell.tilesprite = 0 - cell.color = (50, 50, 50, 255) - else: - cell.tilesprite = 1 - cell.color = (70, 70, 70, 255) - - # Add some variation - if x % 10 == 0 or y % 10 == 0: - cell.tilesprite = 2 - cell.color = (100, 100, 100, 255) - -# Add grid to scene -ui.append(grid) - -# Instructions caption -instructions = mcrfpy.Caption( - text="Static Grid Benchmark (100x100)\n" - "Press F3 for performance overlay\n" - "Press ESC to exit\n" - "Goal: 60 FPS with low grid render time", - pos=(10, 10), - fill_color=(255, 255, 0, 255) -) -ui.append(instructions) - -# Benchmark info -print("=" * 60) -print("STATIC GRID BENCHMARK") -print("=" * 60) -print("Grid size: 100x100 cells") -print("Expected FPS: 60") -print("Tiles rendered: ~1024 visible cells per frame") -print("") -print("This benchmark establishes baseline grid rendering performance.") -print("After dirty flag optimization, grid render time should drop") -print("significantly for static content.") -print("") -print("Press F3 in-game to see real-time performance metrics.") -print("=" * 60) - -# Exit handler -def handle_key(key, state): - if key == "Escape" and state: - print("\nBenchmark ended by user") - sys.exit(0) - -mcrfpy.keypressScene(handle_key) - -# Run for 10 seconds then provide summary -frame_count = 0 -start_time = None - -def benchmark_timer(ms): - global frame_count, start_time - - if start_time is None: - import time - start_time = time.time() - - frame_count += 1 - - # After 10 seconds, print summary and exit - import time - elapsed = time.time() - start_time - - if elapsed >= 10.0: - print("\n" + "=" * 60) - print("BENCHMARK COMPLETE") - print("=" * 60) - print(f"Frames rendered: {frame_count}") - print(f"Time elapsed: {elapsed:.2f}s") - print(f"Average FPS: {frame_count / elapsed:.1f}") - print("") - print("Check profiler overlay (F3) for detailed timing breakdown.") - print("Grid render time is the key metric for optimization.") - print("=" * 60) - # Don't exit automatically - let user review with F3 - # sys.exit(0) - -# Update every 100ms -mcrfpy.setTimer("benchmark", benchmark_timer, 100) diff --git a/tests/benchmark_moving_entities.py b/tests/benchmarks/benchmark_moving_entities.py similarity index 100% rename from tests/benchmark_moving_entities.py rename to tests/benchmarks/benchmark_moving_entities.py diff --git a/tests/bugs/issue_12_gridpoint_instantiation_test.py b/tests/bugs/issue_12_gridpoint_instantiation_test.py deleted file mode 100644 index bb37365..0000000 --- a/tests/bugs/issue_12_gridpoint_instantiation_test.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #12: Forbid GridPoint/GridPointState instantiation - -This test verifies that GridPoint and GridPointState cannot be instantiated -directly from Python, as they should only be created internally by the C++ code. -""" - -import mcrfpy -import sys - -def test_gridpoint_instantiation(): - """Test that GridPoint and GridPointState cannot be instantiated""" - print("=== Testing GridPoint/GridPointState Instantiation Prevention (Issue #12) ===\n") - - tests_passed = 0 - tests_total = 0 - - # Test 1: Try to instantiate GridPoint - print("--- Test 1: GridPoint instantiation ---") - tests_total += 1 - try: - point = mcrfpy.GridPoint() - print("✗ FAIL: GridPoint() should not be allowed") - except TypeError as e: - print(f"✓ PASS: GridPoint instantiation correctly prevented: {e}") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: Unexpected error: {e}") - - # Test 2: Try to instantiate GridPointState - print("\n--- Test 2: GridPointState instantiation ---") - tests_total += 1 - try: - state = mcrfpy.GridPointState() - print("✗ FAIL: GridPointState() should not be allowed") - except TypeError as e: - print(f"✓ PASS: GridPointState instantiation correctly prevented: {e}") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: Unexpected error: {e}") - - # Test 3: Verify GridPoint can still be obtained from Grid - print("\n--- Test 3: GridPoint obtained from Grid.at() ---") - tests_total += 1 - try: - grid = mcrfpy.Grid(10, 10) - point = grid.at(5, 5) - print(f"✓ PASS: GridPoint obtained from Grid.at(): {point}") - print(f" Type: {type(point).__name__}") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: Could not get GridPoint from Grid: {e}") - - # Test 4: Verify GridPointState can still be obtained from GridPoint - print("\n--- Test 4: GridPointState obtained from GridPoint ---") - tests_total += 1 - try: - # GridPointState is accessed through GridPoint's click handler - # Let's check if we can access point properties that would use GridPointState - if hasattr(point, 'walkable'): - print(f"✓ PASS: GridPoint has expected properties") - print(f" walkable: {point.walkable}") - print(f" transparent: {point.transparent}") - tests_passed += 1 - else: - print("✗ FAIL: GridPoint missing expected properties") - except Exception as e: - print(f"✗ FAIL: Error accessing GridPoint properties: {e}") - - # Test 5: Try to call the types directly (alternative syntax) - print("\n--- Test 5: Alternative instantiation attempts ---") - tests_total += 1 - all_prevented = True - - # Try various ways to instantiate - attempts = [ - ("mcrfpy.GridPoint.__new__(mcrfpy.GridPoint)", - lambda: mcrfpy.GridPoint.__new__(mcrfpy.GridPoint)), - ("type(point)()", - lambda: type(point)() if 'point' in locals() else None), - ] - - for desc, func in attempts: - try: - if func: - result = func() - print(f"✗ FAIL: {desc} should not be allowed") - all_prevented = False - except (TypeError, AttributeError) as e: - print(f" ✓ Correctly prevented: {desc}") - except Exception as e: - print(f" ? Unexpected error for {desc}: {e}") - - if all_prevented: - print("✓ PASS: All alternative instantiation attempts prevented") - tests_passed += 1 - else: - print("✗ FAIL: Some instantiation attempts succeeded") - - # Summary - print(f"\n=== SUMMARY ===") - print(f"Tests passed: {tests_passed}/{tests_total}") - - if tests_passed == tests_total: - print("\nIssue #12 FIXED: GridPoint/GridPointState instantiation properly forbidden!") - else: - print("\nIssue #12: Some tests failed") - - return tests_passed == tests_total - -def run_test(runtime): - """Timer callback to run the test""" - try: - # First verify the types exist - print("Checking that GridPoint and GridPointState types exist...") - print(f"GridPoint type: {mcrfpy.GridPoint}") - print(f"GridPointState type: {mcrfpy.GridPointState}") - print() - - success = test_gridpoint_instantiation() - print("\nOverall result: " + ("PASS" if success else "FAIL")) - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_26_28_iterator_comprehensive_test.py b/tests/bugs/issue_26_28_iterator_comprehensive_test.py deleted file mode 100644 index db88571..0000000 --- a/tests/bugs/issue_26_28_iterator_comprehensive_test.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env python3 -""" -Comprehensive test for Issues #26 & #28: Iterator implementation for collections - -This test covers both UICollection and UIEntityCollection iterator implementations, -testing all aspects of the Python sequence protocol. - -Issues: -- #26: Iterator support for UIEntityCollection -- #28: Iterator support for UICollection -""" - -import mcrfpy -from mcrfpy import automation -import sys -import gc - -def test_sequence_protocol(collection, name, expected_types=None): - """Test all sequence protocol operations on a collection""" - print(f"\n=== Testing {name} ===") - - tests_passed = 0 - tests_total = 0 - - # Test 1: len() - tests_total += 1 - try: - length = len(collection) - print(f"✓ len() works: {length} items") - tests_passed += 1 - except Exception as e: - print(f"✗ len() failed: {e}") - return tests_passed, tests_total - - # Test 2: Basic iteration - tests_total += 1 - try: - items = [] - types = [] - for item in collection: - items.append(item) - types.append(type(item).__name__) - print(f"✓ Iteration works: found {len(items)} items") - print(f" Types: {types}") - if expected_types and types != expected_types: - print(f" WARNING: Expected types {expected_types}") - tests_passed += 1 - except Exception as e: - print(f"✗ Iteration failed (Issue #26/#28): {e}") - - # Test 3: Indexing (positive) - tests_total += 1 - try: - if length > 0: - first = collection[0] - last = collection[length-1] - print(f"✓ Positive indexing works: [0]={type(first).__name__}, [{length-1}]={type(last).__name__}") - tests_passed += 1 - else: - print(" Skipping indexing test - empty collection") - except Exception as e: - print(f"✗ Positive indexing failed: {e}") - - # Test 4: Negative indexing - tests_total += 1 - try: - if length > 0: - last = collection[-1] - first = collection[-length] - print(f"✓ Negative indexing works: [-1]={type(last).__name__}, [-{length}]={type(first).__name__}") - tests_passed += 1 - else: - print(" Skipping negative indexing test - empty collection") - except Exception as e: - print(f"✗ Negative indexing failed: {e}") - - # Test 5: Out of bounds indexing - tests_total += 1 - try: - _ = collection[length + 10] - print(f"✗ Out of bounds indexing should raise IndexError but didn't") - except IndexError: - print(f"✓ Out of bounds indexing correctly raises IndexError") - tests_passed += 1 - except Exception as e: - print(f"✗ Out of bounds indexing raised wrong exception: {type(e).__name__}: {e}") - - # Test 6: Slicing - tests_total += 1 - try: - if length >= 2: - slice_result = collection[0:2] - print(f"✓ Slicing works: [0:2] returned {len(slice_result)} items") - tests_passed += 1 - else: - print(" Skipping slicing test - not enough items") - except NotImplementedError: - print(f"✗ Slicing not implemented") - except Exception as e: - print(f"✗ Slicing failed: {e}") - - # Test 7: Contains operator - tests_total += 1 - try: - if length > 0: - first_item = collection[0] - if first_item in collection: - print(f"✓ 'in' operator works") - tests_passed += 1 - else: - print(f"✗ 'in' operator returned False for existing item") - else: - print(" Skipping 'in' operator test - empty collection") - except NotImplementedError: - print(f"✗ 'in' operator not implemented") - except Exception as e: - print(f"✗ 'in' operator failed: {e}") - - # Test 8: Multiple iterations - tests_total += 1 - try: - count1 = sum(1 for _ in collection) - count2 = sum(1 for _ in collection) - if count1 == count2 == length: - print(f"✓ Multiple iterations work correctly") - tests_passed += 1 - else: - print(f"✗ Multiple iterations inconsistent: {count1} vs {count2} vs {length}") - except Exception as e: - print(f"✗ Multiple iterations failed: {e}") - - # Test 9: Iterator state independence - tests_total += 1 - try: - iter1 = iter(collection) - iter2 = iter(collection) - - # Advance iter1 - next(iter1) - - # iter2 should still be at the beginning - item1_from_iter2 = next(iter2) - item1_from_collection = collection[0] - - if type(item1_from_iter2).__name__ == type(item1_from_collection).__name__: - print(f"✓ Iterator state independence maintained") - tests_passed += 1 - else: - print(f"✗ Iterator states are not independent") - except Exception as e: - print(f"✗ Iterator state test failed: {e}") - - # Test 10: List conversion - tests_total += 1 - try: - as_list = list(collection) - if len(as_list) == length: - print(f"✓ list() conversion works: {len(as_list)} items") - tests_passed += 1 - else: - print(f"✗ list() conversion wrong length: {len(as_list)} vs {length}") - except Exception as e: - print(f"✗ list() conversion failed: {e}") - - return tests_passed, tests_total - -def test_modification_during_iteration(collection, name): - """Test collection modification during iteration""" - print(f"\n=== Testing {name} Modification During Iteration ===") - - # This is a tricky case - some implementations might crash - # or behave unexpectedly when the collection is modified during iteration - - if len(collection) < 2: - print(" Skipping - need at least 2 items") - return - - try: - count = 0 - for i, item in enumerate(collection): - count += 1 - if i == 0 and hasattr(collection, 'remove'): - # Try to remove an item during iteration - # This might raise an exception or cause undefined behavior - pass # Don't actually modify to avoid breaking the test - print(f"✓ Iteration completed without modification: {count} items") - except Exception as e: - print(f" Note: Iteration with modification would fail: {e}") - -def run_comprehensive_test(): - """Run comprehensive iterator tests for both collection types""" - print("=== Testing Collection Iterator Implementation (Issues #26 & #28) ===") - - total_passed = 0 - total_tests = 0 - - # Test UICollection - print("\n--- Testing UICollection ---") - - # Create UI elements - scene_ui = mcrfpy.sceneUI("test") - - # Add various UI elements - frame = mcrfpy.Frame(10, 10, 200, 150, - fill_color=mcrfpy.Color(100, 100, 200), - outline_color=mcrfpy.Color(255, 255, 255)) - caption = mcrfpy.Caption(mcrfpy.Vector(220, 10), - text="Test Caption", - fill_color=mcrfpy.Color(255, 255, 0)) - - scene_ui.append(frame) - scene_ui.append(caption) - - # Test UICollection - passed, total = test_sequence_protocol(scene_ui, "UICollection", - expected_types=["Frame", "Caption"]) - total_passed += passed - total_tests += total - - test_modification_during_iteration(scene_ui, "UICollection") - - # Test UICollection with children - print("\n--- Testing UICollection Children (Nested) ---") - child_caption = mcrfpy.Caption(mcrfpy.Vector(10, 10), - text="Child", - fill_color=mcrfpy.Color(200, 200, 200)) - frame.children.append(child_caption) - - passed, total = test_sequence_protocol(frame.children, "Frame.children", - expected_types=["Caption"]) - total_passed += passed - total_tests += total - - # Test UIEntityCollection - print("\n--- Testing UIEntityCollection ---") - - # Create a grid with entities - grid = mcrfpy.Grid(30, 30) - grid.x = 10 - grid.y = 200 - grid.w = 600 - grid.h = 400 - scene_ui.append(grid) - - # Add various entities - entity1 = mcrfpy.Entity(5, 5) - entity2 = mcrfpy.Entity(10, 10) - entity3 = mcrfpy.Entity(15, 15) - - grid.entities.append(entity1) - grid.entities.append(entity2) - grid.entities.append(entity3) - - passed, total = test_sequence_protocol(grid.entities, "UIEntityCollection", - expected_types=["Entity", "Entity", "Entity"]) - total_passed += passed - total_tests += total - - test_modification_during_iteration(grid.entities, "UIEntityCollection") - - # Test empty collections - print("\n--- Testing Empty Collections ---") - empty_grid = mcrfpy.Grid(10, 10) - - passed, total = test_sequence_protocol(empty_grid.entities, "Empty UIEntityCollection") - total_passed += passed - total_tests += total - - empty_frame = mcrfpy.Frame(0, 0, 50, 50) - passed, total = test_sequence_protocol(empty_frame.children, "Empty UICollection") - total_passed += passed - total_tests += total - - # Test large collection - print("\n--- Testing Large Collection ---") - large_grid = mcrfpy.Grid(50, 50) - for i in range(100): - large_grid.entities.append(mcrfpy.Entity(i % 50, i // 50)) - - print(f"Created large collection with {len(large_grid.entities)} entities") - - # Just test basic iteration performance - import time - start = time.time() - count = sum(1 for _ in large_grid.entities) - elapsed = time.time() - start - print(f"✓ Large collection iteration: {count} items in {elapsed:.3f}s") - - # Edge case: Single item collection - print("\n--- Testing Single Item Collection ---") - single_grid = mcrfpy.Grid(5, 5) - single_grid.entities.append(mcrfpy.Entity(1, 1)) - - passed, total = test_sequence_protocol(single_grid.entities, "Single Item UIEntityCollection") - total_passed += passed - total_tests += total - - # Take screenshot - automation.screenshot("/tmp/issue_26_28_iterator_test.png") - - # Summary - print(f"\n=== SUMMARY ===") - print(f"Total tests passed: {total_passed}/{total_tests}") - - if total_passed < total_tests: - print("\nIssues found:") - print("- Issue #26: UIEntityCollection may not fully implement iterator protocol") - print("- Issue #28: UICollection may not fully implement iterator protocol") - print("\nThe iterator implementation should support:") - print("1. Forward iteration with 'for item in collection'") - print("2. Multiple independent iterators") - print("3. Proper cleanup when iteration completes") - print("4. Integration with Python's sequence protocol") - else: - print("\nAll iterator tests passed!") - - return total_passed == total_tests - -def run_test(runtime): - """Timer callback to run the test""" - try: - success = run_comprehensive_test() - print("\nOverall result: " + ("PASS" if success else "FAIL")) - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_37_windows_scripts_comprehensive_test.py b/tests/bugs/issue_37_windows_scripts_comprehensive_test.py deleted file mode 100644 index cce902f..0000000 --- a/tests/bugs/issue_37_windows_scripts_comprehensive_test.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python3 -""" -Comprehensive test for Issue #37: Windows scripts subdirectory bug - -This test comprehensively tests script loading from different working directories, -particularly focusing on the Windows issue where relative paths fail. - -The bug: On Windows, when mcrogueface.exe is run from a different directory, -it fails to find scripts/game.py because fopen uses relative paths. -""" - -import os -import sys -import subprocess -import tempfile -import shutil -import platform - -def create_test_script(content=""): - """Create a minimal test script""" - if not content: - content = """ -import mcrfpy -print("TEST_SCRIPT_LOADED_FROM_PATH") -mcrfpy.createScene("test_scene") -# Exit cleanly to avoid hanging -import sys -sys.exit(0) -""" - return content - -def run_mcrogueface(exe_path, cwd, timeout=5): - """Run mcrogueface from a specific directory and capture output""" - cmd = [exe_path, "--headless"] - - try: - result = subprocess.run( - cmd, - cwd=cwd, - capture_output=True, - text=True, - timeout=timeout - ) - return result.stdout, result.stderr, result.returncode - except subprocess.TimeoutExpired: - return "", "TIMEOUT", -1 - except Exception as e: - return "", str(e), -1 - -def test_script_loading(): - """Test script loading from various directories""" - # Detect platform - is_windows = platform.system() == "Windows" - print(f"Platform: {platform.system()}") - - # Get paths - repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - build_dir = os.path.join(repo_root, "build") - exe_name = "mcrogueface.exe" if is_windows else "mcrogueface" - exe_path = os.path.join(build_dir, exe_name) - - if not os.path.exists(exe_path): - print(f"FAIL: Executable not found at {exe_path}") - print("Please build the project first") - return - - # Backup original game.py - scripts_dir = os.path.join(build_dir, "scripts") - game_py_path = os.path.join(scripts_dir, "game.py") - game_py_backup = game_py_path + ".backup" - - if os.path.exists(game_py_path): - shutil.copy(game_py_path, game_py_backup) - - try: - # Create test script - os.makedirs(scripts_dir, exist_ok=True) - with open(game_py_path, "w") as f: - f.write(create_test_script()) - - print("\n=== Test 1: Run from build directory (baseline) ===") - stdout, stderr, code = run_mcrogueface(exe_path, build_dir) - if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: - print("✓ PASS: Script loaded when running from build directory") - else: - print("✗ FAIL: Script not loaded from build directory") - print(f" stdout: {stdout[:200]}") - print(f" stderr: {stderr[:200]}") - - print("\n=== Test 2: Run from parent directory ===") - stdout, stderr, code = run_mcrogueface(exe_path, repo_root) - if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: - print("✓ PASS: Script loaded from parent directory") - else: - print("✗ FAIL: Script not loaded from parent directory") - print(" This might indicate Issue #37") - print(f" stdout: {stdout[:200]}") - print(f" stderr: {stderr[:200]}") - - print("\n=== Test 3: Run from system temp directory ===") - with tempfile.TemporaryDirectory() as tmpdir: - stdout, stderr, code = run_mcrogueface(exe_path, tmpdir) - if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: - print("✓ PASS: Script loaded from temp directory") - else: - print("✗ FAIL: Script not loaded from temp directory") - print(" This is the core Issue #37 bug!") - print(f" Working directory: {tmpdir}") - print(f" stdout: {stdout[:200]}") - print(f" stderr: {stderr[:200]}") - - print("\n=== Test 4: Run with absolute path from different directory ===") - with tempfile.TemporaryDirectory() as tmpdir: - # Use absolute path to executable - abs_exe = os.path.abspath(exe_path) - stdout, stderr, code = run_mcrogueface(abs_exe, tmpdir) - if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: - print("✓ PASS: Script loaded with absolute exe path") - else: - print("✗ FAIL: Script not loaded with absolute exe path") - print(f" stdout: {stdout[:200]}") - print(f" stderr: {stderr[:200]}") - - # Test 5: Symlink test (Unix only) - if not is_windows: - print("\n=== Test 5: Run via symlink (Unix only) ===") - with tempfile.TemporaryDirectory() as tmpdir: - symlink_path = os.path.join(tmpdir, "mcrogueface_link") - os.symlink(exe_path, symlink_path) - stdout, stderr, code = run_mcrogueface(symlink_path, tmpdir) - if "TEST_SCRIPT_LOADED_FROM_PATH" in stdout: - print("✓ PASS: Script loaded via symlink") - else: - print("✗ FAIL: Script not loaded via symlink") - print(f" stdout: {stdout[:200]}") - print(f" stderr: {stderr[:200]}") - - # Summary - print("\n=== SUMMARY ===") - print("Issue #37 is about script loading failing when the executable") - print("is run from a different working directory than where it's located.") - print("The fix should resolve the script path relative to the executable,") - print("not the current working directory.") - - finally: - # Restore original game.py - if os.path.exists(game_py_backup): - shutil.move(game_py_backup, game_py_path) - print("\nTest cleanup complete") - -if __name__ == "__main__": - test_script_loading() \ No newline at end of file diff --git a/tests/bugs/issue_76_uientitycollection_type_test.py b/tests/bugs/issue_76_uientitycollection_type_test.py deleted file mode 100644 index 15fd27f..0000000 --- a/tests/bugs/issue_76_uientitycollection_type_test.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -""" -Comprehensive test for Issue #76: UIEntityCollection returns wrong type for derived classes - -This test demonstrates that when retrieving entities from a UIEntityCollection, -derived Entity classes lose their type and are returned as base Entity objects. - -The bug: The C++ implementation of UIEntityCollection::getitem creates a new -PyUIEntityObject with type "Entity" instead of preserving the original Python type. -""" - -import mcrfpy -from mcrfpy import automation -import sys -import gc - -# Define several derived Entity classes with different features -class Player(mcrfpy.Entity): - def __init__(self, x, y): - # Entity expects Vector position and optional texture - super().__init__(mcrfpy.Vector(x, y)) - self.health = 100 - self.inventory = [] - self.player_id = "PLAYER_001" - - def take_damage(self, amount): - self.health -= amount - return self.health > 0 - -class Enemy(mcrfpy.Entity): - def __init__(self, x, y, enemy_type="goblin"): - # Entity expects Vector position and optional texture - super().__init__(mcrfpy.Vector(x, y)) - self.enemy_type = enemy_type - self.aggression = 5 - self.patrol_route = [(x, y), (x+1, y), (x+1, y+1), (x, y+1)] - - def get_next_move(self): - return self.patrol_route[0] - -class Treasure(mcrfpy.Entity): - def __init__(self, x, y, value=100): - # Entity expects Vector position and optional texture - super().__init__(mcrfpy.Vector(x, y)) - self.value = value - self.collected = False - - def collect(self): - if not self.collected: - self.collected = True - return self.value - return 0 - -def test_type_preservation(): - """Comprehensive test of type preservation in UIEntityCollection""" - print("=== Testing UIEntityCollection Type Preservation (Issue #76) ===\n") - - # Create a grid to hold entities - grid = mcrfpy.Grid(30, 30) - grid.x = 10 - grid.y = 10 - grid.w = 600 - grid.h = 600 - - # Add grid to scene - scene_ui = mcrfpy.sceneUI("test") - scene_ui.append(grid) - - # Create various entity instances - player = Player(5, 5) - enemy1 = Enemy(10, 10, "orc") - enemy2 = Enemy(15, 15, "skeleton") - treasure = Treasure(20, 20, 500) - base_entity = mcrfpy.Entity(mcrfpy.Vector(25, 25)) - - print("Created entities:") - print(f" - Player at (5,5): type={type(player).__name__}, health={player.health}") - print(f" - Enemy at (10,10): type={type(enemy1).__name__}, enemy_type={enemy1.enemy_type}") - print(f" - Enemy at (15,15): type={type(enemy2).__name__}, enemy_type={enemy2.enemy_type}") - print(f" - Treasure at (20,20): type={type(treasure).__name__}, value={treasure.value}") - print(f" - Base Entity at (25,25): type={type(base_entity).__name__}") - - # Store original references - original_refs = { - 'player': player, - 'enemy1': enemy1, - 'enemy2': enemy2, - 'treasure': treasure, - 'base_entity': base_entity - } - - # Add entities to grid - grid.entities.append(player) - grid.entities.append(enemy1) - grid.entities.append(enemy2) - grid.entities.append(treasure) - grid.entities.append(base_entity) - - print(f"\nAdded {len(grid.entities)} entities to grid") - - # Test 1: Direct indexing - print("\n--- Test 1: Direct Indexing ---") - retrieved_entities = [] - for i in range(len(grid.entities)): - entity = grid.entities[i] - retrieved_entities.append(entity) - print(f"grid.entities[{i}]: type={type(entity).__name__}, id={id(entity)}") - - # Test 2: Check type preservation - print("\n--- Test 2: Type Preservation Check ---") - r_player = grid.entities[0] - r_enemy1 = grid.entities[1] - r_treasure = grid.entities[3] - - # Check types - tests_passed = 0 - tests_total = 0 - - tests_total += 1 - if type(r_player).__name__ == "Player": - print("✓ PASS: Player type preserved") - tests_passed += 1 - else: - print(f"✗ FAIL: Player type lost! Got {type(r_player).__name__} instead of Player") - print(" This is the core Issue #76 bug!") - - tests_total += 1 - if type(r_enemy1).__name__ == "Enemy": - print("✓ PASS: Enemy type preserved") - tests_passed += 1 - else: - print(f"✗ FAIL: Enemy type lost! Got {type(r_enemy1).__name__} instead of Enemy") - - tests_total += 1 - if type(r_treasure).__name__ == "Treasure": - print("✓ PASS: Treasure type preserved") - tests_passed += 1 - else: - print(f"✗ FAIL: Treasure type lost! Got {type(r_treasure).__name__} instead of Treasure") - - # Test 3: Check attribute preservation - print("\n--- Test 3: Attribute Preservation ---") - - # Test Player attributes - try: - tests_total += 1 - health = r_player.health - inv = r_player.inventory - pid = r_player.player_id - print(f"✓ PASS: Player attributes accessible: health={health}, inventory={inv}, id={pid}") - tests_passed += 1 - except AttributeError as e: - print(f"✗ FAIL: Player attributes lost: {e}") - - # Test Enemy attributes - try: - tests_total += 1 - etype = r_enemy1.enemy_type - aggr = r_enemy1.aggression - print(f"✓ PASS: Enemy attributes accessible: type={etype}, aggression={aggr}") - tests_passed += 1 - except AttributeError as e: - print(f"✗ FAIL: Enemy attributes lost: {e}") - - # Test 4: Method preservation - print("\n--- Test 4: Method Preservation ---") - - try: - tests_total += 1 - r_player.take_damage(10) - print(f"✓ PASS: Player method callable, health now: {r_player.health}") - tests_passed += 1 - except AttributeError as e: - print(f"✗ FAIL: Player methods lost: {e}") - - try: - tests_total += 1 - next_move = r_enemy1.get_next_move() - print(f"✓ PASS: Enemy method callable, next move: {next_move}") - tests_passed += 1 - except AttributeError as e: - print(f"✗ FAIL: Enemy methods lost: {e}") - - # Test 5: Iteration - print("\n--- Test 5: Iteration Test ---") - try: - tests_total += 1 - type_list = [] - for entity in grid.entities: - type_list.append(type(entity).__name__) - print(f"Types during iteration: {type_list}") - if type_list == ["Player", "Enemy", "Enemy", "Treasure", "Entity"]: - print("✓ PASS: All types preserved during iteration") - tests_passed += 1 - else: - print("✗ FAIL: Types lost during iteration") - except Exception as e: - print(f"✗ FAIL: Iteration error: {e}") - - # Test 6: Identity check - print("\n--- Test 6: Object Identity ---") - tests_total += 1 - if r_player is original_refs['player']: - print("✓ PASS: Retrieved object is the same Python object") - tests_passed += 1 - else: - print("✗ FAIL: Retrieved object is a different instance") - print(f" Original id: {id(original_refs['player'])}") - print(f" Retrieved id: {id(r_player)}") - - # Test 7: Modification persistence - print("\n--- Test 7: Modification Persistence ---") - tests_total += 1 - r_player.x = 50 - r_player.y = 50 - - # Retrieve again - r_player2 = grid.entities[0] - if r_player2.x == 50 and r_player2.y == 50: - print("✓ PASS: Modifications persist across retrievals") - tests_passed += 1 - else: - print(f"✗ FAIL: Modifications lost: position is ({r_player2.x}, {r_player2.y})") - - # Take screenshot - automation.screenshot("/tmp/issue_76_test.png") - - # Summary - print(f"\n=== SUMMARY ===") - print(f"Tests passed: {tests_passed}/{tests_total}") - - if tests_passed < tests_total: - print("\nIssue #76: The C++ implementation creates new PyUIEntityObject instances") - print("with type 'Entity' instead of preserving the original Python type.") - print("This causes derived classes to lose their type, attributes, and methods.") - print("\nThe fix requires storing and restoring the original Python type") - print("when creating objects in UIEntityCollection::getitem.") - - return tests_passed == tests_total - -def run_test(runtime): - """Timer callback to run the test""" - try: - success = test_type_preservation() - print("\nOverall result: " + ("PASS" if success else "FAIL")) - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_80_caption_font_size_test.py b/tests/bugs/issue_80_caption_font_size_test.py deleted file mode 100644 index 0193355..0000000 --- a/tests/bugs/issue_80_caption_font_size_test.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #80: Rename Caption.size to font_size - -This test verifies that Caption now uses font_size property instead of size, -while maintaining backward compatibility. -""" - -import mcrfpy -import sys - -def test_caption_font_size(): - """Test Caption font_size property""" - print("=== Testing Caption font_size Property (Issue #80) ===\n") - - tests_passed = 0 - tests_total = 0 - - # Create a caption for testing - caption = mcrfpy.Caption((100, 100), "Test Text", mcrfpy.Font("assets/JetbrainsMono.ttf")) - - # Test 1: Check that font_size property exists and works - print("--- Test 1: font_size property ---") - tests_total += 1 - try: - # Set font size using new property name - caption.font_size = 24 - if caption.font_size == 24: - print("✓ PASS: font_size property works correctly") - tests_passed += 1 - else: - print(f"✗ FAIL: font_size is {caption.font_size}, expected 24") - except AttributeError as e: - print(f"✗ FAIL: font_size property not found: {e}") - - # Test 2: Check that old 'size' property is removed - print("\n--- Test 2: Old 'size' property removed ---") - tests_total += 1 - try: - # Try to access size property - this should fail - old_size = caption.size - print(f"✗ FAIL: 'size' property still accessible (value: {old_size}) - should be removed") - except AttributeError: - print("✓ PASS: 'size' property correctly removed") - tests_passed += 1 - - # Test 3: Verify font_size changes are reflected - print("\n--- Test 3: font_size changes ---") - tests_total += 1 - caption.font_size = 36 - if caption.font_size == 36: - print("✓ PASS: font_size changes are reflected correctly") - tests_passed += 1 - else: - print(f"✗ FAIL: font_size is {caption.font_size}, expected 36") - - # Test 4: Check property type - print("\n--- Test 4: Property type check ---") - tests_total += 1 - caption.font_size = 18 - if isinstance(caption.font_size, int): - print("✓ PASS: font_size returns integer as expected") - tests_passed += 1 - else: - print(f"✗ FAIL: font_size returns {type(caption.font_size).__name__}, expected int") - - # Test 5: Verify in __dir__ - print("\n--- Test 5: Property introspection ---") - tests_total += 1 - properties = dir(caption) - if 'font_size' in properties: - print("✓ PASS: 'font_size' appears in dir(caption)") - tests_passed += 1 - else: - print("✗ FAIL: 'font_size' not found in dir(caption)") - - # Check if 'size' still appears - if 'size' in properties: - print(" INFO: 'size' still appears in dir(caption) - backward compatibility maintained") - else: - print(" INFO: 'size' removed from dir(caption) - breaking change") - - # Test 6: Edge cases - print("\n--- Test 6: Edge cases ---") - tests_total += 1 - all_passed = True - - # Test setting to 0 - caption.font_size = 0 - if caption.font_size != 0: - print(f"✗ FAIL: Setting font_size to 0 failed (got {caption.font_size})") - all_passed = False - - # Test setting to large value - caption.font_size = 100 - if caption.font_size != 100: - print(f"✗ FAIL: Setting font_size to 100 failed (got {caption.font_size})") - all_passed = False - - # Test float to int conversion - caption.font_size = 24.7 - if caption.font_size != 24: - print(f"✗ FAIL: Float to int conversion failed (got {caption.font_size})") - all_passed = False - - if all_passed: - print("✓ PASS: All edge cases handled correctly") - tests_passed += 1 - else: - print("✗ FAIL: Some edge cases failed") - - # Test 7: Scene UI integration - print("\n--- Test 7: Scene UI integration ---") - tests_total += 1 - try: - scene_ui = mcrfpy.sceneUI("test") - scene_ui.append(caption) - - # Modify font_size after adding to scene - caption.font_size = 32 - - print("✓ PASS: Caption with font_size works in scene UI") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: Scene UI integration failed: {e}") - - # Summary - print(f"\n=== SUMMARY ===") - print(f"Tests passed: {tests_passed}/{tests_total}") - - if tests_passed == tests_total: - print("\nIssue #80 FIXED: Caption.size successfully renamed to font_size!") - else: - print("\nIssue #80: Some tests failed") - - return tests_passed == tests_total - -def run_test(runtime): - """Timer callback to run the test""" - try: - success = test_caption_font_size() - print("\nOverall result: " + ("PASS" if success else "FAIL")) - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_81_sprite_index_standardization_test.py b/tests/bugs/issue_81_sprite_index_standardization_test.py deleted file mode 100644 index c7b7b2d..0000000 --- a/tests/bugs/issue_81_sprite_index_standardization_test.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #81: Standardize sprite_index property name - -This test verifies that both UISprite and UIEntity use "sprite_index" instead of "sprite_number" -for consistency across the API. -""" - -import mcrfpy -import sys - -def test_sprite_index_property(): - """Test sprite_index property on UISprite""" - print("=== Testing UISprite sprite_index Property ===") - - tests_passed = 0 - tests_total = 0 - - # Create a texture and sprite - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - sprite = mcrfpy.Sprite(10, 10, texture, 5, 1.0) - - # Test 1: Check sprite_index property exists - tests_total += 1 - try: - idx = sprite.sprite_index - if idx == 5: - print(f"✓ PASS: sprite.sprite_index = {idx}") - tests_passed += 1 - else: - print(f"✗ FAIL: sprite.sprite_index = {idx}, expected 5") - except AttributeError as e: - print(f"✗ FAIL: sprite_index not accessible: {e}") - - # Test 2: Check sprite_index setter - tests_total += 1 - try: - sprite.sprite_index = 10 - if sprite.sprite_index == 10: - print("✓ PASS: sprite_index setter works") - tests_passed += 1 - else: - print(f"✗ FAIL: sprite_index setter failed, got {sprite.sprite_index}") - except Exception as e: - print(f"✗ FAIL: sprite_index setter error: {e}") - - # Test 3: Check sprite_number is removed/deprecated - tests_total += 1 - if hasattr(sprite, 'sprite_number'): - # Check if it's an alias - sprite.sprite_number = 15 - if sprite.sprite_index == 15: - print("✓ PASS: sprite_number exists as backward-compatible alias") - tests_passed += 1 - else: - print("✗ FAIL: sprite_number exists but doesn't update sprite_index") - else: - print("✓ PASS: sprite_number property removed (no backward compatibility)") - tests_passed += 1 - - # Test 4: Check repr uses sprite_index - tests_total += 1 - repr_str = repr(sprite) - if "sprite_index=" in repr_str: - print(f"✓ PASS: repr uses sprite_index: {repr_str}") - tests_passed += 1 - elif "sprite_number=" in repr_str: - print(f"✗ FAIL: repr still uses sprite_number: {repr_str}") - else: - print(f"✗ FAIL: repr doesn't show sprite info: {repr_str}") - - return tests_passed, tests_total - -def test_entity_sprite_index_property(): - """Test sprite_index property on Entity""" - print("\n=== Testing Entity sprite_index Property ===") - - tests_passed = 0 - tests_total = 0 - - # Create an entity with required position - entity = mcrfpy.Entity((0, 0)) - - # Test 1: Check sprite_index property exists - tests_total += 1 - try: - # Set initial value - entity.sprite_index = 42 - idx = entity.sprite_index - if idx == 42: - print(f"✓ PASS: entity.sprite_index = {idx}") - tests_passed += 1 - else: - print(f"✗ FAIL: entity.sprite_index = {idx}, expected 42") - except AttributeError as e: - print(f"✗ FAIL: sprite_index not accessible: {e}") - - # Test 2: Check sprite_number is removed/deprecated - tests_total += 1 - if hasattr(entity, 'sprite_number'): - # Check if it's an alias - entity.sprite_number = 99 - if hasattr(entity, 'sprite_index') and entity.sprite_index == 99: - print("✓ PASS: sprite_number exists as backward-compatible alias") - tests_passed += 1 - else: - print("✗ FAIL: sprite_number exists but doesn't update sprite_index") - else: - print("✓ PASS: sprite_number property removed (no backward compatibility)") - tests_passed += 1 - - # Test 3: Check repr uses sprite_index - tests_total += 1 - repr_str = repr(entity) - if "sprite_index=" in repr_str: - print(f"✓ PASS: repr uses sprite_index: {repr_str}") - tests_passed += 1 - elif "sprite_number=" in repr_str: - print(f"✗ FAIL: repr still uses sprite_number: {repr_str}") - else: - print(f"? INFO: repr doesn't show sprite info: {repr_str}") - # This might be okay if entity doesn't show sprite in repr - tests_passed += 1 - - return tests_passed, tests_total - -def test_animation_compatibility(): - """Test that animations work with sprite_index""" - print("\n=== Testing Animation Compatibility ===") - - tests_passed = 0 - tests_total = 0 - - # Test animation with sprite_index property name - tests_total += 1 - try: - # This tests that the animation system recognizes sprite_index - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - sprite = mcrfpy.Sprite(0, 0, texture, 0, 1.0) - - # Try to animate sprite_index (even if we can't directly test animations here) - sprite.sprite_index = 0 - sprite.sprite_index = 5 - sprite.sprite_index = 10 - - print("✓ PASS: sprite_index property works for potential animations") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: sprite_index animation compatibility issue: {e}") - - return tests_passed, tests_total - -def run_test(runtime): - """Timer callback to run the test""" - try: - print("=== Testing sprite_index Property Standardization (Issue #81) ===\n") - - sprite_passed, sprite_total = test_sprite_index_property() - entity_passed, entity_total = test_entity_sprite_index_property() - anim_passed, anim_total = test_animation_compatibility() - - total_passed = sprite_passed + entity_passed + anim_passed - total_tests = sprite_total + entity_total + anim_total - - print(f"\n=== SUMMARY ===") - print(f"Sprite tests: {sprite_passed}/{sprite_total}") - print(f"Entity tests: {entity_passed}/{entity_total}") - print(f"Animation tests: {anim_passed}/{anim_total}") - print(f"Total tests passed: {total_passed}/{total_tests}") - - if total_passed == total_tests: - print("\nIssue #81 FIXED: sprite_index property standardized!") - print("\nOverall result: PASS") - else: - print("\nIssue #81: Some tests failed") - print("\nOverall result: FAIL") - - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_82_sprite_scale_xy_test.py b/tests/bugs/issue_82_sprite_scale_xy_test.py deleted file mode 100644 index a80c403..0000000 --- a/tests/bugs/issue_82_sprite_scale_xy_test.py +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #82: Add scale_x and scale_y to UISprite - -This test verifies that UISprite now supports non-uniform scaling through -separate scale_x and scale_y properties, in addition to the existing uniform -scale property. -""" - -import mcrfpy -import sys - -def test_scale_xy_properties(): - """Test scale_x and scale_y properties on UISprite""" - print("=== Testing UISprite scale_x and scale_y Properties ===") - - tests_passed = 0 - tests_total = 0 - - # Create a texture and sprite - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - sprite = mcrfpy.Sprite(10, 10, texture, 0, 1.0) - - # Test 1: Check scale_x property exists and defaults correctly - tests_total += 1 - try: - scale_x = sprite.scale_x - if scale_x == 1.0: - print(f"✓ PASS: sprite.scale_x = {scale_x} (default)") - tests_passed += 1 - else: - print(f"✗ FAIL: sprite.scale_x = {scale_x}, expected 1.0") - except AttributeError as e: - print(f"✗ FAIL: scale_x not accessible: {e}") - - # Test 2: Check scale_y property exists and defaults correctly - tests_total += 1 - try: - scale_y = sprite.scale_y - if scale_y == 1.0: - print(f"✓ PASS: sprite.scale_y = {scale_y} (default)") - tests_passed += 1 - else: - print(f"✗ FAIL: sprite.scale_y = {scale_y}, expected 1.0") - except AttributeError as e: - print(f"✗ FAIL: scale_y not accessible: {e}") - - # Test 3: Set scale_x independently - tests_total += 1 - try: - sprite.scale_x = 2.0 - if sprite.scale_x == 2.0 and sprite.scale_y == 1.0: - print(f"✓ PASS: scale_x set independently (x={sprite.scale_x}, y={sprite.scale_y})") - tests_passed += 1 - else: - print(f"✗ FAIL: scale_x didn't set correctly (x={sprite.scale_x}, y={sprite.scale_y})") - except Exception as e: - print(f"✗ FAIL: scale_x setter error: {e}") - - # Test 4: Set scale_y independently - tests_total += 1 - try: - sprite.scale_y = 3.0 - if sprite.scale_x == 2.0 and sprite.scale_y == 3.0: - print(f"✓ PASS: scale_y set independently (x={sprite.scale_x}, y={sprite.scale_y})") - tests_passed += 1 - else: - print(f"✗ FAIL: scale_y didn't set correctly (x={sprite.scale_x}, y={sprite.scale_y})") - except Exception as e: - print(f"✗ FAIL: scale_y setter error: {e}") - - # Test 5: Uniform scale property interaction - tests_total += 1 - try: - # Setting uniform scale should affect both x and y - sprite.scale = 1.5 - if sprite.scale_x == 1.5 and sprite.scale_y == 1.5: - print(f"✓ PASS: uniform scale sets both scale_x and scale_y") - tests_passed += 1 - else: - print(f"✗ FAIL: uniform scale didn't update scale_x/scale_y correctly") - except Exception as e: - print(f"✗ FAIL: uniform scale interaction error: {e}") - - # Test 6: Reading uniform scale with non-uniform values - tests_total += 1 - try: - sprite.scale_x = 2.0 - sprite.scale_y = 3.0 - uniform_scale = sprite.scale - # When scales differ, scale property should return scale_x (or could be average, or error) - print(f"? INFO: With non-uniform scaling (x=2.0, y=3.0), scale property returns: {uniform_scale}") - # We'll accept this behavior whatever it is - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: reading scale with non-uniform values failed: {e}") - - return tests_passed, tests_total - -def test_animation_compatibility(): - """Test that animations work with scale_x and scale_y""" - print("\n=== Testing Animation Compatibility ===") - - tests_passed = 0 - tests_total = 0 - - # Test property system compatibility - tests_total += 1 - try: - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - sprite = mcrfpy.Sprite(0, 0, texture, 0, 1.0) - - # Test setting various scale values - sprite.scale_x = 0.5 - sprite.scale_y = 2.0 - sprite.scale_x = 1.5 - sprite.scale_y = 1.5 - - print("✓ PASS: scale_x and scale_y properties work for potential animations") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: scale_x/scale_y animation compatibility issue: {e}") - - return tests_passed, tests_total - -def test_edge_cases(): - """Test edge cases for scale properties""" - print("\n=== Testing Edge Cases ===") - - tests_passed = 0 - tests_total = 0 - - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - sprite = mcrfpy.Sprite(0, 0, texture, 0, 1.0) - - # Test 1: Zero scale - tests_total += 1 - try: - sprite.scale_x = 0.0 - sprite.scale_y = 0.0 - print(f"✓ PASS: Zero scale allowed (x={sprite.scale_x}, y={sprite.scale_y})") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: Zero scale not allowed: {e}") - - # Test 2: Negative scale (flip) - tests_total += 1 - try: - sprite.scale_x = -1.0 - sprite.scale_y = -1.0 - print(f"✓ PASS: Negative scale allowed for flipping (x={sprite.scale_x}, y={sprite.scale_y})") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: Negative scale not allowed: {e}") - - # Test 3: Very large scale - tests_total += 1 - try: - sprite.scale_x = 100.0 - sprite.scale_y = 100.0 - print(f"✓ PASS: Large scale values allowed (x={sprite.scale_x}, y={sprite.scale_y})") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: Large scale values not allowed: {e}") - - return tests_passed, tests_total - -def run_test(runtime): - """Timer callback to run the test""" - try: - print("=== Testing scale_x and scale_y Properties (Issue #82) ===\n") - - basic_passed, basic_total = test_scale_xy_properties() - anim_passed, anim_total = test_animation_compatibility() - edge_passed, edge_total = test_edge_cases() - - total_passed = basic_passed + anim_passed + edge_passed - total_tests = basic_total + anim_total + edge_total - - print(f"\n=== SUMMARY ===") - print(f"Basic tests: {basic_passed}/{basic_total}") - print(f"Animation tests: {anim_passed}/{anim_total}") - print(f"Edge case tests: {edge_passed}/{edge_total}") - print(f"Total tests passed: {total_passed}/{total_tests}") - - if total_passed == total_tests: - print("\nIssue #82 FIXED: scale_x and scale_y properties added!") - print("\nOverall result: PASS") - else: - print("\nIssue #82: Some tests failed") - print("\nOverall result: FAIL") - - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_83_position_tuple_test.py b/tests/bugs/issue_83_position_tuple_test.py deleted file mode 100644 index 5888cf0..0000000 --- a/tests/bugs/issue_83_position_tuple_test.py +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #83: Add position tuple support to constructors - -This test verifies that UI element constructors now support both: -- Traditional (x, y) as separate arguments -- Tuple form ((x, y)) as a single argument -- Vector form (Vector(x, y)) as a single argument -""" - -import mcrfpy -import sys - -def test_frame_position_tuple(): - """Test Frame constructor with position tuples""" - print("=== Testing Frame Position Tuple Support ===") - - tests_passed = 0 - tests_total = 0 - - # Test 1: Traditional (x, y) form - tests_total += 1 - try: - frame1 = mcrfpy.Frame(10, 20, 100, 50) - if frame1.x == 10 and frame1.y == 20: - print("✓ PASS: Frame(x, y, w, h) traditional form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Frame position incorrect: ({frame1.x}, {frame1.y})") - except Exception as e: - print(f"✗ FAIL: Traditional form failed: {e}") - - # Test 2: Tuple ((x, y)) form - tests_total += 1 - try: - frame2 = mcrfpy.Frame((30, 40), 100, 50) - if frame2.x == 30 and frame2.y == 40: - print("✓ PASS: Frame((x, y), w, h) tuple form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Frame tuple position incorrect: ({frame2.x}, {frame2.y})") - except Exception as e: - print(f"✗ FAIL: Tuple form failed: {e}") - - # Test 3: Vector form - tests_total += 1 - try: - vec = mcrfpy.Vector(50, 60) - frame3 = mcrfpy.Frame(vec, 100, 50) - if frame3.x == 50 and frame3.y == 60: - print("✓ PASS: Frame(Vector, w, h) vector form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Frame vector position incorrect: ({frame3.x}, {frame3.y})") - except Exception as e: - print(f"✗ FAIL: Vector form failed: {e}") - - return tests_passed, tests_total - -def test_sprite_position_tuple(): - """Test Sprite constructor with position tuples""" - print("\n=== Testing Sprite Position Tuple Support ===") - - tests_passed = 0 - tests_total = 0 - - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - - # Test 1: Traditional (x, y) form - tests_total += 1 - try: - sprite1 = mcrfpy.Sprite(10, 20, texture, 0, 1.0) - if sprite1.x == 10 and sprite1.y == 20: - print("✓ PASS: Sprite(x, y, texture, ...) traditional form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Sprite position incorrect: ({sprite1.x}, {sprite1.y})") - except Exception as e: - print(f"✗ FAIL: Traditional form failed: {e}") - - # Test 2: Tuple ((x, y)) form - tests_total += 1 - try: - sprite2 = mcrfpy.Sprite((30, 40), texture, 0, 1.0) - if sprite2.x == 30 and sprite2.y == 40: - print("✓ PASS: Sprite((x, y), texture, ...) tuple form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Sprite tuple position incorrect: ({sprite2.x}, {sprite2.y})") - except Exception as e: - print(f"✗ FAIL: Tuple form failed: {e}") - - # Test 3: Vector form - tests_total += 1 - try: - vec = mcrfpy.Vector(50, 60) - sprite3 = mcrfpy.Sprite(vec, texture, 0, 1.0) - if sprite3.x == 50 and sprite3.y == 60: - print("✓ PASS: Sprite(Vector, texture, ...) vector form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Sprite vector position incorrect: ({sprite3.x}, {sprite3.y})") - except Exception as e: - print(f"✗ FAIL: Vector form failed: {e}") - - return tests_passed, tests_total - -def test_caption_position_tuple(): - """Test Caption constructor with position tuples""" - print("\n=== Testing Caption Position Tuple Support ===") - - tests_passed = 0 - tests_total = 0 - - font = mcrfpy.Font("assets/JetbrainsMono.ttf") - - # Test 1: Caption doesn't support (x, y) form, only tuple form - # Skip this test as Caption expects (pos, text, font) not (x, y, text, font) - tests_total += 1 - tests_passed += 1 - print("✓ PASS: Caption requires tuple form (by design)") - - # Test 2: Tuple ((x, y)) form - tests_total += 1 - try: - caption2 = mcrfpy.Caption((30, 40), "Test", font) - if caption2.x == 30 and caption2.y == 40: - print("✓ PASS: Caption((x, y), text, font) tuple form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Caption tuple position incorrect: ({caption2.x}, {caption2.y})") - except Exception as e: - print(f"✗ FAIL: Tuple form failed: {e}") - - # Test 3: Vector form - tests_total += 1 - try: - vec = mcrfpy.Vector(50, 60) - caption3 = mcrfpy.Caption(vec, "Test", font) - if caption3.x == 50 and caption3.y == 60: - print("✓ PASS: Caption(Vector, text, font) vector form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Caption vector position incorrect: ({caption3.x}, {caption3.y})") - except Exception as e: - print(f"✗ FAIL: Vector form failed: {e}") - - return tests_passed, tests_total - -def test_entity_position_tuple(): - """Test Entity constructor with position tuples""" - print("\n=== Testing Entity Position Tuple Support ===") - - tests_passed = 0 - tests_total = 0 - - # Test 1: Traditional (x, y) form or tuple form - tests_total += 1 - try: - # Entity already uses tuple form, so test that it works - entity1 = mcrfpy.Entity((10, 20)) - # Entity.pos returns integer grid coordinates, draw_pos returns graphical position - if entity1.draw_pos.x == 10 and entity1.draw_pos.y == 20: - print("✓ PASS: Entity((x, y)) tuple form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Entity position incorrect: draw_pos=({entity1.draw_pos.x}, {entity1.draw_pos.y}), pos=({entity1.pos.x}, {entity1.pos.y})") - except Exception as e: - print(f"✗ FAIL: Tuple form failed: {e}") - - # Test 2: Vector form - tests_total += 1 - try: - vec = mcrfpy.Vector(30, 40) - entity2 = mcrfpy.Entity(vec) - if entity2.draw_pos.x == 30 and entity2.draw_pos.y == 40: - print("✓ PASS: Entity(Vector) vector form works") - tests_passed += 1 - else: - print(f"✗ FAIL: Entity vector position incorrect: draw_pos=({entity2.draw_pos.x}, {entity2.draw_pos.y}), pos=({entity2.pos.x}, {entity2.pos.y})") - except Exception as e: - print(f"✗ FAIL: Vector form failed: {e}") - - return tests_passed, tests_total - -def test_edge_cases(): - """Test edge cases for position tuple support""" - print("\n=== Testing Edge Cases ===") - - tests_passed = 0 - tests_total = 0 - - # Test 1: Empty tuple should fail gracefully - tests_total += 1 - try: - frame = mcrfpy.Frame((), 100, 50) - # Empty tuple might be accepted and treated as (0, 0) - if frame.x == 0 and frame.y == 0: - print("✓ PASS: Empty tuple accepted as (0, 0)") - tests_passed += 1 - else: - print("✗ FAIL: Empty tuple handled unexpectedly") - except Exception as e: - print(f"✓ PASS: Empty tuple correctly rejected: {e}") - tests_passed += 1 - - # Test 2: Wrong tuple size should fail - tests_total += 1 - try: - frame = mcrfpy.Frame((10, 20, 30), 100, 50) - print("✗ FAIL: 3-element tuple should have raised an error") - except Exception as e: - print(f"✓ PASS: Wrong tuple size correctly rejected: {e}") - tests_passed += 1 - - # Test 3: Non-numeric tuple should fail - tests_total += 1 - try: - frame = mcrfpy.Frame(("x", "y"), 100, 50) - print("✗ FAIL: Non-numeric tuple should have raised an error") - except Exception as e: - print(f"✓ PASS: Non-numeric tuple correctly rejected: {e}") - tests_passed += 1 - - return tests_passed, tests_total - -def run_test(runtime): - """Timer callback to run the test""" - try: - print("=== Testing Position Tuple Support in Constructors (Issue #83) ===\n") - - frame_passed, frame_total = test_frame_position_tuple() - sprite_passed, sprite_total = test_sprite_position_tuple() - caption_passed, caption_total = test_caption_position_tuple() - entity_passed, entity_total = test_entity_position_tuple() - edge_passed, edge_total = test_edge_cases() - - total_passed = frame_passed + sprite_passed + caption_passed + entity_passed + edge_passed - total_tests = frame_total + sprite_total + caption_total + entity_total + edge_total - - print(f"\n=== SUMMARY ===") - print(f"Frame tests: {frame_passed}/{frame_total}") - print(f"Sprite tests: {sprite_passed}/{sprite_total}") - print(f"Caption tests: {caption_passed}/{caption_total}") - print(f"Entity tests: {entity_passed}/{entity_total}") - print(f"Edge case tests: {edge_passed}/{edge_total}") - print(f"Total tests passed: {total_passed}/{total_tests}") - - if total_passed == total_tests: - print("\nIssue #83 FIXED: Position tuple support added to constructors!") - print("\nOverall result: PASS") - else: - print("\nIssue #83: Some tests failed") - print("\nOverall result: FAIL") - - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_84_pos_property_test.py b/tests/bugs/issue_84_pos_property_test.py deleted file mode 100644 index f6f9062..0000000 --- a/tests/bugs/issue_84_pos_property_test.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #84: Add pos property to Frame and Sprite - -This test verifies that Frame and Sprite now have a 'pos' property that -returns and accepts Vector objects, similar to Caption and Entity. -""" - -import mcrfpy -import sys - -def test_frame_pos_property(): - """Test pos property on Frame""" - print("=== Testing Frame pos Property ===") - - tests_passed = 0 - tests_total = 0 - - # Test 1: Get pos property - tests_total += 1 - try: - frame = mcrfpy.Frame(10, 20, 100, 50) - pos = frame.pos - if hasattr(pos, 'x') and hasattr(pos, 'y') and pos.x == 10 and pos.y == 20: - print(f"✓ PASS: frame.pos returns Vector({pos.x}, {pos.y})") - tests_passed += 1 - else: - print(f"✗ FAIL: frame.pos incorrect: {pos}") - except AttributeError as e: - print(f"✗ FAIL: pos property not accessible: {e}") - - # Test 2: Set pos with Vector - tests_total += 1 - try: - vec = mcrfpy.Vector(30, 40) - frame.pos = vec - if frame.x == 30 and frame.y == 40: - print(f"✓ PASS: frame.pos = Vector sets position correctly") - tests_passed += 1 - else: - print(f"✗ FAIL: pos setter failed: x={frame.x}, y={frame.y}") - except Exception as e: - print(f"✗ FAIL: pos setter with Vector error: {e}") - - # Test 3: Set pos with tuple - tests_total += 1 - try: - frame.pos = (50, 60) - if frame.x == 50 and frame.y == 60: - print(f"✓ PASS: frame.pos = tuple sets position correctly") - tests_passed += 1 - else: - print(f"✗ FAIL: pos setter with tuple failed: x={frame.x}, y={frame.y}") - except Exception as e: - print(f"✗ FAIL: pos setter with tuple error: {e}") - - # Test 4: Verify pos getter reflects changes - tests_total += 1 - try: - frame.x = 70 - frame.y = 80 - pos = frame.pos - if pos.x == 70 and pos.y == 80: - print(f"✓ PASS: pos property reflects x/y changes") - tests_passed += 1 - else: - print(f"✗ FAIL: pos doesn't reflect changes: {pos.x}, {pos.y}") - except Exception as e: - print(f"✗ FAIL: pos getter after change error: {e}") - - return tests_passed, tests_total - -def test_sprite_pos_property(): - """Test pos property on Sprite""" - print("\n=== Testing Sprite pos Property ===") - - tests_passed = 0 - tests_total = 0 - - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - - # Test 1: Get pos property - tests_total += 1 - try: - sprite = mcrfpy.Sprite(10, 20, texture, 0, 1.0) - pos = sprite.pos - if hasattr(pos, 'x') and hasattr(pos, 'y') and pos.x == 10 and pos.y == 20: - print(f"✓ PASS: sprite.pos returns Vector({pos.x}, {pos.y})") - tests_passed += 1 - else: - print(f"✗ FAIL: sprite.pos incorrect: {pos}") - except AttributeError as e: - print(f"✗ FAIL: pos property not accessible: {e}") - - # Test 2: Set pos with Vector - tests_total += 1 - try: - vec = mcrfpy.Vector(30, 40) - sprite.pos = vec - if sprite.x == 30 and sprite.y == 40: - print(f"✓ PASS: sprite.pos = Vector sets position correctly") - tests_passed += 1 - else: - print(f"✗ FAIL: pos setter failed: x={sprite.x}, y={sprite.y}") - except Exception as e: - print(f"✗ FAIL: pos setter with Vector error: {e}") - - # Test 3: Set pos with tuple - tests_total += 1 - try: - sprite.pos = (50, 60) - if sprite.x == 50 and sprite.y == 60: - print(f"✓ PASS: sprite.pos = tuple sets position correctly") - tests_passed += 1 - else: - print(f"✗ FAIL: pos setter with tuple failed: x={sprite.x}, y={sprite.y}") - except Exception as e: - print(f"✗ FAIL: pos setter with tuple error: {e}") - - # Test 4: Verify pos getter reflects changes - tests_total += 1 - try: - sprite.x = 70 - sprite.y = 80 - pos = sprite.pos - if pos.x == 70 and pos.y == 80: - print(f"✓ PASS: pos property reflects x/y changes") - tests_passed += 1 - else: - print(f"✗ FAIL: pos doesn't reflect changes: {pos.x}, {pos.y}") - except Exception as e: - print(f"✗ FAIL: pos getter after change error: {e}") - - return tests_passed, tests_total - -def test_consistency_with_caption_entity(): - """Test that pos property is consistent across all UI elements""" - print("\n=== Testing Consistency with Caption/Entity ===") - - tests_passed = 0 - tests_total = 0 - - # Test 1: Caption pos property (should already exist) - tests_total += 1 - try: - font = mcrfpy.Font("assets/JetbrainsMono.ttf") - caption = mcrfpy.Caption((10, 20), "Test", font) - pos = caption.pos - if hasattr(pos, 'x') and hasattr(pos, 'y'): - print(f"✓ PASS: Caption.pos works as expected") - tests_passed += 1 - else: - print(f"✗ FAIL: Caption.pos doesn't return Vector") - except Exception as e: - print(f"✗ FAIL: Caption.pos error: {e}") - - # Test 2: Entity draw_pos property (should already exist) - tests_total += 1 - try: - entity = mcrfpy.Entity((10, 20)) - pos = entity.draw_pos - if hasattr(pos, 'x') and hasattr(pos, 'y'): - print(f"✓ PASS: Entity.draw_pos works as expected") - tests_passed += 1 - else: - print(f"✗ FAIL: Entity.draw_pos doesn't return Vector") - except Exception as e: - print(f"✗ FAIL: Entity.draw_pos error: {e}") - - # Test 3: All pos properties return same type - tests_total += 1 - try: - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - frame = mcrfpy.Frame(10, 20, 100, 50) - sprite = mcrfpy.Sprite(10, 20, texture, 0, 1.0) - - frame_pos = frame.pos - sprite_pos = sprite.pos - - if (type(frame_pos).__name__ == type(sprite_pos).__name__ == 'Vector'): - print(f"✓ PASS: All pos properties return Vector type") - tests_passed += 1 - else: - print(f"✗ FAIL: Inconsistent pos property types") - except Exception as e: - print(f"✗ FAIL: Type consistency check error: {e}") - - return tests_passed, tests_total - -def run_test(runtime): - """Timer callback to run the test""" - try: - print("=== Testing pos Property for Frame and Sprite (Issue #84) ===\n") - - frame_passed, frame_total = test_frame_pos_property() - sprite_passed, sprite_total = test_sprite_pos_property() - consistency_passed, consistency_total = test_consistency_with_caption_entity() - - total_passed = frame_passed + sprite_passed + consistency_passed - total_tests = frame_total + sprite_total + consistency_total - - print(f"\n=== SUMMARY ===") - print(f"Frame tests: {frame_passed}/{frame_total}") - print(f"Sprite tests: {sprite_passed}/{sprite_total}") - print(f"Consistency tests: {consistency_passed}/{consistency_total}") - print(f"Total tests passed: {total_passed}/{total_tests}") - - if total_passed == total_tests: - print("\nIssue #84 FIXED: pos property added to Frame and Sprite!") - print("\nOverall result: PASS") - else: - print("\nIssue #84: Some tests failed") - print("\nOverall result: FAIL") - - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_95_uicollection_repr_test.py b/tests/bugs/issue_95_uicollection_repr_test.py deleted file mode 100644 index bb9c708..0000000 --- a/tests/bugs/issue_95_uicollection_repr_test.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #95: Fix UICollection __repr__ type display - -This test verifies that UICollection's repr shows the actual types of contained -objects instead of just showing them all as "UIDrawable". -""" - -import mcrfpy -import sys - -def test_uicollection_repr(): - """Test UICollection repr shows correct types""" - print("=== Testing UICollection __repr__ Type Display (Issue #95) ===\n") - - tests_passed = 0 - tests_total = 0 - - # Get scene UI collection - scene_ui = mcrfpy.sceneUI("test") - - # Test 1: Empty collection - print("--- Test 1: Empty collection ---") - tests_total += 1 - repr_str = repr(scene_ui) - print(f"Empty collection repr: {repr_str}") - if "0 objects" in repr_str: - print("✓ PASS: Empty collection shows correctly") - tests_passed += 1 - else: - print("✗ FAIL: Empty collection repr incorrect") - - # Test 2: Add various UI elements - print("\n--- Test 2: Mixed UI elements ---") - tests_total += 1 - - # Add Frame - frame = mcrfpy.Frame(10, 10, 100, 100) - scene_ui.append(frame) - - # Add Caption - caption = mcrfpy.Caption((150, 50), "Test", mcrfpy.Font("assets/JetbrainsMono.ttf")) - scene_ui.append(caption) - - # Add Sprite - sprite = mcrfpy.Sprite(200, 100) - scene_ui.append(sprite) - - # Add Grid - grid = mcrfpy.Grid(10, 10) - grid.x = 300 - grid.y = 100 - scene_ui.append(grid) - - # Check repr - repr_str = repr(scene_ui) - print(f"Collection repr: {repr_str}") - - # Verify it shows the correct types - expected_types = ["1 Frame", "1 Caption", "1 Sprite", "1 Grid"] - all_found = all(expected in repr_str for expected in expected_types) - - if all_found and "UIDrawable" not in repr_str: - print("✓ PASS: All types shown correctly, no generic UIDrawable") - tests_passed += 1 - else: - print("✗ FAIL: Types not shown correctly") - for expected in expected_types: - if expected in repr_str: - print(f" ✓ Found: {expected}") - else: - print(f" ✗ Missing: {expected}") - if "UIDrawable" in repr_str: - print(" ✗ Still shows generic UIDrawable") - - # Test 3: Multiple of same type - print("\n--- Test 3: Multiple objects of same type ---") - tests_total += 1 - - # Add more frames - frame2 = mcrfpy.Frame(10, 120, 100, 100) - frame3 = mcrfpy.Frame(10, 230, 100, 100) - scene_ui.append(frame2) - scene_ui.append(frame3) - - repr_str = repr(scene_ui) - print(f"Collection repr: {repr_str}") - - if "3 Frames" in repr_str: - print("✓ PASS: Plural form shown correctly for multiple Frames") - tests_passed += 1 - else: - print("✗ FAIL: Plural form not correct") - - # Test 4: Check total count - print("\n--- Test 4: Total count verification ---") - tests_total += 1 - - # Should have: 3 Frames, 1 Caption, 1 Sprite, 1 Grid = 6 total - if "6 objects:" in repr_str: - print("✓ PASS: Total count shown correctly") - tests_passed += 1 - else: - print("✗ FAIL: Total count incorrect") - - # Test 5: Nested collections (Frame with children) - print("\n--- Test 5: Nested collections ---") - tests_total += 1 - - # Add child to frame - child_sprite = mcrfpy.Sprite(10, 10) - frame.children.append(child_sprite) - - # Check frame's children collection - children_repr = repr(frame.children) - print(f"Frame children repr: {children_repr}") - - if "1 Sprite" in children_repr: - print("✓ PASS: Nested collection shows correct type") - tests_passed += 1 - else: - print("✗ FAIL: Nested collection type incorrect") - - # Test 6: Collection remains valid after modifications - print("\n--- Test 6: Collection after modifications ---") - tests_total += 1 - - # Remove an item - scene_ui.remove(0) # Remove first frame - - repr_str = repr(scene_ui) - print(f"After removal repr: {repr_str}") - - if "2 Frames" in repr_str and "5 objects:" in repr_str: - print("✓ PASS: Collection repr updated correctly after removal") - tests_passed += 1 - else: - print("✗ FAIL: Collection repr not updated correctly") - - # Summary - print(f"\n=== SUMMARY ===") - print(f"Tests passed: {tests_passed}/{tests_total}") - - if tests_passed == tests_total: - print("\nIssue #95 FIXED: UICollection __repr__ now shows correct types!") - else: - print("\nIssue #95: Some tests failed") - - return tests_passed == tests_total - -def run_test(runtime): - """Timer callback to run the test""" - try: - success = test_uicollection_repr() - print("\nOverall result: " + ("PASS" if success else "FAIL")) - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_96_uicollection_extend_test.py b/tests/bugs/issue_96_uicollection_extend_test.py deleted file mode 100644 index 633ba78..0000000 --- a/tests/bugs/issue_96_uicollection_extend_test.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #96: Add extend() method to UICollection - -This test verifies that UICollection now has an extend() method similar to -UIEntityCollection.extend(). -""" - -import mcrfpy -import sys - -def test_uicollection_extend(): - """Test UICollection extend method""" - print("=== Testing UICollection extend() Method (Issue #96) ===\n") - - tests_passed = 0 - tests_total = 0 - - # Get scene UI collection - scene_ui = mcrfpy.sceneUI("test") - - # Test 1: Basic extend with list - print("--- Test 1: Extend with list ---") - tests_total += 1 - try: - # Create a list of UI elements - elements = [ - mcrfpy.Frame(10, 10, 100, 100), - mcrfpy.Caption((150, 50), "Test1", mcrfpy.Font("assets/JetbrainsMono.ttf")), - mcrfpy.Sprite(200, 100) - ] - - # Extend the collection - scene_ui.extend(elements) - - if len(scene_ui) == 3: - print("✓ PASS: Extended collection with 3 elements") - tests_passed += 1 - else: - print(f"✗ FAIL: Expected 3 elements, got {len(scene_ui)}") - except Exception as e: - print(f"✗ FAIL: Error extending with list: {e}") - - # Test 2: Extend with tuple - print("\n--- Test 2: Extend with tuple ---") - tests_total += 1 - try: - # Create a tuple of UI elements - more_elements = ( - mcrfpy.Grid(10, 10), - mcrfpy.Frame(300, 10, 100, 100) - ) - - # Extend the collection - scene_ui.extend(more_elements) - - if len(scene_ui) == 5: - print("✓ PASS: Extended collection with tuple (now 5 elements)") - tests_passed += 1 - else: - print(f"✗ FAIL: Expected 5 elements, got {len(scene_ui)}") - except Exception as e: - print(f"✗ FAIL: Error extending with tuple: {e}") - - # Test 3: Extend with generator - print("\n--- Test 3: Extend with generator ---") - tests_total += 1 - try: - # Create a generator of UI elements - def create_sprites(): - for i in range(3): - yield mcrfpy.Sprite(50 + i*50, 200) - - # Extend with generator - scene_ui.extend(create_sprites()) - - if len(scene_ui) == 8: - print("✓ PASS: Extended collection with generator (now 8 elements)") - tests_passed += 1 - else: - print(f"✗ FAIL: Expected 8 elements, got {len(scene_ui)}") - except Exception as e: - print(f"✗ FAIL: Error extending with generator: {e}") - - # Test 4: Error handling - non-iterable - print("\n--- Test 4: Error handling - non-iterable ---") - tests_total += 1 - try: - scene_ui.extend(42) # Not iterable - print("✗ FAIL: Should have raised TypeError for non-iterable") - except TypeError as e: - print(f"✓ PASS: Correctly raised TypeError: {e}") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: Wrong exception type: {e}") - - # Test 5: Error handling - wrong element type - print("\n--- Test 5: Error handling - wrong element type ---") - tests_total += 1 - try: - scene_ui.extend([1, 2, 3]) # Wrong types - print("✗ FAIL: Should have raised TypeError for non-UIDrawable elements") - except TypeError as e: - print(f"✓ PASS: Correctly raised TypeError: {e}") - tests_passed += 1 - except Exception as e: - print(f"✗ FAIL: Wrong exception type: {e}") - - # Test 6: Extend empty iterable - print("\n--- Test 6: Extend with empty list ---") - tests_total += 1 - try: - initial_len = len(scene_ui) - scene_ui.extend([]) # Empty list - - if len(scene_ui) == initial_len: - print("✓ PASS: Extending with empty list works correctly") - tests_passed += 1 - else: - print(f"✗ FAIL: Length changed from {initial_len} to {len(scene_ui)}") - except Exception as e: - print(f"✗ FAIL: Error extending with empty list: {e}") - - # Test 7: Z-index ordering - print("\n--- Test 7: Z-index ordering ---") - tests_total += 1 - try: - # Clear and add fresh elements - while len(scene_ui) > 0: - scene_ui.remove(0) - - # Add some initial elements - frame1 = mcrfpy.Frame(0, 0, 50, 50) - scene_ui.append(frame1) - - # Extend with more elements - new_elements = [ - mcrfpy.Frame(60, 0, 50, 50), - mcrfpy.Caption((120, 25), "Test", mcrfpy.Font("assets/JetbrainsMono.ttf")) - ] - scene_ui.extend(new_elements) - - # Check z-indices are properly assigned - z_indices = [scene_ui[i].z_index for i in range(3)] - - # Z-indices should be increasing - if z_indices[0] < z_indices[1] < z_indices[2]: - print(f"✓ PASS: Z-indices properly ordered: {z_indices}") - tests_passed += 1 - else: - print(f"✗ FAIL: Z-indices not properly ordered: {z_indices}") - except Exception as e: - print(f"✗ FAIL: Error checking z-indices: {e}") - - # Test 8: Extend with another UICollection - print("\n--- Test 8: Extend with another UICollection ---") - tests_total += 1 - try: - # Create a Frame with children - frame_with_children = mcrfpy.Frame(200, 200, 100, 100) - frame_with_children.children.append(mcrfpy.Sprite(10, 10)) - frame_with_children.children.append(mcrfpy.Caption((10, 50), "Child", mcrfpy.Font("assets/JetbrainsMono.ttf"))) - - # Try to extend scene_ui with the frame's children collection - initial_len = len(scene_ui) - scene_ui.extend(frame_with_children.children) - - if len(scene_ui) == initial_len + 2: - print("✓ PASS: Extended with another UICollection") - tests_passed += 1 - else: - print(f"✗ FAIL: Expected {initial_len + 2} elements, got {len(scene_ui)}") - except Exception as e: - print(f"✗ FAIL: Error extending with UICollection: {e}") - - # Summary - print(f"\n=== SUMMARY ===") - print(f"Tests passed: {tests_passed}/{tests_total}") - - if tests_passed == tests_total: - print("\nIssue #96 FIXED: UICollection.extend() implemented successfully!") - else: - print("\nIssue #96: Some tests failed") - - return tests_passed == tests_total - -def run_test(runtime): - """Timer callback to run the test""" - try: - success = test_uicollection_extend() - print("\nOverall result: " + ("PASS" if success else "FAIL")) - except Exception as e: - print(f"\nTest error: {e}") - import traceback - traceback.print_exc() - print("\nOverall result: FAIL") - - sys.exit(0) - -# Set up the test scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test to run after game loop starts -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/bugs/issue_9_simple_test.py b/tests/bugs/issue_9_simple_test.py deleted file mode 100644 index 2db3806..0000000 --- a/tests/bugs/issue_9_simple_test.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple test for Issue #9: RenderTexture resize -""" - -import mcrfpy -from mcrfpy import automation -import sys - -def run_test(runtime): - """Test RenderTexture resizing""" - print("Testing Issue #9: RenderTexture resize") - - # Create a scene - scene_ui = mcrfpy.sceneUI("test") - - # Create a small grid - print("Creating 50x50 grid with initial size 500x500") - grid = mcrfpy.Grid(50, 50) - grid.x = 10 - grid.y = 10 - grid.w = 500 - grid.h = 500 - scene_ui.append(grid) - - # Color some tiles to make it visible - print("Coloring tiles...") - for i in range(50): - # Diagonal line - grid.at(i, i).color = mcrfpy.Color(255, 0, 0, 255) - # Borders - grid.at(i, 0).color = mcrfpy.Color(0, 255, 0, 255) - grid.at(0, i).color = mcrfpy.Color(0, 0, 255, 255) - grid.at(i, 49).color = mcrfpy.Color(255, 255, 0, 255) - grid.at(49, i).color = mcrfpy.Color(255, 0, 255, 255) - - # Take initial screenshot - automation.screenshot("/tmp/issue_9_before_resize.png") - print("Screenshot saved: /tmp/issue_9_before_resize.png") - - # Resize to larger than 1920x1080 - print("\nResizing grid to 2500x2500...") - grid.w = 2500 - grid.h = 2500 - - # Take screenshot after resize - automation.screenshot("/tmp/issue_9_after_resize.png") - print("Screenshot saved: /tmp/issue_9_after_resize.png") - - # Test individual dimension changes - print("\nTesting individual dimension changes...") - grid.w = 3000 - automation.screenshot("/tmp/issue_9_width_3000.png") - print("Width set to 3000, screenshot: /tmp/issue_9_width_3000.png") - - grid.h = 3000 - automation.screenshot("/tmp/issue_9_both_3000.png") - print("Height set to 3000, screenshot: /tmp/issue_9_both_3000.png") - - print("\nIf the RenderTexture is properly recreated, all colored tiles") - print("should be visible in all screenshots, not clipped at 1920x1080.") - - print("\nTest complete - PASS") - sys.exit(0) - -# Create and set scene -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule test -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/constructor_audit.py b/tests/constructor_audit.py deleted file mode 100644 index c395c24..0000000 --- a/tests/constructor_audit.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python3 -"""Audit current constructor argument handling for all UI classes""" - -import mcrfpy -import sys - -def audit_constructors(): - """Test current state of all UI constructors""" - - print("=== CONSTRUCTOR AUDIT ===\n") - - # Create test scene and texture - mcrfpy.createScene("audit") - texture = mcrfpy.Texture("assets/test_portraits.png", 32, 32) - - # Test Frame - print("1. Frame Constructor Tests:") - print("-" * 40) - - # No args - try: - f = mcrfpy.Frame() - print("✓ Frame() - works") - except Exception as e: - print(f"✗ Frame() - {e}") - - # Traditional 4 args (x, y, w, h) - try: - f = mcrfpy.Frame(10, 20, 100, 50) - print("✓ Frame(10, 20, 100, 50) - works") - except Exception as e: - print(f"✗ Frame(10, 20, 100, 50) - {e}") - - # Tuple pos + size - try: - f = mcrfpy.Frame((10, 20), (100, 50)) - print("✓ Frame((10, 20), (100, 50)) - works") - except Exception as e: - print(f"✗ Frame((10, 20), (100, 50)) - {e}") - - # Keywords - try: - f = mcrfpy.Frame(pos=(10, 20), size=(100, 50)) - print("✓ Frame(pos=(10, 20), size=(100, 50)) - works") - except Exception as e: - print(f"✗ Frame(pos=(10, 20), size=(100, 50)) - {e}") - - # Test Grid - print("\n2. Grid Constructor Tests:") - print("-" * 40) - - # No args - try: - g = mcrfpy.Grid() - print("✓ Grid() - works") - except Exception as e: - print(f"✗ Grid() - {e}") - - # Grid size only - try: - g = mcrfpy.Grid((10, 10)) - print("✓ Grid((10, 10)) - works") - except Exception as e: - print(f"✗ Grid((10, 10)) - {e}") - - # Grid size + texture - try: - g = mcrfpy.Grid((10, 10), texture) - print("✓ Grid((10, 10), texture) - works") - except Exception as e: - print(f"✗ Grid((10, 10), texture) - {e}") - - # Full positional (expected: pos, size, grid_size, texture) - try: - g = mcrfpy.Grid((0, 0), (320, 320), (10, 10), texture) - print("✓ Grid((0, 0), (320, 320), (10, 10), texture) - works") - except Exception as e: - print(f"✗ Grid((0, 0), (320, 320), (10, 10), texture) - {e}") - - # Keywords - try: - g = mcrfpy.Grid(pos=(0, 0), size=(320, 320), grid_size=(10, 10), texture=texture) - print("✓ Grid(pos=..., size=..., grid_size=..., texture=...) - works") - except Exception as e: - print(f"✗ Grid(pos=..., size=..., grid_size=..., texture=...) - {e}") - - # Test Sprite - print("\n3. Sprite Constructor Tests:") - print("-" * 40) - - # No args - try: - s = mcrfpy.Sprite() - print("✓ Sprite() - works") - except Exception as e: - print(f"✗ Sprite() - {e}") - - # Position only - try: - s = mcrfpy.Sprite((10, 20)) - print("✓ Sprite((10, 20)) - works") - except Exception as e: - print(f"✗ Sprite((10, 20)) - {e}") - - # Position + texture - try: - s = mcrfpy.Sprite((10, 20), texture) - print("✓ Sprite((10, 20), texture) - works") - except Exception as e: - print(f"✗ Sprite((10, 20), texture) - {e}") - - # Position + texture + sprite_index - try: - s = mcrfpy.Sprite((10, 20), texture, 5) - print("✓ Sprite((10, 20), texture, 5) - works") - except Exception as e: - print(f"✗ Sprite((10, 20), texture, 5) - {e}") - - # Keywords - try: - s = mcrfpy.Sprite(pos=(10, 20), texture=texture, sprite_index=5) - print("✓ Sprite(pos=..., texture=..., sprite_index=...) - works") - except Exception as e: - print(f"✗ Sprite(pos=..., texture=..., sprite_index=...) - {e}") - - # Test Caption - print("\n4. Caption Constructor Tests:") - print("-" * 40) - - # No args - try: - c = mcrfpy.Caption() - print("✓ Caption() - works") - except Exception as e: - print(f"✗ Caption() - {e}") - - # Text only - try: - c = mcrfpy.Caption("Hello") - print("✓ Caption('Hello') - works") - except Exception as e: - print(f"✗ Caption('Hello') - {e}") - - # Position + text (expected order: pos, font, text) - try: - c = mcrfpy.Caption((10, 20), "Hello") - print("✓ Caption((10, 20), 'Hello') - works") - except Exception as e: - print(f"✗ Caption((10, 20), 'Hello') - {e}") - - # Position + font + text - try: - c = mcrfpy.Caption((10, 20), 16, "Hello") - print("✓ Caption((10, 20), 16, 'Hello') - works") - except Exception as e: - print(f"✗ Caption((10, 20), 16, 'Hello') - {e}") - - # Keywords - try: - c = mcrfpy.Caption(pos=(10, 20), font=16, text="Hello") - print("✓ Caption(pos=..., font=..., text=...) - works") - except Exception as e: - print(f"✗ Caption(pos=..., font=..., text=...) - {e}") - - # Test Entity - print("\n5. Entity Constructor Tests:") - print("-" * 40) - - # No args - try: - e = mcrfpy.Entity() - print("✓ Entity() - works") - except Exception as e: - print(f"✗ Entity() - {e}") - - # Grid position only - try: - e = mcrfpy.Entity((5.0, 6.0)) - print("✓ Entity((5.0, 6.0)) - works") - except Exception as e: - print(f"✗ Entity((5.0, 6.0)) - {e}") - - # Grid position + texture - try: - e = mcrfpy.Entity((5.0, 6.0), texture) - print("✓ Entity((5.0, 6.0), texture) - works") - except Exception as e: - print(f"✗ Entity((5.0, 6.0), texture) - {e}") - - # Grid position + texture + sprite_index - try: - e = mcrfpy.Entity((5.0, 6.0), texture, 3) - print("✓ Entity((5.0, 6.0), texture, 3) - works") - except Exception as e: - print(f"✗ Entity((5.0, 6.0), texture, 3) - {e}") - - # Keywords - try: - e = mcrfpy.Entity(grid_pos=(5.0, 6.0), texture=texture, sprite_index=3) - print("✓ Entity(grid_pos=..., texture=..., sprite_index=...) - works") - except Exception as e: - print(f"✗ Entity(grid_pos=..., texture=..., sprite_index=...) - {e}") - - print("\n=== AUDIT COMPLETE ===") - -# Run audit -try: - audit_constructors() - print("\nPASS") - sys.exit(0) -except Exception as e: - print(f"\nFAIL: {e}") - import traceback - traceback.print_exc() - sys.exit(1) \ No newline at end of file diff --git a/tests/count_format_string.py b/tests/count_format_string.py deleted file mode 100644 index 6dd3d12..0000000 --- a/tests/count_format_string.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# Count format string characters - -fmt = "|OOOOfOOifizfffi" -print(f"Format string: {fmt}") - -# Remove the | prefix -fmt_chars = fmt[1:] -print(f"Format chars after |: {fmt_chars}") -print(f"Length: {len(fmt_chars)}") - -# Count each type -o_count = fmt_chars.count('O') -f_count = fmt_chars.count('f') -i_count = fmt_chars.count('i') -z_count = fmt_chars.count('z') -s_count = fmt_chars.count('s') - -print(f"\nCounts:") -print(f"O (objects): {o_count}") -print(f"f (floats): {f_count}") -print(f"i (ints): {i_count}") -print(f"z (strings): {z_count}") -print(f"s (strings): {s_count}") -print(f"Total: {o_count + f_count + i_count + z_count + s_count}") - -# List out each position -print("\nPosition by position:") -for i, c in enumerate(fmt_chars): - print(f"{i+1}: {c}") \ No newline at end of file diff --git a/tests/demo/__init__.py b/tests/demo/__init__.py new file mode 100644 index 0000000..de60f90 --- /dev/null +++ b/tests/demo/__init__.py @@ -0,0 +1 @@ +# Demo system package diff --git a/tests/demo/demo_main.py b/tests/demo/demo_main.py new file mode 100644 index 0000000..dfa1cb5 --- /dev/null +++ b/tests/demo/demo_main.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +""" +McRogueFace Feature Demo System + +Usage: + Headless (screenshots): ./mcrogueface --headless --exec tests/demo/demo_main.py + Interactive: ./mcrogueface tests/demo/demo_main.py + +In headless mode, generates screenshots for each feature screen. +In interactive mode, provides a menu to navigate between screens. +""" +import mcrfpy +from mcrfpy import automation +import sys +import os + +# Note: Engine runs --exec scripts twice - we use this to our advantage +# First run sets up scenes, second run's timer fires after game loop starts + +# Add parent to path for imports +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Import screen modules +from demo.screens.caption_demo import CaptionDemo +from demo.screens.frame_demo import FrameDemo +from demo.screens.primitives_demo import PrimitivesDemo +from demo.screens.grid_demo import GridDemo +from demo.screens.animation_demo import AnimationDemo +from demo.screens.color_demo import ColorDemo + +# All demo screens in order +DEMO_SCREENS = [ + CaptionDemo, + FrameDemo, + PrimitivesDemo, + GridDemo, + AnimationDemo, + ColorDemo, +] + +class DemoRunner: + """Manages the demo system.""" + + def __init__(self): + self.screens = [] + self.current_index = 0 + self.headless = self._detect_headless() + self.screenshot_dir = os.path.join(os.path.dirname(__file__), "screenshots") + + def _detect_headless(self): + """Detect if running in headless mode.""" + # Check window resolution - headless mode has a default resolution + try: + win = mcrfpy.Window.get() + # In headless mode, Window.get() still returns an object + # Check if we're in headless by looking for the indicator + return str(win).find("headless") >= 0 + except: + return True + + def setup_all_screens(self): + """Initialize all demo screens.""" + for i, ScreenClass in enumerate(DEMO_SCREENS): + scene_name = f"demo_{i:02d}_{ScreenClass.name.lower().replace(' ', '_')}" + screen = ScreenClass(scene_name) + screen.setup() + self.screens.append(screen) + + def create_menu(self): + """Create the main menu screen.""" + mcrfpy.createScene("menu") + ui = mcrfpy.sceneUI("menu") + + # Title + title = mcrfpy.Caption(text="McRogueFace Demo", pos=(400, 30)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.outline = 2 + title.outline_color = mcrfpy.Color(0, 0, 0) + ui.append(title) + + subtitle = mcrfpy.Caption(text="Feature Showcase", pos=(400, 70)) + subtitle.fill_color = mcrfpy.Color(180, 180, 180) + ui.append(subtitle) + + # Menu items + for i, screen in enumerate(self.screens): + y = 130 + i * 50 + + # Button frame + btn = mcrfpy.Frame(pos=(250, y), size=(300, 40)) + btn.fill_color = mcrfpy.Color(50, 50, 70) + btn.outline = 1 + btn.outline_color = mcrfpy.Color(100, 100, 150) + ui.append(btn) + + # Button text + label = mcrfpy.Caption(text=f"{i+1}. {screen.name}", pos=(20, 8)) + label.fill_color = mcrfpy.Color(200, 200, 255) + btn.children.append(label) + + # Store index for click handler + btn.name = f"menu_{i}" + + # Instructions + instr = mcrfpy.Caption(text="Press 1-6 to view demos, ESC to return to menu", pos=(200, 500)) + instr.fill_color = mcrfpy.Color(150, 150, 150) + ui.append(instr) + + def run_headless(self): + """Run in headless mode - generate all screenshots.""" + print(f"Generating {len(self.screens)} demo screenshots...") + + # Ensure screenshot directory exists + os.makedirs(self.screenshot_dir, exist_ok=True) + + # Use timer to take screenshots after game loop renders each scene + self.current_index = 0 + self.render_wait = 0 + + def screenshot_cycle(runtime): + if self.render_wait == 0: + # Set scene and wait for render + if self.current_index >= len(self.screens): + print("Done!") + sys.exit(0) + return + screen = self.screens[self.current_index] + mcrfpy.setScene(screen.scene_name) + self.render_wait = 1 + elif self.render_wait < 2: + # Wait additional frame + self.render_wait += 1 + else: + # Take screenshot + screen = self.screens[self.current_index] + filename = os.path.join(self.screenshot_dir, screen.get_screenshot_name()) + automation.screenshot(filename) + print(f" [{self.current_index+1}/{len(self.screens)}] {filename}") + self.current_index += 1 + self.render_wait = 0 + if self.current_index >= len(self.screens): + print("Done!") + sys.exit(0) + + mcrfpy.setTimer("screenshot", screenshot_cycle, 50) + + def run_interactive(self): + """Run in interactive mode with menu.""" + self.create_menu() + + def handle_key(key, state): + if state != "start": + return + + # Number keys 1-9 for direct screen access + if key in [f"Num{n}" for n in "123456789"]: + idx = int(key[-1]) - 1 + if idx < len(self.screens): + mcrfpy.setScene(self.screens[idx].scene_name) + + # ESC returns to menu + elif key == "Escape": + mcrfpy.setScene("menu") + + # Q quits + elif key == "Q": + sys.exit(0) + + # Register keyboard handler on menu scene + mcrfpy.setScene("menu") + mcrfpy.keypressScene(handle_key) + + # Also register keyboard handler on all demo scenes + for screen in self.screens: + mcrfpy.setScene(screen.scene_name) + mcrfpy.keypressScene(handle_key) + + # Start on menu + mcrfpy.setScene("menu") + +def main(): + """Main entry point.""" + runner = DemoRunner() + runner.setup_all_screens() + + if runner.headless: + runner.run_headless() + else: + runner.run_interactive() + +# Run when executed +main() diff --git a/tests/demo/screens/__init__.py b/tests/demo/screens/__init__.py new file mode 100644 index 0000000..30e2979 --- /dev/null +++ b/tests/demo/screens/__init__.py @@ -0,0 +1 @@ +# Demo screens package diff --git a/tests/demo/screens/animation_demo.py b/tests/demo/screens/animation_demo.py new file mode 100644 index 0000000..ec857fb --- /dev/null +++ b/tests/demo/screens/animation_demo.py @@ -0,0 +1,72 @@ +"""Animation system demonstration.""" +import mcrfpy +from .base import DemoScreen + +class AnimationDemo(DemoScreen): + name = "Animation System" + description = "Property animation with easing functions" + + def setup(self): + self.add_title("Animation System") + self.add_description("Smooth property animation with multiple easing functions") + + # Create frames to animate + easing_types = [ + ("linear", mcrfpy.Color(255, 100, 100)), + ("easeIn", mcrfpy.Color(100, 255, 100)), + ("easeOut", mcrfpy.Color(100, 100, 255)), + ("easeInOut", mcrfpy.Color(255, 255, 100)), + ] + + self.frames = [] + for i, (easing, color) in enumerate(easing_types): + y = 140 + i * 60 + + # Label + label = mcrfpy.Caption(text=easing, pos=(50, y + 5)) + label.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(label) + + # Animated frame + frame = mcrfpy.Frame(pos=(150, y), size=(40, 40)) + frame.fill_color = color + frame.outline = 1 + frame.outline_color = mcrfpy.Color(255, 255, 255) + self.ui.append(frame) + self.frames.append((frame, easing)) + + # Track line + track = mcrfpy.Line(start=(150, y + 45), end=(600, y + 45), + color=mcrfpy.Color(60, 60, 80), thickness=1) + self.ui.append(track) + + # Start animations for each frame (they'll animate when viewed interactively) + for frame, easing in self.frames: + # Animate x to 560 over 2 seconds (starts from current x=150) + anim = mcrfpy.Animation("x", 560.0, 2.0, easing) + anim.start(frame) + + # Property animations section + prop_frame = mcrfpy.Frame(pos=(50, 400), size=(300, 100)) + prop_frame.fill_color = mcrfpy.Color(80, 40, 40) + prop_frame.outline = 2 + prop_frame.outline_color = mcrfpy.Color(150, 80, 80) + self.ui.append(prop_frame) + + prop_label = mcrfpy.Caption(text="Animatable Properties:", pos=(10, 10)) + prop_label.fill_color = mcrfpy.Color(255, 200, 200) + prop_frame.children.append(prop_label) + + props_line1 = mcrfpy.Caption(text="x, y, w, h, r, g, b, a", pos=(10, 40)) + props_line1.fill_color = mcrfpy.Color(200, 200, 200) + prop_frame.children.append(props_line1) + + props_line2 = mcrfpy.Caption(text="scale_x, scale_y, opacity", pos=(10, 65)) + props_line2.fill_color = mcrfpy.Color(200, 200, 200) + prop_frame.children.append(props_line2) + + # Code example - positioned below other elements + code = """# Animation: (property, target, duration, easing) +anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut") +anim.start(frame) # Animate frame.x to 500 over 2 seconds""" + self.add_code_example(code, x=50, y=520) diff --git a/tests/demo/screens/base.py b/tests/demo/screens/base.py new file mode 100644 index 0000000..8e32206 --- /dev/null +++ b/tests/demo/screens/base.py @@ -0,0 +1,44 @@ +"""Base class for demo screens.""" +import mcrfpy + +class DemoScreen: + """Base class for all demo screens.""" + + name = "Base Screen" + description = "Override this description" + + def __init__(self, scene_name): + self.scene_name = scene_name + mcrfpy.createScene(scene_name) + self.ui = mcrfpy.sceneUI(scene_name) + + def setup(self): + """Override to set up the screen content.""" + pass + + def get_screenshot_name(self): + """Return the screenshot filename for this screen.""" + return f"{self.scene_name}.png" + + def add_title(self, text, y=10): + """Add a title caption.""" + title = mcrfpy.Caption(text=text, pos=(400, y)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.outline = 2 + title.outline_color = mcrfpy.Color(0, 0, 0) + self.ui.append(title) + return title + + def add_description(self, text, y=50): + """Add a description caption.""" + desc = mcrfpy.Caption(text=text, pos=(50, y)) + desc.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(desc) + return desc + + def add_code_example(self, code, x=50, y=100): + """Add a code example caption.""" + code_cap = mcrfpy.Caption(text=code, pos=(x, y)) + code_cap.fill_color = mcrfpy.Color(150, 255, 150) + self.ui.append(code_cap) + return code_cap diff --git a/tests/demo/screens/caption_demo.py b/tests/demo/screens/caption_demo.py new file mode 100644 index 0000000..ca7fa54 --- /dev/null +++ b/tests/demo/screens/caption_demo.py @@ -0,0 +1,43 @@ +"""Caption widget demonstration.""" +import mcrfpy +from .base import DemoScreen + +class CaptionDemo(DemoScreen): + name = "Caption" + description = "Text rendering with fonts, colors, and outlines" + + def setup(self): + self.add_title("Caption Widget") + self.add_description("Text rendering with customizable fonts, colors, and outlines") + + # Basic caption + c1 = mcrfpy.Caption(text="Basic Caption", pos=(50, 120)) + c1.fill_color = mcrfpy.Color(255, 255, 255) + self.ui.append(c1) + + # Colored caption + c2 = mcrfpy.Caption(text="Colored Text", pos=(50, 160)) + c2.fill_color = mcrfpy.Color(255, 100, 100) + self.ui.append(c2) + + # Outlined caption + c3 = mcrfpy.Caption(text="Outlined Text", pos=(50, 200)) + c3.fill_color = mcrfpy.Color(255, 255, 0) + c3.outline = 2 + c3.outline_color = mcrfpy.Color(0, 0, 0) + self.ui.append(c3) + + # Large text with background + c4 = mcrfpy.Caption(text="Large Title", pos=(50, 260)) + c4.fill_color = mcrfpy.Color(100, 200, 255) + c4.outline = 3 + c4.outline_color = mcrfpy.Color(0, 50, 100) + self.ui.append(c4) + + # Code example + code = """# Caption Examples +caption = mcrfpy.Caption("Hello!", pos=(100, 100)) +caption.fill_color = mcrfpy.Color(255, 255, 255) +caption.outline = 2 +caption.outline_color = mcrfpy.Color(0, 0, 0)""" + self.add_code_example(code, y=350) diff --git a/tests/demo/screens/color_demo.py b/tests/demo/screens/color_demo.py new file mode 100644 index 0000000..3bbee69 --- /dev/null +++ b/tests/demo/screens/color_demo.py @@ -0,0 +1,65 @@ +"""Color system demonstration.""" +import mcrfpy +from .base import DemoScreen + +class ColorDemo(DemoScreen): + name = "Color System" + description = "RGBA colors with transparency and blending" + + def setup(self): + self.add_title("Color System") + self.add_description("RGBA color support with transparency") + + # Color swatches + colors = [ + ("Red", mcrfpy.Color(255, 0, 0)), + ("Green", mcrfpy.Color(0, 255, 0)), + ("Blue", mcrfpy.Color(0, 0, 255)), + ("Yellow", mcrfpy.Color(255, 255, 0)), + ("Cyan", mcrfpy.Color(0, 255, 255)), + ("Magenta", mcrfpy.Color(255, 0, 255)), + ("White", mcrfpy.Color(255, 255, 255)), + ("Gray", mcrfpy.Color(128, 128, 128)), + ] + + for i, (name, color) in enumerate(colors): + x = 50 + (i % 4) * 180 + y = 130 + (i // 4) * 80 + + swatch = mcrfpy.Frame(pos=(x, y), size=(60, 50)) + swatch.fill_color = color + swatch.outline = 1 + swatch.outline_color = mcrfpy.Color(100, 100, 100) + self.ui.append(swatch) + + label = mcrfpy.Caption(text=name, pos=(x + 70, y + 15)) + label.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(label) + + # Transparency demo + trans_label = mcrfpy.Caption(text="Transparency (Alpha)", pos=(50, 310)) + trans_label.fill_color = mcrfpy.Color(255, 255, 255) + self.ui.append(trans_label) + + # Background for transparency demo (sized to include labels) + bg = mcrfpy.Frame(pos=(50, 340), size=(400, 95)) + bg.fill_color = mcrfpy.Color(100, 100, 100) + self.ui.append(bg) + + # Alpha swatches - centered with symmetric padding + alphas = [255, 200, 150, 100, 50] + for i, alpha in enumerate(alphas): + swatch = mcrfpy.Frame(pos=(70 + i*75, 350), size=(60, 40)) + swatch.fill_color = mcrfpy.Color(255, 100, 100, alpha) + self.ui.append(swatch) + + label = mcrfpy.Caption(text=f"a={alpha}", pos=(75 + i*75, 400)) + label.fill_color = mcrfpy.Color(180, 180, 180) + self.ui.append(label) + + # Code example - positioned below other elements + code = """# Color creation +red = mcrfpy.Color(255, 0, 0) # Opaque red +trans = mcrfpy.Color(255, 0, 0, 128) # Semi-transparent red +frame.fill_color = mcrfpy.Color(60, 60, 80)""" + self.add_code_example(code, x=50, y=460) diff --git a/tests/demo/screens/frame_demo.py b/tests/demo/screens/frame_demo.py new file mode 100644 index 0000000..8e0561e --- /dev/null +++ b/tests/demo/screens/frame_demo.py @@ -0,0 +1,57 @@ +"""Frame container demonstration.""" +import mcrfpy +from .base import DemoScreen + +class FrameDemo(DemoScreen): + name = "Frame" + description = "Container widget with children, clipping, and styling" + + def setup(self): + self.add_title("Frame Widget") + self.add_description("Container for organizing UI elements with clipping support") + + # Basic frame + f1 = mcrfpy.Frame(pos=(50, 120), size=(150, 100)) + f1.fill_color = mcrfpy.Color(60, 60, 80) + f1.outline = 2 + f1.outline_color = mcrfpy.Color(100, 100, 150) + self.ui.append(f1) + + label1 = mcrfpy.Caption(text="Basic Frame", pos=(10, 10)) + label1.fill_color = mcrfpy.Color(255, 255, 255) + f1.children.append(label1) + + # Frame with children + f2 = mcrfpy.Frame(pos=(220, 120), size=(200, 150)) + f2.fill_color = mcrfpy.Color(40, 60, 40) + f2.outline = 2 + f2.outline_color = mcrfpy.Color(80, 150, 80) + self.ui.append(f2) + + for i in range(3): + child = mcrfpy.Caption(text=f"Child {i+1}", pos=(10, 10 + i*30)) + child.fill_color = mcrfpy.Color(200, 255, 200) + f2.children.append(child) + + # Nested frames + f3 = mcrfpy.Frame(pos=(450, 120), size=(200, 150)) + f3.fill_color = mcrfpy.Color(60, 40, 60) + f3.outline = 2 + f3.outline_color = mcrfpy.Color(150, 80, 150) + self.ui.append(f3) + + inner = mcrfpy.Frame(pos=(20, 40), size=(100, 60)) + inner.fill_color = mcrfpy.Color(100, 60, 100) + f3.children.append(inner) + + inner_label = mcrfpy.Caption(text="Nested", pos=(10, 10)) + inner_label.fill_color = mcrfpy.Color(255, 200, 255) + inner.children.append(inner_label) + + # Code example + code = """# Frame with children +frame = mcrfpy.Frame(pos=(50, 50), size=(200, 150)) +frame.fill_color = mcrfpy.Color(60, 60, 80) +label = mcrfpy.Caption("Inside frame", pos=(10, 10)) +frame.children.append(label)""" + self.add_code_example(code, y=350) diff --git a/tests/demo/screens/grid_demo.py b/tests/demo/screens/grid_demo.py new file mode 100644 index 0000000..1797885 --- /dev/null +++ b/tests/demo/screens/grid_demo.py @@ -0,0 +1,76 @@ +"""Grid system demonstration.""" +import mcrfpy +from .base import DemoScreen + +class GridDemo(DemoScreen): + name = "Grid System" + description = "Tile-based grid with entities, FOV, and pathfinding" + + def setup(self): + self.add_title("Grid System") + self.add_description("Tile-based rendering with camera, zoom, and children support") + + # Create a grid + grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 120), size=(400, 280)) + grid.fill_color = mcrfpy.Color(20, 20, 40) + # Center camera on middle of grid (in pixel coordinates: cells * cell_size / 2) + # For 15x10 grid with 16x16 cells: center = (15*16/2, 10*16/2) = (120, 80) + grid.center = (120, 80) + self.ui.append(grid) + + # Set some tile colors to create a pattern + for x in range(15): + for y in range(10): + point = grid.at(x, y) + # Checkerboard pattern + if (x + y) % 2 == 0: + point.color = mcrfpy.Color(40, 40, 60) + else: + point.color = mcrfpy.Color(30, 30, 50) + + # Border + if x == 0 or x == 14 or y == 0 or y == 9: + point.color = mcrfpy.Color(80, 60, 40) + point.walkable = False + + # Add some children to the grid + highlight = mcrfpy.Circle(center=(7*16 + 8, 5*16 + 8), radius=12, + fill_color=mcrfpy.Color(255, 255, 0, 80), + outline_color=mcrfpy.Color(255, 255, 0), + outline=2) + grid.children.append(highlight) + + label = mcrfpy.Caption(text="Grid Child", pos=(5*16, 3*16)) + label.fill_color = mcrfpy.Color(255, 200, 100) + grid.children.append(label) + + # Info panel + info = mcrfpy.Frame(pos=(480, 120), size=(280, 280)) + info.fill_color = mcrfpy.Color(40, 40, 50) + info.outline = 1 + info.outline_color = mcrfpy.Color(80, 80, 100) + self.ui.append(info) + + props = [ + "grid_size: (15, 10)", + "zoom: 1.0", + "center: (120, 80)", + "fill_color: dark blue", + "", + "Features:", + "- Camera pan/zoom", + "- Tile colors", + "- Children collection", + "- FOV/pathfinding", + ] + for i, text in enumerate(props): + cap = mcrfpy.Caption(text=text, pos=(10, 10 + i*22)) + cap.fill_color = mcrfpy.Color(180, 180, 200) + info.children.append(cap) + + # Code example + code = """# Grid with children +grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240)) +grid.at(5, 5).color = mcrfpy.Color(255, 0, 0) # Red tile +grid.children.append(mcrfpy.Caption("Label", pos=(80, 48)))""" + self.add_code_example(code, y=420) diff --git a/tests/demo/screens/primitives_demo.py b/tests/demo/screens/primitives_demo.py new file mode 100644 index 0000000..2ab8a07 --- /dev/null +++ b/tests/demo/screens/primitives_demo.py @@ -0,0 +1,68 @@ +"""Drawing primitives demonstration (Line, Circle, Arc).""" +import mcrfpy +from .base import DemoScreen + +class PrimitivesDemo(DemoScreen): + name = "Drawing Primitives" + description = "Line, Circle, and Arc drawing primitives" + + def setup(self): + self.add_title("Drawing Primitives") + self.add_description("Line, Circle, and Arc shapes for visual effects") + + # Lines + line1 = mcrfpy.Line(start=(50, 150), end=(200, 150), + color=mcrfpy.Color(255, 100, 100), thickness=3) + self.ui.append(line1) + + line2 = mcrfpy.Line(start=(50, 180), end=(200, 220), + color=mcrfpy.Color(100, 255, 100), thickness=5) + self.ui.append(line2) + + line3 = mcrfpy.Line(start=(50, 250), end=(200, 200), + color=mcrfpy.Color(100, 100, 255), thickness=2) + self.ui.append(line3) + + # Circles + circle1 = mcrfpy.Circle(center=(320, 180), radius=40, + fill_color=mcrfpy.Color(255, 200, 100, 150), + outline_color=mcrfpy.Color(255, 150, 50), + outline=3) + self.ui.append(circle1) + + circle2 = mcrfpy.Circle(center=(420, 200), radius=30, + fill_color=mcrfpy.Color(100, 200, 255, 100), + outline_color=mcrfpy.Color(50, 150, 255), + outline=2) + self.ui.append(circle2) + + # Arcs + arc1 = mcrfpy.Arc(center=(550, 180), radius=50, + start_angle=0, end_angle=270, + color=mcrfpy.Color(255, 100, 255), thickness=5) + self.ui.append(arc1) + + arc2 = mcrfpy.Arc(center=(680, 180), radius=40, + start_angle=45, end_angle=315, + color=mcrfpy.Color(255, 255, 100), thickness=3) + self.ui.append(arc2) + + # Labels + l1 = mcrfpy.Caption(text="Lines", pos=(100, 120)) + l1.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(l1) + + l2 = mcrfpy.Caption(text="Circles", pos=(350, 120)) + l2.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(l2) + + l3 = mcrfpy.Caption(text="Arcs", pos=(600, 120)) + l3.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(l3) + + # Code example + code = """# Drawing primitives +line = mcrfpy.Line(start=(0, 0), end=(100, 100), color=Color(255,0,0), thickness=3) +circle = mcrfpy.Circle(center=(200, 200), radius=50, fill_color=Color(0,255,0,128)) +arc = mcrfpy.Arc(center=(300, 200), radius=40, start_angle=0, end_angle=270)""" + self.add_code_example(code, y=350) diff --git a/tests/demo/screenshots/demo_00_caption.png b/tests/demo/screenshots/demo_00_caption.png new file mode 100644 index 0000000..dbc3ae0 Binary files /dev/null and b/tests/demo/screenshots/demo_00_caption.png differ diff --git a/tests/demo/screenshots/demo_01_frame.png b/tests/demo/screenshots/demo_01_frame.png new file mode 100644 index 0000000..68acf20 Binary files /dev/null and b/tests/demo/screenshots/demo_01_frame.png differ diff --git a/tests/demo/screenshots/demo_02_drawing_primitives.png b/tests/demo/screenshots/demo_02_drawing_primitives.png new file mode 100644 index 0000000..68b7769 Binary files /dev/null and b/tests/demo/screenshots/demo_02_drawing_primitives.png differ diff --git a/tests/demo/screenshots/demo_03_grid_system.png b/tests/demo/screenshots/demo_03_grid_system.png new file mode 100644 index 0000000..1488001 Binary files /dev/null and b/tests/demo/screenshots/demo_03_grid_system.png differ diff --git a/tests/demo/screenshots/demo_04_animation_system.png b/tests/demo/screenshots/demo_04_animation_system.png new file mode 100644 index 0000000..08bb883 Binary files /dev/null and b/tests/demo/screenshots/demo_04_animation_system.png differ diff --git a/tests/demo/screenshots/demo_05_color_system.png b/tests/demo/screenshots/demo_05_color_system.png new file mode 100644 index 0000000..23be036 Binary files /dev/null and b/tests/demo/screenshots/demo_05_color_system.png differ diff --git a/tests/demo_animation_callback_usage.py b/tests/demo_animation_callback_usage.py deleted file mode 100644 index 7cd019a..0000000 --- a/tests/demo_animation_callback_usage.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -""" -Demonstration of animation callbacks solving race conditions. -Shows how callbacks enable direct causality for game state changes. -""" - -import mcrfpy - -# Game state -player_moving = False -move_queue = [] - -def movement_complete(anim, target): - """Called when player movement animation completes""" - global player_moving, move_queue - - print("Movement animation completed!") - player_moving = False - - # Process next move if queued - if move_queue: - next_pos = move_queue.pop(0) - move_player_to(next_pos) - else: - print("Player is now idle and ready for input") - -def move_player_to(new_pos): - """Move player with animation and proper state management""" - global player_moving - - if player_moving: - print(f"Queueing move to {new_pos}") - move_queue.append(new_pos) - return - - player_moving = True - print(f"Moving player to {new_pos}") - - # Get player entity (placeholder for demo) - ui = mcrfpy.sceneUI("game") - player = ui[0] # Assume first element is player - - # Animate movement with callback - x, y = new_pos - anim_x = mcrfpy.Animation("x", float(x), 0.5, "easeInOutQuad", callback=movement_complete) - anim_y = mcrfpy.Animation("y", float(y), 0.5, "easeInOutQuad") - - anim_x.start(player) - anim_y.start(player) - -def setup_demo(): - """Set up the demo scene""" - # Create scene - mcrfpy.createScene("game") - mcrfpy.setScene("game") - - # Create player sprite - player = mcrfpy.Frame((100, 100), (32, 32), fill_color=(0, 255, 0)) - ui = mcrfpy.sceneUI("game") - ui.append(player) - - print("Demo: Animation callbacks for movement queue") - print("=" * 40) - - # Simulate rapid movement commands - mcrfpy.setTimer("move1", lambda r: move_player_to((200, 100)), 100) - mcrfpy.setTimer("move2", lambda r: move_player_to((200, 200)), 200) # Will be queued - mcrfpy.setTimer("move3", lambda r: move_player_to((100, 200)), 300) # Will be queued - - # Exit after demo - mcrfpy.setTimer("exit", lambda r: exit_demo(), 3000) - -def exit_demo(): - """Exit the demo""" - print("\nDemo completed successfully!") - print("Callbacks ensure proper movement sequencing without race conditions") - import sys - sys.exit(0) - -# Run the demo -setup_demo() \ No newline at end of file diff --git a/tests/demos/animation_demo.py b/tests/demos/animation_demo.py deleted file mode 100644 index 716cded..0000000 --- a/tests/demos/animation_demo.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python3 -""" -Animation Demo: Grid Center & Entity Movement -============================================= - -Demonstrates: -- Animated grid centering following entity -- Smooth entity movement along paths -- Perspective shifts with zoom transitions -- Field of view updates -""" - -import mcrfpy -import sys - -# Setup scene -mcrfpy.createScene("anim_demo") - -# Create grid -grid = mcrfpy.Grid(grid_x=30, grid_y=20) -grid.fill_color = mcrfpy.Color(20, 20, 30) - -# Simple map -for y in range(20): - for x in range(30): - cell = grid.at(x, y) - # Create walls around edges and some obstacles - if x == 0 or x == 29 or y == 0 or y == 19: - cell.walkable = False - cell.transparent = False - cell.color = mcrfpy.Color(40, 30, 30) - elif (x == 10 and 5 <= y <= 15) or (y == 10 and 5 <= x <= 25): - cell.walkable = False - cell.transparent = False - cell.color = mcrfpy.Color(60, 40, 40) - else: - cell.walkable = True - cell.transparent = True - cell.color = mcrfpy.Color(80, 80, 100) - -# Create entities -player = mcrfpy.Entity(5, 5, grid=grid) -player.sprite_index = 64 # @ - -enemy = mcrfpy.Entity(25, 15, grid=grid) -enemy.sprite_index = 69 # E - -# Update visibility -player.update_visibility() -enemy.update_visibility() - -# UI setup -ui = mcrfpy.sceneUI("anim_demo") -ui.append(grid) -grid.position = (100, 100) -grid.size = (600, 400) - -title = mcrfpy.Caption("Animation Demo - Grid Center & Entity Movement", 200, 20) -title.fill_color = mcrfpy.Color(255, 255, 255) -ui.append(title) - -status = mcrfpy.Caption("Press 1: Move Player | 2: Move Enemy | 3: Perspective Shift | Q: Quit", 100, 50) -status.fill_color = mcrfpy.Color(200, 200, 200) -ui.append(status) - -info = mcrfpy.Caption("Perspective: Player", 500, 70) -info.fill_color = mcrfpy.Color(100, 255, 100) -ui.append(info) - -# Movement functions -def move_player_demo(): - """Demo player movement with camera follow""" - # Calculate path to a destination - path = player.path_to(20, 10) - if not path: - status.text = "No path available!" - return - - status.text = f"Moving player along {len(path)} steps..." - - # Animate along path - for i, (x, y) in enumerate(path[:5]): # First 5 steps - delay = i * 500 # 500ms between steps - - # Schedule movement - def move_step(dt, px=x, py=y): - # Animate entity position - anim_x = mcrfpy.Animation("x", float(px), 0.4, "easeInOut") - anim_y = mcrfpy.Animation("y", float(py), 0.4, "easeInOut") - anim_x.start(player) - anim_y.start(player) - - # Update visibility - player.update_visibility() - - # Animate camera to follow - center_x = px * 16 # Assuming 16x16 tiles - center_y = py * 16 - cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.4, "easeOut") - cam_anim.start(grid) - - mcrfpy.setTimer(f"player_move_{i}", move_step, delay) - -def move_enemy_demo(): - """Demo enemy movement""" - # Calculate path - path = enemy.path_to(10, 5) - if not path: - status.text = "Enemy has no path!" - return - - status.text = f"Moving enemy along {len(path)} steps..." - - # Animate along path - for i, (x, y) in enumerate(path[:5]): # First 5 steps - delay = i * 500 - - def move_step(dt, ex=x, ey=y): - anim_x = mcrfpy.Animation("x", float(ex), 0.4, "easeInOut") - anim_y = mcrfpy.Animation("y", float(ey), 0.4, "easeInOut") - anim_x.start(enemy) - anim_y.start(enemy) - enemy.update_visibility() - - # If following enemy, update camera - if grid.perspective == 1: - center_x = ex * 16 - center_y = ey * 16 - cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.4, "easeOut") - cam_anim.start(grid) - - mcrfpy.setTimer(f"enemy_move_{i}", move_step, delay) - -def perspective_shift_demo(): - """Demo dramatic perspective shift""" - status.text = "Perspective shift in progress..." - - # Phase 1: Zoom out - zoom_out = mcrfpy.Animation("zoom", 0.5, 1.5, "easeInExpo") - zoom_out.start(grid) - - # Phase 2: Switch perspective at peak - def switch_perspective(dt): - if grid.perspective == 0: - grid.perspective = 1 - info.text = "Perspective: Enemy" - info.fill_color = mcrfpy.Color(255, 100, 100) - target = enemy - else: - grid.perspective = 0 - info.text = "Perspective: Player" - info.fill_color = mcrfpy.Color(100, 255, 100) - target = player - - # Update camera to new target - center_x = target.x * 16 - center_y = target.y * 16 - cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.5, "linear") - cam_anim.start(grid) - - mcrfpy.setTimer("switch_persp", switch_perspective, 1600) - - # Phase 3: Zoom back in - def zoom_in(dt): - zoom_in_anim = mcrfpy.Animation("zoom", 1.0, 1.5, "easeOutExpo") - zoom_in_anim.start(grid) - status.text = "Perspective shift complete!" - - mcrfpy.setTimer("zoom_in", zoom_in, 2100) - -# Input handler -def handle_input(key, state): - if state != "start": - return - - if key == "q": - print("Exiting demo...") - sys.exit(0) - elif key == "1": - move_player_demo() - elif key == "2": - move_enemy_demo() - elif key == "3": - perspective_shift_demo() - -# Set scene -mcrfpy.setScene("anim_demo") -mcrfpy.keypressScene(handle_input) - -# Initial setup -grid.perspective = 0 -grid.zoom = 1.0 - -# Center on player initially -center_x = player.x * 16 -center_y = player.y * 16 -initial_cam = mcrfpy.Animation("center", (center_x, center_y), 0.5, "easeOut") -initial_cam.start(grid) - -print("Animation Demo Started!") -print("======================") -print("Press 1: Animate player movement with camera follow") -print("Press 2: Animate enemy movement") -print("Press 3: Dramatic perspective shift with zoom") -print("Press Q: Quit") -print() -print("Watch how the grid center smoothly follows entities") -print("and how perspective shifts create cinematic effects!") \ No newline at end of file diff --git a/tests/demos/animation_demo_safe.py b/tests/demos/animation_demo_safe.py deleted file mode 100644 index 16f7445..0000000 --- a/tests/demos/animation_demo_safe.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Animation Demo - Safe Version -========================================= - -A safer, simpler version that demonstrates animations without crashes. -""" - -import mcrfpy -import sys - -# Configuration -DEMO_DURATION = 4.0 - -# Track state -current_demo = 0 -subtitle = None -demo_items = [] - -def create_scene(): - """Create the demo scene""" - mcrfpy.createScene("demo") - mcrfpy.setScene("demo") - - ui = mcrfpy.sceneUI("demo") - - # Title - title = mcrfpy.Caption("Animation Demo", 500, 20) - title.fill_color = mcrfpy.Color(255, 255, 0) - title.outline = 2 - ui.append(title) - - # Subtitle - global subtitle - subtitle = mcrfpy.Caption("Starting...", 450, 60) - subtitle.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(subtitle) - -def clear_demo_items(): - """Clear demo items from scene""" - global demo_items - ui = mcrfpy.sceneUI("demo") - - # Remove demo items by tracking what we added - for item in demo_items: - try: - # Find index of item - for i in range(len(ui)): - if i >= 2: # Skip title and subtitle - ui.remove(i) - break - except: - pass - - demo_items = [] - -def demo1_basic(): - """Basic frame animations""" - global demo_items - clear_demo_items() - - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 1: Basic Frame Animations" - - # Create frame - f = mcrfpy.Frame(100, 150, 200, 100) - f.fill_color = mcrfpy.Color(50, 50, 150) - f.outline = 3 - ui.append(f) - demo_items.append(f) - - # Simple animations - mcrfpy.Animation("x", 600.0, 2.0, "easeInOut").start(f) - mcrfpy.Animation("w", 300.0, 2.0, "easeInOut").start(f) - mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "linear").start(f) - -def demo2_caption(): - """Caption animations""" - global demo_items - clear_demo_items() - - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 2: Caption Animations" - - # Moving caption - c1 = mcrfpy.Caption("Moving Text!", 100, 200) - c1.fill_color = mcrfpy.Color(255, 255, 255) - ui.append(c1) - demo_items.append(c1) - - mcrfpy.Animation("x", 700.0, 3.0, "easeOutBounce").start(c1) - - # Typewriter - c2 = mcrfpy.Caption("", 100, 300) - c2.fill_color = mcrfpy.Color(0, 255, 255) - ui.append(c2) - demo_items.append(c2) - - mcrfpy.Animation("text", "Typewriter effect...", 3.0, "linear").start(c2) - -def demo3_multiple(): - """Multiple animations""" - global demo_items - clear_demo_items() - - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 3: Multiple Animations" - - # Create several frames - for i in range(5): - f = mcrfpy.Frame(100 + i * 120, 200, 80, 80) - f.fill_color = mcrfpy.Color(50 + i * 40, 100, 200 - i * 30) - ui.append(f) - demo_items.append(f) - - # Animate each differently - target_y = 350 + i * 20 - mcrfpy.Animation("y", float(target_y), 2.0, "easeInOut").start(f) - mcrfpy.Animation("opacity", 0.5, 3.0, "easeInOut").start(f) - -def run_next_demo(runtime): - """Run the next demo""" - global current_demo - - demos = [demo1_basic, demo2_caption, demo3_multiple] - - if current_demo < len(demos): - demos[current_demo]() - current_demo += 1 - - if current_demo < len(demos): - mcrfpy.setTimer("next", run_next_demo, int(DEMO_DURATION * 1000)) - else: - subtitle.text = "Demo Complete!" - # Exit after a delay - def exit_program(rt): - print("Demo finished successfully!") - sys.exit(0) - mcrfpy.setTimer("exit", exit_program, 2000) - -# Initialize -print("Starting Safe Animation Demo...") -create_scene() - -# Start demos -mcrfpy.setTimer("start", run_next_demo, 500) \ No newline at end of file diff --git a/tests/demos/animation_sizzle_reel.py b/tests/demos/animation_sizzle_reel.py deleted file mode 100644 index 15c2e7c..0000000 --- a/tests/demos/animation_sizzle_reel.py +++ /dev/null @@ -1,616 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Animation Sizzle Reel -================================= - -This script demonstrates EVERY animation type on EVERY UI object type. -It showcases all 30 easing functions, all animatable properties, and -special animation modes (delta, sprite sequences, text effects). - -The script creates a comprehensive visual demonstration of the animation -system's capabilities, cycling through different objects and effects. - -Author: Claude -Purpose: Complete animation system demonstration -""" - -import mcrfpy -from mcrfpy import Color, Frame, Caption, Sprite, Grid, Entity, Texture, Animation -import sys -import math - -# Configuration -SCENE_WIDTH = 1280 -SCENE_HEIGHT = 720 -DEMO_DURATION = 5.0 # Duration for each demo section - -# All available easing functions -EASING_FUNCTIONS = [ - "linear", "easeIn", "easeOut", "easeInOut", - "easeInQuad", "easeOutQuad", "easeInOutQuad", - "easeInCubic", "easeOutCubic", "easeInOutCubic", - "easeInQuart", "easeOutQuart", "easeInOutQuart", - "easeInSine", "easeOutSine", "easeInOutSine", - "easeInExpo", "easeOutExpo", "easeInOutExpo", - "easeInCirc", "easeOutCirc", "easeInOutCirc", - "easeInElastic", "easeOutElastic", "easeInOutElastic", - "easeInBack", "easeOutBack", "easeInOutBack", - "easeInBounce", "easeOutBounce", "easeInOutBounce" -] - -# Track current demo state -current_demo = 0 -demo_start_time = 0 -demos = [] - -# Handle ESC key to exit -def handle_keypress(scene_name, keycode): - if keycode == 256: # ESC key - print("Exiting animation sizzle reel...") - sys.exit(0) - -def create_demo_scene(): - """Create the main demo scene with title""" - mcrfpy.createScene("sizzle_reel") - mcrfpy.setScene("sizzle_reel") - mcrfpy.keypressScene(handle_keypress) - - ui = mcrfpy.sceneUI("sizzle_reel") - - # Title caption - title = Caption("McRogueFace Animation Sizzle Reel", - SCENE_WIDTH/2 - 200, 20) - title.fill_color = Color(255, 255, 0) - title.outline = 2 - title.outline_color = Color(0, 0, 0) - ui.append(title) - - # Subtitle showing current demo - global subtitle - subtitle = Caption("Initializing...", - SCENE_WIDTH/2 - 150, 60) - subtitle.fill_color = Color(200, 200, 200) - ui.append(subtitle) - - return ui - -def demo_frame_basic_animations(ui): - """Demo 1: Basic frame animations - position, size, colors""" - subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)" - - # Create test frame - frame = Frame(100, 150, 200, 100) - frame.fill_color = Color(50, 50, 150) - frame.outline = 3 - frame.outline_color = Color(255, 255, 255) - ui.append(frame) - - # Position animations with different easings - x_anim = Animation("x", 800.0, 2.0, "easeInOutBack") - y_anim = Animation("y", 400.0, 2.0, "easeInOutElastic") - x_anim.start(frame) - y_anim.start(frame) - - # Size animations - w_anim = Animation("w", 400.0, 3.0, "easeInOutCubic") - h_anim = Animation("h", 200.0, 3.0, "easeInOutCubic") - w_anim.start(frame) - h_anim.start(frame) - - # Color animations - use tuples instead of Color objects - fill_anim = Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine") - outline_anim = Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce") - fill_anim.start(frame) - outline_anim.start(frame) - - # Outline thickness animation - thickness_anim = Animation("outline", 10.0, 4.5, "easeInOutQuad") - thickness_anim.start(frame) - - return frame - -def demo_frame_opacity_zindex(ui): - """Demo 2: Frame opacity and z-index animations""" - subtitle.text = "Demo 2: Frame Opacity & Z-Index Animations" - - frames = [] - colors = [ - Color(255, 0, 0, 200), - Color(0, 255, 0, 200), - Color(0, 0, 255, 200), - Color(255, 255, 0, 200) - ] - - # Create overlapping frames - for i in range(4): - frame = Frame(200 + i*80, 200 + i*40, 200, 150) - frame.fill_color = colors[i] - frame.outline = 2 - frame.z_index = i - ui.append(frame) - frames.append(frame) - - # Animate opacity in waves - opacity_anim = Animation("opacity", 0.3, 2.0, "easeInOutSine") - opacity_anim.start(frame) - - # Reverse opacity animation - opacity_back = Animation("opacity", 1.0, 2.0, "easeInOutSine", delta=False) - mcrfpy.setTimer(f"opacity_back_{i}", lambda t, f=frame, a=opacity_back: a.start(f), 2000) - - # Z-index shuffle animation - z_anim = Animation("z_index", (i + 2) % 4, 3.0, "linear") - z_anim.start(frame) - - return frames - -def demo_caption_animations(ui): - """Demo 3: Caption text animations and effects""" - subtitle.text = "Demo 3: Caption Animations (Text, Color, Position)" - - # Basic caption with position animation - caption1 = Caption("Moving Text!", 100, 200) - caption1.fill_color = Color(255, 255, 255) - caption1.outline = 1 - ui.append(caption1) - - # Animate across screen with bounce - x_anim = Animation("x", 900.0, 3.0, "easeOutBounce") - x_anim.start(caption1) - - # Color cycling caption - caption2 = Caption("Rainbow Colors", 400, 300) - caption2.outline = 2 - ui.append(caption2) - - # Cycle through colors - use tuples - color_anim1 = Animation("fill_color", (255, 0, 0, 255), 1.0, "linear") - color_anim2 = Animation("fill_color", (0, 255, 0, 255), 1.0, "linear") - color_anim3 = Animation("fill_color", (0, 0, 255, 255), 1.0, "linear") - color_anim4 = Animation("fill_color", (255, 255, 255, 255), 1.0, "linear") - - color_anim1.start(caption2) - mcrfpy.setTimer("color2", lambda t: color_anim2.start(caption2), 1000) - mcrfpy.setTimer("color3", lambda t: color_anim3.start(caption2), 2000) - mcrfpy.setTimer("color4", lambda t: color_anim4.start(caption2), 3000) - - # Typewriter effect caption - caption3 = Caption("", 100, 400) - caption3.fill_color = Color(0, 255, 255) - ui.append(caption3) - - typewriter = Animation("text", "This text appears one character at a time...", 3.0, "linear") - typewriter.start(caption3) - - # Size animation caption - caption4 = Caption("Growing Text", 400, 500) - caption4.fill_color = Color(255, 200, 0) - ui.append(caption4) - - # Note: size animation would require font size property support - # For now, animate position to simulate growth - scale_sim = Animation("y", 480.0, 2.0, "easeInOutElastic") - scale_sim.start(caption4) - - return [caption1, caption2, caption3, caption4] - -def demo_sprite_animations(ui): - """Demo 4: Sprite animations including sprite sequences""" - subtitle.text = "Demo 4: Sprite Animations (Position, Scale, Sprite Sequences)" - - # Load a test texture (you'll need to adjust path) - try: - texture = Texture("assets/sprites/player.png", grid_size=(32, 32)) - except: - # Fallback if texture not found - texture = None - - if texture: - # Basic sprite with position animation - sprite1 = Sprite(100, 200, texture, sprite_index=0) - sprite1.scale = 2.0 - ui.append(sprite1) - - # Circular motion using sin/cos animations - # We'll use delta mode to create circular motion - x_circle = Animation("x", 300.0, 4.0, "easeInOutSine") - y_circle = Animation("y", 300.0, 4.0, "easeInOutCubic") - x_circle.start(sprite1) - y_circle.start(sprite1) - - # Sprite sequence animation (walking cycle) - sprite2 = Sprite(500, 300, texture, sprite_index=0) - sprite2.scale = 3.0 - ui.append(sprite2) - - # Animate through sprite indices for animation - walk_cycle = Animation("sprite_index", [0, 1, 2, 3, 2, 1], 2.0, "linear") - walk_cycle.start(sprite2) - - # Scale pulsing sprite - sprite3 = Sprite(800, 400, texture, sprite_index=4) - ui.append(sprite3) - - # Note: scale animation would need to be supported - # For now use position to simulate - pulse_y = Animation("y", 380.0, 0.5, "easeInOutSine") - pulse_y.start(sprite3) - - # Z-index animation for layering - sprite3_z = Animation("z_index", 10, 2.0, "linear") - sprite3_z.start(sprite3) - - return [sprite1, sprite2, sprite3] - else: - # Create placeholder caption if no texture - no_texture = Caption("(Sprite demo requires texture file)", 400, 350) - no_texture.fill_color = Color(255, 100, 100) - ui.append(no_texture) - return [no_texture] - -def demo_grid_animations(ui): - """Demo 5: Grid animations (position, camera, zoom)""" - subtitle.text = "Demo 5: Grid Animations (Position, Camera Effects)" - - # Create a grid - try: - texture = Texture("assets/sprites/tiles.png", grid_size=(16, 16)) - except: - texture = None - - # Grid constructor: Grid(grid_x, grid_y, texture, position, size) - # Note: tile dimensions are determined by texture's grid_size - grid = Grid(20, 15, texture, (100, 150), (480, 360)) # 20x24, 15x24 - grid.fill_color = Color(20, 20, 40) - ui.append(grid) - - # Fill with some test pattern - for y in range(15): - for x in range(20): - point = grid.at(x, y) - point.tilesprite = (x + y) % 4 - point.walkable = ((x + y) % 3) != 0 - if not point.walkable: - point.color = Color(100, 50, 50, 128) - - # Animate grid position - grid_x = Animation("x", 400.0, 3.0, "easeInOutBack") - grid_x.start(grid) - - # Camera pan animation (if supported) - # center_x = Animation("center", (10.0, 7.5), 4.0, "easeInOutCubic") - # center_x.start(grid) - - # Create entities in the grid - if texture: - entity1 = Entity((5.0, 5.0), texture, 8) # position tuple, texture, sprite_index - entity1.scale = 1.5 - grid.entities.append(entity1) - - # Animate entity movement - entity_pos = Animation("position", (15.0, 10.0), 3.0, "easeInOutQuad") - entity_pos.start(entity1) - - # Create patrolling entity - entity2 = Entity((10.0, 2.0), texture, 12) # position tuple, texture, sprite_index - grid.entities.append(entity2) - - # Animate sprite changes - entity2_sprite = Animation("sprite_index", [12, 13, 14, 15, 14, 13], 2.0, "linear") - entity2_sprite.start(entity2) - - return grid - -def demo_complex_combinations(ui): - """Demo 6: Complex multi-property animations""" - subtitle.text = "Demo 6: Complex Multi-Property Animations" - - # Create a complex UI composition - main_frame = Frame(200, 200, 400, 300) - main_frame.fill_color = Color(30, 30, 60, 200) - main_frame.outline = 2 - ui.append(main_frame) - - # Child elements - title = Caption("Multi-Animation Demo", 20, 20) - title.fill_color = Color(255, 255, 255) - main_frame.children.append(title) - - # Animate everything at once - # Frame animations - frame_x = Animation("x", 600.0, 3.0, "easeInOutElastic") - frame_w = Animation("w", 300.0, 2.5, "easeOutBack") - frame_fill = Animation("fill_color", (60, 30, 90, 220), 4.0, "easeInOutSine") - frame_outline = Animation("outline", 8.0, 3.0, "easeInOutQuad") - - frame_x.start(main_frame) - frame_w.start(main_frame) - frame_fill.start(main_frame) - frame_outline.start(main_frame) - - # Title animations - title_color = Animation("fill_color", (255, 200, 0, 255), 2.0, "easeOutBounce") - title_color.start(title) - - # Add animated sub-frames - for i in range(3): - sub_frame = Frame(50 + i * 100, 100, 80, 80) - sub_frame.fill_color = Color(100 + i*50, 50, 200 - i*50, 180) - main_frame.children.append(sub_frame) - - # Rotate positions using delta animations - sub_y = Animation("y", 50.0, 2.0, "easeInOutSine", delta=True) - sub_y.start(sub_frame) - - return main_frame - -def demo_easing_showcase(ui): - """Demo 7: Showcase all 30 easing functions""" - subtitle.text = "Demo 7: All 30 Easing Functions Showcase" - - # Create small frames for each easing function - frames_per_row = 6 - frame_size = 180 - spacing = 10 - - for i, easing in enumerate(EASING_FUNCTIONS[:12]): # First 12 easings - row = i // frames_per_row - col = i % frames_per_row - - x = 50 + col * (frame_size + spacing) - y = 150 + row * (60 + spacing) - - # Create indicator frame - frame = Frame(x, y, 20, 20) - frame.fill_color = Color(100, 200, 255) - frame.outline = 1 - ui.append(frame) - - # Label - label = Caption(easing, x, y - 20) - label.fill_color = Color(200, 200, 200) - ui.append(label) - - # Animate using this easing - move_anim = Animation("x", x + frame_size - 20, 3.0, easing) - move_anim.start(frame) - - # Continue with remaining easings after a delay - def show_more_easings(runtime): - for j, easing in enumerate(EASING_FUNCTIONS[12:24]): # Next 12 - row = j // frames_per_row + 2 - col = j % frames_per_row - - x = 50 + col * (frame_size + spacing) - y = 150 + row * (60 + spacing) - - frame2 = Frame(x, y, 20, 20) - frame2.fill_color = Color(255, 150, 100) - frame2.outline = 1 - ui.append(frame2) - - label2 = Caption(easing, x, y - 20) - label2.fill_color = Color(200, 200, 200) - ui.append(label2) - - move_anim2 = Animation("x", x + frame_size - 20, 3.0, easing) - move_anim2.start(frame2) - - mcrfpy.setTimer("more_easings", show_more_easings, 1000) - - # Show final easings - def show_final_easings(runtime): - for k, easing in enumerate(EASING_FUNCTIONS[24:]): # Last 6 - row = k // frames_per_row + 4 - col = k % frames_per_row - - x = 50 + col * (frame_size + spacing) - y = 150 + row * (60 + spacing) - - frame3 = Frame(x, y, 20, 20) - frame3.fill_color = Color(150, 255, 150) - frame3.outline = 1 - ui.append(frame3) - - label3 = Caption(easing, x, y - 20) - label3.fill_color = Color(200, 200, 200) - ui.append(label3) - - move_anim3 = Animation("x", x + frame_size - 20, 3.0, easing) - move_anim3.start(frame3) - - mcrfpy.setTimer("final_easings", show_final_easings, 2000) - -def demo_delta_animations(ui): - """Demo 8: Delta mode animations (relative movements)""" - subtitle.text = "Demo 8: Delta Mode Animations (Relative Movements)" - - # Create objects that will move relative to their position - frames = [] - start_positions = [(100, 200), (300, 200), (500, 200), (700, 200)] - colors = [Color(255, 100, 100), Color(100, 255, 100), - Color(100, 100, 255), Color(255, 255, 100)] - - for i, (x, y) in enumerate(start_positions): - frame = Frame(x, y, 80, 80) - frame.fill_color = colors[i] - frame.outline = 2 - ui.append(frame) - frames.append(frame) - - # Delta animations - move relative to current position - # Each frame moves by different amounts - dx = (i + 1) * 50 - dy = math.sin(i) * 100 - - x_delta = Animation("x", dx, 2.0, "easeInOutBack", delta=True) - y_delta = Animation("y", dy, 2.0, "easeInOutElastic", delta=True) - - x_delta.start(frame) - y_delta.start(frame) - - # Create caption showing delta mode - delta_label = Caption("Delta mode: Relative animations from current position", 200, 400) - delta_label.fill_color = Color(255, 255, 255) - ui.append(delta_label) - - # Animate the label with delta mode text append - text_delta = Animation("text", " - ANIMATED!", 2.0, "linear", delta=True) - text_delta.start(delta_label) - - return frames - -def demo_color_component_animations(ui): - """Demo 9: Individual color channel animations""" - subtitle.text = "Demo 9: Color Component Animations (R, G, B, A channels)" - - # Create frames to demonstrate individual color channel animations - base_frame = Frame(300, 200, 600, 300) - base_frame.fill_color = Color(128, 128, 128, 255) - base_frame.outline = 3 - ui.append(base_frame) - - # Labels for each channel - labels = ["Red", "Green", "Blue", "Alpha"] - positions = [(50, 50), (200, 50), (350, 50), (500, 50)] - - for i, (label_text, (x, y)) in enumerate(zip(labels, positions)): - # Create label - label = Caption(label_text, x, y - 30) - label.fill_color = Color(255, 255, 255) - base_frame.children.append(label) - - # Create demo frame for this channel - demo_frame = Frame(x, y, 100, 100) - demo_frame.fill_color = Color(100, 100, 100, 200) - demo_frame.outline = 2 - base_frame.children.append(demo_frame) - - # Animate individual color channel - if i == 0: # Red - r_anim = Animation("fill_color.r", 255, 3.0, "easeInOutSine") - r_anim.start(demo_frame) - elif i == 1: # Green - g_anim = Animation("fill_color.g", 255, 3.0, "easeInOutSine") - g_anim.start(demo_frame) - elif i == 2: # Blue - b_anim = Animation("fill_color.b", 255, 3.0, "easeInOutSine") - b_anim.start(demo_frame) - else: # Alpha - a_anim = Animation("fill_color.a", 50, 3.0, "easeInOutSine") - a_anim.start(demo_frame) - - # Animate main frame outline color components in sequence - outline_r = Animation("outline_color.r", 255, 1.0, "linear") - outline_g = Animation("outline_color.g", 255, 1.0, "linear") - outline_b = Animation("outline_color.b", 0, 1.0, "linear") - - outline_r.start(base_frame) - mcrfpy.setTimer("outline_g", lambda t: outline_g.start(base_frame), 1000) - mcrfpy.setTimer("outline_b", lambda t: outline_b.start(base_frame), 2000) - - return base_frame - -def demo_performance_stress_test(ui): - """Demo 10: Performance test with many simultaneous animations""" - subtitle.text = "Demo 10: Performance Stress Test (100+ Simultaneous Animations)" - - # Create many small objects with different animations - num_objects = 100 - - for i in range(num_objects): - # Random starting position - x = 100 + (i % 20) * 50 - y = 150 + (i // 20) * 50 - - # Create small frame - size = 20 + (i % 3) * 10 - frame = Frame(x, y, size, size) - - # Random color - r = (i * 37) % 256 - g = (i * 73) % 256 - b = (i * 113) % 256 - frame.fill_color = Color(r, g, b, 200) - frame.outline = 1 - ui.append(frame) - - # Random animation properties - target_x = 100 + (i % 15) * 70 - target_y = 150 + (i // 15) * 70 - duration = 2.0 + (i % 30) * 0.1 - easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)] - - # Start multiple animations per object - x_anim = Animation("x", target_x, duration, easing) - y_anim = Animation("y", target_y, duration, easing) - opacity_anim = Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine") - - x_anim.start(frame) - y_anim.start(frame) - opacity_anim.start(frame) - - # Performance counter - perf_caption = Caption(f"Animating {num_objects * 3} properties simultaneously", 400, 600) - perf_caption.fill_color = Color(255, 255, 0) - ui.append(perf_caption) - -def next_demo(runtime): - """Cycle to the next demo""" - global current_demo, demo_start_time - - # Clear the UI except title and subtitle - ui = mcrfpy.sceneUI("sizzle_reel") - - # Keep only the first two elements (title and subtitle) - while len(ui) > 2: - # Remove from the end to avoid index issues - ui.remove(len(ui) - 1) - - # Run the next demo - if current_demo < len(demos): - demos[current_demo](ui) - current_demo += 1 - - # Schedule next demo - if current_demo < len(demos): - mcrfpy.setTimer("next_demo", next_demo, int(DEMO_DURATION * 1000)) - else: - # All demos complete - subtitle.text = "Animation Showcase Complete! Press ESC to exit." - complete = Caption("All animation types demonstrated!", 400, 350) - complete.fill_color = Color(0, 255, 0) - complete.outline = 2 - ui.append(complete) - -def run_sizzle_reel(runtime): - """Main entry point - start the demo sequence""" - global demos - - # List of all demo functions - demos = [ - demo_frame_basic_animations, - demo_frame_opacity_zindex, - demo_caption_animations, - demo_sprite_animations, - demo_grid_animations, - demo_complex_combinations, - demo_easing_showcase, - demo_delta_animations, - demo_color_component_animations, - demo_performance_stress_test - ] - - # Start the first demo - next_demo(runtime) - -# Initialize scene -ui = create_demo_scene() - - -# Start the sizzle reel after a short delay -mcrfpy.setTimer("start_sizzle", run_sizzle_reel, 500) - -print("Starting McRogueFace Animation Sizzle Reel...") -print("This will demonstrate ALL animation types on ALL objects.") -print("Press ESC at any time to exit.") diff --git a/tests/demos/animation_sizzle_reel_fixed.py b/tests/demos/animation_sizzle_reel_fixed.py deleted file mode 100644 index b9c0e2e..0000000 --- a/tests/demos/animation_sizzle_reel_fixed.py +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Animation Sizzle Reel (Fixed) -========================================= - -This script demonstrates EVERY animation type on EVERY UI object type. -Fixed version that works properly with the game loop. -""" - -import mcrfpy - -# Configuration -SCENE_WIDTH = 1280 -SCENE_HEIGHT = 720 -DEMO_DURATION = 5.0 # Duration for each demo section - -# All available easing functions -EASING_FUNCTIONS = [ - "linear", "easeIn", "easeOut", "easeInOut", - "easeInQuad", "easeOutQuad", "easeInOutQuad", - "easeInCubic", "easeOutCubic", "easeInOutCubic", - "easeInQuart", "easeOutQuart", "easeInOutQuart", - "easeInSine", "easeOutSine", "easeInOutSine", - "easeInExpo", "easeOutExpo", "easeInOutExpo", - "easeInCirc", "easeOutCirc", "easeInOutCirc", - "easeInElastic", "easeOutElastic", "easeInOutElastic", - "easeInBack", "easeOutBack", "easeInOutBack", - "easeInBounce", "easeOutBounce", "easeInOutBounce" -] - -# Track current demo state -current_demo = 0 -subtitle = None - -def create_demo_scene(): - """Create the main demo scene with title""" - mcrfpy.createScene("sizzle_reel") - mcrfpy.setScene("sizzle_reel") - - ui = mcrfpy.sceneUI("sizzle_reel") - - # Title caption - title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel", - SCENE_WIDTH/2 - 200, 20) - title.fill_color = mcrfpy.Color(255, 255, 0) - title.outline = 2 - title.outline_color = mcrfpy.Color(0, 0, 0) - ui.append(title) - - # Subtitle showing current demo - global subtitle - subtitle = mcrfpy.Caption("Initializing...", - SCENE_WIDTH/2 - 150, 60) - subtitle.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(subtitle) - - return ui - -def demo_frame_basic_animations(): - """Demo 1: Basic frame animations - position, size, colors""" - ui = mcrfpy.sceneUI("sizzle_reel") - subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)" - - # Create test frame - frame = mcrfpy.Frame(100, 150, 200, 100) - frame.fill_color = mcrfpy.Color(50, 50, 150) - frame.outline = 3 - frame.outline_color = mcrfpy.Color(255, 255, 255) - ui.append(frame) - - # Position animations with different easings - x_anim = mcrfpy.Animation("x", 800.0, 2.0, "easeInOutBack") - y_anim = mcrfpy.Animation("y", 400.0, 2.0, "easeInOutElastic") - x_anim.start(frame) - y_anim.start(frame) - - # Size animations - w_anim = mcrfpy.Animation("w", 400.0, 3.0, "easeInOutCubic") - h_anim = mcrfpy.Animation("h", 200.0, 3.0, "easeInOutCubic") - w_anim.start(frame) - h_anim.start(frame) - - # Color animations - fill_anim = mcrfpy.Animation("fill_color", mcrfpy.Color(255, 100, 50, 200), 4.0, "easeInOutSine") - outline_anim = mcrfpy.Animation("outline_color", mcrfpy.Color(0, 255, 255), 4.0, "easeOutBounce") - fill_anim.start(frame) - outline_anim.start(frame) - - # Outline thickness animation - thickness_anim = mcrfpy.Animation("outline", 10.0, 4.5, "easeInOutQuad") - thickness_anim.start(frame) - -def demo_caption_animations(): - """Demo 2: Caption text animations and effects""" - ui = mcrfpy.sceneUI("sizzle_reel") - subtitle.text = "Demo 2: Caption Animations (Text, Color, Position)" - - # Basic caption with position animation - caption1 = mcrfpy.Caption("Moving Text!", 100, 200) - caption1.fill_color = mcrfpy.Color(255, 255, 255) - caption1.outline = 1 - ui.append(caption1) - - # Animate across screen with bounce - x_anim = mcrfpy.Animation("x", 900.0, 3.0, "easeOutBounce") - x_anim.start(caption1) - - # Color cycling caption - caption2 = mcrfpy.Caption("Rainbow Colors", 400, 300) - caption2.outline = 2 - ui.append(caption2) - - # Cycle through colors - color_anim1 = mcrfpy.Animation("fill_color", mcrfpy.Color(255, 0, 0), 1.0, "linear") - color_anim1.start(caption2) - - # Typewriter effect caption - caption3 = mcrfpy.Caption("", 100, 400) - caption3.fill_color = mcrfpy.Color(0, 255, 255) - ui.append(caption3) - - typewriter = mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear") - typewriter.start(caption3) - -def demo_sprite_animations(): - """Demo 3: Sprite animations (if texture available)""" - ui = mcrfpy.sceneUI("sizzle_reel") - subtitle.text = "Demo 3: Sprite Animations" - - # Create placeholder caption since texture might not exist - no_texture = mcrfpy.Caption("(Sprite demo - textures may not be loaded)", 400, 350) - no_texture.fill_color = mcrfpy.Color(255, 100, 100) - ui.append(no_texture) - -def demo_performance_stress_test(): - """Demo 4: Performance test with many simultaneous animations""" - ui = mcrfpy.sceneUI("sizzle_reel") - subtitle.text = "Demo 4: Performance Test (50+ Simultaneous Animations)" - - # Create many small objects with different animations - num_objects = 50 - - for i in range(num_objects): - # Random starting position - x = 100 + (i % 10) * 100 - y = 150 + (i // 10) * 80 - - # Create small frame - size = 20 + (i % 3) * 10 - frame = mcrfpy.Frame(x, y, size, size) - - # Random color - r = (i * 37) % 256 - g = (i * 73) % 256 - b = (i * 113) % 256 - frame.fill_color = mcrfpy.Color(r, g, b, 200) - frame.outline = 1 - ui.append(frame) - - # Random animation properties - target_x = 100 + (i % 8) * 120 - target_y = 150 + (i // 8) * 100 - duration = 2.0 + (i % 30) * 0.1 - easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)] - - # Start multiple animations per object - x_anim = mcrfpy.Animation("x", float(target_x), duration, easing) - y_anim = mcrfpy.Animation("y", float(target_y), duration, easing) - opacity_anim = mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine") - - x_anim.start(frame) - y_anim.start(frame) - opacity_anim.start(frame) - - # Performance counter - perf_caption = mcrfpy.Caption(f"Animating {num_objects * 3} properties simultaneously", 400, 600) - perf_caption.fill_color = mcrfpy.Color(255, 255, 0) - ui.append(perf_caption) - -def clear_scene(): - """Clear the scene except title and subtitle""" - ui = mcrfpy.sceneUI("sizzle_reel") - - # Keep only the first two elements (title and subtitle) - while len(ui) > 2: - ui.remove(2) - -def run_demo_sequence(runtime): - """Run through all demos""" - global current_demo - - # Clear previous demo - clear_scene() - - # Demo list - demos = [ - demo_frame_basic_animations, - demo_caption_animations, - demo_sprite_animations, - demo_performance_stress_test - ] - - if current_demo < len(demos): - # Run current demo - demos[current_demo]() - current_demo += 1 - - # Schedule next demo - if current_demo < len(demos): - mcrfpy.setTimer("next_demo", run_demo_sequence, int(DEMO_DURATION * 1000)) - else: - # All demos complete - subtitle.text = "Animation Showcase Complete!" - complete = mcrfpy.Caption("All animation types demonstrated!", 400, 350) - complete.fill_color = mcrfpy.Color(0, 255, 0) - complete.outline = 2 - ui = mcrfpy.sceneUI("sizzle_reel") - ui.append(complete) - -# Initialize scene -print("Starting McRogueFace Animation Sizzle Reel...") -print("This will demonstrate animation types on various objects.") - -ui = create_demo_scene() - -# Start the demo sequence after a short delay -mcrfpy.setTimer("start_demos", run_demo_sequence, 500) \ No newline at end of file diff --git a/tests/demos/animation_sizzle_reel_v2.py b/tests/demos/animation_sizzle_reel_v2.py deleted file mode 100644 index 2a43236..0000000 --- a/tests/demos/animation_sizzle_reel_v2.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Animation Sizzle Reel v2 -==================================== - -Fixed version with proper API usage for animations and collections. -""" - -import mcrfpy - -# Configuration -SCENE_WIDTH = 1280 -SCENE_HEIGHT = 720 -DEMO_DURATION = 5.0 # Duration for each demo section - -# All available easing functions -EASING_FUNCTIONS = [ - "linear", "easeIn", "easeOut", "easeInOut", - "easeInQuad", "easeOutQuad", "easeInOutQuad", - "easeInCubic", "easeOutCubic", "easeInOutCubic", - "easeInQuart", "easeOutQuart", "easeInOutQuart", - "easeInSine", "easeOutSine", "easeInOutSine", - "easeInExpo", "easeOutExpo", "easeInOutExpo", - "easeInCirc", "easeOutCirc", "easeInOutCirc", - "easeInElastic", "easeOutElastic", "easeInOutElastic", - "easeInBack", "easeOutBack", "easeInOutBack", - "easeInBounce", "easeOutBounce", "easeInOutBounce" -] - -# Track current demo state -current_demo = 0 -subtitle = None -demo_objects = [] # Track objects from current demo - -def create_demo_scene(): - """Create the main demo scene with title""" - mcrfpy.createScene("sizzle_reel") - mcrfpy.setScene("sizzle_reel") - - ui = mcrfpy.sceneUI("sizzle_reel") - - # Title caption - title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel", - SCENE_WIDTH/2 - 200, 20) - title.fill_color = mcrfpy.Color(255, 255, 0) - title.outline = 2 - title.outline_color = mcrfpy.Color(0, 0, 0) - ui.append(title) - - # Subtitle showing current demo - global subtitle - subtitle = mcrfpy.Caption("Initializing...", - SCENE_WIDTH/2 - 150, 60) - subtitle.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(subtitle) - - return ui - -def demo_frame_basic_animations(): - """Demo 1: Basic frame animations - position, size, colors""" - global demo_objects - demo_objects = [] - - ui = mcrfpy.sceneUI("sizzle_reel") - subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)" - - # Create test frame - frame = mcrfpy.Frame(100, 150, 200, 100) - frame.fill_color = mcrfpy.Color(50, 50, 150) - frame.outline = 3 - frame.outline_color = mcrfpy.Color(255, 255, 255) - ui.append(frame) - demo_objects.append(frame) - - # Position animations with different easings - x_anim = mcrfpy.Animation("x", 800.0, 2.0, "easeInOutBack") - y_anim = mcrfpy.Animation("y", 400.0, 2.0, "easeInOutElastic") - x_anim.start(frame) - y_anim.start(frame) - - # Size animations - w_anim = mcrfpy.Animation("w", 400.0, 3.0, "easeInOutCubic") - h_anim = mcrfpy.Animation("h", 200.0, 3.0, "easeInOutCubic") - w_anim.start(frame) - h_anim.start(frame) - - # Color animations - use tuples instead of Color objects - fill_anim = mcrfpy.Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine") - outline_anim = mcrfpy.Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce") - fill_anim.start(frame) - outline_anim.start(frame) - - # Outline thickness animation - thickness_anim = mcrfpy.Animation("outline", 10.0, 4.5, "easeInOutQuad") - thickness_anim.start(frame) - -def demo_caption_animations(): - """Demo 2: Caption text animations and effects""" - global demo_objects - demo_objects = [] - - ui = mcrfpy.sceneUI("sizzle_reel") - subtitle.text = "Demo 2: Caption Animations (Text, Color, Position)" - - # Basic caption with position animation - caption1 = mcrfpy.Caption("Moving Text!", 100, 200) - caption1.fill_color = mcrfpy.Color(255, 255, 255) - caption1.outline = 1 - ui.append(caption1) - demo_objects.append(caption1) - - # Animate across screen with bounce - x_anim = mcrfpy.Animation("x", 900.0, 3.0, "easeOutBounce") - x_anim.start(caption1) - - # Color cycling caption - caption2 = mcrfpy.Caption("Rainbow Colors", 400, 300) - caption2.outline = 2 - ui.append(caption2) - demo_objects.append(caption2) - - # Cycle through colors using tuples - color_anim1 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear") - color_anim1.start(caption2) - - # Schedule color changes - def change_to_green(rt): - color_anim2 = mcrfpy.Animation("fill_color", (0, 255, 0, 255), 1.0, "linear") - color_anim2.start(caption2) - - def change_to_blue(rt): - color_anim3 = mcrfpy.Animation("fill_color", (0, 0, 255, 255), 1.0, "linear") - color_anim3.start(caption2) - - def change_to_white(rt): - color_anim4 = mcrfpy.Animation("fill_color", (255, 255, 255, 255), 1.0, "linear") - color_anim4.start(caption2) - - mcrfpy.setTimer("color2", change_to_green, 1000) - mcrfpy.setTimer("color3", change_to_blue, 2000) - mcrfpy.setTimer("color4", change_to_white, 3000) - - # Typewriter effect caption - caption3 = mcrfpy.Caption("", 100, 400) - caption3.fill_color = mcrfpy.Color(0, 255, 255) - ui.append(caption3) - demo_objects.append(caption3) - - typewriter = mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear") - typewriter.start(caption3) - -def demo_easing_showcase(): - """Demo 3: Showcase different easing functions""" - global demo_objects - demo_objects = [] - - ui = mcrfpy.sceneUI("sizzle_reel") - subtitle.text = "Demo 3: Easing Functions Showcase" - - # Create small frames for each easing function - frames_per_row = 6 - frame_width = 180 - spacing = 10 - - # Show first 12 easings - for i, easing in enumerate(EASING_FUNCTIONS[:12]): - row = i // frames_per_row - col = i % frames_per_row - - x = 50 + col * (frame_width + spacing) - y = 150 + row * (80 + spacing) - - # Create indicator frame - frame = mcrfpy.Frame(x, y, 20, 20) - frame.fill_color = mcrfpy.Color(100, 200, 255) - frame.outline = 1 - ui.append(frame) - demo_objects.append(frame) - - # Label - label = mcrfpy.Caption(easing[:8], x, y - 20) # Truncate long names - label.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(label) - demo_objects.append(label) - - # Animate using this easing - move_anim = mcrfpy.Animation("x", float(x + frame_width - 20), 3.0, easing) - move_anim.start(frame) - -def demo_performance_stress_test(): - """Demo 4: Performance test with many simultaneous animations""" - global demo_objects - demo_objects = [] - - ui = mcrfpy.sceneUI("sizzle_reel") - subtitle.text = "Demo 4: Performance Test (50+ Simultaneous Animations)" - - # Create many small objects with different animations - num_objects = 50 - - for i in range(num_objects): - # Starting position - x = 100 + (i % 10) * 100 - y = 150 + (i // 10) * 80 - - # Create small frame - size = 20 + (i % 3) * 10 - frame = mcrfpy.Frame(x, y, size, size) - - # Random color - r = (i * 37) % 256 - g = (i * 73) % 256 - b = (i * 113) % 256 - frame.fill_color = mcrfpy.Color(r, g, b, 200) - frame.outline = 1 - ui.append(frame) - demo_objects.append(frame) - - # Random animation properties - target_x = 100 + (i % 8) * 120 - target_y = 150 + (i // 8) * 100 - duration = 2.0 + (i % 30) * 0.1 - easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)] - - # Start multiple animations per object - x_anim = mcrfpy.Animation("x", float(target_x), duration, easing) - y_anim = mcrfpy.Animation("y", float(target_y), duration, easing) - opacity_anim = mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine") - - x_anim.start(frame) - y_anim.start(frame) - opacity_anim.start(frame) - - # Performance counter - perf_caption = mcrfpy.Caption(f"Animating {num_objects * 3} properties simultaneously", 350, 600) - perf_caption.fill_color = mcrfpy.Color(255, 255, 0) - ui.append(perf_caption) - demo_objects.append(perf_caption) - -def clear_scene(): - """Clear the scene except title and subtitle""" - global demo_objects - ui = mcrfpy.sceneUI("sizzle_reel") - - # Remove all demo objects - for obj in demo_objects: - try: - # Find index of object - for i in range(len(ui)): - if ui[i] is obj: - ui.remove(ui[i]) - break - except: - pass # Object might already be removed - - demo_objects = [] - - # Clean up any timers - for timer_name in ["color2", "color3", "color4"]: - try: - mcrfpy.delTimer(timer_name) - except: - pass - -def run_demo_sequence(runtime): - """Run through all demos""" - global current_demo - - # Clear previous demo - clear_scene() - - # Demo list - demos = [ - demo_frame_basic_animations, - demo_caption_animations, - demo_easing_showcase, - demo_performance_stress_test - ] - - if current_demo < len(demos): - # Run current demo - demos[current_demo]() - current_demo += 1 - - # Schedule next demo - if current_demo < len(demos): - mcrfpy.setTimer("next_demo", run_demo_sequence, int(DEMO_DURATION * 1000)) - else: - # Final demo completed - def show_complete(rt): - subtitle.text = "Animation Showcase Complete!" - complete = mcrfpy.Caption("All animation types demonstrated!", 400, 350) - complete.fill_color = mcrfpy.Color(0, 255, 0) - complete.outline = 2 - ui = mcrfpy.sceneUI("sizzle_reel") - ui.append(complete) - - mcrfpy.setTimer("complete", show_complete, 3000) - -# Initialize scene -print("Starting McRogueFace Animation Sizzle Reel v2...") -print("This will demonstrate animation types on various objects.") - -ui = create_demo_scene() - -# Start the demo sequence after a short delay -mcrfpy.setTimer("start_demos", run_demo_sequence, 500) \ No newline at end of file diff --git a/tests/demos/animation_sizzle_reel_working.py b/tests/demos/animation_sizzle_reel_working.py deleted file mode 100644 index bb2f7af..0000000 --- a/tests/demos/animation_sizzle_reel_working.py +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Animation Sizzle Reel - Working Version -=================================================== - -Complete demonstration of all animation capabilities. -Fixed to work properly with the API. -""" - -import mcrfpy -import sys -import math - -# Configuration -DEMO_DURATION = 7.0 # Duration for each demo - -# All available easing functions -EASING_FUNCTIONS = [ - "linear", "easeIn", "easeOut", "easeInOut", - "easeInQuad", "easeOutQuad", "easeInOutQuad", - "easeInCubic", "easeOutCubic", "easeInOutCubic", - "easeInQuart", "easeOutQuart", "easeInOutQuart", - "easeInSine", "easeOutSine", "easeInOutSine", - "easeInExpo", "easeOutExpo", "easeInOutExpo", - "easeInCirc", "easeOutCirc", "easeInOutCirc", - "easeInElastic", "easeOutElastic", "easeInOutElastic", - "easeInBack", "easeOutBack", "easeInOutBack", - "easeInBounce", "easeOutBounce", "easeInOutBounce" -] - -# Track state -current_demo = 0 -subtitle = None -demo_objects = [] - -def create_scene(): - """Create the demo scene with title""" - mcrfpy.createScene("sizzle") - mcrfpy.setScene("sizzle") - - ui = mcrfpy.sceneUI("sizzle") - - # Title - title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel", 340, 20) - title.fill_color = mcrfpy.Color(255, 255, 0) - title.outline = 2 - title.outline_color = mcrfpy.Color(0, 0, 0) - ui.append(title) - - # Subtitle - global subtitle - subtitle = mcrfpy.Caption("Initializing...", 400, 60) - subtitle.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(subtitle) - -def clear_demo(): - """Clear demo objects""" - global demo_objects - ui = mcrfpy.sceneUI("sizzle") - - # Remove items starting from the end - # Skip first 2 (title and subtitle) - while len(ui) > 2: - ui.remove(len(ui) - 1) - - demo_objects = [] - -def demo1_frame_basics(): - """Demo 1: Basic frame animations""" - clear_demo() - print("demo1") - subtitle.text = "Demo 1: Frame Animations (Position, Size, Color)" - - ui = mcrfpy.sceneUI("sizzle") - - # Create frame - frame = mcrfpy.Frame(100, 150, 200, 100) - frame.fill_color = mcrfpy.Color(50, 50, 150) - frame.outline = 3 - frame.outline_color = mcrfpy.Color(255, 255, 255) - ui.append(frame) - - # Animate properties - mcrfpy.Animation("x", 700.0, 2.5, "easeInOutBack").start(frame) - mcrfpy.Animation("y", 350.0, 2.5, "easeInOutElastic").start(frame) - mcrfpy.Animation("w", 350.0, 3.0, "easeInOutCubic").start(frame) - mcrfpy.Animation("h", 180.0, 3.0, "easeInOutCubic").start(frame) - mcrfpy.Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine").start(frame) - mcrfpy.Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce").start(frame) - mcrfpy.Animation("outline", 8.0, 4.0, "easeInOutQuad").start(frame) - -def demo2_opacity_zindex(): - """Demo 2: Opacity and z-index animations""" - clear_demo() - print("demo2") - subtitle.text = "Demo 2: Opacity & Z-Index Animations" - - ui = mcrfpy.sceneUI("sizzle") - - # Create overlapping frames - colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)] - - for i in range(4): - frame = mcrfpy.Frame(200 + i*80, 200 + i*40, 200, 150) - frame.fill_color = mcrfpy.Color(colors[i][0], colors[i][1], colors[i][2], 200) - frame.outline = 2 - frame.z_index = i - ui.append(frame) - - # Animate opacity - mcrfpy.Animation("opacity", 0.3, 2.0, "easeInOutSine").start(frame) - - # Schedule opacity return - def return_opacity(rt): - for i in range(4): - mcrfpy.Animation("opacity", 1.0, 2.0, "easeInOutSine").start(ui[i]) - mcrfpy.setTimer(f"opacity_{i}", return_opacity, 2100) - -def demo3_captions(): - """Demo 3: Caption animations""" - clear_demo() - print("demo3") - subtitle.text = "Demo 3: Caption Animations" - - ui = mcrfpy.sceneUI("sizzle") - - # Moving caption - c1 = mcrfpy.Caption("Bouncing Text!", 100, 200) - c1.fill_color = mcrfpy.Color(255, 255, 255) - c1.outline = 1 - ui.append(c1) - mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1) - - # Color cycling caption - c2 = mcrfpy.Caption("Color Cycle", 400, 300) - c2.outline = 2 - ui.append(c2) - - # Animate through colors - def cycle_colors(): - anim = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 0.5, "linear") - anim.start(c2) - - def to_green(rt): - mcrfpy.Animation("fill_color", (0, 255, 0, 255), 0.5, "linear").start(c2) - def to_blue(rt): - mcrfpy.Animation("fill_color", (0, 0, 255, 255), 0.5, "linear").start(c2) - def to_white(rt): - mcrfpy.Animation("fill_color", (255, 255, 255, 255), 0.5, "linear").start(c2) - - mcrfpy.setTimer("c_green", to_green, 600) - mcrfpy.setTimer("c_blue", to_blue, 1200) - mcrfpy.setTimer("c_white", to_white, 1800) - - cycle_colors() - - # Typewriter effect - c3 = mcrfpy.Caption("", 100, 400) - c3.fill_color = mcrfpy.Color(0, 255, 255) - ui.append(c3) - mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear").start(c3) - -def demo4_easing_showcase(): - """Demo 4: Showcase easing functions""" - clear_demo() - print("demo4") - subtitle.text = "Demo 4: 30 Easing Functions" - - ui = mcrfpy.sceneUI("sizzle") - - # Show first 15 easings - for i in range(15): - row = i // 5 - col = i % 5 - x = 80 + col * 180 - y = 150 + row * 120 - - # Create frame - f = mcrfpy.Frame(x, y, 20, 20) - f.fill_color = mcrfpy.Color(100, 150, 255) - f.outline = 1 - ui.append(f) - - # Label - label = mcrfpy.Caption(EASING_FUNCTIONS[i][:10], x, y - 20) - label.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(label) - - # Animate with this easing - mcrfpy.Animation("x", float(x + 140), 3.0, EASING_FUNCTIONS[i]).start(f) - -def demo5_performance(): - """Demo 5: Many simultaneous animations""" - clear_demo() - print("demo5") - subtitle.text = "Demo 5: 50+ Simultaneous Animations" - - ui = mcrfpy.sceneUI("sizzle") - - # Create many animated objects - for i in range(50): - print(f"{i}...",end='',flush=True) - x = 100 + (i % 10) * 90 - y = 120 + (i // 10) * 80 - - f = mcrfpy.Frame(x, y, 25, 25) - r = (i * 37) % 256 - g = (i * 73) % 256 - b = (i * 113) % 256 - f.fill_color = (r, g, b, 200) #mcrfpy.Color(r, g, b, 200) - f.outline = 1 - ui.append(f) - - # Random animations - target_x = 150 + (i % 8) * 100 - target_y = 150 + (i // 8) * 85 - duration = 2.0 + (i % 30) * 0.1 - easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)] - - mcrfpy.Animation("x", float(target_x), duration, easing).start(f) - mcrfpy.Animation("y", float(target_y), duration, easing).start(f) - mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, 2.5, "easeInOutSine").start(f) - -def demo6_delta_mode(): - """Demo 6: Delta mode animations""" - clear_demo() - print("demo6") - subtitle.text = "Demo 6: Delta Mode (Relative Movement)" - - ui = mcrfpy.sceneUI("sizzle") - - # Create frames that move relative to position - positions = [(100, 300), (300, 300), (500, 300), (700, 300)] - colors = [(255, 100, 100), (100, 255, 100), (100, 100, 255), (255, 255, 100)] - - for i, ((x, y), color) in enumerate(zip(positions, colors)): - f = mcrfpy.Frame(x, y, 60, 60) - f.fill_color = mcrfpy.Color(color[0], color[1], color[2]) - f.outline = 2 - ui.append(f) - - # Delta animations - move by amount, not to position - dx = (i + 1) * 30 - dy = math.sin(i * 0.5) * 50 - - mcrfpy.Animation("x", float(dx), 2.0, "easeInOutBack", delta=True).start(f) - mcrfpy.Animation("y", float(dy), 2.0, "easeInOutElastic", delta=True).start(f) - - # Caption explaining delta mode - info = mcrfpy.Caption("Delta mode: animations move BY amount, not TO position", 200, 450) - info.fill_color = mcrfpy.Color(255, 255, 255) - ui.append(info) - -def run_next_demo(runtime): - """Run the next demo in sequence""" - global current_demo - - demos = [ - demo1_frame_basics, - demo2_opacity_zindex, - demo3_captions, - demo4_easing_showcase, - demo5_performance, - demo6_delta_mode - ] - - if current_demo < len(demos): - # Clean up timers from previous demo - for timer in ["opacity_0", "opacity_1", "opacity_2", "opacity_3", - "c_green", "c_blue", "c_white"]: - try: - mcrfpy.delTimer(timer) - except: - pass - - # Run next demo - print(f"Run next: {current_demo}") - demos[current_demo]() - current_demo += 1 - - # Schedule next demo - if current_demo < len(demos): - #mcrfpy.setTimer("next_demo", run_next_demo, int(DEMO_DURATION * 1000)) - pass - else: - current_demo = 0 - # All done - #subtitle.text = "Animation Showcase Complete!" - #complete = mcrfpy.Caption("All animations demonstrated successfully!", 350, 350) - #complete.fill_color = mcrfpy.Color(0, 255, 0) - #complete.outline = 2 - #ui = mcrfpy.sceneUI("sizzle") - #ui.append(complete) - # - ## Exit after delay - #def exit_program(rt): - # print("\nSizzle reel completed successfully!") - # sys.exit(0) - #mcrfpy.setTimer("exit", exit_program, 3000) - -# Handle ESC key -def handle_keypress(scene_name, keycode): - if keycode == 256: # ESC - print("\nExiting...") - sys.exit(0) - -# Initialize -print("Starting McRogueFace Animation Sizzle Reel...") -print("This demonstrates all animation capabilities.") -print("Press ESC to exit at any time.") - -create_scene() -mcrfpy.keypressScene(handle_keypress) - -# Start the show -mcrfpy.setTimer("start", run_next_demo, int(DEMO_DURATION * 1000)) diff --git a/tests/demos/api_demo_final.py b/tests/demos/api_demo_final.py deleted file mode 100644 index 10a8852..0000000 --- a/tests/demos/api_demo_final.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace API Demo - Final Version -==================================== - -Complete API demonstration with proper error handling. -Tests all constructors and methods systematically. -""" - -import mcrfpy -import sys - -def print_section(title): - """Print a section header""" - print("\n" + "="*60) - print(f" {title}") - print("="*60) - -def print_test(name, success=True): - """Print test result""" - status = "✓" if success else "✗" - print(f" {status} {name}") - -def test_colors(): - """Test Color API""" - print_section("COLOR TESTS") - - try: - # Basic constructors - c1 = mcrfpy.Color(255, 0, 0) # RGB - print_test(f"Color(255,0,0) = ({c1.r},{c1.g},{c1.b},{c1.a})") - - c2 = mcrfpy.Color(100, 150, 200, 128) # RGBA - print_test(f"Color(100,150,200,128) = ({c2.r},{c2.g},{c2.b},{c2.a})") - - # Property modification - c1.r = 128 - c1.g = 128 - c1.b = 128 - c1.a = 200 - print_test(f"Modified color = ({c1.r},{c1.g},{c1.b},{c1.a})") - - except Exception as e: - print_test(f"Color test failed: {e}", False) - -def test_frames(): - """Test Frame API""" - print_section("FRAME TESTS") - - # Create scene - mcrfpy.createScene("test") - mcrfpy.setScene("test") - ui = mcrfpy.sceneUI("test") - - try: - # Constructors - f1 = mcrfpy.Frame() - print_test(f"Frame() at ({f1.x},{f1.y}) size ({f1.w},{f1.h})") - - f2 = mcrfpy.Frame(100, 50) - print_test(f"Frame(100,50) at ({f2.x},{f2.y})") - - f3 = mcrfpy.Frame(200, 100, 150, 75) - print_test(f"Frame(200,100,150,75) size ({f3.w},{f3.h})") - - # Properties - f3.fill_color = mcrfpy.Color(100, 100, 200) - f3.outline = 3 - f3.outline_color = mcrfpy.Color(255, 255, 0) - f3.opacity = 0.8 - f3.visible = True - f3.z_index = 5 - print_test(f"Frame properties set") - - # Add to scene - ui.append(f3) - print_test(f"Frame added to scene") - - # Children - child = mcrfpy.Frame(10, 10, 50, 50) - f3.children.append(child) - print_test(f"Child added, count = {len(f3.children)}") - - except Exception as e: - print_test(f"Frame test failed: {e}", False) - -def test_captions(): - """Test Caption API""" - print_section("CAPTION TESTS") - - ui = mcrfpy.sceneUI("test") - - try: - # Constructors - c1 = mcrfpy.Caption() - print_test(f"Caption() text='{c1.text}'") - - c2 = mcrfpy.Caption("Hello World") - print_test(f"Caption('Hello World') at ({c2.x},{c2.y})") - - c3 = mcrfpy.Caption("Test", 300, 200) - print_test(f"Caption with position at ({c3.x},{c3.y})") - - # Properties - c3.text = "Modified" - c3.fill_color = mcrfpy.Color(255, 255, 0) - c3.outline = 2 - c3.outline_color = mcrfpy.Color(0, 0, 0) - print_test(f"Caption text='{c3.text}'") - - ui.append(c3) - print_test("Caption added to scene") - - except Exception as e: - print_test(f"Caption test failed: {e}", False) - -def test_animations(): - """Test Animation API""" - print_section("ANIMATION TESTS") - - ui = mcrfpy.sceneUI("test") - - try: - # Create target - frame = mcrfpy.Frame(50, 50, 100, 100) - frame.fill_color = mcrfpy.Color(100, 100, 100) - ui.append(frame) - - # Basic animations - a1 = mcrfpy.Animation("x", 300.0, 2.0) - print_test("Animation created (position)") - - a2 = mcrfpy.Animation("opacity", 0.5, 1.5, "easeInOut") - print_test("Animation with easing") - - a3 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 2.0) - print_test("Color animation (tuple)") - - # Start animations - a1.start(frame) - a2.start(frame) - a3.start(frame) - print_test("Animations started") - - # Check properties - print_test(f"Duration = {a1.duration}") - print_test(f"Elapsed = {a1.elapsed}") - print_test(f"Complete = {a1.is_complete}") - - except Exception as e: - print_test(f"Animation test failed: {e}", False) - -def test_collections(): - """Test collection operations""" - print_section("COLLECTION TESTS") - - ui = mcrfpy.sceneUI("test") - - try: - # Clear scene - while len(ui) > 0: - ui.remove(ui[len(ui)-1]) - print_test(f"Scene cleared, length = {len(ui)}") - - # Add items - for i in range(5): - f = mcrfpy.Frame(i*100, 50, 80, 80) - ui.append(f) - print_test(f"Added 5 frames, length = {len(ui)}") - - # Access - first = ui[0] - print_test(f"Accessed ui[0] at ({first.x},{first.y})") - - # Iteration - count = sum(1 for _ in ui) - print_test(f"Iteration count = {count}") - - except Exception as e: - print_test(f"Collection test failed: {e}", False) - -def run_tests(): - """Run all tests""" - print("\n" + "="*60) - print(" McRogueFace API Test Suite") - print("="*60) - - test_colors() - test_frames() - test_captions() - test_animations() - test_collections() - - print("\n" + "="*60) - print(" Tests Complete") - print("="*60) - - # Exit after delay - def exit_program(runtime): - print("\nExiting...") - sys.exit(0) - - mcrfpy.setTimer("exit", exit_program, 3000) - -# Run tests -print("Starting API tests...") -run_tests() \ No newline at end of file diff --git a/tests/demos/debug_astar_demo.py b/tests/demos/debug_astar_demo.py deleted file mode 100644 index 3c26d3c..0000000 --- a/tests/demos/debug_astar_demo.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -"""Debug the astar_vs_dijkstra demo issue""" - -import mcrfpy -import sys - -# Same setup as the demo -start_pos = (5, 10) -end_pos = (25, 10) - -print("Debugging A* vs Dijkstra demo...") -print(f"Start: {start_pos}, End: {end_pos}") - -# Create scene and grid -mcrfpy.createScene("debug") -grid = mcrfpy.Grid(grid_x=30, grid_y=20) - -# Initialize all as floor -print("\nInitializing 30x20 grid...") -for y in range(20): - for x in range(30): - grid.at(x, y).walkable = True - -# Test path before obstacles -print("\nTest 1: Path with no obstacles") -path1 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) -print(f" Path: {path1[:5]}...{path1[-3:] if len(path1) > 5 else ''}") -print(f" Length: {len(path1)}") - -# Add obstacles from the demo -obstacles = [ - # Vertical wall with gaps - [(15, y) for y in range(3, 17) if y not in [8, 12]], - # Horizontal walls - [(x, 5) for x in range(10, 20)], - [(x, 15) for x in range(10, 20)], - # Maze-like structure - [(x, 10) for x in range(20, 25)], - [(25, y) for y in range(5, 15)], -] - -print("\nAdding obstacles...") -wall_count = 0 -for obstacle_group in obstacles: - for x, y in obstacle_group: - grid.at(x, y).walkable = False - wall_count += 1 - if wall_count <= 5: - print(f" Wall at ({x}, {y})") - -print(f" Total walls added: {wall_count}") - -# Check specific cells -print(f"\nChecking key positions:") -print(f" Start ({start_pos[0]}, {start_pos[1]}): walkable={grid.at(start_pos[0], start_pos[1]).walkable}") -print(f" End ({end_pos[0]}, {end_pos[1]}): walkable={grid.at(end_pos[0], end_pos[1]).walkable}") - -# Check if path is blocked -print(f"\nChecking horizontal line at y=10:") -blocked_x = [] -for x in range(30): - if not grid.at(x, 10).walkable: - blocked_x.append(x) - -print(f" Blocked x positions: {blocked_x}") - -# Test path with obstacles -print("\nTest 2: Path with obstacles") -path2 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) -print(f" Path: {path2}") -print(f" Length: {len(path2)}") - -# Check if there's any path at all -if not path2: - print("\n No path found! Checking why...") - - # Check if we can reach the vertical wall gap - print("\n Testing path to wall gap at (15, 8):") - path_to_gap = grid.compute_astar_path(start_pos[0], start_pos[1], 15, 8) - print(f" Path to gap: {path_to_gap}") - - # Check from gap to end - print("\n Testing path from gap (15, 8) to end:") - path_from_gap = grid.compute_astar_path(15, 8, end_pos[0], end_pos[1]) - print(f" Path from gap: {path_from_gap}") - -# Check walls more carefully -print("\nDetailed wall analysis:") -print(" Walls at x=25 (blocking end?):") -for y in range(5, 15): - print(f" ({25}, {y}): walkable={grid.at(25, y).walkable}") - -def timer_cb(dt): - sys.exit(0) - -ui = mcrfpy.sceneUI("debug") -ui.append(grid) -mcrfpy.setScene("debug") -mcrfpy.setTimer("exit", timer_cb, 100) \ No newline at end of file diff --git a/tests/demos/dijkstra_demo_working.py b/tests/demos/dijkstra_demo_working.py deleted file mode 100644 index 91efc51..0000000 --- a/tests/demos/dijkstra_demo_working.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -""" -Working Dijkstra Demo with Clear Visual Feedback -================================================ - -This demo shows pathfinding with high-contrast colors. -""" - -import mcrfpy -import sys - -# High contrast colors -WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown for walls -FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray for floors -PATH_COLOR = mcrfpy.Color(0, 255, 0) # Pure green for paths -START_COLOR = mcrfpy.Color(255, 0, 0) # Red for start -END_COLOR = mcrfpy.Color(0, 0, 255) # Blue for end - -print("Dijkstra Demo - High Contrast") -print("==============================") - -# Create scene -mcrfpy.createScene("dijkstra_demo") - -# Create grid with exact layout from user -grid = mcrfpy.Grid(grid_x=14, grid_y=10) -grid.fill_color = mcrfpy.Color(0, 0, 0) - -# Map layout -map_layout = [ - "..............", # Row 0 - "..W.....WWWW..", # Row 1 - "..W.W...W.EW..", # Row 2 - "..W.....W..W..", # Row 3 - "..W...E.WWWW..", # Row 4 - "E.W...........", # Row 5 - "..W...........", # Row 6 - "..W...........", # Row 7 - "..W.WWW.......", # Row 8 - "..............", # Row 9 -] - -# Create the map -entity_positions = [] -for y, row in enumerate(map_layout): - for x, char in enumerate(row): - cell = grid.at(x, y) - - if char == 'W': - cell.walkable = False - cell.color = WALL_COLOR - else: - cell.walkable = True - cell.color = FLOOR_COLOR - - if char == 'E': - entity_positions.append((x, y)) - -print(f"Map created: {grid.grid_x}x{grid.grid_y}") -print(f"Entity positions: {entity_positions}") - -# Create entities -entities = [] -for i, (x, y) in enumerate(entity_positions): - entity = mcrfpy.Entity(x, y) - entity.sprite_index = 49 + i # '1', '2', '3' - grid.entities.append(entity) - entities.append(entity) - print(f"Entity {i+1} at ({x}, {y})") - -# Highlight a path immediately -if len(entities) >= 2: - e1, e2 = entities[0], entities[1] - print(f"\nCalculating path from Entity 1 ({e1.x}, {e1.y}) to Entity 2 ({e2.x}, {e2.y})...") - - path = e1.path_to(int(e2.x), int(e2.y)) - print(f"Path found: {path}") - print(f"Path length: {len(path)} steps") - - if path: - print("\nHighlighting path in bright green...") - # Color start and end specially - grid.at(int(e1.x), int(e1.y)).color = START_COLOR - grid.at(int(e2.x), int(e2.y)).color = END_COLOR - - # Color the path - for i, (x, y) in enumerate(path): - if i > 0 and i < len(path) - 1: # Skip start and end - grid.at(x, y).color = PATH_COLOR - print(f" Colored ({x}, {y}) green") - -# Keypress handler -def handle_keypress(scene_name, keycode): - if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC - print("\nExiting...") - sys.exit(0) - elif keycode == 32: # Space - print("\nRefreshing path colors...") - # Re-color the path to ensure it's visible - if len(entities) >= 2 and path: - for x, y in path[1:-1]: - grid.at(x, y).color = PATH_COLOR - -# Set up UI -ui = mcrfpy.sceneUI("dijkstra_demo") -ui.append(grid) - -# Scale grid -grid.size = (560, 400) # 14*40, 10*40 -grid.position = (120, 100) - -# Add title -title = mcrfpy.Caption("Dijkstra Pathfinding - High Contrast", 200, 20) -title.fill_color = mcrfpy.Color(255, 255, 255) -ui.append(title) - -# Add legend -legend1 = mcrfpy.Caption("Red=Start, Blue=End, Green=Path", 120, 520) -legend1.fill_color = mcrfpy.Color(200, 200, 200) -ui.append(legend1) - -legend2 = mcrfpy.Caption("Press Q to quit, SPACE to refresh", 120, 540) -legend2.fill_color = mcrfpy.Color(150, 150, 150) -ui.append(legend2) - -# Entity info -info = mcrfpy.Caption(f"Path: Entity 1 to 2 = {len(path) if 'path' in locals() else 0} steps", 120, 60) -info.fill_color = mcrfpy.Color(255, 255, 100) -ui.append(info) - -# Set up input -mcrfpy.keypressScene(handle_keypress) -mcrfpy.setScene("dijkstra_demo") - -print("\nDemo ready! The path should be clearly visible in bright green.") -print("Red = Start, Blue = End, Green = Path") -print("Press SPACE to refresh colors if needed.") \ No newline at end of file diff --git a/tests/demos/exhaustive_api_demo_fixed.py b/tests/demos/exhaustive_api_demo_fixed.py deleted file mode 100644 index 2b7bd40..0000000 --- a/tests/demos/exhaustive_api_demo_fixed.py +++ /dev/null @@ -1,306 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Exhaustive API Demo (Fixed) -======================================= - -Fixed version that properly exits after tests complete. -""" - -import mcrfpy -import sys - -# Test configuration -VERBOSE = True # Print detailed information about each test - -def print_section(title): - """Print a section header""" - print("\n" + "="*60) - print(f" {title}") - print("="*60) - -def print_test(test_name, success=True): - """Print test result""" - status = "✓ PASS" if success else "✗ FAIL" - print(f" {status} - {test_name}") - -def test_color_api(): - """Test all Color constructors and methods""" - print_section("COLOR API TESTS") - - # Constructor variants - print("\n Constructors:") - - # Empty constructor (defaults to white) - c1 = mcrfpy.Color() - print_test(f"Color() = ({c1.r}, {c1.g}, {c1.b}, {c1.a})") - - # Single value (grayscale) - c2 = mcrfpy.Color(128) - print_test(f"Color(128) = ({c2.r}, {c2.g}, {c2.b}, {c2.a})") - - # RGB only (alpha defaults to 255) - c3 = mcrfpy.Color(255, 128, 0) - print_test(f"Color(255, 128, 0) = ({c3.r}, {c3.g}, {c3.b}, {c3.a})") - - # Full RGBA - c4 = mcrfpy.Color(100, 150, 200, 128) - print_test(f"Color(100, 150, 200, 128) = ({c4.r}, {c4.g}, {c4.b}, {c4.a})") - - # Property access - print("\n Properties:") - c = mcrfpy.Color(10, 20, 30, 40) - print_test(f"Initial: r={c.r}, g={c.g}, b={c.b}, a={c.a}") - - c.r = 200 - c.g = 150 - c.b = 100 - c.a = 255 - print_test(f"After modification: r={c.r}, g={c.g}, b={c.b}, a={c.a}") - - return True - -def test_frame_api(): - """Test all Frame constructors and methods""" - print_section("FRAME API TESTS") - - # Create a test scene - mcrfpy.createScene("api_test") - mcrfpy.setScene("api_test") - ui = mcrfpy.sceneUI("api_test") - - # Constructor variants - print("\n Constructors:") - - # Empty constructor - f1 = mcrfpy.Frame() - print_test(f"Frame() - pos=({f1.x}, {f1.y}), size=({f1.w}, {f1.h})") - ui.append(f1) - - # Position only - f2 = mcrfpy.Frame(100, 50) - print_test(f"Frame(100, 50) - pos=({f2.x}, {f2.y}), size=({f2.w}, {f2.h})") - ui.append(f2) - - # Position and size - f3 = mcrfpy.Frame(200, 100, 150, 75) - print_test(f"Frame(200, 100, 150, 75) - pos=({f3.x}, {f3.y}), size=({f3.w}, {f3.h})") - ui.append(f3) - - # Full constructor - f4 = mcrfpy.Frame(300, 200, 200, 100, - fill_color=mcrfpy.Color(100, 100, 200), - outline_color=mcrfpy.Color(255, 255, 0), - outline=3) - print_test("Frame with all parameters") - ui.append(f4) - - # Properties - print("\n Properties:") - - # Position and size - f = mcrfpy.Frame(10, 20, 30, 40) - print_test(f"Initial: x={f.x}, y={f.y}, w={f.w}, h={f.h}") - - f.x = 50 - f.y = 60 - f.w = 70 - f.h = 80 - print_test(f"Modified: x={f.x}, y={f.y}, w={f.w}, h={f.h}") - - # Colors - f.fill_color = mcrfpy.Color(255, 0, 0, 128) - f.outline_color = mcrfpy.Color(0, 255, 0) - f.outline = 5.0 - print_test(f"Colors set, outline={f.outline}") - - # Visibility and opacity - f.visible = False - f.opacity = 0.5 - print_test(f"visible={f.visible}, opacity={f.opacity}") - f.visible = True # Reset - - # Z-index - f.z_index = 10 - print_test(f"z_index={f.z_index}") - - # Children collection - child1 = mcrfpy.Frame(5, 5, 20, 20) - child2 = mcrfpy.Frame(30, 5, 20, 20) - f.children.append(child1) - f.children.append(child2) - print_test(f"children.count = {len(f.children)}") - - return True - -def test_caption_api(): - """Test all Caption constructors and methods""" - print_section("CAPTION API TESTS") - - ui = mcrfpy.sceneUI("api_test") - - # Constructor variants - print("\n Constructors:") - - # Empty constructor - c1 = mcrfpy.Caption() - print_test(f"Caption() - text='{c1.text}', pos=({c1.x}, {c1.y})") - ui.append(c1) - - # Text only - c2 = mcrfpy.Caption("Hello World") - print_test(f"Caption('Hello World') - pos=({c2.x}, {c2.y})") - ui.append(c2) - - # Text and position - c3 = mcrfpy.Caption("Positioned Text", 100, 50) - print_test(f"Caption('Positioned Text', 100, 50)") - ui.append(c3) - - # Full constructor - c5 = mcrfpy.Caption("Styled Text", 300, 150, - fill_color=mcrfpy.Color(255, 255, 0), - outline_color=mcrfpy.Color(255, 0, 0), - outline=2) - print_test("Caption with all style parameters") - ui.append(c5) - - # Properties - print("\n Properties:") - - c = mcrfpy.Caption("Test Caption", 10, 20) - - # Text - c.text = "Modified Text" - print_test(f"text = '{c.text}'") - - # Position - c.x = 50 - c.y = 60 - print_test(f"position = ({c.x}, {c.y})") - - # Colors and style - c.fill_color = mcrfpy.Color(0, 255, 255) - c.outline_color = mcrfpy.Color(255, 0, 255) - c.outline = 3.0 - print_test("Colors and outline set") - - # Size (read-only, computed from text) - print_test(f"size (computed) = ({c.w}, {c.h})") - - return True - -def test_animation_api(): - """Test Animation class API""" - print_section("ANIMATION API TESTS") - - ui = mcrfpy.sceneUI("api_test") - - print("\n Animation Constructors:") - - # Basic animation - anim1 = mcrfpy.Animation("x", 100.0, 2.0) - print_test("Animation('x', 100.0, 2.0)") - - # With easing - anim2 = mcrfpy.Animation("y", 200.0, 3.0, "easeInOut") - print_test("Animation with easing='easeInOut'") - - # Delta mode - anim3 = mcrfpy.Animation("w", 50.0, 1.5, "linear", delta=True) - print_test("Animation with delta=True") - - # Color animation (as tuple) - anim4 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 2.0) - print_test("Animation with Color tuple target") - - # Vector animation - anim5 = mcrfpy.Animation("position", (10.0, 20.0), 2.5, "easeOutBounce") - print_test("Animation with position tuple") - - # Sprite sequence - anim6 = mcrfpy.Animation("sprite_index", [0, 1, 2, 3, 2, 1], 2.0) - print_test("Animation with sprite sequence") - - # Properties - print("\n Animation Properties:") - - # Check properties - print_test(f"property = '{anim1.property}'") - print_test(f"duration = {anim1.duration}") - print_test(f"elapsed = {anim1.elapsed}") - print_test(f"is_complete = {anim1.is_complete}") - print_test(f"is_delta = {anim3.is_delta}") - - # Methods - print("\n Animation Methods:") - - # Create test frame - frame = mcrfpy.Frame(50, 50, 100, 100) - frame.fill_color = mcrfpy.Color(100, 100, 100) - ui.append(frame) - - # Start animation - anim1.start(frame) - print_test("start() called on frame") - - # Test some easing functions - print("\n Sample Easing Functions:") - easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInBounce", "easeOutElastic"] - - for easing in easings: - try: - test_anim = mcrfpy.Animation("x", 100.0, 1.0, easing) - print_test(f"Easing '{easing}' ✓") - except: - print_test(f"Easing '{easing}' failed", False) - - return True - -def run_all_tests(): - """Run all API tests""" - print("\n" + "="*60) - print(" McRogueFace Exhaustive API Test Suite (Fixed)") - print(" Testing constructors and methods...") - print("="*60) - - # Run each test category - test_functions = [ - test_color_api, - test_frame_api, - test_caption_api, - test_animation_api - ] - - passed = 0 - failed = 0 - - for test_func in test_functions: - try: - if test_func(): - passed += 1 - else: - failed += 1 - except Exception as e: - print(f"\n ERROR in {test_func.__name__}: {e}") - failed += 1 - - # Summary - print("\n" + "="*60) - print(f" TEST SUMMARY: {passed} passed, {failed} failed") - print("="*60) - - print("\n Visual elements are displayed in the 'api_test' scene.") - print(" The test is complete.") - - # Exit after a short delay to allow output to be seen - def exit_test(runtime): - print("\nExiting API test suite...") - sys.exit(0) - - mcrfpy.setTimer("exit", exit_test, 2000) - -# Run the tests immediately -print("Starting McRogueFace Exhaustive API Demo (Fixed)...") -print("This will test constructors and methods.") - -run_all_tests() \ No newline at end of file diff --git a/tests/demos/path_vision_sizzle_reel.py b/tests/demos/path_vision_sizzle_reel.py deleted file mode 100644 index b067b6c..0000000 --- a/tests/demos/path_vision_sizzle_reel.py +++ /dev/null @@ -1,391 +0,0 @@ -#!/usr/bin/env python3 -""" -Path & Vision Sizzle Reel -========================= - -A choreographed demo showing: -- Smooth entity movement along paths -- Camera following with grid center animation -- Field of view updates as entities move -- Dramatic perspective transitions with zoom effects -""" - -import mcrfpy -import sys - -# Colors -WALL_COLOR = mcrfpy.Color(40, 30, 30) -FLOOR_COLOR = mcrfpy.Color(80, 80, 100) -PATH_COLOR = mcrfpy.Color(120, 120, 180) -DARK_FLOOR = mcrfpy.Color(40, 40, 50) - -# Global state -grid = None -player = None -enemy = None -sequence_step = 0 -player_path = [] -enemy_path = [] -player_path_index = 0 -enemy_path_index = 0 - -def create_scene(): - """Create the demo environment""" - global grid, player, enemy - - mcrfpy.createScene("path_vision_demo") - - # Create larger grid for more dramatic movement - grid = mcrfpy.Grid(grid_x=40, grid_y=25) - grid.fill_color = mcrfpy.Color(20, 20, 30) - - # Map layout - interconnected rooms with corridors - map_layout = [ - "########################################", # 0 - "#......##########......################", # 1 - "#......##########......################", # 2 - "#......##########......################", # 3 - "#......#.........#.....################", # 4 - "#......#.........#.....################", # 5 - "####.###.........####.#################", # 6 - "####.....................##############", # 7 - "####.....................##############", # 8 - "####.###.........####.#################", # 9 - "#......#.........#.....################", # 10 - "#......#.........#.....################", # 11 - "#......#.........#.....################", # 12 - "#......###.....###.....################", # 13 - "#......###.....###.....################", # 14 - "#......###.....###.....#########......#", # 15 - "#......###.....###.....#########......#", # 16 - "#......###.....###.....#########......#", # 17 - "#####.############.#############......#", # 18 - "#####...........................#.....#", # 19 - "#####...........................#.....#", # 20 - "#####.############.#############......#", # 21 - "#......###########.##########.........#", # 22 - "#......###########.##########.........#", # 23 - "########################################", # 24 - ] - - # Build the map - for y, row in enumerate(map_layout): - for x, char in enumerate(row): - cell = grid.at(x, y) - if char == '#': - cell.walkable = False - cell.transparent = False - cell.color = WALL_COLOR - else: - cell.walkable = True - cell.transparent = True - cell.color = FLOOR_COLOR - - # Create player in top-left room - player = mcrfpy.Entity(3, 3, grid=grid) - player.sprite_index = 64 # @ - - # Create enemy in bottom-right area - enemy = mcrfpy.Entity(35, 20, grid=grid) - enemy.sprite_index = 69 # E - - # Initial visibility - player.update_visibility() - enemy.update_visibility() - - # Set initial perspective to player - grid.perspective = 0 - -def setup_paths(): - """Define the paths for entities""" - global player_path, enemy_path - - # Player path: Top-left room → corridor → middle room - player_waypoints = [ - (3, 3), # Start - (3, 8), # Move down - (7, 8), # Enter corridor - (16, 8), # Through corridor - (16, 12), # Enter middle room - (12, 12), # Move in room - (12, 16), # Move down - (16, 16), # Move right - (16, 19), # Exit room - (25, 19), # Move right - (30, 19), # Continue - (35, 19), # Near enemy start - ] - - # Enemy path: Bottom-right → around → approach player area - enemy_waypoints = [ - (35, 20), # Start - (30, 20), # Move left - (25, 20), # Continue - (20, 20), # Continue - (16, 20), # Corridor junction - (16, 16), # Move up (might see player) - (16, 12), # Continue up - (16, 8), # Top corridor - (10, 8), # Move left - (7, 8), # Continue - (3, 8), # Player's area - (3, 12), # Move down - ] - - # Calculate full paths using pathfinding - player_path = [] - for i in range(len(player_waypoints) - 1): - x1, y1 = player_waypoints[i] - x2, y2 = player_waypoints[i + 1] - - # Use grid's A* pathfinding - segment = grid.compute_astar_path(x1, y1, x2, y2) - if segment: - # Add segment (avoiding duplicates) - if not player_path or segment[0] != player_path[-1]: - player_path.extend(segment) - else: - player_path.extend(segment[1:]) - - enemy_path = [] - for i in range(len(enemy_waypoints) - 1): - x1, y1 = enemy_waypoints[i] - x2, y2 = enemy_waypoints[i + 1] - - segment = grid.compute_astar_path(x1, y1, x2, y2) - if segment: - if not enemy_path or segment[0] != enemy_path[-1]: - enemy_path.extend(segment) - else: - enemy_path.extend(segment[1:]) - - print(f"Player path: {len(player_path)} steps") - print(f"Enemy path: {len(enemy_path)} steps") - -def setup_ui(): - """Create UI elements""" - ui = mcrfpy.sceneUI("path_vision_demo") - ui.append(grid) - - # Position and size grid - grid.position = (50, 80) - grid.size = (700, 500) # Adjust based on zoom - - # Title - title = mcrfpy.Caption("Path & Vision Sizzle Reel", 300, 20) - title.fill_color = mcrfpy.Color(255, 255, 255) - ui.append(title) - - # Status - global status_text, perspective_text - status_text = mcrfpy.Caption("Starting demo...", 50, 50) - status_text.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(status_text) - - perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50) - perspective_text.fill_color = mcrfpy.Color(100, 255, 100) - ui.append(perspective_text) - - # Controls - controls = mcrfpy.Caption("Space: Pause/Resume | R: Restart | Q: Quit", 250, 600) - controls.fill_color = mcrfpy.Color(150, 150, 150) - ui.append(controls) - -# Animation control -paused = False -move_timer = 0 -zoom_transition = False - -def move_entity_smooth(entity, target_x, target_y, duration=0.3): - """Smoothly animate entity to position""" - # Create position animation - anim_x = mcrfpy.Animation("x", float(target_x), duration, "easeInOut") - anim_y = mcrfpy.Animation("y", float(target_y), duration, "easeInOut") - - anim_x.start(entity) - anim_y.start(entity) - -def update_camera_smooth(center_x, center_y, duration=0.3): - """Smoothly move camera center""" - # Convert grid coords to pixel coords (assuming 16x16 tiles) - pixel_x = center_x * 16 - pixel_y = center_y * 16 - - anim = mcrfpy.Animation("center", (pixel_x, pixel_y), duration, "easeOut") - anim.start(grid) - -def start_perspective_transition(): - """Begin the dramatic perspective shift""" - global zoom_transition, sequence_step - zoom_transition = True - sequence_step = 100 # Special sequence number - - status_text.text = "Perspective shift: Zooming out..." - - # Zoom out with elastic easing - zoom_out = mcrfpy.Animation("zoom", 0.5, 2.0, "easeInExpo") - zoom_out.start(grid) - - # Schedule the perspective switch - mcrfpy.setTimer("switch_perspective", switch_perspective, 2100) - -def switch_perspective(dt): - """Switch perspective at the peak of zoom""" - global sequence_step - - # Switch to enemy perspective - grid.perspective = 1 - perspective_text.text = "Perspective: Enemy" - perspective_text.fill_color = mcrfpy.Color(255, 100, 100) - - status_text.text = "Perspective shift: Following enemy..." - - # Update camera to enemy position - update_camera_smooth(enemy.x, enemy.y, 0.1) - - # Zoom back in - zoom_in = mcrfpy.Animation("zoom", 1.2, 2.0, "easeOutExpo") - zoom_in.start(grid) - - # Resume sequence - mcrfpy.setTimer("resume_enemy", resume_enemy_sequence, 2100) - - # Cancel this timer - mcrfpy.delTimer("switch_perspective") - -def resume_enemy_sequence(dt): - """Resume following enemy after perspective shift""" - global sequence_step, zoom_transition - zoom_transition = False - sequence_step = 101 # Continue with enemy movement - mcrfpy.delTimer("resume_enemy") - -def sequence_tick(dt): - """Main sequence controller""" - global sequence_step, player_path_index, enemy_path_index, move_timer - - if paused or zoom_transition: - return - - move_timer += dt - if move_timer < 400: # Move every 400ms - return - move_timer = 0 - - if sequence_step < 50: - # Phase 1: Follow player movement - if player_path_index < len(player_path): - x, y = player_path[player_path_index] - move_entity_smooth(player, x, y) - player.update_visibility() - - # Camera follows player - if grid.perspective == 0: - update_camera_smooth(player.x, player.y) - - player_path_index += 1 - status_text.text = f"Player moving... Step {player_path_index}/{len(player_path)}" - - # Start enemy movement after player has moved a bit - if player_path_index == 10: - sequence_step = 1 # Enable enemy movement - else: - # Player reached destination, start perspective transition - start_perspective_transition() - - if sequence_step >= 1 and sequence_step < 50: - # Phase 2: Enemy movement (concurrent with player) - if enemy_path_index < len(enemy_path): - x, y = enemy_path[enemy_path_index] - move_entity_smooth(enemy, x, y) - enemy.update_visibility() - - # Check if enemy is visible to player - if grid.perspective == 0: - enemy_cell_idx = int(enemy.y) * grid.grid_x + int(enemy.x) - if enemy_cell_idx < len(player.gridstate) and player.gridstate[enemy_cell_idx].visible: - status_text.text = "Enemy spotted!" - - enemy_path_index += 1 - - elif sequence_step == 101: - # Phase 3: Continue following enemy after perspective shift - if enemy_path_index < len(enemy_path): - x, y = enemy_path[enemy_path_index] - move_entity_smooth(enemy, x, y) - enemy.update_visibility() - - # Camera follows enemy - update_camera_smooth(enemy.x, enemy.y) - - enemy_path_index += 1 - status_text.text = f"Following enemy... Step {enemy_path_index}/{len(enemy_path)}" - else: - status_text.text = "Demo complete! Press R to restart" - sequence_step = 200 # Done - -def handle_keys(key, state): - """Handle keyboard input""" - global paused, sequence_step, player_path_index, enemy_path_index, move_timer - key = key.lower() - if state != "start": - return - - if key == "q": - print("Exiting sizzle reel...") - sys.exit(0) - elif key == "space": - paused = not paused - status_text.text = "PAUSED" if paused else "Running..." - elif key == "r": - # Reset everything - player.x, player.y = 3, 3 - enemy.x, enemy.y = 35, 20 - player.update_visibility() - enemy.update_visibility() - grid.perspective = 0 - perspective_text.text = "Perspective: Player" - perspective_text.fill_color = mcrfpy.Color(100, 255, 100) - sequence_step = 0 - player_path_index = 0 - enemy_path_index = 0 - move_timer = 0 - update_camera_smooth(player.x, player.y, 0.5) - - # Reset zoom - zoom_reset = mcrfpy.Animation("zoom", 1.2, 0.5, "easeOut") - zoom_reset.start(grid) - - status_text.text = "Demo restarted!" - -# Initialize everything -print("Path & Vision Sizzle Reel") -print("=========================") -print("Demonstrating:") -print("- Smooth entity movement along calculated paths") -print("- Camera following with animated grid centering") -print("- Field of view updates as entities move") -print("- Dramatic perspective transitions with zoom effects") -print() - -create_scene() -setup_paths() -setup_ui() - -# Set scene and input -mcrfpy.setScene("path_vision_demo") -mcrfpy.keypressScene(handle_keys) - -# Initial camera setup -grid.zoom = 1.2 -update_camera_smooth(player.x, player.y, 0.1) - -# Start the sequence -mcrfpy.setTimer("sequence", sequence_tick, 50) # Tick every 50ms - -print("Demo started!") -print("- Player (@) will navigate through rooms") -print("- Enemy (E) will move on a different path") -print("- Watch for the dramatic perspective shift!") -print() -print("Controls: Space=Pause, R=Restart, Q=Quit") diff --git a/tests/demos/pathfinding_showcase.py b/tests/demos/pathfinding_showcase.py deleted file mode 100644 index 31b9f37..0000000 --- a/tests/demos/pathfinding_showcase.py +++ /dev/null @@ -1,377 +0,0 @@ -#!/usr/bin/env python3 -""" -Pathfinding Showcase Demo -========================= - -Demonstrates various pathfinding scenarios with multiple entities. - -Features: -- Multiple entities pathfinding simultaneously -- Chase mode: entities pursue targets -- Flee mode: entities avoid threats -- Patrol mode: entities follow waypoints -- Visual debugging: show Dijkstra distance field -""" - -import mcrfpy -import sys -import random - -# Colors -WALL_COLOR = mcrfpy.Color(40, 40, 40) -FLOOR_COLOR = mcrfpy.Color(220, 220, 240) -PATH_COLOR = mcrfpy.Color(180, 250, 180) -THREAT_COLOR = mcrfpy.Color(255, 100, 100) -GOAL_COLOR = mcrfpy.Color(100, 255, 100) -DIJKSTRA_COLORS = [ - mcrfpy.Color(50, 50, 100), # Far - mcrfpy.Color(70, 70, 150), - mcrfpy.Color(90, 90, 200), - mcrfpy.Color(110, 110, 250), - mcrfpy.Color(150, 150, 255), - mcrfpy.Color(200, 200, 255), # Near -] - -# Entity types -PLAYER = 64 # @ -ENEMY = 69 # E -TREASURE = 36 # $ -PATROL = 80 # P - -# Global state -grid = None -player = None -enemies = [] -treasures = [] -patrol_entities = [] -mode = "CHASE" -show_dijkstra = False -animation_speed = 3.0 - -# Track waypoints separately since Entity doesn't have custom attributes -entity_waypoints = {} # entity -> [(x, y), ...] -entity_waypoint_indices = {} # entity -> current index - -def create_dungeon(): - """Create a dungeon-like map""" - global grid - - mcrfpy.createScene("pathfinding_showcase") - - # Create larger grid for showcase - grid = mcrfpy.Grid(grid_x=30, grid_y=20) - grid.fill_color = mcrfpy.Color(0, 0, 0) - - # Initialize all as floor - for y in range(20): - for x in range(30): - grid.at(x, y).walkable = True - grid.at(x, y).transparent = True - grid.at(x, y).color = FLOOR_COLOR - - # Create rooms and corridors - rooms = [ - (2, 2, 8, 6), # Top-left room - (20, 2, 8, 6), # Top-right room - (11, 8, 8, 6), # Center room - (2, 14, 8, 5), # Bottom-left room - (20, 14, 8, 5), # Bottom-right room - ] - - # Create room walls - for rx, ry, rw, rh in rooms: - # Top and bottom walls - for x in range(rx, rx + rw): - if 0 <= x < 30: - grid.at(x, ry).walkable = False - grid.at(x, ry).color = WALL_COLOR - grid.at(x, ry + rh - 1).walkable = False - grid.at(x, ry + rh - 1).color = WALL_COLOR - - # Left and right walls - for y in range(ry, ry + rh): - if 0 <= y < 20: - grid.at(rx, y).walkable = False - grid.at(rx, y).color = WALL_COLOR - grid.at(rx + rw - 1, y).walkable = False - grid.at(rx + rw - 1, y).color = WALL_COLOR - - # Create doorways - doorways = [ - (6, 2), (24, 2), # Top room doors - (6, 7), (24, 7), # Top room doors bottom - (15, 8), (15, 13), # Center room doors - (6, 14), (24, 14), # Bottom room doors - (11, 11), (18, 11), # Center room side doors - ] - - for x, y in doorways: - if 0 <= x < 30 and 0 <= y < 20: - grid.at(x, y).walkable = True - grid.at(x, y).color = FLOOR_COLOR - - # Add some corridors - # Horizontal corridors - for x in range(10, 20): - grid.at(x, 5).walkable = True - grid.at(x, 5).color = FLOOR_COLOR - grid.at(x, 16).walkable = True - grid.at(x, 16).color = FLOOR_COLOR - - # Vertical corridors - for y in range(5, 17): - grid.at(10, y).walkable = True - grid.at(10, y).color = FLOOR_COLOR - grid.at(19, y).walkable = True - grid.at(19, y).color = FLOOR_COLOR - -def spawn_entities(): - """Spawn various entity types""" - global player, enemies, treasures, patrol_entities - - # Clear existing entities - #grid.entities.clear() - enemies = [] - treasures = [] - patrol_entities = [] - - # Spawn player in center room - player = mcrfpy.Entity((15, 11), mcrfpy.default_texture, PLAYER) - grid.entities.append(player) - - # Spawn enemies in corners - enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)] - for x, y in enemy_positions: - enemy = mcrfpy.Entity((x, y), mcrfpy.default_texture, ENEMY) - grid.entities.append(enemy) - enemies.append(enemy) - - # Spawn treasures - treasure_positions = [(6, 5), (24, 5), (15, 10)] - for x, y in treasure_positions: - treasure = mcrfpy.Entity((x, y), mcrfpy.default_texture, TREASURE) - grid.entities.append(treasure) - treasures.append(treasure) - - # Spawn patrol entities - patrol = mcrfpy.Entity((10, 10), mcrfpy.default_texture, PATROL) - # Store waypoints separately since Entity doesn't support custom attributes - entity_waypoints[patrol] = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol - entity_waypoint_indices[patrol] = 0 - grid.entities.append(patrol) - patrol_entities.append(patrol) - -def visualize_dijkstra(target_x, target_y): - """Visualize Dijkstra distance field""" - if not show_dijkstra: - return - - # Compute Dijkstra from target - grid.compute_dijkstra(target_x, target_y) - - # Color tiles based on distance - max_dist = 30.0 - for y in range(20): - for x in range(30): - if grid.at(x, y).walkable: - dist = grid.get_dijkstra_distance(x, y) - if dist is not None and dist < max_dist: - # Map distance to color index - color_idx = int((dist / max_dist) * len(DIJKSTRA_COLORS)) - color_idx = min(color_idx, len(DIJKSTRA_COLORS) - 1) - grid.at(x, y).color = DIJKSTRA_COLORS[color_idx] - -def move_enemies(dt): - """Move enemies based on current mode""" - if mode == "CHASE": - # Enemies chase player - for enemy in enemies: - path = enemy.path_to(int(player.x), int(player.y)) - if path and len(path) > 1: # Don't move onto player - # Move towards player - next_x, next_y = path[1] - # Smooth movement - dx = next_x - enemy.x - dy = next_y - enemy.y - enemy.x += dx * dt * animation_speed - enemy.y += dy * dt * animation_speed - - elif mode == "FLEE": - # Enemies flee from player - for enemy in enemies: - # Compute opposite direction - dx = enemy.x - player.x - dy = enemy.y - player.y - - # Find safe spot in that direction - target_x = int(enemy.x + dx * 2) - target_y = int(enemy.y + dy * 2) - - # Clamp to grid - target_x = max(0, min(29, target_x)) - target_y = max(0, min(19, target_y)) - - path = enemy.path_to(target_x, target_y) - if path and len(path) > 0: - next_x, next_y = path[0] - # Move away from player - dx = next_x - enemy.x - dy = next_y - enemy.y - enemy.x += dx * dt * animation_speed - enemy.y += dy * dt * animation_speed - -def move_patrols(dt): - """Move patrol entities along waypoints""" - for patrol in patrol_entities: - if patrol not in entity_waypoints: - continue - - # Get current waypoint - waypoints = entity_waypoints[patrol] - waypoint_index = entity_waypoint_indices[patrol] - target_x, target_y = waypoints[waypoint_index] - - # Check if reached waypoint - dist = abs(patrol.x - target_x) + abs(patrol.y - target_y) - if dist < 0.5: - # Move to next waypoint - entity_waypoint_indices[patrol] = (waypoint_index + 1) % len(waypoints) - waypoint_index = entity_waypoint_indices[patrol] - target_x, target_y = waypoints[waypoint_index] - - # Path to waypoint - path = patrol.path_to(target_x, target_y) - if path and len(path) > 0: - next_x, next_y = path[0] - dx = next_x - patrol.x - dy = next_y - patrol.y - patrol.x += dx * dt * animation_speed * 0.5 # Slower patrol speed - patrol.y += dy * dt * animation_speed * 0.5 - -def update_entities(dt): - """Update all entity movements""" - move_enemies(dt / 1000.0) # Convert to seconds - move_patrols(dt / 1000.0) - - # Update Dijkstra visualization - if show_dijkstra and player: - visualize_dijkstra(int(player.x), int(player.y)) - -def handle_keypress(scene_name, keycode): - """Handle keyboard input""" - global mode, show_dijkstra, player - - # Mode switching - if keycode == 49: # '1' - mode = "CHASE" - mode_text.text = "Mode: CHASE - Enemies pursue player" - clear_colors() - elif keycode == 50: # '2' - mode = "FLEE" - mode_text.text = "Mode: FLEE - Enemies avoid player" - clear_colors() - elif keycode == 51: # '3' - mode = "PATROL" - mode_text.text = "Mode: PATROL - Entities follow waypoints" - clear_colors() - - # Toggle Dijkstra visualization - elif keycode == 68 or keycode == 100: # 'D' or 'd' - show_dijkstra = not show_dijkstra - debug_text.text = f"Dijkstra Debug: {'ON' if show_dijkstra else 'OFF'}" - if not show_dijkstra: - clear_colors() - - # Move player with arrow keys or WASD - elif keycode in [87, 119]: # W/w - Up - if player.y > 0: - path = player.path_to(int(player.x), int(player.y) - 1) - if path: - player.y -= 1 - elif keycode in [83, 115]: # S/s - Down - if player.y < 19: - path = player.path_to(int(player.x), int(player.y) + 1) - if path: - player.y += 1 - elif keycode in [65, 97]: # A/a - Left - if player.x > 0: - path = player.path_to(int(player.x) - 1, int(player.y)) - if path: - player.x -= 1 - elif keycode in [68, 100]: # D/d - Right - if player.x < 29: - path = player.path_to(int(player.x) + 1, int(player.y)) - if path: - player.x += 1 - - # Reset - elif keycode == 82 or keycode == 114: # 'R' or 'r' - spawn_entities() - clear_colors() - - # Quit - elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC - print("\nExiting pathfinding showcase...") - sys.exit(0) - -def clear_colors(): - """Reset floor colors""" - for y in range(20): - for x in range(30): - if grid.at(x, y).walkable: - grid.at(x, y).color = FLOOR_COLOR - -# Create the showcase -print("Pathfinding Showcase Demo") -print("=========================") -print("Controls:") -print(" WASD - Move player") -print(" 1 - Chase mode (enemies pursue)") -print(" 2 - Flee mode (enemies avoid)") -print(" 3 - Patrol mode") -print(" D - Toggle Dijkstra visualization") -print(" R - Reset entities") -print(" Q/ESC - Quit") - -# Create dungeon -create_dungeon() -spawn_entities() - -# Set up UI -ui = mcrfpy.sceneUI("pathfinding_showcase") -ui.append(grid) - -# Scale and position -grid.size = (750, 500) # 30*25, 20*25 -grid.position = (25, 60) - -# Add title -title = mcrfpy.Caption("Pathfinding Showcase", 300, 10) -title.fill_color = mcrfpy.Color(255, 255, 255) -ui.append(title) - -# Add mode text -mode_text = mcrfpy.Caption("Mode: CHASE - Enemies pursue player", 25, 580) -mode_text.fill_color = mcrfpy.Color(255, 255, 200) -ui.append(mode_text) - -# Add debug text -debug_text = mcrfpy.Caption("Dijkstra Debug: OFF", 25, 600) -debug_text.fill_color = mcrfpy.Color(200, 200, 255) -ui.append(debug_text) - -# Add legend -legend = mcrfpy.Caption("@ Player E Enemy $ Treasure P Patrol", 25, 620) -legend.fill_color = mcrfpy.Color(150, 150, 150) -ui.append(legend) - -# Set up input handling -mcrfpy.keypressScene(handle_keypress) - -# Set up animation timer -mcrfpy.setTimer("entities", update_entities, 16) # 60 FPS - -# Show scene -mcrfpy.setScene("pathfinding_showcase") - -print("\nShowcase ready! Move with WASD and watch entities react.") diff --git a/tests/demos/simple_text_input.py b/tests/demos/simple_text_input.py deleted file mode 100644 index ad11509..0000000 --- a/tests/demos/simple_text_input.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple Text Input Widget for McRogueFace -Minimal implementation focusing on core functionality -""" - -import mcrfpy -import sys - - -class TextInput: - """Simple text input widget""" - def __init__(self, x, y, width, label=""): - self.x = x - self.y = y - self.width = width - self.label = label - self.text = "" - self.cursor_pos = 0 - self.focused = False - - # Create UI elements - self.frame = mcrfpy.Frame(self.x, self.y, self.width, 24) - self.frame.fill_color = (255, 255, 255, 255) - self.frame.outline_color = (128, 128, 128, 255) - self.frame.outline = 2 - - # Label - if self.label: - self.label_caption = mcrfpy.Caption(self.label, self.x, self.y - 20) - self.label_caption.fill_color = (255, 255, 255, 255) - - # Text display - self.text_caption = mcrfpy.Caption("", self.x + 4, self.y + 4) - self.text_caption.fill_color = (0, 0, 0, 255) - - # Cursor (a simple vertical line using a frame) - self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, 16) - self.cursor.fill_color = (0, 0, 0, 255) - self.cursor.visible = False - - # Click handler - self.frame.click = self._on_click - - def _on_click(self, x, y, button): - """Handle clicks""" - if button == 1: # Left click - # Request focus - global current_focus - if current_focus and current_focus != self: - current_focus.blur() - current_focus = self - self.focus() - - def focus(self): - """Give focus to this input""" - self.focused = True - self.frame.outline_color = (0, 120, 255, 255) - self.frame.outline = 3 - self.cursor.visible = True - self._update_cursor() - - def blur(self): - """Remove focus""" - self.focused = False - self.frame.outline_color = (128, 128, 128, 255) - self.frame.outline = 2 - self.cursor.visible = False - - def handle_key(self, key): - """Process keyboard input""" - if not self.focused: - return False - - if key == "BackSpace": - if self.cursor_pos > 0: - self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:] - self.cursor_pos -= 1 - elif key == "Delete": - if self.cursor_pos < len(self.text): - self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:] - elif key == "Left": - self.cursor_pos = max(0, self.cursor_pos - 1) - elif key == "Right": - self.cursor_pos = min(len(self.text), self.cursor_pos + 1) - elif key == "Home": - self.cursor_pos = 0 - elif key == "End": - self.cursor_pos = len(self.text) - elif len(key) == 1 and key.isprintable(): - self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:] - self.cursor_pos += 1 - else: - return False - - self._update_display() - return True - - def _update_display(self): - """Update text display""" - self.text_caption.text = self.text - self._update_cursor() - - def _update_cursor(self): - """Update cursor position""" - if self.focused: - # Estimate character width (roughly 10 pixels per char) - self.cursor.x = self.x + 4 + (self.cursor_pos * 10) - - def add_to_scene(self, scene): - """Add all components to scene""" - scene.append(self.frame) - if hasattr(self, 'label_caption'): - scene.append(self.label_caption) - scene.append(self.text_caption) - scene.append(self.cursor) - - -# Global focus tracking -current_focus = None -text_inputs = [] - - -def demo_test(timer_name): - """Run automated demo after scene loads""" - print("\n=== Text Input Widget Demo ===") - - # Test typing in first field - print("Testing first input field...") - text_inputs[0].focus() - for char in "Hello": - text_inputs[0].handle_key(char) - - print(f"First field contains: '{text_inputs[0].text}'") - - # Test second field - print("\nTesting second input field...") - text_inputs[1].focus() - for char in "World": - text_inputs[1].handle_key(char) - - print(f"Second field contains: '{text_inputs[1].text}'") - - # Test text operations - print("\nTesting cursor movement and deletion...") - text_inputs[1].handle_key("Home") - text_inputs[1].handle_key("Delete") - print(f"After delete at start: '{text_inputs[1].text}'") - - text_inputs[1].handle_key("End") - text_inputs[1].handle_key("BackSpace") - print(f"After backspace at end: '{text_inputs[1].text}'") - - print("\n=== Demo Complete! ===") - print("Text input widget is working successfully!") - print("Features demonstrated:") - print(" - Text entry") - print(" - Focus management (blue outline)") - print(" - Cursor positioning") - print(" - Delete/Backspace operations") - - sys.exit(0) - - -def create_scene(): - """Create the demo scene""" - global text_inputs - - mcrfpy.createScene("demo") - scene = mcrfpy.sceneUI("demo") - - # Background - bg = mcrfpy.Frame(0, 0, 800, 600) - bg.fill_color = (40, 40, 40, 255) - scene.append(bg) - - # Title - title = mcrfpy.Caption("Text Input Widget Demo", 10, 10) - title.fill_color = (255, 255, 255, 255) - scene.append(title) - - # Create input fields - input1 = TextInput(50, 100, 300, "Name:") - input1.add_to_scene(scene) - text_inputs.append(input1) - - input2 = TextInput(50, 160, 300, "Email:") - input2.add_to_scene(scene) - text_inputs.append(input2) - - input3 = TextInput(50, 220, 400, "Comment:") - input3.add_to_scene(scene) - text_inputs.append(input3) - - # Status text - status = mcrfpy.Caption("Click to focus, type to enter text", 50, 280) - status.fill_color = (200, 200, 200, 255) - scene.append(status) - - # Keyboard handler - def handle_keys(scene_name, key): - global current_focus, text_inputs - - # Tab to switch fields - if key == "Tab" and current_focus: - idx = text_inputs.index(current_focus) - next_idx = (idx + 1) % len(text_inputs) - text_inputs[next_idx]._on_click(0, 0, 1) - else: - # Pass to focused input - if current_focus: - current_focus.handle_key(key) - # Update status - texts = [inp.text for inp in text_inputs] - status.text = f"Values: {texts[0]} | {texts[1]} | {texts[2]}" - - mcrfpy.keypressScene("demo", handle_keys) - mcrfpy.setScene("demo") - - # Schedule test - mcrfpy.setTimer("test", demo_test, 500) - - -if __name__ == "__main__": - print("Starting simple text input demo...") - create_scene() \ No newline at end of file diff --git a/tests/demos/sizzle_reel_final.py b/tests/demos/sizzle_reel_final.py deleted file mode 100644 index 94ac610..0000000 --- a/tests/demos/sizzle_reel_final.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Animation Sizzle Reel - Final Version -================================================= - -Complete demonstration of all animation capabilities. -This version works properly with the game loop and avoids API issues. - -WARNING: This demo causes a segmentation fault due to a bug in the -AnimationManager. When UI elements with active animations are removed -from the scene, the AnimationManager crashes when trying to update them. - -Use sizzle_reel_final_fixed.py instead, which works around this issue -by hiding objects off-screen instead of removing them. -""" - -import mcrfpy - -# Configuration -DEMO_DURATION = 6.0 # Duration for each demo - -# All available easing functions -EASING_FUNCTIONS = [ - "linear", "easeIn", "easeOut", "easeInOut", - "easeInQuad", "easeOutQuad", "easeInOutQuad", - "easeInCubic", "easeOutCubic", "easeInOutCubic", - "easeInQuart", "easeOutQuart", "easeInOutQuart", - "easeInSine", "easeOutSine", "easeInOutSine", - "easeInExpo", "easeOutExpo", "easeInOutExpo", - "easeInCirc", "easeOutCirc", "easeInOutCirc", - "easeInElastic", "easeOutElastic", "easeInOutElastic", - "easeInBack", "easeOutBack", "easeInOutBack", - "easeInBounce", "easeOutBounce", "easeInOutBounce" -] - -# Track demo state -current_demo = 0 -subtitle = None - -def create_scene(): - """Create the demo scene""" - mcrfpy.createScene("demo") - mcrfpy.setScene("demo") - - ui = mcrfpy.sceneUI("demo") - - # Title - title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20) - title.fill_color = mcrfpy.Color(255, 255, 0) - title.outline = 2 - title.font_size = 28 - ui.append(title) - - # Subtitle - global subtitle - subtitle = mcrfpy.Caption("Starting...", 450, 60) - subtitle.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(subtitle) - - return ui - -def demo1_frame_animations(): - """Frame position, size, and color animations""" - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 1: Frame Animations" - - # Create frame - f = mcrfpy.Frame(100, 150, 200, 100) - f.fill_color = mcrfpy.Color(50, 50, 150) - f.outline = 3 - f.outline_color = mcrfpy.Color(255, 255, 255) - ui.append(f) - - # Animate properties - mcrfpy.Animation("x", 600.0, 2.0, "easeInOutBack").start(f) - mcrfpy.Animation("y", 300.0, 2.0, "easeInOutElastic").start(f) - mcrfpy.Animation("w", 300.0, 2.5, "easeInOutCubic").start(f) - mcrfpy.Animation("h", 150.0, 2.5, "easeInOutCubic").start(f) - mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "easeInOutSine").start(f) - mcrfpy.Animation("outline", 8.0, 3.0, "easeInOutQuad").start(f) - -def demo2_caption_animations(): - """Caption movement and text effects""" - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 2: Caption Animations" - - # Moving caption - c1 = mcrfpy.Caption("Bouncing Text!", 100, 200) - c1.fill_color = mcrfpy.Color(255, 255, 255) - c1.font_size = 28 - ui.append(c1) - mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1) - - # Color cycling - c2 = mcrfpy.Caption("Color Cycle", 400, 300) - c2.outline = 2 - c2.font_size = 28 - ui.append(c2) - mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2) - - # Typewriter effect - c3 = mcrfpy.Caption("", 100, 400) - c3.fill_color = mcrfpy.Color(0, 255, 255) - c3.font_size = 28 - ui.append(c3) - mcrfpy.Animation("text", "Typewriter effect animation...", 3.0, "linear").start(c3) - -def demo3_easing_showcase(): - """Show all 30 easing functions""" - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 3: All 30 Easing Functions" - - # Create a small frame for each easing - for i, easing in enumerate(EASING_FUNCTIONS[:15]): # First 15 - row = i // 5 - col = i % 5 - x = 100 + col * 200 - y = 150 + row * 100 - - # Frame - f = mcrfpy.Frame(x, y, 20, 20) - f.fill_color = mcrfpy.Color(100, 150, 255) - ui.append(f) - - # Label - label = mcrfpy.Caption(easing[:10], x, y - 20) - label.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(label) - - # Animate with this easing - mcrfpy.Animation("x", float(x + 150), 3.0, easing).start(f) - -def demo4_performance(): - """Many simultaneous animations""" - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 4: 50+ Simultaneous Animations" - - for i in range(50): - x = 100 + (i % 10) * 100 - y = 150 + (i // 10) * 100 - - f = mcrfpy.Frame(x, y, 30, 30) - f.fill_color = mcrfpy.Color((i*37)%256, (i*73)%256, (i*113)%256) - ui.append(f) - - # Animate to random position - target_x = 150 + (i % 8) * 110 - target_y = 200 + (i // 8) * 90 - easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)] - - mcrfpy.Animation("x", float(target_x), 2.5, easing).start(f) - mcrfpy.Animation("y", float(target_y), 2.5, easing).start(f) - mcrfpy.Animation("opacity", 0.3 + (i%7)*0.1, 2.0, "easeInOutSine").start(f) - -def clear_demo_objects(): - """Clear scene except title and subtitle""" - ui = mcrfpy.sceneUI("demo") - # Keep removing items after the first 2 (title and subtitle) - while len(ui) > 2: - # Remove the last item - ui.remove(len(ui)-1) - -def next_demo(runtime): - """Run the next demo""" - global current_demo - - clear_demo_objects() - - demos = [ - demo1_frame_animations, - demo2_caption_animations, - demo3_easing_showcase, - demo4_performance - ] - - if current_demo < len(demos): - demos[current_demo]() - current_demo += 1 - - if current_demo < len(demos): - #mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000)) - pass - else: - subtitle.text = "Demo Complete!" - -# Initialize -print("Starting Animation Sizzle Reel...") -create_scene() -mcrfpy.setTimer("start", next_demo, int(DEMO_DURATION * 1000)) -next_demo(0) diff --git a/tests/demos/sizzle_reel_final_fixed.py b/tests/demos/sizzle_reel_final_fixed.py deleted file mode 100644 index 0ecf99a..0000000 --- a/tests/demos/sizzle_reel_final_fixed.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Animation Sizzle Reel - Fixed Version -================================================= - -This version works around the animation crash by: -1. Using shorter demo durations to ensure animations complete before clearing -2. Adding a delay before clearing to let animations finish -3. Not removing objects, just hiding them off-screen instead -""" - -import mcrfpy - -# Configuration -DEMO_DURATION = 3.5 # Slightly shorter to ensure animations complete -CLEAR_DELAY = 0.5 # Extra delay before clearing - -# All available easing functions -EASING_FUNCTIONS = [ - "linear", "easeIn", "easeOut", "easeInOut", - "easeInQuad", "easeOutQuad", "easeInOutQuad", - "easeInCubic", "easeOutCubic", "easeInOutCubic", - "easeInQuart", "easeOutQuart", "easeInOutQuart", - "easeInSine", "easeOutSine", "easeInOutSine", - "easeInExpo", "easeOutExpo", "easeInOutExpo", - "easeInCirc", "easeOutCirc", "easeInOutCirc", - "easeInElastic", "easeOutElastic", "easeInOutElastic", - "easeInBack", "easeOutBack", "easeInOutBack", - "easeInBounce", "easeOutBounce", "easeInOutBounce" -] - -# Track demo state -current_demo = 0 -subtitle = None -demo_objects = [] # Track objects to hide instead of remove - -def create_scene(): - """Create the demo scene""" - mcrfpy.createScene("demo") - mcrfpy.setScene("demo") - - ui = mcrfpy.sceneUI("demo") - - # Title - title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20) - title.fill_color = mcrfpy.Color(255, 255, 0) - title.outline = 2 - ui.append(title) - - # Subtitle - global subtitle - subtitle = mcrfpy.Caption("Starting...", 450, 60) - subtitle.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(subtitle) - - return ui - -def hide_demo_objects(): - """Hide demo objects by moving them off-screen instead of removing""" - global demo_objects - # Move all demo objects far off-screen - for obj in demo_objects: - obj.x = -1000 - obj.y = -1000 - demo_objects = [] - -def demo1_frame_animations(): - """Frame position, size, and color animations""" - global demo_objects - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 1: Frame Animations" - - # Create frame - f = mcrfpy.Frame(100, 150, 200, 100) - f.fill_color = mcrfpy.Color(50, 50, 150) - f.outline = 3 - f.outline_color = mcrfpy.Color(255, 255, 255) - ui.append(f) - demo_objects.append(f) - - # Animate properties with shorter durations - mcrfpy.Animation("x", 600.0, 2.0, "easeInOutBack").start(f) - mcrfpy.Animation("y", 300.0, 2.0, "easeInOutElastic").start(f) - mcrfpy.Animation("w", 300.0, 2.5, "easeInOutCubic").start(f) - mcrfpy.Animation("h", 150.0, 2.5, "easeInOutCubic").start(f) - mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "easeInOutSine").start(f) - mcrfpy.Animation("outline", 8.0, 3.0, "easeInOutQuad").start(f) - -def demo2_caption_animations(): - """Caption movement and text effects""" - global demo_objects - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 2: Caption Animations" - - # Moving caption - c1 = mcrfpy.Caption("Bouncing Text!", 100, 200) - c1.fill_color = mcrfpy.Color(255, 255, 255) - ui.append(c1) - demo_objects.append(c1) - mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1) - - # Color cycling - c2 = mcrfpy.Caption("Color Cycle", 400, 300) - c2.outline = 2 - ui.append(c2) - demo_objects.append(c2) - mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2) - - # Static text (no typewriter effect to avoid issues) - c3 = mcrfpy.Caption("Animation Demo", 100, 400) - c3.fill_color = mcrfpy.Color(0, 255, 255) - ui.append(c3) - demo_objects.append(c3) - -def demo3_easing_showcase(): - """Show all 30 easing functions""" - global demo_objects - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 3: All 30 Easing Functions" - - # Create a small frame for each easing - for i, easing in enumerate(EASING_FUNCTIONS[:15]): # First 15 - row = i // 5 - col = i % 5 - x = 100 + col * 200 - y = 150 + row * 100 - - # Frame - f = mcrfpy.Frame(x, y, 20, 20) - f.fill_color = mcrfpy.Color(100, 150, 255) - ui.append(f) - demo_objects.append(f) - - # Label - label = mcrfpy.Caption(easing[:10], x, y - 20) - label.fill_color = mcrfpy.Color(200, 200, 200) - ui.append(label) - demo_objects.append(label) - - # Animate with this easing - mcrfpy.Animation("x", float(x + 150), 3.0, easing).start(f) - -def demo4_performance(): - """Many simultaneous animations""" - global demo_objects - ui = mcrfpy.sceneUI("demo") - subtitle.text = "Demo 4: 50+ Simultaneous Animations" - - for i in range(50): - x = 100 + (i % 10) * 80 - y = 150 + (i // 10) * 80 - - f = mcrfpy.Frame(x, y, 30, 30) - f.fill_color = mcrfpy.Color((i*37)%256, (i*73)%256, (i*113)%256) - ui.append(f) - demo_objects.append(f) - - # Animate to random position - target_x = 150 + (i % 8) * 90 - target_y = 200 + (i // 8) * 70 - easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)] - - mcrfpy.Animation("x", float(target_x), 2.5, easing).start(f) - mcrfpy.Animation("y", float(target_y), 2.5, easing).start(f) - -def next_demo(runtime): - """Run the next demo with proper cleanup""" - global current_demo - - # First hide old objects - hide_demo_objects() - - demos = [ - demo1_frame_animations, - demo2_caption_animations, - demo3_easing_showcase, - demo4_performance - ] - - if current_demo < len(demos): - demos[current_demo]() - current_demo += 1 - - if current_demo < len(demos): - mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000)) - else: - subtitle.text = "Demo Complete!" - mcrfpy.setTimer("exit", lambda t: mcrfpy.exit(), 2000) - -# Initialize -print("Starting Animation Sizzle Reel (Fixed)...") -create_scene() -mcrfpy.setTimer("start", next_demo, 500) \ No newline at end of file diff --git a/tests/demos/text_input_demo.py b/tests/demos/text_input_demo.py deleted file mode 100644 index 51538bb..0000000 --- a/tests/demos/text_input_demo.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python3 -""" -Text Input Demo with Auto-Test -Demonstrates the text input widget system with automated testing -""" - -import mcrfpy -from mcrfpy import automation -import sys -from text_input_widget import FocusManager, TextInput - - -def test_text_input(timer_name): - """Automated test that runs after scene is loaded""" - print("Testing text input widget system...") - - # Take a screenshot of the initial state - automation.screenshot("text_input_initial.png") - - # Simulate typing in the first field - print("Clicking on first field...") - automation.click(200, 130) # Click on name field - - # Type some text - for char in "John Doe": - mcrfpy.keypressScene("text_input_demo", char) - - # Tab to next field - mcrfpy.keypressScene("text_input_demo", "Tab") - - # Type email - for char in "john@example.com": - mcrfpy.keypressScene("text_input_demo", char) - - # Tab to comment field - mcrfpy.keypressScene("text_input_demo", "Tab") - - # Type comment - for char in "Testing the widget!": - mcrfpy.keypressScene("text_input_demo", char) - - # Take final screenshot - automation.screenshot("text_input_filled.png") - - print("Text input test complete!") - print("Screenshots saved: text_input_initial.png, text_input_filled.png") - - # Exit after test - sys.exit(0) - - -def create_demo(): - """Create a demo scene with multiple text input fields""" - mcrfpy.createScene("text_input_demo") - scene = mcrfpy.sceneUI("text_input_demo") - - # Create background - bg = mcrfpy.Frame(0, 0, 800, 600) - bg.fill_color = (40, 40, 40, 255) - scene.append(bg) - - # Title - title = mcrfpy.Caption(10, 10, "Text Input Widget Demo - Auto Test") - title.color = (255, 255, 255, 255) - scene.append(title) - - # Instructions - instructions = mcrfpy.Caption(10, 50, "This will automatically test the text input system") - instructions.color = (200, 200, 200, 255) - scene.append(instructions) - - # Create focus manager - focus_manager = FocusManager() - - # Create text input fields - fields = [] - - # Name field - name_input = TextInput(50, 120, 300, "Name:", 16) - name_input._focus_manager = focus_manager - focus_manager.register(name_input) - scene.append(name_input.frame) - if hasattr(name_input, 'label_text'): - scene.append(name_input.label_text) - scene.append(name_input.text_display) - scene.append(name_input.cursor) - fields.append(name_input) - - # Email field - email_input = TextInput(50, 180, 300, "Email:", 16) - email_input._focus_manager = focus_manager - focus_manager.register(email_input) - scene.append(email_input.frame) - if hasattr(email_input, 'label_text'): - scene.append(email_input.label_text) - scene.append(email_input.text_display) - scene.append(email_input.cursor) - fields.append(email_input) - - # Comment field - comment_input = TextInput(50, 240, 400, "Comment:", 16) - comment_input._focus_manager = focus_manager - focus_manager.register(comment_input) - scene.append(comment_input.frame) - if hasattr(comment_input, 'label_text'): - scene.append(comment_input.label_text) - scene.append(comment_input.text_display) - scene.append(comment_input.cursor) - fields.append(comment_input) - - # Result display - result_text = mcrfpy.Caption(50, 320, "Values will appear here as you type...") - result_text.color = (150, 255, 150, 255) - scene.append(result_text) - - def update_result(*args): - """Update the result display with current field values""" - name = fields[0].get_text() - email = fields[1].get_text() - comment = fields[2].get_text() - result_text.text = f"Name: {name} | Email: {email} | Comment: {comment}" - - # Set change handlers - for field in fields: - field.on_change = update_result - - # Keyboard handler - def handle_keys(scene_name, key): - """Global keyboard handler""" - # Let focus manager handle the key first - if not focus_manager.handle_key(key): - # Handle focus switching - if key == "Tab": - focus_manager.focus_next() - elif key == "Escape": - print("Demo terminated by user") - sys.exit(0) - - mcrfpy.keypressScene("text_input_demo", handle_keys) - - # Set the scene - mcrfpy.setScene("text_input_demo") - - # Schedule the automated test - mcrfpy.setTimer("test", test_text_input, 500) # Run test after 500ms - - -if __name__ == "__main__": - create_demo() \ No newline at end of file diff --git a/tests/demos/text_input_standalone.py b/tests/demos/text_input_standalone.py deleted file mode 100644 index 2bcf7d8..0000000 --- a/tests/demos/text_input_standalone.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env python3 -""" -Standalone Text Input Widget System for McRogueFace -Complete implementation with demo and automated test -""" - -import mcrfpy -import sys - - -class FocusManager: - """Manages focus state across multiple widgets""" - def __init__(self): - self.widgets = [] - self.focused_widget = None - self.focus_index = -1 - - def register(self, widget): - """Register a widget with the focus manager""" - self.widgets.append(widget) - if self.focused_widget is None: - self.focus(widget) - - def focus(self, widget): - """Set focus to a specific widget""" - if self.focused_widget: - self.focused_widget.on_blur() - - self.focused_widget = widget - self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1 - - if widget: - widget.on_focus() - - def focus_next(self): - """Focus the next widget in the list""" - if not self.widgets: - return - - self.focus_index = (self.focus_index + 1) % len(self.widgets) - self.focus(self.widgets[self.focus_index]) - - def handle_key(self, key): - """Route key events to focused widget. Returns True if handled.""" - if self.focused_widget: - return self.focused_widget.handle_key(key) - return False - - -class TextInput: - """A text input widget with cursor support""" - def __init__(self, x, y, width, label="", font_size=16): - self.x = x - self.y = y - self.width = width - self.label = label - self.font_size = font_size - - # Text state - self.text = "" - self.cursor_pos = 0 - - # Visual state - self.focused = False - - # Create UI elements - self._create_ui() - - def _create_ui(self): - """Create the visual components""" - # Background frame - self.frame = mcrfpy.Frame(self.x, self.y, self.width, self.font_size + 8) - self.frame.outline = 2 - self.frame.fill_color = (255, 255, 255, 255) - self.frame.outline_color = (128, 128, 128, 255) - - # Label (if provided) - if self.label: - self.label_text = mcrfpy.Caption( - self.x - 5, - self.y - self.font_size - 5, - self.label - ) - self.label_text.color = (255, 255, 255, 255) - - # Text display - self.text_display = mcrfpy.Caption( - self.x + 4, - self.y + 4, - "" - ) - self.text_display.color = (0, 0, 0, 255) - - # Cursor (using a thin frame) - self.cursor = mcrfpy.Frame( - self.x + 4, - self.y + 4, - 2, - self.font_size - ) - self.cursor.fill_color = (0, 0, 0, 255) - self.cursor.visible = False - - # Click handler - self.frame.click = self._on_click - - def _on_click(self, x, y, button): - """Handle mouse clicks on the input field""" - if button == 1: # Left click - if hasattr(self, '_focus_manager'): - self._focus_manager.focus(self) - - def on_focus(self): - """Called when this widget receives focus""" - self.focused = True - self.frame.outline_color = (0, 120, 255, 255) - self.frame.outline = 3 - self.cursor.visible = True - self._update_cursor_position() - - def on_blur(self): - """Called when this widget loses focus""" - self.focused = False - self.frame.outline_color = (128, 128, 128, 255) - self.frame.outline = 2 - self.cursor.visible = False - - def handle_key(self, key): - """Handle keyboard input. Returns True if key was handled.""" - if not self.focused: - return False - - handled = True - - # Special keys - if key == "BackSpace": - if self.cursor_pos > 0: - self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:] - self.cursor_pos -= 1 - elif key == "Delete": - if self.cursor_pos < len(self.text): - self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:] - elif key == "Left": - self.cursor_pos = max(0, self.cursor_pos - 1) - elif key == "Right": - self.cursor_pos = min(len(self.text), self.cursor_pos + 1) - elif key == "Home": - self.cursor_pos = 0 - elif key == "End": - self.cursor_pos = len(self.text) - elif key == "Tab": - handled = False # Let focus manager handle - elif len(key) == 1 and key.isprintable(): - # Regular character input - self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:] - self.cursor_pos += 1 - else: - handled = False - - # Update display - self._update_display() - - return handled - - def _update_display(self): - """Update the text display and cursor position""" - self.text_display.text = self.text - self._update_cursor_position() - - def _update_cursor_position(self): - """Update cursor visual position based on text position""" - if not self.focused: - return - - # Simple character width estimation (monospace assumption) - char_width = self.font_size * 0.6 - cursor_x = self.x + 4 + int(self.cursor_pos * char_width) - self.cursor.x = cursor_x - - def get_text(self): - """Get the current text content""" - return self.text - - def add_to_scene(self, scene): - """Add all components to a scene""" - scene.append(self.frame) - if hasattr(self, 'label_text'): - scene.append(self.label_text) - scene.append(self.text_display) - scene.append(self.cursor) - - -def run_automated_test(timer_name): - """Automated test that demonstrates the text input functionality""" - print("\n=== Running Text Input Widget Test ===") - - # Take initial screenshot - if hasattr(mcrfpy, 'automation'): - mcrfpy.automation.screenshot("text_input_test_1_initial.png") - print("Screenshot 1: Initial state saved") - - # Simulate some typing - print("Simulating keyboard input...") - - # The scene's keyboard handler will process these - test_sequence = [ - ("H", "Typing 'H'"), - ("e", "Typing 'e'"), - ("l", "Typing 'l'"), - ("l", "Typing 'l'"), - ("o", "Typing 'o'"), - ("Tab", "Switching to next field"), - ("T", "Typing 'T'"), - ("e", "Typing 'e'"), - ("s", "Typing 's'"), - ("t", "Typing 't'"), - ("Tab", "Switching to comment field"), - ("W", "Typing 'W'"), - ("o", "Typing 'o'"), - ("r", "Typing 'r'"), - ("k", "Typing 'k'"), - ("s", "Typing 's'"), - ("!", "Typing '!'"), - ] - - # Process each key - for key, desc in test_sequence: - print(f" - {desc}") - # Trigger the scene's keyboard handler - if hasattr(mcrfpy, '_scene_key_handler'): - mcrfpy._scene_key_handler("text_input_demo", key) - - # Take final screenshot - if hasattr(mcrfpy, 'automation'): - mcrfpy.automation.screenshot("text_input_test_2_filled.png") - print("Screenshot 2: Filled state saved") - - print("\n=== Text Input Test Complete! ===") - print("The text input widget system is working correctly.") - print("Features demonstrated:") - print(" - Focus management (blue outline on focused field)") - print(" - Text entry with cursor") - print(" - Tab navigation between fields") - print(" - Visual feedback") - - # Exit successfully - sys.exit(0) - - -def create_demo(): - """Create the demo scene""" - mcrfpy.createScene("text_input_demo") - scene = mcrfpy.sceneUI("text_input_demo") - - # Create background - bg = mcrfpy.Frame(0, 0, 800, 600) - bg.fill_color = (40, 40, 40, 255) - scene.append(bg) - - # Title - title = mcrfpy.Caption(10, 10, "Text Input Widget System") - title.color = (255, 255, 255, 255) - scene.append(title) - - # Instructions - info = mcrfpy.Caption(10, 50, "Click to focus | Tab to switch fields | Type to enter text") - info.color = (200, 200, 200, 255) - scene.append(info) - - # Create focus manager - focus_manager = FocusManager() - - # Create text inputs - name_input = TextInput(50, 120, 300, "Name:", 16) - name_input._focus_manager = focus_manager - focus_manager.register(name_input) - name_input.add_to_scene(scene) - - email_input = TextInput(50, 180, 300, "Email:", 16) - email_input._focus_manager = focus_manager - focus_manager.register(email_input) - email_input.add_to_scene(scene) - - comment_input = TextInput(50, 240, 400, "Comment:", 16) - comment_input._focus_manager = focus_manager - focus_manager.register(comment_input) - comment_input.add_to_scene(scene) - - # Status display - status = mcrfpy.Caption(50, 320, "Ready for input...") - status.color = (150, 255, 150, 255) - scene.append(status) - - # Store references for the keyboard handler - widgets = [name_input, email_input, comment_input] - - # Keyboard handler - def handle_keys(scene_name, key): - """Global keyboard handler""" - if not focus_manager.handle_key(key): - if key == "Tab": - focus_manager.focus_next() - - # Update status - texts = [w.get_text() for w in widgets] - status.text = f"Name: '{texts[0]}' | Email: '{texts[1]}' | Comment: '{texts[2]}'" - - # Store handler reference for test - mcrfpy._scene_key_handler = handle_keys - - mcrfpy.keypressScene("text_input_demo", handle_keys) - mcrfpy.setScene("text_input_demo") - - # Schedule automated test - mcrfpy.setTimer("test", run_automated_test, 1000) # Run after 1 second - - -if __name__ == "__main__": - print("Starting Text Input Widget Demo...") - create_demo() \ No newline at end of file diff --git a/tests/demos/text_input_widget.py b/tests/demos/text_input_widget.py deleted file mode 100644 index adbd201..0000000 --- a/tests/demos/text_input_widget.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env python3 -""" -Text Input Widget System for McRogueFace -A pure Python implementation of focusable text input fields -""" - -import mcrfpy -import sys -from dataclasses import dataclass -from typing import Optional, List, Callable - - -class FocusManager: - """Manages focus state across multiple widgets""" - def __init__(self): - self.widgets: List['TextInput'] = [] - self.focused_widget: Optional['TextInput'] = None - self.focus_index: int = -1 - - def register(self, widget: 'TextInput'): - """Register a widget with the focus manager""" - self.widgets.append(widget) - if self.focused_widget is None: - self.focus(widget) - - def focus(self, widget: 'TextInput'): - """Set focus to a specific widget""" - if self.focused_widget: - self.focused_widget.on_blur() - - self.focused_widget = widget - self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1 - - if widget: - widget.on_focus() - - def focus_next(self): - """Focus the next widget in the list""" - if not self.widgets: - return - - self.focus_index = (self.focus_index + 1) % len(self.widgets) - self.focus(self.widgets[self.focus_index]) - - def focus_prev(self): - """Focus the previous widget in the list""" - if not self.widgets: - return - - self.focus_index = (self.focus_index - 1) % len(self.widgets) - self.focus(self.widgets[self.focus_index]) - - def handle_key(self, key: str) -> bool: - """Route key events to focused widget. Returns True if handled.""" - if self.focused_widget: - return self.focused_widget.handle_key(key) - return False - - -class TextInput: - """A text input widget with cursor and selection support""" - def __init__(self, x: int, y: int, width: int = 200, label: str = "", - font_size: int = 16, on_change: Optional[Callable] = None): - self.x = x - self.y = y - self.width = width - self.label = label - self.font_size = font_size - self.on_change = on_change - - # Text state - self.text = "" - self.cursor_pos = 0 - self.selection_start = -1 - self.selection_end = -1 - - # Visual state - self.focused = False - self.cursor_visible = True - self.cursor_blink_timer = 0 - - # Create UI elements - self._create_ui() - - def _create_ui(self): - """Create the visual components""" - # Background frame - self.frame = mcrfpy.Frame(self.x, self.y, self.width, self.font_size + 8) - self.frame.outline = 2 - self.frame.fill_color = (255, 255, 255, 255) - self.frame.outline_color = (128, 128, 128, 255) - - # Label (if provided) - if self.label: - self.label_text = mcrfpy.Caption( - self.x - 5, - self.y - self.font_size - 5, - self.label - ) - self.label_text.color = (255, 255, 255, 255) - - # Text display - self.text_display = mcrfpy.Caption( - self.x + 4, - self.y + 4, - "" - ) - self.text_display.color = (0, 0, 0, 255) - - # Cursor (using a thin frame) - self.cursor = mcrfpy.Frame( - self.x + 4, - self.y + 4, - 2, - self.font_size - ) - self.cursor.fill_color = (0, 0, 0, 255) - self.cursor.visible = False - - # Click handler - self.frame.click = self._on_click - - def _on_click(self, x: int, y: int, button: int): - """Handle mouse clicks on the input field""" - if button == 1: # Left click - # Request focus through the focus manager - if hasattr(self, '_focus_manager'): - self._focus_manager.focus(self) - - def on_focus(self): - """Called when this widget receives focus""" - self.focused = True - self.frame.outline_color = (0, 120, 255, 255) - self.frame.outline = 3 - self.cursor.visible = True - self._update_cursor_position() - - def on_blur(self): - """Called when this widget loses focus""" - self.focused = False - self.frame.outline_color = (128, 128, 128, 255) - self.frame.outline = 2 - self.cursor.visible = False - - def handle_key(self, key: str) -> bool: - """Handle keyboard input. Returns True if key was handled.""" - if not self.focused: - return False - - handled = True - old_text = self.text - - # Special keys - if key == "BackSpace": - if self.cursor_pos > 0: - self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:] - self.cursor_pos -= 1 - elif key == "Delete": - if self.cursor_pos < len(self.text): - self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:] - elif key == "Left": - self.cursor_pos = max(0, self.cursor_pos - 1) - elif key == "Right": - self.cursor_pos = min(len(self.text), self.cursor_pos + 1) - elif key == "Home": - self.cursor_pos = 0 - elif key == "End": - self.cursor_pos = len(self.text) - elif key == "Return": - handled = False # Let parent handle submit - elif key == "Tab": - handled = False # Let focus manager handle - elif len(key) == 1 and key.isprintable(): - # Regular character input - self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:] - self.cursor_pos += 1 - else: - handled = False - - # Update display - if old_text != self.text: - self._update_display() - if self.on_change: - self.on_change(self.text) - else: - self._update_cursor_position() - - return handled - - def _update_display(self): - """Update the text display and cursor position""" - self.text_display.text = self.text - self._update_cursor_position() - - def _update_cursor_position(self): - """Update cursor visual position based on text position""" - if not self.focused: - return - - # Simple character width estimation (monospace assumption) - char_width = self.font_size * 0.6 - cursor_x = self.x + 4 + int(self.cursor_pos * char_width) - self.cursor.x = cursor_x - - def set_text(self, text: str): - """Set the text content""" - self.text = text - self.cursor_pos = len(text) - self._update_display() - - def get_text(self) -> str: - """Get the current text content""" - return self.text - - -# Demo application -def create_demo(): - """Create a demo scene with multiple text input fields""" - mcrfpy.createScene("text_input_demo") - scene = mcrfpy.sceneUI("text_input_demo") - - # Create background - bg = mcrfpy.Frame(0, 0, 800, 600) - bg.fill_color = (40, 40, 40, 255) - scene.append(bg) - - # Title - title = mcrfpy.Caption(10, 10, "Text Input Widget Demo") - title.color = (255, 255, 255, 255) - scene.append(title) - - # Instructions - instructions = mcrfpy.Caption(10, 50, "Click to focus, Tab to switch fields, Type to enter text") - instructions.color = (200, 200, 200, 255) - scene.append(instructions) - - # Create focus manager - focus_manager = FocusManager() - - # Create text input fields - fields = [] - - # Name field - name_input = TextInput(50, 120, 300, "Name:", 16) - name_input._focus_manager = focus_manager - focus_manager.register(name_input) - scene.append(name_input.frame) - if hasattr(name_input, 'label_text'): - scene.append(name_input.label_text) - scene.append(name_input.text_display) - scene.append(name_input.cursor) - fields.append(name_input) - - # Email field - email_input = TextInput(50, 180, 300, "Email:", 16) - email_input._focus_manager = focus_manager - focus_manager.register(email_input) - scene.append(email_input.frame) - if hasattr(email_input, 'label_text'): - scene.append(email_input.label_text) - scene.append(email_input.text_display) - scene.append(email_input.cursor) - fields.append(email_input) - - # Comment field - comment_input = TextInput(50, 240, 400, "Comment:", 16) - comment_input._focus_manager = focus_manager - focus_manager.register(comment_input) - scene.append(comment_input.frame) - if hasattr(comment_input, 'label_text'): - scene.append(comment_input.label_text) - scene.append(comment_input.text_display) - scene.append(comment_input.cursor) - fields.append(comment_input) - - # Result display - result_text = mcrfpy.Caption(50, 320, "Type in the fields above...") - result_text.color = (150, 255, 150, 255) - scene.append(result_text) - - def update_result(*args): - """Update the result display with current field values""" - name = fields[0].get_text() - email = fields[1].get_text() - comment = fields[2].get_text() - result_text.text = f"Name: {name} | Email: {email} | Comment: {comment}" - - # Set change handlers - for field in fields: - field.on_change = update_result - - # Keyboard handler - def handle_keys(scene_name, key): - """Global keyboard handler""" - # Let focus manager handle the key first - if not focus_manager.handle_key(key): - # Handle focus switching - if key == "Tab": - focus_manager.focus_next() - elif key == "Escape": - print("Demo complete!") - sys.exit(0) - - mcrfpy.keypressScene("text_input_demo", handle_keys) - - # Set the scene - mcrfpy.setScene("text_input_demo") - - # Add a timer for cursor blinking (optional enhancement) - def blink_cursor(timer_name): - """Blink the cursor for the focused widget""" - if focus_manager.focused_widget and focus_manager.focused_widget.focused: - cursor = focus_manager.focused_widget.cursor - cursor.visible = not cursor.visible - - mcrfpy.setTimer("cursor_blink", blink_cursor, 500) # Blink every 500ms - - -if __name__ == "__main__": - create_demo() \ No newline at end of file diff --git a/tests/automation/automation_click_issue78_analysis.py b/tests/notes/automation_click_issue78_analysis.py similarity index 100% rename from tests/automation/automation_click_issue78_analysis.py rename to tests/notes/automation_click_issue78_analysis.py diff --git a/tests/bugs/issue_37_simple_test.py b/tests/regression/issue_37_simple_test.py similarity index 100% rename from tests/bugs/issue_37_simple_test.py rename to tests/regression/issue_37_simple_test.py diff --git a/tests/bugs/issue_37_test.py b/tests/regression/issue_37_test.py similarity index 100% rename from tests/bugs/issue_37_test.py rename to tests/regression/issue_37_test.py diff --git a/tests/bugs/issue_76_test.py b/tests/regression/issue_76_test.py similarity index 100% rename from tests/bugs/issue_76_test.py rename to tests/regression/issue_76_test.py diff --git a/tests/bugs/issue_79_color_properties_test.py b/tests/regression/issue_79_color_properties_test.py similarity index 100% rename from tests/bugs/issue_79_color_properties_test.py rename to tests/regression/issue_79_color_properties_test.py diff --git a/tests/bugs/issue_99_texture_font_properties_test.py b/tests/regression/issue_99_texture_font_properties_test.py similarity index 100% rename from tests/bugs/issue_99_texture_font_properties_test.py rename to tests/regression/issue_99_texture_font_properties_test.py diff --git a/tests/bugs/issue_9_minimal_test.py b/tests/regression/issue_9_minimal_test.py similarity index 100% rename from tests/bugs/issue_9_minimal_test.py rename to tests/regression/issue_9_minimal_test.py diff --git a/tests/bugs/issue_9_rendertexture_resize_test.py b/tests/regression/issue_9_rendertexture_resize_test.py similarity index 100% rename from tests/bugs/issue_9_rendertexture_resize_test.py rename to tests/regression/issue_9_rendertexture_resize_test.py diff --git a/tests/bugs/issue_9_test.py b/tests/regression/issue_9_test.py similarity index 100% rename from tests/bugs/issue_9_test.py rename to tests/regression/issue_9_test.py diff --git a/tests/regression/test_type_preservation_solution.py b/tests/regression/test_type_preservation_solution.py new file mode 100644 index 0000000..b2d024e --- /dev/null +++ b/tests/regression/test_type_preservation_solution.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Proof of concept test to demonstrate the solution for preserving Python derived types +in collections. This test outlines the approach that needs to be implemented in C++. + +The solution involves: +1. Adding a PyObject* self member to UIDrawable (like UIEntity has) +2. Storing the Python object reference when objects are created from Python +3. Using the stored reference when retrieving from collections +""" + +import mcrfpy +import sys + +def demonstrate_solution(): + """Demonstrate how the solution should work""" + print("=== Type Preservation Solution Demonstration ===\n") + + print("Current behavior (broken):") + print("1. Python creates derived object (e.g., MyFrame extends Frame)") + print("2. C++ stores only the shared_ptr") + print("3. When retrieved, C++ creates a NEW PyUIFrameObject with type 'Frame'") + print("4. Original type and attributes are lost\n") + + print("Proposed solution (like UIEntity):") + print("1. Add PyObject* self member to UIDrawable base class") + print("2. In Frame/Sprite/Caption/Grid init, store: self->data->self = (PyObject*)self") + print("3. In convertDrawableToPython, check if drawable->self exists") + print("4. If it exists, return the stored Python object (with INCREF)") + print("5. If not, create new base type object as fallback\n") + + print("Benefits:") + print("- Preserves derived Python types") + print("- Maintains object identity (same Python object)") + print("- Keeps all Python attributes and methods") + print("- Minimal performance impact (one pointer per object)") + print("- Backwards compatible (C++-created objects still work)\n") + + print("Implementation steps:") + print("1. Add 'PyObject* self = nullptr;' to UIDrawable class") + print("2. Update Frame/Sprite/Caption/Grid init methods to store self") + print("3. Update convertDrawableToPython in UICollection.cpp") + print("4. Handle reference counting properly (INCREF/DECREF)") + print("5. Clear self pointer in destructor to avoid circular refs\n") + + print("Example code change in UICollection.cpp:") + print(""" + static PyObject* convertDrawableToPython(std::shared_ptr drawable) { + if (!drawable) { + Py_RETURN_NONE; + } + + // Check if we have a stored Python object reference + if (drawable->self != nullptr) { + // Return the original Python object, preserving its type + Py_INCREF(drawable->self); + return drawable->self; + } + + // Otherwise, create new object as before (fallback for C++-created objects) + PyTypeObject* type = nullptr; + PyObject* obj = nullptr; + // ... existing switch statement ... + } + """) + +def run_test(runtime): + """Timer callback""" + try: + demonstrate_solution() + print("\nThis solution approach is proven to work in UIEntityCollection.") + print("It should be applied to UICollection for consistency.") + except Exception as e: + print(f"\nError: {e}") + import traceback + traceback.print_exc() + + sys.exit(0) + +# Set up scene and run +mcrfpy.createScene("test") +mcrfpy.setScene("test") +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh deleted file mode 100755 index 85e7c7f..0000000 --- a/tests/run_all_tests.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# Run all tests and check for failures - -TESTS=( - "test_click_init.py" - "test_drawable_base.py" - "test_frame_children.py" - "test_sprite_texture_swap.py" - "test_timer_object.py" - "test_timer_object_fixed.py" -) - -echo "Running all tests..." -echo "====================" - -failed=0 -passed=0 - -for test in "${TESTS[@]}"; do - echo -n "Running $test... " - if timeout 5 ./mcrogueface --headless --exec ../tests/$test > /tmp/test_output.txt 2>&1; then - if grep -q "FAIL\|✗" /tmp/test_output.txt; then - echo "FAILED" - echo "Output:" - cat /tmp/test_output.txt | grep -E "✗|FAIL|Error|error" | head -10 - ((failed++)) - else - echo "PASSED" - ((passed++)) - fi - else - echo "TIMEOUT/CRASH" - ((failed++)) - fi -done - -echo "====================" -echo "Total: $((passed + failed)) tests" -echo "Passed: $passed" -echo "Failed: $failed" - -exit $failed \ No newline at end of file diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..f51f3a9 --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +McRogueFace Test Runner +Runs all headless tests and reports results. + +Usage: + python3 tests/run_tests.py # Run all tests + python3 tests/run_tests.py unit # Run only unit tests + python3 tests/run_tests.py -v # Verbose output +""" +import os +import subprocess +import sys +import time +import hashlib +from pathlib import Path + +# Configuration +TESTS_DIR = Path(__file__).parent +BUILD_DIR = TESTS_DIR.parent / "build" +MCROGUEFACE = BUILD_DIR / "mcrogueface" +TIMEOUT = 10 # seconds per test + +# Test directories to run (in order) +TEST_DIRS = ['unit', 'integration', 'regression'] + +# ANSI colors +GREEN = '\033[92m' +RED = '\033[91m' +YELLOW = '\033[93m' +RESET = '\033[0m' +BOLD = '\033[1m' + +def get_screenshot_checksum(test_dir): + """Get checksums of any PNG files in build directory.""" + checksums = {} + for png in BUILD_DIR.glob("*.png"): + with open(png, 'rb') as f: + checksums[png.name] = hashlib.md5(f.read()).hexdigest()[:8] + return checksums + +def run_test(test_path, verbose=False): + """Run a single test and return (passed, duration, output).""" + start = time.time() + + # Clean any existing screenshots + for png in BUILD_DIR.glob("test_*.png"): + png.unlink() + + try: + result = subprocess.run( + [str(MCROGUEFACE), '--headless', '--exec', str(test_path)], + capture_output=True, + text=True, + timeout=TIMEOUT, + cwd=str(BUILD_DIR) + ) + duration = time.time() - start + passed = result.returncode == 0 + output = result.stdout + result.stderr + + # Check for PASS/FAIL in output + if 'FAIL' in output and 'PASS' not in output.split('FAIL')[-1]: + passed = False + + return passed, duration, output + + except subprocess.TimeoutExpired: + return False, TIMEOUT, "TIMEOUT" + except Exception as e: + return False, 0, str(e) + +def find_tests(directory): + """Find all test files in a directory.""" + test_dir = TESTS_DIR / directory + if not test_dir.exists(): + return [] + return sorted(test_dir.glob("*.py")) + +def main(): + verbose = '-v' in sys.argv or '--verbose' in sys.argv + + # Determine which directories to test + dirs_to_test = [] + for arg in sys.argv[1:]: + if arg in TEST_DIRS: + dirs_to_test.append(arg) + if not dirs_to_test: + dirs_to_test = TEST_DIRS + + print(f"{BOLD}McRogueFace Test Runner{RESET}") + print(f"Testing: {', '.join(dirs_to_test)}") + print("=" * 60) + + results = {'pass': 0, 'fail': 0, 'total_time': 0} + failures = [] + + for test_dir in dirs_to_test: + tests = find_tests(test_dir) + if not tests: + continue + + print(f"\n{BOLD}{test_dir}/{RESET} ({len(tests)} tests)") + + for test_path in tests: + test_name = test_path.name + passed, duration, output = run_test(test_path, verbose) + results['total_time'] += duration + + if passed: + results['pass'] += 1 + status = f"{GREEN}PASS{RESET}" + else: + results['fail'] += 1 + status = f"{RED}FAIL{RESET}" + failures.append((test_dir, test_name, output)) + + # Get screenshot checksums if any were generated + checksums = get_screenshot_checksum(BUILD_DIR) + checksum_str = "" + if checksums: + checksum_str = f" [{', '.join(f'{k}:{v}' for k,v in checksums.items())}]" + + print(f" {status} {test_name} ({duration:.2f}s){checksum_str}") + + if verbose and not passed: + print(f" Output: {output[:200]}...") + + # Summary + print("\n" + "=" * 60) + total = results['pass'] + results['fail'] + pass_rate = (results['pass'] / total * 100) if total > 0 else 0 + + print(f"{BOLD}Results:{RESET} {results['pass']}/{total} passed ({pass_rate:.1f}%)") + print(f"{BOLD}Time:{RESET} {results['total_time']:.2f}s") + + if failures: + print(f"\n{RED}{BOLD}Failures:{RESET}") + for test_dir, test_name, output in failures: + print(f" {test_dir}/{test_name}") + if verbose: + # Show last few lines of output + lines = output.strip().split('\n')[-5:] + for line in lines: + print(f" {line}") + + sys.exit(0 if results['fail'] == 0 else 1) + +if __name__ == '__main__': + main() diff --git a/tests/unified_click_example.cpp b/tests/unified_click_example.cpp deleted file mode 100644 index 1c7fa1d..0000000 --- a/tests/unified_click_example.cpp +++ /dev/null @@ -1,101 +0,0 @@ -// Example of how UIFrame would implement unified click handling -// -// Click Priority Example: -// - Dialog Frame (has click handler to drag window) -// - Title Caption (no click handler) -// - Button Frame (has click handler) -// - Button Caption "OK" (no click handler) -// - Close X Sprite (has click handler) -// -// Clicking on: -// - "OK" text -> Button Frame gets the click (deepest parent with handler) -// - Close X -> Close sprite gets the click -// - Title bar -> Dialog Frame gets the click (no child has handler there) -// - Outside dialog -> nullptr (bounds check fails) - -class UIFrame : public UIDrawable, protected RectangularContainer { -private: - // Implementation of container interface - sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const override { - // Children use same coordinate system as frame's local coordinates - return localPoint; - } - - UIDrawable* getClickHandler() override { - return click_callable ? this : nullptr; - } - - std::vector getClickableChildren() override { - std::vector result; - for (auto& child : *children) { - result.push_back(child.get()); - } - return result; - } - -public: - UIDrawable* click_at(sf::Vector2f point) override { - // Update bounds from box - bounds = sf::FloatRect(box.getPosition().x, box.getPosition().y, - box.getSize().x, box.getSize().y); - - // Use unified handler - return handleClick(point); - } -}; - -// Example for UIGrid with entity coordinate transformation -class UIGrid : public UIDrawable, protected RectangularContainer { -private: - sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const override { - // For entities, we need to transform from pixel coordinates to grid coordinates - // This is where the grid's special coordinate system is handled - - // Assuming entity positions are in grid cells, not pixels - // We pass pixel coordinates relative to the grid's rendering area - return localPoint; // Entities will handle their own sprite positioning - } - - std::vector getClickableChildren() override { - std::vector result; - - // Only check entities that are visible on screen - float left_edge = center_x - (box.getSize().x / 2.0f) / (grid_size * zoom); - float top_edge = center_y - (box.getSize().y / 2.0f) / (grid_size * zoom); - float right_edge = left_edge + (box.getSize().x / (grid_size * zoom)); - float bottom_edge = top_edge + (box.getSize().y / (grid_size * zoom)); - - for (auto& entity : entities) { - // Check if entity is within visible bounds - if (entity->position.x >= left_edge - 1 && entity->position.x < right_edge + 1 && - entity->position.y >= top_edge - 1 && entity->position.y < bottom_edge + 1) { - result.push_back(&entity->sprite); - } - } - return result; - } -}; - -// For Scene, which has no coordinate transformation -class PyScene : protected UIContainerBase { -private: - sf::Vector2f toLocalCoordinates(sf::Vector2f point) const override { - // Scene uses window coordinates directly - return point; - } - - sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const override { - // Top-level drawables use window coordinates - return localPoint; - } - - bool containsPoint(sf::Vector2f localPoint) const override { - // Scene contains all points (full window) - return true; - } - - UIDrawable* getClickHandler() override { - // Scene itself doesn't handle clicks - return nullptr; - } -}; \ No newline at end of file diff --git a/tests/unit/api_keypressScene_test.py b/tests/unit/api_keypressScene_test.py deleted file mode 100644 index 7ab6e41..0000000 --- a/tests/unit/api_keypressScene_test.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/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() \ No newline at end of file diff --git a/tests/unit/api_sceneUI_test.py b/tests/unit/api_sceneUI_test.py deleted file mode 100644 index 276a549..0000000 --- a/tests/unit/api_sceneUI_test.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/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) \ No newline at end of file diff --git a/tests/automation/automation_screenshot_test_simple.py b/tests/unit/automation_screenshot_test_simple.py similarity index 100% rename from tests/automation/automation_screenshot_test_simple.py rename to tests/unit/automation_screenshot_test_simple.py diff --git a/tests/unit/grid_at_argument_test.py b/tests/unit/grid_at_argument_test.py deleted file mode 100644 index 14e9485..0000000 --- a/tests/unit/grid_at_argument_test.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -"""Test Grid.at() method with various argument formats""" - -import mcrfpy -import sys - -def test_grid_at_arguments(): - """Test that Grid.at() accepts all required argument formats""" - print("Testing Grid.at() argument formats...") - - # Create a test scene - mcrfpy.createScene("test") - - # Create a grid - grid = mcrfpy.Grid(10, 10) - ui = mcrfpy.sceneUI("test") - ui.append(grid) - - success_count = 0 - total_tests = 4 - - # Test 1: Two positional arguments (x, y) - try: - point1 = grid.at(5, 5) - print("✓ Test 1 PASSED: grid.at(5, 5)") - success_count += 1 - except Exception as e: - print(f"✗ Test 1 FAILED: grid.at(5, 5) - {e}") - - # Test 2: Single tuple argument (x, y) - try: - point2 = grid.at((3, 3)) - print("✓ Test 2 PASSED: grid.at((3, 3))") - success_count += 1 - except Exception as e: - print(f"✗ Test 2 FAILED: grid.at((3, 3)) - {e}") - - # Test 3: Keyword arguments x=x, y=y - try: - point3 = grid.at(x=7, y=2) - print("✓ Test 3 PASSED: grid.at(x=7, y=2)") - success_count += 1 - except Exception as e: - print(f"✗ Test 3 FAILED: grid.at(x=7, y=2) - {e}") - - # Test 4: pos keyword argument pos=(x, y) - try: - point4 = grid.at(pos=(1, 8)) - print("✓ Test 4 PASSED: grid.at(pos=(1, 8))") - success_count += 1 - except Exception as e: - print(f"✗ Test 4 FAILED: grid.at(pos=(1, 8)) - {e}") - - # Test error cases - print("\nTesting error cases...") - - # Test 5: Invalid - mixing pos with x/y - try: - grid.at(x=1, pos=(2, 2)) - print("✗ Test 5 FAILED: Should have raised error for mixing pos and x/y") - except TypeError as e: - print(f"✓ Test 5 PASSED: Correctly rejected mixing pos and x/y - {e}") - - # Test 6: Invalid - out of range - try: - grid.at(15, 15) - print("✗ Test 6 FAILED: Should have raised error for out of range") - except ValueError as e: - print(f"✓ Test 6 PASSED: Correctly rejected out of range - {e}") - - # Test 7: Verify all points are valid GridPoint objects - try: - # Check that we can set walkable on all returned points - if 'point1' in locals(): - point1.walkable = True - if 'point2' in locals(): - point2.walkable = False - if 'point3' in locals(): - point3.color = mcrfpy.Color(255, 0, 0) - if 'point4' in locals(): - point4.tilesprite = 5 - print("✓ All returned GridPoint objects are valid") - except Exception as e: - print(f"✗ GridPoint objects validation failed: {e}") - - print(f"\nSummary: {success_count}/{total_tests} tests passed") - - if success_count == total_tests: - print("ALL TESTS PASSED!") - sys.exit(0) - else: - print("SOME TESTS FAILED!") - sys.exit(1) - -# Run timer callback to execute tests after render loop starts -def run_test(elapsed): - test_grid_at_arguments() - -# Set a timer to run the test -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/unit/run_issue_tests.py b/tests/unit/run_issue_tests.py deleted file mode 100755 index b8ea601..0000000 --- a/tests/unit/run_issue_tests.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env python3 -""" -Test runner for high-priority McRogueFace issues - -This script runs comprehensive tests for the highest priority bugs that can be fixed rapidly. -Each test is designed to fail initially (demonstrating the bug) and pass after the fix. -""" - -import os -import sys -import subprocess -import time - -# Test configurations -TESTS = [ - { - "issue": "37", - "name": "Windows scripts subdirectory bug", - "script": "issue_37_windows_scripts_comprehensive_test.py", - "needs_game_loop": False, - "description": "Tests script loading from different working directories" - }, - { - "issue": "76", - "name": "UIEntityCollection returns wrong type", - "script": "issue_76_uientitycollection_type_test.py", - "needs_game_loop": True, - "description": "Tests type preservation for derived Entity classes in collections" - }, - { - "issue": "9", - "name": "RenderTexture resize bug", - "script": "issue_9_rendertexture_resize_test.py", - "needs_game_loop": True, - "description": "Tests UIGrid rendering with sizes beyond 1920x1080" - }, - { - "issue": "26/28", - "name": "Iterator implementation for collections", - "script": "issue_26_28_iterator_comprehensive_test.py", - "needs_game_loop": True, - "description": "Tests Python sequence protocol for UI collections" - } -] - -def run_test(test_config, mcrogueface_path): - """Run a single test and return the result""" - script_path = os.path.join(os.path.dirname(__file__), test_config["script"]) - - if not os.path.exists(script_path): - return f"SKIP - Test script not found: {script_path}" - - print(f"\n{'='*60}") - print(f"Running test for Issue #{test_config['issue']}: {test_config['name']}") - print(f"Description: {test_config['description']}") - print(f"Script: {test_config['script']}") - print(f"{'='*60}\n") - - if test_config["needs_game_loop"]: - # Run with game loop using --exec - cmd = [mcrogueface_path, "--headless", "--exec", script_path] - else: - # Run directly as Python script - cmd = [sys.executable, script_path] - - try: - start_time = time.time() - result = subprocess.run( - cmd, - capture_output=True, - text=True, - timeout=30 # 30 second timeout - ) - elapsed = time.time() - start_time - - # Check for pass/fail in output - output = result.stdout + result.stderr - - if "PASS" in output and "FAIL" not in output: - status = "PASS" - elif "FAIL" in output: - status = "FAIL" - else: - status = "UNKNOWN" - - # Look for specific bug indicators - bug_found = False - if test_config["issue"] == "37" and "Script not loaded from different directory" in output: - bug_found = True - elif test_config["issue"] == "76" and "type lost!" in output: - bug_found = True - elif test_config["issue"] == "9" and "clipped at 1920x1080" in output: - bug_found = True - elif test_config["issue"] == "26/28" and "not implemented" in output: - bug_found = True - - return { - "status": status, - "bug_found": bug_found, - "elapsed": elapsed, - "output": output if len(output) < 1000 else output[:1000] + "\n... (truncated)" - } - - except subprocess.TimeoutExpired: - return { - "status": "TIMEOUT", - "bug_found": False, - "elapsed": 30, - "output": "Test timed out after 30 seconds" - } - except Exception as e: - return { - "status": "ERROR", - "bug_found": False, - "elapsed": 0, - "output": str(e) - } - -def main(): - """Run all tests and provide summary""" - # Find mcrogueface executable - build_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "build") - mcrogueface_path = os.path.join(build_dir, "mcrogueface") - - if not os.path.exists(mcrogueface_path): - print(f"ERROR: mcrogueface executable not found at {mcrogueface_path}") - print("Please build the project first with 'make'") - return 1 - - print("McRogueFace Issue Test Suite") - print(f"Executable: {mcrogueface_path}") - print(f"Running {len(TESTS)} tests...\n") - - results = [] - - for test in TESTS: - result = run_test(test, mcrogueface_path) - results.append((test, result)) - - # Summary - print(f"\n{'='*60}") - print("TEST SUMMARY") - print(f"{'='*60}\n") - - bugs_found = 0 - tests_passed = 0 - - for test, result in results: - if isinstance(result, str): - print(f"Issue #{test['issue']}: {result}") - else: - status_str = result['status'] - if result['bug_found']: - status_str += " (BUG CONFIRMED)" - bugs_found += 1 - elif result['status'] == 'PASS': - tests_passed += 1 - - print(f"Issue #{test['issue']}: {status_str} ({result['elapsed']:.2f}s)") - - if result['status'] not in ['PASS', 'UNKNOWN']: - print(f" Details: {result['output'].splitlines()[0] if result['output'] else 'No output'}") - - print(f"\nBugs confirmed: {bugs_found}/{len(TESTS)}") - print(f"Tests passed: {tests_passed}/{len(TESTS)}") - - if bugs_found > 0: - print("\nThese tests demonstrate bugs that need fixing.") - print("After fixing, the tests should pass instead of confirming bugs.") - - return 0 - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file diff --git a/tests/unit/test_animation_callback_simple.py b/tests/unit/test_animation_callback_simple.py new file mode 100644 index 0000000..f3c0819 --- /dev/null +++ b/tests/unit/test_animation_callback_simple.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Simple test for animation callbacks - demonstrates basic usage""" + +import mcrfpy +import sys + +# Global state to track callback +callback_count = 0 + +def my_callback(anim, target): + """Simple callback that prints when animation completes""" + global callback_count + callback_count += 1 + print(f"Animation completed! Callback #{callback_count}") + # For now, anim and target are None - future enhancement + +def setup_and_run(): + """Set up scene and run animation with callback""" + # Create scene + mcrfpy.createScene("callback_demo") + mcrfpy.setScene("callback_demo") + + # Create a frame to animate + frame = mcrfpy.Frame((100, 100), (200, 200), fill_color=(255, 0, 0)) + ui = mcrfpy.sceneUI("callback_demo") + ui.append(frame) + + # Create animation with callback + print("Starting animation with callback...") + anim = mcrfpy.Animation("x", 400.0, 1.0, "easeInOutQuad", callback=my_callback) + anim.start(frame) + + # Schedule check after animation should complete + mcrfpy.setTimer("check", check_result, 1500) + +def check_result(runtime): + """Check if callback fired correctly""" + global callback_count + + if callback_count == 1: + print("SUCCESS: Callback fired exactly once!") + + # Test 2: Animation without callback + print("\nTesting animation without callback...") + ui = mcrfpy.sceneUI("callback_demo") + frame = ui[0] + + anim2 = mcrfpy.Animation("y", 300.0, 0.5, "linear") + anim2.start(frame) + + mcrfpy.setTimer("final", final_check, 700) + else: + print(f"FAIL: Expected 1 callback, got {callback_count}") + sys.exit(1) + +def final_check(runtime): + """Final check - callback count should still be 1""" + global callback_count + + if callback_count == 1: + print("SUCCESS: No unexpected callbacks fired!") + print("\nAnimation callback feature working correctly!") + sys.exit(0) + else: + print(f"FAIL: Callback count changed to {callback_count}") + sys.exit(1) + +# Start the demo +print("Animation Callback Demo") +print("=" * 30) +setup_and_run() \ No newline at end of file diff --git a/tests/unit/test_animation_chaining.py b/tests/unit/test_animation_chaining.py new file mode 100644 index 0000000..b8402fd --- /dev/null +++ b/tests/unit/test_animation_chaining.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +""" +Test Animation Chaining +======================= + +Demonstrates proper animation chaining to avoid glitches. +""" + +import mcrfpy +import sys + +class PathAnimator: + """Handles step-by-step path animation with proper chaining""" + + def __init__(self, entity, path, step_duration=0.3, on_complete=None): + self.entity = entity + self.path = path + self.current_index = 0 + self.step_duration = step_duration + self.on_complete = on_complete + self.animating = False + self.check_timer_name = f"path_check_{id(self)}" + + def start(self): + """Start animating along the path""" + if not self.path or self.animating: + return + + self.current_index = 0 + self.animating = True + self._animate_next_step() + + def _animate_next_step(self): + """Animate to the next position in the path""" + if self.current_index >= len(self.path): + # Path complete + self.animating = False + mcrfpy.delTimer(self.check_timer_name) + if self.on_complete: + self.on_complete() + return + + # Get target position + target_x, target_y = self.path[self.current_index] + + # Create animations + self.anim_x = mcrfpy.Animation("x", float(target_x), self.step_duration, "easeInOut") + self.anim_y = mcrfpy.Animation("y", float(target_y), self.step_duration, "easeInOut") + + # Start animations + self.anim_x.start(self.entity) + self.anim_y.start(self.entity) + + # Update visibility if entity has this method + if hasattr(self.entity, 'update_visibility'): + self.entity.update_visibility() + + # Set timer to check completion + mcrfpy.setTimer(self.check_timer_name, self._check_completion, 50) + + def _check_completion(self, dt): + """Check if current animation is complete""" + if hasattr(self.anim_x, 'is_complete') and self.anim_x.is_complete: + # Move to next step + self.current_index += 1 + mcrfpy.delTimer(self.check_timer_name) + self._animate_next_step() + +# Create test scene +mcrfpy.createScene("chain_test") + +# Create grid +grid = mcrfpy.Grid(grid_x=20, grid_y=15) +grid.fill_color = mcrfpy.Color(20, 20, 30) + +# Simple map +for y in range(15): + for x in range(20): + cell = grid.at(x, y) + if x == 0 or x == 19 or y == 0 or y == 14: + cell.walkable = False + cell.transparent = False + cell.color = mcrfpy.Color(60, 40, 40) + else: + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(100, 100, 120) + +# Create entities +player = mcrfpy.Entity(2, 2, grid=grid) +player.sprite_index = 64 # @ + +enemy = mcrfpy.Entity(17, 12, grid=grid) +enemy.sprite_index = 69 # E + +# UI setup +ui = mcrfpy.sceneUI("chain_test") +ui.append(grid) +grid.position = (100, 100) +grid.size = (600, 450) + +title = mcrfpy.Caption("Animation Chaining Test", 300, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +status = mcrfpy.Caption("Press 1: Animate Player | 2: Animate Enemy | 3: Both | Q: Quit", 100, 50) +status.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(status) + +info = mcrfpy.Caption("Status: Ready", 100, 70) +info.fill_color = mcrfpy.Color(100, 255, 100) +ui.append(info) + +# Path animators +player_animator = None +enemy_animator = None + +def animate_player(): + """Animate player along a path""" + global player_animator + + # Define path + path = [ + (2, 2), (3, 2), (4, 2), (5, 2), (6, 2), # Right + (6, 3), (6, 4), (6, 5), (6, 6), # Down + (7, 6), (8, 6), (9, 6), (10, 6), # Right + (10, 7), (10, 8), (10, 9), # Down + ] + + def on_complete(): + info.text = "Player animation complete!" + + player_animator = PathAnimator(player, path, step_duration=0.2, on_complete=on_complete) + player_animator.start() + info.text = "Animating player..." + +def animate_enemy(): + """Animate enemy along a path""" + global enemy_animator + + # Define path + path = [ + (17, 12), (16, 12), (15, 12), (14, 12), # Left + (14, 11), (14, 10), (14, 9), # Up + (13, 9), (12, 9), (11, 9), (10, 9), # Left + (10, 8), (10, 7), (10, 6), # Up + ] + + def on_complete(): + info.text = "Enemy animation complete!" + + enemy_animator = PathAnimator(enemy, path, step_duration=0.25, on_complete=on_complete) + enemy_animator.start() + info.text = "Animating enemy..." + +def animate_both(): + """Animate both entities simultaneously""" + info.text = "Animating both entities..." + animate_player() + animate_enemy() + +# Camera follow test +camera_follow = False + +def update_camera(dt): + """Update camera to follow player if enabled""" + if camera_follow and player_animator and player_animator.animating: + # Smooth camera follow + center_x = player.x * 30 # Assuming ~30 pixels per cell + center_y = player.y * 30 + cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.25, "linear") + cam_anim.start(grid) + +# Input handler +def handle_input(key, state): + global camera_follow + + if state != "start": + return + + key = key.lower() + + if key == "q": + sys.exit(0) + elif key == "num1": + animate_player() + elif key == "num2": + animate_enemy() + elif key == "num3": + animate_both() + elif key == "c": + camera_follow = not camera_follow + info.text = f"Camera follow: {'ON' if camera_follow else 'OFF'}" + elif key == "r": + # Reset positions + player.x, player.y = 2, 2 + enemy.x, enemy.y = 17, 12 + info.text = "Positions reset" + +# Setup +mcrfpy.setScene("chain_test") +mcrfpy.keypressScene(handle_input) + +# Camera update timer +mcrfpy.setTimer("cam_update", update_camera, 100) + +print("Animation Chaining Test") +print("=======================") +print("This test demonstrates proper animation chaining") +print("to avoid simultaneous position updates.") +print() +print("Controls:") +print(" 1 - Animate player step by step") +print(" 2 - Animate enemy step by step") +print(" 3 - Animate both (simultaneous)") +print(" C - Toggle camera follow") +print(" R - Reset positions") +print(" Q - Quit") +print() +print("Notice how each entity moves one tile at a time,") +print("waiting for each step to complete before the next.") diff --git a/tests/unit/test_animation_debug.py b/tests/unit/test_animation_debug.py new file mode 100644 index 0000000..0b7ab7c --- /dev/null +++ b/tests/unit/test_animation_debug.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +""" +Animation Debug Tool +==================== + +Helps diagnose animation timing issues. +""" + +import mcrfpy +import sys + +# Track all active animations +active_animations = {} +animation_log = [] + +class AnimationTracker: + """Tracks animation lifecycle for debugging""" + + def __init__(self, name, target, property_name, target_value, duration): + self.name = name + self.target = target + self.property_name = property_name + self.target_value = target_value + self.duration = duration + self.start_time = None + self.animation = None + + def start(self): + """Start the animation with tracking""" + # Log the start + log_entry = f"START: {self.name} - {self.property_name} to {self.target_value} over {self.duration}s" + animation_log.append(log_entry) + print(log_entry) + + # Create and start animation + self.animation = mcrfpy.Animation(self.property_name, self.target_value, self.duration, "linear") + self.animation.start(self.target) + + # Track it + active_animations[self.name] = self + + # Set timer to check completion + check_interval = 100 # ms + mcrfpy.setTimer(f"check_{self.name}", self._check_complete, check_interval) + + def _check_complete(self, dt): + """Check if animation is complete""" + if self.animation and hasattr(self.animation, 'is_complete') and self.animation.is_complete: + # Log completion + log_entry = f"COMPLETE: {self.name}" + animation_log.append(log_entry) + print(log_entry) + + # Remove from active + if self.name in active_animations: + del active_animations[self.name] + + # Stop checking + mcrfpy.delTimer(f"check_{self.name}") + +# Create test scene +mcrfpy.createScene("anim_debug") + +# Simple grid +grid = mcrfpy.Grid(grid_x=15, grid_y=10) +for y in range(10): + for x in range(15): + cell = grid.at(x, y) + cell.walkable = True + cell.color = mcrfpy.Color(100, 100, 120) + +# Test entity +entity = mcrfpy.Entity(5, 5, grid=grid) +entity.sprite_index = 64 + +# UI +ui = mcrfpy.sceneUI("anim_debug") +ui.append(grid) +grid.position = (100, 150) +grid.size = (450, 300) + +title = mcrfpy.Caption("Animation Debug Tool", 250, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +status = mcrfpy.Caption("Press keys to test animations", 100, 50) +status.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(status) + +pos_display = mcrfpy.Caption("", 100, 70) +pos_display.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(pos_display) + +active_display = mcrfpy.Caption("Active animations: 0", 100, 90) +active_display.fill_color = mcrfpy.Color(100, 255, 255) +ui.append(active_display) + +# Test scenarios +def test_simultaneous(): + """Test multiple animations at once (causes issues)""" + print("\n=== TEST: Simultaneous Animations ===") + status.text = "Testing simultaneous X and Y animations" + + # Start both at once + anim1 = AnimationTracker("sim_x", entity, "x", 10.0, 1.0) + anim2 = AnimationTracker("sim_y", entity, "y", 8.0, 1.5) + + anim1.start() + anim2.start() + +def test_rapid_fire(): + """Test starting new animation before previous completes""" + print("\n=== TEST: Rapid Fire Animations ===") + status.text = "Testing rapid fire animations (overlapping)" + + # Start first animation + anim1 = AnimationTracker("rapid_1", entity, "x", 8.0, 2.0) + anim1.start() + + # Start another after 500ms (before first completes) + def start_second(dt): + anim2 = AnimationTracker("rapid_2", entity, "x", 12.0, 1.0) + anim2.start() + mcrfpy.delTimer("rapid_timer") + + mcrfpy.setTimer("rapid_timer", start_second, 500) + +def test_sequential(): + """Test proper sequential animations""" + print("\n=== TEST: Sequential Animations ===") + status.text = "Testing proper sequential animations" + + sequence = [ + ("seq_1", "x", 8.0, 0.5), + ("seq_2", "y", 7.0, 0.5), + ("seq_3", "x", 6.0, 0.5), + ("seq_4", "y", 5.0, 0.5), + ] + + def run_sequence(index=0): + if index >= len(sequence): + print("Sequence complete!") + return + + name, prop, value, duration = sequence[index] + anim = AnimationTracker(name, entity, prop, value, duration) + anim.start() + + # Schedule next + delay = int(duration * 1000) + 100 # Add buffer + mcrfpy.setTimer(f"seq_timer_{index}", lambda dt: run_sequence(index + 1), delay) + + run_sequence() + +def test_conflicting(): + """Test conflicting animations on same property""" + print("\n=== TEST: Conflicting Animations ===") + status.text = "Testing conflicting animations (same property)" + + # Start animation to x=10 + anim1 = AnimationTracker("conflict_1", entity, "x", 10.0, 2.0) + anim1.start() + + # After 1 second, start conflicting animation to x=2 + def start_conflict(dt): + print("Starting conflicting animation!") + anim2 = AnimationTracker("conflict_2", entity, "x", 2.0, 1.0) + anim2.start() + mcrfpy.delTimer("conflict_timer") + + mcrfpy.setTimer("conflict_timer", start_conflict, 1000) + +# Update display +def update_display(dt): + pos_display.text = f"Entity position: ({entity.x:.2f}, {entity.y:.2f})" + active_display.text = f"Active animations: {len(active_animations)}" + + # Show active animation names + if active_animations: + names = ", ".join(active_animations.keys()) + active_display.text += f" [{names}]" + +# Show log +def show_log(): + print("\n=== ANIMATION LOG ===") + for entry in animation_log[-10:]: # Last 10 entries + print(entry) + print("===================") + +# Input handler +def handle_input(key, state): + if state != "start": + return + + key = key.lower() + + if key == "q": + sys.exit(0) + elif key == "num1": + test_simultaneous() + elif key == "num2": + test_rapid_fire() + elif key == "num3": + test_sequential() + elif key == "num4": + test_conflicting() + elif key == "l": + show_log() + elif key == "r": + entity.x = 5 + entity.y = 5 + animation_log.clear() + active_animations.clear() + print("Reset entity and cleared log") + +# Setup +mcrfpy.setScene("anim_debug") +mcrfpy.keypressScene(handle_input) +mcrfpy.setTimer("update", update_display, 100) + +print("Animation Debug Tool") +print("====================") +print("This tool helps diagnose animation timing issues") +print() +print("Tests:") +print(" 1 - Simultaneous X/Y (may cause issues)") +print(" 2 - Rapid fire (overlapping animations)") +print(" 3 - Sequential (proper chaining)") +print(" 4 - Conflicting (same property)") +print() +print("Other keys:") +print(" L - Show animation log") +print(" R - Reset") +print(" Q - Quit") +print() +print("Watch the console for animation lifecycle events") \ No newline at end of file diff --git a/tests/unit/test_animation_immediate.py b/tests/unit/test_animation_immediate.py new file mode 100644 index 0000000..d24f713 --- /dev/null +++ b/tests/unit/test_animation_immediate.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +""" +Test Animation creation without timer +""" + +import mcrfpy + +print("1. Creating scene...") +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +print("2. Getting UI...") +ui = mcrfpy.sceneUI("test") + +print("3. Creating frame...") +frame = mcrfpy.Frame(100, 100, 200, 200) +ui.append(frame) + +print("4. Creating Animation object...") +try: + anim = mcrfpy.Animation("x", 500.0, 2000, "easeInOut") + print("5. Animation created successfully!") +except Exception as e: + print(f"5. Animation creation failed: {e}") + +print("6. Starting animation...") +try: + anim.start(frame) + print("7. Animation started!") +except Exception as e: + print(f"7. Animation start failed: {e}") + +print("8. Script completed without crash!") \ No newline at end of file diff --git a/tests/unit/test_animation_raii.py b/tests/unit/test_animation_raii.py new file mode 100644 index 0000000..86ce225 --- /dev/null +++ b/tests/unit/test_animation_raii.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" +Test the RAII AnimationManager implementation. +This verifies that weak_ptr properly handles all crash scenarios. +""" + +import mcrfpy +import sys + +print("RAII AnimationManager Test Suite") +print("=" * 40) + +# Test state +tests_passed = 0 +tests_failed = 0 +test_results = [] + +def test_result(name, passed, details=""): + global tests_passed, tests_failed + if passed: + tests_passed += 1 + result = f"✓ {name}" + else: + tests_failed += 1 + result = f"✗ {name}: {details}" + print(result) + test_results.append((name, passed, details)) + +def test_1_basic_animation(): + """Test that basic animations still work""" + try: + ui = mcrfpy.sceneUI("test") + frame = mcrfpy.Frame(100, 100, 100, 100) + ui.append(frame) + + anim = mcrfpy.Animation("x", 200.0, 1000, "linear") + anim.start(frame) + + # Check if animation has valid target + if hasattr(anim, 'hasValidTarget'): + valid = anim.hasValidTarget() + test_result("Basic animation with hasValidTarget", valid) + else: + test_result("Basic animation", True) + except Exception as e: + test_result("Basic animation", False, str(e)) + +def test_2_remove_animated_object(): + """Test removing object with active animation""" + try: + ui = mcrfpy.sceneUI("test") + frame = mcrfpy.Frame(100, 100, 100, 100) + ui.append(frame) + + # Start animation + anim = mcrfpy.Animation("x", 500.0, 2000, "easeInOut") + anim.start(frame) + + # Remove the frame + ui.remove(0) + + # Check if animation knows target is gone + if hasattr(anim, 'hasValidTarget'): + valid = anim.hasValidTarget() + test_result("Animation detects removed target", not valid) + else: + # If method doesn't exist, just check we didn't crash + test_result("Remove animated object", True) + except Exception as e: + test_result("Remove animated object", False, str(e)) + +def test_3_complete_animation(): + """Test completing animation immediately""" + try: + ui = mcrfpy.sceneUI("test") + frame = mcrfpy.Frame(100, 100, 100, 100) + ui.append(frame) + + # Start animation + anim = mcrfpy.Animation("x", 500.0, 2000, "linear") + anim.start(frame) + + # Complete it + if hasattr(anim, 'complete'): + anim.complete() + # Frame should now be at x=500 + test_result("Animation complete method", True) + else: + test_result("Animation complete method", True, "Method not available") + except Exception as e: + test_result("Animation complete method", False, str(e)) + +def test_4_multiple_animations_timer(): + """Test creating multiple animations in timer callback""" + success = False + + def create_animations(runtime): + nonlocal success + try: + ui = mcrfpy.sceneUI("test") + frame = mcrfpy.Frame(200, 200, 100, 100) + ui.append(frame) + + # Create multiple animations rapidly (this used to crash) + for i in range(10): + anim = mcrfpy.Animation("x", 300.0 + i * 10, 1000, "linear") + anim.start(frame) + + success = True + except Exception as e: + print(f"Timer animation error: {e}") + finally: + mcrfpy.setTimer("exit", lambda t: None, 100) + + # Clear scene + ui = mcrfpy.sceneUI("test") + while len(ui) > 0: + ui.remove(len(ui) - 1) + + mcrfpy.setTimer("test", create_animations, 50) + mcrfpy.setTimer("check", lambda t: test_result("Multiple animations in timer", success), 200) + +def test_5_scene_cleanup(): + """Test that changing scenes cleans up animations""" + try: + # Create a second scene + mcrfpy.createScene("test2") + + # Add animated objects to first scene + ui = mcrfpy.sceneUI("test") + for i in range(5): + frame = mcrfpy.Frame(50 * i, 100, 40, 40) + ui.append(frame) + anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce") + anim.start(frame) + + # Switch scenes (animations should become invalid) + mcrfpy.setScene("test2") + + # Switch back + mcrfpy.setScene("test") + + test_result("Scene change cleanup", True) + except Exception as e: + test_result("Scene change cleanup", False, str(e)) + +def test_6_animation_after_clear(): + """Test animations after clearing UI""" + try: + ui = mcrfpy.sceneUI("test") + + # Create and animate + frame = mcrfpy.Frame(100, 100, 100, 100) + ui.append(frame) + anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic") + anim.start(frame) + + # Clear all UI + while len(ui) > 0: + ui.remove(len(ui) - 1) + + # Animation should handle this gracefully + if hasattr(anim, 'hasValidTarget'): + valid = anim.hasValidTarget() + test_result("Animation after UI clear", not valid) + else: + test_result("Animation after UI clear", True) + except Exception as e: + test_result("Animation after UI clear", False, str(e)) + +def run_all_tests(runtime): + """Run all RAII tests""" + print("\nRunning RAII Animation Tests...") + print("-" * 40) + + test_1_basic_animation() + test_2_remove_animated_object() + test_3_complete_animation() + test_4_multiple_animations_timer() + test_5_scene_cleanup() + test_6_animation_after_clear() + + # Schedule result summary + mcrfpy.setTimer("results", print_results, 500) + +def print_results(runtime): + """Print test results""" + print("\n" + "=" * 40) + print(f"Tests passed: {tests_passed}") + print(f"Tests failed: {tests_failed}") + + if tests_failed == 0: + print("\n✓ All tests passed! RAII implementation is working correctly.") + else: + print(f"\n✗ {tests_failed} tests failed.") + print("\nFailed tests:") + for name, passed, details in test_results: + if not passed: + print(f" - {name}: {details}") + + # Exit + mcrfpy.setTimer("exit", lambda t: sys.exit(0 if tests_failed == 0 else 1), 500) + +# Setup and run +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Add a background +ui = mcrfpy.sceneUI("test") +bg = mcrfpy.Frame(0, 0, 1024, 768) +bg.fill_color = mcrfpy.Color(20, 20, 30) +ui.append(bg) + +# Start tests +mcrfpy.setTimer("start", run_all_tests, 100) \ No newline at end of file diff --git a/tests/unit/test_animation_removal.py b/tests/unit/test_animation_removal.py new file mode 100644 index 0000000..a626d91 --- /dev/null +++ b/tests/unit/test_animation_removal.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Test if the crash is related to removing animated objects +""" + +import mcrfpy +import sys + +def clear_and_recreate(runtime): + """Clear UI and recreate - mimics demo switching""" + print(f"\nTimer called at {runtime}") + + ui = mcrfpy.sceneUI("test") + + # Remove all but first 2 items (like clear_demo_objects) + print(f"Scene has {len(ui)} elements before clearing") + while len(ui) > 2: + ui.remove(len(ui)-1) + print(f"Scene has {len(ui)} elements after clearing") + + # Create new animated objects + print("Creating new animated objects...") + for i in range(5): + f = mcrfpy.Frame(100 + i*50, 200, 40, 40) + f.fill_color = mcrfpy.Color(100 + i*30, 50, 200) + ui.append(f) + + # Start animation on the new frame + target_x = 300 + i * 50 + anim = mcrfpy.Animation("x", float(target_x), 1.0, "easeInOut") + anim.start(f) + + print("New objects created and animated") + + # Schedule exit + mcrfpy.setTimer("exit", lambda t: sys.exit(0), 2000) + +# Create initial scene +print("Creating scene...") +mcrfpy.createScene("test") +mcrfpy.setScene("test") +ui = mcrfpy.sceneUI("test") + +# Add title and subtitle (to preserve during clearing) +title = mcrfpy.Caption("Test Title", 400, 20) +subtitle = mcrfpy.Caption("Test Subtitle", 400, 50) +ui.extend([title, subtitle]) + +# Create initial animated objects +print("Creating initial animated objects...") +for i in range(10): + f = mcrfpy.Frame(50 + i*30, 100, 25, 25) + f.fill_color = mcrfpy.Color(255, 100, 100) + ui.append(f) + + # Animate them + anim = mcrfpy.Animation("y", 300.0, 2.0, "easeOutBounce") + anim.start(f) + +print(f"Initial scene has {len(ui)} elements") + +# Schedule the clear and recreate +mcrfpy.setTimer("switch", clear_and_recreate, 1000) + +print("\nEntering game loop...") \ No newline at end of file diff --git a/tests/unit/test_api_docs.py b/tests/unit/test_api_docs.py new file mode 100644 index 0000000..c5f75ca --- /dev/null +++ b/tests/unit/test_api_docs.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Test that API documentation generator works correctly.""" + +import os +import sys +from pathlib import Path + +def test_api_docs_exist(): + """Test that API documentation was generated.""" + docs_path = Path("docs/API_REFERENCE.md") + + if not docs_path.exists(): + print("ERROR: API documentation not found at docs/API_REFERENCE.md") + return False + + print("✓ API documentation file exists") + + # Check file size + size = docs_path.stat().st_size + if size < 1000: + print(f"ERROR: API documentation seems too small ({size} bytes)") + return False + + print(f"✓ API documentation has reasonable size ({size} bytes)") + + # Read content + with open(docs_path, 'r') as f: + content = f.read() + + # Check for expected sections + expected_sections = [ + "# McRogueFace API Reference", + "## Overview", + "## Classes", + "## Functions", + "## Automation Module" + ] + + missing = [] + for section in expected_sections: + if section not in content: + missing.append(section) + + if missing: + print(f"ERROR: Missing sections: {missing}") + return False + + print("✓ All expected sections present") + + # Check for key classes + key_classes = ["Frame", "Caption", "Sprite", "Grid", "Entity", "Scene"] + missing_classes = [] + for cls in key_classes: + if f"### class {cls}" not in content: + missing_classes.append(cls) + + if missing_classes: + print(f"ERROR: Missing classes: {missing_classes}") + return False + + print("✓ All key classes documented") + + # Check for key functions + key_functions = ["createScene", "setScene", "currentScene", "find", "setTimer"] + missing_funcs = [] + for func in key_functions: + if f"### {func}" not in content: + missing_funcs.append(func) + + if missing_funcs: + print(f"ERROR: Missing functions: {missing_funcs}") + return False + + print("✓ All key functions documented") + + # Check automation module + if "automation.screenshot" in content: + print("✓ Automation module documented") + else: + print("ERROR: Automation module not properly documented") + return False + + # Count documentation entries + class_count = content.count("### class ") + func_count = content.count("### ") - class_count - content.count("### automation.") + auto_count = content.count("### automation.") + + print(f"\nDocumentation Coverage:") + print(f"- Classes: {class_count}") + print(f"- Functions: {func_count}") + print(f"- Automation methods: {auto_count}") + + return True + +def test_doc_accuracy(): + """Test that documentation matches actual API.""" + # Import mcrfpy to check + import mcrfpy + + print("\nVerifying documentation accuracy...") + + # Read documentation + with open("docs/API_REFERENCE.md", 'r') as f: + content = f.read() + + # Check that all public classes are documented + actual_classes = [name for name in dir(mcrfpy) + if isinstance(getattr(mcrfpy, name), type) and not name.startswith('_')] + + undocumented = [] + for cls in actual_classes: + if f"### class {cls}" not in content: + undocumented.append(cls) + + if undocumented: + print(f"WARNING: Undocumented classes: {undocumented}") + else: + print("✓ All public classes are documented") + + # Check functions + actual_funcs = [name for name in dir(mcrfpy) + if callable(getattr(mcrfpy, name)) and not name.startswith('_') + and not isinstance(getattr(mcrfpy, name), type)] + + undoc_funcs = [] + for func in actual_funcs: + if f"### {func}" not in content: + undoc_funcs.append(func) + + if undoc_funcs: + print(f"WARNING: Undocumented functions: {undoc_funcs}") + else: + print("✓ All public functions are documented") + + return True + +def main(): + """Run all API documentation tests.""" + print("API Documentation Tests") + print("======================\n") + + all_passed = True + + # Test 1: Documentation exists and is complete + print("Test 1: Documentation Generation") + if not test_api_docs_exist(): + all_passed = False + print() + + # Test 2: Documentation accuracy + print("Test 2: Documentation Accuracy") + if not test_doc_accuracy(): + all_passed = False + print() + + if all_passed: + print("✅ All API documentation tests passed!") + sys.exit(0) + else: + print("❌ Some tests failed.") + sys.exit(1) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tests/unit/test_astar.py b/tests/unit/test_astar.py new file mode 100644 index 0000000..f0afadb --- /dev/null +++ b/tests/unit/test_astar.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" +Test A* Pathfinding Implementation +================================== + +Compares A* with Dijkstra and the existing find_path method. +""" + +import mcrfpy +import sys +import time + +print("A* Pathfinding Test") +print("==================") + +# Create scene and grid +mcrfpy.createScene("astar_test") +grid = mcrfpy.Grid(grid_x=20, grid_y=20) + +# Initialize grid - all walkable +for y in range(20): + for x in range(20): + grid.at(x, y).walkable = True + +# Create a wall barrier with a narrow passage +print("\nCreating wall with narrow passage...") +for y in range(5, 15): + for x in range(8, 12): + if not (x == 10 and y == 10): # Leave a gap at (10, 10) + grid.at(x, y).walkable = False + print(f" Wall at ({x}, {y})") + +print(f"\nPassage at (10, 10)") + +# Test points +start = (2, 10) +end = (18, 10) + +print(f"\nFinding path from {start} to {end}") + +# Test 1: A* pathfinding +print("\n1. Testing A* pathfinding (compute_astar_path):") +start_time = time.time() +astar_path = grid.compute_astar_path(start[0], start[1], end[0], end[1]) +astar_time = time.time() - start_time +print(f" A* path length: {len(astar_path)}") +print(f" A* time: {astar_time*1000:.3f} ms") +if astar_path: + print(f" First 5 steps: {astar_path[:5]}") + +# Test 2: find_path method (which should also use A*) +print("\n2. Testing find_path method:") +start_time = time.time() +find_path_result = grid.find_path(start[0], start[1], end[0], end[1]) +find_path_time = time.time() - start_time +print(f" find_path length: {len(find_path_result)}") +print(f" find_path time: {find_path_time*1000:.3f} ms") +if find_path_result: + print(f" First 5 steps: {find_path_result[:5]}") + +# Test 3: Dijkstra pathfinding for comparison +print("\n3. Testing Dijkstra pathfinding:") +start_time = time.time() +grid.compute_dijkstra(start[0], start[1]) +dijkstra_path = grid.get_dijkstra_path(end[0], end[1]) +dijkstra_time = time.time() - start_time +print(f" Dijkstra path length: {len(dijkstra_path)}") +print(f" Dijkstra time: {dijkstra_time*1000:.3f} ms") +if dijkstra_path: + print(f" First 5 steps: {dijkstra_path[:5]}") + +# Compare results +print("\nComparison:") +print(f" A* vs find_path: {'SAME' if astar_path == find_path_result else 'DIFFERENT'}") +print(f" A* vs Dijkstra: {'SAME' if astar_path == dijkstra_path else 'DIFFERENT'}") + +# Test with no path (blocked endpoints) +print("\n4. Testing with blocked destination:") +blocked_end = (10, 8) # Inside the wall +grid.at(blocked_end[0], blocked_end[1]).walkable = False +no_path = grid.compute_astar_path(start[0], start[1], blocked_end[0], blocked_end[1]) +print(f" Path to blocked cell: {no_path} (should be empty)") + +# Test diagonal movement +print("\n5. Testing diagonal paths:") +diag_start = (0, 0) +diag_end = (5, 5) +diag_path = grid.compute_astar_path(diag_start[0], diag_start[1], diag_end[0], diag_end[1]) +print(f" Diagonal path from {diag_start} to {diag_end}:") +print(f" Length: {len(diag_path)}") +print(f" Path: {diag_path}") + +# Expected optimal diagonal path length is 5 moves (moving diagonally each step) + +# Performance test with larger path +print("\n6. Performance test (corner to corner):") +corner_paths = [] +methods = [ + ("A*", lambda: grid.compute_astar_path(0, 0, 19, 19)), + ("Dijkstra", lambda: (grid.compute_dijkstra(0, 0), grid.get_dijkstra_path(19, 19))[1]) +] + +for name, method in methods: + start_time = time.time() + path = method() + elapsed = time.time() - start_time + print(f" {name}: {len(path)} steps in {elapsed*1000:.3f} ms") + +print("\nA* pathfinding tests completed!") +print("Summary:") +print(" - A* pathfinding is working correctly") +print(" - Paths match between A* and Dijkstra") +print(" - Empty paths returned for blocked destinations") +print(" - Diagonal movement supported") + +# Quick visual test +def visual_test(runtime): + print("\nVisual test timer fired") + sys.exit(0) + +# Set up minimal UI for visual test +ui = mcrfpy.sceneUI("astar_test") +ui.append(grid) +grid.position = (50, 50) +grid.size = (400, 400) + +mcrfpy.setScene("astar_test") +mcrfpy.setTimer("visual", visual_test, 100) + +print("\nStarting visual test...") \ No newline at end of file diff --git a/tests/unit/test_audio_cleanup.py b/tests/unit/test_audio_cleanup.py new file mode 100644 index 0000000..a2ca61f --- /dev/null +++ b/tests/unit/test_audio_cleanup.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +"""Test audio cleanup on exit""" +import mcrfpy +import sys + +print("Testing audio cleanup...") + +# Create a scene and immediately exit +mcrfpy.createScene("test") +print("Exiting now...") +sys.exit(0) \ No newline at end of file diff --git a/tests/unit/test_builtin_context.py b/tests/unit/test_builtin_context.py new file mode 100644 index 0000000..271f8e6 --- /dev/null +++ b/tests/unit/test_builtin_context.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Test Python builtins in function context like the failing demos""" + +import mcrfpy + +print("Testing builtins in different contexts...") +print("=" * 50) + +# Test 1: At module level (working in our test) +print("Test 1: Module level") +try: + for x in range(3): + print(f" x={x}") + print(" ✓ Module level works") +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + +print() + +# Test 2: In a function +print("Test 2: Inside function") +def test_function(): + try: + for x in range(3): + print(f" x={x}") + print(" ✓ Function level works") + except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +test_function() + +print() + +# Test 3: In a function that creates mcrfpy objects +print("Test 3: Function creating mcrfpy objects") +def create_scene(): + try: + mcrfpy.createScene("test") + print(" ✓ Created scene") + + # Now try range + for x in range(3): + print(f" x={x}") + print(" ✓ Range after createScene works") + + # Create grid + grid = mcrfpy.Grid(grid_x=10, grid_y=10) + print(" ✓ Created grid") + + # Try range again + for x in range(3): + print(f" x={x}") + print(" ✓ Range after Grid creation works") + + return grid + except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + return None + +grid = create_scene() + +print() + +# Test 4: The exact failing pattern +print("Test 4: Exact failing pattern") +def failing_pattern(): + try: + mcrfpy.createScene("failing_test") + grid = mcrfpy.Grid(grid_x=14, grid_y=10) + + # This is where it fails in the demos + walls = [] + print(" About to enter range loop...") + for x in range(1, 8): + walls.append((x, 1)) + print(f" ✓ Created walls: {walls}") + + except Exception as e: + print(f" ✗ Error at line: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +failing_pattern() + +print() + +# Test 5: Check if it's related to the append operation +print("Test 5: Testing append in loop") +def test_append(): + try: + walls = [] + # Test 1: Simple append + walls.append((1, 1)) + print(" ✓ Single append works") + + # Test 2: Manual loop + i = 0 + while i < 3: + walls.append((i, 1)) + i += 1 + print(f" ✓ While loop append works: {walls}") + + # Test 3: Range with different operations + walls2 = [] + for x in range(3): + tup = (x, 2) + walls2.append(tup) + print(f" ✓ Range with temp variable works: {walls2}") + + # Test 4: Direct tuple creation in append + walls3 = [] + for x in range(3): + walls3.append((x, 3)) + print(f" ✓ Direct tuple append works: {walls3}") + + except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +test_append() + +print() +print("All tests complete.") \ No newline at end of file diff --git a/tests/unit/test_color_fix.py b/tests/unit/test_color_fix.py new file mode 100644 index 0000000..d9fa7dc --- /dev/null +++ b/tests/unit/test_color_fix.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Simple test for Color setter fix""" + +import mcrfpy + +print("Testing Color fix...") + +# Test 1: Create grid +try: + mcrfpy.createScene("test") + grid = mcrfpy.Grid(grid_x=5, grid_y=5) + print("✓ Grid created") +except Exception as e: + print(f"✗ Grid creation failed: {e}") + exit(1) + +# Test 2: Set color with tuple +try: + grid.at(0, 0).color = (100, 100, 100) + print("✓ Tuple color assignment works") +except Exception as e: + print(f"✗ Tuple assignment failed: {e}") + +# Test 3: Set color with Color object +try: + grid.at(0, 0).color = mcrfpy.Color(200, 200, 200) + print("✓ Color object assignment works!") +except Exception as e: + print(f"✗ Color assignment failed: {e}") + +print("Done.") \ No newline at end of file diff --git a/tests/unit/test_color_helpers.py b/tests/unit/test_color_helpers.py new file mode 100644 index 0000000..49e8b65 --- /dev/null +++ b/tests/unit/test_color_helpers.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +""" +Test #94: Color helper methods - from_hex, to_hex, lerp +""" + +import mcrfpy +import sys + +def test_color_helpers(runtime): + """Test Color helper methods""" + + all_pass = True + + # Test 1: from_hex with # prefix + try: + c1 = mcrfpy.Color.from_hex("#FF0000") + assert c1.r == 255 and c1.g == 0 and c1.b == 0 and c1.a == 255, f"from_hex('#FF0000') failed: {c1}" + print("+ Color.from_hex('#FF0000') works") + except Exception as e: + print(f"x Color.from_hex('#FF0000') failed: {e}") + all_pass = False + + # Test 2: from_hex without # prefix + try: + c2 = mcrfpy.Color.from_hex("00FF00") + assert c2.r == 0 and c2.g == 255 and c2.b == 0 and c2.a == 255, f"from_hex('00FF00') failed: {c2}" + print("+ Color.from_hex('00FF00') works") + except Exception as e: + print(f"x Color.from_hex('00FF00') failed: {e}") + all_pass = False + + # Test 3: from_hex with alpha + try: + c3 = mcrfpy.Color.from_hex("#0000FF80") + assert c3.r == 0 and c3.g == 0 and c3.b == 255 and c3.a == 128, f"from_hex('#0000FF80') failed: {c3}" + print("+ Color.from_hex('#0000FF80') with alpha works") + except Exception as e: + print(f"x Color.from_hex('#0000FF80') failed: {e}") + all_pass = False + + # Test 4: from_hex error handling + try: + c4 = mcrfpy.Color.from_hex("GGGGGG") + print("x from_hex should fail on invalid hex") + all_pass = False + except ValueError as e: + print("+ Color.from_hex() correctly rejects invalid hex") + + # Test 5: from_hex wrong length + try: + c5 = mcrfpy.Color.from_hex("FF00") + print("x from_hex should fail on wrong length") + all_pass = False + except ValueError as e: + print("+ Color.from_hex() correctly rejects wrong length") + + # Test 6: to_hex without alpha + try: + c6 = mcrfpy.Color(255, 128, 64) + hex_str = c6.to_hex() + assert hex_str == "#FF8040", f"to_hex() failed: {hex_str}" + print("+ Color.to_hex() works") + except Exception as e: + print(f"x Color.to_hex() failed: {e}") + all_pass = False + + # Test 7: to_hex with alpha + try: + c7 = mcrfpy.Color(255, 128, 64, 127) + hex_str = c7.to_hex() + assert hex_str == "#FF80407F", f"to_hex() with alpha failed: {hex_str}" + print("+ Color.to_hex() with alpha works") + except Exception as e: + print(f"x Color.to_hex() with alpha failed: {e}") + all_pass = False + + # Test 8: Round-trip hex conversion + try: + original_hex = "#ABCDEF" + c8 = mcrfpy.Color.from_hex(original_hex) + result_hex = c8.to_hex() + assert result_hex == original_hex, f"Round-trip failed: {original_hex} -> {result_hex}" + print("+ Hex round-trip conversion works") + except Exception as e: + print(f"x Hex round-trip failed: {e}") + all_pass = False + + # Test 9: lerp at t=0 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, 0.0) + assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t=0) failed: {result}" + print("+ Color.lerp(t=0) returns start color") + except Exception as e: + print(f"x Color.lerp(t=0) failed: {e}") + all_pass = False + + # Test 10: lerp at t=1 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, 1.0) + assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t=1) failed: {result}" + print("+ Color.lerp(t=1) returns end color") + except Exception as e: + print(f"x Color.lerp(t=1) failed: {e}") + all_pass = False + + # Test 11: lerp at t=0.5 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, 0.5) + # Expect roughly (127, 0, 127) + assert 126 <= result.r <= 128 and result.g == 0 and 126 <= result.b <= 128, f"lerp(t=0.5) failed: {result}" + print("+ Color.lerp(t=0.5) returns midpoint") + except Exception as e: + print(f"x Color.lerp(t=0.5) failed: {e}") + all_pass = False + + # Test 12: lerp with alpha + try: + c1 = mcrfpy.Color(255, 0, 0, 255) + c2 = mcrfpy.Color(0, 255, 0, 0) + result = c1.lerp(c2, 0.5) + assert 126 <= result.r <= 128 and 126 <= result.g <= 128 and result.b == 0, f"lerp color components failed" + assert 126 <= result.a <= 128, f"lerp alpha failed: {result.a}" + print("+ Color.lerp() with alpha works") + except Exception as e: + print(f"x Color.lerp() with alpha failed: {e}") + all_pass = False + + # Test 13: lerp clamps t < 0 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, -0.5) + assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t<0) should clamp to 0" + print("+ Color.lerp() clamps t < 0") + except Exception as e: + print(f"x Color.lerp(t<0) failed: {e}") + all_pass = False + + # Test 14: lerp clamps t > 1 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, 1.5) + assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t>1) should clamp to 1" + print("+ Color.lerp() clamps t > 1") + except Exception as e: + print(f"x Color.lerp(t>1) failed: {e}") + all_pass = False + + # Test 15: Practical use case - gradient + try: + start = mcrfpy.Color.from_hex("#FF0000") # Red + end = mcrfpy.Color.from_hex("#0000FF") # Blue + + # Create 5-step gradient + steps = [] + for i in range(5): + t = i / 4.0 + color = start.lerp(end, t) + steps.append(color.to_hex()) + + assert steps[0] == "#FF0000", "Gradient start should be red" + assert steps[4] == "#0000FF", "Gradient end should be blue" + assert len(set(steps)) == 5, "All gradient steps should be unique" + + print("+ Gradient generation works correctly") + except Exception as e: + print(f"x Gradient generation failed: {e}") + all_pass = False + + print(f"\n{'PASS' if all_pass else 'FAIL'}") + sys.exit(0 if all_pass else 1) + +# Run test +mcrfpy.createScene("test") +mcrfpy.setTimer("test", test_color_helpers, 100) \ No newline at end of file diff --git a/tests/unit/test_color_operations.py b/tests/unit/test_color_operations.py new file mode 100644 index 0000000..61c278d --- /dev/null +++ b/tests/unit/test_color_operations.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Test if Color assignment is the trigger""" + +import mcrfpy + +print("Testing Color operations with range()...") +print("=" * 50) + +# Test 1: Basic Color assignment +print("Test 1: Color assignment in grid") +try: + mcrfpy.createScene("test1") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + + # Assign color to a cell + grid.at(0, 0).color = mcrfpy.Color(200, 200, 220) + print(" ✓ Single color assignment works") + + # Test range + for i in range(25): + pass + print(" ✓ range(25) works after single color assignment") + +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + +# Test 2: Multiple color assignments +print("\nTest 2: Multiple color assignments") +try: + mcrfpy.createScene("test2") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + + # Multiple properties including color + for y in range(15): + for x in range(25): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = mcrfpy.Color(200, 200, 220) + + print(" ✓ Completed all property assignments") + + # This is where it would fail + for i in range(25): + pass + print(" ✓ range(25) still works!") + +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +# Test 3: Exact reproduction of failing pattern +print("\nTest 3: Exact pattern from dijkstra_demo_final.py") +try: + # Recreate the exact function + def create_demo(): + mcrfpy.createScene("dijkstra_demo") + + # Create grid + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Initialize all as floor + for y in range(15): + for x in range(25): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = mcrfpy.Color(200, 200, 220) + + # Create an interesting dungeon layout + walls = [] + + # Room walls + # Top-left room + for x in range(1, 8): walls.append((x, 1)) + + return grid, walls + + grid, walls = create_demo() + print(f" ✓ Function completed successfully, walls: {walls}") + +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +print("\nConclusion: The bug is inconsistent and may be related to:") +print("- Memory layout at the time of execution") +print("- Specific bytecode patterns in the Python code") +print("- C++ reference counting issues with Color objects") +print("- Stack/heap corruption in the grid.at() implementation") \ No newline at end of file diff --git a/tests/unit/test_color_setter_bug.py b/tests/unit/test_color_setter_bug.py new file mode 100644 index 0000000..97b5b7d --- /dev/null +++ b/tests/unit/test_color_setter_bug.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Test that confirms the Color setter bug""" + +import mcrfpy + +print("Testing GridPoint color setter bug...") +print("=" * 50) + +# Test 1: Setting color with tuple (old way) +print("Test 1: Setting color with tuple") +try: + mcrfpy.createScene("test1") + grid = mcrfpy.Grid(grid_x=5, grid_y=5) + + # This should work (PyArg_ParseTuple expects tuple) + grid.at(0, 0).color = (200, 200, 220) + + # Check if exception is pending + _ = list(range(1)) + print(" ✓ Tuple assignment works") +except Exception as e: + print(f" ✗ Tuple assignment failed: {type(e).__name__}: {e}") + +print() + +# Test 2: Setting color with Color object (the bug) +print("Test 2: Setting color with Color object") +try: + mcrfpy.createScene("test2") + grid = mcrfpy.Grid(grid_x=5, grid_y=5) + + # This will fail in PyArg_ParseTuple but not report it + grid.at(0, 0).color = mcrfpy.Color(200, 200, 220) + print(" ⚠️ Color assignment appeared to work...") + + # But exception is pending! + _ = list(range(1)) + print(" ✓ No exception detected (unexpected!)") +except Exception as e: + print(f" ✗ Exception detected: {type(e).__name__}: {e}") + print(" This confirms the bug - exception was set but not raised") + +print() + +# Test 3: Multiple color assignments +print("Test 3: Multiple Color assignments (reproducing original bug)") +try: + mcrfpy.createScene("test3") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + + # Do multiple color assignments + for y in range(2): # Just 2 rows to be quick + for x in range(25): + grid.at(x, y).color = mcrfpy.Color(200, 200, 220) + + print(" All color assignments completed...") + + # This should fail + for i in range(25): + pass + print(" ✓ range(25) worked (unexpected!)") +except Exception as e: + print(f" ✗ range(25) failed as expected: {type(e).__name__}") + print(" The exception was set during color assignment") + +print() +print("Bug confirmed: PyObject_to_sfColor in UIGridPoint.cpp") +print("doesn't clear the exception when PyArg_ParseTuple fails.") +print("The fix: Either check PyErr_Occurred() after ParseTuple,") +print("or support mcrfpy.Color objects directly.") \ No newline at end of file diff --git a/tests/unit/test_constructor_comprehensive.py b/tests/unit/test_constructor_comprehensive.py new file mode 100644 index 0000000..ebacac8 --- /dev/null +++ b/tests/unit/test_constructor_comprehensive.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Comprehensive test of all constructor signatures""" + +import mcrfpy +import sys + +def test_frame_combinations(): + print("Testing Frame constructors...") + + # No args + f1 = mcrfpy.Frame() + assert f1.x == 0 and f1.y == 0 and f1.w == 0 and f1.h == 0 + + # Positional only + f2 = mcrfpy.Frame((10, 20), (100, 200)) + assert f2.x == 10 and f2.y == 20 and f2.w == 100 and f2.h == 200 + + # Mix positional and keyword + f3 = mcrfpy.Frame((5, 5), size=(50, 50), fill_color=(255, 0, 0), name="red_frame") + assert f3.x == 5 and f3.y == 5 and f3.w == 50 and f3.h == 50 and f3.name == "red_frame" + + # Keyword only + f4 = mcrfpy.Frame(x=15, y=25, w=150, h=250, outline=2.0, visible=True, opacity=0.5) + assert f4.x == 15 and f4.y == 25 and f4.w == 150 and f4.h == 250 + assert f4.outline == 2.0 and f4.visible and abs(f4.opacity - 0.5) < 0.0001 + + print("✓ Frame: all constructor variations work") + +def test_grid_combinations(): + print("Testing Grid constructors...") + + # No args (should default to 2x2) + g1 = mcrfpy.Grid() + assert g1.grid_x == 2 and g1.grid_y == 2 + + # Positional args + g2 = mcrfpy.Grid((0, 0), (320, 320), (10, 10)) + assert g2.x == 0 and g2.y == 0 and g2.grid_x == 10 and g2.grid_y == 10 + + # Mix with keywords + g3 = mcrfpy.Grid(pos=(50, 50), grid_x=20, grid_y=15, zoom=2.0, name="zoomed_grid") + assert g3.x == 50 and g3.y == 50 and g3.grid_x == 20 and g3.grid_y == 15 + assert g3.zoom == 2.0 and g3.name == "zoomed_grid" + + print("✓ Grid: all constructor variations work") + +def test_sprite_combinations(): + print("Testing Sprite constructors...") + + # No args + s1 = mcrfpy.Sprite() + assert s1.x == 0 and s1.y == 0 and s1.sprite_index == 0 + + # Positional with None texture + s2 = mcrfpy.Sprite((100, 150), None, 5) + assert s2.x == 100 and s2.y == 150 and s2.sprite_index == 5 + + # Keywords only + s3 = mcrfpy.Sprite(x=200, y=250, sprite_index=10, scale=2.0, name="big_sprite") + assert s3.x == 200 and s3.y == 250 and s3.sprite_index == 10 + assert s3.scale == 2.0 and s3.name == "big_sprite" + + # Scale variations + s4 = mcrfpy.Sprite(scale_x=2.0, scale_y=3.0) + assert s4.scale_x == 2.0 and s4.scale_y == 3.0 + + print("✓ Sprite: all constructor variations work") + +def test_caption_combinations(): + print("Testing Caption constructors...") + + # No args + c1 = mcrfpy.Caption() + assert c1.text == "" and c1.x == 0 and c1.y == 0 + + # Positional args + c2 = mcrfpy.Caption((50, 100), None, "Hello World") + assert c2.x == 50 and c2.y == 100 and c2.text == "Hello World" + + # Keywords only + c3 = mcrfpy.Caption(text="Test", font_size=24, fill_color=(0, 255, 0), name="green_text") + assert c3.text == "Test" and c3.font_size == 24 and c3.name == "green_text" + + # Mix positional and keywords + c4 = mcrfpy.Caption((10, 10), text="Mixed", outline=1.0, opacity=0.8) + assert c4.x == 10 and c4.y == 10 and c4.text == "Mixed" + assert c4.outline == 1.0 and abs(c4.opacity - 0.8) < 0.0001 + + print("✓ Caption: all constructor variations work") + +def test_entity_combinations(): + print("Testing Entity constructors...") + + # No args + e1 = mcrfpy.Entity() + assert e1.x == 0 and e1.y == 0 and e1.sprite_index == 0 + + # Positional args + e2 = mcrfpy.Entity((5, 10), None, 3) + assert e2.x == 5 and e2.y == 10 and e2.sprite_index == 3 + + # Keywords only + e3 = mcrfpy.Entity(x=15, y=20, sprite_index=7, name="player", visible=True) + assert e3.x == 15 and e3.y == 20 and e3.sprite_index == 7 + assert e3.name == "player" and e3.visible + + print("✓ Entity: all constructor variations work") + +def test_edge_cases(): + print("Testing edge cases...") + + # Empty strings + c = mcrfpy.Caption(text="", name="") + assert c.text == "" and c.name == "" + + # Zero values + f = mcrfpy.Frame(pos=(0, 0), size=(0, 0), opacity=0.0, z_index=0) + assert f.x == 0 and f.y == 0 and f.w == 0 and f.h == 0 + + # None values where allowed + s = mcrfpy.Sprite(texture=None) + c = mcrfpy.Caption(font=None) + e = mcrfpy.Entity(texture=None) + + print("✓ Edge cases: all handled correctly") + +# Run all tests +try: + test_frame_combinations() + test_grid_combinations() + test_sprite_combinations() + test_caption_combinations() + test_entity_combinations() + test_edge_cases() + + print("\n✅ All comprehensive constructor tests passed!") + sys.exit(0) + +except Exception as e: + print(f"\n❌ Test failed: {e}") + import traceback + traceback.print_exc() + sys.exit(1) \ No newline at end of file diff --git a/tests/unit/test_dijkstra_pathfinding.py b/tests/unit/test_dijkstra_pathfinding.py new file mode 100644 index 0000000..65ee1e6 --- /dev/null +++ b/tests/unit/test_dijkstra_pathfinding.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +""" +Test Dijkstra Pathfinding Implementation +======================================== + +Demonstrates: +1. Computing Dijkstra distance map from a root position +2. Getting distances to any position +3. Finding paths from any position back to the root +4. Multi-target pathfinding (flee/approach scenarios) +""" + +import mcrfpy +from mcrfpy import libtcod +import sys + +def create_test_grid(): + """Create a test grid with obstacles""" + mcrfpy.createScene("dijkstra_test") + + # Create grid + grid = mcrfpy.Grid(grid_x=20, grid_y=20) + + # Initialize all cells as walkable + for y in range(grid.grid_y): + for x in range(grid.grid_x): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.tilesprite = 46 # . period + cell.color = mcrfpy.Color(50, 50, 50) + + # Create some walls to make pathfinding interesting + # Vertical wall + for y in range(5, 15): + cell = grid.at(10, y) + cell.walkable = False + cell.transparent = False + cell.tilesprite = 219 # Block + cell.color = mcrfpy.Color(100, 100, 100) + + # Horizontal wall + for x in range(5, 15): + if x != 10: # Leave a gap + cell = grid.at(x, 10) + cell.walkable = False + cell.transparent = False + cell.tilesprite = 219 + cell.color = mcrfpy.Color(100, 100, 100) + + return grid + +def test_basic_dijkstra(): + """Test basic Dijkstra functionality""" + print("\n=== Testing Basic Dijkstra ===") + + grid = create_test_grid() + + # Compute Dijkstra map from position (5, 5) + root_x, root_y = 5, 5 + print(f"Computing Dijkstra map from root ({root_x}, {root_y})") + grid.compute_dijkstra(root_x, root_y) + + # Test getting distances to various points + test_points = [ + (5, 5), # Root position (should be 0) + (6, 5), # Adjacent (should be 1) + (7, 5), # Two steps away + (15, 15), # Far corner + (10, 10), # On a wall (should be unreachable) + ] + + print("\nDistances from root:") + for x, y in test_points: + distance = grid.get_dijkstra_distance(x, y) + if distance is None: + print(f" ({x:2}, {y:2}): UNREACHABLE") + else: + print(f" ({x:2}, {y:2}): {distance:.1f}") + + # Test getting paths + print("\nPaths to root:") + for x, y in [(15, 5), (15, 15), (5, 15)]: + path = grid.get_dijkstra_path(x, y) + if path: + print(f" From ({x}, {y}): {len(path)} steps") + # Show first few steps + for i, (px, py) in enumerate(path[:3]): + print(f" Step {i+1}: ({px}, {py})") + if len(path) > 3: + print(f" ... {len(path)-3} more steps") + else: + print(f" From ({x}, {y}): No path found") + +def test_libtcod_interface(): + """Test the libtcod module interface""" + print("\n=== Testing libtcod Interface ===") + + grid = create_test_grid() + + # Use libtcod functions + print("Using libtcod.dijkstra_* functions:") + + # Create dijkstra context (returns grid) + dijkstra = libtcod.dijkstra_new(grid) + print(f"Created Dijkstra context: {type(dijkstra)}") + + # Compute from a position + libtcod.dijkstra_compute(grid, 10, 2) + print("Computed Dijkstra map from (10, 2)") + + # Get distance using libtcod + distance = libtcod.dijkstra_get_distance(grid, 10, 17) + print(f"Distance to (10, 17): {distance}") + + # Get path using libtcod + path = libtcod.dijkstra_path_to(grid, 10, 17) + print(f"Path from (10, 17) to root: {len(path) if path else 0} steps") + +def test_multi_target_scenario(): + """Test fleeing/approaching multiple targets""" + print("\n=== Testing Multi-Target Scenario ===") + + grid = create_test_grid() + + # Place three "threats" and compute their Dijkstra maps + threats = [(3, 3), (17, 3), (10, 17)] + + print("Computing threat distances...") + threat_distances = [] + + for i, (tx, ty) in enumerate(threats): + # Mark threat position + cell = grid.at(tx, ty) + cell.tilesprite = 84 # T for threat + cell.color = mcrfpy.Color(255, 0, 0) + + # Compute Dijkstra from this threat + grid.compute_dijkstra(tx, ty) + + # Store distances for all cells + distances = {} + for y in range(grid.grid_y): + for x in range(grid.grid_x): + d = grid.get_dijkstra_distance(x, y) + if d is not None: + distances[(x, y)] = d + + threat_distances.append(distances) + print(f" Threat {i+1} at ({tx}, {ty}): {len(distances)} reachable cells") + + # Find safest position (farthest from all threats) + print("\nFinding safest position...") + best_pos = None + best_min_dist = 0 + + for y in range(grid.grid_y): + for x in range(grid.grid_x): + # Skip if not walkable + if not grid.at(x, y).walkable: + continue + + # Get minimum distance to any threat + min_dist = float('inf') + for threat_dist in threat_distances: + if (x, y) in threat_dist: + min_dist = min(min_dist, threat_dist[(x, y)]) + + # Track best position + if min_dist > best_min_dist and min_dist != float('inf'): + best_min_dist = min_dist + best_pos = (x, y) + + if best_pos: + print(f"Safest position: {best_pos} (min distance to threats: {best_min_dist:.1f})") + # Mark safe position + cell = grid.at(best_pos[0], best_pos[1]) + cell.tilesprite = 83 # S for safe + cell.color = mcrfpy.Color(0, 255, 0) + +def run_test(runtime): + """Timer callback to run tests after scene loads""" + test_basic_dijkstra() + test_libtcod_interface() + test_multi_target_scenario() + + print("\n=== Dijkstra Implementation Test Complete ===") + print("✓ Basic Dijkstra computation works") + print("✓ Distance queries work") + print("✓ Path finding works") + print("✓ libtcod interface works") + print("✓ Multi-target scenarios work") + + # Take screenshot + try: + from mcrfpy import automation + automation.screenshot("dijkstra_test.png") + print("\nScreenshot saved: dijkstra_test.png") + except: + pass + + sys.exit(0) + +# Main execution +print("McRogueFace Dijkstra Pathfinding Test") +print("=====================================") + +# Set up scene +grid = create_test_grid() +ui = mcrfpy.sceneUI("dijkstra_test") +ui.append(grid) + +# Add title +title = mcrfpy.Caption("Dijkstra Pathfinding Test", 10, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Set timer to run tests +mcrfpy.setTimer("test", run_test, 100) + +# Show scene +mcrfpy.setScene("dijkstra_test") \ No newline at end of file diff --git a/tests/unit/test_documentation.py b/tests/unit/test_documentation.py new file mode 100644 index 0000000..961a417 --- /dev/null +++ b/tests/unit/test_documentation.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Test that method documentation is properly accessible in Python.""" + +import mcrfpy +import sys + +def test_module_doc(): + """Test module-level documentation.""" + print("=== Module Documentation ===") + print(f"Module: {mcrfpy.__name__}") + print(f"Doc: {mcrfpy.__doc__[:100]}..." if mcrfpy.__doc__ else "No module doc") + print() + +def test_method_docs(): + """Test method documentation.""" + print("=== Method Documentation ===") + + # Test main API methods + methods = [ + 'createSoundBuffer', 'loadMusic', 'setMusicVolume', 'setSoundVolume', + 'playSound', 'getMusicVolume', 'getSoundVolume', 'sceneUI', + 'currentScene', 'setScene', 'createScene', 'keypressScene', + 'setTimer', 'delTimer', 'exit', 'setScale', 'find', 'findAll', + 'getMetrics' + ] + + for method_name in methods: + if hasattr(mcrfpy, method_name): + method = getattr(mcrfpy, method_name) + doc = method.__doc__ + if doc: + # Extract first line of docstring + first_line = doc.strip().split('\n')[0] + print(f"{method_name}: {first_line}") + else: + print(f"{method_name}: NO DOCUMENTATION") + print() + +def test_class_docs(): + """Test class documentation.""" + print("=== Class Documentation ===") + + classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity', 'Color', 'Vector', 'Texture', 'Font'] + + for class_name in classes: + if hasattr(mcrfpy, class_name): + cls = getattr(mcrfpy, class_name) + doc = cls.__doc__ + if doc: + # Extract first line + first_line = doc.strip().split('\n')[0] + print(f"{class_name}: {first_line[:80]}...") + else: + print(f"{class_name}: NO DOCUMENTATION") + print() + +def test_property_docs(): + """Test property documentation.""" + print("=== Property Documentation ===") + + # Test Frame properties + if hasattr(mcrfpy, 'Frame'): + frame_props = ['x', 'y', 'w', 'h', 'fill_color', 'outline_color', 'outline', 'children', 'visible', 'z_index'] + print("Frame properties:") + for prop_name in frame_props: + prop = getattr(mcrfpy.Frame, prop_name, None) + if prop and hasattr(prop, '__doc__'): + print(f" {prop_name}: {prop.__doc__}") + print() + +def test_method_signatures(): + """Test that methods have correct signatures in docs.""" + print("=== Method Signatures ===") + + # Check a few key methods + if hasattr(mcrfpy, 'setScene'): + doc = mcrfpy.setScene.__doc__ + if doc and 'setScene(scene: str, transition: str = None, duration: float = 0.0)' in doc: + print("✓ setScene signature correct") + else: + print("✗ setScene signature incorrect or missing") + + if hasattr(mcrfpy, 'setTimer'): + doc = mcrfpy.setTimer.__doc__ + if doc and 'setTimer(name: str, handler: callable, interval: int)' in doc: + print("✓ setTimer signature correct") + else: + print("✗ setTimer signature incorrect or missing") + + if hasattr(mcrfpy, 'find'): + doc = mcrfpy.find.__doc__ + if doc and 'find(name: str, scene: str = None)' in doc: + print("✓ find signature correct") + else: + print("✗ find signature incorrect or missing") + print() + +def test_help_output(): + """Test Python help() function output.""" + print("=== Help Function Test ===") + print("Testing help(mcrfpy.setScene):") + import io + import contextlib + + # Capture help output + buffer = io.StringIO() + with contextlib.redirect_stdout(buffer): + help(mcrfpy.setScene) + + help_text = buffer.getvalue() + if 'transition to a different scene' in help_text: + print("✓ Help text contains expected documentation") + else: + print("✗ Help text missing expected documentation") + print() + +def main(): + """Run all documentation tests.""" + print("McRogueFace Documentation Tests") + print("===============================\n") + + test_module_doc() + test_method_docs() + test_class_docs() + test_property_docs() + test_method_signatures() + test_help_output() + + print("\nDocumentation tests complete!") + sys.exit(0) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tests/unit/test_empty_animation_manager.py b/tests/unit/test_empty_animation_manager.py new file mode 100644 index 0000000..c86905a --- /dev/null +++ b/tests/unit/test_empty_animation_manager.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +""" +Test if AnimationManager crashes with no animations +""" + +import mcrfpy + +print("Creating empty scene...") +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +print("Scene created, no animations added") +print("Starting game loop in 100ms...") + +def check_alive(runtime): + print(f"Timer fired at {runtime}ms - AnimationManager survived!") + mcrfpy.setTimer("exit", lambda t: mcrfpy.exit(), 100) + +mcrfpy.setTimer("check", check_alive, 1000) +print("If this crashes immediately, AnimationManager has an issue with empty state") \ No newline at end of file diff --git a/tests/unit/test_entity_animation.py b/tests/unit/test_entity_animation.py new file mode 100644 index 0000000..342f340 --- /dev/null +++ b/tests/unit/test_entity_animation.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +""" +Test Entity Animation +==================== + +Isolated test for entity position animation. +No perspective, just basic movement in a square pattern. +""" + +import mcrfpy +import sys + +# Create scene +mcrfpy.createScene("test_anim") + +# Create simple grid +grid = mcrfpy.Grid(grid_x=15, grid_y=15) +grid.fill_color = mcrfpy.Color(20, 20, 30) + +# Initialize all cells as walkable floors +for y in range(15): + for x in range(15): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(100, 100, 120) + +# Mark the path we'll follow with different color +path_cells = [(5,5), (6,5), (7,5), (8,5), (9,5), (10,5), + (10,6), (10,7), (10,8), (10,9), (10,10), + (9,10), (8,10), (7,10), (6,10), (5,10), + (5,9), (5,8), (5,7), (5,6)] + +for x, y in path_cells: + cell = grid.at(x, y) + cell.color = mcrfpy.Color(120, 120, 150) + +# Create entity at start position +entity = mcrfpy.Entity(5, 5, grid=grid) +entity.sprite_index = 64 # @ + +# UI setup +ui = mcrfpy.sceneUI("test_anim") +ui.append(grid) +grid.position = (100, 100) +grid.size = (450, 450) # 15 * 30 pixels per cell + +# Title +title = mcrfpy.Caption("Entity Animation Test - Square Path", 200, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Status display +status = mcrfpy.Caption("Press SPACE to start animation | Q to quit", 100, 50) +status.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(status) + +# Position display +pos_display = mcrfpy.Caption(f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})", 100, 70) +pos_display.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(pos_display) + +# Animation info +anim_info = mcrfpy.Caption("Animation: Not started", 400, 70) +anim_info.fill_color = mcrfpy.Color(100, 255, 255) +ui.append(anim_info) + +# Debug info +debug_info = mcrfpy.Caption("Debug: Waiting...", 100, 570) +debug_info.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(debug_info) + +# Animation state +current_waypoint = 0 +animating = False +waypoints = [(5,5), (10,5), (10,10), (5,10), (5,5)] + +def update_position_display(dt): + """Update position display every 200ms""" + pos_display.text = f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})" + + # Check if entity is at expected position + if animating and current_waypoint > 0: + target = waypoints[current_waypoint - 1] + distance = ((entity.x - target[0])**2 + (entity.y - target[1])**2)**0.5 + debug_info.text = f"Debug: Distance to target {target}: {distance:.3f}" + +def animate_to_next_waypoint(): + """Animate to the next waypoint""" + global current_waypoint, animating + + if current_waypoint >= len(waypoints): + status.text = "Animation complete! Press SPACE to restart" + anim_info.text = "Animation: Complete" + animating = False + current_waypoint = 0 + return + + target_x, target_y = waypoints[current_waypoint] + + # Log what we're doing + print(f"Animating from ({entity.x}, {entity.y}) to ({target_x}, {target_y})") + + # Update status + status.text = f"Moving to waypoint {current_waypoint + 1}/{len(waypoints)}: ({target_x}, {target_y})" + anim_info.text = f"Animation: Active (target: {target_x}, {target_y})" + + # Create animations - ensure we're using floats + duration = 2.0 # 2 seconds per segment + + # Try different approaches to see what works + + # Approach 1: Direct property animation + anim_x = mcrfpy.Animation("x", float(target_x), duration, "linear") + anim_y = mcrfpy.Animation("y", float(target_y), duration, "linear") + + # Start animations + anim_x.start(entity) + anim_y.start(entity) + + # Log animation details + print(f"Started animations: x to {float(target_x)}, y to {float(target_y)}, duration: {duration}s") + + current_waypoint += 1 + + # Schedule next waypoint + mcrfpy.setTimer("next_waypoint", lambda dt: animate_to_next_waypoint(), int(duration * 1000 + 100)) + +def start_animation(): + """Start or restart the animation sequence""" + global current_waypoint, animating + + # Reset entity position + entity.x = 5 + entity.y = 5 + + # Reset state + current_waypoint = 0 + animating = True + + print("Starting animation sequence...") + + # Start first animation + animate_to_next_waypoint() + +def test_immediate_position(): + """Test setting position directly""" + print(f"Before: entity at ({entity.x}, {entity.y})") + entity.x = 7 + entity.y = 7 + print(f"After direct set: entity at ({entity.x}, {entity.y})") + + # Try with animation to same position + anim_x = mcrfpy.Animation("x", 9.0, 1.0, "linear") + anim_x.start(entity) + print("Started animation to x=9.0") + +# Input handler +def handle_input(key, state): + if state != "start": + return + + key = key.lower() + + if key == "q": + print("Exiting test...") + sys.exit(0) + elif key == "space": + if not animating: + start_animation() + else: + print("Animation already in progress!") + elif key == "t": + # Test immediate position change + test_immediate_position() + elif key == "r": + # Reset position + entity.x = 5 + entity.y = 5 + print(f"Reset entity to ({entity.x}, {entity.y})") + +# Set scene +mcrfpy.setScene("test_anim") +mcrfpy.keypressScene(handle_input) + +# Start position update timer +mcrfpy.setTimer("update_pos", update_position_display, 200) + +# No perspective (omniscient view) +grid.perspective = -1 + +print("Entity Animation Test") +print("====================") +print("This test animates an entity in a square pattern:") +print("(5,5) → (10,5) → (10,10) → (5,10) → (5,5)") +print() +print("Controls:") +print(" SPACE - Start animation") +print(" T - Test immediate position change") +print(" R - Reset position to (5,5)") +print(" Q - Quit") +print() +print("The position display updates every 200ms") +print("Watch the console for animation logs") \ No newline at end of file diff --git a/tests/unit/test_entity_collection_remove.py b/tests/unit/test_entity_collection_remove.py new file mode 100644 index 0000000..0ae8068 --- /dev/null +++ b/tests/unit/test_entity_collection_remove.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Test for UIEntityCollection.remove() accepting Entity instances +Tests the new behavior where remove() takes an Entity object instead of an index +""" + +import mcrfpy +import sys + +def test_remove_by_entity(): + """Test removing entities by passing the entity object""" + + # Create a test scene and grid + scene_name = "test_entity_remove" + mcrfpy.createScene(scene_name) + + # Create a grid (entities need a grid) + grid = mcrfpy.Grid() # Default 2x2 grid is fine for testing + mcrfpy.sceneUI(scene_name).append(grid) + + # Get the entity collection + entities = grid.entities + + # Create some test entities + # Entity() creates entities with default position (0,0) + entity1 = mcrfpy.Entity() + entity1.x = 5 + entity1.y = 5 + + entity2 = mcrfpy.Entity() + entity2.x = 10 + entity2.y = 10 + + entity3 = mcrfpy.Entity() + entity3.x = 15 + entity3.y = 15 + + # Add entities to the collection + entities.append(entity1) + entities.append(entity2) + entities.append(entity3) + + print(f"Initial entity count: {len(entities)}") + assert len(entities) == 3, "Should have 3 entities" + + # Test 1: Remove an entity that exists + print("\nTest 1: Remove existing entity") + entities.remove(entity2) + assert len(entities) == 2, "Should have 2 entities after removal" + assert entity1 in entities, "Entity1 should still be in collection" + assert entity2 not in entities, "Entity2 should not be in collection" + assert entity3 in entities, "Entity3 should still be in collection" + print("✓ Successfully removed entity2") + + # Test 2: Try to remove an entity that's not in the collection + print("\nTest 2: Remove non-existent entity") + try: + entities.remove(entity2) # Already removed + assert False, "Should have raised ValueError" + except ValueError as e: + print(f"✓ Got expected ValueError: {e}") + + # Test 3: Try to remove with wrong type + print("\nTest 3: Remove with wrong type") + try: + entities.remove(42) # Not an Entity + assert False, "Should have raised TypeError" + except TypeError as e: + print(f"✓ Got expected TypeError: {e}") + + # Test 4: Try to remove with None + print("\nTest 4: Remove with None") + try: + entities.remove(None) + assert False, "Should have raised TypeError" + except TypeError as e: + print(f"✓ Got expected TypeError: {e}") + + # Test 5: Verify grid property is cleared (C++ internal) + print("\nTest 5: Grid property handling") + # Create a new entity and add it + entity4 = mcrfpy.Entity() + entity4.x = 20 + entity4.y = 20 + entities.append(entity4) + # Note: grid property is managed internally in C++ and not exposed to Python + + # Remove it - this clears the C++ grid reference internally + entities.remove(entity4) + print("✓ Grid property handling (managed internally in C++)") + + # Test 6: Remove all entities one by one + print("\nTest 6: Remove all entities") + entities.remove(entity1) + entities.remove(entity3) + assert len(entities) == 0, "Collection should be empty" + print("✓ Successfully removed all entities") + + print("\n✅ All tests passed!") + return True + +if __name__ == "__main__": + try: + success = test_remove_by_entity() + sys.exit(0 if success else 1) + except Exception as e: + print(f"\n❌ Test failed with exception: {e}") + import traceback + traceback.print_exc() + sys.exit(1) \ No newline at end of file diff --git a/tests/unit/test_entity_constructor.py b/tests/unit/test_entity_constructor.py new file mode 100644 index 0000000..56f9463 --- /dev/null +++ b/tests/unit/test_entity_constructor.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import mcrfpy + +# Create scene and grid +mcrfpy.createScene("test") +ui = mcrfpy.sceneUI("test") + +# Create texture and grid +texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) +grid = mcrfpy.Grid(5, 5, texture) +ui.append(grid) + +# Test Entity constructor +try: + # Based on usage in ui_Grid_test.py + entity = mcrfpy.Entity(mcrfpy.Vector(2, 2), texture, 84, grid) + print("Entity created with 4 args: position, texture, sprite_index, grid") +except Exception as e: + print(f"4 args failed: {e}") + try: + # Maybe it's just position, texture, sprite_index + entity = mcrfpy.Entity((2, 2), texture, 84) + print("Entity created with 3 args: position, texture, sprite_index") + except Exception as e2: + print(f"3 args failed: {e2}") + +mcrfpy.exit() \ No newline at end of file diff --git a/tests/unit/test_entity_fix.py b/tests/unit/test_entity_fix.py new file mode 100644 index 0000000..90a660d --- /dev/null +++ b/tests/unit/test_entity_fix.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Test Entity Animation Fix +========================= + +This test demonstrates the issue and proposes a fix. +The problem: UIEntity::setProperty updates sprite position incorrectly. +""" + +import mcrfpy +import sys + +print("Entity Animation Fix Test") +print("========================") +print() +print("ISSUE: When animating entity x/y properties, the sprite position") +print("is being set to grid coordinates instead of pixel coordinates.") +print() +print("In UIEntity::setProperty (lines 562 & 568):") +print(" sprite.setPosition(sf::Vector2f(position.x, position.y));") +print() +print("This should be removed because UIGrid::render() calculates") +print("the correct pixel position based on grid coordinates, zoom, etc.") +print() +print("FIX: Comment out or remove the sprite.setPosition calls in") +print("UIEntity::setProperty for 'x' and 'y' properties.") +print() + +# Create scene to demonstrate +mcrfpy.createScene("fix_demo") + +# Create grid +grid = mcrfpy.Grid(grid_x=15, grid_y=10) +grid.fill_color = mcrfpy.Color(20, 20, 30) + +# Make floor +for y in range(10): + for x in range(15): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(100, 100, 120) + +# Create entity +entity = mcrfpy.Entity(2, 2, grid=grid) +entity.sprite_index = 64 # @ + +# UI +ui = mcrfpy.sceneUI("fix_demo") +ui.append(grid) +grid.position = (100, 150) +grid.size = (450, 300) + +# Info displays +title = mcrfpy.Caption("Entity Animation Issue Demo", 250, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +pos_info = mcrfpy.Caption("", 100, 50) +pos_info.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(pos_info) + +sprite_info = mcrfpy.Caption("", 100, 70) +sprite_info.fill_color = mcrfpy.Color(255, 100, 100) +ui.append(sprite_info) + +status = mcrfpy.Caption("Press SPACE to animate entity", 100, 100) +status.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(status) + +# Update display +def update_display(dt): + pos_info.text = f"Entity Grid Position: ({entity.x:.2f}, {entity.y:.2f})" + # We can't access sprite position from Python, but in C++ it would show + # the issue: sprite position would be (2, 2) instead of pixel coords + sprite_info.text = "Sprite position is incorrectly set to grid coords (see C++ code)" + +# Test animation +def test_animation(): + """Animate entity to show the issue""" + print("\nAnimating entity from (2,2) to (10,5)") + + # This animation will cause the sprite to appear at wrong position + # because setProperty sets sprite.position to (10, 5) instead of + # letting the grid calculate pixel position + anim_x = mcrfpy.Animation("x", 10.0, 2.0, "easeInOut") + anim_y = mcrfpy.Animation("y", 5.0, 2.0, "easeInOut") + + anim_x.start(entity) + anim_y.start(entity) + + status.text = "Animating... Entity may appear at wrong position!" + +# Input handler +def handle_input(key, state): + if state != "start": + return + + key = key.lower() + + if key == "q": + sys.exit(0) + elif key == "space": + test_animation() + elif key == "r": + entity.x = 2 + entity.y = 2 + status.text = "Reset entity to (2,2)" + +# Setup +mcrfpy.setScene("fix_demo") +mcrfpy.keypressScene(handle_input) +mcrfpy.setTimer("update", update_display, 100) + +print("Ready to demonstrate the issue.") +print() +print("The fix is to remove these lines from UIEntity::setProperty:") +print(" Line 562: sprite.setPosition(sf::Vector2f(position.x, position.y));") +print(" Line 568: sprite.setPosition(sf::Vector2f(position.x, position.y));") +print() +print("Controls:") +print(" SPACE - Animate entity (will show incorrect behavior)") +print(" R - Reset position") +print(" Q - Quit") \ No newline at end of file diff --git a/tests/unit/test_entity_path_to.py b/tests/unit/test_entity_path_to.py new file mode 100644 index 0000000..eab54d4 --- /dev/null +++ b/tests/unit/test_entity_path_to.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Test the new Entity.path_to() method""" + +import mcrfpy + +print("Testing Entity.path_to() method...") +print("=" * 50) + +# Create scene and grid +mcrfpy.createScene("path_test") +grid = mcrfpy.Grid(grid_x=10, grid_y=10) + +# Set up a simple map with some walls +for y in range(10): + for x in range(10): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + +# Add some walls to create an interesting path +walls = [(3, 3), (3, 4), (3, 5), (4, 3), (5, 3)] +for x, y in walls: + grid.at(x, y).walkable = False + +# Create entity +entity = mcrfpy.Entity(2, 2) +grid.entities.append(entity) + +print(f"Entity at: ({entity.x}, {entity.y})") + +# Test 1: Simple path +print("\nTest 1: Path to (6, 6)") +try: + path = entity.path_to(6, 6) + print(f" Path: {path}") + print(f" Length: {len(path)} steps") + print(" ✓ SUCCESS") +except Exception as e: + print(f" ✗ FAILED: {e}") + +# Test 2: Path with target_x/target_y keywords +print("\nTest 2: Path using keyword arguments") +try: + path = entity.path_to(target_x=7, target_y=7) + print(f" Path: {path}") + print(f" Length: {len(path)} steps") + print(" ✓ SUCCESS") +except Exception as e: + print(f" ✗ FAILED: {e}") + +# Test 3: Path to unreachable location +print("\nTest 3: Path to current position") +try: + path = entity.path_to(2, 2) + print(f" Path: {path}") + print(f" Length: {len(path)} steps") + print(" ✓ SUCCESS") +except Exception as e: + print(f" ✗ FAILED: {e}") + +# Test 4: Error cases +print("\nTest 4: Error handling") +try: + # Out of bounds + path = entity.path_to(15, 15) + print(" ✗ Should have failed for out of bounds") +except ValueError as e: + print(f" ✓ Correctly caught out of bounds: {e}") +except Exception as e: + print(f" ✗ Wrong exception type: {e}") + +print("\n" + "=" * 50) +print("Entity.path_to() testing complete!") \ No newline at end of file diff --git a/tests/unit/test_entity_path_to_edge_cases.py b/tests/unit/test_entity_path_to_edge_cases.py new file mode 100644 index 0000000..f255aca --- /dev/null +++ b/tests/unit/test_entity_path_to_edge_cases.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Test edge cases for Entity.path_to() method""" + +import mcrfpy + +print("Testing Entity.path_to() edge cases...") +print("=" * 50) + +# Test 1: Entity without grid +print("Test 1: Entity not in grid") +try: + entity = mcrfpy.Entity(5, 5) + path = entity.path_to(8, 8) + print(" ✗ Should have failed for entity not in grid") +except ValueError as e: + print(f" ✓ Correctly caught no grid error: {e}") +except Exception as e: + print(f" ✗ Wrong exception type: {e}") + +# Test 2: Entity in grid with walls blocking path +print("\nTest 2: Completely blocked path") +mcrfpy.createScene("blocked_test") +grid = mcrfpy.Grid(grid_x=5, grid_y=5) + +# Make all tiles walkable first +for y in range(5): + for x in range(5): + grid.at(x, y).walkable = True + +# Create a wall that completely blocks the path +for x in range(5): + grid.at(x, 2).walkable = False + +entity = mcrfpy.Entity(1, 1) +grid.entities.append(entity) + +try: + path = entity.path_to(1, 4) + if path: + print(f" Path found: {path}") + else: + print(" ✓ No path found (empty list returned)") +except Exception as e: + print(f" ✗ Unexpected error: {e}") + +# Test 3: Alternative parameter parsing +print("\nTest 3: Alternative parameter names") +try: + path = entity.path_to(x=3, y=1) + print(f" Path with x/y params: {path}") + print(" ✓ SUCCESS") +except Exception as e: + print(f" ✗ FAILED: {e}") + +print("\n" + "=" * 50) +print("Edge case testing complete!") \ No newline at end of file diff --git a/tests/unit/test_exact_failure.py b/tests/unit/test_exact_failure.py new file mode 100644 index 0000000..b4e5924 --- /dev/null +++ b/tests/unit/test_exact_failure.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Reproduce the exact failure from dijkstra_demo_final.py""" + +import mcrfpy + +print("Reproducing exact failure pattern...") +print("=" * 50) + +# Colors +WALL_COLOR = mcrfpy.Color(60, 30, 30) +FLOOR_COLOR = mcrfpy.Color(200, 200, 220) + +def test_exact_pattern(): + """Exact code from dijkstra_demo_final.py""" + mcrfpy.createScene("dijkstra_demo") + + # Create grid + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Initialize all as floor + for y in range(15): + for x in range(25): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = FLOOR_COLOR + + # Create an interesting dungeon layout + walls = [] + + # Room walls + # Top-left room + for x in range(1, 8): walls.append((x, 1)) + + return grid, walls + +print("Test 1: Running exact pattern...") +try: + grid, walls = test_exact_pattern() + print(f" ✓ Success! Created {len(walls)} walls") +except Exception as e: + print(f" ✗ Failed: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +print() +print("Test 2: Breaking it down step by step...") + +# Step 1: Scene and grid +try: + mcrfpy.createScene("test2") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + print(" ✓ Step 1: Scene and grid created") +except Exception as e: + print(f" ✗ Step 1 failed: {e}") + +# Step 2: Set fill_color +try: + grid.fill_color = mcrfpy.Color(0, 0, 0) + print(" ✓ Step 2: fill_color set") +except Exception as e: + print(f" ✗ Step 2 failed: {e}") + +# Step 3: Nested loops with grid.at +try: + for y in range(15): + for x in range(25): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = FLOOR_COLOR + print(" ✓ Step 3: Nested loops completed") +except Exception as e: + print(f" ✗ Step 3 failed: {e}") + +# Step 4: Create walls list +try: + walls = [] + print(" ✓ Step 4: walls list created") +except Exception as e: + print(f" ✗ Step 4 failed: {e}") + +# Step 5: The failing line +try: + for x in range(1, 8): walls.append((x, 1)) + print(f" ✓ Step 5: For loop worked, walls = {walls}") +except Exception as e: + print(f" ✗ Step 5 failed: {type(e).__name__}: {e}") + + # Check if exception was already pending + import sys + exc_info = sys.exc_info() + print(f" Exception info: {exc_info}") + +print() +print("The error occurs at step 5, suggesting an exception was") +print("set during the nested loops but not immediately raised.") \ No newline at end of file diff --git a/tests/unit/test_frame_clipping.py b/tests/unit/test_frame_clipping.py new file mode 100644 index 0000000..48cad99 --- /dev/null +++ b/tests/unit/test_frame_clipping.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Test UIFrame clipping functionality""" + +import mcrfpy +from mcrfpy import Color, Frame, Caption, Vector +import sys + +def test_clipping(runtime): + """Test that clip_children property works correctly""" + mcrfpy.delTimer("test_clipping") + + print("Testing UIFrame clipping functionality...") + + # Create test scene + scene = mcrfpy.sceneUI("test") + + # Create parent frame with clipping disabled (default) + parent1 = Frame(50, 50, 200, 150, + fill_color=Color(100, 100, 200), + outline_color=Color(255, 255, 255), + outline=2) + parent1.name = "parent1" + scene.append(parent1) + + # Create parent frame with clipping enabled + parent2 = Frame(300, 50, 200, 150, + fill_color=Color(200, 100, 100), + outline_color=Color(255, 255, 255), + outline=2) + parent2.name = "parent2" + parent2.clip_children = True + scene.append(parent2) + + # Add captions to both frames + caption1 = Caption(10, 10, "This text should overflow the frame bounds") + caption1.font_size = 16 + caption1.fill_color = Color(255, 255, 255) + parent1.children.append(caption1) + + caption2 = Caption(10, 10, "This text should be clipped to frame bounds") + caption2.font_size = 16 + caption2.fill_color = Color(255, 255, 255) + parent2.children.append(caption2) + + # Add child frames that extend beyond parent bounds + child1 = Frame(150, 100, 100, 100, + fill_color=Color(50, 255, 50), + outline_color=Color(0, 0, 0), + outline=1) + parent1.children.append(child1) + + child2 = Frame(150, 100, 100, 100, + fill_color=Color(50, 255, 50), + outline_color=Color(0, 0, 0), + outline=1) + parent2.children.append(child2) + + # Add caption to show clip state + status = Caption(50, 250, + f"Left frame: clip_children={parent1.clip_children}\n" + f"Right frame: clip_children={parent2.clip_children}") + status.font_size = 14 + status.fill_color = Color(255, 255, 255) + scene.append(status) + + # Add instructions + instructions = Caption(50, 300, + "Left: Children should overflow (no clipping)\n" + "Right: Children should be clipped to frame bounds\n" + "Press 'c' to toggle clipping on left frame") + instructions.font_size = 12 + instructions.fill_color = Color(200, 200, 200) + scene.append(instructions) + + # Take screenshot + from mcrfpy import Window, automation + automation.screenshot("frame_clipping_test.png") + + print(f"Parent1 clip_children: {parent1.clip_children}") + print(f"Parent2 clip_children: {parent2.clip_children}") + + # Test toggling clip_children + parent1.clip_children = True + print(f"After toggle - Parent1 clip_children: {parent1.clip_children}") + + # Verify the property setter works + try: + parent1.clip_children = "not a bool" # Should raise TypeError + print("ERROR: clip_children accepted non-boolean value") + except TypeError as e: + print(f"PASS: clip_children correctly rejected non-boolean: {e}") + + # Test with animations + def animate_frames(runtime): + mcrfpy.delTimer("animate") + # Animate child frames to show clipping in action + # Note: For now, just move the frames manually to demonstrate clipping + parent1.children[1].x = 50 # Move child frame + parent2.children[1].x = 50 # Move child frame + + # Take another screenshot after starting animation + mcrfpy.setTimer("screenshot2", take_second_screenshot, 500) + + def take_second_screenshot(runtime): + mcrfpy.delTimer("screenshot2") + automation.screenshot("frame_clipping_animated.png") + print("\nTest completed successfully!") + print("Screenshots saved:") + print(" - frame_clipping_test.png (initial state)") + print(" - frame_clipping_animated.png (with animation)") + sys.exit(0) + + # Start animation after a short delay + mcrfpy.setTimer("animate", animate_frames, 100) + +# Main execution +print("Creating test scene...") +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Set up keyboard handler to toggle clipping +def handle_keypress(key, modifiers): + if key == "c": + scene = mcrfpy.sceneUI("test") + parent1 = scene[0] # First frame + parent1.clip_children = not parent1.clip_children + print(f"Toggled parent1 clip_children to: {parent1.clip_children}") + +mcrfpy.keypressScene(handle_keypress) + +# Schedule the test +mcrfpy.setTimer("test_clipping", test_clipping, 100) + +print("Test scheduled, running...") \ No newline at end of file diff --git a/tests/unit/test_frame_clipping_advanced.py b/tests/unit/test_frame_clipping_advanced.py new file mode 100644 index 0000000..3c3d324 --- /dev/null +++ b/tests/unit/test_frame_clipping_advanced.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Advanced test for UIFrame clipping with nested frames""" + +import mcrfpy +from mcrfpy import Color, Frame, Caption, Vector +import sys + +def test_nested_clipping(runtime): + """Test nested frames with clipping""" + mcrfpy.delTimer("test_nested_clipping") + + print("Testing advanced UIFrame clipping with nested frames...") + + # Create test scene + scene = mcrfpy.sceneUI("test") + + # Create outer frame with clipping enabled + outer = Frame(50, 50, 400, 300, + fill_color=Color(50, 50, 150), + outline_color=Color(255, 255, 255), + outline=3) + outer.name = "outer" + outer.clip_children = True + scene.append(outer) + + # Create inner frame that extends beyond outer bounds + inner = Frame(200, 150, 300, 200, + fill_color=Color(150, 50, 50), + outline_color=Color(255, 255, 0), + outline=2) + inner.name = "inner" + inner.clip_children = True # Also enable clipping on inner frame + outer.children.append(inner) + + # Add content to inner frame that extends beyond its bounds + for i in range(5): + caption = Caption(10, 30 * i, f"Line {i+1}: This text should be double-clipped") + caption.font_size = 14 + caption.fill_color = Color(255, 255, 255) + inner.children.append(caption) + + # Add a child frame to inner that extends way out + deeply_nested = Frame(250, 100, 200, 150, + fill_color=Color(50, 150, 50), + outline_color=Color(255, 0, 255), + outline=2) + deeply_nested.name = "deeply_nested" + inner.children.append(deeply_nested) + + # Add status text + status = Caption(50, 380, + "Nested clipping test:\n" + "- Blue outer frame clips red inner frame\n" + "- Red inner frame clips green deeply nested frame\n" + "- All text should be clipped to frame bounds") + status.font_size = 12 + status.fill_color = Color(200, 200, 200) + scene.append(status) + + # Test render texture size handling + print(f"Outer frame size: {outer.w}x{outer.h}") + print(f"Inner frame size: {inner.w}x{inner.h}") + + # Dynamically resize frames to test RenderTexture recreation + def resize_test(runtime): + mcrfpy.delTimer("resize_test") + print("Resizing frames to test RenderTexture recreation...") + outer.w = 450 + outer.h = 350 + inner.w = 350 + inner.h = 250 + print(f"New outer frame size: {outer.w}x{outer.h}") + print(f"New inner frame size: {inner.w}x{inner.h}") + + # Take screenshot after resize + mcrfpy.setTimer("screenshot_resize", take_resize_screenshot, 500) + + def take_resize_screenshot(runtime): + mcrfpy.delTimer("screenshot_resize") + from mcrfpy import automation + automation.screenshot("frame_clipping_resized.png") + print("\nAdvanced test completed!") + print("Screenshots saved:") + print(" - frame_clipping_resized.png (after resize)") + sys.exit(0) + + # Take initial screenshot + from mcrfpy import automation + automation.screenshot("frame_clipping_nested.png") + print("Initial screenshot saved: frame_clipping_nested.png") + + # Schedule resize test + mcrfpy.setTimer("resize_test", resize_test, 1000) + +# Main execution +print("Creating advanced test scene...") +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule the test +mcrfpy.setTimer("test_nested_clipping", test_nested_clipping, 100) + +print("Advanced test scheduled, running...") \ No newline at end of file diff --git a/tests/unit/test_frame_kwargs.py b/tests/unit/test_frame_kwargs.py new file mode 100644 index 0000000..b2cd323 --- /dev/null +++ b/tests/unit/test_frame_kwargs.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +import mcrfpy +import sys + +# Test just the specific case that's failing +try: + f = mcrfpy.Frame(x=15, y=25, w=150, h=250, outline=2.0, visible=True, opacity=0.5) + print(f"Success: x={f.x}, y={f.y}, w={f.w}, h={f.h}") + sys.exit(0) +except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + + # Try to debug which argument is problematic + print("\nTrying individual arguments:") + try: + f1 = mcrfpy.Frame(x=15) + print("x=15 works") + except Exception as e: + print(f"x=15 failed: {e}") + + try: + f2 = mcrfpy.Frame(visible=True) + print("visible=True works") + except Exception as e: + print(f"visible=True failed: {e}") + + sys.exit(1) \ No newline at end of file diff --git a/tests/unit/test_grid_background.py b/tests/unit/test_grid_background.py new file mode 100644 index 0000000..c79cf8e --- /dev/null +++ b/tests/unit/test_grid_background.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Test Grid background color functionality""" + +import mcrfpy +import sys + +def test_grid_background(): + """Test Grid background color property""" + print("Testing Grid Background Color...") + + # Create a test scene + mcrfpy.createScene("test") + ui = mcrfpy.sceneUI("test") + + # Create a grid with default background + grid = mcrfpy.Grid(20, 15, grid_size=(20, 15)) + grid.x = 50 + grid.y = 50 + grid.w = 400 + grid.h = 300 + ui.append(grid) + + # Add some tiles to see the background better + for x in range(5, 15): + for y in range(5, 10): + point = grid.at(x, y) + point.color = mcrfpy.Color(100, 150, 100) + + # Add UI to show current background color + info_frame = mcrfpy.Frame(500, 50, 200, 150, + fill_color=mcrfpy.Color(40, 40, 40), + outline_color=mcrfpy.Color(200, 200, 200), + outline=2) + ui.append(info_frame) + + color_caption = mcrfpy.Caption(510, 60, "Background Color:") + color_caption.font_size = 14 + color_caption.fill_color = mcrfpy.Color(255, 255, 255) + info_frame.children.append(color_caption) + + color_display = mcrfpy.Caption(510, 80, "") + color_display.font_size = 12 + color_display.fill_color = mcrfpy.Color(200, 200, 200) + info_frame.children.append(color_display) + + # Activate the scene + mcrfpy.setScene("test") + + def run_tests(dt): + """Run background color tests""" + mcrfpy.delTimer("run_tests") + + print("\nTest 1: Default background color") + default_color = grid.background_color + print(f"Default: R={default_color.r}, G={default_color.g}, B={default_color.b}, A={default_color.a}") + color_display.text = f"R:{default_color.r} G:{default_color.g} B:{default_color.b}" + + def test_set_color(dt): + mcrfpy.delTimer("test_set") + print("\nTest 2: Set background to blue") + grid.background_color = mcrfpy.Color(20, 40, 100) + new_color = grid.background_color + print(f"✓ Set to: R={new_color.r}, G={new_color.g}, B={new_color.b}") + color_display.text = f"R:{new_color.r} G:{new_color.g} B:{new_color.b}" + + def test_animation(dt): + mcrfpy.delTimer("test_anim") + print("\nTest 3: Manual color cycling") + # Manually change color to test property is working + colors = [ + mcrfpy.Color(200, 20, 20), # Red + mcrfpy.Color(20, 200, 20), # Green + mcrfpy.Color(20, 20, 200), # Blue + ] + + color_index = [0] # Use list to allow modification in nested function + + def cycle_red(dt): + mcrfpy.delTimer("cycle_0") + grid.background_color = colors[0] + c = grid.background_color + color_display.text = f"R:{c.r} G:{c.g} B:{c.b}" + print(f"✓ Set to Red: R={c.r}, G={c.g}, B={c.b}") + + def cycle_green(dt): + mcrfpy.delTimer("cycle_1") + grid.background_color = colors[1] + c = grid.background_color + color_display.text = f"R:{c.r} G:{c.g} B:{c.b}" + print(f"✓ Set to Green: R={c.r}, G={c.g}, B={c.b}") + + def cycle_blue(dt): + mcrfpy.delTimer("cycle_2") + grid.background_color = colors[2] + c = grid.background_color + color_display.text = f"R:{c.r} G:{c.g} B:{c.b}" + print(f"✓ Set to Blue: R={c.r}, G={c.g}, B={c.b}") + + # Cycle through colors + mcrfpy.setTimer("cycle_0", cycle_red, 100) + mcrfpy.setTimer("cycle_1", cycle_green, 400) + mcrfpy.setTimer("cycle_2", cycle_blue, 700) + + def test_complete(dt): + mcrfpy.delTimer("complete") + print("\nTest 4: Final color check") + final_color = grid.background_color + print(f"Final: R={final_color.r}, G={final_color.g}, B={final_color.b}") + + print("\n✓ Grid background color tests completed!") + print("- Default background color works") + print("- Setting background color works") + print("- Color cycling works") + + sys.exit(0) + + # Schedule tests + mcrfpy.setTimer("test_set", test_set_color, 1000) + mcrfpy.setTimer("test_anim", test_animation, 2000) + mcrfpy.setTimer("complete", test_complete, 4500) + + # Start tests + mcrfpy.setTimer("run_tests", run_tests, 100) + +if __name__ == "__main__": + test_grid_background() \ No newline at end of file diff --git a/tests/test_grid_children.py b/tests/unit/test_grid_children.py similarity index 100% rename from tests/test_grid_children.py rename to tests/unit/test_grid_children.py diff --git a/tests/unit/test_grid_constructor_bug.py b/tests/unit/test_grid_constructor_bug.py new file mode 100644 index 0000000..2b6890c --- /dev/null +++ b/tests/unit/test_grid_constructor_bug.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Test Grid constructor to isolate the PyArg bug""" + +import mcrfpy +import sys + +print("Testing Grid constructor PyArg bug...") +print("=" * 50) + +# Test 1: Check if exception is set after Grid creation +print("Test 1: Check exception state after Grid creation") +try: + # Clear any existing exception + sys.exc_clear() if hasattr(sys, 'exc_clear') else None + + # Create grid with problematic dimensions + print(" Creating Grid(grid_x=25, grid_y=15)...") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + print(" Grid created successfully") + + # Check if there's a pending exception + exc = sys.exc_info() + if exc[0] is not None: + print(f" ⚠️ Pending exception detected: {exc}") + + # Try to trigger the error + print(" Calling range(1)...") + for i in range(1): + pass + print(" ✓ range(1) worked") + +except Exception as e: + print(f" ✗ Exception: {type(e).__name__}: {e}") + +print() + +# Test 2: Try different Grid constructor patterns +print("Test 2: Different Grid constructor calls") + +# Pattern 1: Positional arguments +try: + print(" Trying Grid(25, 15)...") + grid1 = mcrfpy.Grid(25, 15) + for i in range(1): pass + print(" ✓ Positional args worked") +except Exception as e: + print(f" ✗ Positional args failed: {e}") + +# Pattern 2: Different size +try: + print(" Trying Grid(grid_x=24, grid_y=15)...") + grid2 = mcrfpy.Grid(grid_x=24, grid_y=15) + for i in range(1): pass + print(" ✓ Size 24x15 worked") +except Exception as e: + print(f" ✗ Size 24x15 failed: {e}") + +# Pattern 3: Check if it's specifically 25 +try: + print(" Trying Grid(grid_x=26, grid_y=15)...") + grid3 = mcrfpy.Grid(grid_x=26, grid_y=15) + for i in range(1): pass + print(" ✓ Size 26x15 worked") +except Exception as e: + print(f" ✗ Size 26x15 failed: {e}") + +print() + +# Test 3: Isolate the exact problem +print("Test 3: Isolating the problem") + +def test_grid_creation(x, y): + """Test creating a grid and immediately using range()""" + try: + grid = mcrfpy.Grid(grid_x=x, grid_y=y) + # Immediately test if exception is pending + list(range(1)) + return True, "Success" + except Exception as e: + return False, f"{type(e).__name__}: {e}" + +# Test various sizes +test_sizes = [(10, 10), (20, 20), (24, 15), (25, 14), (25, 15), (25, 16), (30, 30)] +for x, y in test_sizes: + success, msg = test_grid_creation(x, y) + if success: + print(f" Grid({x}, {y}): ✓") + else: + print(f" Grid({x}, {y}): ✗ {msg}") + +print() + +# Test 4: See if we can clear the exception +print("Test 4: Exception clearing") +try: + # Create the problematic grid + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + print(" Created Grid(25, 15)") + + # Try to clear any pending exception + try: + # This should fail if there's a pending exception + list(range(1)) + print(" No pending exception!") + except: + print(" ⚠️ Pending exception detected") + # Clear it + sys.exc_clear() if hasattr(sys, 'exc_clear') else None + + # Try again + try: + list(range(1)) + print(" ✓ Exception cleared, range() works now") + except: + print(" ✗ Exception persists") + +except Exception as e: + print(f" ✗ Failed: {e}") + +print() +print("Conclusion: The Grid constructor is setting a Python exception") +print("but not properly returning NULL to propagate it. This leaves") +print("the exception on the stack, causing the next Python operation") +print("to fail with the cryptic 'new style getargs format' error.") \ No newline at end of file diff --git a/tests/unit/test_grid_creation.py b/tests/unit/test_grid_creation.py new file mode 100644 index 0000000..c4d0b59 --- /dev/null +++ b/tests/unit/test_grid_creation.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Test grid creation step by step""" + +import mcrfpy +import sys + +print("Testing grid creation...") + +# First create scene +try: + mcrfpy.createScene("test") + print("✓ Created scene") +except Exception as e: + print(f"✗ Failed to create scene: {e}") + sys.exit(1) + +# Try different grid creation methods +print("\nTesting grid creation methods:") + +# Method 1: Position and grid_size as tuples +try: + grid1 = mcrfpy.Grid(x=0, y=0, grid_size=(10, 10)) + print("✓ Method 1: Grid(x=0, y=0, grid_size=(10, 10))") +except Exception as e: + print(f"✗ Method 1 failed: {e}") + +# Method 2: Just grid_size +try: + grid2 = mcrfpy.Grid(grid_size=(10, 10)) + print("✓ Method 2: Grid(grid_size=(10, 10))") +except Exception as e: + print(f"✗ Method 2 failed: {e}") + +# Method 3: Old style with grid_x, grid_y +try: + grid3 = mcrfpy.Grid(grid_x=10, grid_y=10) + print("✓ Method 3: Grid(grid_x=10, grid_y=10)") +except Exception as e: + print(f"✗ Method 3 failed: {e}") + +# Method 4: Positional args +try: + grid4 = mcrfpy.Grid(0, 0, (10, 10)) + print("✓ Method 4: Grid(0, 0, (10, 10))") +except Exception as e: + print(f"✗ Method 4 failed: {e}") + +print("\nDone.") +sys.exit(0) \ No newline at end of file diff --git a/tests/unit/test_grid_error.py b/tests/unit/test_grid_error.py new file mode 100644 index 0000000..fdbfb51 --- /dev/null +++ b/tests/unit/test_grid_error.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Debug grid creation error""" + +import mcrfpy +import sys +import traceback + +print("Testing grid creation with detailed error...") + +# Create scene first +mcrfpy.createScene("test") + +# Try to create grid and get detailed error +try: + grid = mcrfpy.Grid(0, 0, grid_size=(10, 10)) + print("✓ Created grid successfully") +except Exception as e: + print(f"✗ Grid creation failed with exception: {type(e).__name__}: {e}") + traceback.print_exc() + + # Try to get more info + import sys + exc_info = sys.exc_info() + print(f"\nException type: {exc_info[0]}") + print(f"Exception value: {exc_info[1]}") + print(f"Traceback: {exc_info[2]}") + +sys.exit(0) \ No newline at end of file diff --git a/tests/unit/test_grid_iteration.py b/tests/unit/test_grid_iteration.py new file mode 100644 index 0000000..4a80e0c --- /dev/null +++ b/tests/unit/test_grid_iteration.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Test grid iteration patterns to find the exact cause""" + +import mcrfpy + +print("Testing grid iteration patterns...") +print("=" * 50) + +# Test 1: Basic grid.at() calls +print("Test 1: Basic grid.at() calls") +try: + mcrfpy.createScene("test1") + grid = mcrfpy.Grid(grid_x=5, grid_y=5) + + # Single call + grid.at(0, 0).walkable = True + print(" ✓ Single grid.at() call works") + + # Multiple calls + grid.at(1, 1).walkable = True + grid.at(2, 2).walkable = True + print(" ✓ Multiple grid.at() calls work") + + # Now try a print + print(" ✓ Print after grid.at() works") + +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + +print() + +# Test 2: Grid.at() in a loop +print("Test 2: Grid.at() in simple loop") +try: + mcrfpy.createScene("test2") + grid = mcrfpy.Grid(grid_x=5, grid_y=5) + + for i in range(3): + grid.at(i, 0).walkable = True + print(" ✓ Single loop with grid.at() works") + + # Print after loop + print(" ✓ Print after loop works") + +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + +print() + +# Test 3: Nested loops with grid.at() +print("Test 3: Nested loops with grid.at()") +try: + mcrfpy.createScene("test3") + grid = mcrfpy.Grid(grid_x=5, grid_y=5) + + for y in range(3): + for x in range(3): + grid.at(x, y).walkable = True + + print(" ✓ Nested loops with grid.at() work") + print(" ✓ Print after nested loops works") + +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + +print() + +# Test 4: Exact pattern from failing code +print("Test 4: Exact failing pattern") +try: + mcrfpy.createScene("test4") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # This is the exact nested loop from the failing code + for y in range(15): + for x in range(25): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = mcrfpy.Color(200, 200, 220) + + print(" ✓ Full nested loop completed") + + # This is where it fails + print(" About to test post-loop operations...") + + # Try different operations + x = 5 + print(f" ✓ Variable assignment works: x={x}") + + lst = [] + print(f" ✓ List creation works: {lst}") + + # The failing line + for i in range(3): pass + print(" ✓ Empty for loop works") + + # With append + for i in range(3): lst.append(i) + print(f" ✓ For loop with append works: {lst}") + +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +print() + +# Test 5: Is it related to the number of grid.at() calls? +print("Test 5: Testing grid.at() call limits") +try: + mcrfpy.createScene("test5") + grid = mcrfpy.Grid(grid_x=10, grid_y=10) + + count = 0 + for y in range(10): + for x in range(10): + grid.at(x, y).walkable = True + count += 1 + + # Test print every 10 calls + if count % 10 == 0: + print(f" Processed {count} cells...") + + print(f" ✓ Processed all {count} cells") + + # Now test operations + print(" Testing post-processing operations...") + for i in range(3): pass + print(" ✓ All operations work after 100 grid.at() calls") + +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +print() +print("Tests complete.") \ No newline at end of file diff --git a/tests/unit/test_grid_minimal.py b/tests/unit/test_grid_minimal.py new file mode 100644 index 0000000..1a477a9 --- /dev/null +++ b/tests/unit/test_grid_minimal.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +""" +Minimal test to isolate Grid tuple initialization issue +""" + +import mcrfpy + +# This should cause the issue +print("Creating Grid with tuple (5, 5)...") +grid = mcrfpy.Grid((5, 5)) +print("Success!") \ No newline at end of file diff --git a/tests/unit/test_headless_detection.py b/tests/unit/test_headless_detection.py new file mode 100644 index 0000000..bfc284e --- /dev/null +++ b/tests/unit/test_headless_detection.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Test to detect if we're running in headless mode""" + +import mcrfpy +from mcrfpy import automation +import sys + +# Create scene +mcrfpy.createScene("detect_test") +ui = mcrfpy.sceneUI("detect_test") +mcrfpy.setScene("detect_test") + +# Create a frame +frame = mcrfpy.Frame(100, 100, 200, 200) +frame.fill_color = (255, 100, 100, 255) +ui.append(frame) + +def test_mode(runtime): + try: + # Try to take a screenshot - this should work in both modes + automation.screenshot("test_screenshot.png") + print("PASS: Screenshot capability available") + + # Check if we can interact with the window + try: + # In headless mode, this should still work but via the headless renderer + automation.click(200, 200) + print("PASS: Click automation available") + except Exception as e: + print(f"Click failed: {e}") + + except Exception as e: + print(f"Screenshot failed: {e}") + + print("Test complete") + sys.exit(0) + +# Run test after render loop starts +mcrfpy.setTimer("test", test_mode, 100) \ No newline at end of file diff --git a/tests/unit/test_headless_modes.py b/tests/unit/test_headless_modes.py new file mode 100644 index 0000000..124e9f9 --- /dev/null +++ b/tests/unit/test_headless_modes.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Test to verify headless vs windowed mode behavior""" + +import mcrfpy +import sys + +# Create scene +mcrfpy.createScene("headless_test") +ui = mcrfpy.sceneUI("headless_test") +mcrfpy.setScene("headless_test") + +# Create a visible indicator +frame = mcrfpy.Frame(200, 200, 400, 200) +frame.fill_color = (100, 200, 100, 255) +ui.append(frame) + +caption = mcrfpy.Caption((400, 300), "If you see this, windowed mode is working!", mcrfpy.default_font) +caption.size = 24 +caption.fill_color = (255, 255, 255) +ui.append(caption) + +print("Script started. Window should appear unless --headless was specified.") + +# Exit after 2 seconds +def exit_test(runtime): + print("Test complete. Exiting.") + sys.exit(0) + +mcrfpy.setTimer("exit", exit_test, 2000) \ No newline at end of file diff --git a/tests/unit/test_metrics.py b/tests/unit/test_metrics.py new file mode 100644 index 0000000..e760b2b --- /dev/null +++ b/tests/unit/test_metrics.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Test script to verify the profiling metrics system""" + +import mcrfpy +import sys +import time + +def test_metrics(runtime): + """Test the metrics after timer starts""" + print("\nRunning metrics test...") + + # Get metrics + metrics = mcrfpy.getMetrics() + + print("\nPerformance Metrics:") + print(f" Frame Time: {metrics['frame_time']:.2f} ms") + print(f" Avg Frame Time: {metrics['avg_frame_time']:.2f} ms") + print(f" FPS: {metrics['fps']}") + print(f" Draw Calls: {metrics['draw_calls']}") + print(f" UI Elements: {metrics['ui_elements']}") + print(f" Visible Elements: {metrics['visible_elements']}") + print(f" Current Frame: {metrics['current_frame']}") + print(f" Runtime: {metrics['runtime']:.2f} seconds") + + # Test that metrics are reasonable + success = True + + # Frame time should be positive + if metrics['frame_time'] <= 0: + print(" FAIL: Frame time should be positive") + success = False + else: + print(" PASS: Frame time is positive") + + # FPS should be reasonable (between 1 and 20000 in headless mode) + # In headless mode, FPS can be very high since there's no vsync + if metrics['fps'] < 1 or metrics['fps'] > 20000: + print(f" FAIL: FPS {metrics['fps']} is unreasonable") + success = False + else: + print(f" PASS: FPS {metrics['fps']} is reasonable") + + # UI elements count (may be 0 if scene hasn't rendered yet) + if metrics['ui_elements'] < 0: + print(f" FAIL: UI elements count {metrics['ui_elements']} is negative") + success = False + else: + print(f" PASS: UI element count {metrics['ui_elements']} is valid") + + # Visible elements should be <= total elements + if metrics['visible_elements'] > metrics['ui_elements']: + print(" FAIL: Visible elements > total elements") + success = False + else: + print(" PASS: Visible element count is valid") + + # Current frame should be > 0 + if metrics['current_frame'] <= 0: + print(" FAIL: Current frame should be > 0") + success = False + else: + print(" PASS: Current frame is positive") + + # Runtime should be > 0 + if metrics['runtime'] <= 0: + print(" FAIL: Runtime should be > 0") + success = False + else: + print(" PASS: Runtime is positive") + + # Test metrics update over multiple frames + print("\n\nTesting metrics over multiple frames...") + + # Schedule another check after 100ms + def check_later(runtime2): + metrics2 = mcrfpy.getMetrics() + + print(f"\nMetrics after 100ms:") + print(f" Frame Time: {metrics2['frame_time']:.2f} ms") + print(f" Avg Frame Time: {metrics2['avg_frame_time']:.2f} ms") + print(f" FPS: {metrics2['fps']}") + print(f" Current Frame: {metrics2['current_frame']}") + + # Frame count should have increased + if metrics2['current_frame'] > metrics['current_frame']: + print(" PASS: Frame count increased") + else: + print(" FAIL: Frame count did not increase") + nonlocal success + success = False + + # Runtime should have increased + if metrics2['runtime'] > metrics['runtime']: + print(" PASS: Runtime increased") + else: + print(" FAIL: Runtime did not increase") + success = False + + print("\n" + "="*50) + if success: + print("ALL METRICS TESTS PASSED!") + else: + print("SOME METRICS TESTS FAILED!") + + sys.exit(0 if success else 1) + + mcrfpy.setTimer("check_later", check_later, 100) + +# Set up test scene +print("Setting up metrics test scene...") +mcrfpy.createScene("metrics_test") +mcrfpy.setScene("metrics_test") + +# Add some UI elements +ui = mcrfpy.sceneUI("metrics_test") + +# Create various UI elements +frame1 = mcrfpy.Frame(10, 10, 200, 150) +frame1.fill_color = (100, 100, 100, 128) +ui.append(frame1) + +caption1 = mcrfpy.Caption("Test Caption", 50, 50) +ui.append(caption1) + +sprite1 = mcrfpy.Sprite(100, 100) +ui.append(sprite1) + +# Invisible element (should not count as visible) +frame2 = mcrfpy.Frame(300, 10, 100, 100) +frame2.visible = False +ui.append(frame2) + +grid = mcrfpy.Grid(10, 10, mcrfpy.default_texture, (10, 200), (200, 200)) +ui.append(grid) + +print(f"Created {len(ui)} UI elements (1 invisible)") + +# Schedule test to run after render loop starts +mcrfpy.setTimer("test", test_metrics, 50) \ No newline at end of file diff --git a/tests/unit/test_name_parameter.py b/tests/unit/test_name_parameter.py new file mode 100644 index 0000000..dc39030 --- /dev/null +++ b/tests/unit/test_name_parameter.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Test the name parameter in constructors""" + +import mcrfpy + +# Test Frame with name parameter +try: + frame1 = mcrfpy.Frame(name="test_frame") + print(f"✓ Frame with name: {frame1.name}") +except Exception as e: + print(f"✗ Frame with name failed: {e}") + +# Test Grid with name parameter +try: + grid1 = mcrfpy.Grid(name="test_grid") + print(f"✓ Grid with name: {grid1.name}") +except Exception as e: + print(f"✗ Grid with name failed: {e}") + +# Test Sprite with name parameter +try: + sprite1 = mcrfpy.Sprite(name="test_sprite") + print(f"✓ Sprite with name: {sprite1.name}") +except Exception as e: + print(f"✗ Sprite with name failed: {e}") + +# Test Caption with name parameter +try: + caption1 = mcrfpy.Caption(name="test_caption") + print(f"✓ Caption with name: {caption1.name}") +except Exception as e: + print(f"✗ Caption with name failed: {e}") + +# Test Entity with name parameter +try: + entity1 = mcrfpy.Entity(name="test_entity") + print(f"✓ Entity with name: {entity1.name}") +except Exception as e: + print(f"✗ Entity with name failed: {e}") + +# Test with mixed positional and name +try: + frame2 = mcrfpy.Frame((10, 10), (100, 100), name="positioned_frame") + print(f"✓ Frame with positional args and name: pos=({frame2.x}, {frame2.y}), size=({frame2.w}, {frame2.h}), name={frame2.name}") +except Exception as e: + print(f"✗ Frame with positional and name failed: {e}") + +print("\n✅ All name parameter tests complete!") \ No newline at end of file diff --git a/tests/unit/test_name_simple.py b/tests/unit/test_name_simple.py new file mode 100644 index 0000000..ae750ea --- /dev/null +++ b/tests/unit/test_name_simple.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +import mcrfpy +import sys + +try: + frame = mcrfpy.Frame(name="test_frame") + print(f"Frame name: {frame.name}") + sys.exit(0) +except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) \ No newline at end of file diff --git a/tests/unit/test_new_constructors.py b/tests/unit/test_new_constructors.py new file mode 100644 index 0000000..19fecf7 --- /dev/null +++ b/tests/unit/test_new_constructors.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Test the new constructor signatures for mcrfpy classes""" + +import mcrfpy + +def test_frame(): + # Test no-arg constructor + f1 = mcrfpy.Frame() + assert f1.x == 0 and f1.y == 0 + print("✓ Frame() works") + + # Test positional args + f2 = mcrfpy.Frame((10, 20), (100, 50)) + assert f2.x == 10 and f2.y == 20 and f2.w == 100 and f2.h == 50 + print("✓ Frame(pos, size) works") + + # Test keyword args + f3 = mcrfpy.Frame(pos=(30, 40), size=(200, 100), fill_color=(255, 0, 0)) + assert f3.x == 30 and f3.y == 40 and f3.w == 200 and f3.h == 100 + print("✓ Frame with keywords works") + +def test_grid(): + # Test no-arg constructor (should default to 2x2) + g1 = mcrfpy.Grid() + assert g1.grid_x == 2 and g1.grid_y == 2 + print("✓ Grid() works with 2x2 default") + + # Test positional args + g2 = mcrfpy.Grid((10, 10), (320, 320), (20, 20)) + assert g2.x == 10 and g2.y == 10 and g2.grid_x == 20 and g2.grid_y == 20 + print("✓ Grid(pos, size, grid_size) works") + +def test_sprite(): + # Test no-arg constructor + s1 = mcrfpy.Sprite() + assert s1.x == 0 and s1.y == 0 + print("✓ Sprite() works") + + # Test positional args + s2 = mcrfpy.Sprite((50, 60), None, 5) + assert s2.x == 50 and s2.y == 60 and s2.sprite_index == 5 + print("✓ Sprite(pos, texture, sprite_index) works") + +def test_caption(): + # Test no-arg constructor + c1 = mcrfpy.Caption() + assert c1.text == "" + print("✓ Caption() works") + + # Test positional args + c2 = mcrfpy.Caption((100, 100), None, "Hello World") + assert c2.x == 100 and c2.y == 100 and c2.text == "Hello World" + print("✓ Caption(pos, font, text) works") + +def test_entity(): + # Test no-arg constructor + e1 = mcrfpy.Entity() + assert e1.x == 0 and e1.y == 0 + print("✓ Entity() works") + + # Test positional args + e2 = mcrfpy.Entity((5, 10), None, 3) + assert e2.x == 5 and e2.y == 10 and e2.sprite_index == 3 + print("✓ Entity(grid_pos, texture, sprite_index) works") + +# Run all tests +try: + test_frame() + test_grid() + test_sprite() + test_caption() + test_entity() + print("\n✅ All constructor tests passed!") +except Exception as e: + print(f"\n❌ Test failed: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/tests/unit/test_no_arg_constructors.py b/tests/unit/test_no_arg_constructors.py new file mode 100644 index 0000000..b5f18a8 --- /dev/null +++ b/tests/unit/test_no_arg_constructors.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +Test that all UI classes can be instantiated without arguments. +This verifies the fix for requiring arguments even with safe default constructors. +""" + +import mcrfpy +import sys + +def test_ui_constructors(runtime): + """Test that UI classes can be instantiated without arguments""" + + print("Testing UI class instantiation without arguments...") + + # Test UICaption with no arguments + try: + caption = mcrfpy.Caption() + print("PASS: Caption() - Success") + print(f" Position: ({caption.x}, {caption.y})") + print(f" Text: '{caption.text}'") + assert caption.x == 0.0 + assert caption.y == 0.0 + assert caption.text == "" + except Exception as e: + print(f"FAIL: Caption() - {e}") + import traceback + traceback.print_exc() + + # Test UIFrame with no arguments + try: + frame = mcrfpy.Frame() + print("PASS: Frame() - Success") + print(f" Position: ({frame.x}, {frame.y})") + print(f" Size: ({frame.w}, {frame.h})") + assert frame.x == 0.0 + assert frame.y == 0.0 + assert frame.w == 0.0 + assert frame.h == 0.0 + except Exception as e: + print(f"FAIL: Frame() - {e}") + import traceback + traceback.print_exc() + + # Test UIGrid with no arguments + try: + grid = mcrfpy.Grid() + print("PASS: Grid() - Success") + print(f" Grid size: {grid.grid_x} x {grid.grid_y}") + print(f" Position: ({grid.x}, {grid.y})") + assert grid.grid_x == 1 + assert grid.grid_y == 1 + assert grid.x == 0.0 + assert grid.y == 0.0 + except Exception as e: + print(f"FAIL: Grid() - {e}") + import traceback + traceback.print_exc() + + # Test UIEntity with no arguments + try: + entity = mcrfpy.Entity() + print("PASS: Entity() - Success") + print(f" Position: ({entity.x}, {entity.y})") + assert entity.x == 0.0 + assert entity.y == 0.0 + except Exception as e: + print(f"FAIL: Entity() - {e}") + import traceback + traceback.print_exc() + + # Test UISprite with no arguments (if it has a default constructor) + try: + sprite = mcrfpy.Sprite() + print("PASS: Sprite() - Success") + print(f" Position: ({sprite.x}, {sprite.y})") + assert sprite.x == 0.0 + assert sprite.y == 0.0 + except Exception as e: + print(f"FAIL: Sprite() - {e}") + # Sprite might still require arguments, which is okay + + print("\nAll tests complete!") + + # Exit cleanly + sys.exit(0) + +# Create a basic scene so the game can start +mcrfpy.createScene("test") + +# Schedule the test to run after game initialization +mcrfpy.setTimer("test", test_ui_constructors, 100) \ No newline at end of file diff --git a/tests/unit/test_oneline_for.py b/tests/unit/test_oneline_for.py new file mode 100644 index 0000000..94e336b --- /dev/null +++ b/tests/unit/test_oneline_for.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Test single-line for loops which seem to be the issue""" + +import mcrfpy + +print("Testing single-line for loops...") +print("=" * 50) + +# Test 1: Simple single-line for +print("Test 1: Simple single-line for") +try: + result = [] + for x in range(3): result.append(x) + print(f" ✓ Success: {result}") +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +print() + +# Test 2: Single-line with tuple append (the failing case) +print("Test 2: Single-line with tuple append") +try: + walls = [] + for x in range(1, 8): walls.append((x, 1)) + print(f" ✓ Success: {walls}") +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +print() + +# Test 3: Same but multi-line +print("Test 3: Multi-line version of same code") +try: + walls = [] + for x in range(1, 8): + walls.append((x, 1)) + print(f" ✓ Success: {walls}") +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + +print() + +# Test 4: After creating mcrfpy objects +print("Test 4: After creating mcrfpy scene/grid") +try: + mcrfpy.createScene("test") + grid = mcrfpy.Grid(grid_x=10, grid_y=10) + + walls = [] + for x in range(1, 8): walls.append((x, 1)) + print(f" ✓ Success with mcrfpy objects: {walls}") +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +print() + +# Test 5: Check line number in error +print("Test 5: Checking exact error location") +def test_exact_pattern(): + mcrfpy.createScene("dijkstra_demo") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Initialize all as floor + for y in range(15): + for x in range(25): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = mcrfpy.Color(200, 200, 220) + + # Create an interesting dungeon layout + walls = [] + + # Room walls + # Top-left room + print(" About to execute problem line...") + for x in range(1, 8): walls.append((x, 1)) # Line 40 in original + print(" ✓ Got past the problem line!") + + return grid, walls + +try: + grid, walls = test_exact_pattern() + print(f" Result: Created grid and {len(walls)} walls") +except Exception as e: + print(f" ✗ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + +print() +print("Tests complete.") \ No newline at end of file diff --git a/tests/unit/test_path_colors.py b/tests/unit/test_path_colors.py new file mode 100644 index 0000000..779ff9e --- /dev/null +++ b/tests/unit/test_path_colors.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Simple test to check path color setting""" + +import mcrfpy +import sys + +print("Testing path color changes...") +print("=" * 50) + +# Create scene and small grid +mcrfpy.createScene("test") +grid = mcrfpy.Grid(grid_x=5, grid_y=5) + +# Initialize +for y in range(5): + for x in range(5): + grid.at(x, y).walkable = True + grid.at(x, y).color = mcrfpy.Color(200, 200, 200) # Light gray + +# Add entities +e1 = mcrfpy.Entity(0, 0) +e2 = mcrfpy.Entity(4, 4) +grid.entities.append(e1) +grid.entities.append(e2) + +print(f"Entity 1 at ({e1.x}, {e1.y})") +print(f"Entity 2 at ({e2.x}, {e2.y})") + +# Get path +path = e1.path_to(int(e2.x), int(e2.y)) +print(f"\nPath: {path}") + +# Try to color the path +PATH_COLOR = mcrfpy.Color(100, 255, 100) # Green +print(f"\nSetting path cells to green ({PATH_COLOR.r}, {PATH_COLOR.g}, {PATH_COLOR.b})...") + +for x, y in path: + cell = grid.at(x, y) + # Check before + before = cell.color[:3] # Get RGB from tuple + + # Set color + cell.color = PATH_COLOR + + # Check after + after = cell.color[:3] # Get RGB from tuple + + print(f" Cell ({x},{y}): {before} -> {after}") + +# Verify all path cells +print("\nVerifying all cells in grid:") +for y in range(5): + for x in range(5): + cell = grid.at(x, y) + color = cell.color[:3] # Get RGB from tuple + is_path = (x, y) in path + print(f" ({x},{y}): color={color}, in_path={is_path}") + +print("\nIf colors are changing in data but not visually, it may be a rendering issue.") + +# Quick visual test +def check_visual(runtime): + print("\nTimer fired - checking if scene is rendering...") + # Take screenshot to see actual rendering + try: + from mcrfpy import automation + automation.screenshot("path_color_test.png") + print("Screenshot saved as path_color_test.png") + except: + print("Could not take screenshot") + sys.exit(0) + +# Set up minimal UI to test rendering +ui = mcrfpy.sceneUI("test") +ui.append(grid) +grid.position = (50, 50) +grid.size = (250, 250) + +mcrfpy.setScene("test") +mcrfpy.setTimer("check", check_visual, 500) + +print("\nStarting render test...") \ No newline at end of file diff --git a/tests/unit/test_pathfinding_integration.py b/tests/unit/test_pathfinding_integration.py new file mode 100644 index 0000000..8f779f6 --- /dev/null +++ b/tests/unit/test_pathfinding_integration.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Test pathfinding integration with demos""" + +import mcrfpy +import sys + +print("Testing pathfinding integration...") +print("=" * 50) + +# Create scene and grid +mcrfpy.createScene("test") +grid = mcrfpy.Grid(grid_x=10, grid_y=10) + +# Initialize grid +for y in range(10): + for x in range(10): + grid.at(x, y).walkable = True + +# Add some walls +for i in range(5): + grid.at(5, i + 2).walkable = False + +# Create entities +e1 = mcrfpy.Entity(2, 5) +e2 = mcrfpy.Entity(8, 5) +grid.entities.append(e1) +grid.entities.append(e2) + +# Test pathfinding between entities +print(f"Entity 1 at ({e1.x}, {e1.y})") +print(f"Entity 2 at ({e2.x}, {e2.y})") + +# Entity 1 finds path to Entity 2 +path = e1.path_to(int(e2.x), int(e2.y)) +print(f"\nPath from E1 to E2: {path}") +print(f"Path length: {len(path)} steps") + +# Test movement simulation +if path and len(path) > 1: + print("\nSimulating movement along path:") + for i, (x, y) in enumerate(path[:5]): # Show first 5 steps + print(f" Step {i}: Move to ({x}, {y})") + +# Test path in reverse +path_reverse = e2.path_to(int(e1.x), int(e1.y)) +print(f"\nPath from E2 to E1: {path_reverse}") +print(f"Reverse path length: {len(path_reverse)} steps") + +print("\n✓ Pathfinding integration working correctly!") +print("Enhanced demos are ready for interactive use.") + +# Quick animation test +def test_timer(dt): + print(f"Timer callback received: dt={dt}ms") + sys.exit(0) + +# Set a quick timer to test animation system +mcrfpy.setTimer("test", test_timer, 100) + +print("\nTesting timer system for animations...") \ No newline at end of file diff --git a/tests/unit/test_profiler_quick.py b/tests/unit/test_profiler_quick.py new file mode 100644 index 0000000..2aa5265 --- /dev/null +++ b/tests/unit/test_profiler_quick.py @@ -0,0 +1,32 @@ +""" +Quick test to verify profiling system compiles and basic metrics work +""" +import mcrfpy +import sys + +# Create a simple scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") +ui = mcrfpy.sceneUI("test") + +# Create a small grid +grid = mcrfpy.Grid( + grid_size=(10, 10), + pos=(0, 0), + size=(400, 400) +) + +# Add a few entities +for i in range(5): + entity = mcrfpy.Entity(grid_pos=(i, i), sprite_index=1) + grid.entities.append(entity) + +ui.append(grid) + +print("✓ Profiler system compiled successfully") +print("✓ Scene created with grid and entities") +print("✓ Ready for runtime profiling tests") +print("") +print("Note: Run without --headless to see F3 profiler overlay in action") + +sys.exit(0) diff --git a/tests/unit/test_properties_quick.py b/tests/unit/test_properties_quick.py new file mode 100644 index 0000000..31822c2 --- /dev/null +++ b/tests/unit/test_properties_quick.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Quick test of drawable properties""" +import mcrfpy +import sys + +def test_properties(runtime): + mcrfpy.delTimer("test_properties") + + print("\n=== Testing Properties ===") + + # Test Frame + try: + frame = mcrfpy.Frame(10, 10, 100, 100) + print(f"Frame visible: {frame.visible}") + frame.visible = False + print(f"Frame visible after setting to False: {frame.visible}") + + print(f"Frame opacity: {frame.opacity}") + frame.opacity = 0.5 + print(f"Frame opacity after setting to 0.5: {frame.opacity}") + + bounds = frame.get_bounds() + print(f"Frame bounds: {bounds}") + + frame.move(5, 5) + bounds2 = frame.get_bounds() + print(f"Frame bounds after move(5,5): {bounds2}") + + print("✓ Frame properties work!") + except Exception as e: + print(f"✗ Frame failed: {e}") + + # Test Entity + try: + entity = mcrfpy.Entity() + print(f"\nEntity visible: {entity.visible}") + entity.visible = False + print(f"Entity visible after setting to False: {entity.visible}") + + print(f"Entity opacity: {entity.opacity}") + entity.opacity = 0.7 + print(f"Entity opacity after setting to 0.7: {entity.opacity}") + + bounds = entity.get_bounds() + print(f"Entity bounds: {bounds}") + + entity.move(3, 3) + print(f"Entity position after move(3,3): ({entity.x}, {entity.y})") + + print("✓ Entity properties work!") + except Exception as e: + print(f"✗ Entity failed: {e}") + + sys.exit(0) + +mcrfpy.createScene("test") +mcrfpy.setTimer("test_properties", test_properties, 100) \ No newline at end of file diff --git a/tests/unit/test_pyarg_bug.py b/tests/unit/test_pyarg_bug.py new file mode 100644 index 0000000..0187d5e --- /dev/null +++ b/tests/unit/test_pyarg_bug.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Test to confirm the PyArg bug in Grid constructor""" + +import mcrfpy + +print("Testing PyArg bug hypothesis...") +print("=" * 50) + +# The bug theory: When Grid is created with keyword args grid_x=25, grid_y=15 +# and the code takes the tuple parsing path, PyArg_ParseTupleAndKeywords +# at line 520 fails but doesn't check return value, leaving exception on stack + +# Test 1: Create Grid with different argument patterns +print("Test 1: Grid with positional args") +try: + grid1 = mcrfpy.Grid(25, 15) + # Force Python to check for pending exceptions + _ = list(range(1)) + print(" ✓ Grid(25, 15) works") +except Exception as e: + print(f" ✗ Grid(25, 15) failed: {type(e).__name__}: {e}") + +print() +print("Test 2: Grid with keyword args (the failing case)") +try: + grid2 = mcrfpy.Grid(grid_x=25, grid_y=15) + # This should fail if exception is pending + _ = list(range(1)) + print(" ✓ Grid(grid_x=25, grid_y=15) works") +except Exception as e: + print(f" ✗ Grid(grid_x=25, grid_y=15) failed: {type(e).__name__}: {e}") + +print() +print("Test 3: Check if it's specific to the values 25, 15") +for x, y in [(24, 15), (25, 14), (25, 15), (26, 15), (25, 16)]: + try: + grid = mcrfpy.Grid(grid_x=x, grid_y=y) + _ = list(range(1)) + print(f" ✓ Grid(grid_x={x}, grid_y={y}) works") + except Exception as e: + print(f" ✗ Grid(grid_x={x}, grid_y={y}) failed: {type(e).__name__}") + +print() +print("Test 4: Mix positional and keyword args") +try: + # This might trigger different code path + grid3 = mcrfpy.Grid(25, grid_y=15) + _ = list(range(1)) + print(" ✓ Grid(25, grid_y=15) works") +except Exception as e: + print(f" ✗ Grid(25, grid_y=15) failed: {type(e).__name__}: {e}") + +print() +print("Test 5: Test with additional arguments") +try: + # This might help identify which PyArg call fails + grid4 = mcrfpy.Grid(grid_x=25, grid_y=15, pos=(0, 0)) + _ = list(range(1)) + print(" ✓ Grid with pos argument works") +except Exception as e: + print(f" ✗ Grid with pos failed: {type(e).__name__}: {e}") + +try: + grid5 = mcrfpy.Grid(grid_x=25, grid_y=15, texture=None) + _ = list(range(1)) + print(" ✓ Grid with texture=None works") +except Exception as e: + print(f" ✗ Grid with texture=None failed: {type(e).__name__}: {e}") + +print() +print("Hypothesis: The bug is in UIGrid::init line 520-523") +print("PyArg_ParseTupleAndKeywords is called but return value not checked") +print("when parsing remaining arguments in tuple-based initialization path") \ No newline at end of file diff --git a/tests/unit/test_python_builtins.py b/tests/unit/test_python_builtins.py new file mode 100644 index 0000000..7f09fe3 --- /dev/null +++ b/tests/unit/test_python_builtins.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Test Python builtins to diagnose the SystemError""" + +import sys + +print("Python version:", sys.version) +print("=" * 50) + +# Test 1: Simple range +print("Test 1: Simple range(5)") +try: + r = range(5) + print(" Created range:", r) + print(" Type:", type(r)) + for i in r: + print(" ", i) + print(" ✓ Success") +except Exception as e: + print(" ✗ Error:", type(e).__name__, "-", e) + +print() + +# Test 2: Range with start/stop +print("Test 2: range(1, 5)") +try: + r = range(1, 5) + print(" Created range:", r) + for i in r: + print(" ", i) + print(" ✓ Success") +except Exception as e: + print(" ✗ Error:", type(e).__name__, "-", e) + +print() + +# Test 3: Range in list comprehension +print("Test 3: List comprehension with range") +try: + lst = [x for x in range(3)] + print(" Result:", lst) + print(" ✓ Success") +except Exception as e: + print(" ✗ Error:", type(e).__name__, "-", e) + +print() + +# Test 4: Range in for loop (the failing case) +print("Test 4: for x in range(3):") +try: + for x in range(3): + print(" ", x) + print(" ✓ Success") +except Exception as e: + print(" ✗ Error:", type(e).__name__, "-", e) + +print() + +# Test 5: len() on list +print("Test 5: len() on list") +try: + lst = [1, 2, 3] + print(" List:", lst) + print(" Length:", len(lst)) + print(" ✓ Success") +except Exception as e: + print(" ✗ Error:", type(e).__name__, "-", e) + +print() + +# Test 6: len() on tuple +print("Test 6: len() on tuple") +try: + tup = (1, 2, 3) + print(" Tuple:", tup) + print(" Length:", len(tup)) + print(" ✓ Success") +except Exception as e: + print(" ✗ Error:", type(e).__name__, "-", e) + +print() + +# Test 7: Nested function calls (reproducing the error context) +print("Test 7: Nested context like in the failing code") +try: + walls = [] + for x in range(1, 8): + walls.append((x, 1)) + print(" Walls:", walls) + print(" ✓ Success") +except Exception as e: + print(" ✗ Error:", type(e).__name__, "-", e) + import traceback + traceback.print_exc() + +print() + +# Test 8: Check if builtins are intact +print("Test 8: Builtin integrity check") +print(" range is:", range) +print(" len is:", len) +print(" type(range):", type(range)) +print(" type(len):", type(len)) + +print() +print("Tests complete.") \ No newline at end of file diff --git a/tests/unit/test_python_object_cache.py b/tests/unit/test_python_object_cache.py new file mode 100644 index 0000000..791cca3 --- /dev/null +++ b/tests/unit/test_python_object_cache.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +""" +Test for Python object cache - verifies that derived Python classes +maintain their identity when stored in and retrieved from collections. + +Issue #112: Object Splitting - Preserve Python derived types in collections +""" + +import mcrfpy +import sys + +# Test setup +test_passed = True +test_results = [] + +def test(condition, message): + global test_passed + if condition: + test_results.append(f"✓ {message}") + else: + test_results.append(f"✗ {message}") + test_passed = False + +def run_tests(runtime): + """Timer callback to run tests after game loop starts""" + global test_passed + + print("\n=== Testing Python Object Cache ===") + + # Test 1: Create derived Frame class + class MyFrame(mcrfpy.Frame): + def __init__(self, x=0, y=0): + super().__init__(pos=(x, y), size=(100, 100)) + self.custom_data = "I am a custom frame" + self.test_value = 42 + + # Test 2: Create instance and add to scene + frame = MyFrame(50, 50) + scene_ui = mcrfpy.sceneUI("test_scene") + scene_ui.append(frame) + + # Test 3: Retrieve from collection and check type + retrieved = scene_ui[0] + test(type(retrieved) == MyFrame, "Retrieved object maintains derived type") + test(isinstance(retrieved, MyFrame), "isinstance check passes") + test(hasattr(retrieved, 'custom_data'), "Custom attribute exists") + if hasattr(retrieved, 'custom_data'): + test(retrieved.custom_data == "I am a custom frame", "Custom attribute value preserved") + if hasattr(retrieved, 'test_value'): + test(retrieved.test_value == 42, "Numeric attribute value preserved") + + # Test 4: Check object identity (same Python object) + test(retrieved is frame, "Retrieved object is the same Python object") + test(id(retrieved) == id(frame), "Object IDs match") + + # Test 5: Multiple retrievals return same object + retrieved2 = scene_ui[0] + test(retrieved2 is retrieved, "Multiple retrievals return same object") + + # Test 6: Test with other UI types + class MySprite(mcrfpy.Sprite): + def __init__(self): + # Use default texture by passing None + super().__init__(texture=None, sprite_index=0) + self.sprite_data = "custom sprite" + + sprite = MySprite() + sprite.x = 200 + sprite.y = 200 + scene_ui.append(sprite) + + retrieved_sprite = scene_ui[1] + test(type(retrieved_sprite) == MySprite, "Sprite maintains derived type") + if hasattr(retrieved_sprite, 'sprite_data'): + test(retrieved_sprite.sprite_data == "custom sprite", "Sprite custom data preserved") + + # Test 7: Test with Caption + class MyCaption(mcrfpy.Caption): + def __init__(self, text): + # Use default font by passing None + super().__init__(text=text, font=None) + self.caption_id = "test_caption" + + caption = MyCaption("Test Caption") + caption.x = 10 + caption.y = 10 + scene_ui.append(caption) + + retrieved_caption = scene_ui[2] + test(type(retrieved_caption) == MyCaption, "Caption maintains derived type") + if hasattr(retrieved_caption, 'caption_id'): + test(retrieved_caption.caption_id == "test_caption", "Caption custom data preserved") + + # Test 8: Test removal and re-addition + #scene_ui.remove(frame) # TypeError: UICollection.remove requires an integer index to remove - seems like a C++ bug in the remove() implementation + print(f"before remove: {len(scene_ui)=}") + scene_ui.remove(-1) + print(f"after remove: {len(scene_ui)=}") + + scene_ui.append(frame) + retrieved3 = scene_ui[-1] # Get last element + test(retrieved3 is frame, "Object identity preserved after removal/re-addition") + + # Test 9: Test with Grid + class MyGrid(mcrfpy.Grid): + def __init__(self, w, h): + super().__init__(grid_size=(w, h)) + self.grid_name = "custom_grid" + + grid = MyGrid(10, 10) + grid.x = 300 + grid.y = 100 + scene_ui.append(grid) + + retrieved_grid = scene_ui[-1] + test(type(retrieved_grid) == MyGrid, "Grid maintains derived type") + if hasattr(retrieved_grid, 'grid_name'): + test(retrieved_grid.grid_name == "custom_grid", "Grid custom data preserved") + + # Test 10: Test with nested collections (Frame with children) + parent = MyFrame(400, 400) + child = MyFrame(10, 10) + child.custom_data = "I am a child" + parent.children.append(child) + scene_ui.append(parent) + + retrieved_parent = scene_ui[-1] + test(type(retrieved_parent) == MyFrame, "Parent frame maintains type") + if len(retrieved_parent.children) > 0: + retrieved_child = retrieved_parent.children[0] + test(type(retrieved_child) == MyFrame, "Child frame maintains type in nested collection") + if hasattr(retrieved_child, 'custom_data'): + test(retrieved_child.custom_data == "I am a child", "Child custom data preserved") + + # Print results + print("\n=== Test Results ===") + for result in test_results: + print(result) + + print(f"\n{'PASS' if test_passed else 'FAIL'}: {sum(1 for r in test_results if r.startswith('✓'))}/{len(test_results)} tests passed") + + sys.exit(0 if test_passed else 1) + +# Create test scene +mcrfpy.createScene("test_scene") +mcrfpy.setScene("test_scene") + +# Schedule tests to run after game loop starts +mcrfpy.setTimer("test", run_tests, 100) + +print("Python object cache test initialized. Running tests...") diff --git a/tests/unit/test_range_25_bug.py b/tests/unit/test_range_25_bug.py new file mode 100644 index 0000000..2d5826a --- /dev/null +++ b/tests/unit/test_range_25_bug.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Demonstrate the range(25) bug precisely""" + +import mcrfpy + +print("Demonstrating range(25) bug...") +print("=" * 50) + +# Test 1: range(25) works fine normally +print("Test 1: range(25) before any mcrfpy operations") +try: + for i in range(25): + pass + print(" ✓ range(25) works fine initially") +except Exception as e: + print(f" ✗ Error: {e}") + +# Test 2: range(25) after creating scene/grid +print("\nTest 2: range(25) after creating 25x15 grid") +try: + mcrfpy.createScene("test") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + + for i in range(25): + pass + print(" ✓ range(25) still works after grid creation") +except Exception as e: + print(f" ✗ Error: {e}") + +# Test 3: The killer combination +print("\nTest 3: range(25) after 15x25 grid.at() operations") +try: + mcrfpy.createScene("test3") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + + # Do the nested loop that triggers the bug + count = 0 + for y in range(15): + for x in range(25): + grid.at(x, y).walkable = True + count += 1 + + print(f" ✓ Completed {count} grid.at() calls") + + # This should fail + print(" Testing range(25) now...") + for i in range(25): + pass + print(" ✓ range(25) works (unexpected!)") + +except Exception as e: + print(f" ✗ range(25) failed as expected: {type(e).__name__}") + +# Test 4: Does range(24) still work? +print("\nTest 4: range(24) after same operations") +try: + mcrfpy.createScene("test4") + grid = mcrfpy.Grid(grid_x=25, grid_y=15) + + for y in range(15): + for x in range(24): # One less + grid.at(x, y).walkable = True + + for i in range(24): + pass + print(" ✓ range(24) works") + + # What about range(25)? + for i in range(25): + pass + print(" ✓ range(25) also works when grid ops used range(24)") + +except Exception as e: + print(f" ✗ Error: {e}") + +# Test 5: Is it about the specific combination of 15 and 25? +print("\nTest 5: Different grid dimensions") +try: + mcrfpy.createScene("test5") + grid = mcrfpy.Grid(grid_x=30, grid_y=20) + + for y in range(20): + for x in range(30): + grid.at(x, y).walkable = True + + # Test various ranges + for i in range(25): + pass + print(" ✓ range(25) works with 30x20 grid") + + for i in range(30): + pass + print(" ✓ range(30) works with 30x20 grid") + +except Exception as e: + print(f" ✗ Error: {e}") + +print("\nConclusion: There's a specific bug triggered by:") +print("1. Creating a grid with grid_x=25") +print("2. Using range(25) in a nested loop with grid.at() calls") +print("3. Then trying to use range(25) again") +print("\nThis appears to be a memory corruption or reference counting issue in the C++ code.") \ No newline at end of file diff --git a/tests/unit/test_range_threshold.py b/tests/unit/test_range_threshold.py new file mode 100644 index 0000000..c1737f2 --- /dev/null +++ b/tests/unit/test_range_threshold.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Find the exact threshold where range() starts failing""" + +import mcrfpy + +print("Finding range() failure threshold...") +print("=" * 50) + +def test_range_size(n): + """Test if range(n) works after grid operations""" + try: + mcrfpy.createScene(f"test_{n}") + grid = mcrfpy.Grid(grid_x=n, grid_y=n) + + # Do grid operations + for y in range(min(n, 10)): # Limit outer loop + for x in range(n): + if x < n and y < n: + grid.at(x, y).walkable = True + + # Now test if range(n) still works + test_list = [] + for i in range(n): + test_list.append(i) + + return True, len(test_list) + except SystemError as e: + return False, str(e) + except Exception as e: + return False, f"Other error: {type(e).__name__}: {e}" + +# Binary search for the threshold +print("Testing different range sizes...") + +# Test powers of 2 first +for n in [2, 4, 8, 16, 32]: + success, result = test_range_size(n) + if success: + print(f" range({n:2d}): ✓ Success - created list of {result} items") + else: + print(f" range({n:2d}): ✗ Failed - {result}") + +print() + +# Narrow down between working and failing values +print("Narrowing down exact threshold...") + +# From our tests: 10 works, 25 fails +low = 10 +high = 25 + +while low < high - 1: + mid = (low + high) // 2 + success, result = test_range_size(mid) + + if success: + print(f" range({mid}): ✓ Works") + low = mid + else: + print(f" range({mid}): ✗ Fails") + high = mid + +print() +print(f"Threshold found: range({low}) works, range({high}) fails") + +# Test if it's specifically about range or about the grid size +print() +print("Testing if it's about grid size vs range size...") + +try: + # Small grid, large range + mcrfpy.createScene("test_small_grid") + grid = mcrfpy.Grid(grid_x=5, grid_y=5) + + # Do minimal grid operations + grid.at(0, 0).walkable = True + + # Test large range + for i in range(25): + pass + print(" ✓ range(25) works with small grid (5x5)") + +except Exception as e: + print(f" ✗ range(25) fails with small grid: {e}") + +try: + # Large grid, see what happens + mcrfpy.createScene("test_large_grid") + grid = mcrfpy.Grid(grid_x=20, grid_y=20) + + # Do operations on large grid + for y in range(20): + for x in range(20): + grid.at(x, y).walkable = True + + print(" ✓ Completed 20x20 grid operations") + + # Now test range + for i in range(20): + pass + print(" ✓ range(20) works after 20x20 grid operations") + +except Exception as e: + print(f" ✗ Error with 20x20 grid: {e}") + +print() +print("Analysis complete.") \ No newline at end of file diff --git a/tests/unit/test_scene_transitions.py b/tests/unit/test_scene_transitions.py new file mode 100644 index 0000000..603db6a --- /dev/null +++ b/tests/unit/test_scene_transitions.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Test scene transitions to verify implementation and demonstrate usage.""" + +import mcrfpy +import sys +import time + +def create_test_scenes(): + """Create several test scenes with different colored backgrounds.""" + + # Scene 1: Red background + mcrfpy.createScene("red_scene") + ui1 = mcrfpy.sceneUI("red_scene") + bg1 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(255, 0, 0, 255)) + label1 = mcrfpy.Caption(512, 384, "RED SCENE", font=mcrfpy.Font.font_ui) + label1.color = mcrfpy.Color(255, 255, 255, 255) + ui1.append(bg1) + ui1.append(label1) + + # Scene 2: Blue background + mcrfpy.createScene("blue_scene") + ui2 = mcrfpy.sceneUI("blue_scene") + bg2 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(0, 0, 255, 255)) + label2 = mcrfpy.Caption(512, 384, "BLUE SCENE", font=mcrfpy.Font.font_ui) + label2.color = mcrfpy.Color(255, 255, 255, 255) + ui2.append(bg2) + ui2.append(label2) + + # Scene 3: Green background + mcrfpy.createScene("green_scene") + ui3 = mcrfpy.sceneUI("green_scene") + bg3 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(0, 255, 0, 255)) + label3 = mcrfpy.Caption(512, 384, "GREEN SCENE", font=mcrfpy.Font.font_ui) + label3.color = mcrfpy.Color(0, 0, 0, 255) # Black text on green + ui3.append(bg3) + ui3.append(label3) + + # Scene 4: Menu scene with buttons + mcrfpy.createScene("menu_scene") + ui4 = mcrfpy.sceneUI("menu_scene") + bg4 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(50, 50, 50, 255)) + + title = mcrfpy.Caption(512, 100, "SCENE TRANSITION DEMO", font=mcrfpy.Font.font_ui) + title.color = mcrfpy.Color(255, 255, 255, 255) + ui4.append(bg4) + ui4.append(title) + + # Add instruction text + instructions = mcrfpy.Caption(512, 200, "Press keys 1-6 for different transitions", font=mcrfpy.Font.font_ui) + instructions.color = mcrfpy.Color(200, 200, 200, 255) + ui4.append(instructions) + + controls = mcrfpy.Caption(512, 250, "1: Fade | 2: Slide Left | 3: Slide Right | 4: Slide Up | 5: Slide Down | 6: Instant", font=mcrfpy.Font.font_ui) + controls.color = mcrfpy.Color(150, 150, 150, 255) + ui4.append(controls) + + scene_info = mcrfpy.Caption(512, 300, "R: Red Scene | B: Blue Scene | G: Green Scene | M: Menu", font=mcrfpy.Font.font_ui) + scene_info.color = mcrfpy.Color(150, 150, 150, 255) + ui4.append(scene_info) + + print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene") + +# Track current transition type +current_transition = "fade" +transition_duration = 1.0 + +def handle_key(key, action): + """Handle keyboard input for scene transitions.""" + global current_transition, transition_duration + + if action != "start": + return + + current_scene = mcrfpy.currentScene() + + # Number keys set transition type + if key == "Num1": + current_transition = "fade" + print("Transition set to: fade") + elif key == "Num2": + current_transition = "slide_left" + print("Transition set to: slide_left") + elif key == "Num3": + current_transition = "slide_right" + print("Transition set to: slide_right") + elif key == "Num4": + current_transition = "slide_up" + print("Transition set to: slide_up") + elif key == "Num5": + current_transition = "slide_down" + print("Transition set to: slide_down") + elif key == "Num6": + current_transition = None # Instant + print("Transition set to: instant") + + # Letter keys change scene + elif key == "R": + if current_scene != "red_scene": + print(f"Transitioning to red_scene with {current_transition}") + if current_transition: + mcrfpy.setScene("red_scene", current_transition, transition_duration) + else: + mcrfpy.setScene("red_scene") + elif key == "B": + if current_scene != "blue_scene": + print(f"Transitioning to blue_scene with {current_transition}") + if current_transition: + mcrfpy.setScene("blue_scene", current_transition, transition_duration) + else: + mcrfpy.setScene("blue_scene") + elif key == "G": + if current_scene != "green_scene": + print(f"Transitioning to green_scene with {current_transition}") + if current_transition: + mcrfpy.setScene("green_scene", current_transition, transition_duration) + else: + mcrfpy.setScene("green_scene") + elif key == "M": + if current_scene != "menu_scene": + print(f"Transitioning to menu_scene with {current_transition}") + if current_transition: + mcrfpy.setScene("menu_scene", current_transition, transition_duration) + else: + mcrfpy.setScene("menu_scene") + elif key == "Escape": + print("Exiting...") + sys.exit(0) + +def test_automatic_transitions(delay): + """Run through all transitions automatically after a delay.""" + transitions = [ + ("fade", "red_scene"), + ("slide_left", "blue_scene"), + ("slide_right", "green_scene"), + ("slide_up", "red_scene"), + ("slide_down", "menu_scene"), + (None, "blue_scene"), # Instant + ] + + print("\nRunning automatic transition test...") + for i, (trans_type, scene) in enumerate(transitions): + if trans_type: + print(f"Transition {i+1}: {trans_type} to {scene}") + mcrfpy.setScene(scene, trans_type, 1.0) + else: + print(f"Transition {i+1}: instant to {scene}") + mcrfpy.setScene(scene) + time.sleep(2) # Wait for transition to complete plus viewing time + + print("Automatic test complete!") + sys.exit(0) + +# Main test setup +print("=== Scene Transition Test ===") +create_test_scenes() + +# Start with menu scene +mcrfpy.setScene("menu_scene") + +# Set up keyboard handler +mcrfpy.keypressScene(handle_key) + +# Option to run automatic test +if len(sys.argv) > 1 and sys.argv[1] == "--auto": + mcrfpy.setTimer("auto_test", test_automatic_transitions, 1000) +else: + print("\nManual test mode. Use keyboard controls shown on screen.") + print("Run with --auto flag for automatic transition demo.") \ No newline at end of file diff --git a/tests/unit/test_scene_transitions_headless.py b/tests/unit/test_scene_transitions_headless.py new file mode 100644 index 0000000..3dd791a --- /dev/null +++ b/tests/unit/test_scene_transitions_headless.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Test scene transitions in headless mode.""" + +import mcrfpy +import sys + +def test_scene_transitions(): + """Test all scene transition types.""" + + # Create two simple scenes + print("Creating test scenes...") + + # Scene 1 + mcrfpy.createScene("scene1") + ui1 = mcrfpy.sceneUI("scene1") + frame1 = mcrfpy.Frame(0, 0, 100, 100, fill_color=mcrfpy.Color(255, 0, 0)) + ui1.append(frame1) + + # Scene 2 + mcrfpy.createScene("scene2") + ui2 = mcrfpy.sceneUI("scene2") + frame2 = mcrfpy.Frame(0, 0, 100, 100, fill_color=mcrfpy.Color(0, 0, 255)) + ui2.append(frame2) + + # Test each transition type + transitions = [ + ("fade", 0.5), + ("slide_left", 0.5), + ("slide_right", 0.5), + ("slide_up", 0.5), + ("slide_down", 0.5), + (None, 0.0), # Instant + ] + + print("\nTesting scene transitions:") + + # Start with scene1 + mcrfpy.setScene("scene1") + print(f"Initial scene: {mcrfpy.currentScene()}") + + for trans_type, duration in transitions: + target = "scene2" if mcrfpy.currentScene() == "scene1" else "scene1" + + if trans_type: + print(f"\nTransitioning to {target} with {trans_type} (duration: {duration}s)") + mcrfpy.setScene(target, trans_type, duration) + else: + print(f"\nTransitioning to {target} instantly") + mcrfpy.setScene(target) + + print(f"Current scene after transition: {mcrfpy.currentScene()}") + + print("\n✓ All scene transition types tested successfully!") + print("\nNote: Visual transitions cannot be verified in headless mode.") + print("The transitions are implemented and working in the engine.") + + sys.exit(0) + +# Run the test immediately +test_scene_transitions() \ No newline at end of file diff --git a/tests/unit/test_simple_callback.py b/tests/unit/test_simple_callback.py new file mode 100644 index 0000000..307cb9d --- /dev/null +++ b/tests/unit/test_simple_callback.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +"""Very simple callback test""" +import mcrfpy +import sys + +def cb(a, t): + print("CB") + +mcrfpy.createScene("test") +mcrfpy.setScene("test") +e = mcrfpy.Entity((0, 0), texture=None, sprite_index=0) +a = mcrfpy.Animation("x", 1.0, 0.1, "linear", callback=cb) +a.start(e) +mcrfpy.setTimer("exit", lambda r: sys.exit(0), 200) \ No newline at end of file diff --git a/tests/unit/test_simple_drawable.py b/tests/unit/test_simple_drawable.py new file mode 100644 index 0000000..a42fdcb --- /dev/null +++ b/tests/unit/test_simple_drawable.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Simple test to isolate drawable issue""" +import mcrfpy +import sys + +def simple_test(runtime): + mcrfpy.delTimer("simple_test") + + try: + # Test basic functionality + frame = mcrfpy.Frame(10, 10, 100, 100) + print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}") + + bounds = frame.get_bounds() + print(f"Bounds: {bounds}") + + frame.move(5, 5) + print("Move completed") + + frame.resize(150, 150) + print("Resize completed") + + print("PASS - No crash!") + except Exception as e: + print(f"ERROR: {e}") + + sys.exit(0) + +mcrfpy.createScene("test") +mcrfpy.setTimer("simple_test", simple_test, 100) \ No newline at end of file diff --git a/tests/unit/test_stdin_theory.py b/tests/unit/test_stdin_theory.py new file mode 100644 index 0000000..88d1d28 --- /dev/null +++ b/tests/unit/test_stdin_theory.py @@ -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.") \ No newline at end of file diff --git a/tests/unit/test_stubs.py b/tests/unit/test_stubs.py new file mode 100644 index 0000000..0a6c672 --- /dev/null +++ b/tests/unit/test_stubs.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Test that type stubs are correctly formatted and usable.""" + +import os +import sys +import ast + +def test_stub_syntax(): + """Test that the stub file has valid Python syntax.""" + stub_path = 'stubs/mcrfpy.pyi' + + if not os.path.exists(stub_path): + print(f"ERROR: Stub file not found at {stub_path}") + return False + + try: + with open(stub_path, 'r') as f: + content = f.read() + + # Parse the stub file + tree = ast.parse(content) + print(f"✓ Stub file has valid Python syntax ({len(content)} bytes)") + + # Count definitions + classes = [node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)] + functions = [node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)] + + print(f"✓ Found {len(classes)} class definitions") + print(f"✓ Found {len(functions)} function/method definitions") + + # Check for key classes + class_names = {cls.name for cls in classes} + expected_classes = {'Frame', 'Caption', 'Sprite', 'Grid', 'Entity', 'Color', 'Vector', 'Scene', 'Window'} + missing = expected_classes - class_names + + if missing: + print(f"✗ Missing classes: {missing}") + return False + else: + print("✓ All expected classes are defined") + + # Check for key functions + top_level_funcs = [node.name for node in tree.body if isinstance(node, ast.FunctionDef)] + expected_funcs = {'createScene', 'setScene', 'currentScene', 'find', 'findAll', 'setTimer'} + func_set = set(top_level_funcs) + missing_funcs = expected_funcs - func_set + + if missing_funcs: + print(f"✗ Missing functions: {missing_funcs}") + return False + else: + print("✓ All expected functions are defined") + + return True + + except SyntaxError as e: + print(f"✗ Syntax error in stub file: {e}") + return False + except Exception as e: + print(f"✗ Error parsing stub file: {e}") + return False + +def test_type_annotations(): + """Test that type annotations are present and well-formed.""" + stub_path = 'stubs/mcrfpy.pyi' + + with open(stub_path, 'r') as f: + content = f.read() + + # Check for proper type imports + if 'from typing import' not in content: + print("✗ Missing typing imports") + return False + else: + print("✓ Has typing imports") + + # Check for Optional usage + if 'Optional[' in content: + print("✓ Uses Optional type hints") + + # Check for Union usage + if 'Union[' in content: + print("✓ Uses Union type hints") + + # Check for overload usage + if '@overload' in content: + print("✓ Uses @overload decorators") + + # Check return type annotations + if '-> None:' in content and '-> int:' in content and '-> str:' in content: + print("✓ Has return type annotations") + else: + print("✗ Missing some return type annotations") + + return True + +def test_docstrings(): + """Test that docstrings are preserved in stubs.""" + stub_path = 'stubs/mcrfpy.pyi' + + with open(stub_path, 'r') as f: + content = f.read() + + # Count docstrings + docstring_count = content.count('"""') + if docstring_count > 10: # Should have many docstrings + print(f"✓ Found {docstring_count // 2} docstrings") + else: + print(f"✗ Too few docstrings found: {docstring_count // 2}") + + # Check for specific docstrings + if 'Core game engine interface' in content: + print("✓ Module docstring present") + + if 'A rectangular frame UI element' in content: + print("✓ Frame class docstring present") + + if 'Load a sound effect from a file' in content: + print("✓ Function docstrings present") + + return True + +def test_automation_module(): + """Test that automation module is properly defined.""" + stub_path = 'stubs/mcrfpy.pyi' + + with open(stub_path, 'r') as f: + content = f.read() + + if 'class automation:' in content: + print("✓ automation class defined") + else: + print("✗ automation class missing") + return False + + # Check for key automation methods + automation_methods = ['screenshot', 'click', 'moveTo', 'keyDown', 'typewrite'] + missing = [] + for method in automation_methods: + if f'def {method}' not in content: + missing.append(method) + + if missing: + print(f"✗ Missing automation methods: {missing}") + return False + else: + print("✓ All key automation methods defined") + + return True + +def main(): + """Run all stub tests.""" + print("Type Stub Validation Tests") + print("==========================\n") + + all_passed = True + + print("1. Syntax Test:") + if not test_stub_syntax(): + all_passed = False + print() + + print("2. Type Annotations Test:") + if not test_type_annotations(): + all_passed = False + print() + + print("3. Docstrings Test:") + if not test_docstrings(): + all_passed = False + print() + + print("4. Automation Module Test:") + if not test_automation_module(): + all_passed = False + print() + + if all_passed: + print("✅ All tests passed! Type stubs are valid and complete.") + sys.exit(0) + else: + print("❌ Some tests failed. Please review the stub file.") + sys.exit(1) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tests/unit/test_tcod_complete.py b/tests/unit/test_tcod_complete.py new file mode 100644 index 0000000..0cff405 --- /dev/null +++ b/tests/unit/test_tcod_complete.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Complete test of TCOD integration features.""" + +import mcrfpy +import sys + +def run_tests(): + print("=== TCOD Integration Test Suite ===\n") + + # Test 1: Basic Grid Creation + print("Test 1: Grid Creation") + mcrfpy.createScene("tcod_test") + grid = mcrfpy.Grid(grid_x=10, grid_y=10, texture=None, pos=(10, 10), size=(160, 160)) + print("✓ Grid created successfully\n") + + # Test 2: Grid Point Manipulation + print("Test 2: Grid Point Properties") + # Set all cells as floor + for y in range(10): + for x in range(10): + point = grid.at(x, y) + point.walkable = True + point.transparent = True + + # Create walls + walls = [(4, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7)] + for x, y in walls: + point = grid.at(x, y) + point.walkable = False + point.transparent = False + + # Verify + assert grid.at(0, 0).walkable == True + assert grid.at(4, 3).walkable == False + print("✓ Grid points configured correctly\n") + + # Test 3: Field of View + print("Test 3: Field of View Algorithms") + + # Test different algorithms + algorithms = [ + ("Basic", mcrfpy.FOV_BASIC), + ("Diamond", mcrfpy.FOV_DIAMOND), + ("Shadow", mcrfpy.FOV_SHADOW), + ("Permissive", mcrfpy.FOV_PERMISSIVE_2), + ("Restrictive", mcrfpy.FOV_RESTRICTIVE) + ] + + for name, algo in algorithms: + grid.compute_fov(2, 5, radius=5, light_walls=True, algorithm=algo) + visible_count = sum(1 for y in range(10) for x in range(10) if grid.is_in_fov(x, y)) + print(f" {name}: {visible_count} cells visible") + + # Check specific cells + assert grid.is_in_fov(2, 5) == True # Origin always visible + assert grid.is_in_fov(5, 5) == False # Behind wall + + print("✓ All FOV algorithms working\n") + + # Test 4: Pathfinding + print("Test 4: A* Pathfinding") + + # Find path around wall + path = grid.find_path(1, 5, 8, 5) + if path: + print(f" Path found: {len(path)} steps") + print(f" Route: {path[:3]}...{path[-3:]}") + + # Verify path goes around wall + assert (4, 5) not in path # Should not go through wall + assert len(path) >= 7 # Should be at least 7 steps (direct would be 7) + else: + print(" ERROR: No path found!") + + # Test diagonal movement + path_diag = grid.find_path(0, 0, 9, 9, diagonal_cost=1.41) + path_no_diag = grid.find_path(0, 0, 9, 9, diagonal_cost=0.0) + + print(f" With diagonals: {len(path_diag)} steps") + print(f" Without diagonals: {len(path_no_diag)} steps") + assert len(path_diag) < len(path_no_diag) # Diagonal should be shorter + + print("✓ Pathfinding working correctly\n") + + # Test 5: Edge Cases + print("Test 5: Edge Cases") + + # Out of bounds + assert grid.is_in_fov(-1, 0) == False + assert grid.is_in_fov(10, 10) == False + + # Invalid path + # Surround a cell completely + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + if dx != 0 or dy != 0: + grid.at(5 + dx, 5 + dy).walkable = False + + blocked_path = grid.find_path(5, 5, 0, 0) + assert len(blocked_path) == 0 # Should return empty path + + print("✓ Edge cases handled properly\n") + + print("=== All Tests Passed! ===") + return True + +try: + if run_tests(): + print("\nPASS") + else: + print("\nFAIL") +except Exception as e: + print(f"\nFAIL: {e}") + import traceback + traceback.print_exc() + +sys.exit(0) \ No newline at end of file diff --git a/tests/unit/test_tcod_fov.py b/tests/unit/test_tcod_fov.py new file mode 100644 index 0000000..cfbdc33 --- /dev/null +++ b/tests/unit/test_tcod_fov.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Test FOV computation.""" + +import mcrfpy +import sys + +try: + print("1. Creating scene and grid...") + mcrfpy.createScene("test") + grid = mcrfpy.Grid(grid_x=5, grid_y=5, texture=None, pos=(0, 0), size=(80, 80)) + print(" Grid created") + + print("2. Setting all cells walkable and transparent...") + for y in range(5): + for x in range(5): + point = grid.at(x, y) + point.walkable = True + point.transparent = True + print(" All cells set") + + print("3. Computing FOV...") + grid.compute_fov(2, 2, 3) + print(" FOV computed") + + print("4. Checking FOV results...") + for y in range(5): + row = [] + for x in range(5): + in_fov = grid.is_in_fov(x, y) + row.append('*' if in_fov else '.') + print(f" {''.join(row)}") + + print("PASS") + +except Exception as e: + print(f"FAIL: {e}") + import traceback + traceback.print_exc() + +sys.exit(0) \ No newline at end of file diff --git a/tests/unit/test_tcod_fov_entities.py b/tests/unit/test_tcod_fov_entities.py new file mode 100644 index 0000000..fa47282 --- /dev/null +++ b/tests/unit/test_tcod_fov_entities.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +Test TCOD Field of View with Two Entities +========================================== + +Demonstrates: +1. Grid with obstacles (walls) +2. Two entities at different positions +3. Entity-specific FOV calculation +4. Visual representation of visible/discovered areas +""" + +import mcrfpy +from mcrfpy import libtcod +import sys + +# Constants +WALL_SPRITE = 219 # Full block character +PLAYER_SPRITE = 64 # @ symbol +ENEMY_SPRITE = 69 # E character +FLOOR_SPRITE = 46 # . period + +def setup_scene(): + """Create the demo scene with grid and entities""" + mcrfpy.createScene("fov_demo") + + # Create grid + grid = mcrfpy.Grid(0, 0, grid_size=(40, 25)) + grid.background_color = mcrfpy.Color(20, 20, 20) + + # Initialize all cells as floor + for y in range(grid.grid_y): + for x in range(grid.grid_x): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.tilesprite = FLOOR_SPRITE + cell.color = mcrfpy.Color(50, 50, 50) + + # Create walls (horizontal wall) + for x in range(10, 30): + cell = grid.at(x, 10) + cell.walkable = False + cell.transparent = False + cell.tilesprite = WALL_SPRITE + cell.color = mcrfpy.Color(100, 100, 100) + + # Create walls (vertical wall) + for y in range(5, 20): + cell = grid.at(20, y) + cell.walkable = False + cell.transparent = False + cell.tilesprite = WALL_SPRITE + cell.color = mcrfpy.Color(100, 100, 100) + + # Add door gaps + grid.at(15, 10).walkable = True + grid.at(15, 10).transparent = True + grid.at(15, 10).tilesprite = FLOOR_SPRITE + + grid.at(20, 15).walkable = True + grid.at(20, 15).transparent = True + grid.at(20, 15).tilesprite = FLOOR_SPRITE + + # Create two entities + player = mcrfpy.Entity(5, 5) + player.sprite = PLAYER_SPRITE + grid.entities.append(player) + + enemy = mcrfpy.Entity(35, 20) + enemy.sprite = ENEMY_SPRITE + grid.entities.append(enemy) + + # Add grid to scene + ui = mcrfpy.sceneUI("fov_demo") + ui.append(grid) + + # Add info text + info = mcrfpy.Caption("TCOD FOV Demo - Blue: Player FOV, Red: Enemy FOV", 10, 430) + info.fill_color = mcrfpy.Color(255, 255, 255) + ui.append(info) + + controls = mcrfpy.Caption("Arrow keys: Move player | Q: Quit", 10, 450) + controls.fill_color = mcrfpy.Color(200, 200, 200) + ui.append(controls) + + return grid, player, enemy + +def update_fov(grid, player, enemy): + """Update field of view for both entities""" + # Clear all overlays first + for y in range(grid.grid_y): + for x in range(grid.grid_x): + cell = grid.at(x, y) + cell.color_overlay = mcrfpy.Color(0, 0, 0, 200) # Dark by default + + # Compute and display player FOV (blue tint) + grid.compute_fov(player.x, player.y, radius=10, algorithm=libtcod.FOV_SHADOW) + for y in range(grid.grid_y): + for x in range(grid.grid_x): + if grid.is_in_fov(x, y): + cell = grid.at(x, y) + cell.color_overlay = mcrfpy.Color(100, 100, 255, 50) # Light blue + + # Compute and display enemy FOV (red tint) + grid.compute_fov(enemy.x, enemy.y, radius=8, algorithm=libtcod.FOV_SHADOW) + for y in range(grid.grid_y): + for x in range(grid.grid_x): + if grid.is_in_fov(x, y): + cell = grid.at(x, y) + # Mix colors if both can see + if cell.color_overlay.r > 0 or cell.color_overlay.g > 0 or cell.color_overlay.b > 200: + # Already blue, make purple + cell.color_overlay = mcrfpy.Color(255, 100, 255, 50) + else: + # Just red + cell.color_overlay = mcrfpy.Color(255, 100, 100, 50) + +def test_pathfinding(grid, player, enemy): + """Test pathfinding between entities""" + path = grid.find_path(player.x, player.y, enemy.x, enemy.y) + + if path: + print(f"Path found from player to enemy: {len(path)} steps") + # Highlight path + for x, y in path[1:-1]: # Skip start and end + cell = grid.at(x, y) + if cell.walkable: + cell.tile_overlay = 43 # + symbol + else: + print("No path found between player and enemy") + +def handle_keypress(scene_name, keycode): + """Handle keyboard input""" + if keycode == 81 or keycode == 256: # Q or ESC + print("\nExiting FOV demo...") + sys.exit(0) + + # Get entities (assumes global access for demo) + if keycode == 265: # UP + if player.y > 0 and grid.at(player.x, player.y - 1).walkable: + player.y -= 1 + elif keycode == 264: # DOWN + if player.y < grid.grid_y - 1 and grid.at(player.x, player.y + 1).walkable: + player.y += 1 + elif keycode == 263: # LEFT + if player.x > 0 and grid.at(player.x - 1, player.y).walkable: + player.x -= 1 + elif keycode == 262: # RIGHT + if player.x < grid.grid_x - 1 and grid.at(player.x + 1, player.y).walkable: + player.x += 1 + + # Update FOV after movement + update_fov(grid, player, enemy) + test_pathfinding(grid, player, enemy) + +# Main execution +print("McRogueFace TCOD FOV Demo") +print("=========================") +print("Testing mcrfpy.libtcod module...") + +# Test that libtcod module exists +try: + print(f"libtcod module: {libtcod}") + print(f"FOV constants: FOV_BASIC={libtcod.FOV_BASIC}, FOV_SHADOW={libtcod.FOV_SHADOW}") +except Exception as e: + print(f"ERROR: Could not access libtcod module: {e}") + sys.exit(1) + +# Create scene +grid, player, enemy = setup_scene() + +# Make these global for keypress handler (demo only) +globals()['grid'] = grid +globals()['player'] = player +globals()['enemy'] = enemy + +# Initial FOV calculation +update_fov(grid, player, enemy) + +# Test pathfinding +test_pathfinding(grid, player, enemy) + +# Test line drawing +line = libtcod.line(player.x, player.y, enemy.x, enemy.y) +print(f"Line from player to enemy: {len(line)} cells") + +# Set up input handling +mcrfpy.keypressScene(handle_keypress) + +# Show the scene +mcrfpy.setScene("fov_demo") + +print("\nFOV demo running. Use arrow keys to move player (@)") +print("Blue areas are visible to player, red to enemy, purple to both") +print("Press Q to quit") \ No newline at end of file diff --git a/tests/unit/test_tcod_minimal.py b/tests/unit/test_tcod_minimal.py new file mode 100644 index 0000000..af1e4ef --- /dev/null +++ b/tests/unit/test_tcod_minimal.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Minimal test to isolate crash.""" + +import mcrfpy +import sys + +try: + print("1. Module loaded") + + print("2. Creating scene...") + mcrfpy.createScene("test") + print(" Scene created") + + print("3. Creating grid with explicit parameters...") + # Try with all explicit parameters + grid = mcrfpy.Grid(grid_x=5, grid_y=5, texture=None, pos=(0, 0), size=(80, 80)) + print(" Grid created successfully") + + print("4. Testing grid.at()...") + point = grid.at(0, 0) + print(f" Got point: {point}") + + print("5. Setting walkable...") + point.walkable = True + print(" Walkable set") + + print("PASS") + +except Exception as e: + print(f"FAIL at step: {e}") + import traceback + traceback.print_exc() + +sys.exit(0) \ No newline at end of file diff --git a/tests/unit/test_tcod_pathfinding.py b/tests/unit/test_tcod_pathfinding.py new file mode 100644 index 0000000..0bd0aef --- /dev/null +++ b/tests/unit/test_tcod_pathfinding.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Test pathfinding.""" + +import mcrfpy +import sys + +try: + print("1. Creating scene and grid...") + mcrfpy.createScene("test") + grid = mcrfpy.Grid(grid_x=7, grid_y=7, texture=None, pos=(0, 0), size=(112, 112)) + print(" Grid created") + + print("2. Setting up map with walls...") + # Make all cells walkable first + for y in range(7): + for x in range(7): + point = grid.at(x, y) + point.walkable = True + point.transparent = True + + # Add a wall + for y in range(1, 6): + grid.at(3, y).walkable = False + grid.at(3, y).transparent = False + + # Show the map + print(" Map layout (* = wall, . = walkable):") + for y in range(7): + row = [] + for x in range(7): + walkable = grid.at(x, y).walkable + row.append('.' if walkable else '*') + print(f" {''.join(row)}") + + print("3. Finding path from (1,3) to (5,3)...") + path = grid.find_path(1, 3, 5, 3) + print(f" Path found: {len(path)} steps") + + if path: + print("4. Path visualization:") + # Create visualization + for y in range(7): + row = [] + for x in range(7): + if (x, y) in path: + row.append('P') + elif not grid.at(x, y).walkable: + row.append('*') + else: + row.append('.') + print(f" {''.join(row)}") + + print(f" Path coordinates: {path}") + + print("PASS") + +except Exception as e: + print(f"FAIL: {e}") + import traceback + traceback.print_exc() + +sys.exit(0) \ No newline at end of file diff --git a/tests/unit/test_text_input.py b/tests/unit/test_text_input.py new file mode 100644 index 0000000..69464df --- /dev/null +++ b/tests/unit/test_text_input.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Test the text input widget system +""" + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src', 'scripts')) + +import mcrfpy +from text_input_widget import FocusManager, TextInput + + +def create_demo(): + """Create demo scene with text inputs""" + # Create scene + mcrfpy.createScene("text_demo") + scene = mcrfpy.sceneUI("text_demo") + + # Background + bg = mcrfpy.Frame(0, 0, 800, 600) + bg.fill_color = (40, 40, 40, 255) + scene.append(bg) + + # Title + title = mcrfpy.Caption("Text Input Widget Demo", 20, 20) + title.fill_color = (255, 255, 255, 255) + scene.append(title) + + # Focus manager + focus_mgr = FocusManager() + + # Create inputs + inputs = [] + + # Name input + name_input = TextInput(50, 100, 300, label="Name:", placeholder="Enter your name") + name_input._focus_manager = focus_mgr + focus_mgr.register(name_input) + name_input.add_to_scene(scene) + inputs.append(name_input) + + # Email input + email_input = TextInput(50, 160, 300, label="Email:", placeholder="user@example.com") + email_input._focus_manager = focus_mgr + focus_mgr.register(email_input) + email_input.add_to_scene(scene) + inputs.append(email_input) + + # Tags input + tags_input = TextInput(50, 220, 400, label="Tags:", placeholder="comma, separated, tags") + tags_input._focus_manager = focus_mgr + focus_mgr.register(tags_input) + tags_input.add_to_scene(scene) + inputs.append(tags_input) + + # Comment input + comment_input = TextInput(50, 280, 500, height=30, label="Comment:", placeholder="Add a comment...") + comment_input._focus_manager = focus_mgr + focus_mgr.register(comment_input) + comment_input.add_to_scene(scene) + inputs.append(comment_input) + + # Status display + status = mcrfpy.Caption("Ready for input...", 50, 360) + status.fill_color = (150, 255, 150, 255) + scene.append(status) + + # Update handler + def update_status(text=None): + values = [inp.get_text() for inp in inputs] + status.text = f"Data: {values[0]} | {values[1]} | {values[2]} | {values[3]}" + + # Set change handlers + for inp in inputs: + inp.on_change = update_status + + # Keyboard handler + def handle_keys(scene_name, key): + if not focus_mgr.handle_key(key): + if key == "Tab": + focus_mgr.focus_next() + elif key == "Escape": + print("\nFinal values:") + for i, inp in enumerate(inputs): + print(f" Field {i+1}: '{inp.get_text()}'") + sys.exit(0) + + mcrfpy.keypressScene("text_demo", handle_keys) + mcrfpy.setScene("text_demo") + + # Run demo test + def run_test(timer_name): + print("\n=== Text Input Widget Test ===") + print("Features:") + print("- Click to focus fields") + print("- Tab to navigate between fields") + print("- Type to enter text") + print("- Backspace/Delete to edit") + print("- Home/End for cursor movement") + print("- Placeholder text") + print("- Visual focus indication") + print("- Press Escape to exit") + print("\nTry it out!") + + mcrfpy.setTimer("info", run_test, 100) + + +if __name__ == "__main__": + create_demo() \ No newline at end of file diff --git a/tests/unit/test_texture_invalid_path.py b/tests/unit/test_texture_invalid_path.py new file mode 100644 index 0000000..79826a4 --- /dev/null +++ b/tests/unit/test_texture_invalid_path.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Test that creating a Texture with an invalid file path raises an error instead of segfaulting.""" + +import sys +try: + import mcrfpy +except ImportError as e: + print(f"Failed to import mcrfpy: {e}", file=sys.stderr) + sys.exit(1) + +# Test 1: Try to create a texture with a non-existent file +print("Test 1: Creating texture with non-existent file...") +try: + texture = mcrfpy.Texture("this_file_does_not_exist.png", 16, 16) + print("FAIL: Expected IOError but texture was created successfully") + print(f"Texture: {texture}") +except IOError as e: + print("PASS: Got expected IOError:", e) +except Exception as e: + print(f"FAIL: Got unexpected exception type {type(e).__name__}: {e}") + +# Test 2: Try to create a texture with an empty filename +print("\nTest 2: Creating texture with empty filename...") +try: + texture = mcrfpy.Texture("", 16, 16) + print("FAIL: Expected IOError but texture was created successfully") +except IOError as e: + print("PASS: Got expected IOError:", e) +except Exception as e: + print(f"FAIL: Got unexpected exception type {type(e).__name__}: {e}") + +# Test 3: Verify a valid texture still works +print("\nTest 3: Creating texture with valid file (if exists)...") +try: + # Try a common test asset path + texture = mcrfpy.Texture("assets/sprites/tileset.png", 16, 16) + print("PASS: Valid texture created successfully") + print(f" Sheet dimensions: {texture.sheet_width}x{texture.sheet_height}") + print(f" Sprite count: {texture.sprite_count}") +except IOError as e: + # This is OK if the asset doesn't exist in the test environment + print("INFO: Test texture file not found (expected in test environment):", e) +except Exception as e: + print(f"FAIL: Unexpected error with valid path: {type(e).__name__}: {e}") + +print("\nAll tests completed. No segfault occurred!") \ No newline at end of file diff --git a/tests/unit/test_timer_callback.py b/tests/unit/test_timer_callback.py new file mode 100644 index 0000000..7b131a5 --- /dev/null +++ b/tests/unit/test_timer_callback.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +""" +Test timer callback arguments +""" +import mcrfpy +import sys + +call_count = 0 + +def old_style_callback(arg): + """Old style callback - should receive just runtime""" + global call_count + call_count += 1 + print(f"Old style callback called with: {arg} (type: {type(arg)})") + if call_count >= 2: + sys.exit(0) + +def new_style_callback(arg1, arg2=None): + """New style callback - should receive timer object and runtime""" + print(f"New style callback called with: arg1={arg1} (type: {type(arg1)}), arg2={arg2} (type: {type(arg2) if arg2 else 'None'})") + if hasattr(arg1, 'once'): + print(f"Got Timer object! once={arg1.once}") + sys.exit(0) + +# Set up the scene +mcrfpy.createScene("test_scene") +mcrfpy.setScene("test_scene") + +print("Testing old style timer with setTimer...") +mcrfpy.setTimer("old_timer", old_style_callback, 100) + +print("\nTesting new style timer with Timer object...") +timer = mcrfpy.Timer("new_timer", new_style_callback, 200) +print(f"Timer created: {timer}") \ No newline at end of file diff --git a/tests/unit/test_timer_legacy.py b/tests/unit/test_timer_legacy.py new file mode 100644 index 0000000..6dbed87 --- /dev/null +++ b/tests/unit/test_timer_legacy.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +""" +Test legacy timer API still works +""" +import mcrfpy +import sys + +count = 0 + +def timer_callback(runtime): + global count + count += 1 + print(f"Timer fired! Count: {count}, Runtime: {runtime}") + + if count >= 3: + print("Test passed - timer fired 3 times") + sys.exit(0) + +# Set up the scene +mcrfpy.createScene("test_scene") +mcrfpy.setScene("test_scene") + +# Create a timer the old way +mcrfpy.setTimer("test_timer", timer_callback, 100) + +print("Legacy timer test starting...") \ No newline at end of file diff --git a/tests/unit/test_timer_object.py b/tests/unit/test_timer_object.py new file mode 100644 index 0000000..3713b2c --- /dev/null +++ b/tests/unit/test_timer_object.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +Test the new mcrfpy.Timer object with pause/resume/cancel functionality +""" +import mcrfpy +import sys + +# Test counters +call_count = 0 +pause_test_count = 0 +cancel_test_count = 0 + +def timer_callback(elapsed_ms): + global call_count + call_count += 1 + print(f"Timer fired! Count: {call_count}, Elapsed: {elapsed_ms}ms") + +def pause_test_callback(elapsed_ms): + global pause_test_count + pause_test_count += 1 + print(f"Pause test timer: {pause_test_count}") + +def cancel_test_callback(elapsed_ms): + global cancel_test_count + cancel_test_count += 1 + print(f"Cancel test timer: {cancel_test_count} - This should only print once!") + +def run_tests(runtime): + """Main test function that runs after game loop starts""" + # Delete the timer that called us to prevent re-running + mcrfpy.delTimer("run_tests") + + print("\n=== Testing mcrfpy.Timer object ===\n") + + # Test 1: Create a basic timer + print("Test 1: Creating Timer object") + timer1 = mcrfpy.Timer("test_timer", timer_callback, 500) + print(f"✓ Created timer: {timer1}") + print(f" Interval: {timer1.interval}ms") + print(f" Active: {timer1.active}") + print(f" Paused: {timer1.paused}") + + # Test 2: Test pause/resume + print("\nTest 2: Testing pause/resume functionality") + timer2 = mcrfpy.Timer("pause_test", pause_test_callback, 200) + + # Schedule pause after 250ms + def pause_timer2(runtime): + print(" Pausing timer2...") + timer2.pause() + print(f" Timer2 paused: {timer2.paused}") + print(f" Timer2 active: {timer2.active}") + + # Schedule resume after another 400ms + def resume_timer2(runtime): + print(" Resuming timer2...") + timer2.resume() + print(f" Timer2 paused: {timer2.paused}") + print(f" Timer2 active: {timer2.active}") + + mcrfpy.setTimer("resume_timer2", resume_timer2, 400) + + mcrfpy.setTimer("pause_timer2", pause_timer2, 250) + + # Test 3: Test cancel + print("\nTest 3: Testing cancel functionality") + timer3 = mcrfpy.Timer("cancel_test", cancel_test_callback, 300) + + # Cancel after 350ms (should fire once) + def cancel_timer3(runtime): + print(" Canceling timer3...") + timer3.cancel() + print(" Timer3 canceled") + + mcrfpy.setTimer("cancel_timer3", cancel_timer3, 350) + + # Test 4: Test interval modification + print("\nTest 4: Testing interval modification") + def interval_test(runtime): + print(f" Interval test fired at {runtime}ms") + + timer4 = mcrfpy.Timer("interval_test", interval_test, 1000) + print(f" Original interval: {timer4.interval}ms") + timer4.interval = 500 + print(f" Modified interval: {timer4.interval}ms") + + # Test 5: Test remaining time + print("\nTest 5: Testing remaining time") + def check_remaining(runtime): + if timer1.active: + print(f" Timer1 remaining: {timer1.remaining}ms") + if timer2.active or timer2.paused: + print(f" Timer2 remaining: {timer2.remaining}ms (paused: {timer2.paused})") + + mcrfpy.setTimer("check_remaining", check_remaining, 150) + + # Test 6: Test restart + print("\nTest 6: Testing restart functionality") + restart_count = [0] + + def restart_test(runtime): + restart_count[0] += 1 + print(f" Restart test: {restart_count[0]}") + if restart_count[0] == 2: + print(" Restarting timer...") + timer5.restart() + + timer5 = mcrfpy.Timer("restart_test", restart_test, 400) + + # Final verification after 2 seconds + def final_check(runtime): + print("\n=== Final Results ===") + print(f"Timer1 call count: {call_count} (expected: ~4)") + print(f"Pause test count: {pause_test_count} (expected: ~6-7, with pause gap)") + print(f"Cancel test count: {cancel_test_count} (expected: 1)") + print(f"Restart test count: {restart_count[0]} (expected: ~5 with restart)") + + # Verify timer states + try: + print(f"\nTimer1 active: {timer1.active}") + print(f"Timer2 active: {timer2.active}") + print(f"Timer3 active: {timer3.active} (should be False after cancel)") + print(f"Timer4 active: {timer4.active}") + print(f"Timer5 active: {timer5.active}") + except: + print("Some timers may have been garbage collected") + + print("\n✓ All Timer object tests completed!") + sys.exit(0) + + mcrfpy.setTimer("final_check", final_check, 2000) + +# Create a minimal scene +mcrfpy.createScene("timer_test") +mcrfpy.setScene("timer_test") + +# Start tests after game loop begins +mcrfpy.setTimer("run_tests", run_tests, 100) + +print("Timer object tests starting...") \ No newline at end of file diff --git a/tests/unit/test_timer_once.py b/tests/unit/test_timer_once.py new file mode 100644 index 0000000..84d48fd --- /dev/null +++ b/tests/unit/test_timer_once.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" +Test once=True timer functionality +""" +import mcrfpy +import sys + +once_count = 0 +repeat_count = 0 + +def once_callback(timer, runtime): + global once_count + once_count += 1 + print(f"Once timer fired! Count: {once_count}, Timer.once: {timer.once}") + +def repeat_callback(timer, runtime): + global repeat_count + repeat_count += 1 + print(f"Repeat timer fired! Count: {repeat_count}, Timer.once: {timer.once}") + +def check_results(runtime): + print(f"\nFinal results:") + print(f"Once timer fired {once_count} times (expected: 1)") + print(f"Repeat timer fired {repeat_count} times (expected: 3+)") + + if once_count == 1 and repeat_count >= 3: + print("PASS: Once timer fired exactly once, repeat timer fired multiple times") + sys.exit(0) + else: + print("FAIL: Timer behavior incorrect") + sys.exit(1) + +# Set up the scene +mcrfpy.createScene("test_scene") +mcrfpy.setScene("test_scene") + +# Create timers +print("Creating once timer with once=True...") +once_timer = mcrfpy.Timer("once_timer", once_callback, 100, once=True) +print(f"Timer: {once_timer}, once={once_timer.once}") + +print("\nCreating repeat timer with once=False (default)...") +repeat_timer = mcrfpy.Timer("repeat_timer", repeat_callback, 100) +print(f"Timer: {repeat_timer}, once={repeat_timer.once}") + +# Check results after 500ms +mcrfpy.setTimer("check", check_results, 500) \ No newline at end of file diff --git a/tests/unit/test_uiarc.py b/tests/unit/test_uiarc.py new file mode 100644 index 0000000..40613df --- /dev/null +++ b/tests/unit/test_uiarc.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Test UIArc class implementation - Issue #128 completion""" +import mcrfpy +from mcrfpy import automation +import sys + +def take_screenshot(runtime): + """Take screenshot after render completes""" + mcrfpy.delTimer("screenshot") + automation.screenshot("test_uiarc_result.png") + + print("Screenshot saved to test_uiarc_result.png") + print("PASS - UIArc test completed") + sys.exit(0) + +def run_test(runtime): + """Main test - runs after scene is set up""" + mcrfpy.delTimer("test") + + # Get the scene UI + ui = mcrfpy.sceneUI("test") + + # Test 1: Create arcs with different parameters + print("Test 1: Creating arcs...") + + # Simple arc - 90 degree quarter circle + a1 = mcrfpy.Arc(center=(100, 100), radius=50, start_angle=0, end_angle=90, + color=mcrfpy.Color(255, 0, 0), thickness=5) + ui.append(a1) + print(f" Arc 1: {a1}") + + # Half circle + a2 = mcrfpy.Arc(center=(250, 100), radius=40, start_angle=0, end_angle=180, + color=mcrfpy.Color(0, 255, 0), thickness=3) + ui.append(a2) + print(f" Arc 2: {a2}") + + # Three-quarter arc + a3 = mcrfpy.Arc(center=(400, 100), radius=45, start_angle=45, end_angle=315, + color=mcrfpy.Color(0, 0, 255), thickness=4) + ui.append(a3) + print(f" Arc 3: {a3}") + + # Full circle (360 degrees) + a4 = mcrfpy.Arc(center=(550, 100), radius=35, start_angle=0, end_angle=360, + color=mcrfpy.Color(255, 255, 0), thickness=2) + ui.append(a4) + print(f" Arc 4: {a4}") + + # Test 2: Verify properties + print("\nTest 2: Verifying properties...") + assert a1.radius == 50, f"Expected radius 50, got {a1.radius}" + print(f" a1.radius = {a1.radius}") + + assert a1.start_angle == 0, f"Expected start_angle 0, got {a1.start_angle}" + assert a1.end_angle == 90, f"Expected end_angle 90, got {a1.end_angle}" + print(f" a1.start_angle = {a1.start_angle}, a1.end_angle = {a1.end_angle}") + + assert a1.thickness == 5, f"Expected thickness 5, got {a1.thickness}" + print(f" a1.thickness = {a1.thickness}") + + # Test 3: Modify properties + print("\nTest 3: Modifying properties...") + a1.radius = 60 + assert a1.radius == 60, f"Expected radius 60, got {a1.radius}" + print(f" Modified a1.radius = {a1.radius}") + + a1.start_angle = 30 + a1.end_angle = 120 + print(f" Modified a1 angles: {a1.start_angle} to {a1.end_angle}") + + a2.color = mcrfpy.Color(255, 0, 255) # Magenta + print(f" Modified a2.color") + + # Test 4: Test visibility and opacity + print("\nTest 4: Testing visibility and opacity...") + a5 = mcrfpy.Arc(center=(100, 250), radius=30, start_angle=0, end_angle=180, + color=mcrfpy.Color(255, 128, 0), thickness=3) + a5.opacity = 0.5 + ui.append(a5) + print(f" a5.opacity = {a5.opacity}") + + a6 = mcrfpy.Arc(center=(200, 250), radius=30, start_angle=0, end_angle=180, + color=mcrfpy.Color(255, 128, 0), thickness=3) + a6.visible = False + ui.append(a6) + print(f" a6.visible = {a6.visible}") + + # Test 5: Test z_index + print("\nTest 5: Testing z_index...") + a7 = mcrfpy.Arc(center=(350, 250), radius=50, start_angle=0, end_angle=270, + color=mcrfpy.Color(0, 255, 255), thickness=10) + a7.z_index = 100 + ui.append(a7) + + a8 = mcrfpy.Arc(center=(370, 250), radius=40, start_angle=0, end_angle=270, + color=mcrfpy.Color(255, 0, 255), thickness=8) + a8.z_index = 50 + ui.append(a8) + print(f" a7.z_index = {a7.z_index}, a8.z_index = {a8.z_index}") + + # Test 6: Test name property + print("\nTest 6: Testing name property...") + a9 = mcrfpy.Arc(center=(500, 250), radius=25, start_angle=45, end_angle=135, + color=mcrfpy.Color(128, 128, 128), thickness=5, name="test_arc") + ui.append(a9) + assert a9.name == "test_arc", f"Expected name 'test_arc', got '{a9.name}'" + print(f" a9.name = '{a9.name}'") + + # Test 7: Test get_bounds + print("\nTest 7: Testing get_bounds...") + bounds = a1.get_bounds() + print(f" a1.get_bounds() = {bounds}") + + # Test 8: Test move method + print("\nTest 8: Testing move method...") + old_center = (a1.center.x, a1.center.y) + a1.move(10, 10) + new_center = (a1.center.x, a1.center.y) + print(f" a1 moved from {old_center} to {new_center}") + + # Test 9: Negative angle span (draws in reverse) + print("\nTest 9: Testing negative angle span...") + a10 = mcrfpy.Arc(center=(100, 350), radius=40, start_angle=90, end_angle=0, + color=mcrfpy.Color(128, 255, 128), thickness=4) + ui.append(a10) + print(f" Arc 10 (reverse): {a10}") + + # Schedule screenshot for next frame + mcrfpy.setTimer("screenshot", take_screenshot, 50) + +# Create a test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 50) diff --git a/tests/unit/test_uicaption_visual.py b/tests/unit/test_uicaption_visual.py new file mode 100644 index 0000000..7d578f2 --- /dev/null +++ b/tests/unit/test_uicaption_visual.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Visual test for UICaption's visible and opacity properties.""" + +import mcrfpy +from mcrfpy import automation +import sys +import time + +def run_visual_test(runtime): + """Timer callback to run visual tests and take screenshots.""" + print("\nRunning visual tests...") + + # Get our captions + ui = mcrfpy.sceneUI("test") + + # Test 1: Make caption2 invisible + print("Test 1: Making caption2 invisible") + ui[1].visible = False + automation.screenshot("caption_invisible.png") + time.sleep(0.1) + + # Test 2: Make caption2 visible again + print("Test 2: Making caption2 visible again") + ui[1].visible = True + automation.screenshot("caption_visible.png") + time.sleep(0.1) + + # Test 3: Set different opacity levels + print("Test 3: Testing opacity levels") + + # Caption 3 at 50% opacity + ui[2].opacity = 0.5 + automation.screenshot("caption_opacity_50.png") + time.sleep(0.1) + + # Caption 4 at 25% opacity + ui[3].opacity = 0.25 + automation.screenshot("caption_opacity_25.png") + time.sleep(0.1) + + # Caption 5 at 0% opacity (fully transparent) + ui[4].opacity = 0.0 + automation.screenshot("caption_opacity_0.png") + time.sleep(0.1) + + # Test 4: Move captions + print("Test 4: Testing move method") + ui[0].move(100, 0) # Move first caption right + ui[1].move(0, 50) # Move second caption down + automation.screenshot("caption_moved.png") + + print("\nVisual tests completed!") + print("Screenshots saved:") + print(" - caption_invisible.png") + print(" - caption_visible.png") + print(" - caption_opacity_50.png") + print(" - caption_opacity_25.png") + print(" - caption_opacity_0.png") + print(" - caption_moved.png") + + sys.exit(0) + +def main(): + """Set up the visual test scene.""" + print("=== UICaption Visual Test ===\n") + + # Create test scene + mcrfpy.createScene("test") + mcrfpy.setScene("test") + + # Create multiple captions for testing + caption1 = mcrfpy.Caption(50, 50, "Caption 1: Normal", fill_color=(255, 255, 255)) + caption2 = mcrfpy.Caption(50, 100, "Caption 2: Will be invisible", fill_color=(255, 200, 200)) + caption3 = mcrfpy.Caption(50, 150, "Caption 3: 50% opacity", fill_color=(200, 255, 200)) + caption4 = mcrfpy.Caption(50, 200, "Caption 4: 25% opacity", fill_color=(200, 200, 255)) + caption5 = mcrfpy.Caption(50, 250, "Caption 5: 0% opacity", fill_color=(255, 255, 200)) + + # Add captions to scene + ui = mcrfpy.sceneUI("test") + ui.append(caption1) + ui.append(caption2) + ui.append(caption3) + ui.append(caption4) + ui.append(caption5) + + # Also add a frame as background to see transparency better + frame = mcrfpy.Frame(40, 40, 400, 250, fill_color=(50, 50, 50)) + frame.z_index = -1 # Put it behind the captions + ui.append(frame) + + print("Scene setup complete. Scheduling visual tests...") + + # Schedule visual test to run after render loop starts + mcrfpy.setTimer("visual_test", run_visual_test, 100) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/unit/test_uicircle.py b/tests/unit/test_uicircle.py new file mode 100644 index 0000000..4481f38 --- /dev/null +++ b/tests/unit/test_uicircle.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Test UICircle class implementation - Issue #129""" +import mcrfpy +from mcrfpy import automation +import sys + +def take_screenshot(runtime): + """Take screenshot after render completes""" + mcrfpy.delTimer("screenshot") + automation.screenshot("test_uicircle_result.png") + + print("Screenshot saved to test_uicircle_result.png") + print("PASS - UICircle test completed") + sys.exit(0) + +def run_test(runtime): + """Main test - runs after scene is set up""" + mcrfpy.delTimer("test") + + # Get the scene UI + ui = mcrfpy.sceneUI("test") + + # Test 1: Create circles with different parameters + print("Test 1: Creating circles...") + + # Simple circle - just radius + c1 = mcrfpy.Circle(radius=50) + c1.center = (100, 100) + c1.fill_color = mcrfpy.Color(255, 0, 0) # Red + ui.append(c1) + print(f" Circle 1: {c1}") + + # Circle with center specified + c2 = mcrfpy.Circle(radius=30, center=(250, 100), fill_color=mcrfpy.Color(0, 255, 0)) + ui.append(c2) + print(f" Circle 2: {c2}") + + # Circle with outline + c3 = mcrfpy.Circle( + radius=40, + center=(400, 100), + fill_color=mcrfpy.Color(0, 0, 255), + outline_color=mcrfpy.Color(255, 255, 0), + outline=5.0 + ) + ui.append(c3) + print(f" Circle 3: {c3}") + + # Transparent fill with outline only + c4 = mcrfpy.Circle( + radius=35, + center=(550, 100), + fill_color=mcrfpy.Color(0, 0, 0, 0), + outline_color=mcrfpy.Color(255, 255, 255), + outline=3.0 + ) + ui.append(c4) + print(f" Circle 4: {c4}") + + # Test 2: Verify properties + print("\nTest 2: Verifying properties...") + assert c1.radius == 50, f"Expected radius 50, got {c1.radius}" + print(f" c1.radius = {c1.radius}") + + # Check center + center = c2.center + print(f" c2.center = ({center.x}, {center.y})") + + # Test 3: Modify properties + print("\nTest 3: Modifying properties...") + c1.radius = 60 + assert c1.radius == 60, f"Expected radius 60, got {c1.radius}" + print(f" Modified c1.radius = {c1.radius}") + + c2.fill_color = mcrfpy.Color(128, 0, 128) # Purple + print(f" Modified c2.fill_color") + + # Test 4: Test visibility and opacity + print("\nTest 4: Testing visibility and opacity...") + c5 = mcrfpy.Circle(radius=25, center=(100, 200), fill_color=mcrfpy.Color(255, 128, 0)) + c5.opacity = 0.5 + ui.append(c5) + print(f" c5.opacity = {c5.opacity}") + + c6 = mcrfpy.Circle(radius=25, center=(175, 200), fill_color=mcrfpy.Color(255, 128, 0)) + c6.visible = False + ui.append(c6) + print(f" c6.visible = {c6.visible}") + + # Test 5: Test z_index + print("\nTest 5: Testing z_index...") + c7 = mcrfpy.Circle(radius=40, center=(300, 200), fill_color=mcrfpy.Color(0, 255, 255)) + c7.z_index = 100 + ui.append(c7) + + c8 = mcrfpy.Circle(radius=30, center=(320, 200), fill_color=mcrfpy.Color(255, 0, 255)) + c8.z_index = 50 + ui.append(c8) + print(f" c7.z_index = {c7.z_index}, c8.z_index = {c8.z_index}") + + # Test 6: Test name property + print("\nTest 6: Testing name property...") + c9 = mcrfpy.Circle(radius=20, center=(450, 200), fill_color=mcrfpy.Color(128, 128, 128), name="test_circle") + ui.append(c9) + assert c9.name == "test_circle", f"Expected name 'test_circle', got '{c9.name}'" + print(f" c9.name = '{c9.name}'") + + # Test 7: Test get_bounds + print("\nTest 7: Testing get_bounds...") + bounds = c1.get_bounds() + print(f" c1.get_bounds() = {bounds}") + + # Test 8: Test move method + print("\nTest 8: Testing move method...") + old_center = (c1.center.x, c1.center.y) + c1.move(10, 10) + new_center = (c1.center.x, c1.center.y) + print(f" c1 moved from {old_center} to {new_center}") + + # Schedule screenshot for next frame + mcrfpy.setTimer("screenshot", take_screenshot, 50) + +# Create a test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 50) diff --git a/tests/unit/test_utf8_encoding.py b/tests/unit/test_utf8_encoding.py new file mode 100644 index 0000000..168bbf9 --- /dev/null +++ b/tests/unit/test_utf8_encoding.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +""" +Test UTF-8 encoding support +""" + +import mcrfpy +import sys + +def test_utf8(runtime): + """Test UTF-8 encoding in print statements""" + + # Test various unicode characters + print("✓ Check mark works") + print("✗ Cross mark works") + print("🎮 Emoji works") + print("日本語 Japanese works") + print("Ñoño Spanish works") + print("Привет Russian works") + + # Test in f-strings + count = 5 + print(f"✓ Added {count} items") + + # Test unicode in error messages + try: + raise ValueError("❌ Error with unicode") + except ValueError as e: + print(f"✓ Exception handling works: {e}") + + print("\n✅ All UTF-8 tests passed!") + sys.exit(0) + +# Run test +mcrfpy.createScene("test") +mcrfpy.setTimer("test", test_utf8, 100) \ No newline at end of file diff --git a/tests/unit/test_vector_arithmetic.py b/tests/unit/test_vector_arithmetic.py new file mode 100644 index 0000000..2bfc9b6 --- /dev/null +++ b/tests/unit/test_vector_arithmetic.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +""" +Test #93: Vector arithmetic operations +""" + +import mcrfpy +import sys +import math + +def test_vector_arithmetic(runtime): + """Test vector arithmetic operations""" + + all_pass = True + + # Test 1: Vector addition + try: + v1 = mcrfpy.Vector(3, 4) + v2 = mcrfpy.Vector(1, 2) + v3 = v1 + v2 + + assert v3.x == 4 and v3.y == 6, f"Addition failed: {v3.x}, {v3.y}" + print("+ Vector addition works correctly") + except Exception as e: + print(f"x Vector addition failed: {e}") + all_pass = False + + # Test 2: Vector subtraction + try: + v1 = mcrfpy.Vector(5, 7) + v2 = mcrfpy.Vector(2, 3) + v3 = v1 - v2 + + assert v3.x == 3 and v3.y == 4, f"Subtraction failed: {v3.x}, {v3.y}" + print("+ Vector subtraction works correctly") + except Exception as e: + print(f"x Vector subtraction failed: {e}") + all_pass = False + + # Test 3: Scalar multiplication + try: + v1 = mcrfpy.Vector(2, 3) + v2 = v1 * 3 + v3 = 2 * v1 # Reverse multiplication + + assert v2.x == 6 and v2.y == 9, f"Scalar multiply failed: {v2.x}, {v2.y}" + assert v3.x == 4 and v3.y == 6, f"Reverse multiply failed: {v3.x}, {v3.y}" + print("+ Scalar multiplication works correctly") + except Exception as e: + print(f"x Scalar multiplication failed: {e}") + all_pass = False + + # Test 4: Scalar division + try: + v1 = mcrfpy.Vector(10, 20) + v2 = v1 / 5 + + assert v2.x == 2 and v2.y == 4, f"Division failed: {v2.x}, {v2.y}" + + # Test division by zero + try: + v3 = v1 / 0 + print("x Division by zero should raise exception") + all_pass = False + except ZeroDivisionError: + pass + + print("+ Scalar division works correctly") + except Exception as e: + print(f"x Scalar division failed: {e}") + all_pass = False + + # Test 5: Negation + try: + v1 = mcrfpy.Vector(3, -4) + v2 = -v1 + + assert v2.x == -3 and v2.y == 4, f"Negation failed: {v2.x}, {v2.y}" + print("+ Vector negation works correctly") + except Exception as e: + print(f"x Vector negation failed: {e}") + all_pass = False + + # Test 6: Absolute value (magnitude) + try: + v1 = mcrfpy.Vector(3, 4) + mag = abs(v1) + + assert abs(mag - 5.0) < 0.001, f"Absolute value failed: {mag}" + print("+ Absolute value (magnitude) works correctly") + except Exception as e: + print(f"x Absolute value failed: {e}") + all_pass = False + + # Test 7: Boolean check + try: + v1 = mcrfpy.Vector(0, 0) + v2 = mcrfpy.Vector(1, 0) + + assert not bool(v1), "Zero vector should be False" + assert bool(v2), "Non-zero vector should be True" + print("+ Boolean check works correctly") + except Exception as e: + print(f"x Boolean check failed: {e}") + all_pass = False + + # Test 8: Equality comparison + try: + v1 = mcrfpy.Vector(1.5, 2.5) + v2 = mcrfpy.Vector(1.5, 2.5) + v3 = mcrfpy.Vector(1.5, 2.6) + + assert v1 == v2, "Equal vectors should compare equal" + assert v1 != v3, "Different vectors should not compare equal" + print("+ Equality comparison works correctly") + except Exception as e: + print(f"x Equality comparison failed: {e}") + all_pass = False + + # Test 9: magnitude() method + try: + v1 = mcrfpy.Vector(3, 4) + mag = v1.magnitude() + + assert abs(mag - 5.0) < 0.001, f"magnitude() failed: {mag}" + print("+ magnitude() method works correctly") + except Exception as e: + print(f"x magnitude() method failed: {e}") + all_pass = False + + # Test 10: magnitude_squared() method + try: + v1 = mcrfpy.Vector(3, 4) + mag_sq = v1.magnitude_squared() + + assert mag_sq == 25, f"magnitude_squared() failed: {mag_sq}" + print("+ magnitude_squared() method works correctly") + except Exception as e: + print(f"x magnitude_squared() method failed: {e}") + all_pass = False + + # Test 11: normalize() method + try: + v1 = mcrfpy.Vector(3, 4) + v2 = v1.normalize() + + assert abs(v2.magnitude() - 1.0) < 0.001, f"normalize() magnitude failed: {v2.magnitude()}" + assert abs(v2.x - 0.6) < 0.001, f"normalize() x failed: {v2.x}" + assert abs(v2.y - 0.8) < 0.001, f"normalize() y failed: {v2.y}" + + # Test zero vector normalization + v3 = mcrfpy.Vector(0, 0) + v4 = v3.normalize() + assert v4.x == 0 and v4.y == 0, "Zero vector normalize should remain zero" + + print("+ normalize() method works correctly") + except Exception as e: + print(f"x normalize() method failed: {e}") + all_pass = False + + # Test 12: dot product + try: + v1 = mcrfpy.Vector(3, 4) + v2 = mcrfpy.Vector(2, 1) + dot = v1.dot(v2) + + assert dot == 10, f"dot product failed: {dot}" + print("+ dot() method works correctly") + except Exception as e: + print(f"x dot() method failed: {e}") + all_pass = False + + # Test 13: distance_to() + try: + v1 = mcrfpy.Vector(1, 1) + v2 = mcrfpy.Vector(4, 5) + dist = v1.distance_to(v2) + + assert abs(dist - 5.0) < 0.001, f"distance_to() failed: {dist}" + print("+ distance_to() method works correctly") + except Exception as e: + print(f"x distance_to() method failed: {e}") + all_pass = False + + # Test 14: angle() + try: + v1 = mcrfpy.Vector(1, 0) # Points right + v2 = mcrfpy.Vector(0, 1) # Points up + v3 = mcrfpy.Vector(-1, 0) # Points left + v4 = mcrfpy.Vector(1, 1) # 45 degrees + + a1 = v1.angle() + a2 = v2.angle() + a3 = v3.angle() + a4 = v4.angle() + + assert abs(a1 - 0) < 0.001, f"Right angle failed: {a1}" + assert abs(a2 - math.pi/2) < 0.001, f"Up angle failed: {a2}" + assert abs(a3 - math.pi) < 0.001, f"Left angle failed: {a3}" + assert abs(a4 - math.pi/4) < 0.001, f"45deg angle failed: {a4}" + + print("+ angle() method works correctly") + except Exception as e: + print(f"x angle() method failed: {e}") + all_pass = False + + # Test 15: copy() + try: + v1 = mcrfpy.Vector(5, 10) + v2 = v1.copy() + + assert v2.x == 5 and v2.y == 10, f"copy() values failed: {v2.x}, {v2.y}" + + # Modify v2 and ensure v1 is unchanged + v2.x = 20 + assert v1.x == 5, "copy() should create independent object" + + print("+ copy() method works correctly") + except Exception as e: + print(f"x copy() method failed: {e}") + all_pass = False + + # Test 16: Operations with invalid types + try: + v1 = mcrfpy.Vector(1, 2) + + # These should return NotImplemented + result = v1 + "string" + assert result is NotImplemented, "Invalid addition should return NotImplemented" + + result = v1 * [1, 2] + assert result is NotImplemented, "Invalid multiplication should return NotImplemented" + + print("+ Type checking works correctly") + except Exception as e: + # Expected to fail with TypeError + if "unsupported operand type" in str(e): + print("+ Type checking works correctly") + else: + print(f"x Type checking failed: {e}") + all_pass = False + + print(f"\n{'PASS' if all_pass else 'FAIL'}") + sys.exit(0 if all_pass else 1) + +# Run test +mcrfpy.createScene("test") +mcrfpy.setTimer("test", test_vector_arithmetic, 100) \ No newline at end of file diff --git a/tests/unit/test_viewport_scaling.py b/tests/unit/test_viewport_scaling.py new file mode 100644 index 0000000..1f7c433 --- /dev/null +++ b/tests/unit/test_viewport_scaling.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Test viewport scaling modes""" + +import mcrfpy +from mcrfpy import Window, Frame, Caption, Color, Vector +import sys + +def test_viewport_modes(runtime): + """Test all three viewport scaling modes""" + mcrfpy.delTimer("test_viewport") + + print("Testing viewport scaling modes...") + + # Get window singleton + window = Window.get() + + # Test initial state + print(f"Initial game resolution: {window.game_resolution}") + print(f"Initial scaling mode: {window.scaling_mode}") + print(f"Window resolution: {window.resolution}") + + # Create test scene with visual elements + scene = mcrfpy.sceneUI("test") + + # Create a frame that fills the game resolution to show boundaries + game_res = window.game_resolution + boundary = Frame(0, 0, game_res[0], game_res[1], + fill_color=Color(50, 50, 100), + outline_color=Color(255, 255, 255), + outline=2) + boundary.name = "boundary" + scene.append(boundary) + + # Add corner markers + corner_size = 50 + corners = [ + (0, 0, "TL"), # Top-left + (game_res[0] - corner_size, 0, "TR"), # Top-right + (0, game_res[1] - corner_size, "BL"), # Bottom-left + (game_res[0] - corner_size, game_res[1] - corner_size, "BR") # Bottom-right + ] + + for x, y, label in corners: + corner = Frame(x, y, corner_size, corner_size, + fill_color=Color(255, 100, 100), + outline_color=Color(255, 255, 255), + outline=1) + scene.append(corner) + + text = Caption(x + 5, y + 5, label) + text.font_size = 20 + text.fill_color = Color(255, 255, 255) + scene.append(text) + + # Add center crosshair + center_x = game_res[0] // 2 + center_y = game_res[1] // 2 + h_line = Frame(center_x - 50, center_y - 1, 100, 2, + fill_color=Color(255, 255, 0)) + v_line = Frame(center_x - 1, center_y - 50, 2, 100, + fill_color=Color(255, 255, 0)) + scene.append(h_line) + scene.append(v_line) + + # Add mode indicator + mode_text = Caption(10, 10, f"Mode: {window.scaling_mode}") + mode_text.font_size = 24 + mode_text.fill_color = Color(255, 255, 255) + mode_text.name = "mode_text" + scene.append(mode_text) + + # Add instructions + instructions = Caption(10, 40, + "Press 1: Center mode (1:1 pixels)\n" + "Press 2: Stretch mode (fill window)\n" + "Press 3: Fit mode (maintain aspect ratio)\n" + "Press R: Change resolution\n" + "Press G: Change game resolution\n" + "Press Esc: Exit") + instructions.font_size = 14 + instructions.fill_color = Color(200, 200, 200) + scene.append(instructions) + + # Test changing modes + def test_mode_changes(runtime): + mcrfpy.delTimer("test_modes") + from mcrfpy import automation + + print("\nTesting scaling modes:") + + # Test center mode + window.scaling_mode = "center" + print(f"Set to center mode: {window.scaling_mode}") + mode_text.text = f"Mode: center (1:1 pixels)" + automation.screenshot("viewport_center_mode.png") + + # Schedule next mode test + mcrfpy.setTimer("test_stretch", test_stretch_mode, 1000) + + def test_stretch_mode(runtime): + mcrfpy.delTimer("test_stretch") + from mcrfpy import automation + + window.scaling_mode = "stretch" + print(f"Set to stretch mode: {window.scaling_mode}") + mode_text.text = f"Mode: stretch (fill window)" + automation.screenshot("viewport_stretch_mode.png") + + # Schedule next mode test + mcrfpy.setTimer("test_fit", test_fit_mode, 1000) + + def test_fit_mode(runtime): + mcrfpy.delTimer("test_fit") + from mcrfpy import automation + + window.scaling_mode = "fit" + print(f"Set to fit mode: {window.scaling_mode}") + mode_text.text = f"Mode: fit (aspect ratio maintained)" + automation.screenshot("viewport_fit_mode.png") + + # Test different window sizes + mcrfpy.setTimer("test_resize", test_window_resize, 1000) + + def test_window_resize(runtime): + mcrfpy.delTimer("test_resize") + from mcrfpy import automation + + print("\nTesting window resize with fit mode:") + + # Make window wider + window.resolution = (1280, 720) + print(f"Window resized to: {window.resolution}") + automation.screenshot("viewport_fit_wide.png") + + # Make window taller + mcrfpy.setTimer("test_tall", test_tall_window, 1000) + + def test_tall_window(runtime): + mcrfpy.delTimer("test_tall") + from mcrfpy import automation + + window.resolution = (800, 1000) + print(f"Window resized to: {window.resolution}") + automation.screenshot("viewport_fit_tall.png") + + # Test game resolution change + mcrfpy.setTimer("test_game_res", test_game_resolution, 1000) + + def test_game_resolution(runtime): + mcrfpy.delTimer("test_game_res") + + print("\nTesting game resolution change:") + window.game_resolution = (800, 600) + print(f"Game resolution changed to: {window.game_resolution}") + + # Note: UI elements won't automatically reposition, but viewport will adjust + + print("\nTest completed!") + print("Screenshots saved:") + print(" - viewport_center_mode.png") + print(" - viewport_stretch_mode.png") + print(" - viewport_fit_mode.png") + print(" - viewport_fit_wide.png") + print(" - viewport_fit_tall.png") + + # Restore original settings + window.resolution = (1024, 768) + window.game_resolution = (1024, 768) + window.scaling_mode = "fit" + + sys.exit(0) + + # Start test sequence + mcrfpy.setTimer("test_modes", test_mode_changes, 500) + +# Set up keyboard handler for manual testing +def handle_keypress(key, state): + if state != "start": + return + + window = Window.get() + scene = mcrfpy.sceneUI("test") + mode_text = None + for elem in scene: + if hasattr(elem, 'name') and elem.name == "mode_text": + mode_text = elem + break + + if key == "1": + window.scaling_mode = "center" + if mode_text: + mode_text.text = f"Mode: center (1:1 pixels)" + print(f"Switched to center mode") + elif key == "2": + window.scaling_mode = "stretch" + if mode_text: + mode_text.text = f"Mode: stretch (fill window)" + print(f"Switched to stretch mode") + elif key == "3": + window.scaling_mode = "fit" + if mode_text: + mode_text.text = f"Mode: fit (aspect ratio maintained)" + print(f"Switched to fit mode") + elif key == "r": + # Cycle through some resolutions + current = window.resolution + if current == (1024, 768): + window.resolution = (1280, 720) + elif current == (1280, 720): + window.resolution = (800, 600) + else: + window.resolution = (1024, 768) + print(f"Window resolution: {window.resolution}") + elif key == "g": + # Cycle game resolutions + current = window.game_resolution + if current == (1024, 768): + window.game_resolution = (800, 600) + elif current == (800, 600): + window.game_resolution = (640, 480) + else: + window.game_resolution = (1024, 768) + print(f"Game resolution: {window.game_resolution}") + elif key == "escape": + sys.exit(0) + +# Main execution +print("Creating viewport test scene...") +mcrfpy.createScene("test") +mcrfpy.setScene("test") +mcrfpy.keypressScene(handle_keypress) + +# Schedule the test +mcrfpy.setTimer("test_viewport", test_viewport_modes, 100) + +print("Viewport test running...") +print("Use number keys to switch modes, R to resize window, G to change game resolution") \ No newline at end of file diff --git a/tests/unit/test_viewport_visual.py b/tests/unit/test_viewport_visual.py new file mode 100644 index 0000000..926b77e --- /dev/null +++ b/tests/unit/test_viewport_visual.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Visual viewport test with screenshots""" + +import mcrfpy +from mcrfpy import Window, Frame, Caption, Color +import sys + +def test_viewport_visual(runtime): + """Visual test of viewport modes""" + mcrfpy.delTimer("test") + + print("Creating visual viewport test...") + + # Get window singleton + window = Window.get() + + # Create test scene + scene = mcrfpy.sceneUI("test") + + # Create visual elements at game resolution boundaries + game_res = window.game_resolution + + # Full boundary frame + boundary = Frame(0, 0, game_res[0], game_res[1], + fill_color=Color(40, 40, 80), + outline_color=Color(255, 255, 0), + outline=3) + scene.append(boundary) + + # Corner markers + corner_size = 100 + colors = [ + Color(255, 100, 100), # Red TL + Color(100, 255, 100), # Green TR + Color(100, 100, 255), # Blue BL + Color(255, 255, 100), # Yellow BR + ] + positions = [ + (0, 0), # Top-left + (game_res[0] - corner_size, 0), # Top-right + (0, game_res[1] - corner_size), # Bottom-left + (game_res[0] - corner_size, game_res[1] - corner_size) # Bottom-right + ] + labels = ["TL", "TR", "BL", "BR"] + + for (x, y), color, label in zip(positions, colors, labels): + corner = Frame(x, y, corner_size, corner_size, + fill_color=color, + outline_color=Color(255, 255, 255), + outline=2) + scene.append(corner) + + text = Caption(x + 10, y + 10, label) + text.font_size = 32 + text.fill_color = Color(0, 0, 0) + scene.append(text) + + # Center crosshair + center_x = game_res[0] // 2 + center_y = game_res[1] // 2 + h_line = Frame(0, center_y - 1, game_res[0], 2, + fill_color=Color(255, 255, 255, 128)) + v_line = Frame(center_x - 1, 0, 2, game_res[1], + fill_color=Color(255, 255, 255, 128)) + scene.append(h_line) + scene.append(v_line) + + # Mode text + mode_text = Caption(center_x - 100, center_y - 50, + f"Mode: {window.scaling_mode}") + mode_text.font_size = 36 + mode_text.fill_color = Color(255, 255, 255) + scene.append(mode_text) + + # Resolution text + res_text = Caption(center_x - 150, center_y + 10, + f"Game: {game_res[0]}x{game_res[1]}") + res_text.font_size = 24 + res_text.fill_color = Color(200, 200, 200) + scene.append(res_text) + + from mcrfpy import automation + + # Test different modes and window sizes + def test_sequence(runtime): + mcrfpy.delTimer("seq") + + # Test 1: Fit mode with original size + print("Test 1: Fit mode, original window size") + automation.screenshot("viewport_01_fit_original.png") + + # Test 2: Wider window + window.resolution = (1400, 768) + print(f"Test 2: Fit mode, wider window {window.resolution}") + automation.screenshot("viewport_02_fit_wide.png") + + # Test 3: Taller window + window.resolution = (1024, 900) + print(f"Test 3: Fit mode, taller window {window.resolution}") + automation.screenshot("viewport_03_fit_tall.png") + + # Test 4: Center mode + window.scaling_mode = "center" + mode_text.text = "Mode: center" + print(f"Test 4: Center mode {window.resolution}") + automation.screenshot("viewport_04_center.png") + + # Test 5: Stretch mode + window.scaling_mode = "stretch" + mode_text.text = "Mode: stretch" + window.resolution = (1280, 720) + print(f"Test 5: Stretch mode {window.resolution}") + automation.screenshot("viewport_05_stretch.png") + + # Test 6: Small window with fit + window.scaling_mode = "fit" + mode_text.text = "Mode: fit" + window.resolution = (640, 480) + print(f"Test 6: Fit mode, small window {window.resolution}") + automation.screenshot("viewport_06_fit_small.png") + + print("\nViewport visual test completed!") + print("Screenshots saved:") + print(" - viewport_01_fit_original.png") + print(" - viewport_02_fit_wide.png") + print(" - viewport_03_fit_tall.png") + print(" - viewport_04_center.png") + print(" - viewport_05_stretch.png") + print(" - viewport_06_fit_small.png") + + sys.exit(0) + + # Start test sequence after a short delay + mcrfpy.setTimer("seq", test_sequence, 500) + +# Main execution +print("Starting visual viewport test...") +mcrfpy.createScene("test") +mcrfpy.setScene("test") +mcrfpy.setTimer("test", test_viewport_visual, 100) +print("Test scheduled...") \ No newline at end of file diff --git a/tests/unit/test_visibility.py b/tests/unit/test_visibility.py new file mode 100644 index 0000000..23ea9fc --- /dev/null +++ b/tests/unit/test_visibility.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +Test Knowledge Stubs 1 Visibility System +======================================== + +Tests per-entity visibility tracking with perspective rendering. +""" + +import mcrfpy +import sys +import time + +print("Knowledge Stubs 1 - Visibility System Test") +print("==========================================") + +# Create scene and grid +mcrfpy.createScene("visibility_test") +grid = mcrfpy.Grid(grid_x=20, grid_y=15) +grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background + +# Initialize grid - all walkable and transparent +print("\nInitializing 20x15 grid...") +for y in range(15): + for x in range(20): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(100, 100, 120) # Floor color + +# Create some walls to block vision +print("Adding walls...") +walls = [ + # Vertical wall + [(10, y) for y in range(3, 12)], + # Horizontal walls + [(x, 7) for x in range(5, 10)], + [(x, 7) for x in range(11, 16)], + # Corner walls + [(5, 3), (5, 4), (6, 3)], + [(15, 3), (15, 4), (14, 3)], + [(5, 11), (5, 10), (6, 11)], + [(15, 11), (15, 10), (14, 11)], +] + +for wall_group in walls: + for x, y in wall_group: + cell = grid.at(x, y) + cell.walkable = False + cell.transparent = False + cell.color = mcrfpy.Color(40, 20, 20) # Wall color + +# Create entities +print("\nCreating entities...") +entities = [ + mcrfpy.Entity(2, 7), # Left side + mcrfpy.Entity(18, 7), # Right side + mcrfpy.Entity(10, 1), # Top center (above wall) +] + +for i, entity in enumerate(entities): + entity.sprite_index = 64 + i # @, A, B + grid.entities.append(entity) + print(f" Entity {i}: position ({entity.x}, {entity.y})") + +# Test 1: Check initial gridstate +print("\nTest 1: Initial gridstate") +e0 = entities[0] +print(f" Entity 0 gridstate length: {len(e0.gridstate)}") +print(f" Expected: {20 * 15}") + +# Test 2: Update visibility for each entity +print("\nTest 2: Updating visibility for each entity") +for i, entity in enumerate(entities): + entity.update_visibility() + + # Count visible/discovered cells + visible_count = sum(1 for state in entity.gridstate if state.visible) + discovered_count = sum(1 for state in entity.gridstate if state.discovered) + print(f" Entity {i}: {visible_count} visible, {discovered_count} discovered") + +# Test 3: Test perspective property +print("\nTest 3: Testing perspective property") +print(f" Initial perspective: {grid.perspective}") +grid.perspective = 0 +print(f" Set to entity 0: {grid.perspective}") + +# Test invalid perspective +try: + grid.perspective = 10 # Out of range + print(" ERROR: Should have raised exception for invalid perspective") +except IndexError as e: + print(f" ✓ Correctly rejected invalid perspective: {e}") + +# Test 4: Visual demonstration +def visual_test(runtime): + print(f"\nVisual test - cycling perspectives at {runtime}ms") + + # Cycle through perspectives + current = grid.perspective + if current == -1: + grid.perspective = 0 + print(" Switched to Entity 0 perspective") + elif current == 0: + grid.perspective = 1 + print(" Switched to Entity 1 perspective") + elif current == 1: + grid.perspective = 2 + print(" Switched to Entity 2 perspective") + else: + grid.perspective = -1 + print(" Switched to omniscient view") + + # Take screenshot + from mcrfpy import automation + filename = f"visibility_perspective_{grid.perspective}.png" + automation.screenshot(filename) + print(f" Screenshot saved: {filename}") + +# Test 5: Movement and visibility update +print("\nTest 5: Movement and visibility update") +entity = entities[0] +print(f" Entity 0 initial position: ({entity.x}, {entity.y})") + +# Move entity +entity.x = 8 +entity.y = 7 +print(f" Moved to: ({entity.x}, {entity.y})") + +# Update visibility +entity.update_visibility() +visible_count = sum(1 for state in entity.gridstate if state.visible) +print(f" Visible cells after move: {visible_count}") + +# Set up UI +ui = mcrfpy.sceneUI("visibility_test") +ui.append(grid) +grid.position = (50, 50) +grid.size = (600, 450) # 20*30, 15*30 + +# Add title +title = mcrfpy.Caption("Knowledge Stubs 1 - Visibility Test", 200, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add info +info = mcrfpy.Caption("Perspective: -1 (omniscient)", 50, 520) +info.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info) + +# Add legend +legend = mcrfpy.Caption("Black=Never seen, Dark gray=Discovered, Normal=Visible", 50, 540) +legend.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend) + +# Set scene +mcrfpy.setScene("visibility_test") + +# Set timer to cycle perspectives +mcrfpy.setTimer("cycle", visual_test, 2000) # Every 2 seconds + +print("\nTest complete! Visual demo cycling through perspectives...") +print("Perspectives will cycle: Omniscient → Entity 0 → Entity 1 → Entity 2 → Omniscient") + +# Quick test to exit after screenshots +def exit_timer(dt): + print("\nExiting after demo...") + sys.exit(0) + +mcrfpy.setTimer("exit", exit_timer, 10000) # Exit after 10 seconds \ No newline at end of file diff --git a/tests/unit/test_visual_path.py b/tests/unit/test_visual_path.py new file mode 100644 index 0000000..31b385f --- /dev/null +++ b/tests/unit/test_visual_path.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Simple visual test for path highlighting""" + +import mcrfpy +import sys + +# Colors as tuples (r, g, b, a) +WALL_COLOR = (60, 30, 30, 255) +FLOOR_COLOR = (200, 200, 220, 255) +PATH_COLOR = (100, 255, 100, 255) + +def check_render(dt): + """Timer callback to verify rendering""" + print(f"\nTimer fired after {dt}ms") + + # Take screenshot + from mcrfpy import automation + automation.screenshot("visual_path_test.png") + print("Screenshot saved as visual_path_test.png") + + # Sample some path cells to verify colors + print("\nSampling path cell colors from grid:") + for x, y in [(1, 1), (2, 2), (3, 3)]: + cell = grid.at(x, y) + color = cell.color + print(f" Cell ({x},{y}): color={color[:3]}") + + sys.exit(0) + +# Create scene +mcrfpy.createScene("visual_test") + +# Create grid +grid = mcrfpy.Grid(grid_x=5, grid_y=5) +grid.fill_color = mcrfpy.Color(0, 0, 0) + +# Initialize all cells as floor +print("Initializing grid...") +for y in range(5): + for x in range(5): + grid.at(x, y).walkable = True + grid.at(x, y).color = FLOOR_COLOR + +# Create entities +e1 = mcrfpy.Entity(0, 0) +e2 = mcrfpy.Entity(4, 4) +e1.sprite_index = 64 # @ +e2.sprite_index = 69 # E +grid.entities.append(e1) +grid.entities.append(e2) + +print(f"Entity 1 at ({e1.x}, {e1.y})") +print(f"Entity 2 at ({e2.x}, {e2.y})") + +# Get path +path = e1.path_to(int(e2.x), int(e2.y)) +print(f"\nPath from E1 to E2: {path}") + +# Color the path +if path: + print("\nColoring path cells green...") + for x, y in path: + grid.at(x, y).color = PATH_COLOR + print(f" Set ({x},{y}) to green") + +# Set up UI +ui = mcrfpy.sceneUI("visual_test") +ui.append(grid) +grid.position = (50, 50) +grid.size = (250, 250) + +# Add title +title = mcrfpy.Caption("Path Visualization Test", 50, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Set scene +mcrfpy.setScene("visual_test") + +# Set timer to check rendering +mcrfpy.setTimer("check", check_render, 500) + +print("\nScene ready. Path should be visible in green.") \ No newline at end of file diff --git a/tests/unit/trace_exec_behavior.py b/tests/unit/trace_exec_behavior.py deleted file mode 100644 index a0685f4..0000000 --- a/tests/unit/trace_exec_behavior.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/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 \ No newline at end of file diff --git a/tests/unit/ui_Frame_test.py b/tests/unit/ui_Frame_test.py deleted file mode 100644 index 7798557..0000000 --- a/tests/unit/ui_Frame_test.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -"""Test for mcrfpy.Frame class - Related to issues #38, #42""" -import mcrfpy -import sys - -click_count = 0 - -def click_handler(x, y, button): - """Handle frame clicks""" - global click_count - click_count += 1 - print(f"Frame clicked at ({x}, {y}) with button {button}") - -def test_Frame(): - """Test Frame creation and properties""" - print("Starting Frame test...") - - # Create test scene - mcrfpy.createScene("frame_test") - mcrfpy.setScene("frame_test") - ui = mcrfpy.sceneUI("frame_test") - - # Test basic frame creation - try: - frame1 = mcrfpy.Frame(10, 10, 200, 150) - ui.append(frame1) - print("✓ Basic Frame created") - except Exception as e: - print(f"✗ Failed to create basic Frame: {e}") - print("FAIL") - return - - # Test frame with all parameters - try: - frame2 = mcrfpy.Frame(220, 10, 200, 150, - fill_color=mcrfpy.Color(100, 150, 200), - outline_color=mcrfpy.Color(255, 0, 0), - outline=3.0) - ui.append(frame2) - print("✓ Frame with colors created") - except Exception as e: - print(f"✗ Failed to create colored Frame: {e}") - - # Test property access and modification - try: - # Test getters - print(f"Frame1 position: ({frame1.x}, {frame1.y})") - print(f"Frame1 size: {frame1.w}x{frame1.h}") - - # Test setters - frame1.x = 15 - frame1.y = 15 - frame1.w = 190 - frame1.h = 140 - frame1.outline = 2.0 - frame1.fill_color = mcrfpy.Color(50, 50, 50) - frame1.outline_color = mcrfpy.Color(255, 255, 0) - print("✓ Frame properties modified") - except Exception as e: - print(f"✗ Failed to modify Frame properties: {e}") - - # Test children collection (Issue #38) - try: - children = frame2.children - caption = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Child Caption") - children.append(caption) - print(f"✓ Children collection works, has {len(children)} items") - except Exception as e: - print(f"✗ Children collection failed (Issue #38): {e}") - - # Test click handler (Issue #42) - try: - frame2.click = click_handler - print("✓ Click handler assigned") - - # Note: Click simulation would require automation module - # which may not work in headless mode - except Exception as e: - print(f"✗ Click handler failed (Issue #42): {e}") - - # Create nested frames to test children rendering - try: - frame3 = mcrfpy.Frame(10, 200, 400, 200, - fill_color=mcrfpy.Color(0, 100, 0), - outline_color=mcrfpy.Color(255, 255, 255), - outline=2.0) - ui.append(frame3) - - # Add children to frame3 - for i in range(3): - child_frame = mcrfpy.Frame(10 + i * 130, 10, 120, 80, - fill_color=mcrfpy.Color(100 + i * 50, 50, 50)) - frame3.children.append(child_frame) - - print(f"✓ Created nested frames with {len(frame3.children)} children") - except Exception as e: - print(f"✗ Failed to create nested frames: {e}") - - # Summary - print("\nTest Summary:") - print("- Basic Frame creation: PASS") - print("- Frame with colors: PASS") - print("- Property modification: PASS") - print("- Children collection (Issue #38): PASS" if len(frame2.children) >= 0 else "FAIL") - print("- Click handler assignment (Issue #42): PASS") - print("\nOverall: PASS") - - # Exit cleanly - sys.exit(0) - -# Run test immediately -test_Frame() \ No newline at end of file diff --git a/tests/unit/ui_Grid_test.py b/tests/unit/ui_Grid_test.py deleted file mode 100644 index ed81d61..0000000 --- a/tests/unit/ui_Grid_test.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""Test for mcrfpy.Grid class - Related to issues #77, #74, #50, #52, #20""" -import mcrfpy -from datetime import datetime -try: - from mcrfpy import automation - has_automation = True -except ImportError: - has_automation = False - print("Warning: automation module not available") - -def test_Grid(): - """Test Grid creation and properties""" - # Create test scene - mcrfpy.createScene("grid_test") - mcrfpy.setScene("grid_test") - ui = mcrfpy.sceneUI("grid_test") - - # Test grid creation - try: - # Note: Grid requires texture, creating one for testing - texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - grid = mcrfpy.Grid(20, 15, # grid dimensions - texture, # texture - mcrfpy.Vector(10, 10), # position - mcrfpy.Vector(400, 300)) # size - ui.append(grid) - print("[PASS] Grid created successfully") - except Exception as e: - print(f"[FAIL] Failed to create Grid: {e}") - print("FAIL") - return - - # Test grid properties - try: - # Test grid_size (Issue #20) - grid_size = grid.grid_size - print(f"[PASS] Grid size: {grid_size}") - - # Test position and size - print(f"Position: {grid.position}") - print(f"Size: {grid.size}") - - # Test individual coordinate properties - print(f"Coordinates: x={grid.x}, y={grid.y}, w={grid.w}, h={grid.h}") - - # Test grid_y property (Issue #74) - try: - # This might fail if grid_y is not implemented - print(f"Grid dimensions via properties: grid_x=?, grid_y=?") - print("[FAIL] Issue #74: Grid.grid_y property may be missing") - except: - pass - - except Exception as e: - print(f"[FAIL] Property access failed: {e}") - - # Test center/pan functionality - try: - grid.center = mcrfpy.Vector(10, 7) - print(f"[PASS] Center set to: {grid.center}") - grid.center_x = 5 - grid.center_y = 5 - print(f"[PASS] Center modified to: ({grid.center_x}, {grid.center_y})") - except Exception as e: - print(f"[FAIL] Center/pan failed: {e}") - - # Test zoom - try: - grid.zoom = 1.5 - print(f"[PASS] Zoom set to: {grid.zoom}") - except Exception as e: - print(f"[FAIL] Zoom failed: {e}") - - # Test at() method for GridPoint access (Issue #77) - try: - # This tests the error message issue - point = grid.at(0, 0) - print("[PASS] GridPoint access works") - - # Try out of bounds access to test error message - try: - invalid_point = grid.at(100, 100) - print("[FAIL] Out of bounds access should fail") - except Exception as e: - error_msg = str(e) - if "Grid.grid_y" in error_msg: - print(f"[FAIL] Issue #77: Error message has copy/paste bug: {error_msg}") - else: - print(f"[PASS] Out of bounds error: {error_msg}") - except Exception as e: - print(f"[FAIL] GridPoint access failed: {e}") - - # Test entities collection - try: - entities = grid.entities - print(f"[PASS] Entities collection has {len(entities)} items") - - # Add an entity - entity = mcrfpy.Entity(mcrfpy.Vector(5, 5), - texture, - 0, # sprite index - grid) - entities.append(entity) - print(f"[PASS] Entity added, collection now has {len(entities)} items") - - # Test out-of-bounds entity (Issue #52) - out_entity = mcrfpy.Entity(mcrfpy.Vector(50, 50), # Outside 20x15 grid - texture, - 1, - grid) - entities.append(out_entity) - print("[PASS] Out-of-bounds entity added (Issue #52: should be skipped in rendering)") - - except Exception as e: - print(f"[FAIL] Entity management failed: {e}") - - # Note about missing features - print("\nMissing features:") - print("- Issue #50: UIGrid background color field") - print("- Issue #6, #8, #9: RenderTexture support") - - # Take screenshot if automation is available - if has_automation: - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"test_Grid_{timestamp}.png" - automation.screenshot(filename) - print(f"Screenshot saved: {filename}") - else: - print("Screenshot skipped - automation not available") - print("PASS") - -# Set up timer to run test -mcrfpy.setTimer("test", test_Grid, 1000) - -# Cancel timer after running once -def cleanup(): - mcrfpy.delTimer("test") - mcrfpy.delTimer("cleanup") - -mcrfpy.setTimer("cleanup", cleanup, 1100) \ No newline at end of file