From f4343e1e823cb5dd41758ef5b8a09d7e881dfaf7 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 15 Jul 2025 21:30:49 -0400 Subject: [PATCH] Squashed commit of the following: [alpha_presentable] Author: John McCardle Co-Authored-By: Claude commit dc47f2474c7b2642d368f9772894aed857527807 the UIEntity rant commit 673ca8e1b089ea670257fc04ae1a676ed95a40ed I forget when these tests were written, but I want them in the squash merge commit 70c71565c684fa96e222179271ecb13a156d80ad Fix UI object segfault by switching from managed to manual weakref management The UI types (Frame, Caption, Sprite, Grid, Entity) were using Py_TPFLAGS_MANAGED_WEAKREF while also trying to manually create weakrefs for the PythonObjectCache. This is fundamentally incompatible - when Python manages weakrefs internally, PyWeakref_NewRef() cannot access the weakref list properly, causing segfaults. Changed all UI types to use manual weakref management (like PyTimer): - Restored weakreflist field in all UI type structures - Removed Py_TPFLAGS_MANAGED_WEAKREF from all UI type flags - Added tp_weaklistoffset for all UI types in module initialization - Initialize weakreflist=NULL in tp_new and init methods - Call PyObject_ClearWeakRefs() in dealloc functions This allows the PythonObjectCache to continue working correctly, maintaining Python object identity for C++ objects across the boundary. Fixes segfault when creating UI objects (e.g., Caption, Grid) that was preventing tutorial scripts from running. This is the bulk of the required behavior for Issue #126. that issure isn't ready for closure yet; several other sub-issues left. closes #110 mention issue #109 - resolves some __init__ related nuisances commit 3dce3ec539ae99e32d869007bf3f49d03e4e2f89 Refactor timer system for cleaner architecture and enhanced functionality Major improvements to the timer system: - Unified all timer logic in the Timer class (C++) - Removed PyTimerCallable subclass, now using PyCallable directly - Timer objects are now passed to callbacks as first argument - Added 'once' parameter for one-shot timers that auto-stop - Implemented proper PythonObjectCache integration with weakref support API enhancements: - New callback signature: callback(timer, runtime) instead of just (runtime) - Timer objects expose: name, interval, remaining, paused, active, once properties - Methods: pause(), resume(), cancel(), restart() - Comprehensive documentation with examples - Enhanced repr showing timer state (active/paused/once/remaining time) This cleanup follows the UIEntity/PyUIEntity pattern and makes the timer system more Pythonic while maintaining backward compatibility through the legacy setTimer/delTimer API. closes #121 commit 145834cfc31b8dabc4cb3591b9cb4ed99fc8b964 Implement Python object cache to preserve derived types in collections Add a global cache system that maintains weak references to Python objects, ensuring that derived Python classes maintain their identity when stored in and retrieved from C++ collections. Key changes: - Add PythonObjectCache singleton with serial number system - Each cacheable object (UIDrawable, UIEntity, Timer, Animation) gets unique ID - Cache stores weak references to prevent circular reference memory leaks - Update all UI type definitions to support weak references (Py_TPFLAGS_MANAGED_WEAKREF) - Enable subclassing for all UI types (Py_TPFLAGS_BASETYPE) - Collections check cache before creating new Python wrappers - Register objects in cache during __init__ methods - Clean up cache entries in C++ destructors This ensures that Python code like: ```python class MyFrame(mcrfpy.Frame): def __init__(self): super().__init__() self.custom_data = "preserved" frame = MyFrame() scene.ui.append(frame) retrieved = scene.ui[0] # Same MyFrame instance with custom_data intact ``` Works correctly, with retrieved maintaining the derived type and custom attributes. Closes #112 commit c5e7e8e29835a69f4c50f3c99fd3123012635a9a Update test demos for new Python API and entity system - Update all text input demos to use new Entity constructor signature - Fix pathfinding showcase to work with new entity position handling - Remove entity_waypoints tracking in favor of simplified movement - Delete obsolete exhaustive_api_demo.py (superseded by newer demos) - Adjust entity creation calls to match Entity((x, y), texture, sprite_index) pattern commit 6d29652ae7418745dc24066532454167d447df89 Update animation demo suite with crash fixes and improvements - Add warnings about AnimationManager segfault bug in sizzle_reel_final.py - Create sizzle_reel_final_fixed.py that works around the crash by hiding objects instead of removing them - Increase font sizes for better visibility in demos - Extend demo durations for better showcase of animations - Remove debug prints from animation_sizzle_reel_working.py - Minor cleanup and improvements to all animation demos commit a010e5fa968feaba620dcf2eda44fb9514512151 Update game scripts for new Python API - Convert entity position access from tuple to x/y properties - Update caption size property to font_size - Fix grid boundary checks to use grid_size instead of exceptions - Clean up demo timer on menu exit to prevent callbacks These changes adapt the game scripts to work with the new standardized Python API constructors and property names. commit 9c8d6c459109be883cb8070b8ef83c60bfc1a970 Fix click event z-order handling in PyScene Changed click detection to properly respect z-index by: - Sorting ui_elements in-place when needed (same as render order) - Using reverse iterators to check highest z-index elements first - This ensures top-most elements receive clicks before lower ones commit dcd1b0ca33d46639023221f4d7d52000b947dbdf Add roguelike tutorial implementation files Implement Parts 0-2 of the classic roguelike tutorial adapted for McRogueFace: - Part 0: Basic grid setup and tile rendering - Part 1: Drawing '@' symbol and basic movement - Part 1b: Variant with sprite-based player - Part 2: Entity system and NPC implementation with three movement variants: - part_2.py: Standard implementation - part_2-naive.py: Naive movement approach - part_2-onemovequeued.py: Queued movement system Includes tutorial assets: - tutorial2.png: Tileset for dungeon tiles - tutorial_hero.png: Player sprite sheet commit 6813fb5129738cca2d79c80304834523561ba7fb Standardize Python API constructors and remove PyArgHelpers - Remove PyArgHelpers.h and all macro-based argument parsing - Convert all UI class constructors to use PyArg_ParseTupleAndKeywords - Standardize constructor signatures across UICaption, UIEntity, UIFrame, UIGrid, and UISprite - Replace PYARGHELPER_SINGLE/MULTI macros with explicit argument parsing - Improve error messages and argument validation - Maintain backward compatibility with existing Python code This change improves code maintainability and consistency across the Python API. commit 6f67fbb51efaf70e52fba8c939298dcdff50450a Fix animation callback crashes from iterator invalidation (#119) Resolved segfaults caused by creating new animations from within animation callbacks. The issue was iterator invalidation in AnimationManager::update() when callbacks modified the active animations vector. Changes: - Add deferred animation queue to AnimationManager - New animations created during update are queued and added after - Set isUpdating flag to track when in update loop - Properly handle Animation destructor during callback execution - Add clearCallback() method for safe cleanup scenarios This fixes the "free(): invalid pointer" and "malloc(): unaligned fastbin chunk detected" errors that occurred with rapid animation creation in callbacks. commit eb88c7b3aab3da519db7569106c34f3510b6e963 Add animation completion callbacks (#119) Implement callbacks that fire when animations complete, enabling direct causality between animation end and game state changes. This eliminates race conditions from parallel timer workarounds. - Add optional callback parameter to Animation constructor - Callbacks execute synchronously when animation completes - Proper Python reference counting with GIL safety - Callbacks receive (anim, target) parameters (currently None) - Exception handling prevents crashes from Python errors Example usage: ```python def on_complete(anim, target): player_moving = False anim = mcrfpy.Animation("x", 300.0, 1.0, "easeOut", callback=on_complete) anim.start(player) ``` closes #119 commit 9fb428dd0176a4d7cfad09deb7509d8aa5562868 Update ROADMAP with GitHub issue numbers (#111-#125) Added issue numbers from GitHub tracker to roadmap items: - #111: Grid Click Events Broken in Headless - #112: Object Splitting Bug (Python type preservation) - #113: Batch Operations for Grid - #114: CellView API - #115: SpatialHash Implementation - #116: Dirty Flag System - #117: Memory Pool for Entities - #118: Scene as Drawable - #119: Animation Completion Callbacks - #120: Animation Property Locking - #121: Timer Object System - #122: Parent-Child UI System - #123: Grid Subgrid System - #124: Grid Point Animation - #125: GitHub Issues Automation Also updated existing references: - #101/#110: Constructor standardization - #109: Vector class indexing Note: Tutorial-specific items and Python-implementable features (input queue, collision reservation) are not tracked as engine issues. commit 062e4dadc42833bf5a3559e5d7c4ceb4abb7e9c0 Fix animation segfaults with RAII weak_ptr implementation Resolved two critical segmentation faults in AnimationManager: 1. Race condition when creating multiple animations in timer callbacks 2. Exit crash when animations outlive their target objects Changes: - Replace raw pointers with std::weak_ptr for automatic target invalidation - Add Animation::complete() to jump animations to final value - Add Animation::hasValidTarget() to check if target still exists - Update AnimationManager to auto-remove invalid animations - Add AnimationManager::clear() call to GameEngine::cleanup() - Update Python bindings to pass shared_ptr instead of raw pointers This ensures animations can never reference destroyed objects, following proper RAII principles. Tested with sizzle_reel_final.py and stress tests creating/destroying hundreds of animated objects. commit 98fc49a978ec792ee6096f40fd4e19841b8ec6a3 Directory structure cleanup and organization overhaul --- .gitignore | 1 + ALPHA_STREAMLINE_WORKLOG.md | 1093 ----------- README.md | 67 +- build_windows_cmake.bat | 42 + css_colors.txt | 157 -- docs/API_REFERENCE_COMPLETE.md | 284 +-- docs/api_reference_complete.html | 1629 ----------------- docs/api_reference_dynamic.html | 1059 +++++++++++ docs/stubs/mcrfpy.pyi | 532 ++++++ docs/stubs/mcrfpy/__init__.pyi | 209 +++ docs/stubs/mcrfpy/automation.pyi | 24 + docs/stubs/py.typed | 0 roguelike_tutorial/part_0.py | 80 + roguelike_tutorial/part_1.py | 116 ++ roguelike_tutorial/part_1b.py | 117 ++ roguelike_tutorial/part_2-naive.py | 149 ++ roguelike_tutorial/part_2-onemovequeued.py | 241 +++ roguelike_tutorial/part_2.py | 149 ++ roguelike_tutorial/tutorial2.png | Bin 0 -> 5741 bytes roguelike_tutorial/tutorial_hero.png | Bin 0 -> 16742 bytes src/Animation.cpp | 272 ++- src/Animation.h | 49 +- src/GameEngine.cpp | 19 +- src/GameEngine.h | 5 +- src/McRFPy_API.cpp | 8 + src/PyAnimation.cpp | 89 +- src/PyAnimation.h | 2 + src/PyArgHelpers.h | 410 ----- src/PyCallable.cpp | 112 +- src/PyCallable.h | 45 +- src/PyScene.cpp | 17 +- src/PyTimer.cpp | 95 +- src/PyTimer.h | 38 +- src/PythonObjectCache.cpp | 85 + src/PythonObjectCache.h | 40 + src/Timer.cpp | 127 +- src/Timer.h | 47 +- src/UIBase.h | 5 + src/UICaption.cpp | 278 ++- src/UICaption.h | 35 +- src/UICollection.cpp | 13 + src/UIDrawable.cpp | 104 ++ src/UIDrawable.h | 12 + src/UIEntity.cpp | 154 +- src/UIEntity.h | 57 +- src/UIFrame.cpp | 197 +- src/UIFrame.h | 34 +- src/UIGrid.cpp | 255 +-- src/UIGrid.h | 70 +- src/UISprite.cpp | 171 +- src/UISprite.h | 47 +- src/scripts/cos_entities.py | 28 +- src/scripts/cos_tiles.py | 13 +- src/scripts/game.py | 17 +- .../generate_caption_screenshot_fixed.py | 0 .../generate_docs_screenshots_simple.py | 0 .../generate_entity_screenshot_fixed.py | 0 tests/{ => archive}/path_vision_fixed.py | 0 tests/{ => archive}/ui_Grid_test_simple.py | 0 .../automation_click_issue78_analysis.py | 0 .../automation_click_issue78_test.py | 0 .../automation_screenshot_test.py | 0 .../automation_screenshot_test_simple.py | 0 .../issue_12_gridpoint_instantiation_test.py | 0 ...issue_26_28_iterator_comprehensive_test.py | 0 tests/{ => bugs}/issue_37_simple_test.py | 0 tests/{ => bugs}/issue_37_test.py | 0 ...e_37_windows_scripts_comprehensive_test.py | 0 tests/{ => bugs}/issue_76_test.py | 0 .../issue_76_uientitycollection_type_test.py | 0 .../issue_79_color_properties_test.py | 0 .../issue_80_caption_font_size_test.py | 0 ...ue_81_sprite_index_standardization_test.py | 0 .../issue_82_sprite_scale_xy_test.py | 0 .../issue_83_position_tuple_test.py | 0 .../{ => bugs}/issue_84_pos_property_test.py | 0 .../issue_95_uicollection_repr_test.py | 0 .../issue_96_uicollection_extend_test.py | 0 .../issue_99_texture_font_properties_test.py | 0 tests/{ => bugs}/issue_9_minimal_test.py | 0 .../issue_9_rendertexture_resize_test.py | 0 tests/{ => bugs}/issue_9_simple_test.py | 0 tests/{ => bugs}/issue_9_test.py | 0 tests/constructor_audit.py | 215 +++ tests/count_format_string.py | 30 + tests/demo_animation_callback_usage.py | 81 + tests/{ => demos}/animation_demo.py | 0 tests/demos/animation_demo_safe.py | 146 ++ tests/demos/animation_sizzle_reel.py | 616 +++++++ tests/demos/animation_sizzle_reel_fixed.py | 227 +++ tests/demos/animation_sizzle_reel_v2.py | 307 ++++ tests/demos/animation_sizzle_reel_working.py | 316 ++++ tests/demos/api_demo_final.py | 207 +++ tests/demos/debug_astar_demo.py | 99 + tests/demos/dijkstra_demo_working.py | 137 ++ tests/demos/exhaustive_api_demo_fixed.py | 306 ++++ tests/demos/path_vision_sizzle_reel.py | 391 ++++ tests/demos/pathfinding_showcase.py | 377 ++++ tests/demos/simple_text_input.py | 226 +++ tests/demos/sizzle_reel_final.py | 190 ++ tests/demos/sizzle_reel_final_fixed.py | 193 ++ tests/demos/text_input_demo.py | 149 ++ tests/demos/text_input_standalone.py | 320 ++++ tests/demos/text_input_widget.py | 320 ++++ tests/integration/astar_vs_dijkstra.py | 235 +++ tests/integration/debug_visibility.py | 59 + tests/integration/dijkstra_all_paths.py | 234 +++ tests/integration/dijkstra_cycle_paths.py | 236 +++ tests/integration/dijkstra_debug.py | 161 ++ tests/integration/dijkstra_interactive.py | 244 +++ .../dijkstra_interactive_enhanced.py | 344 ++++ tests/integration/dijkstra_test.py | 146 ++ .../force_non_interactive.py | 0 tests/integration/interactive_visibility.py | 201 ++ .../simple_interactive_visibility.py | 46 + tests/integration/simple_visibility_test.py | 39 + tests/{ => integration}/trace_interactive.py | 0 tests/test_stdin_theory.py | 32 - tests/unified_click_example.cpp | 101 + .../WORKING_automation_test_example.py | 0 tests/{ => unit}/api_createScene_test.py | 0 tests/{ => unit}/api_keypressScene_test.py | 0 tests/{ => unit}/api_sceneUI_test.py | 0 .../api_setScene_currentScene_test.py | 0 tests/{ => unit}/api_timer_test.py | 0 tests/unit/check_entity_attrs.py | 4 + tests/unit/debug_empty_paths.py | 80 + tests/{ => unit}/debug_render_test.py | 0 tests/{ => unit}/empty_script.py | 0 tests/{ => unit}/exit_immediately_test.py | 0 tests/{ => unit}/generate_docs_screenshots.py | 0 tests/{ => unit}/generate_grid_screenshot.py | 0 .../{ => unit}/generate_sprite_screenshot.py | 0 tests/unit/grid_at_argument_test.py | 100 + .../keypress_scene_validation_test.py | 0 tests/{ => unit}/run_issue_tests.py | 0 .../screenshot_transparency_fix_test.py | 0 tests/{ => unit}/simple_screenshot_test.py | 0 .../simple_timer_screenshot_test.py | 0 tests/{ => unit}/trace_exec_behavior.py | 0 tests/{ => unit}/ui_Entity_issue73_test.py | 0 tests/{ => unit}/ui_Frame_test.py | 0 tests/{ => unit}/ui_Frame_test_detailed.py | 0 tests/{ => unit}/ui_Grid_none_texture_test.py | 0 tests/{ => unit}/ui_Grid_null_texture_test.py | 0 tests/{ => unit}/ui_Grid_test.py | 0 tests/{ => unit}/ui_Grid_test_no_grid.py | 0 tests/{ => unit}/ui_Sprite_issue19_test.py | 0 .../ui_UICollection_issue69_test.py | 0 tests/{ => unit}/validate_screenshot_test.py | 0 tests/{ => unit}/working_timer_test.py | 0 .../generate_api_docs.py | 0 .../generate_api_docs_html.py | 0 .../generate_api_docs_simple.py | 0 .../generate_color_table.py | 0 .../generate_complete_api_docs.py | 0 .../generate_complete_markdown_docs.py | 0 tools/generate_dynamic_docs.py | 510 ++++++ tools/generate_stubs.py | 268 +++ .../generate_stubs_v2.py | 0 tools/ui_methods_documentation.py | 344 ++++ wikicrayons_colors.txt | 168 -- xkcd_colors.txt | 949 ---------- 163 files changed, 12812 insertions(+), 5441 deletions(-) delete mode 100644 ALPHA_STREAMLINE_WORKLOG.md create mode 100644 build_windows_cmake.bat delete mode 100644 css_colors.txt delete mode 100644 docs/api_reference_complete.html create mode 100644 docs/api_reference_dynamic.html create mode 100644 docs/stubs/mcrfpy.pyi create mode 100644 docs/stubs/mcrfpy/__init__.pyi create mode 100644 docs/stubs/mcrfpy/automation.pyi create mode 100644 docs/stubs/py.typed create mode 100644 roguelike_tutorial/part_0.py create mode 100644 roguelike_tutorial/part_1.py create mode 100644 roguelike_tutorial/part_1b.py create mode 100644 roguelike_tutorial/part_2-naive.py create mode 100644 roguelike_tutorial/part_2-onemovequeued.py create mode 100644 roguelike_tutorial/part_2.py create mode 100644 roguelike_tutorial/tutorial2.png create mode 100644 roguelike_tutorial/tutorial_hero.png delete mode 100644 src/PyArgHelpers.h create mode 100644 src/PythonObjectCache.cpp create mode 100644 src/PythonObjectCache.h rename tests/{ => archive}/generate_caption_screenshot_fixed.py (100%) rename tests/{ => archive}/generate_docs_screenshots_simple.py (100%) rename tests/{ => archive}/generate_entity_screenshot_fixed.py (100%) rename tests/{ => archive}/path_vision_fixed.py (100%) rename tests/{ => archive}/ui_Grid_test_simple.py (100%) rename tests/{ => automation}/automation_click_issue78_analysis.py (100%) rename tests/{ => automation}/automation_click_issue78_test.py (100%) rename tests/{ => automation}/automation_screenshot_test.py (100%) rename tests/{ => automation}/automation_screenshot_test_simple.py (100%) rename tests/{ => bugs}/issue_12_gridpoint_instantiation_test.py (100%) rename tests/{ => bugs}/issue_26_28_iterator_comprehensive_test.py (100%) rename tests/{ => bugs}/issue_37_simple_test.py (100%) rename tests/{ => bugs}/issue_37_test.py (100%) rename tests/{ => bugs}/issue_37_windows_scripts_comprehensive_test.py (100%) rename tests/{ => bugs}/issue_76_test.py (100%) rename tests/{ => bugs}/issue_76_uientitycollection_type_test.py (100%) rename tests/{ => bugs}/issue_79_color_properties_test.py (100%) rename tests/{ => bugs}/issue_80_caption_font_size_test.py (100%) rename tests/{ => bugs}/issue_81_sprite_index_standardization_test.py (100%) rename tests/{ => bugs}/issue_82_sprite_scale_xy_test.py (100%) rename tests/{ => bugs}/issue_83_position_tuple_test.py (100%) rename tests/{ => bugs}/issue_84_pos_property_test.py (100%) rename tests/{ => bugs}/issue_95_uicollection_repr_test.py (100%) rename tests/{ => bugs}/issue_96_uicollection_extend_test.py (100%) rename tests/{ => bugs}/issue_99_texture_font_properties_test.py (100%) rename tests/{ => bugs}/issue_9_minimal_test.py (100%) rename tests/{ => bugs}/issue_9_rendertexture_resize_test.py (100%) rename tests/{ => bugs}/issue_9_simple_test.py (100%) rename tests/{ => bugs}/issue_9_test.py (100%) create mode 100644 tests/constructor_audit.py create mode 100644 tests/count_format_string.py create mode 100644 tests/demo_animation_callback_usage.py rename tests/{ => demos}/animation_demo.py (100%) create mode 100644 tests/demos/animation_demo_safe.py create mode 100644 tests/demos/animation_sizzle_reel.py create mode 100644 tests/demos/animation_sizzle_reel_fixed.py create mode 100644 tests/demos/animation_sizzle_reel_v2.py create mode 100644 tests/demos/animation_sizzle_reel_working.py create mode 100644 tests/demos/api_demo_final.py create mode 100644 tests/demos/debug_astar_demo.py create mode 100644 tests/demos/dijkstra_demo_working.py create mode 100644 tests/demos/exhaustive_api_demo_fixed.py create mode 100644 tests/demos/path_vision_sizzle_reel.py create mode 100644 tests/demos/pathfinding_showcase.py create mode 100644 tests/demos/simple_text_input.py create mode 100644 tests/demos/sizzle_reel_final.py create mode 100644 tests/demos/sizzle_reel_final_fixed.py create mode 100644 tests/demos/text_input_demo.py create mode 100644 tests/demos/text_input_standalone.py create mode 100644 tests/demos/text_input_widget.py create mode 100644 tests/integration/astar_vs_dijkstra.py create mode 100644 tests/integration/debug_visibility.py create mode 100644 tests/integration/dijkstra_all_paths.py create mode 100644 tests/integration/dijkstra_cycle_paths.py create mode 100644 tests/integration/dijkstra_debug.py create mode 100644 tests/integration/dijkstra_interactive.py create mode 100644 tests/integration/dijkstra_interactive_enhanced.py create mode 100644 tests/integration/dijkstra_test.py rename tests/{ => integration}/force_non_interactive.py (100%) create mode 100644 tests/integration/interactive_visibility.py create mode 100644 tests/integration/simple_interactive_visibility.py create mode 100644 tests/integration/simple_visibility_test.py rename tests/{ => integration}/trace_interactive.py (100%) delete mode 100644 tests/test_stdin_theory.py create mode 100644 tests/unified_click_example.cpp rename tests/{ => unit}/WORKING_automation_test_example.py (100%) rename tests/{ => unit}/api_createScene_test.py (100%) rename tests/{ => unit}/api_keypressScene_test.py (100%) rename tests/{ => unit}/api_sceneUI_test.py (100%) rename tests/{ => unit}/api_setScene_currentScene_test.py (100%) rename tests/{ => unit}/api_timer_test.py (100%) create mode 100644 tests/unit/check_entity_attrs.py create mode 100644 tests/unit/debug_empty_paths.py rename tests/{ => unit}/debug_render_test.py (100%) rename tests/{ => unit}/empty_script.py (100%) rename tests/{ => unit}/exit_immediately_test.py (100%) rename tests/{ => unit}/generate_docs_screenshots.py (100%) rename tests/{ => unit}/generate_grid_screenshot.py (100%) rename tests/{ => unit}/generate_sprite_screenshot.py (100%) create mode 100644 tests/unit/grid_at_argument_test.py rename tests/{ => unit}/keypress_scene_validation_test.py (100%) rename tests/{ => unit}/run_issue_tests.py (100%) rename tests/{ => unit}/screenshot_transparency_fix_test.py (100%) rename tests/{ => unit}/simple_screenshot_test.py (100%) rename tests/{ => unit}/simple_timer_screenshot_test.py (100%) rename tests/{ => unit}/trace_exec_behavior.py (100%) rename tests/{ => unit}/ui_Entity_issue73_test.py (100%) rename tests/{ => unit}/ui_Frame_test.py (100%) rename tests/{ => unit}/ui_Frame_test_detailed.py (100%) rename tests/{ => unit}/ui_Grid_none_texture_test.py (100%) rename tests/{ => unit}/ui_Grid_null_texture_test.py (100%) rename tests/{ => unit}/ui_Grid_test.py (100%) rename tests/{ => unit}/ui_Grid_test_no_grid.py (100%) rename tests/{ => unit}/ui_Sprite_issue19_test.py (100%) rename tests/{ => unit}/ui_UICollection_issue69_test.py (100%) rename tests/{ => unit}/validate_screenshot_test.py (100%) rename tests/{ => unit}/working_timer_test.py (100%) rename generate_api_docs.py => tools/generate_api_docs.py (100%) rename generate_api_docs_html.py => tools/generate_api_docs_html.py (100%) rename generate_api_docs_simple.py => tools/generate_api_docs_simple.py (100%) rename generate_color_table.py => tools/generate_color_table.py (100%) rename generate_complete_api_docs.py => tools/generate_complete_api_docs.py (100%) rename generate_complete_markdown_docs.py => tools/generate_complete_markdown_docs.py (100%) create mode 100644 tools/generate_dynamic_docs.py create mode 100644 tools/generate_stubs.py rename generate_stubs_v2.py => tools/generate_stubs_v2.py (100%) create mode 100644 tools/ui_methods_documentation.py delete mode 100644 wikicrayons_colors.txt delete mode 100644 xkcd_colors.txt diff --git a/.gitignore b/.gitignore index 174f159..aaee9ad 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ scripts/ test_* tcod_reference +.archive diff --git a/ALPHA_STREAMLINE_WORKLOG.md b/ALPHA_STREAMLINE_WORKLOG.md deleted file mode 100644 index e6ada2b..0000000 --- a/ALPHA_STREAMLINE_WORKLOG.md +++ /dev/null @@ -1,1093 +0,0 @@ -# Alpha Streamline 2 Work Log - -## Phase 6: Rendering Revolution - -### Task: RenderTexture Base Infrastructure (#6 - Part 1) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement opt-in RenderTexture support in UIDrawable base class and enable clipping for UIFrame - -**Implementation**: -1. Added RenderTexture infrastructure to UIDrawable: - - `std::unique_ptr render_texture` - - `sf::Sprite render_sprite` - - `bool use_render_texture` and `bool render_dirty` flags - - `enableRenderTexture()` and `markDirty()` methods -2. Implemented clip_children property for UIFrame: - - Python property getter/setter - - Automatic RenderTexture creation when enabled - - Proper handling of nested render contexts -3. Updated UIFrame::render() to support clipping: - - Renders frame and children to RenderTexture when clipping enabled - - Handles coordinate transformations correctly - - Optimizes by only re-rendering when dirty -4. Added dirty flag propagation: - - All property setters call markDirty() - - Size changes recreate RenderTexture - - Animation system integration - -**Technical Details**: -- RenderTexture created lazily on first use -- Size matches frame dimensions, recreated on resize -- Children rendered at local coordinates (0,0) in texture -- Final texture drawn at frame's world position -- Transparent background preserves alpha blending - -**Test Results**: -- Basic clipping works correctly - children are clipped to parent bounds -- Nested clipping (frames within frames) works properly -- Dynamic resizing recreates RenderTexture as needed -- No performance regression for non-clipped frames -- Memory usage reasonable (textures only created when needed) - -**Result**: Foundation laid for advanced rendering features. UIFrame can now clip children to bounds, enabling professional UI layouts. Architecture supports future effects like blur, glow, and shaders. - ---- - -### Task: Grid Background Colors (#50) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Add customizable background color to UIGrid - -**Implementation**: -1. Added `sf::Color background_color` member to UIGrid class -2. Implemented Python property getter/setter for background_color -3. Updated UIGrid::render() to clear RenderTexture with background color -4. Added animation support for individual color components: - - background_color.r, background_color.g, background_color.b, background_color.a -5. Default background color set to dark gray (8, 8, 8, 255) - -**Test Results**: -- Background color properly renders behind grid content -- Python property access works correctly -- Color animation would work with Animation system -- No performance impact - -**Result**: Quick win completed. Grids now have customizable background colors, improving visual flexibility for game developers. - ---- - -## Phase 5: Window/Scene Architecture - -### Task: Window Object Singleton (#34) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement Window singleton object with access to resolution, fullscreen, vsync properties - -**Implementation**: -1. Created PyWindow.h/cpp with singleton pattern -2. Window.get() class method returns singleton instance -3. Properties implemented: - - resolution: Get/set window resolution as (width, height) tuple - - fullscreen: Toggle fullscreen mode - - vsync: Enable/disable vertical sync - - title: Get/set window title string - - visible: Window visibility state - - framerate_limit: FPS limit (0 for unlimited) -4. Methods implemented: - - center(): Center window on screen - - screenshot(filename=None): Take screenshot to file or return bytes -5. Proper handling for headless mode - -**Technical Details**: -- Uses static singleton instance -- Window properties tracked in GameEngine for persistence -- Resolution/fullscreen changes recreate window with SFML -- Screenshot supports both RenderWindow and RenderTexture targets - -**Test Results**: -- Singleton pattern works correctly -- All properties accessible and modifiable -- Screenshot functionality works in both modes -- Center method appropriately fails in headless mode - -**Result**: Window singleton provides clean Python API for window control. Games can now easily manage window properties and take screenshots. - ---- - -### Task: Object-Oriented Scene Support (#61) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Create Python Scene class that can be subclassed with methods like on_keypress(), on_enter(), on_exit() - -**Implementation**: -1. Created PySceneObject.h/cpp with Python Scene type -2. Scene class features: - - Can be subclassed in Python - - Constructor creates underlying C++ PyScene - - Lifecycle methods: on_enter(), on_exit(), on_keypress(key, state), update(dt) - - Properties: name (string), active (bool) - - Methods: activate(), get_ui(), register_keyboard(callable) -3. Integration with GameEngine: - - changeScene() triggers on_exit/on_enter callbacks - - update() called each frame with delta time - - Maintains registry of Python scene objects -4. Backward compatibility maintained with existing scene API - -**Technical Details**: -- PySceneObject wraps C++ PyScene -- Python objects stored in static registry by name -- GIL management for thread-safe callbacks -- Lifecycle events triggered from C++ side -- Update loop integrated with game loop - -**Usage Example**: -```python -class MenuScene(mcrfpy.Scene): - def __init__(self): - super().__init__("menu") - # Create UI elements - - def on_enter(self): - print("Entering menu") - - def on_keypress(self, key, state): - if key == "Space" and state == "start": - mcrfpy.setScene("game") - - def update(self, dt): - # Update logic - pass - -menu = MenuScene() -menu.activate() -``` - -**Test Results**: -- Scene creation and subclassing works -- Lifecycle callbacks (on_enter, on_exit) trigger correctly -- update() called each frame with proper delta time -- Scene switching preserves Python object state -- Properties and methods accessible - -**Result**: Object-oriented scenes provide a much more Pythonic and maintainable way to structure game code. Developers can now use inheritance, encapsulation, and clean method overrides instead of registering callback functions. - ---- - -### Task: Window Resize Events (#1) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Enable window resize events to trigger scene.on_resize(width, height) callbacks - -**Implementation**: -1. Added `triggerResize(int width, int height)` to McRFPy_API -2. Enabled window resizing by adding `sf::Style::Resize` to window creation -3. Modified GameEngine::processEvent() to handle resize events: - - Updates the view to match new window size - - Calls McRFPy_API::triggerResize() to notify Python scenes -4. PySceneClass already had `call_on_resize()` method implemented -5. Python Scene objects can override `on_resize(self, width, height)` - -**Technical Details**: -- Window style changed from `Titlebar | Close` to `Titlebar | Close | Resize` -- Resize event updates `visible` view with new dimensions -- Only the active scene receives resize notifications -- Resize callbacks work the same as other lifecycle events - -**Test Results**: -- Window is now resizable by dragging edges/corners -- Python scenes receive resize callbacks with new dimensions -- View properly adjusts to maintain correct coordinate system -- Manual testing required (can't resize in headless mode) - -**Result**: Window resize events are now fully functional. Games can respond to window size changes by overriding the `on_resize` method in their Scene classes. This enables responsive UI layouts and proper view adjustments. - ---- - -### Task: Scene Transitions (#105) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement smooth scene transitions with methods like fade_to() and slide_out() - -**Implementation**: -1. Created SceneTransition class to manage transition state and rendering -2. Added transition support to GameEngine: - - New overload: `changeScene(sceneName, transitionType, duration)` - - Transition types: Fade, SlideLeft, SlideRight, SlideUp, SlideDown - - Renders both scenes to textures during transition - - Smooth easing function for natural motion -3. Extended Python API: - - `mcrfpy.setScene(scene, transition=None, duration=0.0)` - - Transition strings: "fade", "slide_left", "slide_right", "slide_up", "slide_down" -4. Integrated with render loop: - - Transitions update each frame - - Scene lifecycle events trigger after transition completes - - Normal rendering resumes when transition finishes - -**Technical Details**: -- Uses sf::RenderTexture to capture scene states -- Transitions manipulate sprite alpha (fade) or position (slides) -- Easing function: smooth ease-in-out curve -- Duration specified in seconds (float) -- Immediate switch if duration <= 0 or transition is None - -**Test Results**: -- All transition types work correctly -- Smooth animations between scenes -- Lifecycle events (on_exit, on_enter) properly sequenced -- API is clean and intuitive - -**Usage Example**: -```python -# Fade transition over 1 second -mcrfpy.setScene("menu", "fade", 1.0) - -# Slide left transition over 0.5 seconds -mcrfpy.setScene("game", "slide_left", 0.5) - -# Instant transition (no animation) -mcrfpy.setScene("credits") -``` - -**Result**: Scene transitions provide a professional polish to games. The implementation leverages SFML's render textures for smooth, GPU-accelerated transitions. Games can now have cinematic scene changes that enhance the player experience. - ---- - -### Task: SFML Exposure Research (#14) - -**Status**: Research Completed -**Date**: 2025-07-06 - -**Research Summary**: -1. Analyzed current SFML usage in McRogueFace: - - Using SFML 2.6.1 (built from source in modules/SFML) - - Moderate to heavy integration with SFML types throughout codebase - - Already exposing Color, Vector, Font, and Texture to Python - - All rendering, input, and audio systems depend on SFML - -2. Evaluated python-sfml (pysfml): - - Last version 2.3.2 only supports SFML 2.3.2 (incompatible with our 2.6.1) - - Project appears abandoned since ~2019 - - No viable maintained alternatives found - - Installation issues widely reported - -3. Recommendation: **Direct Integration** - - Implement `mcrfpy.sfml` as built-in module - - Maintain API compatibility with python-sfml where sensible - - Gives full control and ensures version compatibility - - Can selectively expose only what makes sense for game scripting - -**Key Findings**: -- Direct integration allows resource sharing between mcrfpy and sfml modules -- Can prevent unsafe operations (e.g., closing the game window) -- Opportunity to provide modern SFML 2.6+ Python bindings -- Implementation phases outlined in SFML_EXPOSURE_RESEARCH.md - -**Result**: Created comprehensive research document recommending direct integration approach with detailed implementation plan. - ---- - -### Task: SFML 3.0 Migration Research - -**Status**: Research Completed -**Date**: 2025-07-06 - -**Research Summary**: -1. SFML 3.0 Release Analysis: - - Released December 21, 2024 (very recent) - - First major version in 12 years - - Requires C++17 (vs C++03 for SFML 2.x) - - Major breaking changes in event system, enums, resource loading - -2. McRogueFace Impact Assessment: - - 40+ source files use SFML directly - - Event handling requires complete rewrite (high impact) - - All keyboard/mouse enums need updating (medium impact) - - Resource loading needs exception handling (medium impact) - - Geometry constructors need updating (low impact) - -3. Key Breaking Changes: - - Event system now uses `std::variant` with `getIf()` API - - All enums are now scoped (e.g., `sf::Keyboard::Key::A`) - - Resource loading via constructors that throw exceptions - - `pollEvent()` returns `std::optional` - - CMake targets now namespaced (e.g., `SFML::Graphics`) - -4. Recommendation: **Defer Migration** - - SFML 3.0 is too new (potential stability issues) - - Migration effort is substantial (especially event system) - - Better to implement `mcrfpy.sfml` with stable SFML 2.6.1 first - - Revisit migration in 6-12 months - -**Key Decisions**: -- Proceed with `mcrfpy.sfml` implementation using SFML 2.6.1 -- Design module API to minimize future breaking changes -- Monitor SFML 3.0 adoption and stability -- Plan migration for late 2025 or early 2026 - -**Result**: Created SFML_3_MIGRATION_RESEARCH.md with comprehensive analysis and migration strategy. - ---- - -## Phase 4: Visibility & Performance - -### Task 3: Basic Profiling/Metrics (#104) - -**Status**: Completed -**Date**: 2025-07-06 - -**Implementation**: -1. Added ProfilingMetrics struct to GameEngine: - - Frame time tracking (current and 60-frame average) - - FPS calculation from average frame time - - Draw call counting per frame - - UI element counting (total and visible) - - Runtime tracking - -2. Integrated metrics collection: - - GameEngine::run() updates frame time metrics each frame - - PyScene::render() counts UI elements and draw calls - - Metrics reset at start of each frame - -3. Exposed metrics to Python: - - Added mcrfpy.getMetrics() function - - Returns dictionary with all metrics - - Accessible from Python scripts for monitoring - -**Features**: -- Real-time frame time and FPS tracking -- 60-frame rolling average for stable FPS display -- Per-frame draw call counting -- UI element counting (total vs visible) -- Total runtime tracking -- Current frame counter - -**Testing**: -- Created test scripts (test_metrics.py, test_metrics_simple.py) -- Verified metrics API is accessible from Python -- Note: Metrics are only populated after game loop starts - -**Result**: Basic profiling system ready for performance monitoring and optimization. - ---- - -### Task 2: Click Handling Improvements - -**Status**: Completed -**Date**: 2025-07-06 - -**Implementation**: -1. Fixed UIFrame coordinate transformation: - - Now correctly subtracts parent position for child coordinates (was adding) - - Checks children in reverse order (highest z-index first) - - Checks bounds first for optimization - - Invisible elements are skipped entirely - -2. Fixed Scene click handling z-order: - - PyScene::do_mouse_input now sorts elements by z-index (highest first) - - Click events stop at the first handler found - - Ensures top-most elements receive clicks first - -3. Implemented UIGrid entity clicking: - - Transforms screen coordinates to grid coordinates - - Checks entities in reverse order - - Returns entity sprite as click target (entities delegate to their sprite) - - Accounts for grid zoom and center position - -**Features**: -- Correct z-order click priority (top elements get clicks first) -- Click transparency (elements without handlers don't block clicks) -- Proper coordinate transformation for nested frames -- Grid entity click detection with coordinate transformation -- Invisible elements don't receive or block clicks - -**Testing**: -- Created comprehensive test suite (test_click_handling.py) -- Tests cannot run in headless mode due to PyScene::do_mouse_input early return -- Manual testing would be required to verify functionality - -**Result**: Click handling now correctly respects z-order, coordinate transforms, and visibility. - ---- - -### Task 1: Name System Implementation (#39/40/41) - -**Status**: Completed -**Date**: 2025-07-06 - -**Implementation**: -1. Added `std::string name` member to UIDrawable base class -2. Implemented get_name/set_name static methods in UIDrawable for Python bindings -3. Added name property to all UI class Python getsetters: - - Frame, Caption, Sprite, Grid: Use UIDrawable::get_name/set_name directly - - Entity: Special handlers that delegate to entity->sprite.name -4. Implemented find() and findAll() functions in McRFPy_API: - - find(name, scene=None) - Returns first element with exact name match - - findAll(pattern, scene=None) - Returns list of elements matching pattern (supports * wildcards) - - Both functions search recursively through Frame children and Grid entities - - Can search current scene or specific named scene - -**Features**: -- All UI elements (Frame, Caption, Sprite, Grid, Entity) support .name property -- Names default to empty string "" -- Names support Unicode characters -- find() returns None if no match found -- findAll() returns empty list if no matches -- Wildcard patterns: "*_frame" matches "main_frame", "sidebar_frame" -- Searches nested elements: Frame children and Grid entities - -**Testing**: -- Created comprehensive test suite (test_name_property.py, test_find_functions.py) -- All tests pass for name property on all UI types -- All tests pass for find/findAll functionality including wildcards - -**Result**: Complete name-based element finding system ready for use. - ---- - -## Phase 1: Foundation Stabilization - -### Task #7: Audit Unsafe Constructors - -**Status**: Completed -**Date**: 2025-07-06 - -**Findings**: -- All UI classes (UIFrame, UICaption, UISprite, UIGrid, UIEntity) have no-argument constructors -- These are required by the Python C API's two-phase initialization pattern: - - `tp_new` creates a default C++ object with `std::make_shared()` - - `tp_init` initializes the object with actual values -- This pattern ensures proper shared_ptr lifetime management and exception safety - -**Decision**: Keep the no-argument constructors but ensure they're safe: -1. Initialize all members to safe defaults -2. Set reasonable default sizes (0,0) and positions (0,0) -3. Ensure no uninitialized pointers - -**Code Changes**: -- UIFrame: Already safe - initializes outline, children, position, and size -- UISprite: Empty constructor - needs safety improvements -- UIGrid: Empty constructor - needs safety improvements -- UIEntity: Empty constructor with TODO comment - needs safety improvements -- UICaption: Uses compiler default - needs explicit constructor with safe defaults - -**Recommendation**: Rather than remove these constructors (which would break Python bindings), we should ensure they initialize all members to safe, predictable values. - -**Implementation**: -1. Added safe default constructors for all UI classes: - - UISprite: Initializes sprite_index=0, ptex=nullptr, position=(0,0), scale=(1,1) - - UIGrid: Initializes all dimensions to 0, creates empty entity list, minimal render texture - - UIEntity: Initializes self=nullptr, grid=nullptr, position=(0,0), collision_pos=(0,0) - - UICaption: Initializes empty text, position=(0,0), size=12, white color - -2. Fixed Python init functions to accept no arguments: - - Changed PyArg_ParseTupleAndKeywords format strings to make all args optional (using |) - - Properly initialized all variables that receive optional arguments - - Added NULL checks for optional PyObject* parameters - - Set sensible defaults when no arguments provided - -**Result**: All UI classes can now be safely instantiated with no arguments from both C++ and Python. - ---- - -### Task #71: Create Python Base Class _Drawable - -**Status**: In Progress -**Date**: 2025-07-06 - -**Implementation**: -1. Created PyDrawable.h/cpp with Python type for _Drawable base class -2. Added properties to UIDrawable base class: - - visible (bool) - #87 - - opacity (float) - #88 -3. Added virtual methods to UIDrawable: - - get_bounds() - returns sf::FloatRect - #89 - - move(dx, dy) - relative movement - #98 - - resize(w, h) - absolute sizing - #98 -4. Implemented these methods in all derived classes: - - UIFrame: Uses box position/size - - UICaption: Uses text bounds, resize is no-op - - UISprite: Uses sprite bounds, resize scales sprite - - UIGrid: Uses box position/size, recreates render texture -5. Updated render methods to check visibility and apply opacity -6. Registered PyDrawableType in McRFPy_API module initialization - -**Decision**: While the C++ implementation is complete, updating the Python type hierarchy to inherit from PyDrawable would require significant refactoring of the existing getsetters. This is deferred to a future phase to avoid breaking existing code. The properties and methods are implemented at the C++ level and will take effect when rendering. - -**Result**: -- C++ UIDrawable base class now has visible (bool) and opacity (float) properties -- All derived classes implement get_bounds(), move(dx,dy), and resize(w,h) methods -- Render methods check visibility and apply opacity where supported -- Python _Drawable type created but not yet used as base class - ---- - -### Task #101: Standardize Default Positions - -**Status**: Completed (already implemented) -**Date**: 2025-07-06 - -**Findings**: All UI classes (Frame, Caption, Sprite, Grid) already default to position (0,0) when position arguments are not provided. This was implemented as part of the safe constructor work in #7. - ---- - -### Task #38: Frame Children Parameter - -**Status**: In Progress -**Date**: 2025-07-06 - -**Goal**: Allow Frame initialization with children parameter: `Frame(x, y, w, h, children=[...])` - -**Implementation**: -1. Added `children` parameter to Frame.__init__ keyword arguments -2. Process children after frame initialization -3. Validate each child is a Frame, Caption, Sprite, or Grid -4. Add valid children to frame's children collection -5. Set children_need_sort flag for z-index sorting - -**Result**: Frames can now be initialized with their children in a single call, making UI construction more concise. - ---- - -### Task #42: Click Handler in __init__ - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Allow setting click handlers during initialization for all UI elements - -**Implementation**: -1. Added `click` parameter to __init__ methods for Frame, Caption, and Sprite -2. Validates that click handler is callable (or None) -3. Registers click handler using existing click_register() method -4. Works alongside other initialization parameters - -**Changes Made**: -- UIFrame: Added click parameter to init, validates and registers handler -- UICaption: Added click parameter to init, validates and registers handler -- UISprite: Added click parameter to init, validates and registers handler -- UIGrid: Already had click parameter support - -**Result**: All UI elements can now have click handlers set during initialization, making interactive UI creation more concise. Lambda functions and other callables work correctly. - ---- - -### Task #90: Grid Size Tuple Support - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Allow Grid to accept grid_size=(width, height) as an alternative to separate grid_x, grid_y arguments - -**Implementation**: -1. Added `grid_size` keyword parameter to Grid.__init__ -2. Accepts either tuple or list of two integers -3. If provided, grid_size overrides any grid_x/grid_y values -4. Maintains backward compatibility with positional grid_x, grid_y arguments - -**Changes Made**: -- Modified UIGrid::init to use PyArg_ParseTupleAndKeywords -- Added parsing logic for grid_size parameter -- Validates that grid_size contains exactly 2 integers -- Falls back to positional arguments if keywords not used - -**Test Results**: -- grid_size tuple works correctly -- grid_size list works correctly -- Traditional grid_x, grid_y still works -- grid_size properly overrides grid_x, grid_y if both provided -- Proper error handling for invalid grid_size values - -**Result**: Grid initialization is now more flexible, allowing either `Grid(10, 15)` or `Grid(grid_size=(10, 15))` syntax - ---- - -### Task #19: Sprite Texture Swapping - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Verify and document sprite texture swapping functionality - -**Findings**: -- Sprite texture swapping was already implemented via the `texture` property -- The getter and setter were already exposed in the Python API -- `setTexture()` method preserves sprite position and scale - -**Implementation Details**: -- UISprite::get_texture returns the texture via pyObject() -- UISprite::set_texture validates the input is a Texture instance -- The C++ setTexture method updates the sprite with the new texture -- Sprite index can be optionally updated when setting texture - -**Test Results**: -- Texture swapping works correctly -- Position and scale are preserved during texture swap -- Type validation prevents assigning non-Texture objects -- Sprite count changes verify texture was actually swapped - -**Result**: Sprite texture swapping is fully functional. Sprites can change their texture at runtime while preserving position and scale. - ---- - -### Task #52: Grid Skip Out-of-Bounds Entities - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Add bounds checking to skip rendering entities outside the visible grid area for performance - -**Implementation**: -1. Added visibility bounds check in UIGrid::render() entity loop -2. Calculate visible bounds based on left_edge, top_edge, width_sq, height_sq -3. Skip entities outside bounds with 1 cell margin for partially visible entities -4. Bounds check considers zoom and pan settings - -**Code Changes**: -```cpp -// Check if entity is within visible bounds (with 1 cell margin) -if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 || - e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) { - continue; // Skip this entity -} -``` - -**Test Results**: -- Entities outside view bounds are successfully skipped -- Performance improvement when rendering grids with many entities -- Zoom and pan correctly affect culling bounds -- 1 cell margin ensures partially visible entities still render - -**Result**: Grid rendering now skips out-of-bounds entities, improving performance for large grids with many entities. This is especially beneficial for games with large maps. - ---- - -## Phase 3: Entity Lifecycle Management - -### Task #30: Entity.die() Method - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement Entity.die() method to remove entity from its grid - -**Implementation**: -1. Added die() method to UIEntity class -2. Method finds and removes entity from grid's entity list -3. Clears entity's grid reference after removal -4. Safe to call multiple times (no-op if not on grid) - -**Code Details**: -- UIEntityCollection::append already sets entity->grid when added -- UIEntityCollection::remove already clears grid reference when removed -- die() method uses std::find_if to locate entity in grid's list -- Uses shared_ptr comparison to find correct entity - -**Test Results**: -- Basic die() functionality works correctly -- Safe to call on entities not in a grid -- Works correctly with multiple entities -- Can be called multiple times safely -- Works in loops over entity collections -- Python references remain valid after die() - -**Result**: Entities can now remove themselves from their grid with a simple die() call. This enables cleaner entity lifecycle management in games. - ---- - -### Standardized Position Arguments - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Standardize position argument handling across all UI classes for consistency - -**Problem**: -- Caption expected pos first, not x, y -- Grid didn't use keywords -- Grid.at() didn't accept tuple format -- Inconsistent position argument formats across classes - -**Implementation**: -1. Created PyPositionHelper.h with standardized position parsing utilities -2. Updated Grid.at() to accept: (x, y), ((x,y)), x=x, y=y, pos=(x,y) -3. Updated Caption to accept: (x, y), ((x,y)), x=x, y=y, pos=(x,y) -4. Ensured Grid supports keyword arguments -5. Maintained backward compatibility for all formats - -**Standardized Formats**: -All position arguments now support: -- `(x, y)` - two positional arguments -- `((x, y))` - single tuple argument -- `x=x, y=y` - keyword arguments -- `pos=(x,y)` - pos keyword with tuple -- `pos=Vector` - pos keyword with Vector object - -**Classes Updated**: -- Grid.at() - Now accepts all standard position formats -- Caption - Now accepts x,y in addition to pos -- Grid - Keywords fully supported -- Frame - Already supported both formats -- Sprite - Already supported both formats -- Entity - Uses pos keyword - -**Test Results**: -- All position formats work correctly -- Backward compatibility maintained -- Consistent error messages across classes - -**Result**: All UI classes now have consistent, flexible position argument handling. This improves API usability and reduces confusion when working with different UI elements. - -**Update**: Extended standardization to Frame, Sprite, and Entity: -- Frame already had dual format support, improved with pos keyword override -- Sprite already had dual format support, improved with pos keyword override -- Entity now supports x, y arguments in addition to pos (was previously pos-only) -- No blockers found - all classes benefit from standardization -- PyPositionHelper could be used for even cleaner implementation in future - ---- - -### Bug Fix: Click Handler Segfault - -**Status**: Completed -**Date**: 2025-07-06 - -**Issue**: Accessing the `click` property on UI elements that don't have a click handler set caused a segfault. - -**Root Cause**: In `UIDrawable::get_click()`, the code was calling `->borrow()` on the `click_callable` unique_ptr without checking if it was null first. - -**Fix**: Added null checks before accessing `click_callable->borrow()` for all UI element types. - -**Result**: Click handler property access is now safe. Elements without click handlers return None as expected. - ---- - -## Phase 3: Enhanced Core Types - -### Task #93: Vector Arithmetic - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement arithmetic operations for the Vector class - -**Implementation**: -1. Added PyNumberMethods structure with arithmetic operators: - - Addition (`__add__`): v1 + v2 - - Subtraction (`__sub__`): v1 - v2 - - Multiplication (`__mul__`): v * scalar or scalar * v - - Division (`__truediv__`): v / scalar - - Negation (`__neg__`): -v - - Absolute value (`__abs__`): abs(v) returns magnitude - - Boolean check (`__bool__`): False for zero vector - - Rich comparison (`__eq__`, `__ne__`) - -2. Added vector-specific methods: - - `magnitude()`: Returns length of vector - - `magnitude_squared()`: Returns length squared (faster for comparisons) - - `normalize()`: Returns unit vector in same direction - - `dot(other)`: Dot product with another vector - - `distance_to(other)`: Euclidean distance to another vector - - `angle()`: Angle in radians from positive X axis - - `copy()`: Create an independent copy - -**Technical Details**: -- PyNumberMethods structure defined in mcrfpydef namespace -- Type checking returns NotImplemented for invalid operations -- Zero division protection in divide operation -- Zero vector normalization returns zero vector - -**Test Results**: -All arithmetic operations work correctly: -- Basic arithmetic (add, subtract, multiply, divide, negate) -- Comparison operations (equality, inequality) -- Vector methods (magnitude, normalize, dot product, etc.) -- Type safety with proper error handling - -**Result**: Vector class now supports full arithmetic operations, making game math much more convenient and Pythonic. - ---- - -### Bug Fix: UTF-8 Encoding for Python Output - -**Status**: Completed -**Date**: 2025-07-06 - -**Issue**: Python print statements with unicode characters (like ✓ or emoji) were causing UnicodeEncodeError because stdout/stderr were using ASCII encoding. - -**Root Cause**: Python's stdout and stderr were defaulting to ASCII encoding instead of UTF-8, even though `utf8_mode = 1` was set in PyPreConfig. - -**Fix**: Properly configure UTF-8 encoding in PyConfig during initialization: -```cpp -PyConfig_SetString(&config, &config.stdio_encoding, L"UTF-8"); -PyConfig_SetString(&config, &config.stdio_errors, L"surrogateescape"); -config.configure_c_stdio = 1; -``` - -**Implementation**: -- Added UTF-8 configuration in `init_python()` for normal game mode -- Added UTF-8 configuration in `init_python_with_config()` for interpreter mode -- Used `surrogateescape` error handler for robustness with invalid UTF-8 -- Removed temporary stream wrapper hack in favor of proper configuration - -**Technical Details**: -- `stdio_encoding`: Sets encoding for stdin, stdout, stderr -- `stdio_errors`: "surrogateescape" allows round-tripping invalid byte sequences -- `configure_c_stdio`: Lets Python properly configure C runtime stdio behavior - -**Result**: Unicode characters now work correctly in all Python output, including print statements, f-strings, and error messages. Tests can now use checkmarks (✓), cross marks (✗), emojis (🎮), and any other Unicode characters. The solution is cleaner and more robust than wrapping streams after initialization. - ---- - -### Task #94: Color Helper Methods - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Add helper methods to the Color class for hex conversion and interpolation - -**Implementation**: -1. **from_hex(hex_string)** - Class method to create Color from hex string - - Accepts formats: "#RRGGBB", "RRGGBB", "#RRGGBBAA", "RRGGBBAA" - - Automatically strips "#" prefix if present - - Validates hex string length and format - - Returns new Color instance - -2. **to_hex()** - Instance method to convert Color to hex string - - Returns "#RRGGBB" for fully opaque colors - - Returns "#RRGGBBAA" for colors with alpha < 255 - - Always includes "#" prefix - -3. **lerp(other_color, t)** - Linear interpolation between colors - - Interpolates all components (r, g, b, a) - - Clamps t to [0.0, 1.0] range - - t=0 returns self, t=1 returns other_color - - Returns new Color instance - -**Technical Details**: -- Used `std::stoul` for hex parsing with base 16 -- `snprintf` for efficient hex string formatting -- Linear interpolation: `result = start + (end - start) * t` -- Added as methods to PyColorType with METH_CLASS flag for from_hex - -**Test Results**: -- All hex formats parse correctly -- Round-trip conversion preserves values -- Interpolation produces smooth gradients -- Error handling works for invalid input - -**Result**: Color class now has convenient helper methods for common color operations. This makes it easier to work with colors in games, especially for UI theming and effects. - -### Task: #103 - Timer objects - -**Issue**: Add mcrfpy.Timer object to encapsulate timer functionality with pause/resume/cancel capabilities - -**Research**: -- Current timer system uses setTimer/delTimer with string names -- Timers stored in GameEngine::timers map as shared_ptr -- No pause/resume functionality exists -- Need object-oriented interface for better control - -**Implementation**: -1. Created PyTimer.h/cpp with PyTimerObject structure -2. Enhanced PyTimerCallable with pause/resume state tracking: - - Added paused, pause_start_time, total_paused_time members - - Modified hasElapsed() to check paused state - - Adjusted timing calculations to account for paused duration -3. Timer object features: - - Constructor: Timer(name, callback, interval) - - Methods: pause(), resume(), cancel(), restart() - - Properties: interval, remaining, paused, active, callback - - Automatically registers with game engine on creation -4. Pause/resume logic: - - When paused: Store pause time, set paused flag - - When resumed: Calculate pause duration, adjust last_ran time - - Prevents timer from "catching up" after resume - -**Key Decisions**: -- Timer object owns a shared_ptr to PyTimerCallable for lifetime management -- Made GameEngine::runtime and timers public for Timer access -- Used placement new for std::string member in PyTimerObject -- Fixed const-correctness issue with isNone() method - -**Test Results**: -- Timer creation and basic firing works correctly -- Pause/resume maintains proper timing without rapid catch-up -- Cancel removes timer from system properly -- Restart resets timer to current time -- Interval modification takes effect immediately -- Timer states (active, paused) report correctly - -**Result**: Timer objects provide a cleaner, more intuitive API for managing timed callbacks. Games can now pause/resume timers for menus, animations, or gameplay mechanics. The object-oriented interface is more Pythonic than the string-based setTimer/delTimer approach. - ---- - -### Test Suite Stabilization - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Make all test files terminate properly and fix various test failures - -**Issues Addressed**: - -1. **Audio Cleanup Warning** - - Issue: `AL lib: (EE) alc_cleanup: 1 device not closed` warning on exit - - Attempted Fix: Converted static audio objects (sf::Music, sf::Sound) to pointers and added explicit cleanup in api_shutdown() - - Result: Warning persists but is a known OpenAL/SFML issue that doesn't affect functionality - - This is a benign warning seen in many SFML applications - -2. **Test Termination Issues** - - Issue: test_click_init.py and test_frame_children.py didn't terminate on their own - - Fix: Added `mcrfpy.delTimer("test")` at start of test functions to prevent re-running - - Added fallback exit timers with 1-2 second timeouts as safety net - - Result: All tests now terminate properly - -3. **Missing Python Methods/Properties** - - Issue: visible, opacity, get_bounds, move, resize methods were missing from UI objects - - Implementation: - - Created UIDrawable_methods.h with template functions for shared functionality - - Added UIDRAWABLE_METHODS and UIDRAWABLE_GETSETTERS macros - - Updated all UI classes (Frame, Caption, Sprite, Grid) to include these - - Special handling for UIEntity which wraps UISprite - created template specializations - - Technical Details: - - Template functions allow code reuse across different PyObject types - - UIEntity delegates to its sprite member for drawable properties - - Fixed static/extern linkage issues with method arrays - - Result: All UI objects now have complete drawable interface - -4. **test_sprite_texture_swap.py Fixes** - - TypeError Issue: Click handler was missing 4th parameter 'action' - - Fix: Updated click handler signature from (x, y, button) to (x, y, button, action) - - Texture Comparison Issue: Direct object comparison failed because sprite.texture returns new wrapper - - Fix: Changed tests to avoid direct texture object comparison, use state tracking instead - - Result: Test passes with all functionality verified - -5. **Timer Test Segfaults** - - Issue: test_timer_object.py and test_timer_object_fixed.py mentioned potential segfaults - - Investigation: Tests were actually running fine, no segfaults detected - - Both timer tests complete successfully with proper exit codes - -6. **test_drawable_base.py Segfault** - - Issue: Segmentation fault when rendering Caption objects in headless mode - - Root Cause: Graphics driver crash in iris_dri.so when rendering text without display - - Stack trace showed crash in sf::Text::draw -> Font::getGlyph -> Texture::update - - Fix: Skip visual test portion in headless mode to avoid rendering - - Result: Test completes successfully, all non-visual tests pass - -**Additional Issues Resolved**: - -1. **Caption Constructor Format** - - Issue: test_drawable_base.py was using incorrect Caption constructor format - - Fix: Changed from keyword arguments to positional format: `Caption((x, y), text)` - - Caption doesn't support x=, y= keywords yet, only positional or pos= formats - -2. **Debug Print Cleanup** - - Removed debug print statement in UICaption color setter that was outputting "got 255, 255, 255, 255" - - This was cluttering test output - -**Test Suite Status**: -- ✓ test_click_init.py - Terminates properly -- ✓ test_frame_children.py - Terminates properly -- ✓ test_sprite_texture_swap.py - All tests pass, terminates properly -- ✓ test_timer_object.py - All tests pass, terminates properly -- ✓ test_timer_object_fixed.py - All tests pass, terminates properly -- ✓ test_drawable_base.py - All tests pass (visual test skipped in headless) - -**Result**: All test files are now "airtight" - they complete successfully, terminate on their own, and handle edge cases properly. The only remaining output is the benign OpenAL cleanup warning. - ---- - -### Window Close Segfault Fix - -**Status**: Completed -**Date**: 2025-07-06 - -**Issue**: Segmentation fault when closing the window via the OS X button (but not when exiting via Ctrl+C) - -**Root Cause**: -When the window was closed externally via the X button, the cleanup order was incorrect: -1. SFML window would be destroyed by the window manager -2. GameEngine destructor would delete scenes containing Python objects -3. Python was still running and might try to access destroyed C++ objects -4. This caused a segfault due to accessing freed memory - -**Solution**: -1. Added `cleanup()` method to GameEngine class that properly clears Python references before C++ destruction -2. The cleanup method: - - Clears all timers (which hold Python callables) - - Clears McRFPy_API's reference to the game engine - - Explicitly closes the window if still open -3. Call `cleanup()` at the end of the run loop when window close is detected -4. Also call in destructor with guard to prevent double cleanup -5. Added `cleaned_up` member variable to track cleanup state - -**Implementation Details**: -- Modified `GameEngine::run()` to call `cleanup()` before exiting -- Modified `GameEngine::~GameEngine()` to call `cleanup()` before deleting scenes -- Added `GameEngine::cleanup()` method with proper cleanup sequence -- Added `bool cleaned_up` member to prevent double cleanup - -**Result**: Window can now be closed via the X button without segfaulting. Python references are properly cleared before C++ objects are destroyed. - ---- - -### Additional Improvements - -**Status**: Completed -**Date**: 2025-07-06 - -1. **Caption Keyword Arguments** - - Issue: Caption didn't accept `x, y` as keyword arguments (e.g., `Caption("text", x=5, y=10)`) - - Solution: Rewrote Caption init to handle multiple argument patterns: - - `Caption("text", x=10, y=20)` - text first with keyword position - - `Caption(x, y, "text")` - traditional positional arguments - - `Caption((x, y), "text")` - position tuple format - - All patterns now work correctly with full keyword support - -2. **Code Organization Refactoring** - - Issue: `UIDrawable_methods.h` was a separate file that could have been better integrated - - Solution: - - Moved template functions and macros from `UIDrawable_methods.h` into `UIBase.h` - - Created `UIEntityPyMethods.h` for UIEntity-specific implementations - - Removed the now-unnecessary `UIDrawable_methods.h` - - Result: Better code organization with Python binding code in appropriate headers ---- - -## Phase 6: Rendering Revolution - -### Task: Grid Background Colors (#50) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Add background_color property to UIGrid for customizable grid backgrounds - -**Implementation**: -1. Added `sf::Color background_color` member to UIGrid class -2. Initialized with default dark gray (8, 8, 8, 255) in constructors -3. Replaced hardcoded clear color with `renderTexture.clear(background_color)` -4. Added Python property getter/setter: - - `grid.background_color` returns Color object - - Can set with any Color object -5. Added animation support via property system: - - `background_color.r/g/b/a` can be animated individually - - Proper clamping to 0-255 range - -**Technical Details**: -- Background renders before grid tiles and entities -- Animation support through existing property system -- Type-safe Color object validation -- No performance impact (just changes clear color) - -**Test Results**: -- Default background color (8, 8, 8) works correctly -- Setting background_color property changes render -- Individual color components can be modified -- Color cycling demonstration successful - -**Result**: Grid backgrounds are now customizable, allowing for themed dungeons, environmental effects, and visual polish. This was a perfect warm-up task for Phase 6. - ---- diff --git a/README.md b/README.md index c4de080..b7e69e0 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,27 @@ A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML. +* Core roguelike logic from libtcod: field of view, pathfinding +* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera +* Simple GUI element system allows keyboard and mouse input, composition +* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship" + +![ Image ]() + **Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items. -## Tenets - -- **Python & C++ Hand-in-Hand**: Create your game without ever recompiling. Your Python commands create C++ objects, and animations can occur without calling Python at all. -- **Simple Yet Flexible UI System**: Sprites, Grids, Frames, and Captions with full animation support -- **Entity-Component Architecture**: Implement your game objects with Python integration -- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod (demos still under construction) -- **Automation API**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration -- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter - ## Quick Start +**Download**: + +- The entire McRogueFace visual framework: + - **Sprite**: an image file or one sprite from a shared sprite sheet + - **Caption**: load a font, display text + - **Frame**: A rectangle; put other things on it to move or manage GUIs as modules + - **Grid**: A 2D array of tiles with zoom + position control + - **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path + - **Animation**: Change any property on any of the above over time + ```bash # Clone and build git clone @@ -49,28 +57,59 @@ mcrfpy.setScene("intro") ## Documentation +### 📚 Full Documentation Site + For comprehensive documentation, tutorials, and API reference, visit: **[https://mcrogueface.github.io](https://mcrogueface.github.io)** -## Requirements +The documentation site includes: + +- **[Quickstart Guide](https://mcrogueface.github.io/quickstart/)** - Get running in 5 minutes +- **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials/)** - Step-by-step game building +- **[Complete API Reference](https://mcrogueface.github.io/api/)** - Every function documented +- **[Cookbook](https://mcrogueface.github.io/cookbook/)** - Ready-to-use code recipes +- **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp/)** - For C++ developers: Add engine features + +## Build Requirements - C++17 compiler (GCC 7+ or Clang 5+) - CMake 3.14+ - Python 3.12+ -- SFML 2.5+ +- SFML 2.6 - Linux or Windows (macOS untested) ## Project Structure ``` McRogueFace/ -├── src/ # C++ engine source -├── scripts/ # Python game scripts ├── assets/ # Sprites, fonts, audio -├── build/ # Build output directory +├── build/ # Build output directory: zip + ship +│ ├─ (*)assets/ # (copied location of assets) +│ ├─ (*)scripts/ # (copied location of src/scripts) +│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules +├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build +│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config +├── docs/ # generated HTML, markdown docs +│ └─ stubs/ # .pyi files for editor integration +├── modules/ # git submodules, to build all of McRogueFace's dependencies from source +├── src/ # C++ engine source +│ └─ scripts/ # Python game scripts (copied during build) └── tests/ # Automated test suite +└── tools/ # For the McRogueFace ecosystem: docs generation ``` +If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project. + +If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory. + +## Philosophy + +- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python. +- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship +- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod +- **Hands-Off Testing**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration +- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter + ## Contributing PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request. diff --git a/build_windows_cmake.bat b/build_windows_cmake.bat new file mode 100644 index 0000000..7102ea4 --- /dev/null +++ b/build_windows_cmake.bat @@ -0,0 +1,42 @@ +@echo off +REM Windows build script using cmake --build (generator-agnostic) +REM This version works with any CMake generator + +echo Building McRogueFace for Windows using CMake... + +REM Set build directory +set BUILD_DIR=build_win +set CONFIG=Release + +REM Clean previous build +if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR% +mkdir %BUILD_DIR% +cd %BUILD_DIR% + +REM Configure with CMake +REM You can change the generator here if needed: +REM -G "Visual Studio 17 2022" (VS 2022) +REM -G "Visual Studio 16 2019" (VS 2019) +REM -G "MinGW Makefiles" (MinGW) +REM -G "Ninja" (Ninja build system) +cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% .. +if errorlevel 1 ( + echo CMake configuration failed! + cd .. + exit /b 1 +) + +REM Build using cmake (works with any generator) +cmake --build . --config %CONFIG% --parallel +if errorlevel 1 ( + echo Build failed! + cd .. + exit /b 1 +) + +echo. +echo Build completed successfully! +echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe +echo. + +cd .. \ No newline at end of file diff --git a/css_colors.txt b/css_colors.txt deleted file mode 100644 index 6e14aa2..0000000 --- a/css_colors.txt +++ /dev/null @@ -1,157 +0,0 @@ -aqua #00FFFF -black #000000 -blue #0000FF -fuchsia #FF00FF -gray #808080 -green #008000 -lime #00FF00 -maroon #800000 -navy #000080 -olive #808000 -purple #800080 -red #FF0000 -silver #C0C0C0 -teal #008080 -white #FFFFFF -yellow #FFFF00 -aliceblue #F0F8FF -antiquewhite #FAEBD7 -aqua #00FFFF -aquamarine #7FFFD4 -azure #F0FFFF -beige #F5F5DC -bisque #FFE4C4 -black #000000 -blanchedalmond #FFEBCD -blue #0000FF -blueviolet #8A2BE2 -brown #A52A2A -burlywood #DEB887 -cadetblue #5F9EA0 -chartreuse #7FFF00 -chocolate #D2691E -coral #FF7F50 -cornflowerblue #6495ED -cornsilk #FFF8DC -crimson #DC143C -cyan #00FFFF -darkblue #00008B -darkcyan #008B8B -darkgoldenrod #B8860B -darkgray #A9A9A9 -darkgreen #006400 -darkkhaki #BDB76B -darkmagenta #8B008B -darkolivegreen #556B2F -darkorange #FF8C00 -darkorchid #9932CC -darkred #8B0000 -darksalmon #E9967A -darkseagreen #8FBC8F -darkslateblue #483D8B -darkslategray #2F4F4F -darkturquoise #00CED1 -darkviolet #9400D3 -deeppink #FF1493 -deepskyblue #00BFFF -dimgray #696969 -dodgerblue #1E90FF -firebrick #B22222 -floralwhite #FFFAF0 -forestgreen #228B22 -fuchsia #FF00FF -gainsboro #DCDCDC -ghostwhite #F8F8FF -gold #FFD700 -goldenrod #DAA520 -gray #7F7F7F -green #008000 -greenyellow #ADFF2F -honeydew #F0FFF0 -hotpink #FF69B4 -indianred #CD5C5C -indigo #4B0082 -ivory #FFFFF0 -khaki #F0E68C -lavender #E6E6FA -lavenderblush #FFF0F5 -lawngreen #7CFC00 -lemonchiffon #FFFACD -lightblue #ADD8E6 -lightcoral #F08080 -lightcyan #E0FFFF -lightgoldenrodyellow #FAFAD2 -lightgreen #90EE90 -lightgrey #D3D3D3 -lightpink #FFB6C1 -lightsalmon #FFA07A -lightseagreen #20B2AA -lightskyblue #87CEFA -lightslategray #778899 -lightsteelblue #B0C4DE -lightyellow #FFFFE0 -lime #00FF00 -limegreen #32CD32 -linen #FAF0E6 -magenta #FF00FF -maroon #800000 -mediumaquamarine #66CDAA -mediumblue #0000CD -mediumorchid #BA55D3 -mediumpurple #9370DB -mediumseagreen #3CB371 -mediumslateblue #7B68EE -mediumspringgreen #00FA9A -mediumturquoise #48D1CC -mediumvioletred #C71585 -midnightblue #191970 -mintcream #F5FFFA -mistyrose #FFE4E1 -moccasin #FFE4B5 -navajowhite #FFDEAD -navy #000080 -navyblue #9FAFDF -oldlace #FDF5E6 -olive #808000 -olivedrab #6B8E23 -orange #FFA500 -orangered #FF4500 -orchid #DA70D6 -palegoldenrod #EEE8AA -palegreen #98FB98 -paleturquoise #AFEEEE -palevioletred #DB7093 -papayawhip #FFEFD5 -peachpuff #FFDAB9 -peru #CD853F -pink #FFC0CB -plum #DDA0DD -powderblue #B0E0E6 -purple #800080 -red #FF0000 -rosybrown #BC8F8F -royalblue #4169E1 -saddlebrown #8B4513 -salmon #FA8072 -sandybrown #FA8072 -seagreen #2E8B57 -seashell #FFF5EE -sienna #A0522D -silver #C0C0C0 -skyblue #87CEEB -slateblue #6A5ACD -slategray #708090 -snow #FFFAFA -springgreen #00FF7F -steelblue #4682B4 -tan #D2B48C -teal #008080 -thistle #D8BFD8 -tomato #FF6347 -turquoise #40E0D0 -violet #EE82EE -wheat #F5DEB3 -white #FFFFFF -whitesmoke #F5F5F5 -yellow #FFFF00 -yellowgreen #9ACD32 diff --git a/docs/API_REFERENCE_COMPLETE.md b/docs/API_REFERENCE_COMPLETE.md index e50d008..d9638f7 100644 --- a/docs/API_REFERENCE_COMPLETE.md +++ b/docs/API_REFERENCE_COMPLETE.md @@ -1,5 +1,7 @@ # McRogueFace API Reference +*Generated on 2025-07-15 21:28:42* + ## Overview McRogueFace Python API @@ -373,14 +375,6 @@ A rectangular frame UI element that can contain other drawable elements. #### Methods -#### `get_bounds()` - -Get the bounding rectangle of this drawable element. - -**Returns:** tuple: (x, y, width, height) representing the element's bounds - -**Note:** The bounds are in screen coordinates and account for current position and size. - #### `resize(width, height)` Resize the element to new dimensions. @@ -401,6 +395,14 @@ Move the element by a relative offset. **Note:** This modifies the x and y position properties by the given amounts. +#### `get_bounds()` + +Get the bounding rectangle of this drawable element. + +**Returns:** tuple: (x, y, width, height) representing the element's bounds + +**Note:** The bounds are in screen coordinates and account for current position and size. + --- ### class `Caption` @@ -409,14 +411,6 @@ A text display UI element with customizable font and styling. #### Methods -#### `get_bounds()` - -Get the bounding rectangle of this drawable element. - -**Returns:** tuple: (x, y, width, height) representing the element's bounds - -**Note:** The bounds are in screen coordinates and account for current position and size. - #### `resize(width, height)` Resize the element to new dimensions. @@ -437,6 +431,14 @@ Move the element by a relative offset. **Note:** This modifies the x and y position properties by the given amounts. +#### `get_bounds()` + +Get the bounding rectangle of this drawable element. + +**Returns:** tuple: (x, y, width, height) representing the element's bounds + +**Note:** The bounds are in screen coordinates and account for current position and size. + --- ### class `Sprite` @@ -445,14 +447,6 @@ A sprite UI element that displays a texture or portion of a texture atlas. #### Methods -#### `get_bounds()` - -Get the bounding rectangle of this drawable element. - -**Returns:** tuple: (x, y, width, height) representing the element's bounds - -**Note:** The bounds are in screen coordinates and account for current position and size. - #### `resize(width, height)` Resize the element to new dimensions. @@ -473,6 +467,14 @@ Move the element by a relative offset. **Note:** This modifies the x and y position properties by the given amounts. +#### `get_bounds()` + +Get the bounding rectangle of this drawable element. + +**Returns:** tuple: (x, y, width, height) representing the element's bounds + +**Note:** The bounds are in screen coordinates and account for current position and size. + --- ### class `Grid` @@ -481,6 +483,16 @@ A grid-based tilemap UI element for rendering tile-based levels and game worlds. #### Methods +#### `resize(width, height)` + +Resize the element to new dimensions. + +**Arguments:** +- `width` (*float*): New width in pixels +- `height` (*float*): New height in pixels + +**Note:** For Caption and Sprite, this may not change actual size if determined by content. + #### `at(x, y)` Get the GridPoint at the specified grid coordinates. @@ -491,24 +503,6 @@ Get the GridPoint at the specified grid coordinates. **Returns:** GridPoint or None: The grid point at (x, y), or None if out of bounds -#### `get_bounds()` - -Get the bounding rectangle of this drawable element. - -**Returns:** tuple: (x, y, width, height) representing the element's bounds - -**Note:** The bounds are in screen coordinates and account for current position and size. - -#### `resize(width, height)` - -Resize the element to new dimensions. - -**Arguments:** -- `width` (*float*): New width in pixels -- `height` (*float*): New height in pixels - -**Note:** For Caption and Sprite, this may not change actual size if determined by content. - #### `move(dx, dy)` Move the element by a relative offset. @@ -519,6 +513,14 @@ Move the element by a relative offset. **Note:** This modifies the x and y position properties by the given amounts. +#### `get_bounds()` + +Get the bounding rectangle of this drawable element. + +**Returns:** tuple: (x, y, width, height) representing the element's bounds + +**Note:** The bounds are in screen coordinates and account for current position and size. + --- ### class `Entity` @@ -527,12 +529,6 @@ Game entity that can be placed in a Grid. #### Methods -#### `die()` - -Remove this entity from its parent grid. - -**Note:** The entity object remains valid but is no longer rendered or updated. - #### `move(dx, dy)` Move the element by a relative offset. @@ -561,11 +557,11 @@ Get the bounding rectangle of this drawable element. **Note:** The bounds are in screen coordinates and account for current position and size. -#### `index()` +#### `die()` -Get the index of this entity in its parent grid's entity list. +Remove this entity from its parent grid. -**Returns:** int: Index position, or -1 if not in a grid +**Note:** The entity object remains valid but is no longer rendered or updated. #### `resize(width, height)` @@ -577,6 +573,12 @@ Resize the element to new dimensions. **Note:** For Caption and Sprite, this may not change actual size if determined by content. +#### `index()` + +Get the index of this entity in its parent grid's entity list. + +**Returns:** int: Index position, or -1 if not in a grid + --- ### Collections @@ -587,13 +589,6 @@ Container for Entity objects in a Grid. Supports iteration and indexing. #### Methods -#### `append(entity)` - -Add an entity to the end of the collection. - -**Arguments:** -- `entity` (*Entity*): The entity to add - #### `remove(entity)` Remove the first occurrence of an entity from the collection. @@ -603,6 +598,13 @@ Remove the first occurrence of an entity from the collection. **Raises:** ValueError: If entity is not in collection +#### `extend(iterable)` + +Add all entities from an iterable to the collection. + +**Arguments:** +- `iterable` (*Iterable[Entity]*): Entities to add + #### `count(entity)` Count the number of occurrences of an entity in the collection. @@ -623,12 +625,12 @@ Find the index of the first occurrence of an entity. **Raises:** ValueError: If entity is not in collection -#### `extend(iterable)` +#### `append(entity)` -Add all entities from an iterable to the collection. +Add an entity to the end of the collection. **Arguments:** -- `iterable` (*Iterable[Entity]*): Entities to add +- `entity` (*Entity*): The entity to add --- @@ -638,13 +640,6 @@ Container for UI drawable elements. Supports iteration and indexing. #### Methods -#### `append(drawable)` - -Add a drawable element to the end of the collection. - -**Arguments:** -- `drawable` (*UIDrawable*): The drawable element to add - #### `remove(drawable)` Remove the first occurrence of a drawable from the collection. @@ -654,6 +649,13 @@ Remove the first occurrence of a drawable from the collection. **Raises:** ValueError: If drawable is not in collection +#### `extend(iterable)` + +Add all drawables from an iterable to the collection. + +**Arguments:** +- `iterable` (*Iterable[UIDrawable]*): Drawables to add + #### `count(drawable)` Count the number of occurrences of a drawable in the collection. @@ -674,12 +676,12 @@ Find the index of the first occurrence of a drawable. **Raises:** ValueError: If drawable is not in collection -#### `extend(iterable)` +#### `append(drawable)` -Add all drawables from an iterable to the collection. +Add a drawable element to the end of the collection. **Arguments:** -- `iterable` (*Iterable[UIDrawable]*): Drawables to add +- `drawable` (*UIDrawable*): The drawable element to add --- @@ -703,6 +705,17 @@ RGBA color representation. #### Methods +#### `to_hex()` + +Convert this Color to a hexadecimal string. + +**Returns:** str: Hex color string in format "#RRGGBB" + +**Example:** +```python +hex_str = color.to_hex() # Returns "#FF0000" +``` + #### `from_hex(hex_string)` Create a Color from a hexadecimal color string. @@ -717,17 +730,6 @@ Create a Color from a hexadecimal color string. red = Color.from_hex("#FF0000") ``` -#### `to_hex()` - -Convert this Color to a hexadecimal string. - -**Returns:** str: Hex color string in format "#RRGGBB" - -**Example:** -```python -hex_str = color.to_hex() # Returns "#FF0000" -``` - #### `lerp(other, t)` Linearly interpolate between this color and another. @@ -757,14 +759,13 @@ Calculate the length/magnitude of this vector. **Returns:** float: The magnitude of the vector -#### `distance_to(other)` +#### `normalize()` -Calculate the distance to another vector. +Return a unit vector in the same direction. -**Arguments:** -- `other` (*Vector*): The other vector +**Returns:** Vector: New normalized vector with magnitude 1.0 -**Returns:** float: Distance between the two vectors +**Raises:** ValueError: If vector has zero magnitude #### `dot(other)` @@ -775,6 +776,21 @@ Calculate the dot product with another vector. **Returns:** float: Dot product of the two vectors +#### `distance_to(other)` + +Calculate the distance to another vector. + +**Arguments:** +- `other` (*Vector*): The other vector + +**Returns:** float: Distance between the two vectors + +#### `copy()` + +Create a copy of this vector. + +**Returns:** Vector: New Vector object with same x and y values + #### `angle()` Get the angle of this vector in radians. @@ -789,20 +805,6 @@ Calculate the squared magnitude of this vector. **Note:** Use this for comparisons to avoid expensive square root calculation. -#### `copy()` - -Create a copy of this vector. - -**Returns:** Vector: New Vector object with same x and y values - -#### `normalize()` - -Return a unit vector in the same direction. - -**Returns:** Vector: New normalized vector with magnitude 1.0 - -**Raises:** ValueError: If vector has zero magnitude - --- ### class `Texture` @@ -834,6 +836,12 @@ Animate UI element properties over time. #### Methods +#### `get_current_value()` + +Get the current interpolated value of the animation. + +**Returns:** float: Current animation value between start and end + #### `update(delta_time)` Update the animation by the given time delta. @@ -852,12 +860,6 @@ Start the animation on a target UI element. **Note:** The target must have the property specified in the animation constructor. -#### `get_current_value()` - -Get the current interpolated value of the animation. - -**Returns:** float: Current animation value between start and end - --- ### class `Drawable` @@ -866,14 +868,6 @@ Base class for all drawable UI elements. #### Methods -#### `get_bounds()` - -Get the bounding rectangle of this drawable element. - -**Returns:** tuple: (x, y, width, height) representing the element's bounds - -**Note:** The bounds are in screen coordinates and account for current position and size. - #### `resize(width, height)` Resize the element to new dimensions. @@ -894,6 +888,14 @@ Move the element by a relative offset. **Note:** This modifies the x and y position properties by the given amounts. +#### `get_bounds()` + +Get the bounding rectangle of this drawable element. + +**Returns:** tuple: (x, y, width, height) representing the element's bounds + +**Note:** The bounds are in screen coordinates and account for current position and size. + --- ### class `GridPoint` @@ -945,18 +947,18 @@ def handle_keyboard(key, action): scene.register_keyboard(handle_keyboard) ``` -#### `activate()` - -Make this scene the active scene. - -**Note:** Equivalent to calling setScene() with this scene's name. - #### `get_ui()` Get the UI element collection for this scene. **Returns:** UICollection: Collection of all UI elements in this scene +#### `activate()` + +Make this scene the active scene. + +**Note:** Equivalent to calling setScene() with this scene's name. + #### `keypress(handler)` Register a keyboard handler function for this scene. @@ -974,18 +976,6 @@ Timer object for scheduled callbacks. #### Methods -#### `restart()` - -Restart the timer from the beginning. - -**Note:** Resets the timer's internal clock to zero. - -#### `cancel()` - -Cancel the timer and remove it from the system. - -**Note:** After cancelling, the timer object cannot be reused. - #### `pause()` Pause the timer, stopping its callback execution. @@ -998,6 +988,18 @@ Resume a paused timer. **Note:** Has no effect if timer is not paused. +#### `restart()` + +Restart the timer from the beginning. + +**Note:** Resets the timer's internal clock to zero. + +#### `cancel()` + +Cancel the timer and remove it from the system. + +**Note:** After cancelling, the timer object cannot be reused. + --- ### class `Window` @@ -1006,14 +1008,6 @@ Window singleton for accessing and modifying the game window properties. #### Methods -#### `get()` - -Get the Window singleton instance. - -**Returns:** Window: The singleton window object - -**Note:** This is a static method that returns the same instance every time. - #### `screenshot(filename)` Take a screenshot and save it to a file. @@ -1023,6 +1017,14 @@ Take a screenshot and save it to a file. **Note:** Supports PNG, JPG, and BMP formats based on file extension. +#### `get()` + +Get the Window singleton instance. + +**Returns:** Window: The singleton window object + +**Note:** This is a static method that returns the same instance every time. + #### `center()` Center the window on the screen. diff --git a/docs/api_reference_complete.html b/docs/api_reference_complete.html deleted file mode 100644 index 73dd72a..0000000 --- a/docs/api_reference_complete.html +++ /dev/null @@ -1,1629 +0,0 @@ - - - - - - McRogueFace API Reference - Complete Documentation - - - -
- -

McRogueFace API Reference - Complete Documentation

-

Generated on 2025-07-10 01:04:50

-
-

Table of Contents

- -
-

Functions

-

Scene Management

-
-

createScene(name: str) -> None

-

Create a new empty scene with the given name.

-
-
Arguments:
-
-name -(str): -Unique name for the new scene -
-
-
-Raises: ValueError: If a scene with this name already exists -
-
-Note: The scene is created but not made active. Use setScene() to switch to it. -
-
-
Example:
-

-mcrfpy.createScene("game_over")
-
-
-
-
-

setScene(scene: str, transition: str = None, duration: float = 0.0) -> None

-

Switch to a different scene with optional transition effect.

-
-
Arguments:
-
-scene -(str): -Name of the scene to switch to -
-
-transition -(str): -Transition type: "fade", "slide_left", "slide_right", "slide_up", "slide_down" -
-
-duration -(float): -Transition duration in seconds (default: 0.0 for instant) -
-
-
-Raises: KeyError: If the scene doesn't exist -
-
-
Example:
-

-mcrfpy.setScene("game", "fade", 0.5)
-
-
-
-
-

currentScene() -> str

-

Get the name of the currently active scene.

-
-Returns: str: Name of the current scene -
-
-
Example:
-

-scene_name = mcrfpy.currentScene()
-
-
-
-
-

sceneUI(scene: str = None) -> UICollection

-

Get all UI elements for a scene.

-
-
Arguments:
-
-scene -(str): -Scene name. If None, uses current scene -
-
-
-Returns: UICollection: All UI elements in the scene -
-
-Raises: KeyError: If the specified scene doesn't exist -
-
-
Example:
-

-ui_elements = mcrfpy.sceneUI("game")
-
-
-
-
-

keypressScene(handler: callable) -> None

-

Set the keyboard event handler for the current scene.

-
-
Arguments:
-
-handler -(callable): -Function that receives (key_name: str, is_pressed: bool) -
-
-
-
Example:
-

-def on_key(key, pressed):
-    if key == "SPACE" and pressed:
-        player.jump()
-mcrfpy.keypressScene(on_key)
-
-
-
-

Audio

-
-

createSoundBuffer(filename: str) -> int

-

Load a sound effect from a file and return its buffer ID.

-
-
Arguments:
-
-filename -(str): -Path to the sound file (WAV, OGG, FLAC) -
-
-
-Returns: int: Buffer ID for use with playSound() -
-
-Raises: RuntimeError: If the file cannot be loaded -
-
-
Example:
-

-jump_sound = mcrfpy.createSoundBuffer("assets/jump.wav")
-
-
-
-
-

loadMusic(filename: str, loop: bool = True) -> None

-

Load and immediately play background music from a file.

-
-
Arguments:
-
-filename -(str): -Path to the music file (WAV, OGG, FLAC) -
-
-loop -(bool): -Whether to loop the music (default: True) -
-
-
-Note: Only one music track can play at a time. Loading new music stops the current track. -
-
-
Example:
-

-mcrfpy.loadMusic("assets/background.ogg", True)
-
-
-
-
-

playSound(buffer_id: int) -> None

-

Play a sound effect using a previously loaded buffer.

-
-
Arguments:
-
-buffer_id -(int): -Sound buffer ID returned by createSoundBuffer() -
-
-
-Raises: RuntimeError: If the buffer ID is invalid -
-
-
Example:
-

-mcrfpy.playSound(jump_sound)
-
-
-
-
-

getMusicVolume() -> int

-

Get the current music volume level.

-
-Returns: int: Current volume (0-100) -
-
-
Example:
-

-current_volume = mcrfpy.getMusicVolume()
-
-
-
-
-

getSoundVolume() -> int

-

Get the current sound effects volume level.

-
-Returns: int: Current volume (0-100) -
-
-
Example:
-

-current_volume = mcrfpy.getSoundVolume()
-
-
-
-
-

setMusicVolume(volume: int) -> None

-

Set the global music volume.

-
-
Arguments:
-
-volume -(int): -Volume level from 0 (silent) to 100 (full volume) -
-
-
-
Example:
-

-mcrfpy.setMusicVolume(50)  # Set to 50% volume
-
-
-
-
-

setSoundVolume(volume: int) -> None

-

Set the global sound effects volume.

-
-
Arguments:
-
-volume -(int): -Volume level from 0 (silent) to 100 (full volume) -
-
-
-
Example:
-

-mcrfpy.setSoundVolume(75)  # Set to 75% volume
-
-
-
-

UI Utilities

-
-

find(name: str, scene: str = None) -> UIDrawable | None

-

Find the first UI element with the specified name.

-
-
Arguments:
-
-name -(str): -Exact name to search for -
-
-scene -(str): -Scene to search in (default: current scene) -
-
-
-Returns: UIDrawable or None: The found element, or None if not found -
-
-Note: Searches scene UI elements and entities within grids. -
-
-
Example:
-

-button = mcrfpy.find("start_button")
-
-
-
-
-

findAll(pattern: str, scene: str = None) -> list

-

Find all UI elements matching a name pattern.

-
-
Arguments:
-
-pattern -(str): -Name pattern with optional wildcards (* matches any characters) -
-
-scene -(str): -Scene to search in (default: current scene) -
-
-
-Returns: list: All matching UI elements and entities -
-
-
Example:
-

-enemies = mcrfpy.findAll("enemy_*")
-
-
-
-

System

-
-

exit() -> None

-

Cleanly shut down the game engine and exit the application.

-
-Note: This immediately closes the window and terminates the program. -
-
-
Example:
-

-mcrfpy.exit()
-
-
-
-
-

getMetrics() -> dict

-

Get current performance metrics.

-
-Returns: dict: Performance data with keys: -- frame_time: Last frame duration in seconds -- avg_frame_time: Average frame time -- fps: Frames per second -- draw_calls: Number of draw calls -- ui_elements: Total UI element count -- visible_elements: Visible element count -- current_frame: Frame counter -- runtime: Total runtime in seconds -
-
-
Example:
-

-metrics = mcrfpy.getMetrics()
-
-
-
-
-

setTimer(name: str, handler: callable, interval: int) -> None

-

Create or update a recurring timer.

-
-
Arguments:
-
-name -(str): -Unique identifier for the timer -
-
-handler -(callable): -Function called with (runtime: float) parameter -
-
-interval -(int): -Time between calls in milliseconds -
-
-
-Note: If a timer with this name exists, it will be replaced. -
-
-
Example:
-

-def update_score(runtime):
-    score += 1
-mcrfpy.setTimer("score_update", update_score, 1000)
-
-
-
-
-

delTimer(name: str) -> None

-

Stop and remove a timer.

-
-
Arguments:
-
-name -(str): -Timer identifier to remove -
-
-
-Note: No error is raised if the timer doesn't exist. -
-
-
Example:
-

-mcrfpy.delTimer("score_update")
-
-
-
-
-

setScale(multiplier: float) -> None

-

Scale the game window size.

-
-
Arguments:
-
-multiplier -(float): -Scale factor (e.g., 2.0 for double size) -
-
-
-Note: The internal resolution remains 1024x768, but the window is scaled. -
-
-
Example:
-

-mcrfpy.setScale(2.0)  # Double the window size
-
-
-
-

Classes

-
-

Animation

-

Animation object for animating UI properties

-

Properties:

-
-property: str: Name of the property being animated (e.g., "x", "y", "scale") -
-
-duration: float: Total duration of the animation in seconds -
-
-elapsed_time: float: Time elapsed since animation started (read-only) -
-
-current_value: float: Current interpolated value of the animation (read-only) -
-
-is_running: bool: True if animation is currently running (read-only) -
-
-is_finished: bool: True if animation has completed (read-only) -
-

Methods:

-
-
update(delta_time)
-

Update the animation by the given time delta.

-
-delta_time -(float): -Time elapsed since last update in seconds -
-
-Returns: bool: True if animation is still running, False if finished -
-
-
-
start(target)
-

Start the animation on a target UI element.

-
-target -(UIDrawable): -The UI element to animate -
-
-Note: The target must have the property specified in the animation constructor. -
-
-
-
get_current_value()
-

Get the current interpolated value of the animation.

-
-Returns: float: Current animation value between start and end -
-
-
-
-

Caption

-

Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None) - -A text display UI element with customizable font and styling. - -Args: - text (str): The text content to display. Default: '' - x (float): X position in pixels. Default: 0 - y (float): Y position in pixels. Default: 0 - font (Font): Font object for text rendering. Default: engine default font - fill_color (Color): Text fill color. Default: (255, 255, 255, 255) - outline_color (Color): Text outline color. Default: (0, 0, 0, 255) - outline (float): Text outline thickness. Default: 0 - click (callable): Click event handler. Default: None - -Attributes: - text (str): The displayed text content - x, y (float): Position in pixels - font (Font): Font used for rendering - fill_color, outline_color (Color): Text appearance - outline (float): Outline thickness - click (callable): Click event handler - visible (bool): Visibility state - z_index (int): Rendering order - w, h (float): Read-only computed size based on text and font

-

Methods:

-
-
get_bounds()
-

Get the bounding rectangle of this drawable element.

-
-Returns: tuple: (x, y, width, height) representing the element's bounds -
-
-Note: The bounds are in screen coordinates and account for current position and size. -
-
-
-
move(dx, dy)
-

Move the element by a relative offset.

-
-dx -(float): -Horizontal offset in pixels -
-
-dy -(float): -Vertical offset in pixels -
-
-Note: This modifies the x and y position properties by the given amounts. -
-
-
-
resize(width, height)
-

Resize the element to new dimensions.

-
-width -(float): -New width in pixels -
-
-height -(float): -New height in pixels -
-
-Note: For Caption and Sprite, this may not change actual size if determined by content. -
-
-
-
-

Color

-

SFML Color Object

-

Methods:

-
-
from_hex(hex_string)
-

Create a Color from a hexadecimal color string.

-
-hex_string -(str): -Hex color string (e.g., "#FF0000" or "FF0000") -
-
-Returns: Color: New Color object from hex string -
-
-Example: -

-red = Color.from_hex("#FF0000")
-
-
-
-
-
lerp(other, t)
-

Linearly interpolate between this color and another.

-
-other -(Color): -The color to interpolate towards -
-
-t -(float): -Interpolation factor from 0.0 to 1.0 -
-
-Returns: Color: New interpolated Color object -
-
-Example: -

-mixed = red.lerp(blue, 0.5)  # 50% between red and blue
-
-
-
-
-
to_hex()
-

Convert this Color to a hexadecimal string.

-
-Returns: str: Hex color string in format "#RRGGBB" -
-
-Example: -

-hex_str = color.to_hex()  # Returns "#FF0000"
-
-
-
-
-
-

Drawable

-

Base class for all drawable UI elements

-

Methods:

-
-
get_bounds()
-

Get the bounding rectangle of this drawable element.

-
-Returns: tuple: (x, y, width, height) representing the element's bounds -
-
-Note: The bounds are in screen coordinates and account for current position and size. -
-
-
-
move(dx, dy)
-

Move the element by a relative offset.

-
-dx -(float): -Horizontal offset in pixels -
-
-dy -(float): -Vertical offset in pixels -
-
-Note: This modifies the x and y position properties by the given amounts. -
-
-
-
resize(width, height)
-

Resize the element to new dimensions.

-
-width -(float): -New width in pixels -
-
-height -(float): -New height in pixels -
-
-Note: For Caption and Sprite, this may not change actual size if determined by content. -
-
-
-
-

Entity

-

UIEntity objects

-

Methods:

-
-
move(dx, dy)
-

Move the element by a relative offset.

-
-dx -(float): -Horizontal offset in pixels -
-
-dy -(float): -Vertical offset in pixels -
-
-Note: This modifies the x and y position properties by the given amounts. -
-
-
-
resize(width, height)
-

Resize the element to new dimensions.

-
-width -(float): -New width in pixels -
-
-height -(float): -New height in pixels -
-
-Note: For Caption and Sprite, this may not change actual size if determined by content. -
-
-
-update_visibility(...) -
-
-
index()
-

Get the index of this entity in its parent grid's entity list.

-
-Returns: int: Index position, or -1 if not in a grid -
-
-
-path_to(...) -
-
-
at(x, y)
-

Check if this entity is at the specified grid coordinates.

-
-x -(int): -Grid x coordinate to check -
-
-y -(int): -Grid y coordinate to check -
-
-Returns: bool: True if entity is at position (x, y), False otherwise -
-
-
-
get_bounds()
-

Get the bounding rectangle of this drawable element.

-
-Returns: tuple: (x, y, width, height) representing the element's bounds -
-
-Note: The bounds are in screen coordinates and account for current position and size. -
-
-
-
die()
-

Remove this entity from its parent grid.

-
-Note: The entity object remains valid but is no longer rendered or updated. -
-
-
-
-

EntityCollection

-

Iterable, indexable collection of Entities

-

Methods:

-
-
remove(entity)
-

Remove the first occurrence of an entity from the collection.

-
-entity -(Entity): -The entity to remove -
-
-
-
count(entity)
-

Count the number of occurrences of an entity in the collection.

-
-entity -(Entity): -The entity to count -
-
-Returns: int: Number of times entity appears in collection -
-
-
-
index(entity)
-

Find the index of the first occurrence of an entity.

-
-entity -(Entity): -The entity to find -
-
-Returns: int: Index of entity in collection -
-
-
-
extend(iterable)
-

Add all entities from an iterable to the collection.

-
-iterable -(Iterable[Entity]): -Entities to add -
-
-
-
append(entity)
-

Add an entity to the end of the collection.

-
-entity -(Entity): -The entity to add -
-
-
-
-

Font

-

SFML Font Object

-
-
-

Frame

-

Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None) - -A rectangular frame UI element that can contain other drawable elements. - -Args: - x (float): X position in pixels. Default: 0 - y (float): Y position in pixels. Default: 0 - w (float): Width in pixels. Default: 0 - h (float): Height in pixels. Default: 0 - fill_color (Color): Background fill color. Default: (0, 0, 0, 128) - outline_color (Color): Border outline color. Default: (255, 255, 255, 255) - outline (float): Border outline thickness. Default: 0 - click (callable): Click event handler. Default: None - children (list): Initial list of child drawable elements. Default: None - -Attributes: - x, y (float): Position in pixels - w, h (float): Size in pixels - fill_color, outline_color (Color): Visual appearance - outline (float): Border thickness - click (callable): Click event handler - children (list): Collection of child drawable elements - visible (bool): Visibility state - z_index (int): Rendering order - clip_children (bool): Whether to clip children to frame bounds

-

Methods:

-
-
get_bounds()
-

Get the bounding rectangle of this drawable element.

-
-Returns: tuple: (x, y, width, height) representing the element's bounds -
-
-Note: The bounds are in screen coordinates and account for current position and size. -
-
-
-
move(dx, dy)
-

Move the element by a relative offset.

-
-dx -(float): -Horizontal offset in pixels -
-
-dy -(float): -Vertical offset in pixels -
-
-Note: This modifies the x and y position properties by the given amounts. -
-
-
-
resize(width, height)
-

Resize the element to new dimensions.

-
-width -(float): -New width in pixels -
-
-height -(float): -New height in pixels -
-
-Note: For Caption and Sprite, this may not change actual size if determined by content. -
-
-
-
-

Grid

-

Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None) - -A grid-based tilemap UI element for rendering tile-based levels and game worlds. - -Args: - x (float): X position in pixels. Default: 0 - y (float): Y position in pixels. Default: 0 - grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20) - texture (Texture): Texture atlas containing tile sprites. Default: None - tile_width (int): Width of each tile in pixels. Default: 16 - tile_height (int): Height of each tile in pixels. Default: 16 - scale (float): Grid scaling factor. Default: 1.0 - click (callable): Click event handler. Default: None - -Attributes: - x, y (float): Position in pixels - grid_size (tuple): Grid dimensions (width, height) in tiles - tile_width, tile_height (int): Tile dimensions in pixels - texture (Texture): Tile texture atlas - scale (float): Scale multiplier - points (list): 2D array of GridPoint objects for tile data - entities (list): Collection of Entity objects in the grid - background_color (Color): Grid background color - click (callable): Click event handler - visible (bool): Visibility state - z_index (int): Rendering order

-

Methods:

-
-
move(dx, dy)
-

Move the element by a relative offset.

-
-dx -(float): -Horizontal offset in pixels -
-
-dy -(float): -Vertical offset in pixels -
-
-Note: This modifies the x and y position properties by the given amounts. -
-
-
-compute_fov(...) -
-
-
resize(width, height)
-

Resize the element to new dimensions.

-
-width -(float): -New width in pixels -
-
-height -(float): -New height in pixels -
-
-Note: For Caption and Sprite, this may not change actual size if determined by content. -
-
-
-compute_dijkstra(...) -
-
-get_dijkstra_path(...) -
-
-is_in_fov(...) -
-
-find_path(...) -
-
-compute_astar_path(...) -
-
-
at(x, y)
-

Get the GridPoint at the specified grid coordinates.

-
-x -(int): -Grid x coordinate -
-
-y -(int): -Grid y coordinate -
-
-Returns: GridPoint or None: The grid point at (x, y), or None if out of bounds -
-
-
-get_dijkstra_distance(...) -
-
-
get_bounds()
-

Get the bounding rectangle of this drawable element.

-
-Returns: tuple: (x, y, width, height) representing the element's bounds -
-
-Note: The bounds are in screen coordinates and account for current position and size. -
-
-
-
-

GridPoint

-

UIGridPoint object

-

Properties:

-
-x: int: Grid x coordinate of this point -
-
-y: int: Grid y coordinate of this point -
-
-texture_index: int: Index of the texture/sprite to display at this point -
-
-solid: bool: Whether this point blocks movement -
-
-transparent: bool: Whether this point allows light/vision through -
-
-color: Color: Color tint applied to the texture at this point -
-
-
-

GridPointState

-

UIGridPointState object

-

Properties:

-
-visible: bool: Whether this point is currently visible to the player -
-
-discovered: bool: Whether this point has been discovered/explored -
-
-custom_flags: int: Bitfield for custom game-specific flags -
-
-
-

Scene

-

Base class for object-oriented scenes

-

Methods:

-
-
keypress(handler)
-

Register a keyboard handler function for this scene.

-
-handler -(callable): -Function that takes (key_name: str, is_pressed: bool) -
-
-Note: Alternative to overriding the on_keypress method. -
-
-
-
get_ui()
-

Get the UI element collection for this scene.

-
-Returns: UICollection: Collection of all UI elements in this scene -
-
-
-
activate()
-

Make this scene the active scene.

-
-Note: Equivalent to calling setScene() with this scene's name. -
-
-
-
register_keyboard(callable)
-

Register a keyboard event handler function for the scene.

-
-callable -(callable): -Function that takes (key: str, action: str) parameters -
-
-Note: Alternative to overriding the on_keypress method when subclassing Scene objects. -
-
-Example: -

-def handle_keyboard(key, action):
-    print(f"Key '{key}' was {action}")
-    if key == "q" and action == "press":
-        # Handle quit
-        pass
-scene.register_keyboard(handle_keyboard)
-
-
-
-
-
-

Sprite

-

Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None) - -A sprite UI element that displays a texture or portion of a texture atlas. - -Args: - x (float): X position in pixels. Default: 0 - y (float): Y position in pixels. Default: 0 - texture (Texture): Texture object to display. Default: None - sprite_index (int): Index into texture atlas (if applicable). Default: 0 - scale (float): Sprite scaling factor. Default: 1.0 - click (callable): Click event handler. Default: None - -Attributes: - x, y (float): Position in pixels - texture (Texture): The texture being displayed - sprite_index (int): Current sprite index in texture atlas - scale (float): Scale multiplier - click (callable): Click event handler - visible (bool): Visibility state - z_index (int): Rendering order - w, h (float): Read-only computed size based on texture and scale

-

Methods:

-
-
get_bounds()
-

Get the bounding rectangle of this drawable element.

-
-Returns: tuple: (x, y, width, height) representing the element's bounds -
-
-Note: The bounds are in screen coordinates and account for current position and size. -
-
-
-
move(dx, dy)
-

Move the element by a relative offset.

-
-dx -(float): -Horizontal offset in pixels -
-
-dy -(float): -Vertical offset in pixels -
-
-Note: This modifies the x and y position properties by the given amounts. -
-
-
-
resize(width, height)
-

Resize the element to new dimensions.

-
-width -(float): -New width in pixels -
-
-height -(float): -New height in pixels -
-
-Note: For Caption and Sprite, this may not change actual size if determined by content. -
-
-
-
-

Texture

-

SFML Texture Object

-
-
-

Timer

-

Timer object for scheduled callbacks

-

Methods:

-
-
resume()
-

Resume a paused timer.

-
-Note: Has no effect if timer is not paused. -
-
-
-
restart()
-

Restart the timer from the beginning.

-
-Note: Resets the timer's internal clock to zero. -
-
-
-
pause()
-

Pause the timer, stopping its callback execution.

-
-Note: Use resume() to continue the timer from where it was paused. -
-
-
-
cancel()
-

Cancel the timer and remove it from the system.

-
-Note: After cancelling, the timer object cannot be reused. -
-
-
-
-

UICollection

-

Iterable, indexable collection of UI objects

-

Methods:

-
-
remove(drawable)
-

Remove the first occurrence of a drawable from the collection.

-
-drawable -(UIDrawable): -The drawable to remove -
-
-
-
count(drawable)
-

Count the number of occurrences of a drawable in the collection.

-
-drawable -(UIDrawable): -The drawable to count -
-
-Returns: int: Number of times drawable appears in collection -
-
-
-
index(drawable)
-

Find the index of the first occurrence of a drawable.

-
-drawable -(UIDrawable): -The drawable to find -
-
-Returns: int: Index of drawable in collection -
-
-
-
extend(iterable)
-

Add all drawables from an iterable to the collection.

-
-iterable -(Iterable[UIDrawable]): -Drawables to add -
-
-
-
append(drawable)
-

Add a drawable element to the end of the collection.

-
-drawable -(UIDrawable): -The drawable element to add -
-
-
-
-

UICollectionIter

-

Iterator for a collection of UI objects

-
-
-

UIEntityCollectionIter

-

Iterator for a collection of UI objects

-
-
-

Vector

-

SFML Vector Object

-

Methods:

-
-
copy()
-

Create a copy of this vector.

-
-Returns: Vector: New Vector object with same x and y values -
-
-
-
angle()
-

Get the angle of this vector in radians.

-
-Returns: float: Angle in radians from positive x-axis -
-
-
-
dot(other)
-

Calculate the dot product with another vector.

-
-other -(Vector): -The other vector -
-
-Returns: float: Dot product of the two vectors -
-
-
-
magnitude()
-

Calculate the length/magnitude of this vector.

-
-Returns: float: The magnitude of the vector -
-
-Example: -

-length = vector.magnitude()
-
-
-
-
-
normalize()
-

Return a unit vector in the same direction.

-
-Returns: Vector: New normalized vector with magnitude 1.0 -
-
-
-
magnitude_squared()
-

Calculate the squared magnitude of this vector.

-
-Returns: float: The squared magnitude (faster than magnitude()) -
-
-Note: Use this for comparisons to avoid expensive square root calculation. -
-
-
-
distance_to(other)
-

Calculate the distance to another vector.

-
-other -(Vector): -The other vector -
-
-Returns: float: Distance between the two vectors -
-
-
-
-

Window

-

Window singleton for accessing and modifying the game window properties

-

Methods:

-
-
get()
-

Get the Window singleton instance.

-
-Returns: Window: The singleton window object -
-
-Note: This is a static method that returns the same instance every time. -
-
-
-
screenshot(filename)
-

Take a screenshot and save it to a file.

-
-filename -(str): -Path where to save the screenshot -
-
-Note: Supports PNG, JPG, and BMP formats based on file extension. -
-
-
-
center()
-

Center the window on the screen.

-
-Note: Only works if the window is not fullscreen. -
-
-
-

Automation Module

-

The mcrfpy.automation module provides testing and automation capabilities.

-
-

automation.click

-

Click at position

-
-
-

automation.doubleClick

-

Double click at position

-
-
-

automation.dragRel

-

Drag mouse relative to current position

-
-
-

automation.dragTo

-

Drag mouse to position

-
-
-

automation.hotkey

-

Press a hotkey combination (e.g., hotkey('ctrl', 'c'))

-
-
-

automation.keyDown

-

Press and hold a key

-
-
-

automation.keyUp

-

Release a key

-
-
-

automation.middleClick

-

Middle click at position

-
-
-

automation.mouseDown

-

Press mouse button

-
-
-

automation.mouseUp

-

Release mouse button

-
-
-

automation.moveRel

-

Move mouse relative to current position

-
-
-

automation.moveTo

-

Move mouse to absolute position

-
-
-

automation.onScreen

-

Check if coordinates are within screen bounds

-
-
-

automation.position

-

Get current mouse position as (x, y) tuple

-
-
-

automation.rightClick

-

Right click at position

-
-
-

automation.screenshot

-

Save a screenshot to the specified file

-
-
-

automation.scroll

-

Scroll wheel at position

-
-
-

automation.size

-

Get screen size as (width, height) tuple

-
-
-

automation.tripleClick

-

Triple click at position

-
-
-

automation.typewrite

-

Type text with optional interval between keystrokes

-
-
- - \ No newline at end of file diff --git a/docs/api_reference_dynamic.html b/docs/api_reference_dynamic.html new file mode 100644 index 0000000..82c247d --- /dev/null +++ b/docs/api_reference_dynamic.html @@ -0,0 +1,1059 @@ + + + + + + McRogueFace API Reference + + + +
+

McRogueFace API Reference

+

Generated on 2025-07-15 21:28:24

+

This documentation was dynamically generated from the compiled module.

+ + + +

Functions

+ +
+

createScenecreateScene(name: str) -> None

+

Create a new empty scene. + + +Note:

+

Arguments:

+
    +
  • name: Unique name for the new scene
  • +
  • ValueError: If a scene with this name already exists
  • +
+
+ +
+

createSoundBuffercreateSoundBuffer(filename: str) -> int

+

Load a sound effect from a file and return its buffer ID.

+

Arguments:

+
    +
  • filename: Path to the sound file (WAV, OGG, FLAC)
  • +
+

Returns: int: Buffer ID for use with playSound() RuntimeError: If the file cannot be loaded

+
+ +
+

currentScenecurrentScene() -> str

+

Get the name of the currently active scene.

+

Returns: str: Name of the current scene

+
+ +
+

delTimerdelTimer(name: str) -> None

+

Stop and remove a timer. + + +Note:

+

Arguments:

+
    +
  • name: Timer identifier to remove
  • +
+
+ +
+

exitexit() -> None

+

Cleanly shut down the game engine and exit the application. + + +Note:

+
+ +
+

findfind(name: str, scene: str = None) -> UIDrawable | None

+

Find the first UI element with the specified name. + + +Note:

+

Arguments:

+
    +
  • name: Exact name to search for
  • +
  • scene: Scene to search in (default: current scene)
  • +
+

Returns: Frame, Caption, Sprite, Grid, or Entity if found; None otherwise Searches scene UI elements and entities within grids.

+
+ +
+

findAllfindAll(pattern: str, scene: str = None) -> list

+

Find all UI elements matching a name pattern.

+

Arguments:

+
    +
  • pattern: Name pattern with optional wildcards (* matches any characters)
  • +
  • scene: Scene to search in (default: current scene)
  • +
+

Returns: list: All matching UI elements and entities

+

Example:

+
findAll('enemy*')  # Find all elements starting with 'enemy'
+    findAll('*_button')  # Find all elements ending with '_button'
+
+ +
+

getMetricsgetMetrics() -> dict

+

Get current performance metrics.

+

Returns: dict: Performance data with keys: - frame_time: Last frame duration in seconds - avg_frame_time: Average frame time - fps: Frames per second - draw_calls: Number of draw calls - ui_elements: Total UI element count - visible_elements: Visible element count - current_frame: Frame counter - runtime: Total runtime in seconds

+
+ +
+

getMusicVolumegetMusicVolume() -> int

+

Get the current music volume level.

+

Returns: int: Current volume (0-100)

+
+ +
+

getSoundVolumegetSoundVolume() -> int

+

Get the current sound effects volume level.

+

Returns: int: Current volume (0-100)

+
+ +
+

keypressScenekeypressScene(handler: callable) -> None

+

Set the keyboard event handler for the current scene.

+

Arguments:

+
    +
  • handler: Callable that receives (key_name: str, is_pressed: bool)
  • +
+

Example:

+
def on_key(key, pressed):
+        if key == 'A' and pressed:
+            print('A key pressed')
+    mcrfpy.keypressScene(on_key)
+
+ +
+

loadMusicloadMusic(filename: str) -> None

+

Load and immediately play background music from a file. + + +Note:

+

Arguments:

+
    +
  • filename: Path to the music file (WAV, OGG, FLAC)
  • +
+
+ +
+

playSoundplaySound(buffer_id: int) -> None

+

Play a sound effect using a previously loaded buffer.

+

Arguments:

+
    +
  • buffer_id: Sound buffer ID returned by createSoundBuffer()
  • +
  • RuntimeError: If the buffer ID is invalid
  • +
+
+ +
+

sceneUIsceneUI(scene: str = None) -> list

+

Get all UI elements for a scene.

+

Arguments:

+
    +
  • scene: Scene name. If None, uses current scene
  • +
+

Returns: list: All UI elements (Frame, Caption, Sprite, Grid) in the scene KeyError: If the specified scene doesn't exist

+
+ +
+

setMusicVolumesetMusicVolume(volume: int) -> None

+

Set the global music volume.

+

Arguments:

+
    +
  • volume: Volume level from 0 (silent) to 100 (full volume)
  • +
+
+ +
+

setScalesetScale(multiplier: float) -> None

+

Scale the game window size. + + +Note:

+

Arguments:

+
    +
  • multiplier: Scale factor (e.g., 2.0 for double size)
  • +
+
+ +
+

setScenesetScene(scene: str, transition: str = None, duration: float = 0.0) -> None

+

Switch to a different scene with optional transition effect.

+

Arguments:

+
    +
  • scene: Name of the scene to switch to
  • +
  • transition: Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')
  • +
  • duration: Transition duration in seconds (default: 0.0 for instant)
  • +
  • KeyError: If the scene doesn't exist
  • +
  • ValueError: If the transition type is invalid
  • +
+
+ +
+

setSoundVolumesetSoundVolume(volume: int) -> None

+

Set the global sound effects volume.

+

Arguments:

+
    +
  • volume: Volume level from 0 (silent) to 100 (full volume)
  • +
+
+ +
+

setTimersetTimer(name: str, handler: callable, interval: int) -> None

+

Create or update a recurring timer. + + +Note:

+

Arguments:

+
    +
  • name: Unique identifier for the timer
  • +
  • handler: Function called with (runtime: float) parameter
  • +
  • interval: Time between calls in milliseconds
  • +
+
+ +

Classes

+ +
+

Animation

+

Animation object for animating UI properties

+

Methods:

+ +
+
completecomplete() -> None
+

Complete the animation immediately by jumping to the final value.

+
+ +
+
get_current_value(...)
+

Get the current interpolated value

+
+ +
+
hasValidTargethasValidTarget() -> bool
+

Check if the animation still has a valid target.

+

Returns: True if the target still exists, False if it was destroyed.

+
+ +
+
startstart(target) -> None
+

Start the animation on a target UI element. + + +Note:

+
+
target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)
+
+
+ +
+
updateUpdate the animation by deltaTime (returns True if still running)
+
+
+ +
+

Caption

+

Inherits from: Drawable

+

Caption(pos=None, font=None, text='', **kwargs) + +A text display UI element with customizable font and styling. + +Args: + pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0) + font (Font, optional): Font object for text rendering. Default: engine default font + text (str, optional): The text content to display. Default: '' + +Keyword Args: + fill_color (Color): Text fill color. Default: (255, 255, 255, 255) + outline_color (Color): Text outline color. Default: (0, 0, 0, 255) + outline (float): Text outline thickness. Default: 0 + font_size (float): Font size in points. Default: 16 + click (callable): Click event handler. Default: None + visible (bool): Visibility state. Default: True + opacity (float): Opacity (0.0-1.0). Default: 1.0 + z_index (int): Rendering order. Default: 0 + name (str): Element name for finding. Default: None + x (float): X position override. Default: 0 + y (float): Y position override. Default: 0 + +Attributes: + text (str): The displayed text content + x, y (float): Position in pixels + pos (Vector): Position as a Vector object + font (Font): Font used for rendering + font_size (float): Font size in points + fill_color, outline_color (Color): Text appearance + outline (float): Outline thickness + click (callable): Click event handler + visible (bool): Visibility state + opacity (float): Opacity value + z_index (int): Rendering order + name (str): Element name + w, h (float): Read-only computed size based on text and font

+

Methods:

+ +
+
get_boundsGet bounding box as (x, y, width, height)
+
+ +
+
moveMove by relative offset (dx, dy)
+
+ +
+
resizeResize to new dimensions (width, height)
+
+
+ +
+

Color

+

SFML Color Object

+

Methods:

+ +
+
from_hexCreate Color from hex string (e.g., '#FF0000' or 'FF0000')
+
+ +
+
lerp(...)
+

Linearly interpolate between this color and another

+
+ +
+
to_hex(...)
+

Convert Color to hex string

+
+
+ +
+

Drawable

+

Base class for all drawable UI elements

+

Methods:

+ +
+
get_boundsGet bounding box as (x, y, width, height)
+
+ +
+
moveMove by relative offset (dx, dy)
+
+ +
+
resizeResize to new dimensions (width, height)
+
+
+ +
+

Entity

+

Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs) + +A game entity that exists on a grid with sprite rendering. + +Args: + grid_pos (tuple, optional): Grid position as (x, y) tuple. Default: (0, 0) + texture (Texture, optional): Texture object for sprite. Default: default texture + sprite_index (int, optional): Index into texture atlas. Default: 0 + +Keyword Args: + grid (Grid): Grid to attach entity to. Default: None + visible (bool): Visibility state. Default: True + opacity (float): Opacity (0.0-1.0). Default: 1.0 + name (str): Element name for finding. Default: None + x (float): X grid position override. Default: 0 + y (float): Y grid position override. Default: 0 + +Attributes: + pos (tuple): Grid position as (x, y) tuple + x, y (float): Grid position coordinates + draw_pos (tuple): Pixel position for rendering + gridstate (GridPointState): Visibility state for grid points + sprite_index (int): Current sprite index + visible (bool): Visibility state + opacity (float): Opacity value + name (str): Element name

+

Methods:

+ +
+
at(...)
+
+ +
+
die(...)
+

Remove this entity from its grid

+
+ +
+
get_boundsGet bounding box as (x, y, width, height)
+
+ +
+
index(...)
+

Return the index of this entity in its grid's entity collection

+
+ +
+
moveMove by relative offset (dx, dy)
+
+ +
+
path_topath_to(x: int, y: int) -> bool
+

Find and follow path to target position using A* pathfinding.

+
+
x: Target X coordinate
+
y: Target Y coordinate
+
+

Returns: True if a path was found and the entity started moving, False otherwise

+
+ +
+
resizeResize to new dimensions (width, height)
+
+ +
+
update_visibilityupdate_visibility() -> None
+

Update entity's visibility state based on current FOV. + +Recomputes which cells are visible from the entity's position and updates +the entity's gridstate to track explored areas. This is called automatically +when the entity moves if it has a grid with perspective set.

+
+
+ +
+

EntityCollection

+

Iterable, indexable collection of Entities

+

Methods:

+ +
+
append(...)
+
+ +
+
count(...)
+
+ +
+
extend(...)
+
+ +
+
index(...)
+
+ +
+
remove(...)
+
+
+ +
+

Font

+

SFML Font Object

+

Methods:

+
+ +
+

Frame

+

Inherits from: Drawable

+

Frame(pos=None, size=None, **kwargs) + +A rectangular frame UI element that can contain other drawable elements. + +Args: + pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0) + size (tuple, optional): Size as (width, height) tuple. Default: (0, 0) + +Keyword Args: + fill_color (Color): Background fill color. Default: (0, 0, 0, 128) + outline_color (Color): Border outline color. Default: (255, 255, 255, 255) + outline (float): Border outline thickness. Default: 0 + click (callable): Click event handler. Default: None + children (list): Initial list of child drawable elements. Default: None + visible (bool): Visibility state. Default: True + opacity (float): Opacity (0.0-1.0). Default: 1.0 + z_index (int): Rendering order. Default: 0 + name (str): Element name for finding. Default: None + x (float): X position override. Default: 0 + y (float): Y position override. Default: 0 + w (float): Width override. Default: 0 + h (float): Height override. Default: 0 + clip_children (bool): Whether to clip children to frame bounds. Default: False + +Attributes: + x, y (float): Position in pixels + w, h (float): Size in pixels + pos (Vector): Position as a Vector object + fill_color, outline_color (Color): Visual appearance + outline (float): Border thickness + click (callable): Click event handler + children (list): Collection of child drawable elements + visible (bool): Visibility state + opacity (float): Opacity value + z_index (int): Rendering order + name (str): Element name + clip_children (bool): Whether to clip children to frame bounds

+

Methods:

+ +
+
get_boundsGet bounding box as (x, y, width, height)
+
+ +
+
moveMove by relative offset (dx, dy)
+
+ +
+
resizeResize to new dimensions (width, height)
+
+
+ +
+

Grid

+

Inherits from: Drawable

+

Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs) + +A grid-based UI element for tile-based rendering and entity management. + +Args: + pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0) + size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size + grid_size (tuple, optional): Grid dimensions as (grid_x, grid_y) tuple. Default: (2, 2) + texture (Texture, optional): Texture containing tile sprites. Default: default texture + +Keyword Args: + fill_color (Color): Background fill color. Default: None + click (callable): Click event handler. Default: None + center_x (float): X coordinate of center point. Default: 0 + center_y (float): Y coordinate of center point. Default: 0 + zoom (float): Zoom level for rendering. Default: 1.0 + perspective (int): Entity perspective index (-1 for omniscient). Default: -1 + visible (bool): Visibility state. Default: True + opacity (float): Opacity (0.0-1.0). Default: 1.0 + z_index (int): Rendering order. Default: 0 + name (str): Element name for finding. Default: None + x (float): X position override. Default: 0 + y (float): Y position override. Default: 0 + w (float): Width override. Default: auto-calculated + h (float): Height override. Default: auto-calculated + grid_x (int): Grid width override. Default: 2 + grid_y (int): Grid height override. Default: 2 + +Attributes: + x, y (float): Position in pixels + w, h (float): Size in pixels + pos (Vector): Position as a Vector object + size (tuple): Size as (width, height) tuple + center (tuple): Center point as (x, y) tuple + center_x, center_y (float): Center point coordinates + zoom (float): Zoom level for rendering + grid_size (tuple): Grid dimensions (width, height) in tiles + grid_x, grid_y (int): Grid dimensions + texture (Texture): Tile texture atlas + fill_color (Color): Background color + entities (EntityCollection): Collection of entities in the grid + perspective (int): Entity perspective index + click (callable): Click event handler + visible (bool): Visibility state + opacity (float): Opacity value + z_index (int): Rendering order + name (str): Element name

+

Methods:

+ +
+
at(...)
+
+ +
+
compute_astar_pathcompute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]
+

Compute A* path between two points.

+
+
x1: Starting X coordinate
+
y1: Starting Y coordinate
+
x2: Target X coordinate
+
y2: Target Y coordinate
+
diagonal_cost: Cost of diagonal movement (default: 1.41)
+
+

Returns: List of (x, y) tuples representing the path, empty list if no path exists

+
+ +
+
compute_dijkstracompute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None
+

Compute Dijkstra map from root position.

+
+
root_x: X coordinate of the root/target
+
root_y: Y coordinate of the root/target
+
diagonal_cost: Cost of diagonal movement (default: 1.41)
+
+
+ +
+
compute_fovcompute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None
+

Compute field of view from a position.

+
+
x: X coordinate of the viewer
+
y: Y coordinate of the viewer
+
radius: Maximum view distance (0 = unlimited)
+
light_walls: Whether walls are lit when visible
+
algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)
+
+
+ +
+
find_pathfind_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]
+

Find A* path between two points.

+
+
x1: Starting X coordinate
+
y1: Starting Y coordinate
+
x2: Target X coordinate
+
y2: Target Y coordinate
+
diagonal_cost: Cost of diagonal movement (default: 1.41)
+
+

Returns: List of (x, y) tuples representing the path, empty list if no path exists

+
+ +
+
get_boundsGet bounding box as (x, y, width, height)
+
+ +
+
get_dijkstra_distanceget_dijkstra_distance(x: int, y: int) -> Optional[float]
+

Get distance from Dijkstra root to position.

+
+
x: X coordinate to query
+
y: Y coordinate to query
+
+

Returns: Distance as float, or None if position is unreachable or invalid

+
+ +
+
get_dijkstra_pathget_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]
+

Get path from position to Dijkstra root.

+
+
x: Starting X coordinate
+
y: Starting Y coordinate
+
+

Returns: List of (x, y) tuples representing path to root, empty if unreachable

+
+ +
+
is_in_fovis_in_fov(x: int, y: int) -> bool
+

Check if a cell is in the field of view.

+
+
x: X coordinate to check
+
y: Y coordinate to check
+
+

Returns: True if the cell is visible, False otherwise

+
+ +
+
moveMove by relative offset (dx, dy)
+
+ +
+
resizeResize to new dimensions (width, height)
+
+
+ +
+

GridPoint

+

UIGridPoint object

+

Methods:

+
+ +
+

GridPointState

+

UIGridPointState object

+

Methods:

+
+ +
+

Scene

+

Base class for object-oriented scenes

+

Methods:

+ +
+
activate(...)
+

Make this the active scene

+
+ +
+
get_ui(...)
+

Get the UI element collection for this scene

+
+ +
+
register_keyboardRegister a keyboard handler function (alternative to overriding on_keypress)
+
+
+ +
+

Sprite

+

Inherits from: Drawable

+

Sprite(pos=None, texture=None, sprite_index=0, **kwargs) + +A sprite UI element that displays a texture or portion of a texture atlas. + +Args: + pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0) + texture (Texture, optional): Texture object to display. Default: default texture + sprite_index (int, optional): Index into texture atlas. Default: 0 + +Keyword Args: + scale (float): Uniform scale factor. Default: 1.0 + scale_x (float): Horizontal scale factor. Default: 1.0 + scale_y (float): Vertical scale factor. Default: 1.0 + click (callable): Click event handler. Default: None + visible (bool): Visibility state. Default: True + opacity (float): Opacity (0.0-1.0). Default: 1.0 + z_index (int): Rendering order. Default: 0 + name (str): Element name for finding. Default: None + x (float): X position override. Default: 0 + y (float): Y position override. Default: 0 + +Attributes: + x, y (float): Position in pixels + pos (Vector): Position as a Vector object + texture (Texture): The texture being displayed + sprite_index (int): Current sprite index in texture atlas + scale (float): Uniform scale factor + scale_x, scale_y (float): Individual scale factors + click (callable): Click event handler + visible (bool): Visibility state + opacity (float): Opacity value + z_index (int): Rendering order + name (str): Element name + w, h (float): Read-only computed size based on texture and scale

+

Methods:

+ +
+
get_boundsGet bounding box as (x, y, width, height)
+
+ +
+
moveMove by relative offset (dx, dy)
+
+ +
+
resizeResize to new dimensions (width, height)
+
+
+ +
+

Texture

+

SFML Texture Object

+

Methods:

+
+ +
+

Timer

+

Timer(name, callback, interval, once=False) + +Create a timer that calls a function at regular intervals. + +Args: + name (str): Unique identifier for the timer + callback (callable): Function to call - receives (timer, runtime) args + interval (int): Time between calls in milliseconds + once (bool): If True, timer stops after first call. Default: False + +Attributes: + interval (int): Time between calls in milliseconds + remaining (int): Time until next call in milliseconds (read-only) + paused (bool): Whether timer is paused (read-only) + active (bool): Whether timer is active and not paused (read-only) + callback (callable): The callback function + once (bool): Whether timer stops after firing once + +Methods: + pause(): Pause the timer, preserving time remaining + resume(): Resume a paused timer + cancel(): Stop and remove the timer + restart(): Reset timer to start from beginning + +Example: + def on_timer(timer, runtime): + print(f'Timer {timer} fired at {runtime}ms') + if runtime > 5000: + timer.cancel() + + timer = mcrfpy.Timer('my_timer', on_timer, 1000) + timer.pause() # Pause timer + timer.resume() # Resume timer + timer.once = True # Make it one-shot

+

Methods:

+ +
+
cancelcancel() -> None
+

Cancel the timer and remove it from the timer system. +The timer will no longer fire and cannot be restarted.

+
+ +
+
pausepause() -> None
+

Pause the timer, preserving the time remaining until next trigger. +The timer can be resumed later with resume().

+
+ +
+
restartrestart() -> None
+

Restart the timer from the beginning. +Resets the timer to fire after a full interval from now.

+
+ +
+
resumeresume() -> None
+

Resume a paused timer from where it left off. +Has no effect if the timer is not paused.

+
+
+ +
+

UICollection

+

Iterable, indexable collection of UI objects

+

Methods:

+ +
+
append(...)
+
+ +
+
count(...)
+
+ +
+
extend(...)
+
+ +
+
index(...)
+
+ +
+
remove(...)
+
+
+ +
+

UICollectionIter

+

Iterator for a collection of UI objects

+

Methods:

+
+ +
+

UIEntityCollectionIter

+

Iterator for a collection of UI objects

+

Methods:

+
+ +
+

Vector

+

SFML Vector Object

+

Methods:

+ +
+
angle(...)
+

Return the angle in radians from the positive X axis

+
+ +
+
copy(...)
+

Return a copy of this vector

+
+ +
+
distance_to(...)
+

Return the distance to another vector

+
+ +
+
dot(...)
+

Return the dot product with another vector

+
+ +
+
magnitude(...)
+

Return the length of the vector

+
+ +
+
magnitude_squared(...)
+

Return the squared length of the vector

+
+ +
+
normalize(...)
+

Return a unit vector in the same direction

+
+
+ +
+

Window

+

Window singleton for accessing and modifying the game window properties

+

Methods:

+ +
+
center(...)
+

Center the window on the screen

+
+ +
+
get(...)
+

Get the Window singleton instance

+
+ +
+
screenshot(...)
+

Take a screenshot. Pass filename to save to file, or get raw bytes if no filename.

+
+
+ +

Constants

+
    +
  • FOV_BASIC (int): 0
  • +
  • FOV_DIAMOND (int): 1
  • +
  • FOV_PERMISSIVE_0 (int): 3
  • +
  • FOV_PERMISSIVE_1 (int): 4
  • +
  • FOV_PERMISSIVE_2 (int): 5
  • +
  • FOV_PERMISSIVE_3 (int): 6
  • +
  • FOV_PERMISSIVE_4 (int): 7
  • +
  • FOV_PERMISSIVE_5 (int): 8
  • +
  • FOV_PERMISSIVE_6 (int): 9
  • +
  • FOV_PERMISSIVE_7 (int): 10
  • +
  • FOV_PERMISSIVE_8 (int): 11
  • +
  • FOV_RESTRICTIVE (int): 12
  • +
  • FOV_SHADOW (int): 2
  • +
+ +
+ + diff --git a/docs/stubs/mcrfpy.pyi b/docs/stubs/mcrfpy.pyi new file mode 100644 index 0000000..919794b --- /dev/null +++ b/docs/stubs/mcrfpy.pyi @@ -0,0 +1,532 @@ +"""Type stubs for McRogueFace Python API. + +Core game engine interface for creating roguelike games with Python. +""" + +from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload + +# Type aliases +UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid'] +Transition = Union[str, None] + +# Classes + +class Color: + """SFML Color Object for RGBA colors.""" + + r: int + g: int + b: int + a: int + + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ... + + def from_hex(self, hex_string: str) -> 'Color': + """Create color from hex string (e.g., '#FF0000' or 'FF0000').""" + ... + + def to_hex(self) -> str: + """Convert color to hex string format.""" + ... + + def lerp(self, other: 'Color', t: float) -> 'Color': + """Linear interpolation between two colors.""" + ... + +class Vector: + """SFML Vector Object for 2D coordinates.""" + + x: float + y: float + + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, x: float, y: float) -> None: ... + + def add(self, other: 'Vector') -> 'Vector': ... + def subtract(self, other: 'Vector') -> 'Vector': ... + def multiply(self, scalar: float) -> 'Vector': ... + def divide(self, scalar: float) -> 'Vector': ... + def distance(self, other: 'Vector') -> float: ... + def normalize(self) -> 'Vector': ... + def dot(self, other: 'Vector') -> float: ... + +class Texture: + """SFML Texture Object for images.""" + + def __init__(self, filename: str) -> None: ... + + filename: str + width: int + height: int + sprite_count: int + +class Font: + """SFML Font Object for text rendering.""" + + def __init__(self, filename: str) -> None: ... + + filename: str + family: str + +class Drawable: + """Base class for all drawable UI elements.""" + + x: float + y: float + visible: bool + z_index: int + name: str + pos: Vector + + def get_bounds(self) -> Tuple[float, float, float, float]: + """Get bounding box as (x, y, width, height).""" + ... + + def move(self, dx: float, dy: float) -> None: + """Move by relative offset (dx, dy).""" + ... + + def resize(self, width: float, height: float) -> None: + """Resize to new dimensions (width, height).""" + ... + +class Frame(Drawable): + """Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None) + + A rectangular frame UI element that can contain other drawable elements. + """ + + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0, + fill_color: Optional[Color] = None, outline_color: Optional[Color] = None, + outline: float = 0, click: Optional[Callable] = None, + children: Optional[List[UIElement]] = None) -> None: ... + + w: float + h: float + fill_color: Color + outline_color: Color + outline: float + click: Optional[Callable[[float, float, int], None]] + children: 'UICollection' + clip_children: bool + +class Caption(Drawable): + """Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None) + + A text display UI element with customizable font and styling. + """ + + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, text: str = '', x: float = 0, y: float = 0, + font: Optional[Font] = None, fill_color: Optional[Color] = None, + outline_color: Optional[Color] = None, outline: float = 0, + click: Optional[Callable] = None) -> None: ... + + text: str + font: Font + fill_color: Color + outline_color: Color + outline: float + click: Optional[Callable[[float, float, int], None]] + w: float # Read-only, computed from text + h: float # Read-only, computed from text + +class Sprite(Drawable): + """Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None) + + A sprite UI element that displays a texture or portion of a texture atlas. + """ + + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None, + sprite_index: int = 0, scale: float = 1.0, + click: Optional[Callable] = None) -> None: ... + + texture: Texture + sprite_index: int + scale: float + click: Optional[Callable[[float, float, int], None]] + w: float # Read-only, computed from texture + h: float # Read-only, computed from texture + +class Grid(Drawable): + """Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None) + + A grid-based tilemap UI element for rendering tile-based levels and game worlds. + """ + + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20), + texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16, + scale: float = 1.0, click: Optional[Callable] = None) -> None: ... + + grid_size: Tuple[int, int] + tile_width: int + tile_height: int + texture: Texture + scale: float + points: List[List['GridPoint']] + entities: 'EntityCollection' + background_color: Color + click: Optional[Callable[[int, int, int], None]] + + def at(self, x: int, y: int) -> 'GridPoint': + """Get grid point at tile coordinates.""" + ... + +class GridPoint: + """Grid point representing a single tile.""" + + texture_index: int + solid: bool + color: Color + +class GridPointState: + """State information for a grid point.""" + + texture_index: int + color: Color + +class Entity(Drawable): + """Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='') + + Game entity that lives within a Grid. + """ + + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None, + sprite_index: int = 0, name: str = '') -> None: ... + + grid_x: float + grid_y: float + texture: Texture + sprite_index: int + grid: Optional[Grid] + + def at(self, grid_x: float, grid_y: float) -> None: + """Move entity to grid position.""" + ... + + def die(self) -> None: + """Remove entity from its grid.""" + ... + + def index(self) -> int: + """Get index in parent grid's entity collection.""" + ... + +class UICollection: + """Collection of UI drawable elements (Frame, Caption, Sprite, Grid).""" + + def __len__(self) -> int: ... + def __getitem__(self, index: int) -> UIElement: ... + def __setitem__(self, index: int, value: UIElement) -> None: ... + def __delitem__(self, index: int) -> None: ... + def __contains__(self, item: UIElement) -> bool: ... + def __iter__(self) -> Any: ... + def __add__(self, other: 'UICollection') -> 'UICollection': ... + def __iadd__(self, other: 'UICollection') -> 'UICollection': ... + + def append(self, item: UIElement) -> None: ... + def extend(self, items: List[UIElement]) -> None: ... + def remove(self, item: UIElement) -> None: ... + def index(self, item: UIElement) -> int: ... + def count(self, item: UIElement) -> int: ... + +class EntityCollection: + """Collection of Entity objects.""" + + def __len__(self) -> int: ... + def __getitem__(self, index: int) -> Entity: ... + def __setitem__(self, index: int, value: Entity) -> None: ... + def __delitem__(self, index: int) -> None: ... + def __contains__(self, item: Entity) -> bool: ... + def __iter__(self) -> Any: ... + def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ... + def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ... + + def append(self, item: Entity) -> None: ... + def extend(self, items: List[Entity]) -> None: ... + def remove(self, item: Entity) -> None: ... + def index(self, item: Entity) -> int: ... + def count(self, item: Entity) -> int: ... + +class Scene: + """Base class for object-oriented scenes.""" + + name: str + + def __init__(self, name: str) -> None: ... + + def activate(self) -> None: + """Called when scene becomes active.""" + ... + + def deactivate(self) -> None: + """Called when scene becomes inactive.""" + ... + + def get_ui(self) -> UICollection: + """Get UI elements collection.""" + ... + + def on_keypress(self, key: str, pressed: bool) -> None: + """Handle keyboard events.""" + ... + + def on_click(self, x: float, y: float, button: int) -> None: + """Handle mouse clicks.""" + ... + + def on_enter(self) -> None: + """Called when entering the scene.""" + ... + + def on_exit(self) -> None: + """Called when leaving the scene.""" + ... + + def on_resize(self, width: int, height: int) -> None: + """Handle window resize events.""" + ... + + def update(self, dt: float) -> None: + """Update scene logic.""" + ... + +class Timer: + """Timer object for scheduled callbacks.""" + + name: str + interval: int + active: bool + + def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ... + + def pause(self) -> None: + """Pause the timer.""" + ... + + def resume(self) -> None: + """Resume the timer.""" + ... + + def cancel(self) -> None: + """Cancel and remove the timer.""" + ... + +class Window: + """Window singleton for managing the game window.""" + + resolution: Tuple[int, int] + fullscreen: bool + vsync: bool + title: str + fps_limit: int + game_resolution: Tuple[int, int] + scaling_mode: str + + @staticmethod + def get() -> 'Window': + """Get the window singleton instance.""" + ... + +class Animation: + """Animation object for animating UI properties.""" + + target: Any + property: str + duration: float + easing: str + loop: bool + on_complete: Optional[Callable] + + def __init__(self, target: Any, property: str, start_value: Any, end_value: Any, + duration: float, easing: str = 'linear', loop: bool = False, + on_complete: Optional[Callable] = None) -> None: ... + + def start(self) -> None: + """Start the animation.""" + ... + + def update(self, dt: float) -> bool: + """Update animation, returns True if still running.""" + ... + + def get_current_value(self) -> Any: + """Get the current interpolated value.""" + ... + +# Module functions + +def createSoundBuffer(filename: str) -> int: + """Load a sound effect from a file and return its buffer ID.""" + ... + +def loadMusic(filename: str) -> None: + """Load and immediately play background music from a file.""" + ... + +def setMusicVolume(volume: int) -> None: + """Set the global music volume (0-100).""" + ... + +def setSoundVolume(volume: int) -> None: + """Set the global sound effects volume (0-100).""" + ... + +def playSound(buffer_id: int) -> None: + """Play a sound effect using a previously loaded buffer.""" + ... + +def getMusicVolume() -> int: + """Get the current music volume level (0-100).""" + ... + +def getSoundVolume() -> int: + """Get the current sound effects volume level (0-100).""" + ... + +def sceneUI(scene: Optional[str] = None) -> UICollection: + """Get all UI elements for a scene.""" + ... + +def currentScene() -> str: + """Get the name of the currently active scene.""" + ... + +def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None: + """Switch to a different scene with optional transition effect.""" + ... + +def createScene(name: str) -> None: + """Create a new empty scene.""" + ... + +def keypressScene(handler: Callable[[str, bool], None]) -> None: + """Set the keyboard event handler for the current scene.""" + ... + +def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None: + """Create or update a recurring timer.""" + ... + +def delTimer(name: str) -> None: + """Stop and remove a timer.""" + ... + +def exit() -> None: + """Cleanly shut down the game engine and exit the application.""" + ... + +def setScale(multiplier: float) -> None: + """Scale the game window size (deprecated - use Window.resolution).""" + ... + +def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]: + """Find the first UI element with the specified name.""" + ... + +def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]: + """Find all UI elements matching a name pattern (supports * wildcards).""" + ... + +def getMetrics() -> Dict[str, Union[int, float]]: + """Get current performance metrics.""" + ... + +# Submodule +class automation: + """Automation API for testing and scripting.""" + + @staticmethod + def screenshot(filename: str) -> bool: + """Save a screenshot to the specified file.""" + ... + + @staticmethod + def position() -> Tuple[int, int]: + """Get current mouse position as (x, y) tuple.""" + ... + + @staticmethod + def size() -> Tuple[int, int]: + """Get screen size as (width, height) tuple.""" + ... + + @staticmethod + def onScreen(x: int, y: int) -> bool: + """Check if coordinates are within screen bounds.""" + ... + + @staticmethod + def moveTo(x: int, y: int, duration: float = 0.0) -> None: + """Move mouse to absolute position.""" + ... + + @staticmethod + def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None: + """Move mouse relative to current position.""" + ... + + @staticmethod + def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None: + """Drag mouse to position.""" + ... + + @staticmethod + def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None: + """Drag mouse relative to current position.""" + ... + + @staticmethod + def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1, + interval: float = 0.0, button: str = 'left') -> None: + """Click mouse at position.""" + ... + + @staticmethod + def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None: + """Press mouse button down.""" + ... + + @staticmethod + def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None: + """Release mouse button.""" + ... + + @staticmethod + def keyDown(key: str) -> None: + """Press key down.""" + ... + + @staticmethod + def keyUp(key: str) -> None: + """Release key.""" + ... + + @staticmethod + def press(key: str) -> None: + """Press and release a key.""" + ... + + @staticmethod + def typewrite(text: str, interval: float = 0.0) -> None: + """Type text with optional interval between characters.""" + ... diff --git a/docs/stubs/mcrfpy/__init__.pyi b/docs/stubs/mcrfpy/__init__.pyi new file mode 100644 index 0000000..39353c3 --- /dev/null +++ b/docs/stubs/mcrfpy/__init__.pyi @@ -0,0 +1,209 @@ +"""Type stubs for McRogueFace Python API. + +Auto-generated - do not edit directly. +""" + +from typing import Any, List, Dict, Tuple, Optional, Callable, Union + +# Module documentation +# McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n + +# Classes + +class Animation: + """Animation object for animating UI properties""" + def __init__(selftype(self)) -> None: ... + + def get_current_value(self, *args, **kwargs) -> Any: ... + def start(self, *args, **kwargs) -> Any: ... + def update(selfreturns True if still running) -> Any: ... + +class Caption: + """Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)""" + def __init__(selftype(self)) -> None: ... + + def get_bounds(selfx, y, width, height) -> Any: ... + def move(selfdx, dy) -> Any: ... + def resize(selfwidth, height) -> Any: ... + +class Color: + """SFML Color Object""" + def __init__(selftype(self)) -> None: ... + + def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ... + def lerp(self, *args, **kwargs) -> Any: ... + def to_hex(self, *args, **kwargs) -> Any: ... + +class Drawable: + """Base class for all drawable UI elements""" + def __init__(selftype(self)) -> None: ... + + def get_bounds(selfx, y, width, height) -> Any: ... + def move(selfdx, dy) -> Any: ... + def resize(selfwidth, height) -> Any: ... + +class Entity: + """UIEntity objects""" + def __init__(selftype(self)) -> None: ... + + def at(self, *args, **kwargs) -> Any: ... + def die(self, *args, **kwargs) -> Any: ... + def get_bounds(selfx, y, width, height) -> Any: ... + def index(self, *args, **kwargs) -> Any: ... + def move(selfdx, dy) -> Any: ... + def path_to(selfx: int, y: int) -> bool: ... + def resize(selfwidth, height) -> Any: ... + def update_visibility(self) -> None: ... + +class EntityCollection: + """Iterable, indexable collection of Entities""" + def __init__(selftype(self)) -> None: ... + + def append(self, *args, **kwargs) -> Any: ... + def count(self, *args, **kwargs) -> Any: ... + def extend(self, *args, **kwargs) -> Any: ... + def index(self, *args, **kwargs) -> Any: ... + def remove(self, *args, **kwargs) -> Any: ... + +class Font: + """SFML Font Object""" + def __init__(selftype(self)) -> None: ... + +class Frame: + """Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)""" + def __init__(selftype(self)) -> None: ... + + def get_bounds(selfx, y, width, height) -> Any: ... + def move(selfdx, dy) -> Any: ... + def resize(selfwidth, height) -> Any: ... + +class Grid: + """Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)""" + def __init__(selftype(self)) -> None: ... + + def at(self, *args, **kwargs) -> Any: ... + def compute_astar_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ... + def compute_dijkstra(selfroot_x: int, root_y: int, diagonal_cost: float = 1.41) -> None: ... + def compute_fov(selfx: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None: ... + def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ... + def get_bounds(selfx, y, width, height) -> Any: ... + def get_dijkstra_distance(selfx: int, y: int) -> Optional[float]: ... + def get_dijkstra_path(selfx: int, y: int) -> List[Tuple[int, int]]: ... + def is_in_fov(selfx: int, y: int) -> bool: ... + def move(selfdx, dy) -> Any: ... + def resize(selfwidth, height) -> Any: ... + +class GridPoint: + """UIGridPoint object""" + def __init__(selftype(self)) -> None: ... + +class GridPointState: + """UIGridPointState object""" + def __init__(selftype(self)) -> None: ... + +class Scene: + """Base class for object-oriented scenes""" + def __init__(selftype(self)) -> None: ... + + def activate(self, *args, **kwargs) -> Any: ... + def get_ui(self, *args, **kwargs) -> Any: ... + def register_keyboard(selfalternative to overriding on_keypress) -> Any: ... + +class Sprite: + """Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)""" + def __init__(selftype(self)) -> None: ... + + def get_bounds(selfx, y, width, height) -> Any: ... + def move(selfdx, dy) -> Any: ... + def resize(selfwidth, height) -> Any: ... + +class Texture: + """SFML Texture Object""" + def __init__(selftype(self)) -> None: ... + +class Timer: + """Timer object for scheduled callbacks""" + def __init__(selftype(self)) -> None: ... + + def cancel(self, *args, **kwargs) -> Any: ... + def pause(self, *args, **kwargs) -> Any: ... + def restart(self, *args, **kwargs) -> Any: ... + def resume(self, *args, **kwargs) -> Any: ... + +class UICollection: + """Iterable, indexable collection of UI objects""" + def __init__(selftype(self)) -> None: ... + + def append(self, *args, **kwargs) -> Any: ... + def count(self, *args, **kwargs) -> Any: ... + def extend(self, *args, **kwargs) -> Any: ... + def index(self, *args, **kwargs) -> Any: ... + def remove(self, *args, **kwargs) -> Any: ... + +class UICollectionIter: + """Iterator for a collection of UI objects""" + def __init__(selftype(self)) -> None: ... + +class UIEntityCollectionIter: + """Iterator for a collection of UI objects""" + def __init__(selftype(self)) -> None: ... + +class Vector: + """SFML Vector Object""" + def __init__(selftype(self)) -> None: ... + + def angle(self, *args, **kwargs) -> Any: ... + def copy(self, *args, **kwargs) -> Any: ... + def distance_to(self, *args, **kwargs) -> Any: ... + def dot(self, *args, **kwargs) -> Any: ... + def magnitude(self, *args, **kwargs) -> Any: ... + def magnitude_squared(self, *args, **kwargs) -> Any: ... + def normalize(self, *args, **kwargs) -> Any: ... + +class Window: + """Window singleton for accessing and modifying the game window properties""" + def __init__(selftype(self)) -> None: ... + + def center(self, *args, **kwargs) -> Any: ... + def get(self, *args, **kwargs) -> Any: ... + def screenshot(self, *args, **kwargs) -> Any: ... + +# Functions + +def createScene(name: str) -> None: ... +def createSoundBuffer(filename: str) -> int: ... +def currentScene() -> str: ... +def delTimer(name: str) -> None: ... +def exit() -> None: ... +def find(name: str, scene: str = None) -> UIDrawable | None: ... +def findAll(pattern: str, scene: str = None) -> list: ... +def getMetrics() -> dict: ... +def getMusicVolume() -> int: ... +def getSoundVolume() -> int: ... +def keypressScene(handler: callable) -> None: ... +def loadMusic(filename: str) -> None: ... +def playSound(buffer_id: int) -> None: ... +def sceneUI(scene: str = None) -> list: ... +def setMusicVolume(volume: int) -> None: ... +def setScale(multiplier: float) -> None: ... +def setScene(scene: str, transition: str = None, duration: float = 0.0) -> None: ... +def setSoundVolume(volume: int) -> None: ... +def setTimer(name: str, handler: callable, interval: int) -> None: ... + +# Constants + +FOV_BASIC: int +FOV_DIAMOND: int +FOV_PERMISSIVE_0: int +FOV_PERMISSIVE_1: int +FOV_PERMISSIVE_2: int +FOV_PERMISSIVE_3: int +FOV_PERMISSIVE_4: int +FOV_PERMISSIVE_5: int +FOV_PERMISSIVE_6: int +FOV_PERMISSIVE_7: int +FOV_PERMISSIVE_8: int +FOV_RESTRICTIVE: int +FOV_SHADOW: int +default_font: Any +default_texture: Any \ No newline at end of file diff --git a/docs/stubs/mcrfpy/automation.pyi b/docs/stubs/mcrfpy/automation.pyi new file mode 100644 index 0000000..57ed71a --- /dev/null +++ b/docs/stubs/mcrfpy/automation.pyi @@ -0,0 +1,24 @@ +"""Type stubs for McRogueFace automation API.""" + +from typing import Optional, Tuple + +def click(x=None, y=None, clicks=1, interval=0.0, button='left') -> Any: ... +def doubleClick(x=None, y=None) -> Any: ... +def dragRel(xOffset, yOffset, duration=0.0, button='left') -> Any: ... +def dragTo(x, y, duration=0.0, button='left') -> Any: ... +def hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c')) -> Any: ... +def keyDown(key) -> Any: ... +def keyUp(key) -> Any: ... +def middleClick(x=None, y=None) -> Any: ... +def mouseDown(x=None, y=None, button='left') -> Any: ... +def mouseUp(x=None, y=None, button='left') -> Any: ... +def moveRel(xOffset, yOffset, duration=0.0) -> Any: ... +def moveTo(x, y, duration=0.0) -> Any: ... +def onScreen(x, y) -> Any: ... +def position() - Get current mouse position as (x, y) -> Any: ... +def rightClick(x=None, y=None) -> Any: ... +def screenshot(filename) -> Any: ... +def scroll(clicks, x=None, y=None) -> Any: ... +def size() - Get screen size as (width, height) -> Any: ... +def tripleClick(x=None, y=None) -> Any: ... +def typewrite(message, interval=0.0) -> Any: ... \ No newline at end of file diff --git a/docs/stubs/py.typed b/docs/stubs/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/roguelike_tutorial/part_0.py b/roguelike_tutorial/part_0.py new file mode 100644 index 0000000..eb9ed94 --- /dev/null +++ b/roguelike_tutorial/part_0.py @@ -0,0 +1,80 @@ +""" +McRogueFace Tutorial - Part 0: Introduction to Scene, Texture, and Grid + +This tutorial introduces the basic building blocks: +- Scene: A container for UI elements and game state +- Texture: Loading image assets for use in the game +- Grid: A tilemap component for rendering tile-based worlds +""" +import mcrfpy +import random + +# Create and activate a new scene +mcrfpy.createScene("tutorial") +mcrfpy.setScene("tutorial") + +# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile) +texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = zoom +grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 0", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((280, 750), + text="Scene + Texture + Grid = Tilemap!", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 0 loaded!") +print(f"Created a {grid.grid_size[0]}x{grid.grid_size[1]} grid") +print(f"Grid positioned at ({grid.x}, {grid.y})") diff --git a/roguelike_tutorial/part_1.py b/roguelike_tutorial/part_1.py new file mode 100644 index 0000000..4c19d6d --- /dev/null +++ b/roguelike_tutorial/part_1.py @@ -0,0 +1,116 @@ +""" +McRogueFace Tutorial - Part 1: Entities and Keyboard Input + +This tutorial builds on Part 0 by adding: +- Entity: A game object that can be placed in a grid +- Keyboard handling: Responding to key presses to move the entity +""" +import mcrfpy +import random + +# Create and activate a new scene +mcrfpy.createScene("tutorial") +mcrfpy.setScene("tutorial") + +# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile) +texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16) + +# Load the hero sprite texture (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = zoom +grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) + +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + if state == "start": # Only respond to key press, not release + # Get current player position in grid coordinates + px, py = player.x, player.y + + # Calculate new position based on key press + if key == "W" or key == "Up": + py -= 1 + elif key == "S" or key == "Down": + py += 1 + elif key == "A" or key == "Left": + px -= 1 + elif key == "D" or key == "Right": + px += 1 + + # Update player position (no collision checking yet) + player.x = px + player.y = py + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 1", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((200, 750), + text="Use WASD or Arrow Keys to move the hero!", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 1 loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/part_1b.py b/roguelike_tutorial/part_1b.py new file mode 100644 index 0000000..3894fc7 --- /dev/null +++ b/roguelike_tutorial/part_1b.py @@ -0,0 +1,117 @@ +""" +McRogueFace Tutorial - Part 1: Entities and Keyboard Input + +This tutorial builds on Part 0 by adding: +- Entity: A game object that can be placed in a grid +- Keyboard handling: Responding to key presses to move the entity +""" +import mcrfpy +import random + +# Create and activate a new scene +mcrfpy.createScene("tutorial") +mcrfpy.setScene("tutorial") + +# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile) +texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16) + +# Load the hero sprite texture (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big! + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) +grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + if state == "start": # Only respond to key press, not release + # Get current player position in grid coordinates + px, py = player.x, player.y + + # Calculate new position based on key press + if key == "W" or key == "Up": + py -= 1 + elif key == "S" or key == "Down": + py += 1 + elif key == "A" or key == "Left": + px -= 1 + elif key == "D" or key == "Right": + px += 1 + + # Update player position (no collision checking yet) + player.x = px + player.y = py + grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 1", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((200, 750), + text="Use WASD or Arrow Keys to move the hero!", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 1 loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/part_2-naive.py b/roguelike_tutorial/part_2-naive.py new file mode 100644 index 0000000..6959a4b --- /dev/null +++ b/roguelike_tutorial/part_2-naive.py @@ -0,0 +1,149 @@ +""" +McRogueFace Tutorial - Part 2: Animated Movement + +This tutorial builds on Part 1 by adding: +- Animation system for smooth movement +- Movement that takes 0.5 seconds per tile +- Input blocking during movement animation +""" +import mcrfpy +import random + +# Create and activate a new scene +mcrfpy.createScene("tutorial") +mcrfpy.setScene("tutorial") + +# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile) +texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16) + +# Load the hero sprite texture (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big! + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) +grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Movement state tracking +is_moving = False +move_animations = [] # Track active animations + +# Animation completion callback +def movement_complete(runtime): + """Called when movement animation completes""" + global is_moving + is_moving = False + # Ensure grid is centered on final position + grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 + +motion_speed = 0.30 # seconds per tile +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + global is_moving, move_animations + + if state == "start" and not is_moving: # Only respond to key press when not moving + # Get current player position in grid coordinates + px, py = player.x, player.y + new_x, new_y = px, py + + # Calculate new position based on key press + if key == "W" or key == "Up": + new_y -= 1 + elif key == "S" or key == "Down": + new_y += 1 + elif key == "A" or key == "Left": + new_x -= 1 + elif key == "D" or key == "Right": + new_x += 1 + + # If position changed, start movement animation + if new_x != px or new_y != py: + is_moving = True + + # Create animations for player position + anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad") + anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad") + anim_x.start(player) + anim_y.start(player) + + # Animate grid center to follow player + center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear") + center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear") + center_x.start(grid) + center_y.start(grid) + + # Set a timer to mark movement as complete + mcrfpy.setTimer("move_complete", movement_complete, 500) + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 2", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((150, 750), + text="Smooth movement! Each step takes 0.5 seconds.", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 2 loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Movement is now animated over 0.5 seconds per tile!") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/part_2-onemovequeued.py b/roguelike_tutorial/part_2-onemovequeued.py new file mode 100644 index 0000000..126c433 --- /dev/null +++ b/roguelike_tutorial/part_2-onemovequeued.py @@ -0,0 +1,241 @@ +""" +McRogueFace Tutorial - Part 2: Enhanced with Single Move Queue + +This tutorial builds on Part 2 by adding: +- Single queued move system for responsive input +- Debug display showing position and queue status +- Smooth continuous movement when keys are held +- Animation callbacks to prevent race conditions +""" +import mcrfpy +import random + +# Create and activate a new scene +mcrfpy.createScene("tutorial") +mcrfpy.setScene("tutorial") + +# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile) +texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16) + +# Load the hero sprite texture (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big! + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) +grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Movement state tracking +is_moving = False +move_queue = [] # List to store queued moves (max 1 item) +#last_position = (4, 4) # Track last position +current_destination = None # Track where we're currently moving to +current_move = None # Track current move direction + +# Store animation references +player_anim_x = None +player_anim_y = None +grid_anim_x = None +grid_anim_y = None + +# Debug display caption +debug_caption = mcrfpy.Caption((10, 40), + text="Last: (4, 4) | Queue: 0 | Dest: None", +) +debug_caption.font_size = 16 +debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255) +mcrfpy.sceneUI("tutorial").append(debug_caption) + +# Additional debug caption for movement state +move_debug_caption = mcrfpy.Caption((10, 60), + text="Moving: False | Current: None | Queued: None", +) +move_debug_caption.font_size = 16 +move_debug_caption.fill_color = mcrfpy.Color(255, 200, 0, 255) +mcrfpy.sceneUI("tutorial").append(move_debug_caption) + +def key_to_direction(key): + """Convert key to direction string""" + if key == "W" or key == "Up": + return "Up" + elif key == "S" or key == "Down": + return "Down" + elif key == "A" or key == "Left": + return "Left" + elif key == "D" or key == "Right": + return "Right" + return None + +def update_debug_display(): + """Update the debug caption with current state""" + queue_count = len(move_queue) + dest_text = f"({current_destination[0]}, {current_destination[1]})" if current_destination else "None" + debug_caption.text = f"Last: ({player.x}, {player.y}) | Queue: {queue_count} | Dest: {dest_text}" + + # Update movement state debug + current_dir = key_to_direction(current_move) if current_move else "None" + queued_dir = key_to_direction(move_queue[0]) if move_queue else "None" + move_debug_caption.text = f"Moving: {is_moving} | Current: {current_dir} | Queued: {queued_dir}" + +# Animation completion callback +def movement_complete(anim, target): + """Called when movement animation completes""" + global is_moving, move_queue, current_destination, current_move + global player_anim_x, player_anim_y + print(f"In callback for animation: {anim=} {target=}") + # Clear movement state + is_moving = False + current_move = None + current_destination = None + # Clear animation references + player_anim_x = None + player_anim_y = None + + # Update last position to where we actually are now + #last_position = (int(player.x), int(player.y)) + + # Ensure grid is centered on final position + grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 + + # Check if there's a queued move + if move_queue: + # Pop the next move from the queue + next_move = move_queue.pop(0) + print(f"Processing queued move: {next_move}") + # Process it like a fresh input + process_move(next_move) + + update_debug_display() + +motion_speed = 0.30 # seconds per tile + +def process_move(key): + """Process a move based on the key""" + global is_moving, current_move, current_destination, move_queue + global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y + + # If already moving, just update the queue + if is_moving: + print(f"process_move processing {key=} as a queued move (is_moving = True)") + # Clear queue and add new move (only keep 1 queued move) + move_queue.clear() + move_queue.append(key) + update_debug_display() + return + print(f"process_move processing {key=} as a new, immediate animation (is_moving = False)") + # Calculate new position from current position + px, py = int(player.x), int(player.y) + new_x, new_y = px, py + + # Calculate new position based on key press (only one tile movement) + if key == "W" or key == "Up": + new_y -= 1 + elif key == "S" or key == "Down": + new_y += 1 + elif key == "A" or key == "Left": + new_x -= 1 + elif key == "D" or key == "Right": + new_x += 1 + + # Start the move if position changed + if new_x != px or new_y != py: + is_moving = True + current_move = key + current_destination = (new_x, new_y) + # only animate a single axis, same callback from either + if new_x != px: + player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete) + player_anim_x.start(player) + elif new_y != py: + player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete) + player_anim_y.start(player) + + # Animate grid center to follow player + grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear") + grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear") + grid_anim_x.start(grid) + grid_anim_y.start(grid) + + update_debug_display() + +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + if state == "start": + # Only process movement keys + if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]: + print(f"handle_keys producing actual input: {key=}") + process_move(key) + + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 2 Enhanced", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((150, 750), + text="One-move queue system with animation callbacks!", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 2 Enhanced loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Movement now uses animation callbacks to prevent race conditions!") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/part_2.py b/roguelike_tutorial/part_2.py new file mode 100644 index 0000000..66a11b0 --- /dev/null +++ b/roguelike_tutorial/part_2.py @@ -0,0 +1,149 @@ +""" +McRogueFace Tutorial - Part 2: Animated Movement + +This tutorial builds on Part 1 by adding: +- Animation system for smooth movement +- Movement that takes 0.5 seconds per tile +- Input blocking during movement animation +""" +import mcrfpy +import random + +# Create and activate a new scene +mcrfpy.createScene("tutorial") +mcrfpy.setScene("tutorial") + +# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile) +texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16) + +# Load the hero sprite texture (32x32 sprite sheet) +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile + +grid_width, grid_height = 25, 20 # width, height in number of tiles + +# calculating the size in pixels to fit the entire grid on-screen +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# calculating the position to center the grid on the screen - assuming default 1024x768 resolution +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, # height and width on screen +) + +grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big! + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Fill the grid with a simple pattern +for y in range(grid_height): + for x in range(grid_width): + # Create walls around the edges + if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1: + tile_index = random.choice(WALL_TILES) + else: + # Fill interior with floor tiles + tile_index = random.choice(FLOOR_TILES) + + # Set the tile at this position + point = grid.at(x, y) + if point: + point.tilesprite = tile_index + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Create a player entity at position (4, 4) +player = mcrfpy.Entity( + (4, 4), # Entity positions are tile coordinates + texture=hero_texture, + sprite_index=0 # Use the first sprite in the texture +) + +# Add the player entity to the grid +grid.entities.append(player) +grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates + +# Movement state tracking +is_moving = False +move_animations = [] # Track active animations + +# Animation completion callback +def movement_complete(runtime): + """Called when movement animation completes""" + global is_moving + is_moving = False + # Ensure grid is centered on final position + grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 + +motion_speed = 0.30 # seconds per tile +# Define keyboard handler +def handle_keys(key, state): + """Handle keyboard input to move the player""" + global is_moving, move_animations + + if state == "start" and not is_moving: # Only respond to key press when not moving + # Get current player position in grid coordinates + px, py = player.x, player.y + new_x, new_y = px, py + + # Calculate new position based on key press + if key == "W" or key == "Up": + new_y -= 1 + elif key == "S" or key == "Down": + new_y += 1 + elif key == "A" or key == "Left": + new_x -= 1 + elif key == "D" or key == "Right": + new_x += 1 + + # If position changed, start movement animation + if new_x != px or new_y != py: + is_moving = True + + # Create animations for player position + anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad") + anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad") + anim_x.start(player) + anim_y.start(player) + + # Animate grid center to follow player + center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear") + center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear") + center_x.start(grid) + center_y.start(grid) + + # Set a timer to mark movement as complete + mcrfpy.setTimer("move_complete", movement_complete, 500) + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add a title caption +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 2", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +# Add instructions +instructions = mcrfpy.Caption((150, 750), + "Smooth movement! Each step takes 0.5 seconds.", +) +instructions.font_size=18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +print("Tutorial Part 2 loaded!") +print(f"Player entity created at grid position (4, 4)") +print("Movement is now animated over 0.5 seconds per tile!") +print("Use WASD or Arrow keys to move!") diff --git a/roguelike_tutorial/tutorial2.png b/roguelike_tutorial/tutorial2.png new file mode 100644 index 0000000000000000000000000000000000000000..e7854196517cae002aa2d3381728d0adbc125ff8 GIT binary patch literal 5741 zcmeHKc~BE+77qpuAeVxItUxBLin_TILLxyVK?xW@hzg@3oumU%a+ov_5Fx_i0HUa? zj-YZ4qIiq~>Np$<0t+&zc+3bg90MK$9)wFo5VjLMYqx64s@?xgCF$;W{N8)N?|m=% zQv7^YnHbp^ArJ@?jwjn6zPiC5g24>L1Q(vm-qyB-6)Vy=n_muU_31J&p0^(=&q{~LXnEJ&{8!(W zBrJ=XZ{r!6c4J@kfc|&+qyD~-(WwRAd1&gJ+l*M_aT}XMyZ8UjYF?LHch7P2%0I1I z*mL`U`_;)!(+v{;=xDg)T^0u{8!+nsM_S--Mezd#t*yJSvXW7HAc%RTSMS52P{JDA zLiRi)B!|`(K;%vJ*O?1+I&-2t5fk=^NL( z#C30!2X$rutKq08bl%U`#hJCvO{ zbijRE&RFd>*MF)jaZRV_w?>M(DL#lvvi!tPX!BKDotcOY*Ny4}qpH#KeR_keq?X zM@2>9qR2RzA{0-g)9H8u2~Q$nVGFD>S_%OwtW@csg_y!%gGzxyEQiE0DO!sO@MRGY z6N7>E==b?a2cUhj5a<)C=-Z<4Bah_NCTXO zAb_PfInl8cp)(%~h^Ta|le4oEML?uchyd^j6i2Fr0I2}fLc!oTF^uCZjiQ0p$}2UxgU%N+A5xth7)<7>Y&$KoZr7h$Zu>d@O}1 zqG0JlGJplh0+F*wKqEN=A{~@az*s3$NC0>_#S$PC#LK0jI)#>S#tJ_U6GOrgK1%$; z0Z0TpFfpse(g@W@1y3vi10X=lCXq%Zk?14>i9#Sa5uB%T*5R!K6-u}hwU|T#jy$E& zE(-%L1|}BJ_9+aYgN18huoNHw$rL=9ES!naLZG#tAJklUI|%^@U;_{cgAzzo27$&P z5_u#tgF<5vNLT`eLHNX8CKQXJ|C_aT^PpX(mfTaUgyTo+L{mE|0Ngh9H1!xR)@>y; zTDK_}fMBWwB@hV;b$P;AQz}6iAPoiK>G7^z-^<1Sp%q9X0m0b`q+t1U0o+o~d;rVm z!{vYi0g*s;qS1sz(&y+(nFxvk6ySf^@b3!7YiGv$ie2#k#)*s0;FBQ(`%TH<;RVk^{D)!qjx+7p`9Hqit;PQ_0u22% z$QSYZm9DRJeGvm+Wc;9PqU>Vyc=FUnGQ>&r0Hk5S?cKVpD1(?^t-d<^k*2wsvLkfa}6_LR3w8dPekQ z#$@B<$k@%zrIU3h89#0c#NbnHK$c}Gd6<}dO!_v-Zwb*Qgrqy`!8 z_C`6reOZ0KqPf;s9r^n-grnZIeV3nKB+~+_ADGQSxD_q_#vz+NqET7r=BNIgkqMZ` z=A|u&e-wkZvvk^r?9a)S3>?L#+n3j9pqaG=`iT1~lKH%!U(D*cVc`;|@h?Z2Cq|t% zN$d}H*-3@ac9e@10g_`|l~LaqxJ@sfg>Wm(xr}nKT3NI>m*=;p%{X=QG_T6^<6Dp^ zfvs+C4{VO{U}F9;6G;zw`P8DwTWx$j z_hH_o1#$Z60%d5!jhxe&d)cdr7bQ|z&mD7jj_rwrErBDo1%DqZq+J-9U9{$JOq<>p z3st9LI`mVj+8SQHvAOV)_FPYBm|rrhcOqY3c0R92@oY;{Rn=c;YtBDflhCju>()vQ zR9G?p^idMyv@0lshjulp3y~!+O(;}&bWs-@Cm~Az(EIJ{p;ojCRld(%CHcO(c5-e@T zV(8t-el)c{%_Q)L#s}fHW$Gmi$AIb+1J-cN;Z*%XmtC)u;rGhy_*ZK;_vKy)2=Dys zlfi~Nkrryjo9!l1d3tFD?Rq~huUko1m&IQ)w>^kIY#an2OV&IOiP$}Q{FUBe3ysmT z!FRlBh-?bOV-VXh4XNpi+ak{#6vTHzZS6%gy`lS6Z z2aj52{tag@|9)3^Z8^J9KmBIWvvwmS^sJ+|UDja>s7k@g$$3Gj;#ookLt`k9&-?*( z;hR;4uNS9qYAhd+S3F!@W9hY}G55|mj#wTr>C)G$Wx5IfR9du`4xiT_Z(bIz$es>20uSMp*3X+uPQm!vztKAj zj1M+vppuPuNUtCLeqv57^$z?w;6khT4WG^0zK-A14h+sW)!c=x0}4)_95^`X{g`-v>1?=_ z{G-!c(@h&LUX?Jy3re13bQE4ThAvwzDOo!iUMOAS{7~9;3l*0Z9B+zF+B2~HO4-$2 zVO-!M(kezhXWt(?vnE+v76og*H8ameZKSyMPJa^kwrnMh^^_KWM1Xx4 zdc4X;dRIjDS}-ep;wr6{dx*EY&Wdd}9FI6X*jng2geV);8!U-i(IfThhalAf& zJS#d<_Y`k|Ge2P8WHO|AU2c0b7;2#oC34j;C!+%+f55vYBNXFM5nM|)Ro zdW^cz-(r&82R8nsB;@Ij?;Sv19d(TiHY3>kmi;=$u8URfSbC$86NgxKqu9W;Z#cF? iyrX?!`*Hiu;&Bv>mn*qp^YotfM}_0z!~W4dB;j9PcJK@U literal 0 HcmV?d00001 diff --git a/roguelike_tutorial/tutorial_hero.png b/roguelike_tutorial/tutorial_hero.png new file mode 100644 index 0000000000000000000000000000000000000000..c202176caf74bd28325f0e4352e28e35db03d2ec GIT binary patch literal 16742 zcma*P2RK`Q|37>ZL8A7kEtFbCsaZ5)RP9l;I*d}KYPEJ7#HLoz8l^_HRP9==8C#2@ zHdV2AZ6Zeg@%?`9-*x|<|9wBtlk3WLPI6Apb;kSie!pMilSm_j8#GiLQ~&_bXlrTQ zBrVYZ07gS8NNc9qZZo6>#9d9>6iRxbpbsMefEUo#P&M_jT=%1?Fdcfm;2R&^#b%rX z$qRnyHuwXqdOfXz|4C30yJsL~REm$oX8b*qP=W#715LI>bWTLs&|@aqx4SD3on*Fz zx-^2AoHd%rrQ?m~_!Jn(nMFNhP6Hzg6~<*3l{^pp^8VDCdF~hO>{yod-rX=8s;~Z0 zeNcUH_2Aaf$-c498%bVjC|NUF5^HbW?PJyfC%)Z|&2C0onW@v`r59y>*E^=(tjsf; zZ>T8)ZRiJD3@v2uH+Y*#L7P+@e_@rjl=tm&IY(fTu8TA0a0pHTSVcI&* z37g8#kik*UR2ZJ&qYN3aAg@G$U0Xyk;Wx4#s)KJbw(B7q+SCuY%^0%~UTfxi#Zxh| z+GrK!4!Oy97WBD#*lbRN2D@M9o_XWCE2SK5;O3;cJ|rarz^ z^0r~Bwu+r^r>F?5B_KD{`9SLU*A<4h zz}D|=$1`6nFGly<#-3*RzE1^UfwpRutuVIlMek2OGiC)0n5+ct>2g z)#A0%DeC!5k}0+@rG<>V9+^7-ljpLJy4m*HXRFx;=&tTvF)J7vd?jL{W*Nc0R=|2$ zGQ|r#Bg;s?z8Y0R^EkL^H1pm`9s9s4!Lbox(;7i&h?$ZO@QHOE&HDyBG^W??V}u`p zXAkbL-N^&E9d@2>^!pAB{Z>NcuJjhlgy0UT>et(nX^j*X)~lyzH3uUT1*_ILk3Po* ziO0or_fRWjf+fGRS|Tn790VE`I;trU7vd0P8 zai|xRjX}4Ff05Y#Yyhgw@V~lIKGboI%Y7HmZ&CU-j$I48P6l)6QCR9Uz=kTqJl_rb z31*d-wX7lYAV56tD^!ao`lJV5H6Z_#DXCC4Jd{_(3`|>|^10hnk$m);jZB7rbM~Ww znw}TkIVXx5xIoZ-vRuw$vhR%aivf<=Yn`CybCCnoM!TMCOYIZG&Ms>r4H7&Xv$EvO zk#2qWCI)|xSLH&Kg~{TvN7nZU^b{eW2S~R6?}aq)9w;n;qcnh4F!=)lyGe&o{&;~_ zmfvcHY*!O(S_4O(t!!TE1$=CEe5(d2H*08B%P6C3h)Eo45Ok+{AB zOmc38Oh60kC`LE!UB-x>8xw}8%7Sp5875^Bw}KepTv~nrTuknuNe39DM5@O_A-^Q3 z1J|Jl?%An9O(NFdHJT5lGbtnlB77z&9Ugkw2INT4zYS1ljnsLI&JSh*XC?v^jd~gS zqTI?9uk3{kvGgFVNugEStXuOb%bA*W&dOB};IXhV3|dR(9*1U0SAz6=@%p~%ojBF& zzFta;>cf=C81Sc?xAc(rIYXLV7B}2}FTMiEw%4*v;91dxx`dArEV@t{k1K~g7$X_!8eNY86m)Fz=9+)W)>MoMQ6ZDY}mPMm+E(PMuBv~Q%bX4Q@1K}qW*w0($ z6^HRdPqyu57<`1E^=WbNXu(IXfTZ~Q#X>|crbfI@{6+o!`Kr+^aNo;Gd%Ytq6F)$! zO$2XOqiXf)q5;y%g*x9~A*VGO`y^THJ9pN_i84@EmB~2|s*fsl&v377ZCRPNlUQ*e8Xth-68Q81z zkHv{IMednrqqU{2=V(B_C zWagf+&uq?#&Nl@%g+Qj*tUrNl3k`Ik+_Iv7~ zGF90RyYhOKFI4Yz6j(++-y%l|;7FM!LBP5jfF=E_fc{fQ|MvnH4swsHl=(ZC1fF05 zbyO%K#cI8;guVsdcUR8g9EYxausI)&fj9}^+H^VNs>KU4Foj2;??H4+dKbBhB!EpS z7#n%5R(Tx2eB|Nhw}SoIL)8n1WAwgwJ&2tZKp$d}m%(=rrqgO=o={`Bap! zti59{J7?d8eDsGQv&-v|heBQH9+p1Y&YB6AUEF@rWhWu+z8-&`E@8$Aa zwyO61mkoXRd}88}Ks(Y0o}_Ox4PCwy@<%ugX~z}!T3hypFQ`OE#s50#+l zcTn+x#g+&}5zE|Y|CYJ+mm`nB3VhAo>q6&29B8Cf#p!wxD zE#k-edvXKv=Q(|8{Jf6yZ33kvzdyc&XQ@fW%$~n;>}>#`#nLM# zN29FxqPCsbLc?&ik1~D9a#Uk2hu0veNBuIxg|~*S#^mrCku+8eZna~mhmAjzJO_s4 zCIUpeZm(f3joDuWurF1!Mt0{lB5*l|5B)DS8)pSEVgy#R*RT%n<8mewtWW_3BiD&t zn~?#<#2+vt$!3(euBy~cXN$}m)x0@$+$Kl{ptem5`x#3=y}yjn^}o|tR5f-A-yN2# ztgaGI?g%Do7A1i)?(UiV`jZa$L2YcR6)R6|_V@RXc zh5`uG66yVOk$CAMat2^~s&mc{RZ$|#D42DdSa2V*u)@2v(@#afi~9Xs8()@64xcr! zW{gZwR{FWUb!}edfFp|@ebWyWF%D=&3pzPDvW{MfZ+6(Nx6 z3_$%!7pD%NT28m!*tkx-1M7<-_!xk9-x1T#$w`x6+1*NSu$Z-Wa>LkDw)~Dek(W{f zRtS!q1rZ|$&c_*tE$&V&2(DR<9;%L^-ZZKDi-`Tu_6=Dgsm+EICL%KKWuX%x@4L$P zwtQ5W%PvJDPJx{FH2K5Jq+LudYARM7uJ3Mj{qT#sV`5GjIOLtD0m!Y88M*g$yy&ne z^@1;HGSv?^oIW}O5@Cqr{n*?TRSB9Vfdu}<2&IE{#^W%ACHw1A3r_U!C#$n%;stcL zp)k1LwuT7=Y^C=4(9lZHZVyyv6TY44NtHzaFt!DSJZNrZVY|zN7amOIWJ;oj)T}_$lSSkl_&uP ztBStjk;EaW&EB20l7T0qz%^wKaky(Et?UuT!h>6*Bx?Ry>9zMX4JY;|9MF!J0ldI- z-=E#yR9SH+eJzQCJSQwxM_7ju%0pe4)!h5Z7k+D(2=`DuO-s)!F7W3YX^{G;Rm4@= zR)kl;j=WrBkkH>K^#2Z15IfuMqzh7P`R>2^yUkWKonM`hNrK003$fV$OFRLH;k5pM zKuZkMyF*FsuG!`9I|aixtS1>YRVB`$f8UOiW#sKmA^`p?{G~3?Y{=3@Kh*2gg5=|p zR{*bgB_hL#SLrjkc+T6G&)bNiFzdNHkzmXv4?{$5S(wBc*8R!+pFZtAlT-H+kXYod z#@^iik^Ti4St?4z($r6rqA-Dyw@kTK z=wwjsPn8wY+v*T+yV>GqVF{DG`(Ami8+aR0(#C}qh)%T(5sD}10grN=Egig z5gq95-HhGL6DfKS(7$gcUoUYkc6E2msSQz)js12Dz#C;#q zhXKvnlD;P*jd3^OSr!<-uJS%tx~iSfeTD;xj%UZ-a)WJv zK>Mi?90+t7Q2;rN#Bm>Io56W6{WOyC56Gvp&OD6>wo=~A5-|&|s?dtg8{s3}eZ!P0 zhAJx$OIp4d+Wff1Rthg`!S@3eTlF{LNZmGd0kpmhm;|Yghi+K=Q3a6q zbtWYL>TBbKtlMYdl21m00N7irE2hv7hB9}Cbx&{Wpa(%ndV04{3C2)=1WU;CpMQPX z3X-(4u>M9!uDD69JIb0=LdSBD>(BI8k`qARCQIr6WSDxT&4JM&Qf_I_DO)R-n(A;l zH}80naYzmbtn|IQPrfMsQawuokQD%J_Jajgdk=e4L3b$t{sc|FPvIe=eUb-1dqu?9 ziJdF5U#ZiXuR@izTgaBlUi=I&0DmN%K+_E++wmK3R>+V%22It5lnd5zMXV=_>EIQ* z60VeIPiieY^TjEEuG-c1<%x@Wk06${4~_i`om)m&tcZ{tp6pI^XRWjGA#;A}4-*IJD8Q$t-I-tk5!0!RaNp#qwYs zr$~kDd)_86ko#d)eU-7Y{Mw?*3yMD7j|Ja)xvjxg)YzHw*xOO?@w;?vPywfUy^P6M zkswPXIPc`<`Z3*-yzks3Y0Oyf&U4`PJ0MwO^}0)28*kh(zAc@u*OpG(C1UFc>04Tx zszb)n-0q0n($*It0+KxV?aU9f6p}-fNET|ndM^tEhA9{z6i$7*rOGPha(D|*PF$my zY6|Yr?V?rwv$?FY$^i`U9zOkZ)t>Gd><&9W=;K+ve({Xc1_!1d{X&sFK#!`rN3Db+ z^cnzKP2LUos;yKoa@)-*sw|{VTstllmVV{FQ(h2D-!h#%@y-a~cAh5oCL`}`<$!Mt zpeQ)UFPVG|Z=(fWY$wK zbsW3B7c)*u&zwEtE6|%CR^JFNa#`-aByxS;-iQ_k#9vCg5j?VLmN6)DS10iTCVH)8 z=oeD<;w*d*%4!X!>KvvR-2=OIICTo3X|RUzXujh`0W=J8_Z=`us>s($sBNhGV_bCe zUhCzD={6eQa|^QGts0ImL}WH-lwym&hFJMY-aN?6w>j*TNP zkb{IRsWqV*XN~$!fgA3@U7q|W$LsO#|G-eMJ#V08BTejv+o)=c6vIb1&b>1fiYu2C$&mNOGxevta_vR}9b8_dU zre>mecl=CR?+d+j=-*sjIy>6@v+!##|MGA$li3$;e7>xOR%FgkR8LdgIquo4?gYN! z$aKQmc8^2a*gT?oYxqL2+JgEe*}ZXBnclbO;cNli<=hP~Y837^fu`E&aF>mMdWMko zzXNuhc*h)>HP`jhhE1nN1Y`V|0uY|-A6#1`yVruyjWqn$U{`b#uO4*`KrfRu>VVmv zcGmjK%asfVE~Z)ww2x5bZ|P%A1hWiLf390g&|Mx_2)~c;Z;nh{y+@Ew(co95S_Zqh zi~jj`F4FbgZ-=jnRC^44l0Y9={7zca0-fd7+Y%5xTHf--S8W=|MM(~@_3EDAWjNhh z=5O~r0-n5lsEIPRv&?4t9)mD7dh9FFj=sDF2Y>>w&$(%9ix( zQ$MaV=L+8vXgW0~o1@iva{)C5h89-Nwk#X}#(p1eo*L7iH2RJR=}n1j?k$zf z(%}hQQ$GXNTBJag;NM$HmJl_x8=;QIab~G(fo`$p#-hHwS|36BQV%@CmUI8{;=1bW z-l2392WwXT<}~#Rq|&j%l>}&mBOm(BD*4f&qct&n8g(b@BJBY`=a|@ypeDiQgVlOb znnTT9i`P4PH5(NPOF2iokBbq+QSZU%I|ACSJ~89aGkV-N@FaUh0N?Jd11Q)TKJjt} zl$I(g>MS)UUx4Xp*CJvJVE|`)3}+rD1TV1pwpZBmn~Kb^ar*(&9bYcnPa7aKVp@p? zu)_b?k-e%BiQGFBmh@?*eo7+E*Y%q+@|vg0Y{L&_&>Q@(?iTTwKh50QN@p+cwvE81 z#gS*pzRpeQ~w1qN>@la01>{exDm5IozP z!Jo<0C^DTQV&dFYgkayni|!NRhAJhmB+_HvKpk*!Y2@qn4A4v7POYEIi4W_1^g2hR zwv;ZaF7;cr+g6`?zXj|x_qMBUX`}DFOFlV2_1~!XEK0Ub{{u}rw+j+~DOSAH9uIjd z5h}9&H-f^NK(7O^T1CBvF>#{_V)s|bmL_Ix9Opye|V(bn94*c2NO=$CiwJcFe~tX{zI8~zY|?#5U+%R7(rUWum(Kk7mM;2tr_|#ZsCh4?aqh8-Of1Di&e=6_*o+jL>YRqXq0QEA9(=oNo3b&dp7{!GD)9XgKn8d{FmsKu zAW>@f)N!QR@qT57?gIgJKMQ;KAvKW?j0=K;<ilmf8Kx-|A9ecbOn55edNp zhPfkS7rq;_e8y@H$FfHL=k8`podF|n@q=Q~U2pL^H zY^wpR$+ZDz-n&@>S~0FcQ!W5T?k2orddO{Iv~ITi5m0zHCNu$lr(Up8{0*~|8`|ad z4@PXi&_N61Y_%S;J3{8%J5vL}cYALks-%cT>b7S}t@G>efp}T+aN%_-2o?lO1PMw! z8hbuPhDrP&NLFI;Ss4DAR8i)7%LfM0?udm ztlq*K^*e&JmtTBx5$GX4&T%{h%-z~ve;~^e)eJw_TTcD(hcnX*4p7+a=Byw7S<8(J z5N4`=ao`fV!ew;M(=B)Vj~9N#klRk+S2p>Zhsuof{ep0)PZwdg?%_5}#dUsb1Ic`(Ivwtz~8){3*BFL1^Ab_VRXi zV&0^6!%ipX`Ci&|l;;$_&;(hq!im2Lz0OqCP*V~~KXqAg`iRy)^+ap)3=MgWg^Hdx zP@4oy-n2ilxPUX7)~y%)!fuV&ddmc)78SV4=&vtkxL5Ik;U}0A&h7%q9&(k|rIw&R zy23BL3ZXNJ4$MB?sNEQckKcu%4fQg0uBb&5Ae6;KYIlpoGF{LMTp(PKYmf!UD5rlZ zT}OH{SI}#-F=oC7+%l40hL@X@NP^Xo992P{XE5KUgI<**6Le(75N2ytZbPccupgo# zP86|`NcnG|4W!kbnz^me(QXqW7$=G zxy|>f;@i}?@Bg_2bNd~=?08fhaBHzw;ga{8htm(Cx0s1*7p>a;4W@#%#Kw#|{0_g| zIGttf{^1-ENg9^vTm3t4^k^{lC^5KT8Z$4l>?FO7W2i`Ut9ae2lBu%EWMrPs27#Pdg#K{o`t}?Tj zI4$IHwnKY)JO#lj0Mog~CAJ~*h!nKGAimjNe@H8+opC|O&vC5bIA{qg zbc=14x7QLiy!Bg_Fn4vc`o5GI_qEeb0sO;o36=p5Q4Na_y3=LQN&y{%{Hm#NT6>T z)_-v~+EkD4qnb*HPlI@BL%hI7z;}UhfacEo!1p65Ft;)jxs$NgYO=exSb<7N-^UD~ zzG`Z5)t4R709P$%$~IwCFl2eC(3#gvUnOlSa3KWoyPe$;KPNxtKy_#79j(t~Bx>UF z$l`TkZQGefL2@E6_!d6TMv$B+Xq&L}=TuvZ>3H!aGG_ddJpoei|Bk9il?!WRpSA4W zlkHMC`VT29Y08E+FB3N75v^LM^TOjY_uNbMMX`?$cT@I2FXeEag(O7v{@t8m;6@Zt zM};y`FBKZ7BTq60L3m-dwk;d4@Q6eEDaNE21ruf)-PwKRL`sGgvN^KSXrR9QS$s&H zZHEx4A|}O^d=L1*4k)Yg==b>l>3ncIJic5Dsnh%kVk#vd+hBNFAb@7LQMc-CyW_~` zlK~Ta=U0Rw)kTUdHSqKK%ohwPU5QL>~L(W*Us6`oQ-ivMLi=r-uxqG|Ccb~o0 zrtTGof~Yx7W+|sh`L4p~!f@FQelxKi$o`cTYMZxo`Xm|*j9aTiNJ2=687)?0jtOMU zCb@syby5RirO0B}N65U87TtTgVD4@IsS8!=4B{DB*9KgMBAvi2IxX7tJ zRXPedwo=C$dfd>$uDUv)U>YZ}{zT&yA@6yZ`9UiBI?B4?zT1*vp0F+Fn}w!J2-V72 z__HmR>Z$gHsjDFC*04P~c0eHTaYyDu9!JA#E<9y)9U+by$bxu!QP8(cdJxV{HX>lP zCqk9wlvE`79hbRtR?nI7!_f7I{ZkxF4udV&58i|I21Gek4FGI0{E6OZ*s#Ev58Ovp z@TESQI3q>6=b3p=CGPqd&pN|Tf7&_@eKK3A?>)I~tJ{go324qxo-cg2vEa!9)=_dS zlag)dQB92`{YVI4-x_`ZHzK+p8tQ|8xefHJSk<4dTJukN4I5xDz|5e+`~AzFj;NW& z?jNEf*IqNCvFb@`BJP+fKB(PM!P$xAIp=RFjR_t7!+!h+)Ff<|wafYXC%;|JzZz8x zNrHPwWmg=M)r6)yy+mZw*mg*{um=h%Oc<8y_1o&L5qTvhns}ol0~P_~(`6-Z4^`i| zH9vJ2%d5ogw=yu1>l^jf4)&4mH31OEJ63=FNsXp8`Ce|ax??hXGaoh!wMn#q^V0 zPDEiQ)xYEuC|E%xg-r!Y;xHxezt%2Y2Ghsgc}hWBQrtnohh@1BPv#(PEQ|~6%d;iq zF}Nn)m{1#}EPZ-rcRDhmte4O=&_utb)sLKtB$D(F-dY6N3kpEjl%}xLyz?B=ngL{Y zDV!hlfK{unh{uLubr9d`ed+XeK+zWYWM`EB!kWs_2$ESByE0vYy2>7k3qGcAWuho0 z`SD6py!A_14aQw8pD@-~p;t-K{|_39^th+s@}t5PN>kxf&||U!`MZAjDpKZ= zzc*$@b0h%c$O^P}UlO`+Tx0#N&N1&X2?dF9#$BBK8wI7ENimb3xJjO@1fW|>=3KUp zcGd-k{4F!+=^Ypvm-F`D0MH<12rpJ)ZuI7CIBsg08Ey7?_vQUZzW6g^*7CdmY|JWH zU6@O=U3+SNE2F5LDk&kd{NNYAZ3j2Yi>=$Z!bQtGz6 zyLR&abj7gx%#k8K=@DIX;yehGOzS4+l{h7tN}~nbhI8eA_Z)4c0X)wMpuX6E;>!vR z&Y>9b6HY>@gygBj$qMsc_q)d**xK%bK2Z9OL%~M;xc%z9lpuP1PCP!xFvDSYCnHOz zR{LxKik^C>gGz-xY%4dnUcMh`rKY-`^q)+INjEP?=f?JzjEkhsAYI*Q1@p2JkUmnf%DQ7Yp99fBjy3*-W z<&J&!*A{X`M0<&}i;#*&DJS{)i;Eyvb;jwgKUFq6V6qs#5S+-JSr=TIGLO zuK&28)};tK^^g-|XUO@h0E}9(ku)BS_wQd_-?$w3crnlLbPR_4ZmvsPd`sL!^wCpj zCZvMDW~kv6>EUen+`#Cou`BG`i%vUug3!0Mu)?F8mCb(|wQY>q%1=7(958KwO*#oi!hGPb* z&(U$i@Ef-+Q@MzfIVx<9z@YkOFYIU53Y&FW5+>8D{o&p%Vvl1BA$;(s(ux+?V9Eg2 z7mL2vuoLqTe{ocm0$HXUM|V1*)AG_d5X`3gaf;;V+PTq`*G$fe^8D~dd0s3>pt*jx z?ZEqpBQJR-C|=>DPsCxdDbpZ`u90eVvgR(~lyr?Z9wTTHL%;CgzgbM(>fn+$tKY(l zuT1q2a%&cIynv1dvAsxWF*h~XtlMmK)lcd{ink9;Lqe?2R#~hTs^H6HE4LcqPK@r`CV}S zv%u5rSaX_{^Zp4S%tWP!umpMa zImjf!zHpH_bAe;_^X-n_K(TTd`p=ckl#f`p3q~C$NG zd_4e)%H6p2Q5;vh{=7!&$ND>F-YiZ=1-YbY_um_{=AS{os%NHH!mlaD2fmN0R&^7n z-=zA592-)3$+-0jFI5`(B#lgo*36ki7d`XcCcgMJYEno?zcicq*8Y}X5bNNoqv-%? z3`vzzerwqiVT}=3HFL7=0jfZzl)!{F@KW9B)Qq18)Icu7&AKH*(7^s4;W$hOXOsW3vGBN%=A8bFv7_e_KhVmY zG*8BI$nQ#-K0ZEPrHUzW^7WxU4qMbGKHbvg@1+hC1Eu4|mg8!_ar)l~F2^l`M!iQn z?_*TG7k<_R@+G<6*o03U);-6!Bo6YsHt3!VWQ+`D!xbAUUNHhzKmSF0yE5dDrx=2p%;Xheo$}7-|&ek%Q)r2c|?#Q%;!{^<}rszf`Q`G zs_8(IT6MHP2~f*0p)&CjFl)_BZs6lndbh%HeUcOw!9Q-MTg)jr)*>B^f;4W8n&(Lh zB91Rpz+Kun0QOM*ri0LpS3n!kUBBzzE_idEkzxPJpVWQ?4}oWS!i3nI9BVRlJYhd1 z)VSTI`gs?N_nom&&Xbxc)3WP^2r#PSy!8OT{-J|EErf00{#Nyf40+~X*pP{BkY;nt zt>3%(LHMG@`6rC6*pX9Cl-}N#KatA7v#GORqHQGkzG>3%c9t2_0W{|nM;v8Csm^C3 zV0Bdkuii=}Y%=4qnop@kHC4;uRkOYyLDuO%?p`@&YUi=EIvIf%&Kf5_&R;E9SJdD* zlJoEV3q)pC4IN$XULy9?A|4P70!vPAjR%B)7N?U=ZQ-$j^}d73*ZTb^n!E`YmTuQh z8JQRyF;$+r{*XC${meFcN3-F7e50w^8NXcqP(;b+VTFhfdLqY!vaV1*hQHrIBm(IP zqT}-Z9UCB=aC#uf=!4CeSH+nJw6*Ck52z)fh_n8Cj^xfS)Tx&2!SDK&S@+W`iU@M+ z?*W!(0lmf@`SJlfh3wQ`_nuDCdY-sbrs(g)`E_)*`DwSR%_v<&a|6*7wF;nkbe2g{ zY~7Om@AMnQlpxQRit)Uy7{#%qa9eu^l27}NJW;X4&~273Q*fkbmL!p<5}<=9@=QXG z3Gj67+|=>~x1Ap&goWP)D?ab@i}aFX+0X3ZatyPxq3zchcc^ipqnarJ;zc*R3F925KdSX6h*WfUwM#56vBVyS*>GQcU{4Xj3M) zE2N$19C+4lsRwvOc%v2}r7_j@~{wvF7aAGMC29<_VdK&j!w*>PyA zi^P`f@Og0wB|ZV%$LOGx7z(*bz?G9`~zA4SBBbbM;|D$*4+{0hLQcn;Zi@K;7!oW zj=rdKbCzzpPa*Br@UeXOgPOwS8t?fv>==o2 zrMyP~%w_LGu7fc$ik#$Teoy_a98qlJqM$tq(rgYcZ92-_k95{Ty!SV=drQ>~qUhB<8$-C+7$3=}c>(=H!{(FhL47@otT8|EcfWJ56u*syK{KUx>|mXd zpkFx?B3Rmf)wWJ|z^f&zC(llO`dyj2@0M=2_XU7>Wg4X~Zoe6D}} z4Z$`2oy5-}Wo3DLl-Aem-&>PkU{b>1p?wctuRUBSetBniAh;0ggb#hz& zXlU^<$^h&r{^0=P(LPeA)LWNs2-0D-mmE-u@lnE(=%gAOA|Habh9vcQXMlLW{oGzu4AL9Ien+c`gjj45kHb%>Zn{84vIX~tB#%_z zo+IXKy_%G#0DQ_W&fpP25@CM#SnMANZb<=drz$!A4)iSX=MWv<`@*V%@6MzBXd(S6 zV0oueT;&z5JAlay8K+$3HvEI_$Jn3~v(mU__!q9@D}TOMl|2H|gZoI@;Zy2GF4IS* z1tfN+(t=HL@7AenA8W&0V@cM;eVigxyN|Y!#k;7g96_FyXqf4%ky%XHSM9QsfnW=9 z{z!+E1>Xgcz)0h4u=}7O){5%Xug#e7kALw$PDhLALHAC0*-l9K{G+T)N@7p1j#69_ z4YKWePwExakBxjUuP)+jHu++5lSD#uL?4>Wv}_E*mdHrp8W=661Fi__^|i`fkP~M#^dX&;Fk_(~L@GVIs zCo!{!E5IILNs<`{VOaMHr=dP;)m}w|7}svCGZpOL_jl-Dt6gCz+7N&Sk#zIBQb*B`;g3Jj0r?8#je7D+m@$L! z_w9!NPoa_Se3D*XnB8Q4s51Hc)&QS*tVr*Ey#i2LSMS2*VnKnt20@HyiLsW@{xegv zJ5_pTr(s<+3j%299A|I=Bymnk-`Uq3cMf}@!k3%9(J*d}@b8-9;OHNDiYEI@!IFgj zeU!HsX$q7nbfW%+e9`V?eXaO&hwy-tLF2=jpW?rNj7MK_-@U!BDZqQ&4wua=x1v51 z;$$B89cKfrKlM9gwp6FcyH)nN&zfmf$h&_N6gO27Ma(bv2)PCv_A7tQs&H&CeA-St zn>{rHnlYc`9djJFoWE{{N{ISuLz;* z1-n`QrZT=T;#fUwkX8ubyWL9-;aSM*zOk>v~DH}Z7|Kk$>4}-<};}uN6*fmoc=GuSQQm_=3Ad7L@U^2=Rdm-yi!5B~zqYjaVNhE3s( zp&U=DVd|%y}=|Gf!Qk-b&$^iaxg4v=t;76b!&J&z5i}DFGgF?k17ci3Hj!?vT+YoDT z%%@ix&GK_5{1z7~K<3xEwIiI3j8@OjaPITs3!eZ~=$j=g45L{=n()5_M$;!^Ed<8~ zu1jtzI!nF@Xtxk;exiNvSVE=yC^UlyXN+4Ijtw^oxTBek(wb+u z`q7@zq{7Jg;=>?BTJCTsUXv=e@l?M?`Hjo%%a+zWwiiivd=Vn787a_TrP=5cy)k06 z*if}|z4A79g6?PjnS<>8I6rRstDs;~xFIi7JA*efa$>t|qaHt=d-Ij*(i7rw&Bjrz zuSvk&H@Ug#`$8)piL$(1v5~Hv1CN1M%LG)y?72)?Gj+~Ub2W>6L-C1ZD|&8xE7dIL z+*nkv;_o!gMlH58RdMK}8`E@}#)v2AjluPgQJYi4YCMR(VVCj#-T2q7AFgM9svAB@ znBFD*Pr&Ng{{m#n)J>E4M7F5;E(YMp5nskY7-1MVUfJpDOf7v_G*-8{?R)qq1-}U& zx%xcvd2r)ddmgtDK #include #include @@ -9,75 +12,105 @@ #define M_PI 3.14159265358979323846 #endif +// Forward declaration of PyAnimation type +namespace mcrfpydef { + extern PyTypeObject PyAnimationType; +} + // Animation implementation Animation::Animation(const std::string& targetProperty, const AnimationValue& targetValue, float duration, EasingFunction easingFunc, - bool delta) + bool delta, + PyObject* callback) : targetProperty(targetProperty) , targetValue(targetValue) , duration(duration) , easingFunc(easingFunc) , delta(delta) + , pythonCallback(callback) { + // Increase reference count for Python callback + if (pythonCallback) { + Py_INCREF(pythonCallback); + } } -void Animation::start(UIDrawable* target) { - currentTarget = target; +Animation::~Animation() { + // Decrease reference count for Python callback if we still own it + PyObject* callback = pythonCallback; + if (callback) { + pythonCallback = nullptr; + + PyGILState_STATE gstate = PyGILState_Ensure(); + Py_DECREF(callback); + PyGILState_Release(gstate); + } + + // Clean up cache entry + if (serial_number != 0) { + PythonObjectCache::getInstance().remove(serial_number); + } +} + +void Animation::start(std::shared_ptr target) { + if (!target) return; + + targetWeak = target; elapsed = 0.0f; + callbackTriggered = false; // Reset callback state - // Capture startValue from target based on targetProperty - if (!currentTarget) return; - - // Try to get the current value based on the expected type - std::visit([this](const auto& targetVal) { + // Capture start value from target + std::visit([this, &target](const auto& targetVal) { using T = std::decay_t; if constexpr (std::is_same_v) { float value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { int value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v>) { // For sprite animation, get current sprite index int value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { sf::Color value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { sf::Vector2f value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { std::string value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } }, targetValue); } -void Animation::startEntity(UIEntity* target) { - currentEntityTarget = target; - currentTarget = nullptr; // Clear drawable target +void Animation::startEntity(std::shared_ptr target) { + if (!target) return; + + entityTargetWeak = target; elapsed = 0.0f; + callbackTriggered = false; // Reset callback state // Capture the starting value from the entity std::visit([this, target](const auto& val) { @@ -99,8 +132,49 @@ void Animation::startEntity(UIEntity* target) { }, targetValue); } +bool Animation::hasValidTarget() const { + return !targetWeak.expired() || !entityTargetWeak.expired(); +} + +void Animation::clearCallback() { + // Safely clear the callback when PyAnimation is being destroyed + PyObject* callback = pythonCallback; + if (callback) { + pythonCallback = nullptr; + callbackTriggered = true; // Prevent future triggering + + PyGILState_STATE gstate = PyGILState_Ensure(); + Py_DECREF(callback); + PyGILState_Release(gstate); + } +} + +void Animation::complete() { + // Jump to end of animation + elapsed = duration; + + // Apply final value + if (auto target = targetWeak.lock()) { + AnimationValue finalValue = interpolate(1.0f); + applyValue(target.get(), finalValue); + } + else if (auto entity = entityTargetWeak.lock()) { + AnimationValue finalValue = interpolate(1.0f); + applyValue(entity.get(), finalValue); + } +} + bool Animation::update(float deltaTime) { - if ((!currentTarget && !currentEntityTarget) || isComplete()) { + // Try to lock weak_ptr to get shared_ptr + std::shared_ptr target = targetWeak.lock(); + std::shared_ptr entity = entityTargetWeak.lock(); + + // If both are null, target was destroyed + if (!target && !entity) { + return false; // Remove this animation + } + + if (isComplete()) { return false; } @@ -114,39 +188,18 @@ bool Animation::update(float deltaTime) { // Get interpolated value AnimationValue currentValue = interpolate(easedT); - // Apply currentValue to target (either drawable or entity) - std::visit([this](const auto& value) { - using T = std::decay_t; - - if (currentTarget) { - // Handle UIDrawable targets - if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - } - else if (currentEntityTarget) { - // Handle UIEntity targets - if constexpr (std::is_same_v) { - currentEntityTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentEntityTarget->setProperty(targetProperty, value); - } - // Entities don't support other types yet - } - }, currentValue); + // Apply to whichever target is valid + if (target) { + applyValue(target.get(), currentValue); + } else if (entity) { + applyValue(entity.get(), currentValue); + } + + // Trigger callback when animation completes + // Check pythonCallback again in case it was cleared during update + if (isComplete() && !callbackTriggered && pythonCallback) { + triggerCallback(); + } return !isComplete(); } @@ -254,6 +307,77 @@ AnimationValue Animation::interpolate(float t) const { }, targetValue); } +void Animation::applyValue(UIDrawable* target, const AnimationValue& value) { + if (!target) return; + + std::visit([this, target](const auto& val) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + target->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + target->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + target->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + target->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + target->setProperty(targetProperty, val); + } + }, value); +} + +void Animation::applyValue(UIEntity* entity, const AnimationValue& value) { + if (!entity) return; + + std::visit([this, entity](const auto& val) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + entity->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + entity->setProperty(targetProperty, val); + } + // Entities don't support other types yet + }, value); +} + +void Animation::triggerCallback() { + if (!pythonCallback) return; + + // Ensure we only trigger once + if (callbackTriggered) return; + callbackTriggered = true; + + PyGILState_STATE gstate = PyGILState_Ensure(); + + // TODO: In future, create PyAnimation wrapper for this animation + // For now, pass None for both parameters + PyObject* args = PyTuple_New(2); + Py_INCREF(Py_None); + Py_INCREF(Py_None); + PyTuple_SetItem(args, 0, Py_None); // animation parameter + PyTuple_SetItem(args, 1, Py_None); // target parameter + + PyObject* result = PyObject_CallObject(pythonCallback, args); + Py_DECREF(args); + + if (!result) { + // Print error but don't crash + PyErr_Print(); + PyErr_Clear(); // Clear the error state + } else { + Py_DECREF(result); + } + + PyGILState_Release(gstate); +} + // Easing functions implementation namespace EasingFunctions { @@ -502,26 +626,50 @@ AnimationManager& AnimationManager::getInstance() { } void AnimationManager::addAnimation(std::shared_ptr animation) { - activeAnimations.push_back(animation); + if (animation && animation->hasValidTarget()) { + if (isUpdating) { + // Defer adding during update to avoid iterator invalidation + pendingAnimations.push_back(animation); + } else { + activeAnimations.push_back(animation); + } + } } void AnimationManager::update(float deltaTime) { - for (auto& anim : activeAnimations) { - anim->update(deltaTime); - } - cleanup(); -} - -void AnimationManager::cleanup() { + // Set flag to defer new animations + isUpdating = true; + + // Remove completed or invalid animations activeAnimations.erase( std::remove_if(activeAnimations.begin(), activeAnimations.end(), - [](const std::shared_ptr& anim) { - return anim->isComplete(); + [deltaTime](std::shared_ptr& anim) { + return !anim || !anim->update(deltaTime); }), activeAnimations.end() ); + + // Clear update flag + isUpdating = false; + + // Add any animations that were created during update + if (!pendingAnimations.empty()) { + activeAnimations.insert(activeAnimations.end(), + pendingAnimations.begin(), + pendingAnimations.end()); + pendingAnimations.clear(); + } } -void AnimationManager::clear() { + +void AnimationManager::clear(bool completeAnimations) { + if (completeAnimations) { + // Complete all animations before clearing + for (auto& anim : activeAnimations) { + if (anim) { + anim->complete(); + } + } + } activeAnimations.clear(); } \ No newline at end of file diff --git a/src/Animation.h b/src/Animation.h index 6308f32..d64f0ab 100644 --- a/src/Animation.h +++ b/src/Animation.h @@ -6,6 +6,7 @@ #include #include #include +#include "Python.h" // Forward declarations class UIDrawable; @@ -36,13 +37,20 @@ public: const AnimationValue& targetValue, float duration, EasingFunction easingFunc = EasingFunctions::linear, - bool delta = false); + bool delta = false, + PyObject* callback = nullptr); + + // Destructor - cleanup Python callback reference + ~Animation(); // Apply this animation to a drawable - void start(UIDrawable* target); + void start(std::shared_ptr target); // Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable) - void startEntity(UIEntity* target); + void startEntity(std::shared_ptr target); + + // Complete the animation immediately (jump to final value) + void complete(); // Update animation (called each frame) // Returns true if animation is still running, false if complete @@ -51,6 +59,12 @@ public: // Get current interpolated value AnimationValue getCurrentValue() const; + // Check if animation has valid target + bool hasValidTarget() const; + + // Clear the callback (called when PyAnimation is deallocated) + void clearCallback(); + // Animation properties std::string getTargetProperty() const { return targetProperty; } float getDuration() const { return duration; } @@ -67,11 +81,27 @@ private: EasingFunction easingFunc; // Easing function to use bool delta; // If true, targetValue is relative to start - UIDrawable* currentTarget = nullptr; // Current target being animated - UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable) + // RAII: Use weak_ptr for safe target tracking + std::weak_ptr targetWeak; + std::weak_ptr entityTargetWeak; + + // Callback support + PyObject* pythonCallback = nullptr; // Python callback function (we own a reference) + bool callbackTriggered = false; // Ensure callback only fires once + PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python + + // Python object cache support + uint64_t serial_number = 0; // Helper to interpolate between values AnimationValue interpolate(float t) const; + + // Helper to apply value to target + void applyValue(UIDrawable* target, const AnimationValue& value); + void applyValue(UIEntity* entity, const AnimationValue& value); + + // Trigger callback when animation completes + void triggerCallback(); }; // Easing functions library @@ -134,13 +164,12 @@ public: // Update all animations void update(float deltaTime); - // Remove completed animations - void cleanup(); - - // Clear all animations - void clear(); + // Clear all animations (optionally completing them first) + void clear(bool completeAnimations = false); private: AnimationManager() = default; std::vector> activeAnimations; + std::vector> pendingAnimations; // Animations to add after update + bool isUpdating = false; // Flag to track if we're in update loop }; \ No newline at end of file diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 5b35d79..43b9c03 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -5,6 +5,7 @@ #include "UITestScene.h" #include "Resources.h" #include "Animation.h" +#include "Timer.h" #include GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{}) @@ -16,7 +17,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) { Resources::font.loadFromFile("./assets/JetbrainsMono.ttf"); Resources::game = this; - window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine"; + window_title = "McRogueFace Engine"; // Initialize rendering based on headless mode if (headless) { @@ -91,6 +92,9 @@ void GameEngine::cleanup() if (cleaned_up) return; cleaned_up = true; + // Clear all animations first (RAII handles invalidation) + AnimationManager::getInstance().clear(); + // Clear Python references before destroying C++ objects // Clear all timers (they hold Python callables) timers.clear(); @@ -182,7 +186,7 @@ void GameEngine::setWindowScale(float multiplier) void GameEngine::run() { - std::cout << "GameEngine::run() starting main loop..." << std::endl; + //std::cout << "GameEngine::run() starting main loop..." << std::endl; float fps = 0.0; frameTime = 0.016f; // Initialize to ~60 FPS clock.restart(); @@ -259,7 +263,7 @@ void GameEngine::run() int tenth_fps = (metrics.fps * 10) % 10; if (!headless && window) { - window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS"); + window->setTitle(window_title); } // In windowed mode, check if window was closed @@ -272,7 +276,7 @@ void GameEngine::run() cleanup(); } -std::shared_ptr GameEngine::getTimer(const std::string& name) +std::shared_ptr GameEngine::getTimer(const std::string& name) { auto it = timers.find(name); if (it != timers.end()) { @@ -290,7 +294,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval) { // Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check // see gitea issue #4: this allows for a timer to be deleted during its own call to itself - timers[name] = std::make_shared(Py_None, 1000, runtime.getElapsedTime().asMilliseconds()); + timers[name] = std::make_shared(Py_None, 1000, runtime.getElapsedTime().asMilliseconds()); return; } } @@ -299,7 +303,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval) std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl; return; } - timers[name] = std::make_shared(target, interval, runtime.getElapsedTime().asMilliseconds()); + timers[name] = std::make_shared(target, interval, runtime.getElapsedTime().asMilliseconds()); } void GameEngine::testTimers() @@ -310,7 +314,8 @@ void GameEngine::testTimers() { it->second->test(now); - if (it->second->isNone()) + // Remove timers that have been cancelled or are one-shot and fired + if (!it->second->getCallback() || it->second->getCallback() == Py_None) { it = timers.erase(it); } diff --git a/src/GameEngine.h b/src/GameEngine.h index e6371b5..30ed619 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -58,8 +58,7 @@ private: public: sf::Clock runtime; - //std::map timers; - std::map> timers; + std::map> timers; std::string scene; // Profiling metrics @@ -116,7 +115,7 @@ public: float getFrameTime() { return frameTime; } sf::View getView() { return visible; } void manageTimer(std::string, PyObject*, int); - std::shared_ptr getTimer(const std::string& name); + std::shared_ptr getTimer(const std::string& name); void setWindowScale(float); bool isHeadless() const { return headless; } void processEvent(const sf::Event& event); diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 2aa7905..7ba99ab 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -267,6 +267,14 @@ PyObject* PyInit_mcrfpy() PySceneType.tp_methods = PySceneClass::methods; PySceneType.tp_getset = PySceneClass::getsetters; + // Set up weakref support for all types that need it + PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist); + PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist); + PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist); + PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist); + PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist); + PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist); + int i = 0; auto t = pytypes[i]; while (t != nullptr) diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index 720b8d9..d45c6eb 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -18,19 +18,31 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds } int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr}; + static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr}; const char* property_name; PyObject* target_value; float duration; const char* easing_name = "linear"; int delta = 0; + PyObject* callback = nullptr; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast(keywords), - &property_name, &target_value, &duration, &easing_name, &delta)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast(keywords), + &property_name, &target_value, &duration, &easing_name, &delta, &callback)) { return -1; } + // Validate callback is callable if provided + if (callback && callback != Py_None && !PyCallable_Check(callback)) { + PyErr_SetString(PyExc_TypeError, "callback must be callable"); + return -1; + } + + // Convert None to nullptr for C++ + if (callback == Py_None) { + callback = nullptr; + } + // Convert Python target value to AnimationValue AnimationValue animValue; @@ -90,7 +102,7 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) { EasingFunction easingFunc = EasingFunctions::getByName(easing_name); // Create the Animation - self->data = std::make_shared(property_name, animValue, duration, easingFunc, delta != 0); + self->data = std::make_shared(property_name, animValue, duration, easingFunc, delta != 0, callback); return 0; } @@ -126,50 +138,50 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) { return NULL; } - // Get the UIDrawable from the Python object - UIDrawable* drawable = nullptr; - // Check type by comparing type names const char* type_name = Py_TYPE(target_obj)->tp_name; if (strcmp(type_name, "mcrfpy.Frame") == 0) { PyUIFrameObject* frame = (PyUIFrameObject*)target_obj; - drawable = frame->data.get(); + if (frame->data) { + self->data->start(frame->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else if (strcmp(type_name, "mcrfpy.Caption") == 0) { PyUICaptionObject* caption = (PyUICaptionObject*)target_obj; - drawable = caption->data.get(); + if (caption->data) { + self->data->start(caption->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else if (strcmp(type_name, "mcrfpy.Sprite") == 0) { PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj; - drawable = sprite->data.get(); + if (sprite->data) { + self->data->start(sprite->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else if (strcmp(type_name, "mcrfpy.Grid") == 0) { PyUIGridObject* grid = (PyUIGridObject*)target_obj; - drawable = grid->data.get(); + if (grid->data) { + self->data->start(grid->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else if (strcmp(type_name, "mcrfpy.Entity") == 0) { // Special handling for Entity since it doesn't inherit from UIDrawable PyUIEntityObject* entity = (PyUIEntityObject*)target_obj; - // Start the animation directly on the entity - self->data->startEntity(entity->data.get()); - - // Add to AnimationManager - AnimationManager::getInstance().addAnimation(self->data); - - Py_RETURN_NONE; + if (entity->data) { + self->data->startEntity(entity->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else { PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity"); return NULL; } - // Start the animation - self->data->start(drawable); - - // Add to AnimationManager - AnimationManager::getInstance().addAnimation(self->data); - Py_RETURN_NONE; } @@ -214,6 +226,20 @@ PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args }, value); } +PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) { + if (self->data) { + self->data->complete(); + } + Py_RETURN_NONE; +} + +PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) { + if (self->data && self->data->hasValidTarget()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + PyGetSetDef PyAnimation::getsetters[] = { {"property", (getter)get_property, NULL, "Target property name", NULL}, {"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL}, @@ -225,10 +251,23 @@ PyGetSetDef PyAnimation::getsetters[] = { PyMethodDef PyAnimation::methods[] = { {"start", (PyCFunction)start, METH_VARARGS, - "Start the animation on a target UIDrawable"}, + "start(target) -> None\n\n" + "Start the animation on a target UI element.\n\n" + "Args:\n" + " target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)\n\n" + "Note:\n" + " The animation will automatically stop if the target is destroyed."}, {"update", (PyCFunction)update, METH_VARARGS, "Update the animation by deltaTime (returns True if still running)"}, {"get_current_value", (PyCFunction)get_current_value, METH_NOARGS, "Get the current interpolated value"}, + {"complete", (PyCFunction)complete, METH_NOARGS, + "complete() -> None\n\n" + "Complete the animation immediately by jumping to the final value."}, + {"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS, + "hasValidTarget() -> bool\n\n" + "Check if the animation still has a valid target.\n\n" + "Returns:\n" + " True if the target still exists, False if it was destroyed."}, {NULL} }; \ No newline at end of file diff --git a/src/PyAnimation.h b/src/PyAnimation.h index 9976cb2..ccb4f36 100644 --- a/src/PyAnimation.h +++ b/src/PyAnimation.h @@ -28,6 +28,8 @@ public: static PyObject* start(PyAnimationObject* self, PyObject* args); static PyObject* update(PyAnimationObject* self, PyObject* args); static PyObject* get_current_value(PyAnimationObject* self, PyObject* args); + static PyObject* complete(PyAnimationObject* self, PyObject* args); + static PyObject* has_valid_target(PyAnimationObject* self, PyObject* args); static PyGetSetDef getsetters[]; static PyMethodDef methods[]; diff --git a/src/PyArgHelpers.h b/src/PyArgHelpers.h deleted file mode 100644 index d827789..0000000 --- a/src/PyArgHelpers.h +++ /dev/null @@ -1,410 +0,0 @@ -#pragma once -#include "Python.h" -#include "PyVector.h" -#include "PyColor.h" -#include -#include - -// Unified argument parsing helpers for Python API consistency -namespace PyArgHelpers { - - // Position in pixels (float) - struct PositionResult { - float x, y; - bool valid; - const char* error; - }; - - // Size in pixels (float) - struct SizeResult { - float w, h; - bool valid; - const char* error; - }; - - // Grid position in tiles (float - for animation) - struct GridPositionResult { - float grid_x, grid_y; - bool valid; - const char* error; - }; - - // Grid size in tiles (int - can't have fractional tiles) - struct GridSizeResult { - int grid_w, grid_h; - bool valid; - const char* error; - }; - - // Color parsing - struct ColorResult { - sf::Color color; - bool valid; - const char* error; - }; - - // Helper to check if a keyword conflicts with positional args - static bool hasConflict(PyObject* kwds, const char* key, bool has_positional) { - if (!kwds || !has_positional) return false; - PyObject* value = PyDict_GetItemString(kwds, key); - return value != nullptr; - } - - // Parse position with conflict detection - static PositionResult parsePosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) { - PositionResult result = {0.0f, 0.0f, false, nullptr}; - int start_idx = next_arg ? *next_arg : 0; - bool has_positional = false; - - // Check for positional tuple argument first - if (args && PyTuple_Size(args) > start_idx) { - PyObject* first = PyTuple_GetItem(args, start_idx); - - // Is it a tuple/Vector? - if (PyTuple_Check(first) && PyTuple_Size(first) == 2) { - // Extract from tuple - PyObject* x_obj = PyTuple_GetItem(first, 0); - PyObject* y_obj = PyTuple_GetItem(first, 1); - - if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) && - (PyFloat_Check(y_obj) || PyLong_Check(y_obj))) { - result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj); - result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj); - result.valid = true; - has_positional = true; - if (next_arg) (*next_arg)++; - } - } else if (PyObject_TypeCheck(first, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) { - // It's a Vector object - PyVectorObject* vec = (PyVectorObject*)first; - result.x = vec->data.x; - result.y = vec->data.y; - result.valid = true; - has_positional = true; - if (next_arg) (*next_arg)++; - } - } - - // Check for keyword conflicts - if (has_positional) { - if (hasConflict(kwds, "pos", true) || hasConflict(kwds, "x", true) || hasConflict(kwds, "y", true)) { - result.valid = false; - result.error = "position specified both positionally and by keyword"; - return result; - } - } - - // If no positional, try keywords - if (!has_positional && kwds) { - PyObject* pos_obj = PyDict_GetItemString(kwds, "pos"); - PyObject* x_obj = PyDict_GetItemString(kwds, "x"); - PyObject* y_obj = PyDict_GetItemString(kwds, "y"); - - // Check for conflicts between pos and x/y - if (pos_obj && (x_obj || y_obj)) { - result.valid = false; - result.error = "pos and x/y cannot both be specified"; - return result; - } - - if (pos_obj) { - // Parse pos keyword - if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(pos_obj, 1); - - if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && - (PyFloat_Check(y_val) || PyLong_Check(y_val))) { - result.x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); - result.y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); - result.valid = true; - } - } else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) { - PyVectorObject* vec = (PyVectorObject*)pos_obj; - result.x = vec->data.x; - result.y = vec->data.y; - result.valid = true; - } - } else if (x_obj && y_obj) { - // Parse x, y keywords - if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) && - (PyFloat_Check(y_obj) || PyLong_Check(y_obj))) { - result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj); - result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj); - result.valid = true; - } - } - } - - return result; - } - - // Parse size with conflict detection - static SizeResult parseSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) { - SizeResult result = {0.0f, 0.0f, false, nullptr}; - int start_idx = next_arg ? *next_arg : 0; - bool has_positional = false; - - // Check for positional tuple argument - if (args && PyTuple_Size(args) > start_idx) { - PyObject* first = PyTuple_GetItem(args, start_idx); - - if (PyTuple_Check(first) && PyTuple_Size(first) == 2) { - PyObject* w_obj = PyTuple_GetItem(first, 0); - PyObject* h_obj = PyTuple_GetItem(first, 1); - - if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) && - (PyFloat_Check(h_obj) || PyLong_Check(h_obj))) { - result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj); - result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj); - result.valid = true; - has_positional = true; - if (next_arg) (*next_arg)++; - } - } - } - - // Check for keyword conflicts - if (has_positional) { - if (hasConflict(kwds, "size", true) || hasConflict(kwds, "w", true) || hasConflict(kwds, "h", true)) { - result.valid = false; - result.error = "size specified both positionally and by keyword"; - return result; - } - } - - // If no positional, try keywords - if (!has_positional && kwds) { - PyObject* size_obj = PyDict_GetItemString(kwds, "size"); - PyObject* w_obj = PyDict_GetItemString(kwds, "w"); - PyObject* h_obj = PyDict_GetItemString(kwds, "h"); - - // Check for conflicts between size and w/h - if (size_obj && (w_obj || h_obj)) { - result.valid = false; - result.error = "size and w/h cannot both be specified"; - return result; - } - - if (size_obj) { - // Parse size keyword - if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { - PyObject* w_val = PyTuple_GetItem(size_obj, 0); - PyObject* h_val = PyTuple_GetItem(size_obj, 1); - - if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) && - (PyFloat_Check(h_val) || PyLong_Check(h_val))) { - result.w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); - result.h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); - result.valid = true; - } - } - } else if (w_obj && h_obj) { - // Parse w, h keywords - if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) && - (PyFloat_Check(h_obj) || PyLong_Check(h_obj))) { - result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj); - result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj); - result.valid = true; - } - } - } - - return result; - } - - // Parse grid position (float for smooth animation) - static GridPositionResult parseGridPosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) { - GridPositionResult result = {0.0f, 0.0f, false, nullptr}; - int start_idx = next_arg ? *next_arg : 0; - bool has_positional = false; - - // Check for positional tuple argument - if (args && PyTuple_Size(args) > start_idx) { - PyObject* first = PyTuple_GetItem(args, start_idx); - - if (PyTuple_Check(first) && PyTuple_Size(first) == 2) { - PyObject* x_obj = PyTuple_GetItem(first, 0); - PyObject* y_obj = PyTuple_GetItem(first, 1); - - if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) && - (PyFloat_Check(y_obj) || PyLong_Check(y_obj))) { - result.grid_x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj); - result.grid_y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj); - result.valid = true; - has_positional = true; - if (next_arg) (*next_arg)++; - } - } - } - - // Check for keyword conflicts - if (has_positional) { - if (hasConflict(kwds, "grid_pos", true) || hasConflict(kwds, "grid_x", true) || hasConflict(kwds, "grid_y", true)) { - result.valid = false; - result.error = "grid position specified both positionally and by keyword"; - return result; - } - } - - // If no positional, try keywords - if (!has_positional && kwds) { - PyObject* grid_pos_obj = PyDict_GetItemString(kwds, "grid_pos"); - PyObject* grid_x_obj = PyDict_GetItemString(kwds, "grid_x"); - PyObject* grid_y_obj = PyDict_GetItemString(kwds, "grid_y"); - - // Check for conflicts between grid_pos and grid_x/grid_y - if (grid_pos_obj && (grid_x_obj || grid_y_obj)) { - result.valid = false; - result.error = "grid_pos and grid_x/grid_y cannot both be specified"; - return result; - } - - if (grid_pos_obj) { - // Parse grid_pos keyword - if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1); - - if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && - (PyFloat_Check(y_val) || PyLong_Check(y_val))) { - result.grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); - result.grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); - result.valid = true; - } - } - } else if (grid_x_obj && grid_y_obj) { - // Parse grid_x, grid_y keywords - if ((PyFloat_Check(grid_x_obj) || PyLong_Check(grid_x_obj)) && - (PyFloat_Check(grid_y_obj) || PyLong_Check(grid_y_obj))) { - result.grid_x = PyFloat_Check(grid_x_obj) ? PyFloat_AsDouble(grid_x_obj) : PyLong_AsLong(grid_x_obj); - result.grid_y = PyFloat_Check(grid_y_obj) ? PyFloat_AsDouble(grid_y_obj) : PyLong_AsLong(grid_y_obj); - result.valid = true; - } - } - } - - return result; - } - - // Parse grid size (int - no fractional tiles) - static GridSizeResult parseGridSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) { - GridSizeResult result = {0, 0, false, nullptr}; - int start_idx = next_arg ? *next_arg : 0; - bool has_positional = false; - - // Check for positional tuple argument - if (args && PyTuple_Size(args) > start_idx) { - PyObject* first = PyTuple_GetItem(args, start_idx); - - if (PyTuple_Check(first) && PyTuple_Size(first) == 2) { - PyObject* w_obj = PyTuple_GetItem(first, 0); - PyObject* h_obj = PyTuple_GetItem(first, 1); - - if (PyLong_Check(w_obj) && PyLong_Check(h_obj)) { - result.grid_w = PyLong_AsLong(w_obj); - result.grid_h = PyLong_AsLong(h_obj); - result.valid = true; - has_positional = true; - if (next_arg) (*next_arg)++; - } else { - result.valid = false; - result.error = "grid size must be specified with integers"; - return result; - } - } - } - - // Check for keyword conflicts - if (has_positional) { - if (hasConflict(kwds, "grid_size", true) || hasConflict(kwds, "grid_w", true) || hasConflict(kwds, "grid_h", true)) { - result.valid = false; - result.error = "grid size specified both positionally and by keyword"; - return result; - } - } - - // If no positional, try keywords - if (!has_positional && kwds) { - PyObject* grid_size_obj = PyDict_GetItemString(kwds, "grid_size"); - PyObject* grid_w_obj = PyDict_GetItemString(kwds, "grid_w"); - PyObject* grid_h_obj = PyDict_GetItemString(kwds, "grid_h"); - - // Check for conflicts between grid_size and grid_w/grid_h - if (grid_size_obj && (grid_w_obj || grid_h_obj)) { - result.valid = false; - result.error = "grid_size and grid_w/grid_h cannot both be specified"; - return result; - } - - if (grid_size_obj) { - // Parse grid_size keyword - if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) { - PyObject* w_val = PyTuple_GetItem(grid_size_obj, 0); - PyObject* h_val = PyTuple_GetItem(grid_size_obj, 1); - - if (PyLong_Check(w_val) && PyLong_Check(h_val)) { - result.grid_w = PyLong_AsLong(w_val); - result.grid_h = PyLong_AsLong(h_val); - result.valid = true; - } else { - result.valid = false; - result.error = "grid size must be specified with integers"; - return result; - } - } - } else if (grid_w_obj && grid_h_obj) { - // Parse grid_w, grid_h keywords - if (PyLong_Check(grid_w_obj) && PyLong_Check(grid_h_obj)) { - result.grid_w = PyLong_AsLong(grid_w_obj); - result.grid_h = PyLong_AsLong(grid_h_obj); - result.valid = true; - } else { - result.valid = false; - result.error = "grid size must be specified with integers"; - return result; - } - } - } - - return result; - } - - // Parse color using existing PyColor infrastructure - static ColorResult parseColor(PyObject* obj, const char* param_name = nullptr) { - ColorResult result = {sf::Color::White, false, nullptr}; - - if (!obj) { - return result; - } - - // Use existing PyColor::from_arg which handles tuple/Color conversion - auto py_color = PyColor::from_arg(obj); - if (py_color) { - result.color = py_color->data; - result.valid = true; - } else { - result.valid = false; - std::string error_msg = param_name - ? std::string(param_name) + " must be a color tuple (r,g,b) or (r,g,b,a)" - : "Invalid color format - expected tuple (r,g,b) or (r,g,b,a)"; - result.error = error_msg.c_str(); - } - - return result; - } - - // Helper to validate a texture object - static bool isValidTexture(PyObject* obj) { - if (!obj) return false; - PyObject* texture_type = PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Texture"); - bool is_texture = PyObject_IsInstance(obj, texture_type); - Py_DECREF(texture_type); - return is_texture; - } - - // Helper to validate a click handler - static bool isValidClickHandler(PyObject* obj) { - return obj && PyCallable_Check(obj); - } -} \ No newline at end of file diff --git a/src/PyCallable.cpp b/src/PyCallable.cpp index c68275c..0a3fb53 100644 --- a/src/PyCallable.cpp +++ b/src/PyCallable.cpp @@ -5,6 +5,21 @@ PyCallable::PyCallable(PyObject* _target) target = Py_XNewRef(_target); } +PyCallable::PyCallable(const PyCallable& other) +{ + target = Py_XNewRef(other.target); +} + +PyCallable& PyCallable::operator=(const PyCallable& other) +{ + if (this != &other) { + PyObject* old_target = target; + target = Py_XNewRef(other.target); + Py_XDECREF(old_target); + } + return *this; +} + PyCallable::~PyCallable() { if (target) @@ -21,103 +36,6 @@ bool PyCallable::isNone() const return (target == Py_None || target == NULL); } -PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now) -: PyCallable(_target), interval(_interval), last_ran(now), - paused(false), pause_start_time(0), total_paused_time(0) -{} - -PyTimerCallable::PyTimerCallable() -: PyCallable(Py_None), interval(0), last_ran(0), - paused(false), pause_start_time(0), total_paused_time(0) -{} - -bool PyTimerCallable::hasElapsed(int now) -{ - if (paused) return false; - return now >= last_ran + interval; -} - -void PyTimerCallable::call(int now) -{ - PyObject* args = Py_BuildValue("(i)", now); - PyObject* retval = PyCallable::call(args, NULL); - if (!retval) - { - PyErr_Print(); - PyErr_Clear(); - } else if (retval != Py_None) - { - std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; - std::cout << PyUnicode_AsUTF8(PyObject_Repr(retval)) << std::endl; - } -} - -bool PyTimerCallable::test(int now) -{ - if(hasElapsed(now)) - { - call(now); - last_ran = now; - return true; - } - return false; -} - -void PyTimerCallable::pause(int current_time) -{ - if (!paused) { - paused = true; - pause_start_time = current_time; - } -} - -void PyTimerCallable::resume(int current_time) -{ - if (paused) { - paused = false; - int paused_duration = current_time - pause_start_time; - total_paused_time += paused_duration; - // Adjust last_ran to account for the pause - last_ran += paused_duration; - } -} - -void PyTimerCallable::restart(int current_time) -{ - last_ran = current_time; - paused = false; - pause_start_time = 0; - total_paused_time = 0; -} - -void PyTimerCallable::cancel() -{ - // Cancel by setting target to None - if (target && target != Py_None) { - Py_DECREF(target); - } - target = Py_None; - Py_INCREF(Py_None); -} - -int PyTimerCallable::getRemaining(int current_time) const -{ - if (paused) { - // When paused, calculate time remaining from when it was paused - int elapsed_when_paused = pause_start_time - last_ran; - return interval - elapsed_when_paused; - } - int elapsed = current_time - last_ran; - return interval - elapsed; -} - -void PyTimerCallable::setCallback(PyObject* new_callback) -{ - if (target && target != Py_None) { - Py_DECREF(target); - } - target = Py_XNewRef(new_callback); -} PyClickCallable::PyClickCallable(PyObject* _target) : PyCallable(_target) diff --git a/src/PyCallable.h b/src/PyCallable.h index 6a4c7f6..5fa876c 100644 --- a/src/PyCallable.h +++ b/src/PyCallable.h @@ -6,45 +6,15 @@ class PyCallable { protected: PyObject* target; + +public: PyCallable(PyObject*); + PyCallable(const PyCallable& other); + PyCallable& operator=(const PyCallable& other); ~PyCallable(); PyObject* call(PyObject*, PyObject*); -public: bool isNone() const; -}; - -class PyTimerCallable: public PyCallable -{ -private: - int interval; - int last_ran; - void call(int); - - // Pause/resume support - bool paused; - int pause_start_time; - int total_paused_time; - -public: - bool hasElapsed(int); - bool test(int); - PyTimerCallable(PyObject*, int, int); - PyTimerCallable(); - - // Timer control methods - void pause(int current_time); - void resume(int current_time); - void restart(int current_time); - void cancel(); - - // Timer state queries - bool isPaused() const { return paused; } - bool isActive() const { return !isNone() && !paused; } - int getInterval() const { return interval; } - void setInterval(int new_interval) { interval = new_interval; } - int getRemaining(int current_time) const; - PyObject* getCallback() { return target; } - void setCallback(PyObject* new_callback); + PyObject* borrow() const { return target; } }; class PyClickCallable: public PyCallable @@ -54,6 +24,11 @@ public: PyObject* borrow(); PyClickCallable(PyObject*); PyClickCallable(); + PyClickCallable(const PyClickCallable& other) : PyCallable(other) {} + PyClickCallable& operator=(const PyClickCallable& other) { + PyCallable::operator=(other); + return *this; + } }; class PyKeyCallable: public PyCallable diff --git a/src/PyScene.cpp b/src/PyScene.cpp index fb2a49e..84b92a7 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -31,13 +31,18 @@ void PyScene::do_mouse_input(std::string button, std::string type) // Convert window coordinates to game coordinates using the viewport auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos)); - // Create a sorted copy by z-index (highest first) - std::vector> sorted_elements(*ui_elements); - std::sort(sorted_elements.begin(), sorted_elements.end(), - [](const auto& a, const auto& b) { return a->z_index > b->z_index; }); + // Only sort if z_index values have changed + if (ui_elements_need_sort) { + // Sort in ascending order (same as render) + std::sort(ui_elements->begin(), ui_elements->end(), + [](const auto& a, const auto& b) { return a->z_index < b->z_index; }); + ui_elements_need_sort = false; + } - // Check elements in z-order (top to bottom) - for (const auto& element : sorted_elements) { + // Check elements in reverse z-order (highest z_index first, top to bottom) + // Use reverse iterators to go from end to beginning + for (auto it = ui_elements->rbegin(); it != ui_elements->rend(); ++it) { + const auto& element = *it; if (!element->visible) continue; if (auto target = element->click_at(sf::Vector2f(mousepos))) { diff --git a/src/PyTimer.cpp b/src/PyTimer.cpp index df80bc5..d1e89ec 100644 --- a/src/PyTimer.cpp +++ b/src/PyTimer.cpp @@ -1,7 +1,8 @@ #include "PyTimer.h" -#include "PyCallable.h" +#include "Timer.h" #include "GameEngine.h" #include "Resources.h" +#include "PythonObjectCache.h" #include PyObject* PyTimer::repr(PyObject* self) { @@ -11,7 +12,22 @@ PyObject* PyTimer::repr(PyObject* self) { if (timer->data) { oss << "interval=" << timer->data->getInterval() << "ms "; - oss << (timer->data->isPaused() ? "paused" : "active"); + if (timer->data->isOnce()) { + oss << "once=True "; + } + if (timer->data->isPaused()) { + oss << "paused"; + // Get current time to show remaining + int current_time = 0; + if (Resources::game) { + current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); + } + oss << " (remaining=" << timer->data->getRemaining(current_time) << "ms)"; + } else if (timer->data->isActive()) { + oss << "active"; + } else { + oss << "cancelled"; + } } else { oss << "uninitialized"; } @@ -25,18 +41,20 @@ PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { if (self) { new(&self->name) std::string(); // Placement new for std::string self->data = nullptr; + self->weakreflist = nullptr; // Initialize weakref list } return (PyObject*)self; } int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = {"name", "callback", "interval", NULL}; + static const char* kwlist[] = {"name", "callback", "interval", "once", NULL}; const char* name = nullptr; PyObject* callback = nullptr; int interval = 0; + int once = 0; // Use int for bool parameter - if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", const_cast(kwlist), - &name, &callback, &interval)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi|p", const_cast(kwlist), + &name, &callback, &interval, &once)) { return -1; } @@ -58,8 +76,18 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) { current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); } - // Create the timer callable - self->data = std::make_shared(callback, interval, current_time); + // Create the timer + self->data = std::make_shared(callback, interval, current_time, (bool)once); + + // Register in Python object cache + if (self->data->serial_number == 0) { + self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); + PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL); + if (weakref) { + PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref); + Py_DECREF(weakref); // Cache owns the reference now + } + } // Register with game engine if (Resources::game) { @@ -70,6 +98,11 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) { } void PyTimer::dealloc(PyTimerObject* self) { + // Clear weakrefs first + if (self->weakreflist != nullptr) { + PyObject_ClearWeakRefs((PyObject*)self); + } + // Remove from game engine if still registered if (Resources::game && !self->name.empty()) { auto it = Resources::game->timers.find(self->name); @@ -244,7 +277,37 @@ int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) { return 0; } +PyObject* PyTimer::get_once(PyTimerObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); + return nullptr; + } + + return PyBool_FromLong(self->data->isOnce()); +} + +int PyTimer::set_once(PyTimerObject* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); + return -1; + } + + if (!PyBool_Check(value)) { + PyErr_SetString(PyExc_TypeError, "once must be a boolean"); + return -1; + } + + self->data->setOnce(PyObject_IsTrue(value)); + return 0; +} + +PyObject* PyTimer::get_name(PyTimerObject* self, void* closure) { + return PyUnicode_FromString(self->name.c_str()); +} + PyGetSetDef PyTimer::getsetters[] = { + {"name", (getter)PyTimer::get_name, NULL, + "Timer name (read-only)", NULL}, {"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval, "Timer interval in milliseconds", NULL}, {"remaining", (getter)PyTimer::get_remaining, NULL, @@ -255,17 +318,27 @@ PyGetSetDef PyTimer::getsetters[] = { "Whether the timer is active and not paused", NULL}, {"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback, "The callback function to be called", NULL}, + {"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once, + "Whether the timer stops after firing once", NULL}, {NULL} }; PyMethodDef PyTimer::methods[] = { {"pause", (PyCFunction)PyTimer::pause, METH_NOARGS, - "Pause the timer"}, + "pause() -> None\n\n" + "Pause the timer, preserving the time remaining until next trigger.\n" + "The timer can be resumed later with resume()."}, {"resume", (PyCFunction)PyTimer::resume, METH_NOARGS, - "Resume a paused timer"}, + "resume() -> None\n\n" + "Resume a paused timer from where it left off.\n" + "Has no effect if the timer is not paused."}, {"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS, - "Cancel the timer and remove it from the system"}, + "cancel() -> None\n\n" + "Cancel the timer and remove it from the timer system.\n" + "The timer will no longer fire and cannot be restarted."}, {"restart", (PyCFunction)PyTimer::restart, METH_NOARGS, - "Restart the timer from the current time"}, + "restart() -> None\n\n" + "Restart the timer from the beginning.\n" + "Resets the timer to fire after a full interval from now."}, {NULL} }; \ No newline at end of file diff --git a/src/PyTimer.h b/src/PyTimer.h index 16c4deb..3ee210c 100644 --- a/src/PyTimer.h +++ b/src/PyTimer.h @@ -4,12 +4,13 @@ #include #include -class PyTimerCallable; +class Timer; typedef struct { PyObject_HEAD - std::shared_ptr data; + std::shared_ptr data; std::string name; + PyObject* weakreflist; // Weak reference support } PyTimerObject; class PyTimer @@ -28,6 +29,7 @@ public: static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)); // Timer property getters + static PyObject* get_name(PyTimerObject* self, void* closure); static PyObject* get_interval(PyTimerObject* self, void* closure); static int set_interval(PyTimerObject* self, PyObject* value, void* closure); static PyObject* get_remaining(PyTimerObject* self, void* closure); @@ -35,6 +37,8 @@ public: static PyObject* get_active(PyTimerObject* self, void* closure); static PyObject* get_callback(PyTimerObject* self, void* closure); static int set_callback(PyTimerObject* self, PyObject* value, void* closure); + static PyObject* get_once(PyTimerObject* self, void* closure); + static int set_once(PyTimerObject* self, PyObject* value, void* closure); static PyGetSetDef getsetters[]; static PyMethodDef methods[]; @@ -49,7 +53,35 @@ namespace mcrfpydef { .tp_dealloc = (destructor)PyTimer::dealloc, .tp_repr = PyTimer::repr, .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Timer object for scheduled callbacks"), + .tp_doc = PyDoc_STR("Timer(name, callback, interval, once=False)\n\n" + "Create a timer that calls a function at regular intervals.\n\n" + "Args:\n" + " name (str): Unique identifier for the timer\n" + " callback (callable): Function to call - receives (timer, runtime) args\n" + " interval (int): Time between calls in milliseconds\n" + " once (bool): If True, timer stops after first call. Default: False\n\n" + "Attributes:\n" + " interval (int): Time between calls in milliseconds\n" + " remaining (int): Time until next call in milliseconds (read-only)\n" + " paused (bool): Whether timer is paused (read-only)\n" + " active (bool): Whether timer is active and not paused (read-only)\n" + " callback (callable): The callback function\n" + " once (bool): Whether timer stops after firing once\n\n" + "Methods:\n" + " pause(): Pause the timer, preserving time remaining\n" + " resume(): Resume a paused timer\n" + " cancel(): Stop and remove the timer\n" + " restart(): Reset timer to start from beginning\n\n" + "Example:\n" + " def on_timer(timer, runtime):\n" + " print(f'Timer {timer} fired at {runtime}ms')\n" + " if runtime > 5000:\n" + " timer.cancel()\n" + " \n" + " timer = mcrfpy.Timer('my_timer', on_timer, 1000)\n" + " timer.pause() # Pause timer\n" + " timer.resume() # Resume timer\n" + " timer.once = True # Make it one-shot"), .tp_methods = PyTimer::methods, .tp_getset = PyTimer::getsetters, .tp_init = (initproc)PyTimer::init, diff --git a/src/PythonObjectCache.cpp b/src/PythonObjectCache.cpp new file mode 100644 index 0000000..b280eb2 --- /dev/null +++ b/src/PythonObjectCache.cpp @@ -0,0 +1,85 @@ +#include "PythonObjectCache.h" +#include + +PythonObjectCache* PythonObjectCache::instance = nullptr; + +PythonObjectCache& PythonObjectCache::getInstance() { + if (!instance) { + instance = new PythonObjectCache(); + } + return *instance; +} + +PythonObjectCache::~PythonObjectCache() { + clear(); +} + +uint64_t PythonObjectCache::assignSerial() { + return next_serial.fetch_add(1, std::memory_order_relaxed); +} + +void PythonObjectCache::registerObject(uint64_t serial, PyObject* weakref) { + if (!weakref || serial == 0) return; + + std::lock_guard lock(serial_mutex); + + // Clean up any existing entry + auto it = cache.find(serial); + if (it != cache.end()) { + Py_DECREF(it->second); + } + + // Store the new weak reference + Py_INCREF(weakref); + cache[serial] = weakref; +} + +PyObject* PythonObjectCache::lookup(uint64_t serial) { + if (serial == 0) return nullptr; + + // No mutex needed for read - GIL protects PyWeakref_GetObject + auto it = cache.find(serial); + if (it != cache.end()) { + PyObject* obj = PyWeakref_GetObject(it->second); + if (obj && obj != Py_None) { + Py_INCREF(obj); + return obj; + } + } + return nullptr; +} + +void PythonObjectCache::remove(uint64_t serial) { + if (serial == 0) return; + + std::lock_guard lock(serial_mutex); + auto it = cache.find(serial); + if (it != cache.end()) { + Py_DECREF(it->second); + cache.erase(it); + } +} + +void PythonObjectCache::cleanup() { + std::lock_guard lock(serial_mutex); + + auto it = cache.begin(); + while (it != cache.end()) { + PyObject* obj = PyWeakref_GetObject(it->second); + if (!obj || obj == Py_None) { + Py_DECREF(it->second); + it = cache.erase(it); + } else { + ++it; + } + } +} + +void PythonObjectCache::clear() { + std::lock_guard lock(serial_mutex); + + for (auto& pair : cache) { + Py_DECREF(pair.second); + } + cache.clear(); +} \ No newline at end of file diff --git a/src/PythonObjectCache.h b/src/PythonObjectCache.h new file mode 100644 index 0000000..0481374 --- /dev/null +++ b/src/PythonObjectCache.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include +#include + +class PythonObjectCache { +private: + static PythonObjectCache* instance; + std::mutex serial_mutex; + std::atomic next_serial{1}; + std::unordered_map cache; + + PythonObjectCache() = default; + ~PythonObjectCache(); + +public: + static PythonObjectCache& getInstance(); + + // Assign a new serial number + uint64_t assignSerial(); + + // Register a Python object with a serial number + void registerObject(uint64_t serial, PyObject* weakref); + + // Lookup a Python object by serial number + // Returns new reference or nullptr + PyObject* lookup(uint64_t serial); + + // Remove an entry from the cache + void remove(uint64_t serial); + + // Clean up dead weak references + void cleanup(); + + // Clear entire cache (for module cleanup) + void clear(); +}; \ No newline at end of file diff --git a/src/Timer.cpp b/src/Timer.cpp index 3547488..0784f13 100644 --- a/src/Timer.cpp +++ b/src/Timer.cpp @@ -1,31 +1,140 @@ #include "Timer.h" +#include "PythonObjectCache.h" +#include "PyCallable.h" -Timer::Timer(PyObject* _target, int _interval, int now) -: target(_target), interval(_interval), last_ran(now) +Timer::Timer(PyObject* _target, int _interval, int now, bool _once) +: callback(std::make_shared(_target)), interval(_interval), last_ran(now), + paused(false), pause_start_time(0), total_paused_time(0), once(_once) {} Timer::Timer() -: target(Py_None), interval(0), last_ran(0) +: callback(std::make_shared(Py_None)), interval(0), last_ran(0), + paused(false), pause_start_time(0), total_paused_time(0), once(false) {} +Timer::~Timer() { + if (serial_number != 0) { + PythonObjectCache::getInstance().remove(serial_number); + } +} + +bool Timer::hasElapsed(int now) const +{ + if (paused) return false; + return now >= last_ran + interval; +} + bool Timer::test(int now) { - if (!target || target == Py_None) return false; - if (now > last_ran + interval) + if (!callback || callback->isNone()) return false; + + if (hasElapsed(now)) { last_ran = now; - PyObject* args = Py_BuildValue("(i)", now); - PyObject* retval = PyObject_Call(target, args, NULL); + + // Get the PyTimer wrapper from cache to pass to callback + PyObject* timer_obj = nullptr; + if (serial_number != 0) { + timer_obj = PythonObjectCache::getInstance().lookup(serial_number); + } + + // Build args: (timer, runtime) or just (runtime) if no wrapper found + PyObject* args; + if (timer_obj) { + args = Py_BuildValue("(Oi)", timer_obj, now); + } else { + // Fallback to old behavior if no wrapper found + args = Py_BuildValue("(i)", now); + } + + PyObject* retval = callback->call(args, NULL); + Py_DECREF(args); + if (!retval) { - std::cout << "timer has raised an exception. It's going to STDERR and being dropped:" << std::endl; + std::cout << "Timer callback has raised an exception. It's going to STDERR and being dropped:" << std::endl; PyErr_Print(); PyErr_Clear(); } else if (retval != Py_None) { - std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; + std::cout << "Timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; + Py_DECREF(retval); } + + // Handle one-shot timers + if (once) { + cancel(); + } + return true; } return false; } + +void Timer::pause(int current_time) +{ + if (!paused) { + paused = true; + pause_start_time = current_time; + } +} + +void Timer::resume(int current_time) +{ + if (paused) { + paused = false; + int paused_duration = current_time - pause_start_time; + total_paused_time += paused_duration; + // Adjust last_ran to account for the pause + last_ran += paused_duration; + } +} + +void Timer::restart(int current_time) +{ + last_ran = current_time; + paused = false; + pause_start_time = 0; + total_paused_time = 0; +} + +void Timer::cancel() +{ + // Cancel by setting callback to None + callback = std::make_shared(Py_None); +} + +bool Timer::isActive() const +{ + return callback && !callback->isNone() && !paused; +} + +int Timer::getRemaining(int current_time) const +{ + if (paused) { + // When paused, calculate time remaining from when it was paused + int elapsed_when_paused = pause_start_time - last_ran; + return interval - elapsed_when_paused; + } + int elapsed = current_time - last_ran; + return interval - elapsed; +} + +int Timer::getElapsed(int current_time) const +{ + if (paused) { + return pause_start_time - last_ran; + } + return current_time - last_ran; +} + +PyObject* Timer::getCallback() +{ + if (!callback) return Py_None; + return callback->borrow(); +} + +void Timer::setCallback(PyObject* new_callback) +{ + callback = std::make_shared(new_callback); +} \ No newline at end of file diff --git a/src/Timer.h b/src/Timer.h index bc9b7dd..c52b261 100644 --- a/src/Timer.h +++ b/src/Timer.h @@ -1,15 +1,54 @@ #pragma once #include "Common.h" #include "Python.h" +#include + +class PyCallable; class GameEngine; // forward declare class Timer { -public: - PyObject* target; +private: + std::shared_ptr callback; int interval; int last_ran; + + // Pause/resume support + bool paused; + int pause_start_time; + int total_paused_time; + + // One-shot timer support + bool once; + +public: + uint64_t serial_number = 0; // For Python object cache + Timer(); // for map to build - Timer(PyObject*, int, int); - bool test(int); + Timer(PyObject* target, int interval, int now, bool once = false); + ~Timer(); + + // Core timer functionality + bool test(int now); + bool hasElapsed(int now) const; + + // Timer control methods + void pause(int current_time); + void resume(int current_time); + void restart(int current_time); + void cancel(); + + // Timer state queries + bool isPaused() const { return paused; } + bool isActive() const; + int getInterval() const { return interval; } + void setInterval(int new_interval) { interval = new_interval; } + int getRemaining(int current_time) const; + int getElapsed(int current_time) const; + bool isOnce() const { return once; } + void setOnce(bool value) { once = value; } + + // Callback management + PyObject* getCallback(); + void setCallback(PyObject* new_callback); }; diff --git a/src/UIBase.h b/src/UIBase.h index c1707bf..d57e54c 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -6,12 +6,14 @@ class UIEntity; typedef struct { PyObject_HEAD std::shared_ptr data; + PyObject* weakreflist; // Weak reference support } PyUIEntityObject; class UIFrame; typedef struct { PyObject_HEAD std::shared_ptr data; + PyObject* weakreflist; // Weak reference support } PyUIFrameObject; class UICaption; @@ -19,18 +21,21 @@ typedef struct { PyObject_HEAD std::shared_ptr data; PyObject* font; + PyObject* weakreflist; // Weak reference support } PyUICaptionObject; class UIGrid; typedef struct { PyObject_HEAD std::shared_ptr data; + PyObject* weakreflist; // Weak reference support } PyUIGridObject; class UISprite; typedef struct { PyObject_HEAD std::shared_ptr data; + PyObject* weakreflist; // Weak reference support } PyUISpriteObject; // Common Python method implementations for UIDrawable-derived classes diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 1df752a..6ac1adb 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -3,7 +3,7 @@ #include "PyColor.h" #include "PyVector.h" #include "PyFont.h" -#include "PyArgHelpers.h" +#include "PythonObjectCache.h" // UIDrawable methods now in UIBase.h #include @@ -303,183 +303,135 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) { using namespace mcrfpydef; - // Try parsing with PyArgHelpers - int arg_idx = 0; - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); - - // Default values - float x = 0.0f, y = 0.0f, outline = 0.0f; - char* text = nullptr; + // Define all parameters with defaults + PyObject* pos_obj = nullptr; PyObject* font = nullptr; + const char* text = ""; PyObject* fill_color = nullptr; PyObject* outline_color = nullptr; + float outline = 0.0f; + float font_size = 16.0f; PyObject* click_handler = nullptr; + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + float x = 0.0f, y = 0.0f; - // Case 1: Got position from helpers (tuple format) - if (pos_result.valid) { - x = pos_result.x; - y = pos_result.y; - - // Parse remaining arguments - static const char* remaining_keywords[] = { - "text", "font", "fill_color", "outline_color", "outline", "click", nullptr - }; - - // Create new tuple with remaining args - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|zOOOfO", - const_cast(remaining_keywords), - &text, &font, &fill_color, &outline_color, - &outline, &click_handler)) { - Py_DECREF(remaining_args); - if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error); + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "pos", "font", "text", // Positional args (as per spec) + // Keyword-only args + "fill_color", "outline_color", "outline", "font_size", "click", + "visible", "opacity", "z_index", "name", "x", "y", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizff", const_cast(kwlist), + &pos_obj, &font, &text, // Positional + &fill_color, &outline_color, &outline, &font_size, &click_handler, + &visible, &opacity, &z_index, &name, &x, &y)) { + return -1; + } + + // Handle position argument (can be tuple, Vector, or use x/y keywords) + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (vec) { + x = vec->data.x; + y = vec->data.y; + Py_DECREF(vec); + } else { + PyErr_Clear(); + if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { + PyObject* x_val = PyTuple_GetItem(pos_obj, 0); + PyObject* y_val = PyTuple_GetItem(pos_obj, 1); + if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && + (PyFloat_Check(y_val) || PyLong_Check(y_val))) { + x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); + y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); + } else { + PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers"); + return -1; + } + } else { + PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); + return -1; + } + } + } + + // Handle font argument + std::shared_ptr pyfont = nullptr; + if (font && font != Py_None) { + if (!PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font"))) { + PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance"); return -1; } - Py_DECREF(remaining_args); - } - // Case 2: Traditional format - else { - PyErr_Clear(); // Clear any errors from helpers - - // First check if this is the old (text, x, y, ...) format - PyObject* first_arg = args && PyTuple_Size(args) > 0 ? PyTuple_GetItem(args, 0) : nullptr; - bool text_first = first_arg && PyUnicode_Check(first_arg); - - if (text_first) { - // Pattern: (text, x, y, ...) - static const char* text_first_keywords[] = { - "text", "x", "y", "font", "fill_color", "outline_color", - "outline", "click", "pos", nullptr - }; - PyObject* pos_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO", - const_cast(text_first_keywords), - &text, &x, &y, &font, &fill_color, &outline_color, - &outline, &click_handler, &pos_obj)) { - return -1; - } - - // Handle pos keyword override - if (pos_obj && pos_obj != Py_None) { - if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(pos_obj, 1); - if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && - (PyFloat_Check(y_val) || PyLong_Check(y_val))) { - x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); - y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); - } - } else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString( - PyImport_ImportModule("mcrfpy"), "Vector"))) { - PyVectorObject* vec = (PyVectorObject*)pos_obj; - x = vec->data.x; - y = vec->data.y; - } else { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); - return -1; - } - } - } else { - // Pattern: (x, y, text, ...) - static const char* xy_keywords[] = { - "x", "y", "text", "font", "fill_color", "outline_color", - "outline", "click", "pos", nullptr - }; - PyObject* pos_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO", - const_cast(xy_keywords), - &x, &y, &text, &font, &fill_color, &outline_color, - &outline, &click_handler, &pos_obj)) { - return -1; - } - - // Handle pos keyword override - if (pos_obj && pos_obj != Py_None) { - if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(pos_obj, 1); - if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && - (PyFloat_Check(y_val) || PyLong_Check(y_val))) { - x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); - y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); - } - } else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString( - PyImport_ImportModule("mcrfpy"), "Vector"))) { - PyVectorObject* vec = (PyVectorObject*)pos_obj; - x = vec->data.x; - y = vec->data.y; - } else { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); - return -1; - } - } - } + auto obj = (PyFontObject*)font; + pyfont = obj->data; } - self->data->position = sf::Vector2f(x, y); // Set base class position - self->data->text.setPosition(self->data->position); // Sync text position - // check types for font, fill_color, outline_color - - //std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl; - if (font != NULL && font != Py_None && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){ - PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance or None"); - return -1; - } else if (font != NULL && font != Py_None) - { - auto font_obj = (PyFontObject*)font; - self->data->text.setFont(font_obj->data->font); - self->font = font; - Py_INCREF(font); - } else - { + // Create the caption + self->data = std::make_shared(); + self->data->position = sf::Vector2f(x, y); + self->data->text.setPosition(self->data->position); + self->data->text.setOutlineThickness(outline); + + // Set the font + if (pyfont) { + self->data->text.setFont(pyfont->font); + } else { // Use default font when None or not provided if (McRFPy_API::default_font) { self->data->text.setFont(McRFPy_API::default_font->font); - // Store reference to default font - PyObject* default_font_obj = PyObject_GetAttrString(McRFPy_API::mcrf_module, "default_font"); - if (default_font_obj) { - self->font = default_font_obj; - // Don't need to DECREF since we're storing it - } } } - - // Handle text - default to empty string if not provided - if (text && text != NULL) { - self->data->text.setString((std::string)text); - } else { - self->data->text.setString(""); + + // Set character size + self->data->text.setCharacterSize(static_cast(font_size)); + + // Set text + if (text && strlen(text) > 0) { + self->data->text.setString(std::string(text)); } - self->data->text.setOutlineThickness(outline); - if (fill_color) { - auto fc = PyColor::from_arg(fill_color); - if (!fc) { - PyErr_SetString(PyExc_TypeError, "fill_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__"); + + // Handle fill_color + if (fill_color && fill_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(fill_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple"); return -1; } - self->data->text.setFillColor(PyColor::fromPy(fc)); - //Py_DECREF(fc); + self->data->text.setFillColor(color_obj->data); + Py_DECREF(color_obj); } else { - self->data->text.setFillColor(sf::Color(0,0,0,255)); + self->data->text.setFillColor(sf::Color(255, 255, 255, 255)); // Default: white } - - if (outline_color) { - auto oc = PyColor::from_arg(outline_color); - if (!oc) { - PyErr_SetString(PyExc_TypeError, "outline_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__"); + + // Handle outline_color + if (outline_color && outline_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(outline_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple"); return -1; } - self->data->text.setOutlineColor(PyColor::fromPy(oc)); - //Py_DECREF(oc); + self->data->text.setOutlineColor(color_obj->data); + Py_DECREF(color_obj); } else { - self->data->text.setOutlineColor(sf::Color(128,128,128,255)); + self->data->text.setOutlineColor(sf::Color(0, 0, 0, 255)); // Default: black } - - // Process click handler if provided + + // Set other properties + self->data->visible = visible; + self->data->opacity = opacity; + self->data->z_index = z_index; + if (name) { + self->data->name = std::string(name); + } + + // Handle click handler if (click_handler && click_handler != Py_None) { if (!PyCallable_Check(click_handler)) { PyErr_SetString(PyExc_TypeError, "click must be callable"); @@ -487,10 +439,24 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) } self->data->click_register(click_handler); } - + + // Initialize weak reference list + self->weakreflist = NULL; + + // Register in Python object cache + if (self->data->serial_number == 0) { + self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); + PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL); + if (weakref) { + PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref); + Py_DECREF(weakref); // Cache owns the reference now + } + } + return 0; } + // Property system implementation for animations bool UICaption::setProperty(const std::string& name, float value) { if (name == "x") { diff --git a/src/UICaption.h b/src/UICaption.h index 9e29a35..851b960 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -54,6 +54,10 @@ namespace mcrfpydef { .tp_dealloc = (destructor)[](PyObject* self) { PyUICaptionObject* obj = (PyUICaptionObject*)self; + // Clear weak references + if (obj->weakreflist != NULL) { + PyObject_ClearWeakRefs(self); + } // TODO - reevaluate with PyFont usage; UICaption does not own the font // release reference to font object if (obj->font) Py_DECREF(obj->font); @@ -64,27 +68,38 @@ namespace mcrfpydef { //.tp_hash = NULL, //.tp_iter //.tp_iternext - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)\n\n" + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n" "A text display UI element with customizable font and styling.\n\n" "Args:\n" - " text (str): The text content to display. Default: ''\n" - " x (float): X position in pixels. Default: 0\n" - " y (float): Y position in pixels. Default: 0\n" - " font (Font): Font object for text rendering. Default: engine default font\n" + " pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n" + " font (Font, optional): Font object for text rendering. Default: engine default font\n" + " text (str, optional): The text content to display. Default: ''\n\n" + "Keyword Args:\n" " fill_color (Color): Text fill color. Default: (255, 255, 255, 255)\n" " outline_color (Color): Text outline color. Default: (0, 0, 0, 255)\n" " outline (float): Text outline thickness. Default: 0\n" - " click (callable): Click event handler. Default: None\n\n" + " font_size (float): Font size in points. Default: 16\n" + " click (callable): Click event handler. Default: None\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " z_index (int): Rendering order. Default: 0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X position override. Default: 0\n" + " y (float): Y position override. Default: 0\n\n" "Attributes:\n" " text (str): The displayed text content\n" " x, y (float): Position in pixels\n" + " pos (Vector): Position as a Vector object\n" " font (Font): Font used for rendering\n" + " font_size (float): Font size in points\n" " fill_color, outline_color (Color): Text appearance\n" " outline (float): Outline thickness\n" " click (callable): Click event handler\n" " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" + " name (str): Element name\n" " w, h (float): Read-only computed size based on text and font"), .tp_methods = UICaption_methods, //.tp_members = PyUIFrame_members, @@ -95,7 +110,11 @@ namespace mcrfpydef { .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { PyUICaptionObject* self = (PyUICaptionObject*)type->tp_alloc(type, 0); - if (self) self->data = std::make_shared(); + if (self) { + self->data = std::make_shared(); + self->font = nullptr; + self->weakreflist = nullptr; + } return (PyObject*)self; } }; diff --git a/src/UICollection.cpp b/src/UICollection.cpp index 309a994..5e749cb 100644 --- a/src/UICollection.cpp +++ b/src/UICollection.cpp @@ -6,6 +6,7 @@ #include "UIGrid.h" #include "McRFPy_API.h" #include "PyObjectUtils.h" +#include "PythonObjectCache.h" #include #include @@ -17,6 +18,14 @@ static PyObject* convertDrawableToPython(std::shared_ptr drawable) { Py_RETURN_NONE; } + // Check cache first + if (drawable->serial_number != 0) { + PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number); + if (cached) { + return cached; // Already INCREF'd by lookup + } + } + PyTypeObject* type = nullptr; PyObject* obj = nullptr; @@ -28,6 +37,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr drawable) { auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0); if (pyObj) { pyObj->data = std::static_pointer_cast(drawable); + pyObj->weakreflist = NULL; } obj = (PyObject*)pyObj; break; @@ -40,6 +50,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr drawable) { if (pyObj) { pyObj->data = std::static_pointer_cast(drawable); pyObj->font = nullptr; + pyObj->weakreflist = NULL; } obj = (PyObject*)pyObj; break; @@ -51,6 +62,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr drawable) { auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0); if (pyObj) { pyObj->data = std::static_pointer_cast(drawable); + pyObj->weakreflist = NULL; } obj = (PyObject*)pyObj; break; @@ -62,6 +74,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr drawable) { auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0); if (pyObj) { pyObj->data = std::static_pointer_cast(drawable); + pyObj->weakreflist = NULL; } obj = (PyObject*)pyObj; break; diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index 5e10b62..882b3c3 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -5,9 +5,113 @@ #include "UIGrid.h" #include "GameEngine.h" #include "McRFPy_API.h" +#include "PythonObjectCache.h" UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; } +UIDrawable::UIDrawable(const UIDrawable& other) + : z_index(other.z_index), + name(other.name), + position(other.position), + visible(other.visible), + opacity(other.opacity), + serial_number(0), // Don't copy serial number + use_render_texture(other.use_render_texture), + render_dirty(true) // Force redraw after copy +{ + // Deep copy click_callable if it exists + if (other.click_callable) { + click_callable = std::make_unique(*other.click_callable); + } + + // Deep copy render texture if needed + if (other.render_texture && other.use_render_texture) { + auto size = other.render_texture->getSize(); + enableRenderTexture(size.x, size.y); + } +} + +UIDrawable& UIDrawable::operator=(const UIDrawable& other) { + if (this != &other) { + // Copy basic members + z_index = other.z_index; + name = other.name; + position = other.position; + visible = other.visible; + opacity = other.opacity; + use_render_texture = other.use_render_texture; + render_dirty = true; // Force redraw after copy + + // Deep copy click_callable + if (other.click_callable) { + click_callable = std::make_unique(*other.click_callable); + } else { + click_callable.reset(); + } + + // Deep copy render texture if needed + if (other.render_texture && other.use_render_texture) { + auto size = other.render_texture->getSize(); + enableRenderTexture(size.x, size.y); + } else { + render_texture.reset(); + use_render_texture = false; + } + } + return *this; +} + +UIDrawable::UIDrawable(UIDrawable&& other) noexcept + : z_index(other.z_index), + name(std::move(other.name)), + position(other.position), + visible(other.visible), + opacity(other.opacity), + serial_number(other.serial_number), + click_callable(std::move(other.click_callable)), + render_texture(std::move(other.render_texture)), + render_sprite(std::move(other.render_sprite)), + use_render_texture(other.use_render_texture), + render_dirty(other.render_dirty) +{ + // Clear the moved-from object's serial number to avoid cache issues + other.serial_number = 0; +} + +UIDrawable& UIDrawable::operator=(UIDrawable&& other) noexcept { + if (this != &other) { + // Clear our own cache entry if we have one + if (serial_number != 0) { + PythonObjectCache::getInstance().remove(serial_number); + } + + // Move basic members + z_index = other.z_index; + name = std::move(other.name); + position = other.position; + visible = other.visible; + opacity = other.opacity; + serial_number = other.serial_number; + use_render_texture = other.use_render_texture; + render_dirty = other.render_dirty; + + // Move unique_ptr members + click_callable = std::move(other.click_callable); + render_texture = std::move(other.render_texture); + render_sprite = std::move(other.render_sprite); + + // Clear the moved-from object's serial number + other.serial_number = 0; + } + return *this; +} + +UIDrawable::~UIDrawable() { + if (serial_number != 0) { + PythonObjectCache::getInstance().remove(serial_number); + } +} + void UIDrawable::click_unregister() { click_callable.reset(); diff --git a/src/UIDrawable.h b/src/UIDrawable.h index b18bf54..12a3ed0 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -39,6 +39,15 @@ public: void click_unregister(); UIDrawable(); + virtual ~UIDrawable(); + + // Copy constructor and assignment operator + UIDrawable(const UIDrawable& other); + UIDrawable& operator=(const UIDrawable& other); + + // Move constructor and assignment operator + UIDrawable(UIDrawable&& other) noexcept; + UIDrawable& operator=(UIDrawable&& other) noexcept; static PyObject* get_click(PyObject* self, void* closure); static int set_click(PyObject* self, PyObject* value, void* closure); @@ -90,6 +99,9 @@ public: virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; } virtual bool getProperty(const std::string& name, std::string& value) const { return false; } + // Python object cache support + uint64_t serial_number = 0; + protected: // RenderTexture support (opt-in) std::unique_ptr render_texture; diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 4143ed0..85c6d5f 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -4,7 +4,7 @@ #include #include "PyObjectUtils.h" #include "PyVector.h" -#include "PyArgHelpers.h" +#include "PythonObjectCache.h" // UIDrawable methods now in UIBase.h #include "UIEntityPyMethods.h" @@ -17,6 +17,12 @@ UIEntity::UIEntity() // gridstate vector starts empty - will be lazily initialized when needed } +UIEntity::~UIEntity() { + if (serial_number != 0) { + PythonObjectCache::getInstance().remove(serial_number); + } +} + // Removed UIEntity(UIGrid&) constructor - using lazy initialization instead void UIEntity::updateVisibility() @@ -121,81 +127,57 @@ PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) } int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { - // Try parsing with PyArgHelpers for grid position - int arg_idx = 0; - auto grid_pos_result = PyArgHelpers::parseGridPosition(args, kwds, &arg_idx); - - // Default values - float grid_x = 0.0f, grid_y = 0.0f; - int sprite_index = 0; + // Define all parameters with defaults + PyObject* grid_pos_obj = nullptr; PyObject* texture = nullptr; + int sprite_index = 0; PyObject* grid_obj = nullptr; + int visible = 1; + float opacity = 1.0f; + const char* name = nullptr; + float x = 0.0f, y = 0.0f; - // Case 1: Got grid position from helpers (tuple format) - if (grid_pos_result.valid) { - grid_x = grid_pos_result.grid_x; - grid_y = grid_pos_result.grid_y; - - // Parse remaining arguments - static const char* remaining_keywords[] = { - "texture", "sprite_index", "grid", nullptr - }; - - // Create new tuple with remaining args - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OiO", - const_cast(remaining_keywords), - &texture, &sprite_index, &grid_obj)) { - Py_DECREF(remaining_args); - if (grid_pos_result.error) PyErr_SetString(PyExc_TypeError, grid_pos_result.error); - return -1; - } - Py_DECREF(remaining_args); + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "grid_pos", "texture", "sprite_index", // Positional args (as per spec) + // Keyword-only args + "grid", "visible", "opacity", "name", "x", "y", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiOifzff", const_cast(kwlist), + &grid_pos_obj, &texture, &sprite_index, // Positional + &grid_obj, &visible, &opacity, &name, &x, &y)) { + return -1; } - // Case 2: Traditional format - else { - PyErr_Clear(); // Clear any errors from helpers - - static const char* keywords[] = { - "grid_x", "grid_y", "texture", "sprite_index", "grid", "grid_pos", nullptr - }; - PyObject* grid_pos_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO", - const_cast(keywords), - &grid_x, &grid_y, &texture, &sprite_index, - &grid_obj, &grid_pos_obj)) { - return -1; - } - - // Handle grid_pos keyword override - if (grid_pos_obj && grid_pos_obj != Py_None) { - if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1); - if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && - (PyFloat_Check(y_val) || PyLong_Check(y_val))) { - grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); - grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); - } + + // Handle grid position argument (can be tuple or use x/y keywords) + if (grid_pos_obj) { + if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) { + PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0); + PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1); + if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && + (PyFloat_Check(y_val) || PyLong_Check(y_val))) { + x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); + y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); } else { - PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)"); + PyErr_SetString(PyExc_TypeError, "grid_pos tuple must contain numbers"); return -1; } + } else { + PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)"); + return -1; } } - // check types for texture - // - // Set Texture - allow None or use default - // + // Handle texture argument std::shared_ptr texture_ptr = nullptr; - if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){ - PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); - return -1; - } else if (texture != NULL && texture != Py_None) { + if (texture && texture != Py_None) { + if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) { + PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); + return -1; + } auto pytexture = (PyTextureObject*)texture; texture_ptr = pytexture->data; } else { @@ -203,25 +185,33 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { texture_ptr = McRFPy_API::default_texture; } - // Allow creation without texture for testing purposes - // if (!texture_ptr) { - // PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available"); - // return -1; - // } - - if (grid_obj != NULL && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { + // Handle grid argument + if (grid_obj && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance"); return -1; } - // Always use default constructor for lazy initialization + // Create the entity self->data = std::make_shared(); + + // Initialize weak reference list + self->weakreflist = NULL; - // Store reference to Python object + // Register in Python object cache + if (self->data->serial_number == 0) { + self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); + PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL); + if (weakref) { + PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref); + Py_DECREF(weakref); // Cache owns the reference now + } + } + + // Store reference to Python object (legacy - to be removed) self->data->self = (PyObject*)self; Py_INCREF(self); - // TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers + // Set texture and sprite index if (texture_ptr) { self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0); } else { @@ -230,12 +220,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { } // Set position using grid coordinates - self->data->position = sf::Vector2f(grid_x, grid_y); + self->data->position = sf::Vector2f(x, y); - if (grid_obj != NULL) { + // Set other properties (delegate to sprite) + self->data->sprite.visible = visible; + self->data->sprite.opacity = opacity; + if (name) { + self->data->sprite.name = std::string(name); + } + + // Handle grid attachment + if (grid_obj) { PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj; self->data->grid = pygrid->data; - // todone - on creation of Entity with Grid assignment, also append it to the entity list + // Append entity to grid's entity list pygrid->data->entities->push_back(self->data); // Don't initialize gridstate here - lazy initialization to support large numbers of entities diff --git a/src/UIEntity.h b/src/UIEntity.h index dfd155e..fa92330 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -14,12 +14,37 @@ #include "PyFont.h" #include "UIGridPoint.h" -#include "UIDrawable.h" #include "UIBase.h" #include "UISprite.h" class UIGrid; +// UIEntity +/* + + **************************************** + * say it with me: * + * ✨ UIEntity is not a UIDrawable ✨ * + **************************************** + + **Why Not, John?** + Doesn't it say "UI" on the front of it? + It sure does. Probably should have called it "GridEntity", but it's a bit late now. + + UIDrawables have positions in **screen pixel coordinates**. Their position is an offset from their parent's position, and they are deeply nestable (Scene -> Frame -> Frame -> ...) + + However: + UIEntity has a position in **Grid tile coordinates**. + UIEntity is not nestable at all. Grid -> Entity. + UIEntity has a strict one/none relationship with a Grid: if you add it to another grid, it will have itself removed from the losing grid's collection. + UIEntity originally only allowed a single-tile sprite, but around mid-July 2025, I'm working to change that to allow any UIDrawable to go there, or multi-tile sprites. + UIEntity is, at its core, the container for *a perspective of map data*. + The Grid should contain the true, complete contents of the game's space, and the Entity should use pathfinding, field of view, and game logic to interact with the Grid's layer data. + + In Conclusion, because UIEntity cannot be drawn on a Frame or Scene, and has the unique role of serving as a filter of the data contained in a Grid, UIEntity will not become a UIDrawable. + +*/ + //class UIEntity; //typedef struct { // PyObject_HEAD @@ -32,11 +57,11 @@ sf::Vector2f PyObject_to_sfVector2f(PyObject* obj); PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state); PyObject* UIGridPointStateVector_to_PyList(const std::vector& vec); -// TODO: make UIEntity a drawable -class UIEntity//: public UIDrawable +class UIEntity { public: PyObject* self = nullptr; // Reference to the Python object (if created from Python) + uint64_t serial_number = 0; // For Python object cache std::shared_ptr grid; std::vector gridstate; UISprite sprite; @@ -44,6 +69,7 @@ public: //void render(sf::Vector2f); //override final; UIEntity(); + ~UIEntity(); // Visibility methods void updateVisibility(); // Update gridstate from current FOV @@ -88,10 +114,31 @@ namespace mcrfpydef { .tp_itemsize = 0, .tp_repr = (reprfunc)UIEntity::repr, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - .tp_doc = "UIEntity objects", + .tp_doc = PyDoc_STR("Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)\n\n" + "A game entity that exists on a grid with sprite rendering.\n\n" + "Args:\n" + " grid_pos (tuple, optional): Grid position as (x, y) tuple. Default: (0, 0)\n" + " texture (Texture, optional): Texture object for sprite. Default: default texture\n" + " sprite_index (int, optional): Index into texture atlas. Default: 0\n\n" + "Keyword Args:\n" + " grid (Grid): Grid to attach entity to. Default: None\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X grid position override. Default: 0\n" + " y (float): Y grid position override. Default: 0\n\n" + "Attributes:\n" + " pos (tuple): Grid position as (x, y) tuple\n" + " x, y (float): Grid position coordinates\n" + " draw_pos (tuple): Pixel position for rendering\n" + " gridstate (GridPointState): Visibility state for grid points\n" + " sprite_index (int): Current sprite index\n" + " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" + " name (str): Element name"), .tp_methods = UIEntity_all_methods, .tp_getset = UIEntity::getsetters, - .tp_base = &mcrfpydef::PyDrawableType, + .tp_base = NULL, .tp_init = (initproc)UIEntity::init, .tp_new = PyType_GenericNew, }; diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index aeb03bb..4ceb8b8 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -6,7 +6,7 @@ #include "UISprite.h" #include "UIGrid.h" #include "McRFPy_API.h" -#include "PyArgHelpers.h" +#include "PythonObjectCache.h" // UIDrawable methods now in UIBase.h UIDrawable* UIFrame::click_at(sf::Vector2f point) @@ -432,67 +432,50 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) // Initialize children first self->data->children = std::make_shared>>(); - // Try parsing with PyArgHelpers - int arg_idx = 0; - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); - auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx); + // Initialize weak reference list + self->weakreflist = NULL; - // Default values - float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f, outline = 0.0f; + // Define all parameters with defaults + PyObject* pos_obj = nullptr; + PyObject* size_obj = nullptr; PyObject* fill_color = nullptr; PyObject* outline_color = nullptr; + float outline = 0.0f; PyObject* children_arg = nullptr; PyObject* click_handler = nullptr; + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; + int clip_children = 0; - // Case 1: Got position and size from helpers (tuple format) - if (pos_result.valid && size_result.valid) { - x = pos_result.x; - y = pos_result.y; - w = size_result.w; - h = size_result.h; - - // Parse remaining arguments - static const char* remaining_keywords[] = { - "fill_color", "outline_color", "outline", "children", "click", nullptr - }; - - // Create new tuple with remaining args - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OOfOO", - const_cast(remaining_keywords), - &fill_color, &outline_color, &outline, - &children_arg, &click_handler)) { - Py_DECREF(remaining_args); - if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error); - else if (size_result.error) PyErr_SetString(PyExc_TypeError, size_result.error); - return -1; - } - Py_DECREF(remaining_args); + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "pos", "size", // Positional args (as per spec) + // Keyword-only args + "fill_color", "outline_color", "outline", "children", "click", + "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffi", const_cast(kwlist), + &pos_obj, &size_obj, // Positional + &fill_color, &outline_color, &outline, &children_arg, &click_handler, + &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children)) { + return -1; } - // Case 2: Traditional format (x, y, w, h, ...) - else { - PyErr_Clear(); // Clear any errors from helpers - - static const char* keywords[] = { - "x", "y", "w", "h", "fill_color", "outline_color", "outline", - "children", "click", "pos", "size", nullptr - }; - - PyObject* pos_obj = nullptr; - PyObject* size_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOOO", - const_cast(keywords), - &x, &y, &w, &h, &fill_color, &outline_color, - &outline, &children_arg, &click_handler, - &pos_obj, &size_obj)) { - return -1; - } - - // Handle pos keyword override - if (pos_obj && pos_obj != Py_None) { + + // Handle position argument (can be tuple, Vector, or use x/y keywords) + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (vec) { + x = vec->data.x; + y = vec->data.y; + Py_DECREF(vec); + } else { + PyErr_Clear(); if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { PyObject* x_val = PyTuple_GetItem(pos_obj, 0); PyObject* y_val = PyTuple_GetItem(pos_obj, 1); @@ -500,47 +483,87 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) (PyFloat_Check(y_val) || PyLong_Check(y_val))) { x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); + } else { + PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers"); + return -1; } - } else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString( - PyImport_ImportModule("mcrfpy"), "Vector"))) { - PyVectorObject* vec = (PyVectorObject*)pos_obj; - x = vec->data.x; - y = vec->data.y; } else { PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); return -1; } } - - // Handle size keyword override - if (size_obj && size_obj != Py_None) { - if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { - PyObject* w_val = PyTuple_GetItem(size_obj, 0); - PyObject* h_val = PyTuple_GetItem(size_obj, 1); - if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) && - (PyFloat_Check(h_val) || PyLong_Check(h_val))) { - w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); - h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); - } + } + // If no pos_obj but x/y keywords were provided, they're already in x, y variables + + // Handle size argument (can be tuple or use w/h keywords) + if (size_obj) { + if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { + PyObject* w_val = PyTuple_GetItem(size_obj, 0); + PyObject* h_val = PyTuple_GetItem(size_obj, 1); + if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) && + (PyFloat_Check(h_val) || PyLong_Check(h_val))) { + w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); + h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); } else { - PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)"); + PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers"); return -1; } + } else { + PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)"); + return -1; } } + // If no size_obj but w/h keywords were provided, they're already in w, h variables - self->data->position = sf::Vector2f(x, y); // Set base class position - self->data->box.setPosition(self->data->position); // Sync box position + // Set the position and size + self->data->position = sf::Vector2f(x, y); + self->data->box.setPosition(self->data->position); self->data->box.setSize(sf::Vector2f(w, h)); self->data->box.setOutlineThickness(outline); - // getsetter abuse because I haven't standardized Color object parsing (TODO) - int err_val = 0; - if (fill_color && fill_color != Py_None) err_val = UIFrame::set_color_member(self, fill_color, (void*)0); - else self->data->box.setFillColor(sf::Color(0,0,0,255)); - if (err_val) return err_val; - if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1); - else self->data->box.setOutlineColor(sf::Color(128,128,128,255)); - if (err_val) return err_val; + + // Handle fill_color + if (fill_color && fill_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(fill_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple"); + return -1; + } + self->data->box.setFillColor(color_obj->data); + Py_DECREF(color_obj); + } else { + self->data->box.setFillColor(sf::Color(0, 0, 0, 128)); // Default: semi-transparent black + } + + // Handle outline_color + if (outline_color && outline_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(outline_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple"); + return -1; + } + self->data->box.setOutlineColor(color_obj->data); + Py_DECREF(color_obj); + } else { + self->data->box.setOutlineColor(sf::Color(255, 255, 255, 255)); // Default: white + } + + // Set other properties + self->data->visible = visible; + self->data->opacity = opacity; + self->data->z_index = z_index; + self->data->clip_children = clip_children; + if (name) { + self->data->name = std::string(name); + } + + // Handle click handler + if (click_handler && click_handler != Py_None) { + if (!PyCallable_Check(click_handler)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_handler); + } // Process children argument if provided if (children_arg && children_arg != Py_None) { @@ -605,6 +628,16 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) self->data->click_register(click_handler); } + // Register in Python object cache + if (self->data->serial_number == 0) { + self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); + PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL); + if (weakref) { + PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref); + Py_DECREF(weakref); // Cache owns the reference now + } + } + return 0; } diff --git a/src/UIFrame.h b/src/UIFrame.h index 2478001..3b5e025 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -78,6 +78,10 @@ namespace mcrfpydef { .tp_dealloc = (destructor)[](PyObject* self) { PyUIFrameObject* obj = (PyUIFrameObject*)self; + // Clear weak references + if (obj->weakreflist != NULL) { + PyObject_ClearWeakRefs(self); + } obj->data.reset(); Py_TYPE(self)->tp_free(self); }, @@ -85,28 +89,39 @@ namespace mcrfpydef { //.tp_hash = NULL, //.tp_iter //.tp_iternext - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)\n\n" + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n" "A rectangular frame UI element that can contain other drawable elements.\n\n" "Args:\n" - " x (float): X position in pixels. Default: 0\n" - " y (float): Y position in pixels. Default: 0\n" - " w (float): Width in pixels. Default: 0\n" - " h (float): Height in pixels. Default: 0\n" + " pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n" + " size (tuple, optional): Size as (width, height) tuple. Default: (0, 0)\n\n" + "Keyword Args:\n" " fill_color (Color): Background fill color. Default: (0, 0, 0, 128)\n" " outline_color (Color): Border outline color. Default: (255, 255, 255, 255)\n" " outline (float): Border outline thickness. Default: 0\n" " click (callable): Click event handler. Default: None\n" - " children (list): Initial list of child drawable elements. Default: None\n\n" + " children (list): Initial list of child drawable elements. Default: None\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " z_index (int): Rendering order. Default: 0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X position override. Default: 0\n" + " y (float): Y position override. Default: 0\n" + " w (float): Width override. Default: 0\n" + " h (float): Height override. Default: 0\n" + " clip_children (bool): Whether to clip children to frame bounds. Default: False\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" " w, h (float): Size in pixels\n" + " pos (Vector): Position as a Vector object\n" " fill_color, outline_color (Color): Visual appearance\n" " outline (float): Border thickness\n" " click (callable): Click event handler\n" " children (list): Collection of child drawable elements\n" " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" + " name (str): Element name\n" " clip_children (bool): Whether to clip children to frame bounds"), .tp_methods = UIFrame_methods, //.tp_members = PyUIFrame_members, @@ -116,7 +131,10 @@ namespace mcrfpydef { .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { PyUIFrameObject* self = (PyUIFrameObject*)type->tp_alloc(type, 0); - if (self) self->data = std::make_shared(); + if (self) { + self->data = std::make_shared(); + self->weakreflist = nullptr; + } return (PyObject*)self; } }; diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index d6a109e..a282b6d 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -1,7 +1,7 @@ #include "UIGrid.h" #include "GameEngine.h" #include "McRFPy_API.h" -#include "PyArgHelpers.h" +#include "PythonObjectCache.h" #include // UIDrawable methods now in UIBase.h @@ -518,102 +518,49 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - // Default values - int grid_x = 0, grid_y = 0; - float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; + // Define all parameters with defaults + PyObject* pos_obj = nullptr; + PyObject* size_obj = nullptr; + PyObject* grid_size_obj = nullptr; PyObject* textureObj = nullptr; + PyObject* fill_color = nullptr; + PyObject* click_handler = nullptr; + float center_x = 0.0f, center_y = 0.0f; + float zoom = 1.0f; + int perspective = -1; // perspective is a difficult __init__ arg; needs an entity in collection to work + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; + int grid_x = 2, grid_y = 2; // Default to 2x2 grid - // Check if first argument is a tuple (for tuple-based initialization) - bool has_tuple_first_arg = false; - if (args && PyTuple_Size(args) > 0) { - PyObject* first_arg = PyTuple_GetItem(args, 0); - if (PyTuple_Check(first_arg)) { - has_tuple_first_arg = true; - } + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "pos", "size", "grid_size", "texture", // Positional args (as per spec) + // Keyword-only args + "fill_color", "click", "center_x", "center_y", "zoom", "perspective", + "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffiifizffffii", const_cast(kwlist), + &pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional + &fill_color, &click_handler, ¢er_x, ¢er_y, &zoom, &perspective, + &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) { + return -1; } - // Try tuple-based parsing if we have a tuple as first argument - if (has_tuple_first_arg) { - int arg_idx = 0; - auto grid_size_result = PyArgHelpers::parseGridSize(args, kwds, &arg_idx); - - // If grid size parsing failed with an error, report it - if (!grid_size_result.valid) { - if (grid_size_result.error) { - PyErr_SetString(PyExc_TypeError, grid_size_result.error); - } else { - PyErr_SetString(PyExc_TypeError, "Invalid grid size tuple"); - } - return -1; - } - - // We got a valid grid size - grid_x = grid_size_result.grid_w; - grid_y = grid_size_result.grid_h; - - // Try to parse position and size - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); - if (pos_result.valid) { - x = pos_result.x; - y = pos_result.y; - } - - auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx); - if (size_result.valid) { - w = size_result.w; - h = size_result.h; + // Handle position argument (can be tuple, Vector, or use x/y keywords) + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (vec) { + x = vec->data.x; + y = vec->data.y; + Py_DECREF(vec); } else { - // Default size based on grid dimensions - w = grid_x * 16.0f; - h = grid_y * 16.0f; - } - - // Parse remaining arguments (texture) - static const char* remaining_keywords[] = { "texture", nullptr }; - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|O", - const_cast(remaining_keywords), - &textureObj); - Py_DECREF(remaining_args); - } - // Traditional format parsing - else { - static const char* keywords[] = { - "grid_x", "grid_y", "texture", "pos", "size", "grid_size", nullptr - }; - PyObject* pos_obj = nullptr; - PyObject* size_obj = nullptr; - PyObject* grid_size_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO", - const_cast(keywords), - &grid_x, &grid_y, &textureObj, - &pos_obj, &size_obj, &grid_size_obj)) { - return -1; - } - - // Handle grid_size override - if (grid_size_obj && grid_size_obj != Py_None) { - if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) { - PyObject* x_obj = PyTuple_GetItem(grid_size_obj, 0); - PyObject* y_obj = PyTuple_GetItem(grid_size_obj, 1); - if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - grid_x = PyLong_AsLong(x_obj); - grid_y = PyLong_AsLong(y_obj); - } else { - PyErr_SetString(PyExc_TypeError, "grid_size must contain integers"); - return -1; - } - } else { - PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple of two integers"); - return -1; - } - } - - // Handle position - if (pos_obj && pos_obj != Py_None) { + PyErr_Clear(); if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { PyObject* x_val = PyTuple_GetItem(pos_obj, 0); PyObject* y_val = PyTuple_GetItem(pos_obj, 1); @@ -622,36 +569,50 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); } else { - PyErr_SetString(PyExc_TypeError, "pos must contain numbers"); + PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers"); return -1; } } else { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two numbers"); + PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); return -1; } } - - // Handle size - if (size_obj && size_obj != Py_None) { - if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { - PyObject* w_val = PyTuple_GetItem(size_obj, 0); - PyObject* h_val = PyTuple_GetItem(size_obj, 1); - if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) && - (PyFloat_Check(h_val) || PyLong_Check(h_val))) { - w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); - h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); - } else { - PyErr_SetString(PyExc_TypeError, "size must contain numbers"); - return -1; - } + } + + // Handle size argument (can be tuple or use w/h keywords) + if (size_obj) { + if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { + PyObject* w_val = PyTuple_GetItem(size_obj, 0); + PyObject* h_val = PyTuple_GetItem(size_obj, 1); + if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) && + (PyFloat_Check(h_val) || PyLong_Check(h_val))) { + w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); + h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); } else { - PyErr_SetString(PyExc_TypeError, "size must be a tuple of two numbers"); + PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers"); return -1; } } else { - // Default size based on grid - w = grid_x * 16.0f; - h = grid_y * 16.0f; + PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)"); + return -1; + } + } + + // Handle grid_size argument (can be tuple or use grid_x/grid_y keywords) + if (grid_size_obj) { + if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) { + PyObject* gx_val = PyTuple_GetItem(grid_size_obj, 0); + PyObject* gy_val = PyTuple_GetItem(grid_size_obj, 1); + if (PyLong_Check(gx_val) && PyLong_Check(gy_val)) { + grid_x = PyLong_AsLong(gx_val); + grid_y = PyLong_AsLong(gy_val); + } else { + PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers"); + return -1; + } + } else { + PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (grid_x, grid_y)"); + return -1; } } @@ -661,12 +622,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { return -1; } - // At this point we have x, y, w, h values from either parsing method - - // Convert PyObject texture to shared_ptr + // Handle texture argument std::shared_ptr texture_ptr = nullptr; - - // Allow None or NULL for texture - use default texture in that case if (textureObj && textureObj != Py_None) { if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) { PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); @@ -679,14 +636,64 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { texture_ptr = McRFPy_API::default_texture; } - // Adjust size based on texture if available and size not explicitly set - if (texture_ptr && w == grid_x * 16.0f && h == grid_y * 16.0f) { + // If size wasn't specified, calculate based on grid dimensions and texture + if (!size_obj && texture_ptr) { w = grid_x * texture_ptr->sprite_width; h = grid_y * texture_ptr->sprite_height; + } else if (!size_obj) { + w = grid_x * 16.0f; // Default tile size + h = grid_y * 16.0f; } + // Create the grid self->data = std::make_shared(grid_x, grid_y, texture_ptr, sf::Vector2f(x, y), sf::Vector2f(w, h)); + + // Set additional properties + self->data->center_x = center_x; + self->data->center_y = center_y; + self->data->zoom = zoom; + self->data->perspective = perspective; + self->data->visible = visible; + self->data->opacity = opacity; + self->data->z_index = z_index; + if (name) { + self->data->name = std::string(name); + } + + // Handle fill_color + if (fill_color && fill_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(fill_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple"); + return -1; + } + self->data->box.setFillColor(color_obj->data); + Py_DECREF(color_obj); + } + + // Handle click handler + if (click_handler && click_handler != Py_None) { + if (!PyCallable_Check(click_handler)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_handler); + } + + // Initialize weak reference list + self->weakreflist = NULL; + + // Register in Python object cache + if (self->data->serial_number == 0) { + self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); + PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL); + if (weakref) { + PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref); + Py_DECREF(weakref); // Cache owns the reference now + } + } + return 0; // Success } @@ -1401,7 +1408,15 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize std::advance(l_begin, index); auto target = *l_begin; //auto target = (*vec)[index]; - // If the entity has a stored Python object reference, return that to preserve derived class + // Check cache first to preserve derived class + if (target->serial_number != 0) { + PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number); + if (cached) { + return cached; // Already INCREF'd by lookup + } + } + + // Legacy: If the entity has a stored Python object reference, return that to preserve derived class if (target->self != nullptr) { Py_INCREF(target->self); return target->self; diff --git a/src/UIGrid.h b/src/UIGrid.h index 96f41ed..af1c078 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -172,41 +172,65 @@ namespace mcrfpydef { .tp_name = "mcrfpy.Grid", .tp_basicsize = sizeof(PyUIGridObject), .tp_itemsize = 0, - //.tp_dealloc = (destructor)[](PyObject* self) - //{ - // PyUIGridObject* obj = (PyUIGridObject*)self; - // obj->data.reset(); - // Py_TYPE(self)->tp_free(self); - //}, + .tp_dealloc = (destructor)[](PyObject* self) + { + PyUIGridObject* obj = (PyUIGridObject*)self; + // Clear weak references + if (obj->weakreflist != NULL) { + PyObject_ClearWeakRefs(self); + } + obj->data.reset(); + Py_TYPE(self)->tp_free(self); + }, //TODO - PyUIGrid REPR def: .tp_repr = (reprfunc)UIGrid::repr, //.tp_hash = NULL, //.tp_iter //.tp_iternext - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)\n\n" - "A grid-based tilemap UI element for rendering tile-based levels and game worlds.\n\n" + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n" + "A grid-based UI element for tile-based rendering and entity management.\n\n" "Args:\n" - " x (float): X position in pixels. Default: 0\n" - " y (float): Y position in pixels. Default: 0\n" - " grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)\n" - " texture (Texture): Texture atlas containing tile sprites. Default: None\n" - " tile_width (int): Width of each tile in pixels. Default: 16\n" - " tile_height (int): Height of each tile in pixels. Default: 16\n" - " scale (float): Grid scaling factor. Default: 1.0\n" - " click (callable): Click event handler. Default: None\n\n" + " pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n" + " size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size\n" + " grid_size (tuple, optional): Grid dimensions as (grid_x, grid_y) tuple. Default: (2, 2)\n" + " texture (Texture, optional): Texture containing tile sprites. Default: default texture\n\n" + "Keyword Args:\n" + " fill_color (Color): Background fill color. Default: None\n" + " click (callable): Click event handler. Default: None\n" + " center_x (float): X coordinate of center point. Default: 0\n" + " center_y (float): Y coordinate of center point. Default: 0\n" + " zoom (float): Zoom level for rendering. Default: 1.0\n" + " perspective (int): Entity perspective index (-1 for omniscient). Default: -1\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " z_index (int): Rendering order. Default: 0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X position override. Default: 0\n" + " y (float): Y position override. Default: 0\n" + " w (float): Width override. Default: auto-calculated\n" + " h (float): Height override. Default: auto-calculated\n" + " grid_x (int): Grid width override. Default: 2\n" + " grid_y (int): Grid height override. Default: 2\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" + " w, h (float): Size in pixels\n" + " pos (Vector): Position as a Vector object\n" + " size (tuple): Size as (width, height) tuple\n" + " center (tuple): Center point as (x, y) tuple\n" + " center_x, center_y (float): Center point coordinates\n" + " zoom (float): Zoom level for rendering\n" " grid_size (tuple): Grid dimensions (width, height) in tiles\n" - " tile_width, tile_height (int): Tile dimensions in pixels\n" + " grid_x, grid_y (int): Grid dimensions\n" " texture (Texture): Tile texture atlas\n" - " scale (float): Scale multiplier\n" - " points (list): 2D array of GridPoint objects for tile data\n" - " entities (list): Collection of Entity objects in the grid\n" - " background_color (Color): Grid background color\n" + " fill_color (Color): Background color\n" + " entities (EntityCollection): Collection of entities in the grid\n" + " perspective (int): Entity perspective index\n" " click (callable): Click event handler\n" " visible (bool): Visibility state\n" - " z_index (int): Rendering order"), + " opacity (float): Opacity value\n" + " z_index (int): Rendering order\n" + " name (str): Element name"), .tp_methods = UIGrid_all_methods, //.tp_members = UIGrid::members, .tp_getset = UIGrid::getsetters, diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 8daf639..4be581c 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -1,7 +1,7 @@ #include "UISprite.h" #include "GameEngine.h" #include "PyVector.h" -#include "PyArgHelpers.h" +#include "PythonObjectCache.h" // UIDrawable methods now in UIBase.h UIDrawable* UISprite::click_at(sf::Vector2f point) @@ -29,6 +29,42 @@ UISprite::UISprite(std::shared_ptr _ptex, int _sprite_index, sf::Vect sprite = ptex->sprite(sprite_index, position, sf::Vector2f(_scale, _scale)); } +UISprite::UISprite(const UISprite& other) + : UIDrawable(other), + sprite_index(other.sprite_index), + sprite(other.sprite), + ptex(other.ptex) +{ +} + +UISprite& UISprite::operator=(const UISprite& other) { + if (this != &other) { + UIDrawable::operator=(other); + sprite_index = other.sprite_index; + sprite = other.sprite; + ptex = other.ptex; + } + return *this; +} + +UISprite::UISprite(UISprite&& other) noexcept + : UIDrawable(std::move(other)), + sprite_index(other.sprite_index), + sprite(std::move(other.sprite)), + ptex(std::move(other.ptex)) +{ +} + +UISprite& UISprite::operator=(UISprite&& other) noexcept { + if (this != &other) { + UIDrawable::operator=(std::move(other)); + sprite_index = other.sprite_index; + sprite = std::move(other.sprite); + ptex = std::move(other.ptex); + } + return *this; +} + /* void UISprite::render(sf::Vector2f offset) { @@ -327,57 +363,46 @@ PyObject* UISprite::repr(PyUISpriteObject* self) int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) { - // Try parsing with PyArgHelpers - int arg_idx = 0; - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); - - // Default values - float x = 0.0f, y = 0.0f, scale = 1.0f; - int sprite_index = 0; + // Define all parameters with defaults + PyObject* pos_obj = nullptr; PyObject* texture = nullptr; + int sprite_index = 0; + float scale = 1.0f; + float scale_x = 1.0f; + float scale_y = 1.0f; PyObject* click_handler = nullptr; + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + float x = 0.0f, y = 0.0f; - // Case 1: Got position from helpers (tuple format) - if (pos_result.valid) { - x = pos_result.x; - y = pos_result.y; - - // Parse remaining arguments - static const char* remaining_keywords[] = { - "texture", "sprite_index", "scale", "click", nullptr - }; - - // Create new tuple with remaining args - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OifO", - const_cast(remaining_keywords), - &texture, &sprite_index, &scale, &click_handler)) { - Py_DECREF(remaining_args); - if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error); - return -1; - } - Py_DECREF(remaining_args); + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "pos", "texture", "sprite_index", // Positional args (as per spec) + // Keyword-only args + "scale", "scale_x", "scale_y", "click", + "visible", "opacity", "z_index", "name", "x", "y", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizff", const_cast(kwlist), + &pos_obj, &texture, &sprite_index, // Positional + &scale, &scale_x, &scale_y, &click_handler, + &visible, &opacity, &z_index, &name, &x, &y)) { + return -1; } - // Case 2: Traditional format - else { - PyErr_Clear(); // Clear any errors from helpers - - static const char* keywords[] = { - "x", "y", "texture", "sprite_index", "scale", "click", "pos", nullptr - }; - PyObject* pos_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifOO", - const_cast(keywords), - &x, &y, &texture, &sprite_index, &scale, - &click_handler, &pos_obj)) { - return -1; - } - - // Handle pos keyword override - if (pos_obj && pos_obj != Py_None) { + + // Handle position argument (can be tuple, Vector, or use x/y keywords) + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (vec) { + x = vec->data.x; + y = vec->data.y; + Py_DECREF(vec); + } else { + PyErr_Clear(); if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { PyObject* x_val = PyTuple_GetItem(pos_obj, 0); PyObject* y_val = PyTuple_GetItem(pos_obj, 1); @@ -385,12 +410,10 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) (PyFloat_Check(y_val) || PyLong_Check(y_val))) { x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); + } else { + PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers"); + return -1; } - } else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString( - PyImport_ImportModule("mcrfpy"), "Vector"))) { - PyVectorObject* vec = (PyVectorObject*)pos_obj; - x = vec->data.x; - y = vec->data.y; } else { PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); return -1; @@ -400,10 +423,11 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) // Handle texture - allow None or use default std::shared_ptr texture_ptr = nullptr; - if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){ - PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); - return -1; - } else if (texture != NULL && texture != Py_None) { + if (texture && texture != Py_None) { + if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) { + PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); + return -1; + } auto pytexture = (PyTextureObject*)texture; texture_ptr = pytexture->data; } else { @@ -416,9 +440,27 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) return -1; } + // Create the sprite self->data = std::make_shared(texture_ptr, sprite_index, sf::Vector2f(x, y), scale); + + // Set scale properties + if (scale_x != 1.0f || scale_y != 1.0f) { + // If scale_x or scale_y were explicitly set, use them + self->data->setScale(sf::Vector2f(scale_x, scale_y)); + } else if (scale != 1.0f) { + // Otherwise use uniform scale + self->data->setScale(sf::Vector2f(scale, scale)); + } + + // Set other properties + self->data->visible = visible; + self->data->opacity = opacity; + self->data->z_index = z_index; + if (name) { + self->data->name = std::string(name); + } - // Process click handler if provided + // Handle click handler if (click_handler && click_handler != Py_None) { if (!PyCallable_Check(click_handler)) { PyErr_SetString(PyExc_TypeError, "click must be callable"); @@ -427,6 +469,19 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) self->data->click_register(click_handler); } + // Initialize weak reference list + self->weakreflist = NULL; + + // Register in Python object cache + if (self->data->serial_number == 0) { + self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); + PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL); + if (weakref) { + PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref); + Py_DECREF(weakref); // Cache owns the reference now + } + } + return 0; } diff --git a/src/UISprite.h b/src/UISprite.h index 5e18ade..9e99d25 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -25,6 +25,14 @@ protected: public: UISprite(); UISprite(std::shared_ptr, int, sf::Vector2f, float); + + // Copy constructor and assignment operator + UISprite(const UISprite& other); + UISprite& operator=(const UISprite& other); + + // Move constructor and assignment operator + UISprite(UISprite&& other) noexcept; + UISprite& operator=(UISprite&& other) noexcept; void update(); void render(sf::Vector2f, sf::RenderTarget&) override final; virtual UIDrawable* click_at(sf::Vector2f point) override final; @@ -82,6 +90,10 @@ namespace mcrfpydef { .tp_dealloc = (destructor)[](PyObject* self) { PyUISpriteObject* obj = (PyUISpriteObject*)self; + // Clear weak references + if (obj->weakreflist != NULL) { + PyObject_ClearWeakRefs(self); + } // release reference to font object //if (obj->texture) Py_DECREF(obj->texture); obj->data.reset(); @@ -91,24 +103,36 @@ namespace mcrfpydef { //.tp_hash = NULL, //.tp_iter //.tp_iternext - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)\n\n" + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n" "A sprite UI element that displays a texture or portion of a texture atlas.\n\n" "Args:\n" - " x (float): X position in pixels. Default: 0\n" - " y (float): Y position in pixels. Default: 0\n" - " texture (Texture): Texture object to display. Default: None\n" - " sprite_index (int): Index into texture atlas (if applicable). Default: 0\n" - " scale (float): Sprite scaling factor. Default: 1.0\n" - " click (callable): Click event handler. Default: None\n\n" + " pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n" + " texture (Texture, optional): Texture object to display. Default: default texture\n" + " sprite_index (int, optional): Index into texture atlas. Default: 0\n\n" + "Keyword Args:\n" + " scale (float): Uniform scale factor. Default: 1.0\n" + " scale_x (float): Horizontal scale factor. Default: 1.0\n" + " scale_y (float): Vertical scale factor. Default: 1.0\n" + " click (callable): Click event handler. Default: None\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " z_index (int): Rendering order. Default: 0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X position override. Default: 0\n" + " y (float): Y position override. Default: 0\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" + " pos (Vector): Position as a Vector object\n" " texture (Texture): The texture being displayed\n" " sprite_index (int): Current sprite index in texture atlas\n" - " scale (float): Scale multiplier\n" + " scale (float): Uniform scale factor\n" + " scale_x, scale_y (float): Individual scale factors\n" " click (callable): Click event handler\n" " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" + " name (str): Element name\n" " w, h (float): Read-only computed size based on texture and scale"), .tp_methods = UISprite_methods, //.tp_members = PyUIFrame_members, @@ -118,7 +142,10 @@ namespace mcrfpydef { .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { PyUISpriteObject* self = (PyUISpriteObject*)type->tp_alloc(type, 0); - //if (self) self->data = std::make_shared(); + if (self) { + self->data = std::make_shared(); + self->weakreflist = nullptr; + } return (PyObject*)self; } }; diff --git a/src/scripts/cos_entities.py b/src/scripts/cos_entities.py index 6b8ff59..6519630 100644 --- a/src/scripts/cos_entities.py +++ b/src/scripts/cos_entities.py @@ -67,10 +67,10 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu self.draw_pos = (tx, ty) for e in self.game.entities: if e is self: continue - if e.draw_pos == old_pos: e.ev_exit(self) + if e.draw_pos.x == old_pos.x and e.draw_pos.y == old_pos.y: e.ev_exit(self) for e in self.game.entities: if e is self: continue - if e.draw_pos == (tx, ty): e.ev_enter(self) + if e.draw_pos.x == tx and e.draw_pos.y == ty: e.ev_enter(self) def act(self): pass @@ -83,12 +83,12 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu def try_move(self, dx, dy, test=False): x_max, y_max = self.grid.grid_size - tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy) #for e in iterable_entities(self.grid): # sorting entities to test against the boulder instead of the button when they overlap. for e in sorted(self.game.entities, key = lambda i: i.draw_order, reverse = True): - if e.draw_pos == (tx, ty): + if e.draw_pos.x == tx and e.draw_pos.y == ty: #print(f"bumping {e}") return e.bump(self, dx, dy) @@ -106,7 +106,7 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu return False def _relative_move(self, dx, dy): - tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy) #self.draw_pos = (tx, ty) self.do_move(tx, ty) @@ -181,7 +181,7 @@ class Equippable: if self.zap_cooldown_remaining != 0: print("zap is cooling down.") return False - fx, fy = caster.draw_pos + fx, fy = caster.draw_pos.x, caster.draw_pos.y x, y = int(fx), int (fy) dist = lambda tx, ty: abs(int(tx) - x) + abs(int(ty) - y) targets = [] @@ -293,7 +293,7 @@ class PlayerEntity(COSEntity): ## TODO - find other entities to avoid spawning on top of for spawn in spawn_points: for e in avoid or []: - if e.draw_pos == spawn: break + if e.draw_pos.x == spawn[0] and e.draw_pos.y == spawn[1]: break else: break self.draw_pos = spawn @@ -314,9 +314,9 @@ class BoulderEntity(COSEntity): elif type(other) == EnemyEntity: if not other.can_push: return False #tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) - tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy) # Is the boulder blocked the same direction as the bumper? If not, let's both move - old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + old_pos = int(self.draw_pos.x), int(self.draw_pos.y) if self.try_move(dx, dy, test=test): if not test: other.do_move(*old_pos) @@ -342,7 +342,7 @@ class ButtonEntity(COSEntity): # self.exit.unlock() # TODO: unlock, and then lock again, when player steps on/off if not test: - pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + pos = int(self.draw_pos.x), int(self.draw_pos.y) other.do_move(*pos) return True @@ -393,7 +393,7 @@ class EnemyEntity(COSEntity): def bump(self, other, dx, dy, test=False): if self.hp == 0: if not test: - old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + old_pos = int(self.draw_pos.x), int(self.draw_pos.y) other.do_move(*old_pos) return True if type(other) == PlayerEntity: @@ -415,7 +415,7 @@ class EnemyEntity(COSEntity): print("Ouch, my entire body!!") self._entity.sprite_number = self.base_sprite + 246 self.hp = 0 - old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + old_pos = int(self.draw_pos.x), int(self.draw_pos.y) if not test: other.do_move(*old_pos) return True @@ -423,8 +423,8 @@ class EnemyEntity(COSEntity): def act(self): if self.hp > 0: # if player nearby: attack - x, y = self.draw_pos - px, py = self.game.player.draw_pos + x, y = self.draw_pos.x, self.draw_pos.y + px, py = self.game.player.draw_pos.x, self.game.player.draw_pos.y for d in ((1, 0), (0, 1), (-1, 0), (1, 0)): if int(x + d[0]) == int(px) and int(y + d[1]) == int(py): self.try_move(*d) diff --git a/src/scripts/cos_tiles.py b/src/scripts/cos_tiles.py index 4b80785..079516f 100644 --- a/src/scripts/cos_tiles.py +++ b/src/scripts/cos_tiles.py @@ -22,12 +22,13 @@ class TileInfo: @staticmethod def from_grid(grid, xy:tuple): values = {} + x_max, y_max = grid.grid_size for d in deltas: tx, ty = d[0] + xy[0], d[1] + xy[1] - try: - values[d] = grid.at((tx, ty)).walkable - except ValueError: + if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max: values[d] = True + else: + values[d] = grid.at((tx, ty)).walkable return TileInfo(values) @staticmethod @@ -70,10 +71,10 @@ def special_rule_verify(rule, grid, xy, unverified_tiles, pass_unverified=False) tx, ty = xy[0] + dxy[0], xy[1] + dxy[1] #print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}") if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified - try: - return grid.at((tx, ty)).tilesprite == allowed_tile - except ValueError: + x_max, y_max = grid.grid_size + if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max: return False + return grid.at((tx, ty)).tilesprite == allowed_tile import random tile_of_last_resort = 431 diff --git a/src/scripts/game.py b/src/scripts/game.py index 8bee8c9..0a7b6e4 100644 --- a/src/scripts/game.py +++ b/src/scripts/game.py @@ -87,7 +87,7 @@ class Crypt: # Side Bar (inventory, level info) config self.level_caption = mcrfpy.Caption((5,5), "Level: 1", font, fill_color=(255, 255, 255)) - self.level_caption.size = 26 + self.level_caption.font_size = 26 self.level_caption.outline = 3 self.level_caption.outline_color = (0, 0, 0) self.sidebar.children.append(self.level_caption) @@ -103,7 +103,7 @@ class Crypt: mcrfpy.Caption((25, 130 + 95 * i), "x", font, fill_color=(255, 255, 255)) for i in range(5) ] for i in self.inv_captions: - i.size = 16 + i.font_size = 16 self.sidebar.children.append(i) liminal_void = mcrfpy.Grid(1, 1, t, (0, 0), (16, 16)) @@ -382,7 +382,7 @@ class Crypt: def pull_boulder_search(self): for dx, dy in ( (0, -1), (-1, 0), (1, 0), (0, 1) ): for e in self.entities: - if e.draw_pos != (self.player.draw_pos[0] + dx, self.player.draw_pos[1] + dy): continue + if e.draw_pos.x != self.player.draw_pos.x + dx or e.draw_pos.y != self.player.draw_pos.y + dy: continue if type(e) == ce.BoulderEntity: self.pull_boulder_move((dx, dy), e) return self.enemy_turn() @@ -395,7 +395,7 @@ class Crypt: if self.player.try_move(-p[0], -p[1], test=True): old_pos = self.player.draw_pos self.player.try_move(-p[0], -p[1]) - target_boulder.do_move(*old_pos) + target_boulder.do_move(old_pos.x, old_pos.y) def swap_level(self, new_level, spawn_point): self.level = new_level @@ -451,7 +451,7 @@ class SweetButton: # main button caption self.caption = mcrfpy.Caption((40, 3), caption, font, fill_color=font_color) - self.caption.size = font_size + self.caption.font_size = font_size self.caption.outline_color=font_outline_color self.caption.outline=font_outline_width self.main_button.children.append(self.caption) @@ -548,20 +548,20 @@ class MainMenu: # title text drop_shadow = mcrfpy.Caption((150, 10), "Crypt Of Sokoban", font, fill_color=(96, 96, 96), outline_color=(192, 0, 0)) drop_shadow.outline = 3 - drop_shadow.size = 64 + drop_shadow.font_size = 64 components.append( drop_shadow ) title_txt = mcrfpy.Caption((158, 18), "Crypt Of Sokoban", font, fill_color=(255, 255, 255)) - title_txt.size = 64 + title_txt.font_size = 64 components.append( title_txt ) # toast: text over the demo grid that fades out on a timer self.toast = mcrfpy.Caption((150, 400), "", font, fill_color=(0, 0, 0)) - self.toast.size = 28 + self.toast.font_size = 28 self.toast.outline = 2 self.toast.outline_color = (255, 255, 255) self.toast_event = None @@ -626,6 +626,7 @@ class MainMenu: def play(self, sweet_btn, args): #if args[3] == "start": return # DRAMATIC on release action! if args[3] == "end": return + mcrfpy.delTimer("demo_motion") # Clean up the demo timer self.crypt = Crypt() #mcrfpy.setScene("play") self.crypt.start() diff --git a/tests/generate_caption_screenshot_fixed.py b/tests/archive/generate_caption_screenshot_fixed.py similarity index 100% rename from tests/generate_caption_screenshot_fixed.py rename to tests/archive/generate_caption_screenshot_fixed.py diff --git a/tests/generate_docs_screenshots_simple.py b/tests/archive/generate_docs_screenshots_simple.py similarity index 100% rename from tests/generate_docs_screenshots_simple.py rename to tests/archive/generate_docs_screenshots_simple.py diff --git a/tests/generate_entity_screenshot_fixed.py b/tests/archive/generate_entity_screenshot_fixed.py similarity index 100% rename from tests/generate_entity_screenshot_fixed.py rename to tests/archive/generate_entity_screenshot_fixed.py diff --git a/tests/path_vision_fixed.py b/tests/archive/path_vision_fixed.py similarity index 100% rename from tests/path_vision_fixed.py rename to tests/archive/path_vision_fixed.py diff --git a/tests/ui_Grid_test_simple.py b/tests/archive/ui_Grid_test_simple.py similarity index 100% rename from tests/ui_Grid_test_simple.py rename to tests/archive/ui_Grid_test_simple.py diff --git a/tests/automation_click_issue78_analysis.py b/tests/automation/automation_click_issue78_analysis.py similarity index 100% rename from tests/automation_click_issue78_analysis.py rename to tests/automation/automation_click_issue78_analysis.py diff --git a/tests/automation_click_issue78_test.py b/tests/automation/automation_click_issue78_test.py similarity index 100% rename from tests/automation_click_issue78_test.py rename to tests/automation/automation_click_issue78_test.py diff --git a/tests/automation_screenshot_test.py b/tests/automation/automation_screenshot_test.py similarity index 100% rename from tests/automation_screenshot_test.py rename to tests/automation/automation_screenshot_test.py diff --git a/tests/automation_screenshot_test_simple.py b/tests/automation/automation_screenshot_test_simple.py similarity index 100% rename from tests/automation_screenshot_test_simple.py rename to tests/automation/automation_screenshot_test_simple.py diff --git a/tests/issue_12_gridpoint_instantiation_test.py b/tests/bugs/issue_12_gridpoint_instantiation_test.py similarity index 100% rename from tests/issue_12_gridpoint_instantiation_test.py rename to tests/bugs/issue_12_gridpoint_instantiation_test.py diff --git a/tests/issue_26_28_iterator_comprehensive_test.py b/tests/bugs/issue_26_28_iterator_comprehensive_test.py similarity index 100% rename from tests/issue_26_28_iterator_comprehensive_test.py rename to tests/bugs/issue_26_28_iterator_comprehensive_test.py diff --git a/tests/issue_37_simple_test.py b/tests/bugs/issue_37_simple_test.py similarity index 100% rename from tests/issue_37_simple_test.py rename to tests/bugs/issue_37_simple_test.py diff --git a/tests/issue_37_test.py b/tests/bugs/issue_37_test.py similarity index 100% rename from tests/issue_37_test.py rename to tests/bugs/issue_37_test.py diff --git a/tests/issue_37_windows_scripts_comprehensive_test.py b/tests/bugs/issue_37_windows_scripts_comprehensive_test.py similarity index 100% rename from tests/issue_37_windows_scripts_comprehensive_test.py rename to tests/bugs/issue_37_windows_scripts_comprehensive_test.py diff --git a/tests/issue_76_test.py b/tests/bugs/issue_76_test.py similarity index 100% rename from tests/issue_76_test.py rename to tests/bugs/issue_76_test.py diff --git a/tests/issue_76_uientitycollection_type_test.py b/tests/bugs/issue_76_uientitycollection_type_test.py similarity index 100% rename from tests/issue_76_uientitycollection_type_test.py rename to tests/bugs/issue_76_uientitycollection_type_test.py diff --git a/tests/issue_79_color_properties_test.py b/tests/bugs/issue_79_color_properties_test.py similarity index 100% rename from tests/issue_79_color_properties_test.py rename to tests/bugs/issue_79_color_properties_test.py diff --git a/tests/issue_80_caption_font_size_test.py b/tests/bugs/issue_80_caption_font_size_test.py similarity index 100% rename from tests/issue_80_caption_font_size_test.py rename to tests/bugs/issue_80_caption_font_size_test.py diff --git a/tests/issue_81_sprite_index_standardization_test.py b/tests/bugs/issue_81_sprite_index_standardization_test.py similarity index 100% rename from tests/issue_81_sprite_index_standardization_test.py rename to tests/bugs/issue_81_sprite_index_standardization_test.py diff --git a/tests/issue_82_sprite_scale_xy_test.py b/tests/bugs/issue_82_sprite_scale_xy_test.py similarity index 100% rename from tests/issue_82_sprite_scale_xy_test.py rename to tests/bugs/issue_82_sprite_scale_xy_test.py diff --git a/tests/issue_83_position_tuple_test.py b/tests/bugs/issue_83_position_tuple_test.py similarity index 100% rename from tests/issue_83_position_tuple_test.py rename to tests/bugs/issue_83_position_tuple_test.py diff --git a/tests/issue_84_pos_property_test.py b/tests/bugs/issue_84_pos_property_test.py similarity index 100% rename from tests/issue_84_pos_property_test.py rename to tests/bugs/issue_84_pos_property_test.py diff --git a/tests/issue_95_uicollection_repr_test.py b/tests/bugs/issue_95_uicollection_repr_test.py similarity index 100% rename from tests/issue_95_uicollection_repr_test.py rename to tests/bugs/issue_95_uicollection_repr_test.py diff --git a/tests/issue_96_uicollection_extend_test.py b/tests/bugs/issue_96_uicollection_extend_test.py similarity index 100% rename from tests/issue_96_uicollection_extend_test.py rename to tests/bugs/issue_96_uicollection_extend_test.py diff --git a/tests/issue_99_texture_font_properties_test.py b/tests/bugs/issue_99_texture_font_properties_test.py similarity index 100% rename from tests/issue_99_texture_font_properties_test.py rename to tests/bugs/issue_99_texture_font_properties_test.py diff --git a/tests/issue_9_minimal_test.py b/tests/bugs/issue_9_minimal_test.py similarity index 100% rename from tests/issue_9_minimal_test.py rename to tests/bugs/issue_9_minimal_test.py diff --git a/tests/issue_9_rendertexture_resize_test.py b/tests/bugs/issue_9_rendertexture_resize_test.py similarity index 100% rename from tests/issue_9_rendertexture_resize_test.py rename to tests/bugs/issue_9_rendertexture_resize_test.py diff --git a/tests/issue_9_simple_test.py b/tests/bugs/issue_9_simple_test.py similarity index 100% rename from tests/issue_9_simple_test.py rename to tests/bugs/issue_9_simple_test.py diff --git a/tests/issue_9_test.py b/tests/bugs/issue_9_test.py similarity index 100% rename from tests/issue_9_test.py rename to tests/bugs/issue_9_test.py diff --git a/tests/constructor_audit.py b/tests/constructor_audit.py new file mode 100644 index 0000000..c395c24 --- /dev/null +++ b/tests/constructor_audit.py @@ -0,0 +1,215 @@ +#!/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 new file mode 100644 index 0000000..6dd3d12 --- /dev/null +++ b/tests/count_format_string.py @@ -0,0 +1,30 @@ +#!/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_animation_callback_usage.py b/tests/demo_animation_callback_usage.py new file mode 100644 index 0000000..7cd019a --- /dev/null +++ b/tests/demo_animation_callback_usage.py @@ -0,0 +1,81 @@ +#!/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/animation_demo.py b/tests/demos/animation_demo.py similarity index 100% rename from tests/animation_demo.py rename to tests/demos/animation_demo.py diff --git a/tests/demos/animation_demo_safe.py b/tests/demos/animation_demo_safe.py new file mode 100644 index 0000000..16f7445 --- /dev/null +++ b/tests/demos/animation_demo_safe.py @@ -0,0 +1,146 @@ +#!/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 new file mode 100644 index 0000000..15c2e7c --- /dev/null +++ b/tests/demos/animation_sizzle_reel.py @@ -0,0 +1,616 @@ +#!/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 new file mode 100644 index 0000000..b9c0e2e --- /dev/null +++ b/tests/demos/animation_sizzle_reel_fixed.py @@ -0,0 +1,227 @@ +#!/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 new file mode 100644 index 0000000..2a43236 --- /dev/null +++ b/tests/demos/animation_sizzle_reel_v2.py @@ -0,0 +1,307 @@ +#!/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 new file mode 100644 index 0000000..bb2f7af --- /dev/null +++ b/tests/demos/animation_sizzle_reel_working.py @@ -0,0 +1,316 @@ +#!/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 new file mode 100644 index 0000000..10a8852 --- /dev/null +++ b/tests/demos/api_demo_final.py @@ -0,0 +1,207 @@ +#!/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 new file mode 100644 index 0000000..3c26d3c --- /dev/null +++ b/tests/demos/debug_astar_demo.py @@ -0,0 +1,99 @@ +#!/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 new file mode 100644 index 0000000..91efc51 --- /dev/null +++ b/tests/demos/dijkstra_demo_working.py @@ -0,0 +1,137 @@ +#!/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 new file mode 100644 index 0000000..2b7bd40 --- /dev/null +++ b/tests/demos/exhaustive_api_demo_fixed.py @@ -0,0 +1,306 @@ +#!/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 new file mode 100644 index 0000000..b067b6c --- /dev/null +++ b/tests/demos/path_vision_sizzle_reel.py @@ -0,0 +1,391 @@ +#!/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 new file mode 100644 index 0000000..31b9f37 --- /dev/null +++ b/tests/demos/pathfinding_showcase.py @@ -0,0 +1,377 @@ +#!/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 new file mode 100644 index 0000000..ad11509 --- /dev/null +++ b/tests/demos/simple_text_input.py @@ -0,0 +1,226 @@ +#!/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 new file mode 100644 index 0000000..94ac610 --- /dev/null +++ b/tests/demos/sizzle_reel_final.py @@ -0,0 +1,190 @@ +#!/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 new file mode 100644 index 0000000..0ecf99a --- /dev/null +++ b/tests/demos/sizzle_reel_final_fixed.py @@ -0,0 +1,193 @@ +#!/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 new file mode 100644 index 0000000..51538bb --- /dev/null +++ b/tests/demos/text_input_demo.py @@ -0,0 +1,149 @@ +#!/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 new file mode 100644 index 0000000..2bcf7d8 --- /dev/null +++ b/tests/demos/text_input_standalone.py @@ -0,0 +1,320 @@ +#!/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 new file mode 100644 index 0000000..adbd201 --- /dev/null +++ b/tests/demos/text_input_widget.py @@ -0,0 +1,320 @@ +#!/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/integration/astar_vs_dijkstra.py b/tests/integration/astar_vs_dijkstra.py new file mode 100644 index 0000000..5b93c99 --- /dev/null +++ b/tests/integration/astar_vs_dijkstra.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +""" +A* vs Dijkstra Visual Comparison +================================= + +Shows the difference between A* (single target) and Dijkstra (multi-target). +""" + +import mcrfpy +import sys + +# Colors +WALL_COLOR = mcrfpy.Color(40, 20, 20) +FLOOR_COLOR = mcrfpy.Color(60, 60, 80) +ASTAR_COLOR = mcrfpy.Color(0, 255, 0) # Green for A* +DIJKSTRA_COLOR = mcrfpy.Color(0, 150, 255) # Blue for Dijkstra +START_COLOR = mcrfpy.Color(255, 100, 100) # Red for start +END_COLOR = mcrfpy.Color(255, 255, 100) # Yellow for end + +# Global state +grid = None +mode = "ASTAR" +start_pos = (5, 10) +end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall + +def create_map(): + """Create a map with obstacles to show pathfinding differences""" + global grid + + mcrfpy.createScene("pathfinding_comparison") + + # Create grid + 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).color = FLOOR_COLOR + + # Create obstacles that make A* and Dijkstra differ + 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)], + ] + + for obstacle_group in obstacles: + for x, y in obstacle_group: + grid.at(x, y).walkable = False + grid.at(x, y).color = WALL_COLOR + + # Mark start and end + grid.at(start_pos[0], start_pos[1]).color = START_COLOR + grid.at(end_pos[0], end_pos[1]).color = END_COLOR + +def clear_paths(): + """Clear path highlighting""" + for y in range(20): + for x in range(30): + cell = grid.at(x, y) + if cell.walkable: + cell.color = FLOOR_COLOR + + # Restore start and end colors + grid.at(start_pos[0], start_pos[1]).color = START_COLOR + grid.at(end_pos[0], end_pos[1]).color = END_COLOR + +def show_astar(): + """Show A* path""" + clear_paths() + + # Compute A* path + path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) + + # Color the path + for i, (x, y) in enumerate(path): + if (x, y) != start_pos and (x, y) != end_pos: + grid.at(x, y).color = ASTAR_COLOR + + status_text.text = f"A* Path: {len(path)} steps (optimized for single target)" + status_text.fill_color = ASTAR_COLOR + +def show_dijkstra(): + """Show Dijkstra exploration""" + clear_paths() + + # Compute Dijkstra from start + grid.compute_dijkstra(start_pos[0], start_pos[1]) + + # Color cells by distance (showing exploration) + max_dist = 40.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: + # Color based on distance + intensity = int(255 * (1 - dist / max_dist)) + grid.at(x, y).color = mcrfpy.Color(0, intensity // 2, intensity) + + # Get the actual path + path = grid.get_dijkstra_path(end_pos[0], end_pos[1]) + + # Highlight the actual path more brightly + for x, y in path: + if (x, y) != start_pos and (x, y) != end_pos: + grid.at(x, y).color = DIJKSTRA_COLOR + + # Restore start and end + grid.at(start_pos[0], start_pos[1]).color = START_COLOR + grid.at(end_pos[0], end_pos[1]).color = END_COLOR + + status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)" + status_text.fill_color = DIJKSTRA_COLOR + +def show_both(): + """Show both paths overlaid""" + clear_paths() + + # Get both paths + astar_path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) + grid.compute_dijkstra(start_pos[0], start_pos[1]) + dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1]) + + print(astar_path, dijkstra_path) + + # Color Dijkstra path first (blue) + for x, y in dijkstra_path: + if (x, y) != start_pos and (x, y) != end_pos: + grid.at(x, y).color = DIJKSTRA_COLOR + + # Then A* path (green) - will overwrite shared cells + for x, y in astar_path: + if (x, y) != start_pos and (x, y) != end_pos: + grid.at(x, y).color = ASTAR_COLOR + + # Mark differences + different_cells = [] + for cell in dijkstra_path: + if cell not in astar_path: + different_cells.append(cell) + + status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps" + if different_cells: + info_text.text = f"Paths differ at {len(different_cells)} cells" + else: + info_text.text = "Paths are identical" + +def handle_keypress(key_str, state): + """Handle keyboard input""" + global mode + if state == "end": return + print(key_str) + if key_str == "Esc" or key_str == "Q": + print("\nExiting...") + sys.exit(0) + elif key_str == "A" or key_str == "1": + mode = "ASTAR" + show_astar() + elif key_str == "D" or key_str == "2": + mode = "DIJKSTRA" + show_dijkstra() + elif key_str == "B" or key_str == "3": + mode = "BOTH" + show_both() + elif key_str == "Space": + # Refresh current mode + if mode == "ASTAR": + show_astar() + elif mode == "DIJKSTRA": + show_dijkstra() + else: + show_both() + +# Create the demo +print("A* vs Dijkstra Pathfinding Comparison") +print("=====================================") +print("Controls:") +print(" A or 1 - Show A* path (green)") +print(" D or 2 - Show Dijkstra (blue gradient)") +print(" B or 3 - Show both paths") +print(" Q/ESC - Quit") +print() +print("A* is optimized for single-target pathfinding") +print("Dijkstra explores in all directions (good for multiple targets)") + +create_map() + +# Set up UI +ui = mcrfpy.sceneUI("pathfinding_comparison") +ui.append(grid) + +# Scale and position +grid.size = (600, 400) # 30*20, 20*20 +grid.position = (100, 100) + +# Add title +title = mcrfpy.Caption("A* vs Dijkstra Pathfinding", 250, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status +status_text = mcrfpy.Caption("Press A for A*, D for Dijkstra, B for Both", 100, 60) +status_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(status_text) + +# Add info +info_text = mcrfpy.Caption("", 100, 520) +info_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info_text) + +# Add legend +legend1 = mcrfpy.Caption("Red=Start, Yellow=End, Green=A*, Blue=Dijkstra", 100, 540) +legend1.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend1) + +legend2 = mcrfpy.Caption("Dark=Walls, Light=Floor", 100, 560) +legend2.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend2) + +# Set scene and input +mcrfpy.setScene("pathfinding_comparison") +mcrfpy.keypressScene(handle_keypress) + +# Show initial A* path +show_astar() + +print("\nDemo ready!") diff --git a/tests/integration/debug_visibility.py b/tests/integration/debug_visibility.py new file mode 100644 index 0000000..da0bd60 --- /dev/null +++ b/tests/integration/debug_visibility.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Debug visibility crash""" + +import mcrfpy +import sys + +print("Debug visibility...") + +# Create scene and grid +mcrfpy.createScene("debug") +grid = mcrfpy.Grid(grid_x=5, grid_y=5) + +# Initialize grid +print("Initializing grid...") +for y in range(5): + for x in range(5): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + +# Create entity +print("Creating entity...") +entity = mcrfpy.Entity(2, 2) +entity.sprite_index = 64 +grid.entities.append(entity) +print(f"Entity at ({entity.x}, {entity.y})") + +# Check gridstate +print(f"\nGridstate length: {len(entity.gridstate)}") +print(f"Expected: {5 * 5}") + +# Try to access gridstate +print("\nChecking gridstate access...") +try: + if len(entity.gridstate) > 0: + state = entity.gridstate[0] + print(f"First state: visible={state.visible}, discovered={state.discovered}") +except Exception as e: + print(f"Error accessing gridstate: {e}") + +# Try update_visibility +print("\nTrying update_visibility...") +try: + entity.update_visibility() + print("update_visibility succeeded") +except Exception as e: + print(f"Error in update_visibility: {e}") + +# Try perspective +print("\nTesting perspective...") +print(f"Initial perspective: {grid.perspective}") +try: + grid.perspective = 0 + print(f"Set perspective to 0: {grid.perspective}") +except Exception as e: + print(f"Error setting perspective: {e}") + +print("\nTest complete") +sys.exit(0) \ No newline at end of file diff --git a/tests/integration/dijkstra_all_paths.py b/tests/integration/dijkstra_all_paths.py new file mode 100644 index 0000000..e205f08 --- /dev/null +++ b/tests/integration/dijkstra_all_paths.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +""" +Dijkstra Demo - Shows ALL Path Combinations (Including Invalid) +=============================================================== + +Cycles through every possible entity pair to demonstrate both +valid paths and properly handled invalid paths (empty lists). +""" + +import mcrfpy +import sys + +# High contrast colors +WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown +FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray +PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green +START_COLOR = mcrfpy.Color(255, 100, 100) # Light red +END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue +NO_PATH_COLOR = mcrfpy.Color(255, 0, 0) # Pure red for unreachable + +# Global state +grid = None +entities = [] +current_combo_index = 0 +all_combinations = [] # All possible pairs +current_path = [] + +def create_map(): + """Create the map with entities""" + global grid, entities, all_combinations + + mcrfpy.createScene("dijkstra_all") + + # Create grid + grid = mcrfpy.Grid(grid_x=14, grid_y=10) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Map layout - Entity 1 is intentionally trapped! + map_layout = [ + "..............", # Row 0 + "..W.....WWWW..", # Row 1 + "..W.W...W.EW..", # Row 2 - Entity 1 TRAPPED at (10,2) + "..W.....W..W..", # Row 3 + "..W...E.WWWW..", # Row 4 - Entity 2 at (6,4) + "E.W...........", # Row 5 - Entity 3 at (0,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)) + + # 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("Map Analysis:") + print("=============") + for i, (x, y) in enumerate(entity_positions): + print(f"Entity {i+1} at ({x}, {y})") + + # Generate ALL combinations (including invalid ones) + all_combinations = [] + for i in range(len(entities)): + for j in range(len(entities)): + if i != j: # Skip self-paths + all_combinations.append((i, j)) + + print(f"\nTotal path combinations to test: {len(all_combinations)}") + +def clear_path_colors(): + """Reset all floor tiles to original color""" + global current_path + + for y in range(grid.grid_y): + for x in range(grid.grid_x): + cell = grid.at(x, y) + if cell.walkable: + cell.color = FLOOR_COLOR + + current_path = [] + +def show_combination(index): + """Show a specific path combination (valid or invalid)""" + global current_combo_index, current_path + + current_combo_index = index % len(all_combinations) + from_idx, to_idx = all_combinations[current_combo_index] + + # Clear previous path + clear_path_colors() + + # Get entities + e_from = entities[from_idx] + e_to = entities[to_idx] + + # Calculate path + path = e_from.path_to(int(e_to.x), int(e_to.y)) + current_path = path if path else [] + + # Always color start and end positions + grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR + grid.at(int(e_to.x), int(e_to.y)).color = NO_PATH_COLOR if not path else END_COLOR + + # Color the path if it exists + if path: + # Color intermediate steps + for i, (x, y) in enumerate(path): + if i > 0 and i < len(path) - 1: + grid.at(x, y).color = PATH_COLOR + + status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = {len(path)} steps" + status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green for valid + + # Show path steps + path_display = [] + for i, (x, y) in enumerate(path[:5]): + path_display.append(f"({x},{y})") + if len(path) > 5: + path_display.append("...") + path_text.text = "Path: " + " → ".join(path_display) + else: + status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = NO PATH!" + status_text.fill_color = mcrfpy.Color(255, 100, 100) # Red for invalid + path_text.text = "Path: [] (No valid path exists)" + + # Update info + info_text.text = f"From: Entity {from_idx+1} at ({int(e_from.x)}, {int(e_from.y)}) | To: Entity {to_idx+1} at ({int(e_to.x)}, {int(e_to.y)})" + +def handle_keypress(key_str, state): + """Handle keyboard input""" + global current_combo_index + if state == "end": return + + if key_str == "Esc" or key_str == "Q": + print("\nExiting...") + sys.exit(0) + elif key_str == "Space" or key_str == "N": + show_combination(current_combo_index + 1) + elif key_str == "P": + show_combination(current_combo_index - 1) + elif key_str == "R": + show_combination(current_combo_index) + elif key_str in "123456": + combo_num = int(key_str) - 1 # 0-based index + if combo_num < len(all_combinations): + show_combination(combo_num) + +# Create the demo +print("Dijkstra All Paths Demo") +print("=======================") +print("Shows ALL path combinations including invalid ones") +print("Entity 1 is trapped - paths to/from it will be empty!") +print() + +create_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_all") +ui.append(grid) + +# Scale and position +grid.size = (560, 400) +grid.position = (120, 100) + +# Add title +title = mcrfpy.Caption("Dijkstra - All Paths (Valid & Invalid)", 200, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status (will change color based on validity) +status_text = mcrfpy.Caption("Ready", 120, 60) +status_text.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(status_text) + +# Add info +info_text = mcrfpy.Caption("", 120, 80) +info_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info_text) + +# Add path display +path_text = mcrfpy.Caption("Path: None", 120, 520) +path_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(path_text) + +# Add controls +controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit", 120, 540) +controls.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(controls) + +# Add legend +legend = mcrfpy.Caption("Red Start→Blue End (valid) | Red Start→Red End (invalid)", 120, 560) +legend.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend) + +# Expected results info +expected = mcrfpy.Caption("Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail", 120, 580) +expected.fill_color = mcrfpy.Color(255, 150, 150) +ui.append(expected) + +# Set scene first, then set up input handler +mcrfpy.setScene("dijkstra_all") +mcrfpy.keypressScene(handle_keypress) + +# Show first combination +show_combination(0) + +print("\nDemo ready!") +print("Expected results:") +print(" Path 1: Entity 1→2 = NO PATH (Entity 1 is trapped)") +print(" Path 2: Entity 1→3 = NO PATH (Entity 1 is trapped)") +print(" Path 3: Entity 2→1 = NO PATH (Entity 1 is trapped)") +print(" Path 4: Entity 2→3 = Valid path") +print(" Path 5: Entity 3→1 = NO PATH (Entity 1 is trapped)") +print(" Path 6: Entity 3→2 = Valid path") \ No newline at end of file diff --git a/tests/integration/dijkstra_cycle_paths.py b/tests/integration/dijkstra_cycle_paths.py new file mode 100644 index 0000000..201219c --- /dev/null +++ b/tests/integration/dijkstra_cycle_paths.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +""" +Dijkstra Demo - Cycles Through Different Path Combinations +========================================================== + +Shows paths between different entity pairs, skipping impossible paths. +""" + +import mcrfpy +import sys + +# High contrast colors +WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown +FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray +PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green +START_COLOR = mcrfpy.Color(255, 100, 100) # Light red +END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue + +# Global state +grid = None +entities = [] +current_path_index = 0 +path_combinations = [] +current_path = [] + +def create_map(): + """Create the map with entities""" + global grid, entities + + mcrfpy.createScene("dijkstra_cycle") + + # Create grid + 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 - Entity 1 at (10,2) is TRAPPED! + "..W.....W..W..", # Row 3 + "..W...E.WWWW..", # Row 4 - Entity 2 at (6,4) + "E.W...........", # Row 5 - Entity 3 at (0,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)) + + # 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("Entities created:") + for i, (x, y) in enumerate(entity_positions): + print(f" Entity {i+1} at ({x}, {y})") + + # Check which entity is trapped + print("\nChecking accessibility:") + for i, e in enumerate(entities): + # Try to path to each other entity + can_reach = [] + for j, other in enumerate(entities): + if i != j: + path = e.path_to(int(other.x), int(other.y)) + if path: + can_reach.append(j+1) + + if not can_reach: + print(f" Entity {i+1} at ({int(e.x)}, {int(e.y)}) is TRAPPED!") + else: + print(f" Entity {i+1} can reach entities: {can_reach}") + + # Generate valid path combinations (excluding trapped entity) + global path_combinations + path_combinations = [] + + # Only paths between entities 2 and 3 (indices 1 and 2) will work + # since entity 1 (index 0) is trapped + if len(entities) >= 3: + # Entity 2 to Entity 3 + path = entities[1].path_to(int(entities[2].x), int(entities[2].y)) + if path: + path_combinations.append((1, 2, path)) + + # Entity 3 to Entity 2 + path = entities[2].path_to(int(entities[1].x), int(entities[1].y)) + if path: + path_combinations.append((2, 1, path)) + + print(f"\nFound {len(path_combinations)} valid paths") + +def clear_path_colors(): + """Reset all floor tiles to original color""" + global current_path + + for y in range(grid.grid_y): + for x in range(grid.grid_x): + cell = grid.at(x, y) + if cell.walkable: + cell.color = FLOOR_COLOR + + current_path = [] + +def show_path(index): + """Show a specific path combination""" + global current_path_index, current_path + + if not path_combinations: + status_text.text = "No valid paths available (Entity 1 is trapped!)" + return + + current_path_index = index % len(path_combinations) + from_idx, to_idx, path = path_combinations[current_path_index] + + # Clear previous path + clear_path_colors() + + # Get entities + e_from = entities[from_idx] + e_to = entities[to_idx] + + # Color the path + current_path = path + if path: + # Color start and end + grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR + grid.at(int(e_to.x), int(e_to.y)).color = END_COLOR + + # Color intermediate steps + for i, (x, y) in enumerate(path): + if i > 0 and i < len(path) - 1: + grid.at(x, y).color = PATH_COLOR + + # Update status + status_text.text = f"Path {current_path_index + 1}/{len(path_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} ({len(path)} steps)" + + # Update path display + path_display = [] + for i, (x, y) in enumerate(path[:5]): # Show first 5 steps + path_display.append(f"({x},{y})") + if len(path) > 5: + path_display.append("...") + path_text.text = "Path: " + " → ".join(path_display) if path_display else "Path: None" + +def handle_keypress(key_str, state): + """Handle keyboard input""" + global current_path_index + if state == "end": return + if key_str == "Esc": + print("\nExiting...") + sys.exit(0) + elif key_str == "N" or key_str == "Space": + show_path(current_path_index + 1) + elif key_str == "P": + show_path(current_path_index - 1) + elif key_str == "R": + show_path(current_path_index) + +# Create the demo +print("Dijkstra Path Cycling Demo") +print("==========================") +print("Note: Entity 1 is trapped by walls!") +print() + +create_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_cycle") +ui.append(grid) + +# Scale and position +grid.size = (560, 400) +grid.position = (120, 100) + +# Add title +title = mcrfpy.Caption("Dijkstra Pathfinding - Cycle Paths", 200, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status +status_text = mcrfpy.Caption("Press SPACE to cycle paths", 120, 60) +status_text.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(status_text) + +# Add path display +path_text = mcrfpy.Caption("Path: None", 120, 520) +path_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(path_text) + +# Add controls +controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, R=Refresh, Q=Quit", 120, 540) +controls.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(controls) + +# Add legend +legend = mcrfpy.Caption("Red=Start, Blue=End, Green=Path, Dark=Wall", 120, 560) +legend.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend) + +# Show first valid path +mcrfpy.setScene("dijkstra_cycle") +mcrfpy.keypressScene(handle_keypress) + +# Display initial path +if path_combinations: + show_path(0) +else: + status_text.text = "No valid paths! Entity 1 is trapped!" + +print("\nDemo ready!") +print("Controls:") +print(" SPACE or N - Next path") +print(" P - Previous path") +print(" R - Refresh current path") +print(" Q - Quit") diff --git a/tests/integration/dijkstra_debug.py b/tests/integration/dijkstra_debug.py new file mode 100644 index 0000000..fd182b8 --- /dev/null +++ b/tests/integration/dijkstra_debug.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +Debug version of Dijkstra pathfinding to diagnose visualization issues +""" + +import mcrfpy +import sys + +# Colors +WALL_COLOR = mcrfpy.Color(60, 30, 30) +FLOOR_COLOR = mcrfpy.Color(200, 200, 220) +PATH_COLOR = mcrfpy.Color(200, 250, 220) +ENTITY_COLORS = [ + mcrfpy.Color(255, 100, 100), # Entity 1 - Red + mcrfpy.Color(100, 255, 100), # Entity 2 - Green + mcrfpy.Color(100, 100, 255), # Entity 3 - Blue +] + +# Global state +grid = None +entities = [] +first_point = None +second_point = None + +def create_simple_map(): + """Create a simple test map""" + global grid, entities + + mcrfpy.createScene("dijkstra_debug") + + # Small grid for easy debugging + grid = mcrfpy.Grid(grid_x=10, grid_y=10) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + print("Initializing 10x10 grid...") + + # Initialize all as floor + for y in range(10): + for x in range(10): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = FLOOR_COLOR + + # Add a simple wall + print("Adding walls at:") + walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)] + for x, y in walls: + print(f" Wall at ({x}, {y})") + grid.at(x, y).walkable = False + grid.at(x, y).color = WALL_COLOR + + # Create 3 entities + entity_positions = [(2, 5), (8, 5), (5, 8)] + entities = [] + + print("\nCreating entities at:") + for i, (x, y) in enumerate(entity_positions): + print(f" Entity {i+1} at ({x}, {y})") + entity = mcrfpy.Entity(x, y) + entity.sprite_index = 49 + i # '1', '2', '3' + grid.entities.append(entity) + entities.append(entity) + + return grid + +def test_path_highlighting(): + """Test path highlighting with debug output""" + print("\n" + "="*50) + print("Testing path highlighting...") + + # Select first two entities + e1 = entities[0] + e2 = entities[1] + + print(f"\nEntity 1 position: ({e1.x}, {e1.y})") + print(f"Entity 2 position: ({e2.x}, {e2.y})") + + # Use entity.path_to() + print("\nCalling entity.path_to()...") + path = e1.path_to(int(e2.x), int(e2.y)) + + print(f"Path returned: {path}") + print(f"Path length: {len(path)} steps") + + if path: + print("\nHighlighting path cells:") + for i, (x, y) in enumerate(path): + print(f" Step {i}: ({x}, {y})") + # Get current color for debugging + cell = grid.at(x, y) + old_color = (cell.color.r, cell.color.g, cell.color.b) + + # Set new color + cell.color = PATH_COLOR + new_color = (cell.color.r, cell.color.g, cell.color.b) + + print(f" Color changed from {old_color} to {new_color}") + print(f" Walkable: {cell.walkable}") + + # Also test grid's Dijkstra methods + print("\n" + "-"*30) + print("Testing grid Dijkstra methods...") + + grid.compute_dijkstra(int(e1.x), int(e1.y)) + grid_path = grid.get_dijkstra_path(int(e2.x), int(e2.y)) + distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y)) + + print(f"Grid path: {grid_path}") + print(f"Grid distance: {distance}") + + # Verify colors were set + print("\nVerifying cell colors after highlighting:") + for x, y in path[:3]: # Check first 3 cells + cell = grid.at(x, y) + color = (cell.color.r, cell.color.g, cell.color.b) + expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b) + match = color == expected + print(f" Cell ({x}, {y}): color={color}, expected={expected}, match={match}") + +def handle_keypress(scene_name, keycode): + """Simple keypress handler""" + if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC + print("\nExiting debug...") + sys.exit(0) + elif keycode == 32: # Space + print("\nSpace pressed - retesting path highlighting...") + test_path_highlighting() + +# Create the map +print("Dijkstra Debug Test") +print("===================") +grid = create_simple_map() + +# Initial path test +test_path_highlighting() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_debug") +ui.append(grid) + +# Position and scale +grid.position = (50, 50) +grid.size = (400, 400) # 10*40 + +# Add title +title = mcrfpy.Caption("Dijkstra Debug - Press SPACE to retest, Q to quit", 50, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add debug info +info = mcrfpy.Caption("Check console for debug output", 50, 470) +info.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info) + +# Set up scene +mcrfpy.keypressScene(handle_keypress) +mcrfpy.setScene("dijkstra_debug") + +print("\nScene ready. The path should be highlighted in cyan.") +print("If you don't see the path, there may be a rendering issue.") +print("Press SPACE to retest, Q to quit.") \ No newline at end of file diff --git a/tests/integration/dijkstra_interactive.py b/tests/integration/dijkstra_interactive.py new file mode 100644 index 0000000..fdf2176 --- /dev/null +++ b/tests/integration/dijkstra_interactive.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +""" +Dijkstra Pathfinding Interactive Demo +===================================== + +Interactive visualization showing Dijkstra pathfinding between entities. + +Controls: +- Press 1/2/3 to select the first entity +- Press A/B/C to select the second entity +- Space to clear selection +- Q or ESC to quit + +The path between selected entities is automatically highlighted. +""" + +import mcrfpy +import sys + +# Colors - using more distinct values +WALL_COLOR = mcrfpy.Color(60, 30, 30) +FLOOR_COLOR = mcrfpy.Color(100, 100, 120) # Darker floor for better contrast +PATH_COLOR = mcrfpy.Color(50, 255, 50) # Bright green for path +ENTITY_COLORS = [ + mcrfpy.Color(255, 100, 100), # Entity 1 - Red + mcrfpy.Color(100, 255, 100), # Entity 2 - Green + mcrfpy.Color(100, 100, 255), # Entity 3 - Blue +] + +# Global state +grid = None +entities = [] +first_point = None +second_point = None + +def create_map(): + """Create the interactive map with the layout specified by the user""" + global grid, entities + + mcrfpy.createScene("dijkstra_interactive") + + # Create grid - 14x10 as specified + grid = mcrfpy.Grid(grid_x=14, grid_y=10) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Define the map layout from user's specification + # . = floor, W = wall, E = entity position + 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': + # Wall + cell.walkable = False + cell.transparent = False + cell.color = WALL_COLOR + else: + # Floor + cell.walkable = True + cell.transparent = True + cell.color = FLOOR_COLOR + + if char == 'E': + # Entity position + entity_positions.append((x, y)) + + # Create entities at marked positions + 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) + + return grid + +def clear_path_highlight(): + """Clear any existing path highlighting""" + # Reset all floor tiles to original color + for y in range(grid.grid_y): + for x in range(grid.grid_x): + cell = grid.at(x, y) + if cell.walkable: + cell.color = FLOOR_COLOR + +def highlight_path(): + """Highlight the path between selected entities""" + if first_point is None or second_point is None: + return + + # Clear previous highlighting + clear_path_highlight() + + # Get entities + entity1 = entities[first_point] + entity2 = entities[second_point] + + # Compute Dijkstra from first entity + grid.compute_dijkstra(int(entity1.x), int(entity1.y)) + + # Get path to second entity + path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y)) + + if path: + # Highlight the path + for x, y in path: + cell = grid.at(x, y) + if cell.walkable: + cell.color = PATH_COLOR + + # Also highlight start and end with entity colors + grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point] + grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point] + + # Update info + distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y)) + info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units" + else: + info_text.text = f"No path between Entity {first_point+1} and Entity {second_point+1}" + +def handle_keypress(scene_name, keycode): + """Handle keyboard input""" + global first_point, second_point + + # Number keys for first entity + if keycode == 49: # '1' + first_point = 0 + status_text.text = f"First: Entity 1 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + elif keycode == 50: # '2' + first_point = 1 + status_text.text = f"First: Entity 2 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + elif keycode == 51: # '3' + first_point = 2 + status_text.text = f"First: Entity 3 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + + # Letter keys for second entity + elif keycode == 65 or keycode == 97: # 'A' or 'a' + second_point = 0 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 1" + highlight_path() + elif keycode == 66 or keycode == 98: # 'B' or 'b' + second_point = 1 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 2" + highlight_path() + elif keycode == 67 or keycode == 99: # 'C' or 'c' + second_point = 2 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 3" + highlight_path() + + # Clear selection + elif keycode == 32: # Space + first_point = None + second_point = None + clear_path_highlight() + status_text.text = "Press 1/2/3 for first entity, A/B/C for second" + info_text.text = "Space to clear, Q to quit" + + # Quit + elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC + print("\nExiting Dijkstra interactive demo...") + sys.exit(0) + +# Create the visualization +print("Dijkstra Pathfinding Interactive Demo") +print("=====================================") +print("Controls:") +print(" 1/2/3 - Select first entity") +print(" A/B/C - Select second entity") +print(" Space - Clear selection") +print(" Q/ESC - Quit") + +# Create map +grid = create_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_interactive") +ui.append(grid) + +# Scale and position grid for better visibility +grid.size = (560, 400) # 14*40, 10*40 +grid.position = (120, 60) + +# Add title +title = mcrfpy.Caption("Dijkstra Pathfinding Interactive", 250, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status text +status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480) +status_text.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(status_text) + +# Add info text +info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500) +info_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info_text) + +# Add legend +legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 540) +legend1.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend1) + +legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 560) +legend2.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend2) + +# Mark entity positions with colored indicators +for i, entity in enumerate(entities): + marker = mcrfpy.Caption(str(i+1), + 120 + int(entity.x) * 40 + 15, + 60 + int(entity.y) * 40 + 10) + marker.fill_color = ENTITY_COLORS[i] + marker.outline = 1 + marker.outline_color = mcrfpy.Color(0, 0, 0) + ui.append(marker) + +# Set up input handling +mcrfpy.keypressScene(handle_keypress) + +# Show the scene +mcrfpy.setScene("dijkstra_interactive") + +print("\nVisualization ready!") +print("Entities are at:") +for i, entity in enumerate(entities): + print(f" Entity {i+1}: ({int(entity.x)}, {int(entity.y)})") \ No newline at end of file diff --git a/tests/integration/dijkstra_interactive_enhanced.py b/tests/integration/dijkstra_interactive_enhanced.py new file mode 100644 index 0000000..34da805 --- /dev/null +++ b/tests/integration/dijkstra_interactive_enhanced.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +""" +Enhanced Dijkstra Pathfinding Interactive Demo +============================================== + +Interactive visualization with entity pathfinding animations. + +Controls: +- Press 1/2/3 to select the first entity +- Press A/B/C to select the second entity +- Space to clear selection +- M to make selected entity move along path +- P to pause/resume animation +- R to reset entity positions +- Q or ESC to quit +""" + +import mcrfpy +import sys +import math + +# Colors +WALL_COLOR = mcrfpy.Color(60, 30, 30) +FLOOR_COLOR = mcrfpy.Color(200, 200, 220) +PATH_COLOR = mcrfpy.Color(200, 250, 220) +VISITED_COLOR = mcrfpy.Color(180, 230, 200) +ENTITY_COLORS = [ + mcrfpy.Color(255, 100, 100), # Entity 1 - Red + mcrfpy.Color(100, 255, 100), # Entity 2 - Green + mcrfpy.Color(100, 100, 255), # Entity 3 - Blue +] + +# Global state +grid = None +entities = [] +first_point = None +second_point = None +current_path = [] +animating = False +animation_progress = 0.0 +animation_speed = 2.0 # cells per second +original_positions = [] # Store original entity positions + +def create_map(): + """Create the interactive map with the layout specified by the user""" + global grid, entities, original_positions + + mcrfpy.createScene("dijkstra_enhanced") + + # Create grid - 14x10 as specified + grid = mcrfpy.Grid(grid_x=14, grid_y=10) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Define the map layout from user's specification + # . = floor, W = wall, E = entity position + 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': + # Wall + cell.walkable = False + cell.transparent = False + cell.color = WALL_COLOR + else: + # Floor + cell.walkable = True + cell.transparent = True + cell.color = FLOOR_COLOR + + if char == 'E': + # Entity position + entity_positions.append((x, y)) + + # Create entities at marked positions + entities = [] + original_positions = [] + 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) + original_positions.append((x, y)) + + return grid + +def clear_path_highlight(): + """Clear any existing path highlighting""" + global current_path + + # Reset all floor tiles to original color + for y in range(grid.grid_y): + for x in range(grid.grid_x): + cell = grid.at(x, y) + if cell.walkable: + cell.color = FLOOR_COLOR + + current_path = [] + +def highlight_path(): + """Highlight the path between selected entities using entity.path_to()""" + global current_path + + if first_point is None or second_point is None: + return + + # Clear previous highlighting + clear_path_highlight() + + # Get entities + entity1 = entities[first_point] + entity2 = entities[second_point] + + # Use the new path_to method! + path = entity1.path_to(int(entity2.x), int(entity2.y)) + + if path: + current_path = path + + # Highlight the path + for i, (x, y) in enumerate(path): + cell = grid.at(x, y) + if cell.walkable: + # Use gradient for path visualization + if i < len(path) - 1: + cell.color = PATH_COLOR + else: + cell.color = VISITED_COLOR + + # Highlight start and end with entity colors + grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point] + grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point] + + # Update info + info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps" + else: + info_text.text = f"No path between Entity {first_point+1} and Entity {second_point+1}" + current_path = [] + +def animate_movement(dt): + """Animate entity movement along path""" + global animation_progress, animating, current_path + + if not animating or not current_path or first_point is None: + return + + entity = entities[first_point] + + # Update animation progress + animation_progress += animation_speed * dt + + # Calculate current position along path + path_index = int(animation_progress) + + if path_index >= len(current_path): + # Animation complete + animating = False + animation_progress = 0.0 + # Snap to final position + if current_path: + final_x, final_y = current_path[-1] + entity.x = float(final_x) + entity.y = float(final_y) + return + + # Interpolate between path points + if path_index < len(current_path) - 1: + curr_x, curr_y = current_path[path_index] + next_x, next_y = current_path[path_index + 1] + + # Calculate interpolation factor + t = animation_progress - path_index + + # Smooth interpolation + entity.x = curr_x + (next_x - curr_x) * t + entity.y = curr_y + (next_y - curr_y) * t + else: + # At last point + entity.x, entity.y = current_path[path_index] + +def handle_keypress(scene_name, keycode): + """Handle keyboard input""" + global first_point, second_point, animating, animation_progress + + # Number keys for first entity + if keycode == 49: # '1' + first_point = 0 + status_text.text = f"First: Entity 1 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + elif keycode == 50: # '2' + first_point = 1 + status_text.text = f"First: Entity 2 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + elif keycode == 51: # '3' + first_point = 2 + status_text.text = f"First: Entity 3 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + + # Letter keys for second entity + elif keycode == 65 or keycode == 97: # 'A' or 'a' + second_point = 0 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 1" + highlight_path() + elif keycode == 66 or keycode == 98: # 'B' or 'b' + second_point = 1 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 2" + highlight_path() + elif keycode == 67 or keycode == 99: # 'C' or 'c' + second_point = 2 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 3" + highlight_path() + + # Movement control + elif keycode == 77 or keycode == 109: # 'M' or 'm' + if current_path and first_point is not None: + animating = True + animation_progress = 0.0 + control_text.text = "Animation: MOVING (press P to pause)" + + # Pause/Resume + elif keycode == 80 or keycode == 112: # 'P' or 'p' + animating = not animating + control_text.text = f"Animation: {'MOVING' if animating else 'PAUSED'} (press P to {'pause' if animating else 'resume'})" + + # Reset positions + elif keycode == 82 or keycode == 114: # 'R' or 'r' + animating = False + animation_progress = 0.0 + for i, entity in enumerate(entities): + entity.x, entity.y = original_positions[i] + control_text.text = "Entities reset to original positions" + highlight_path() # Re-highlight path after reset + + # Clear selection + elif keycode == 32: # Space + first_point = None + second_point = None + animating = False + animation_progress = 0.0 + clear_path_highlight() + status_text.text = "Press 1/2/3 for first entity, A/B/C for second" + info_text.text = "Space to clear, Q to quit" + control_text.text = "Press M to move, P to pause, R to reset" + + # Quit + elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC + print("\nExiting enhanced Dijkstra demo...") + sys.exit(0) + +# Timer callback for animation +def update_animation(dt): + """Update animation state""" + animate_movement(dt / 1000.0) # Convert ms to seconds + +# Create the visualization +print("Enhanced Dijkstra Pathfinding Demo") +print("==================================") +print("Controls:") +print(" 1/2/3 - Select first entity") +print(" A/B/C - Select second entity") +print(" M - Move first entity along path") +print(" P - Pause/Resume animation") +print(" R - Reset entity positions") +print(" Space - Clear selection") +print(" Q/ESC - Quit") + +# Create map +grid = create_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_enhanced") +ui.append(grid) + +# Scale and position grid for better visibility +grid.size = (560, 400) # 14*40, 10*40 +grid.position = (120, 60) + +# Add title +title = mcrfpy.Caption("Enhanced Dijkstra Pathfinding", 250, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status text +status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480) +status_text.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(status_text) + +# Add info text +info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500) +info_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info_text) + +# Add control text +control_text = mcrfpy.Caption("Press M to move, P to pause, R to reset", 120, 520) +control_text.fill_color = mcrfpy.Color(150, 200, 150) +ui.append(control_text) + +# Add legend +legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 560) +legend1.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend1) + +legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 580) +legend2.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend2) + +# Mark entity positions with colored indicators +for i, entity in enumerate(entities): + marker = mcrfpy.Caption(str(i+1), + 120 + int(entity.x) * 40 + 15, + 60 + int(entity.y) * 40 + 10) + marker.fill_color = ENTITY_COLORS[i] + marker.outline = 1 + marker.outline_color = mcrfpy.Color(0, 0, 0) + ui.append(marker) + +# Set up input handling +mcrfpy.keypressScene(handle_keypress) + +# Set up animation timer (60 FPS) +mcrfpy.setTimer("animation", update_animation, 16) + +# Show the scene +mcrfpy.setScene("dijkstra_enhanced") + +print("\nVisualization ready!") +print("Entities are at:") +for i, entity in enumerate(entities): + print(f" Entity {i+1}: ({int(entity.x)}, {int(entity.y)})") \ No newline at end of file diff --git a/tests/integration/dijkstra_test.py b/tests/integration/dijkstra_test.py new file mode 100644 index 0000000..9f99eeb --- /dev/null +++ b/tests/integration/dijkstra_test.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +Dijkstra Pathfinding Test - Headless +==================================== + +Tests all Dijkstra functionality and generates a screenshot. +""" + +import mcrfpy +from mcrfpy import automation +import sys + +def create_test_map(): + """Create a test map with obstacles""" + mcrfpy.createScene("dijkstra_test") + + # Create grid + grid = mcrfpy.Grid(grid_x=20, grid_y=12) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Initialize all cells as walkable floor + for y in range(12): + for x in range(20): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = mcrfpy.Color(200, 200, 220) + + # Add walls to create interesting paths + walls = [ + # Vertical wall in the middle + (10, 1), (10, 2), (10, 3), (10, 4), (10, 5), (10, 6), (10, 7), (10, 8), + # Horizontal walls + (2, 6), (3, 6), (4, 6), (5, 6), (6, 6), + (14, 6), (15, 6), (16, 6), (17, 6), + # Some scattered obstacles + (5, 2), (15, 2), (5, 9), (15, 9) + ] + + for x, y in walls: + grid.at(x, y).walkable = False + grid.at(x, y).color = mcrfpy.Color(60, 30, 30) + + # Place test entities + entities = [] + positions = [(2, 2), (17, 2), (9, 10)] + colors = [ + mcrfpy.Color(255, 100, 100), # Red + mcrfpy.Color(100, 255, 100), # Green + mcrfpy.Color(100, 100, 255) # Blue + ] + + for i, (x, y) in enumerate(positions): + entity = mcrfpy.Entity(x, y) + entity.sprite_index = 49 + i # '1', '2', '3' + grid.entities.append(entity) + entities.append(entity) + # Mark entity positions + grid.at(x, y).color = colors[i] + + return grid, entities + +def test_dijkstra(grid, entities): + """Test Dijkstra pathfinding between all entity pairs""" + results = [] + + for i in range(len(entities)): + for j in range(len(entities)): + if i != j: + # Compute Dijkstra from entity i + e1 = entities[i] + e2 = entities[j] + grid.compute_dijkstra(int(e1.x), int(e1.y)) + + # Get distance and path to entity j + distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y)) + path = grid.get_dijkstra_path(int(e2.x), int(e2.y)) + + if path: + results.append(f"Path {i+1}→{j+1}: {len(path)} steps, {distance:.1f} units") + + # Color one interesting path + if i == 0 and j == 2: # Path from 1 to 3 + for x, y in path[1:-1]: # Skip endpoints + if grid.at(x, y).walkable: + grid.at(x, y).color = mcrfpy.Color(200, 250, 220) + else: + results.append(f"Path {i+1}→{j+1}: No path found!") + + return results + +def run_test(runtime): + """Timer callback to run tests and take screenshot""" + # Run pathfinding tests + results = test_dijkstra(grid, entities) + + # Update display with results + y_pos = 380 + for result in results: + caption = mcrfpy.Caption(result, 50, y_pos) + caption.fill_color = mcrfpy.Color(200, 200, 200) + ui.append(caption) + y_pos += 20 + + # Take screenshot + mcrfpy.setTimer("screenshot", lambda rt: take_screenshot(), 500) + +def take_screenshot(): + """Take screenshot and exit""" + try: + automation.screenshot("dijkstra_test.png") + print("Screenshot saved: dijkstra_test.png") + except Exception as e: + print(f"Screenshot failed: {e}") + + # Exit + sys.exit(0) + +# Create test map +print("Creating Dijkstra pathfinding test...") +grid, entities = create_test_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_test") +ui.append(grid) + +# Position and scale grid +grid.position = (50, 50) +grid.size = (500, 300) + +# Add title +title = mcrfpy.Caption("Dijkstra Pathfinding Test", 200, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add legend +legend = mcrfpy.Caption("Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3", 50, 360) +legend.fill_color = mcrfpy.Color(180, 180, 180) +ui.append(legend) + +# Set scene +mcrfpy.setScene("dijkstra_test") + +# Run test after scene loads +mcrfpy.setTimer("test", run_test, 100) + +print("Running Dijkstra tests...") \ No newline at end of file diff --git a/tests/force_non_interactive.py b/tests/integration/force_non_interactive.py similarity index 100% rename from tests/force_non_interactive.py rename to tests/integration/force_non_interactive.py diff --git a/tests/integration/interactive_visibility.py b/tests/integration/interactive_visibility.py new file mode 100644 index 0000000..3d7aef8 --- /dev/null +++ b/tests/integration/interactive_visibility.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Interactive Visibility Demo +========================== + +Controls: + - WASD: Move the player (green @) + - Arrow keys: Move enemy (red E) + - Tab: Cycle perspective (Omniscient → Player → Enemy → Omniscient) + - Space: Update visibility for current entity + - R: Reset positions +""" + +import mcrfpy +import sys + +# Create scene and grid +mcrfpy.createScene("visibility_demo") +grid = mcrfpy.Grid(grid_x=30, grid_y=20) +grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background + +# Initialize grid - all walkable and transparent +for y in range(20): + for x in range(30): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(100, 100, 120) # Floor color + +# Create walls +walls = [ + # Central cross + [(15, y) for y in range(8, 12)], + [(x, 10) for x in range(13, 18)], + + # Rooms + # Top-left room + [(x, 5) for x in range(2, 8)] + [(8, y) for y in range(2, 6)], + [(2, y) for y in range(2, 6)] + [(x, 2) for x in range(2, 8)], + + # Top-right room + [(x, 5) for x in range(22, 28)] + [(22, y) for y in range(2, 6)], + [(28, y) for y in range(2, 6)] + [(x, 2) for x in range(22, 28)], + + # Bottom-left room + [(x, 15) for x in range(2, 8)] + [(8, y) for y in range(15, 18)], + [(2, y) for y in range(15, 18)] + [(x, 18) for x in range(2, 8)], + + # Bottom-right room + [(x, 15) for x in range(22, 28)] + [(22, y) for y in range(15, 18)], + [(28, y) for y in range(15, 18)] + [(x, 18) for x in range(22, 28)], +] + +for wall_group in walls: + for x, y in wall_group: + if 0 <= x < 30 and 0 <= y < 20: + cell = grid.at(x, y) + cell.walkable = False + cell.transparent = False + cell.color = mcrfpy.Color(40, 20, 20) # Wall color + +# Create entities +player = mcrfpy.Entity(5, 10, grid=grid) +player.sprite_index = 64 # @ +enemy = mcrfpy.Entity(25, 10, grid=grid) +enemy.sprite_index = 69 # E + +# Update initial visibility +player.update_visibility() +enemy.update_visibility() + +# Global state +current_perspective = -1 +perspective_names = ["Omniscient", "Player", "Enemy"] + +# UI Setup +ui = mcrfpy.sceneUI("visibility_demo") +ui.append(grid) +grid.position = (50, 100) +grid.size = (900, 600) # 30*30, 20*30 + +# Title +title = mcrfpy.Caption("Interactive Visibility Demo", 350, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Info displays +perspective_label = mcrfpy.Caption("Perspective: Omniscient", 50, 50) +perspective_label.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(perspective_label) + +controls = mcrfpy.Caption("WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset", 50, 730) +controls.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(controls) + +player_info = mcrfpy.Caption("Player: (5, 10)", 700, 50) +player_info.fill_color = mcrfpy.Color(100, 255, 100) +ui.append(player_info) + +enemy_info = mcrfpy.Caption("Enemy: (25, 10)", 700, 70) +enemy_info.fill_color = mcrfpy.Color(255, 100, 100) +ui.append(enemy_info) + +# Helper functions +def move_entity(entity, dx, dy): + """Move entity if target is walkable""" + new_x = int(entity.x + dx) + new_y = int(entity.y + dy) + + if 0 <= new_x < 30 and 0 <= new_y < 20: + cell = grid.at(new_x, new_y) + if cell.walkable: + entity.x = new_x + entity.y = new_y + entity.update_visibility() + return True + return False + +def update_info(): + """Update info displays""" + player_info.text = f"Player: ({int(player.x)}, {int(player.y)})" + enemy_info.text = f"Enemy: ({int(enemy.x)}, {int(enemy.y)})" + +def cycle_perspective(): + """Cycle through perspectives""" + global current_perspective + + # Cycle: -1 → 0 → 1 → -1 + current_perspective = (current_perspective + 2) % 3 - 1 + + grid.perspective = current_perspective + name = perspective_names[current_perspective + 1] + perspective_label.text = f"Perspective: {name}" + +# Key handlers +def handle_keys(key, state): + """Handle keyboard input""" + if state == "end": return + key = key.lower() + # Player movement (WASD) + if key == "w": + move_entity(player, 0, -1) + elif key == "s": + move_entity(player, 0, 1) + elif key == "a": + move_entity(player, -1, 0) + elif key == "d": + move_entity(player, 1, 0) + + # Enemy movement (Arrows) + elif key == "up": + move_entity(enemy, 0, -1) + elif key == "down": + move_entity(enemy, 0, 1) + elif key == "left": + move_entity(enemy, -1, 0) + elif key == "right": + move_entity(enemy, 1, 0) + + # Tab to cycle perspective + elif key == "tab": + cycle_perspective() + + # Space to update visibility + elif key == "space": + player.update_visibility() + enemy.update_visibility() + print("Updated visibility for both entities") + + # R to reset + elif key == "r": + player.x, player.y = 5, 10 + enemy.x, enemy.y = 25, 10 + player.update_visibility() + enemy.update_visibility() + update_info() + print("Reset positions") + + # Q to quit + elif key == "q": + print("Exiting...") + sys.exit(0) + + update_info() + +# Set scene first +mcrfpy.setScene("visibility_demo") + +# Register key handler (operates on current scene) +mcrfpy.keypressScene(handle_keys) + +print("Interactive Visibility Demo") +print("===========================") +print("WASD: Move player (green @)") +print("Arrows: Move enemy (red E)") +print("Tab: Cycle perspective") +print("Space: Update visibility") +print("R: Reset positions") +print("Q: Quit") +print("\nCurrent perspective: Omniscient (shows all)") +print("Try moving entities and switching perspectives!") diff --git a/tests/integration/simple_interactive_visibility.py b/tests/integration/simple_interactive_visibility.py new file mode 100644 index 0000000..fd95d5a --- /dev/null +++ b/tests/integration/simple_interactive_visibility.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Simple interactive visibility test""" + +import mcrfpy +import sys + +# Create scene and grid +print("Creating scene...") +mcrfpy.createScene("vis_test") + +print("Creating grid...") +grid = mcrfpy.Grid(grid_x=10, grid_y=10) + +# Initialize grid +print("Initializing grid...") +for y in range(10): + for x in range(10): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(100, 100, 120) + +# Create entity +print("Creating entity...") +entity = mcrfpy.Entity(5, 5, grid=grid) +entity.sprite_index = 64 + +print("Updating visibility...") +entity.update_visibility() + +# Set up UI +print("Setting up UI...") +ui = mcrfpy.sceneUI("vis_test") +ui.append(grid) +grid.position = (50, 50) +grid.size = (300, 300) + +# Test perspective +print("Testing perspective...") +grid.perspective = -1 # Omniscient +print(f"Perspective set to: {grid.perspective}") + +print("Setting scene...") +mcrfpy.setScene("vis_test") + +print("Ready!") \ No newline at end of file diff --git a/tests/integration/simple_visibility_test.py b/tests/integration/simple_visibility_test.py new file mode 100644 index 0000000..5c20758 --- /dev/null +++ b/tests/integration/simple_visibility_test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Simple visibility test without entity append""" + +import mcrfpy +import sys + +print("Simple visibility test...") + +# Create scene and grid +mcrfpy.createScene("simple") +print("Scene created") + +grid = mcrfpy.Grid(grid_x=5, grid_y=5) +print("Grid created") + +# Create entity without appending +entity = mcrfpy.Entity(2, 2, grid=grid) +print(f"Entity created at ({entity.x}, {entity.y})") + +# Check if gridstate is initialized +print(f"Gridstate length: {len(entity.gridstate)}") + +# Try to access at method +try: + state = entity.at(0, 0) + print(f"at(0,0) returned: {state}") + print(f"visible: {state.visible}, discovered: {state.discovered}") +except Exception as e: + print(f"Error in at(): {e}") + +# Try update_visibility +try: + entity.update_visibility() + print("update_visibility() succeeded") +except Exception as e: + print(f"Error in update_visibility(): {e}") + +print("Test complete") +sys.exit(0) \ No newline at end of file diff --git a/tests/trace_interactive.py b/tests/integration/trace_interactive.py similarity index 100% rename from tests/trace_interactive.py rename to tests/integration/trace_interactive.py diff --git a/tests/test_stdin_theory.py b/tests/test_stdin_theory.py deleted file mode 100644 index 88d1d28..0000000 --- a/tests/test_stdin_theory.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/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/unified_click_example.cpp b/tests/unified_click_example.cpp new file mode 100644 index 0000000..1c7fa1d --- /dev/null +++ b/tests/unified_click_example.cpp @@ -0,0 +1,101 @@ +// 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/WORKING_automation_test_example.py b/tests/unit/WORKING_automation_test_example.py similarity index 100% rename from tests/WORKING_automation_test_example.py rename to tests/unit/WORKING_automation_test_example.py diff --git a/tests/api_createScene_test.py b/tests/unit/api_createScene_test.py similarity index 100% rename from tests/api_createScene_test.py rename to tests/unit/api_createScene_test.py diff --git a/tests/api_keypressScene_test.py b/tests/unit/api_keypressScene_test.py similarity index 100% rename from tests/api_keypressScene_test.py rename to tests/unit/api_keypressScene_test.py diff --git a/tests/api_sceneUI_test.py b/tests/unit/api_sceneUI_test.py similarity index 100% rename from tests/api_sceneUI_test.py rename to tests/unit/api_sceneUI_test.py diff --git a/tests/api_setScene_currentScene_test.py b/tests/unit/api_setScene_currentScene_test.py similarity index 100% rename from tests/api_setScene_currentScene_test.py rename to tests/unit/api_setScene_currentScene_test.py diff --git a/tests/api_timer_test.py b/tests/unit/api_timer_test.py similarity index 100% rename from tests/api_timer_test.py rename to tests/unit/api_timer_test.py diff --git a/tests/unit/check_entity_attrs.py b/tests/unit/check_entity_attrs.py new file mode 100644 index 0000000..d0a44b8 --- /dev/null +++ b/tests/unit/check_entity_attrs.py @@ -0,0 +1,4 @@ +import mcrfpy +e = mcrfpy.Entity(0, 0) +print("Entity attributes:", dir(e)) +print("\nEntity repr:", repr(e)) \ No newline at end of file diff --git a/tests/unit/debug_empty_paths.py b/tests/unit/debug_empty_paths.py new file mode 100644 index 0000000..1485177 --- /dev/null +++ b/tests/unit/debug_empty_paths.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Debug empty paths issue""" + +import mcrfpy +import sys + +print("Debugging empty paths...") + +# Create scene and grid +mcrfpy.createScene("debug") +grid = mcrfpy.Grid(grid_x=10, grid_y=10) + +# Initialize grid - all walkable +print("\nInitializing grid...") +for y in range(10): + for x in range(10): + grid.at(x, y).walkable = True + +# Test simple path +print("\nTest 1: Simple path from (0,0) to (5,5)") +path = grid.compute_astar_path(0, 0, 5, 5) +print(f" A* path: {path}") +print(f" Path length: {len(path)}") + +# Test with Dijkstra +print("\nTest 2: Same path with Dijkstra") +grid.compute_dijkstra(0, 0) +dpath = grid.get_dijkstra_path(5, 5) +print(f" Dijkstra path: {dpath}") +print(f" Path length: {len(dpath)}") + +# Check if grid is properly initialized +print("\nTest 3: Checking grid cells") +for y in range(3): + for x in range(3): + cell = grid.at(x, y) + print(f" Cell ({x},{y}): walkable={cell.walkable}") + +# Test with walls +print("\nTest 4: Path with wall") +grid.at(2, 2).walkable = False +grid.at(3, 2).walkable = False +grid.at(4, 2).walkable = False +print(" Added wall at y=2, x=2,3,4") + +path2 = grid.compute_astar_path(0, 0, 5, 5) +print(f" A* path with wall: {path2}") +print(f" Path length: {len(path2)}") + +# Test invalid paths +print("\nTest 5: Path to blocked cell") +grid.at(9, 9).walkable = False +path3 = grid.compute_astar_path(0, 0, 9, 9) +print(f" Path to blocked cell: {path3}") + +# Check TCOD map sync +print("\nTest 6: Verify TCOD map is synced") +# Try to force a sync +print(" Checking if syncTCODMap exists...") +if hasattr(grid, 'sync_tcod_map'): + print(" Calling sync_tcod_map()") + grid.sync_tcod_map() +else: + print(" No sync_tcod_map method found") + +# Try path again +print("\nTest 7: Path after potential sync") +path4 = grid.compute_astar_path(0, 0, 5, 5) +print(f" A* path: {path4}") + +def timer_cb(dt): + sys.exit(0) + +# Quick UI setup +ui = mcrfpy.sceneUI("debug") +ui.append(grid) +mcrfpy.setScene("debug") +mcrfpy.setTimer("exit", timer_cb, 100) + +print("\nStarting timer...") \ No newline at end of file diff --git a/tests/debug_render_test.py b/tests/unit/debug_render_test.py similarity index 100% rename from tests/debug_render_test.py rename to tests/unit/debug_render_test.py diff --git a/tests/empty_script.py b/tests/unit/empty_script.py similarity index 100% rename from tests/empty_script.py rename to tests/unit/empty_script.py diff --git a/tests/exit_immediately_test.py b/tests/unit/exit_immediately_test.py similarity index 100% rename from tests/exit_immediately_test.py rename to tests/unit/exit_immediately_test.py diff --git a/tests/generate_docs_screenshots.py b/tests/unit/generate_docs_screenshots.py similarity index 100% rename from tests/generate_docs_screenshots.py rename to tests/unit/generate_docs_screenshots.py diff --git a/tests/generate_grid_screenshot.py b/tests/unit/generate_grid_screenshot.py similarity index 100% rename from tests/generate_grid_screenshot.py rename to tests/unit/generate_grid_screenshot.py diff --git a/tests/generate_sprite_screenshot.py b/tests/unit/generate_sprite_screenshot.py similarity index 100% rename from tests/generate_sprite_screenshot.py rename to tests/unit/generate_sprite_screenshot.py diff --git a/tests/unit/grid_at_argument_test.py b/tests/unit/grid_at_argument_test.py new file mode 100644 index 0000000..14e9485 --- /dev/null +++ b/tests/unit/grid_at_argument_test.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Test Grid.at() method with various argument formats""" + +import mcrfpy +import sys + +def test_grid_at_arguments(): + """Test that Grid.at() accepts all required argument formats""" + print("Testing Grid.at() argument formats...") + + # Create a test scene + mcrfpy.createScene("test") + + # Create a grid + grid = mcrfpy.Grid(10, 10) + ui = mcrfpy.sceneUI("test") + ui.append(grid) + + success_count = 0 + total_tests = 4 + + # Test 1: Two positional arguments (x, y) + try: + point1 = grid.at(5, 5) + print("✓ Test 1 PASSED: grid.at(5, 5)") + success_count += 1 + except Exception as e: + print(f"✗ Test 1 FAILED: grid.at(5, 5) - {e}") + + # Test 2: Single tuple argument (x, y) + try: + point2 = grid.at((3, 3)) + print("✓ Test 2 PASSED: grid.at((3, 3))") + success_count += 1 + except Exception as e: + print(f"✗ Test 2 FAILED: grid.at((3, 3)) - {e}") + + # Test 3: Keyword arguments x=x, y=y + try: + point3 = grid.at(x=7, y=2) + print("✓ Test 3 PASSED: grid.at(x=7, y=2)") + success_count += 1 + except Exception as e: + print(f"✗ Test 3 FAILED: grid.at(x=7, y=2) - {e}") + + # Test 4: pos keyword argument pos=(x, y) + try: + point4 = grid.at(pos=(1, 8)) + print("✓ Test 4 PASSED: grid.at(pos=(1, 8))") + success_count += 1 + except Exception as e: + print(f"✗ Test 4 FAILED: grid.at(pos=(1, 8)) - {e}") + + # Test error cases + print("\nTesting error cases...") + + # Test 5: Invalid - mixing pos with x/y + try: + grid.at(x=1, pos=(2, 2)) + print("✗ Test 5 FAILED: Should have raised error for mixing pos and x/y") + except TypeError as e: + print(f"✓ Test 5 PASSED: Correctly rejected mixing pos and x/y - {e}") + + # Test 6: Invalid - out of range + try: + grid.at(15, 15) + print("✗ Test 6 FAILED: Should have raised error for out of range") + except ValueError as e: + print(f"✓ Test 6 PASSED: Correctly rejected out of range - {e}") + + # Test 7: Verify all points are valid GridPoint objects + try: + # Check that we can set walkable on all returned points + if 'point1' in locals(): + point1.walkable = True + if 'point2' in locals(): + point2.walkable = False + if 'point3' in locals(): + point3.color = mcrfpy.Color(255, 0, 0) + if 'point4' in locals(): + point4.tilesprite = 5 + print("✓ All returned GridPoint objects are valid") + except Exception as e: + print(f"✗ GridPoint objects validation failed: {e}") + + print(f"\nSummary: {success_count}/{total_tests} tests passed") + + if success_count == total_tests: + print("ALL TESTS PASSED!") + sys.exit(0) + else: + print("SOME TESTS FAILED!") + sys.exit(1) + +# Run timer callback to execute tests after render loop starts +def run_test(elapsed): + test_grid_at_arguments() + +# Set a timer to run the test +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/keypress_scene_validation_test.py b/tests/unit/keypress_scene_validation_test.py similarity index 100% rename from tests/keypress_scene_validation_test.py rename to tests/unit/keypress_scene_validation_test.py diff --git a/tests/run_issue_tests.py b/tests/unit/run_issue_tests.py similarity index 100% rename from tests/run_issue_tests.py rename to tests/unit/run_issue_tests.py diff --git a/tests/screenshot_transparency_fix_test.py b/tests/unit/screenshot_transparency_fix_test.py similarity index 100% rename from tests/screenshot_transparency_fix_test.py rename to tests/unit/screenshot_transparency_fix_test.py diff --git a/tests/simple_screenshot_test.py b/tests/unit/simple_screenshot_test.py similarity index 100% rename from tests/simple_screenshot_test.py rename to tests/unit/simple_screenshot_test.py diff --git a/tests/simple_timer_screenshot_test.py b/tests/unit/simple_timer_screenshot_test.py similarity index 100% rename from tests/simple_timer_screenshot_test.py rename to tests/unit/simple_timer_screenshot_test.py diff --git a/tests/trace_exec_behavior.py b/tests/unit/trace_exec_behavior.py similarity index 100% rename from tests/trace_exec_behavior.py rename to tests/unit/trace_exec_behavior.py diff --git a/tests/ui_Entity_issue73_test.py b/tests/unit/ui_Entity_issue73_test.py similarity index 100% rename from tests/ui_Entity_issue73_test.py rename to tests/unit/ui_Entity_issue73_test.py diff --git a/tests/ui_Frame_test.py b/tests/unit/ui_Frame_test.py similarity index 100% rename from tests/ui_Frame_test.py rename to tests/unit/ui_Frame_test.py diff --git a/tests/ui_Frame_test_detailed.py b/tests/unit/ui_Frame_test_detailed.py similarity index 100% rename from tests/ui_Frame_test_detailed.py rename to tests/unit/ui_Frame_test_detailed.py diff --git a/tests/ui_Grid_none_texture_test.py b/tests/unit/ui_Grid_none_texture_test.py similarity index 100% rename from tests/ui_Grid_none_texture_test.py rename to tests/unit/ui_Grid_none_texture_test.py diff --git a/tests/ui_Grid_null_texture_test.py b/tests/unit/ui_Grid_null_texture_test.py similarity index 100% rename from tests/ui_Grid_null_texture_test.py rename to tests/unit/ui_Grid_null_texture_test.py diff --git a/tests/ui_Grid_test.py b/tests/unit/ui_Grid_test.py similarity index 100% rename from tests/ui_Grid_test.py rename to tests/unit/ui_Grid_test.py diff --git a/tests/ui_Grid_test_no_grid.py b/tests/unit/ui_Grid_test_no_grid.py similarity index 100% rename from tests/ui_Grid_test_no_grid.py rename to tests/unit/ui_Grid_test_no_grid.py diff --git a/tests/ui_Sprite_issue19_test.py b/tests/unit/ui_Sprite_issue19_test.py similarity index 100% rename from tests/ui_Sprite_issue19_test.py rename to tests/unit/ui_Sprite_issue19_test.py diff --git a/tests/ui_UICollection_issue69_test.py b/tests/unit/ui_UICollection_issue69_test.py similarity index 100% rename from tests/ui_UICollection_issue69_test.py rename to tests/unit/ui_UICollection_issue69_test.py diff --git a/tests/validate_screenshot_test.py b/tests/unit/validate_screenshot_test.py similarity index 100% rename from tests/validate_screenshot_test.py rename to tests/unit/validate_screenshot_test.py diff --git a/tests/working_timer_test.py b/tests/unit/working_timer_test.py similarity index 100% rename from tests/working_timer_test.py rename to tests/unit/working_timer_test.py diff --git a/generate_api_docs.py b/tools/generate_api_docs.py similarity index 100% rename from generate_api_docs.py rename to tools/generate_api_docs.py diff --git a/generate_api_docs_html.py b/tools/generate_api_docs_html.py similarity index 100% rename from generate_api_docs_html.py rename to tools/generate_api_docs_html.py diff --git a/generate_api_docs_simple.py b/tools/generate_api_docs_simple.py similarity index 100% rename from generate_api_docs_simple.py rename to tools/generate_api_docs_simple.py diff --git a/generate_color_table.py b/tools/generate_color_table.py similarity index 100% rename from generate_color_table.py rename to tools/generate_color_table.py diff --git a/generate_complete_api_docs.py b/tools/generate_complete_api_docs.py similarity index 100% rename from generate_complete_api_docs.py rename to tools/generate_complete_api_docs.py diff --git a/generate_complete_markdown_docs.py b/tools/generate_complete_markdown_docs.py similarity index 100% rename from generate_complete_markdown_docs.py rename to tools/generate_complete_markdown_docs.py diff --git a/tools/generate_dynamic_docs.py b/tools/generate_dynamic_docs.py new file mode 100644 index 0000000..92e65cc --- /dev/null +++ b/tools/generate_dynamic_docs.py @@ -0,0 +1,510 @@ +#!/usr/bin/env python3 +""" +Dynamic documentation generator for McRogueFace. +Extracts all documentation directly from the compiled module using introspection. +""" + +import os +import sys +import inspect +import datetime +import html +import re +from pathlib import Path + +# Must be run with McRogueFace as interpreter +try: + import mcrfpy +except ImportError: + print("Error: This script must be run with McRogueFace as the interpreter") + print("Usage: ./build/mcrogueface --exec generate_dynamic_docs.py") + sys.exit(1) + +def parse_docstring(docstring): + """Parse a docstring to extract signature, description, args, and returns.""" + if not docstring: + return {"signature": "", "description": "", "args": [], "returns": "", "example": ""} + + lines = docstring.strip().split('\n') + result = { + "signature": "", + "description": "", + "args": [], + "returns": "", + "example": "" + } + + # First line often contains the signature + if lines and '(' in lines[0] and ')' in lines[0]: + result["signature"] = lines[0].strip() + lines = lines[1:] if len(lines) > 1 else [] + + # Parse the rest + current_section = "description" + description_lines = [] + example_lines = [] + in_example = False + + for line in lines: + line_lower = line.strip().lower() + + if line_lower.startswith("args:") or line_lower.startswith("arguments:"): + current_section = "args" + continue + elif line_lower.startswith("returns:") or line_lower.startswith("return:"): + current_section = "returns" + result["returns"] = line[line.find(':')+1:].strip() + continue + elif line_lower.startswith("example:") or line_lower.startswith("examples:"): + in_example = True + continue + elif line_lower.startswith("note:"): + if description_lines: + description_lines.append("") + description_lines.append(line) + continue + + if in_example: + example_lines.append(line) + elif current_section == "description" and not line.startswith(" "): + description_lines.append(line) + elif current_section == "args" and line.strip(): + # Parse argument lines like " x: X coordinate" + match = re.match(r'\s+(\w+):\s*(.+)', line) + if match: + result["args"].append({ + "name": match.group(1), + "description": match.group(2).strip() + }) + elif current_section == "returns" and line.strip() and line.startswith(" "): + result["returns"] += " " + line.strip() + + result["description"] = '\n'.join(description_lines).strip() + result["example"] = '\n'.join(example_lines).strip() + + return result + +def get_all_functions(): + """Get all module-level functions.""" + functions = {} + for name in dir(mcrfpy): + if name.startswith('_'): + continue + obj = getattr(mcrfpy, name) + if inspect.isbuiltin(obj) or inspect.isfunction(obj): + doc_info = parse_docstring(obj.__doc__) + functions[name] = { + "name": name, + "doc": obj.__doc__ or "", + "parsed": doc_info + } + return functions + +def get_all_classes(): + """Get all classes and their methods/properties.""" + classes = {} + for name in dir(mcrfpy): + if name.startswith('_'): + continue + obj = getattr(mcrfpy, name) + if inspect.isclass(obj): + class_info = { + "name": name, + "doc": obj.__doc__ or "", + "methods": {}, + "properties": {}, + "bases": [base.__name__ for base in obj.__bases__ if base.__name__ != 'object'] + } + + # Get methods and properties + for attr_name in dir(obj): + if attr_name.startswith('__') and attr_name != '__init__': + continue + + try: + attr = getattr(obj, attr_name) + if callable(attr): + method_doc = attr.__doc__ or "" + class_info["methods"][attr_name] = { + "doc": method_doc, + "parsed": parse_docstring(method_doc) + } + elif isinstance(attr, property): + prop_doc = (attr.fget.__doc__ if attr.fget else "") or "" + class_info["properties"][attr_name] = { + "doc": prop_doc, + "readonly": attr.fset is None + } + except: + pass + + classes[name] = class_info + return classes + +def get_constants(): + """Get module constants.""" + constants = {} + for name in dir(mcrfpy): + if name.startswith('_') or name[0].islower(): + continue + obj = getattr(mcrfpy, name) + if not (inspect.isclass(obj) or callable(obj)): + constants[name] = { + "name": name, + "value": repr(obj) if not name.startswith('default_') else f"<{name}>", + "type": type(obj).__name__ + } + return constants + +def generate_html_docs(): + """Generate HTML documentation.""" + functions = get_all_functions() + classes = get_all_classes() + constants = get_constants() + + html_content = f""" + + + + + McRogueFace API Reference + + + +
+

McRogueFace API Reference

+

Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+

This documentation was dynamically generated from the compiled module.

+ +
+

Table of Contents

+ +
+ +

Functions

+""" + + # Generate function documentation + for func_name in sorted(functions.keys()): + func_info = functions[func_name] + parsed = func_info["parsed"] + + html_content += f""" +
+

{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}

+

{html.escape(parsed['description'])}

+""" + + if parsed['args']: + html_content += "

Arguments:

\n
    \n" + for arg in parsed['args']: + html_content += f"
  • {arg['name']}: {html.escape(arg['description'])}
  • \n" + html_content += "
\n" + + if parsed['returns']: + html_content += f"

Returns: {html.escape(parsed['returns'])}

\n" + + if parsed['example']: + html_content += f"

Example:

\n
{html.escape(parsed['example'])}
\n" + + html_content += "
\n" + + # Generate class documentation + html_content += "\n

Classes

\n" + + for class_name in sorted(classes.keys()): + class_info = classes[class_name] + + html_content += f""" +
+

{class_name}

+""" + + if class_info['bases']: + html_content += f"

Inherits from: {', '.join(class_info['bases'])}

\n" + + if class_info['doc']: + html_content += f"

{html.escape(class_info['doc'])}

\n" + + # Properties + if class_info['properties']: + html_content += "

Properties:

\n
    \n" + for prop_name, prop_info in sorted(class_info['properties'].items()): + readonly = " (read-only)" if prop_info['readonly'] else "" + html_content += f"
  • {prop_name}{readonly}" + if prop_info['doc']: + html_content += f": {html.escape(prop_info['doc'])}" + html_content += "
  • \n" + html_content += "
\n" + + # Methods + if class_info['methods']: + html_content += "

Methods:

\n" + for method_name, method_info in sorted(class_info['methods'].items()): + if method_name == '__init__': + continue + parsed = method_info['parsed'] + + html_content += f""" +
+
{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}
+""" + + if parsed['description']: + html_content += f"

{html.escape(parsed['description'])}

\n" + + if parsed['args']: + html_content += "
\n" + for arg in parsed['args']: + html_content += f"
{arg['name']}: {html.escape(arg['description'])}
\n" + html_content += "
\n" + + if parsed['returns']: + html_content += f"

Returns: {html.escape(parsed['returns'])}

\n" + + html_content += "
\n" + + html_content += "
\n" + + # Constants + html_content += "\n

Constants

\n
    \n" + for const_name, const_info in sorted(constants.items()): + html_content += f"
  • {const_name} ({const_info['type']}): {const_info['value']}
  • \n" + html_content += "
\n" + + html_content += """ +
+ + +""" + + # Write the file + output_path = Path("docs/api_reference_dynamic.html") + output_path.parent.mkdir(exist_ok=True) + output_path.write_text(html_content) + print(f"Generated {output_path}") + print(f"Found {len(functions)} functions, {len(classes)} classes, {len(constants)} constants") + +def generate_markdown_docs(): + """Generate Markdown documentation.""" + functions = get_all_functions() + classes = get_all_classes() + constants = get_constants() + + md_content = f"""# McRogueFace API Reference + +*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}* + +*This documentation was dynamically generated from the compiled module.* + +## Table of Contents + +- [Functions](#functions) +- [Classes](#classes) +""" + + # Add classes to TOC + for class_name in sorted(classes.keys()): + md_content += f" - [{class_name}](#{class_name.lower()})\n" + + md_content += "- [Constants](#constants)\n\n" + + # Functions + md_content += "## Functions\n\n" + + for func_name in sorted(functions.keys()): + func_info = functions[func_name] + parsed = func_info["parsed"] + + md_content += f"### `{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n" + + if parsed['description']: + md_content += f"{parsed['description']}\n\n" + + if parsed['args']: + md_content += "**Arguments:**\n" + for arg in parsed['args']: + md_content += f"- `{arg['name']}`: {arg['description']}\n" + md_content += "\n" + + if parsed['returns']: + md_content += f"**Returns:** {parsed['returns']}\n\n" + + if parsed['example']: + md_content += f"**Example:**\n```python\n{parsed['example']}\n```\n\n" + + # Classes + md_content += "## Classes\n\n" + + for class_name in sorted(classes.keys()): + class_info = classes[class_name] + + md_content += f"### {class_name}\n\n" + + if class_info['bases']: + md_content += f"*Inherits from: {', '.join(class_info['bases'])}*\n\n" + + if class_info['doc']: + md_content += f"{class_info['doc']}\n\n" + + # Properties + if class_info['properties']: + md_content += "**Properties:**\n" + for prop_name, prop_info in sorted(class_info['properties'].items()): + readonly = " *(read-only)*" if prop_info['readonly'] else "" + md_content += f"- `{prop_name}`{readonly}" + if prop_info['doc']: + md_content += f": {prop_info['doc']}" + md_content += "\n" + md_content += "\n" + + # Methods + if class_info['methods']: + md_content += "**Methods:**\n\n" + for method_name, method_info in sorted(class_info['methods'].items()): + if method_name == '__init__': + continue + parsed = method_info['parsed'] + + md_content += f"#### `{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n" + + if parsed['description']: + md_content += f"{parsed['description']}\n\n" + + if parsed['args']: + md_content += "**Arguments:**\n" + for arg in parsed['args']: + md_content += f"- `{arg['name']}`: {arg['description']}\n" + md_content += "\n" + + if parsed['returns']: + md_content += f"**Returns:** {parsed['returns']}\n\n" + + # Constants + md_content += "## Constants\n\n" + for const_name, const_info in sorted(constants.items()): + md_content += f"- `{const_name}` ({const_info['type']}): {const_info['value']}\n" + + # Write the file + output_path = Path("docs/API_REFERENCE_DYNAMIC.md") + output_path.parent.mkdir(exist_ok=True) + output_path.write_text(md_content) + print(f"Generated {output_path}") + +if __name__ == "__main__": + print("Generating dynamic documentation from mcrfpy module...") + generate_html_docs() + generate_markdown_docs() + print("Documentation generation complete!") \ No newline at end of file diff --git a/tools/generate_stubs.py b/tools/generate_stubs.py new file mode 100644 index 0000000..1ddacf7 --- /dev/null +++ b/tools/generate_stubs.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +"""Generate .pyi type stub files for McRogueFace Python API. + +This script introspects the mcrfpy module and generates type stubs +for better IDE support and type checking. +""" + +import os +import sys +import inspect +import types +from typing import Dict, List, Set, Any + +# Add the build directory to path to import mcrfpy +sys.path.insert(0, './build') + +try: + import mcrfpy +except ImportError: + print("Error: Could not import mcrfpy. Make sure to run this from the project root after building.") + sys.exit(1) + +def parse_docstring_signature(doc: str) -> tuple[str, str]: + """Extract signature and description from docstring.""" + if not doc: + return "", "" + + lines = doc.strip().split('\n') + if lines: + # First line often contains the signature + first_line = lines[0] + if '(' in first_line and ')' in first_line: + # Extract just the part after the function name + start = first_line.find('(') + end = first_line.rfind(')') + 1 + if start != -1 and end != 0: + sig = first_line[start:end] + # Get return type if present + if '->' in first_line: + ret_start = first_line.find('->') + ret_type = first_line[ret_start:].strip() + return sig, ret_type + return sig, "" + return "", "" + +def get_type_hint(obj_type: type) -> str: + """Convert Python type to type hint string.""" + if obj_type == int: + return "int" + elif obj_type == float: + return "float" + elif obj_type == str: + return "str" + elif obj_type == bool: + return "bool" + elif obj_type == list: + return "List[Any]" + elif obj_type == dict: + return "Dict[Any, Any]" + elif obj_type == tuple: + return "Tuple[Any, ...]" + elif obj_type == type(None): + return "None" + else: + return "Any" + +def generate_class_stub(class_name: str, cls: type) -> List[str]: + """Generate stub for a class.""" + lines = [] + + # Get class docstring + if cls.__doc__: + doc_lines = cls.__doc__.strip().split('\n') + # Use only the first paragraph for the stub + lines.append(f'class {class_name}:') + lines.append(f' """{doc_lines[0]}"""') + else: + lines.append(f'class {class_name}:') + + # Check for __init__ method + if hasattr(cls, '__init__'): + init_doc = cls.__init__.__doc__ or cls.__doc__ + if init_doc: + sig, ret = parse_docstring_signature(init_doc) + if sig: + lines.append(f' def __init__(self{sig[1:-1]}) -> None: ...') + else: + lines.append(f' def __init__(self, *args, **kwargs) -> None: ...') + else: + lines.append(f' def __init__(self, *args, **kwargs) -> None: ...') + + # Get properties and methods + properties = [] + methods = [] + + for attr_name in dir(cls): + if attr_name.startswith('_') and not attr_name.startswith('__'): + continue + + try: + attr = getattr(cls, attr_name) + + if isinstance(attr, property): + properties.append((attr_name, attr)) + elif callable(attr) and not attr_name.startswith('__'): + methods.append((attr_name, attr)) + except: + pass + + # Add properties + if properties: + lines.append('') + for prop_name, prop in properties: + # Try to determine property type from docstring + if prop.fget and prop.fget.__doc__: + lines.append(f' @property') + lines.append(f' def {prop_name}(self) -> Any: ...') + if prop.fset: + lines.append(f' @{prop_name}.setter') + lines.append(f' def {prop_name}(self, value: Any) -> None: ...') + else: + lines.append(f' {prop_name}: Any') + + # Add methods + if methods: + lines.append('') + for method_name, method in methods: + if method.__doc__: + sig, ret = parse_docstring_signature(method.__doc__) + if sig and ret: + lines.append(f' def {method_name}(self{sig[1:-1]}) {ret}: ...') + elif sig: + lines.append(f' def {method_name}(self{sig[1:-1]}) -> Any: ...') + else: + lines.append(f' def {method_name}(self, *args, **kwargs) -> Any: ...') + else: + lines.append(f' def {method_name}(self, *args, **kwargs) -> Any: ...') + + lines.append('') + return lines + +def generate_function_stub(func_name: str, func: Any) -> str: + """Generate stub for a function.""" + if func.__doc__: + sig, ret = parse_docstring_signature(func.__doc__) + if sig and ret: + return f'def {func_name}{sig} {ret}: ...' + elif sig: + return f'def {func_name}{sig} -> Any: ...' + + return f'def {func_name}(*args, **kwargs) -> Any: ...' + +def generate_stubs(): + """Generate the main mcrfpy.pyi file.""" + lines = [ + '"""Type stubs for McRogueFace Python API.', + '', + 'Auto-generated - do not edit directly.', + '"""', + '', + 'from typing import Any, List, Dict, Tuple, Optional, Callable, Union', + '', + '# Module documentation', + ] + + # Add module docstring as comment + if mcrfpy.__doc__: + for line in mcrfpy.__doc__.strip().split('\n')[:3]: + lines.append(f'# {line}') + + lines.extend(['', '# Classes', '']) + + # Collect all classes + classes = [] + functions = [] + constants = [] + + for name in sorted(dir(mcrfpy)): + if name.startswith('_'): + continue + + obj = getattr(mcrfpy, name) + + if isinstance(obj, type): + classes.append((name, obj)) + elif callable(obj): + functions.append((name, obj)) + elif not inspect.ismodule(obj): + constants.append((name, obj)) + + # Generate class stubs + for class_name, cls in classes: + lines.extend(generate_class_stub(class_name, cls)) + + # Generate function stubs + if functions: + lines.extend(['# Functions', '']) + for func_name, func in functions: + lines.append(generate_function_stub(func_name, func)) + lines.append('') + + # Generate constants + if constants: + lines.extend(['# Constants', '']) + for const_name, const in constants: + const_type = get_type_hint(type(const)) + lines.append(f'{const_name}: {const_type}') + + return '\n'.join(lines) + +def generate_automation_stubs(): + """Generate stubs for the automation submodule.""" + if not hasattr(mcrfpy, 'automation'): + return None + + automation = mcrfpy.automation + + lines = [ + '"""Type stubs for McRogueFace automation API."""', + '', + 'from typing import Optional, Tuple', + '', + ] + + # Get all automation functions + for name in sorted(dir(automation)): + if name.startswith('_'): + continue + + obj = getattr(automation, name) + if callable(obj): + lines.append(generate_function_stub(name, obj)) + + return '\n'.join(lines) + +def main(): + """Main entry point.""" + print("Generating type stubs for McRogueFace...") + + # Generate main module stubs + stubs = generate_stubs() + + # Create stubs directory + os.makedirs('stubs', exist_ok=True) + + # Write main module stubs + with open('stubs/mcrfpy.pyi', 'w') as f: + f.write(stubs) + print("Generated stubs/mcrfpy.pyi") + + # Generate automation module stubs if available + automation_stubs = generate_automation_stubs() + if automation_stubs: + os.makedirs('stubs/mcrfpy', exist_ok=True) + with open('stubs/mcrfpy/__init__.pyi', 'w') as f: + f.write(stubs) + with open('stubs/mcrfpy/automation.pyi', 'w') as f: + f.write(automation_stubs) + print("Generated stubs/mcrfpy/automation.pyi") + + print("\nType stubs generated successfully!") + print("\nTo use in your IDE:") + print("1. Add the 'stubs' directory to your PYTHONPATH") + print("2. Or configure your IDE to look for stubs in the 'stubs' directory") + print("3. Most IDEs will automatically detect .pyi files") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/generate_stubs_v2.py b/tools/generate_stubs_v2.py similarity index 100% rename from generate_stubs_v2.py rename to tools/generate_stubs_v2.py diff --git a/tools/ui_methods_documentation.py b/tools/ui_methods_documentation.py new file mode 100644 index 0000000..c5999ac --- /dev/null +++ b/tools/ui_methods_documentation.py @@ -0,0 +1,344 @@ +# Comprehensive UI Element Method Documentation +# This can be inserted into generate_api_docs_html.py in the method_docs dictionary + +ui_method_docs = { + # Base Drawable methods (inherited by all UI elements) + 'Drawable': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of this drawable element.', + 'returns': 'tuple: (x, y, width, height) representing the element\'s bounds', + 'note': 'The bounds are in screen coordinates and account for current position and size.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the element by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'This modifies the x and y position properties by the given amounts.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the element to new dimensions.', + 'args': [ + ('width', 'float', 'New width in pixels'), + ('height', 'float', 'New height in pixels') + ], + 'note': 'Behavior varies by element type. Some elements may ignore or constrain dimensions.' + } + }, + + # Caption-specific methods + 'Caption': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the text.', + 'returns': 'tuple: (x, y, width, height) based on text content and font size', + 'note': 'Bounds are automatically calculated from the rendered text dimensions.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the caption by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ] + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Set text wrapping bounds (limited support).', + 'args': [ + ('width', 'float', 'Maximum width for text wrapping'), + ('height', 'float', 'Currently unused') + ], + 'note': 'Full text wrapping is not yet implemented. This prepares for future multiline support.' + } + }, + + # Entity-specific methods + 'Entity': { + 'at': { + 'signature': 'at(x, y)', + 'description': 'Get the GridPointState at the specified grid coordinates relative to this entity.', + 'args': [ + ('x', 'int', 'Grid x offset from entity position'), + ('y', 'int', 'Grid y offset from entity position') + ], + 'returns': 'GridPointState: State of the grid point at the specified position', + 'note': 'Requires entity to be associated with a grid. Raises ValueError if not.' + }, + 'die': { + 'signature': 'die()', + 'description': 'Remove this entity from its parent grid.', + 'returns': 'None', + 'note': 'The entity object remains valid but is no longer rendered or updated.' + }, + 'index': { + 'signature': 'index()', + 'description': 'Get the index of this entity in its grid\'s entity collection.', + 'returns': 'int: Zero-based index in the parent grid\'s entity list', + 'note': 'Raises RuntimeError if not associated with a grid, ValueError if not found.' + }, + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the entity\'s sprite.', + 'returns': 'tuple: (x, y, width, height) of the sprite bounds', + 'note': 'Delegates to the internal sprite\'s get_bounds method.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the entity by a relative offset in pixels.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'Updates both sprite position and entity grid position.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Entities do not support direct resizing.', + 'args': [ + ('width', 'float', 'Ignored'), + ('height', 'float', 'Ignored') + ], + 'note': 'This method exists for interface compatibility but has no effect.' + } + }, + + # Frame-specific methods + 'Frame': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the frame.', + 'returns': 'tuple: (x, y, width, height) representing the frame bounds' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the frame and all its children by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'Child elements maintain their relative positions within the frame.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the frame to new dimensions.', + 'args': [ + ('width', 'float', 'New width in pixels'), + ('height', 'float', 'New height in pixels') + ], + 'note': 'Does not automatically resize children. Set clip_children=True to clip overflow.' + } + }, + + # Grid-specific methods + 'Grid': { + 'at': { + 'signature': 'at(x, y) or at((x, y))', + 'description': 'Get the GridPoint at the specified grid coordinates.', + 'args': [ + ('x', 'int', 'Grid x coordinate (0-based)'), + ('y', 'int', 'Grid y coordinate (0-based)') + ], + 'returns': 'GridPoint: The grid point at (x, y)', + 'note': 'Raises IndexError if coordinates are out of range. Accepts either two arguments or a tuple.', + 'example': 'point = grid.at(5, 3) # or grid.at((5, 3))' + }, + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the entire grid.', + 'returns': 'tuple: (x, y, width, height) of the grid\'s display area' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the grid display by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'Moves the entire grid viewport. Use center property to pan within the grid.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the grid\'s display viewport.', + 'args': [ + ('width', 'float', 'New viewport width in pixels'), + ('height', 'float', 'New viewport height in pixels') + ], + 'note': 'Changes the visible area, not the grid dimensions. Use zoom to scale content.' + } + }, + + # Sprite-specific methods + 'Sprite': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the sprite.', + 'returns': 'tuple: (x, y, width, height) based on texture size and scale', + 'note': 'Bounds account for current scale. Returns (x, y, 0, 0) if no texture.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the sprite by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ] + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the sprite by adjusting its scale.', + 'args': [ + ('width', 'float', 'Target width in pixels'), + ('height', 'float', 'Target height in pixels') + ], + 'note': 'Calculates and applies uniform scale to best fit the target dimensions.' + } + }, + + # Collection methods (shared by EntityCollection and UICollection) + 'EntityCollection': { + 'append': { + 'signature': 'append(entity)', + 'description': 'Add an entity to the end of the collection.', + 'args': [ + ('entity', 'Entity', 'The entity to add') + ] + }, + 'remove': { + 'signature': 'remove(entity)', + 'description': 'Remove the first occurrence of an entity from the collection.', + 'args': [ + ('entity', 'Entity', 'The entity to remove') + ], + 'note': 'Raises ValueError if entity is not found.' + }, + 'extend': { + 'signature': 'extend(iterable)', + 'description': 'Add multiple entities from an iterable.', + 'args': [ + ('iterable', 'iterable', 'An iterable of Entity objects') + ] + }, + 'count': { + 'signature': 'count(entity)', + 'description': 'Count occurrences of an entity in the collection.', + 'args': [ + ('entity', 'Entity', 'The entity to count') + ], + 'returns': 'int: Number of times the entity appears' + }, + 'index': { + 'signature': 'index(entity)', + 'description': 'Find the index of the first occurrence of an entity.', + 'args': [ + ('entity', 'Entity', 'The entity to find') + ], + 'returns': 'int: Zero-based index of the entity', + 'note': 'Raises ValueError if entity is not found.' + } + }, + + 'UICollection': { + 'append': { + 'signature': 'append(drawable)', + 'description': 'Add a drawable element to the end of the collection.', + 'args': [ + ('drawable', 'Drawable', 'Any UI element (Frame, Caption, Sprite, Grid)') + ] + }, + 'remove': { + 'signature': 'remove(drawable)', + 'description': 'Remove the first occurrence of a drawable from the collection.', + 'args': [ + ('drawable', 'Drawable', 'The drawable to remove') + ], + 'note': 'Raises ValueError if drawable is not found.' + }, + 'extend': { + 'signature': 'extend(iterable)', + 'description': 'Add multiple drawables from an iterable.', + 'args': [ + ('iterable', 'iterable', 'An iterable of Drawable objects') + ] + }, + 'count': { + 'signature': 'count(drawable)', + 'description': 'Count occurrences of a drawable in the collection.', + 'args': [ + ('drawable', 'Drawable', 'The drawable to count') + ], + 'returns': 'int: Number of times the drawable appears' + }, + 'index': { + 'signature': 'index(drawable)', + 'description': 'Find the index of the first occurrence of a drawable.', + 'args': [ + ('drawable', 'Drawable', 'The drawable to find') + ], + 'returns': 'int: Zero-based index of the drawable', + 'note': 'Raises ValueError if drawable is not found.' + } + } +} + +# Additional property documentation to complement the methods +ui_property_docs = { + 'Drawable': { + 'visible': 'bool: Whether this element is rendered (default: True)', + 'opacity': 'float: Transparency level from 0.0 (invisible) to 1.0 (opaque)', + 'z_index': 'int: Rendering order, higher values appear on top', + 'name': 'str: Optional name for finding elements', + 'x': 'float: Horizontal position in pixels', + 'y': 'float: Vertical position in pixels', + 'click': 'callable: Click event handler function' + }, + 'Caption': { + 'text': 'str: The displayed text content', + 'font': 'Font: Font used for rendering', + 'fill_color': 'Color: Text color', + 'outline_color': 'Color: Text outline color', + 'outline': 'float: Outline thickness in pixels', + 'w': 'float: Read-only computed width based on text', + 'h': 'float: Read-only computed height based on text' + }, + 'Entity': { + 'grid_x': 'float: X position in grid coordinates', + 'grid_y': 'float: Y position in grid coordinates', + 'sprite_index': 'int: Index of sprite in texture atlas', + 'texture': 'Texture: Texture used for rendering', + 'gridstate': 'list: Read-only list of GridPointState objects' + }, + 'Frame': { + 'w': 'float: Width in pixels', + 'h': 'float: Height in pixels', + 'fill_color': 'Color: Background fill color', + 'outline_color': 'Color: Border color', + 'outline': 'float: Border thickness in pixels', + 'children': 'UICollection: Child drawable elements', + 'clip_children': 'bool: Whether to clip children to frame bounds' + }, + 'Grid': { + 'grid_size': 'tuple: Read-only (width, height) in tiles', + 'grid_x': 'int: Read-only width in tiles', + 'grid_y': 'int: Read-only height in tiles', + 'tile_width': 'int: Width of each tile in pixels', + 'tile_height': 'int: Height of each tile in pixels', + 'center': 'tuple: (x, y) center point for viewport', + 'zoom': 'float: Scale factor for rendering', + 'texture': 'Texture: Tile texture atlas', + 'background_color': 'Color: Grid background color', + 'entities': 'EntityCollection: Entities in this grid', + 'points': 'list: 2D array of GridPoint objects' + }, + 'Sprite': { + 'texture': 'Texture: The displayed texture', + 'sprite_index': 'int: Index in texture atlas', + 'scale': 'float: Scaling factor', + 'w': 'float: Read-only computed width (texture width * scale)', + 'h': 'float: Read-only computed height (texture height * scale)' + } +} \ No newline at end of file diff --git a/wikicrayons_colors.txt b/wikicrayons_colors.txt deleted file mode 100644 index 2bbfe43..0000000 --- a/wikicrayons_colors.txt +++ /dev/null @@ -1,168 +0,0 @@ -Red #ED0A3F -Maroon #C32148 -Scarlet #FD0E35 -Brick Red #C62D42 -English Vermilion #CC474B -Madder Lake #CC3336 -Permanent Geranium Lake #E12C2C -Maximum Red #D92121 -Chestnut #B94E48 -Orange-Red #FF5349 -Sunset Orange #FE4C40 -Bittersweet #FE6F5E -Dark Venetian Red #B33B24 -Venetian Red #CC553D -Light Venetian Red #E6735C -Vivid Tangerine #FF9980 -Middle Red #E58E73 -Burnt Orange #FF7034 -Red-Orange #FF3F34 -Orange #FF8833 -Macaroni and Cheese #FFB97B -Middle Yellow Red #ECAC76 -Mango Tango #E77200 -Yellow-Orange #FFAE42 -Maximum Yellow Red #F2BA49 -Banana Mania #FBE7B2 -Maize #F2C649 -Orange-Yellow #F8D568 -Goldenrod #FCD667 -Dandelion #FED85D -Yellow #FBE870 -Green-Yellow #F1E788 -Middle Yellow #FFEB00 -Olive Green #B5B35C -Spring Green #ECEBBD -Maximum Yellow #FAFA37 -Canary #FFFF99 -Lemon Yellow #FFFF9F -Maximum Green Yellow #D9E650 -Middle Green Yellow #ACBF60 -Inchworm #B0E313 -Light Chrome Green #BEE64B -Yellow-Green #C5E17A -Maximum Green #5E8C31 -Asparagus #7BA05B -Granny Smith Apple #9DE093 -Fern #63B76C -Middle Green #4D8C57 -Green #01A638 -Medium Chrome Green #6CA67C -Forest Green #5FA777 -Sea Green #93DFB8 -Shamrock #33CC99 -Mountain Meadow #1AB385 -Jungle Green #29AB87 -Caribbean Green #00CC99 -Tropical Rain Forest #00755E -Middle Blue Green #8DD9CC -Pine Green #01796F -Maximum Blue Green #30BFBF -Robin's Egg Blue #00CCCC -Teal Blue #008080 -Light Blue #8FD8D8 -Aquamarine #458B74 -Turquoise Blue #6CDAE7 -Outer Space #2D383A -Sky Blue #76D7EA -Middle Blue #7ED4E6 -Blue-Green #0095B7 -Pacific Blue #009DC4 -Cerulean #02A4D3 -Maximum Blue #47ABCC -Blue (I) #2EB4E6 -Cerulean Blue #339ACC -Cornflower #93CCEA -Green-Blue #2887C8 -Midnight Blue #003366 -Navy Blue #0066CC -Denim #1560BD -Blue (III) #0066FF -Cadet Blue #A9B2C3 -Periwinkle #C3CDE6 -Blue (II) #4570E6 -Bluetiful #3C69E7 -Wild Blue Yonder #7A89B8 -Indigo #4F69C6 -Manatee #8D90A1 -Cobalt Blue #8C90C8 -Celestial Blue #7070CC -Blue Bell #9999CC -Maximum Blue Purple #ACACE6 -Violet-Blue #766EC8 -Blue-Violet #6456B7 -Ultramarine Blue #3F26BF -Middle Blue Purple #8B72BE -Purple Heart #652DC1 -Royal Purple #6B3FA0 -Violet (II) #8359A3 -Medium Violet #8F47B3 -Wisteria #C9A0DC -Lavender (I) #BF8FCC -Vivid Violet #803790 -Maximum Purple #733380 -Purple Mountains' Majesty #D6AEDD -Fuchsia #C154C1 -Pink Flamingo #F2583E -Violet (I) #732E6C -Brilliant Rose #E667CE -Orchid #E29CD2 -Plum #843179 -Medium Rose #D96CBE -Thistle #D8BFD8 -Mulberry #C8509B -Red-Violet #BB3385 -Middle Purple #D982B5 -Maximum Red Purple #A63A79 -Jazzberry Jam #A50B5E -Eggplant #614051 -Magenta #F653A6 -Cerise #DA3287 -Wild Strawberry #FF3399 -Lavender (II) #FBAED2 -Cotton Candy #FFB7D5 -Carnation Pink #FFA6C9 -Violet-Red #F7468A -Razzmatazz #E30B5C -Piggy Pink #FDD7E4 -Carmine #E62E6B -Blush #DB5079 -Tickle Me Pink #FC80A5 -Mauvelous #F091A9 -Salmon #FF91A4 -Middle Red Purple #A55353 -Mahogany #CA3435 -Melon #FEBAAD -Pink Sherbert #F7A38E -Burnt Sienna #E97451 -Brown #AF593E -Sepia #9E5B40 -Fuzzy Wuzzy #87421F -Beaver #926F5B -Tumbleweed #DEA681 -Raw Sienna #D27D46 -Van Dyke Brown #664228 -Tan #FA9D5A -Desert Sand #EDC9AF -Peach #FFCBA4 -Burnt Umber #805533 -Apricot #FDD5B1 -Almond #EED9C4 -Raw Umber #665233 -Shadow #837050 -Raw Sienna (I) #E6BC5C -Gold (I) #92926E -Gold (II) #E6BE8A -Silver #C9C0BB -Copper #DA8A67 -Antique Brass #C88A65 -Black #000000 -Charcoal Gray #736A62 -Gray #8B8680 -Blue-Gray #C8C8CD -Timberwolf #D9D6CF -White #FFFFFF -Crayellow #F1D651[6] -Cool Mint #DDEBEC[6] -Oatmeal #D9DAD2[6] -Powder Blue #C0D5F0[6] diff --git a/xkcd_colors.txt b/xkcd_colors.txt deleted file mode 100644 index eb3cd41..0000000 --- a/xkcd_colors.txt +++ /dev/null @@ -1,949 +0,0 @@ -cloudy blue #acc2d9 -dark pastel green #56ae57 -dust #b2996e -electric lime #a8ff04 -fresh green #69d84f -light eggplant #894585 -nasty green #70b23f -really light blue #d4ffff -tea #65ab7c -warm purple #952e8f -yellowish tan #fcfc81 -cement #a5a391 -dark grass green #388004 -dusty teal #4c9085 -grey teal #5e9b8a -macaroni and cheese #efb435 -pinkish tan #d99b82 -spruce #0a5f38 -strong blue #0c06f7 -toxic green #61de2a -windows blue #3778bf -blue blue #2242c7 -blue with a hint of purple #533cc6 -booger #9bb53c -bright sea green #05ffa6 -dark green blue #1f6357 -deep turquoise #017374 -green teal #0cb577 -strong pink #ff0789 -bland #afa88b -deep aqua #08787f -lavender pink #dd85d7 -light moss green #a6c875 -light seafoam green #a7ffb5 -olive yellow #c2b709 -pig pink #e78ea5 -deep lilac #966ebd -desert #ccad60 -dusty lavender #ac86a8 -purpley grey #947e94 -purply #983fb2 -candy pink #ff63e9 -light pastel green #b2fba5 -boring green #63b365 -kiwi green #8ee53f -light grey green #b7e1a1 -orange pink #ff6f52 -tea green #bdf8a3 -very light brown #d3b683 -egg shell #fffcc4 -eggplant purple #430541 -powder pink #ffb2d0 -reddish grey #997570 -baby shit brown #ad900d -liliac #c48efd -stormy blue #507b9c -ugly brown #7d7103 -custard #fffd78 -darkish pink #da467d -deep brown #410200 -greenish beige #c9d179 -manilla #fffa86 -off blue #5684ae -battleship grey #6b7c85 -browny green #6f6c0a -bruise #7e4071 -kelley green #009337 -sickly yellow #d0e429 -sunny yellow #fff917 -azul #1d5dec -darkgreen #054907 -green/yellow #b5ce08 -lichen #8fb67b -light light green #c8ffb0 -pale gold #fdde6c -sun yellow #ffdf22 -tan green #a9be70 -burple #6832e3 -butterscotch #fdb147 -toupe #c7ac7d -dark cream #fff39a -indian red #850e04 -light lavendar #efc0fe -poison green #40fd14 -baby puke green #b6c406 -bright yellow green #9dff00 -charcoal grey #3c4142 -squash #f2ab15 -cinnamon #ac4f06 -light pea green #c4fe82 -radioactive green #2cfa1f -raw sienna #9a6200 -baby purple #ca9bf7 -cocoa #875f42 -light royal blue #3a2efe -orangeish #fd8d49 -rust brown #8b3103 -sand brown #cba560 -swamp #698339 -tealish green #0cdc73 -burnt siena #b75203 -camo #7f8f4e -dusk blue #26538d -fern #63a950 -old rose #c87f89 -pale light green #b1fc99 -peachy pink #ff9a8a -rosy pink #f6688e -light bluish green #76fda8 -light bright green #53fe5c -light neon green #4efd54 -light seafoam #a0febf -tiffany blue #7bf2da -washed out green #bcf5a6 -browny orange #ca6b02 -nice blue #107ab0 -sapphire #2138ab -greyish teal #719f91 -orangey yellow #fdb915 -parchment #fefcaf -straw #fcf679 -very dark brown #1d0200 -terracota #cb6843 -ugly blue #31668a -clear blue #247afd -creme #ffffb6 -foam green #90fda9 -grey/green #86a17d -light gold #fddc5c -seafoam blue #78d1b6 -topaz #13bbaf -violet pink #fb5ffc -wintergreen #20f986 -yellow tan #ffe36e -dark fuchsia #9d0759 -indigo blue #3a18b1 -light yellowish green #c2ff89 -pale magenta #d767ad -rich purple #720058 -sunflower yellow #ffda03 -green/blue #01c08d -leather #ac7434 -racing green #014600 -vivid purple #9900fa -dark royal blue #02066f -hazel #8e7618 -muted pink #d1768f -booger green #96b403 -canary #fdff63 -cool grey #95a3a6 -dark taupe #7f684e -darkish purple #751973 -true green #089404 -coral pink #ff6163 -dark sage #598556 -dark slate blue #214761 -flat blue #3c73a8 -mushroom #ba9e88 -rich blue #021bf9 -dirty purple #734a65 -greenblue #23c48b -icky green #8fae22 -light khaki #e6f2a2 -warm blue #4b57db -dark hot pink #d90166 -deep sea blue #015482 -carmine #9d0216 -dark yellow green #728f02 -pale peach #ffe5ad -plum purple #4e0550 -golden rod #f9bc08 -neon red #ff073a -old pink #c77986 -very pale blue #d6fffe -blood orange #fe4b03 -grapefruit #fd5956 -sand yellow #fce166 -clay brown #b2713d -dark blue grey #1f3b4d -flat green #699d4c -light green blue #56fca2 -warm pink #fb5581 -dodger blue #3e82fc -gross green #a0bf16 -ice #d6fffa -metallic blue #4f738e -pale salmon #ffb19a -sap green #5c8b15 -algae #54ac68 -bluey grey #89a0b0 -greeny grey #7ea07a -highlighter green #1bfc06 -light light blue #cafffb -light mint #b6ffbb -raw umber #a75e09 -vivid blue #152eff -deep lavender #8d5eb7 -dull teal #5f9e8f -light greenish blue #63f7b4 -mud green #606602 -pinky #fc86aa -red wine #8c0034 -shit green #758000 -tan brown #ab7e4c -darkblue #030764 -rosa #fe86a4 -lipstick #d5174e -pale mauve #fed0fc -claret #680018 -dandelion #fedf08 -orangered #fe420f -poop green #6f7c00 -ruby #ca0147 -dark #1b2431 -greenish turquoise #00fbb0 -pastel red #db5856 -piss yellow #ddd618 -bright cyan #41fdfe -dark coral #cf524e -algae green #21c36f -darkish red #a90308 -reddy brown #6e1005 -blush pink #fe828c -camouflage green #4b6113 -lawn green #4da409 -putty #beae8a -vibrant blue #0339f8 -dark sand #a88f59 -purple/blue #5d21d0 -saffron #feb209 -twilight #4e518b -warm brown #964e02 -bluegrey #85a3b2 -bubble gum pink #ff69af -duck egg blue #c3fbf4 -greenish cyan #2afeb7 -petrol #005f6a -royal #0c1793 -butter #ffff81 -dusty orange #f0833a -off yellow #f1f33f -pale olive green #b1d27b -orangish #fc824a -leaf #71aa34 -light blue grey #b7c9e2 -dried blood #4b0101 -lightish purple #a552e6 -rusty red #af2f0d -lavender blue #8b88f8 -light grass green #9af764 -light mint green #a6fbb2 -sunflower #ffc512 -velvet #750851 -brick orange #c14a09 -lightish red #fe2f4a -pure blue #0203e2 -twilight blue #0a437a -violet red #a50055 -yellowy brown #ae8b0c -carnation #fd798f -muddy yellow #bfac05 -dark seafoam green #3eaf76 -deep rose #c74767 -dusty red #b9484e -grey/blue #647d8e -lemon lime #bffe28 -purple/pink #d725de -brown yellow #b29705 -purple brown #673a3f -wisteria #a87dc2 -banana yellow #fafe4b -lipstick red #c0022f -water blue #0e87cc -brown grey #8d8468 -vibrant purple #ad03de -baby green #8cff9e -barf green #94ac02 -eggshell blue #c4fff7 -sandy yellow #fdee73 -cool green #33b864 -pale #fff9d0 -blue/grey #758da3 -hot magenta #f504c9 -greyblue #77a1b5 -purpley #8756e4 -baby shit green #889717 -brownish pink #c27e79 -dark aquamarine #017371 -diarrhea #9f8303 -light mustard #f7d560 -pale sky blue #bdf6fe -turtle green #75b84f -bright olive #9cbb04 -dark grey blue #29465b -greeny brown #696006 -lemon green #adf802 -light periwinkle #c1c6fc -seaweed green #35ad6b -sunshine yellow #fffd37 -ugly purple #a442a0 -medium pink #f36196 -puke brown #947706 -very light pink #fff4f2 -viridian #1e9167 -bile #b5c306 -faded yellow #feff7f -very pale green #cffdbc -vibrant green #0add08 -bright lime #87fd05 -spearmint #1ef876 -light aquamarine #7bfdc7 -light sage #bcecac -yellowgreen #bbf90f -baby poo #ab9004 -dark seafoam #1fb57a -deep teal #00555a -heather #a484ac -rust orange #c45508 -dirty blue #3f829d -fern green #548d44 -bright lilac #c95efb -weird green #3ae57f -peacock blue #016795 -avocado green #87a922 -faded orange #f0944d -grape purple #5d1451 -hot green #25ff29 -lime yellow #d0fe1d -mango #ffa62b -shamrock #01b44c -bubblegum #ff6cb5 -purplish brown #6b4247 -vomit yellow #c7c10c -pale cyan #b7fffa -key lime #aeff6e -tomato red #ec2d01 -lightgreen #76ff7b -merlot #730039 -night blue #040348 -purpleish pink #df4ec8 -apple #6ecb3c -baby poop green #8f9805 -green apple #5edc1f -heliotrope #d94ff5 -yellow/green #c8fd3d -almost black #070d0d -cool blue #4984b8 -leafy green #51b73b -mustard brown #ac7e04 -dusk #4e5481 -dull brown #876e4b -frog green #58bc08 -vivid green #2fef10 -bright light green #2dfe54 -fluro green #0aff02 -kiwi #9cef43 -seaweed #18d17b -navy green #35530a -ultramarine blue #1805db -iris #6258c4 -pastel orange #ff964f -yellowish orange #ffab0f -perrywinkle #8f8ce7 -tealish #24bca8 -dark plum #3f012c -pear #cbf85f -pinkish orange #ff724c -midnight purple #280137 -light urple #b36ff6 -dark mint #48c072 -greenish tan #bccb7a -light burgundy #a8415b -turquoise blue #06b1c4 -ugly pink #cd7584 -sandy #f1da7a -electric pink #ff0490 -muted purple #805b87 -mid green #50a747 -greyish #a8a495 -neon yellow #cfff04 -banana #ffff7e -carnation pink #ff7fa7 -tomato #ef4026 -sea #3c9992 -muddy brown #886806 -turquoise green #04f489 -buff #fef69e -fawn #cfaf7b -muted blue #3b719f -pale rose #fdc1c5 -dark mint green #20c073 -amethyst #9b5fc0 -blue/green #0f9b8e -chestnut #742802 -sick green #9db92c -pea #a4bf20 -rusty orange #cd5909 -stone #ada587 -rose red #be013c -pale aqua #b8ffeb -deep orange #dc4d01 -earth #a2653e -mossy green #638b27 -grassy green #419c03 -pale lime green #b1ff65 -light grey blue #9dbcd4 -pale grey #fdfdfe -asparagus #77ab56 -blueberry #464196 -purple red #990147 -pale lime #befd73 -greenish teal #32bf84 -caramel #af6f09 -deep magenta #a0025c -light peach #ffd8b1 -milk chocolate #7f4e1e -ocher #bf9b0c -off green #6ba353 -purply pink #f075e6 -lightblue #7bc8f6 -dusky blue #475f94 -golden #f5bf03 -light beige #fffeb6 -butter yellow #fffd74 -dusky purple #895b7b -french blue #436bad -ugly yellow #d0c101 -greeny yellow #c6f808 -orangish red #f43605 -shamrock green #02c14d -orangish brown #b25f03 -tree green #2a7e19 -deep violet #490648 -gunmetal #536267 -blue/purple #5a06ef -cherry #cf0234 -sandy brown #c4a661 -warm grey #978a84 -dark indigo #1f0954 -midnight #03012d -bluey green #2bb179 -grey pink #c3909b -soft purple #a66fb5 -blood #770001 -brown red #922b05 -medium grey #7d7f7c -berry #990f4b -poo #8f7303 -purpley pink #c83cb9 -light salmon #fea993 -snot #acbb0d -easter purple #c071fe -light yellow green #ccfd7f -dark navy blue #00022e -drab #828344 -light rose #ffc5cb -rouge #ab1239 -purplish red #b0054b -slime green #99cc04 -baby poop #937c00 -irish green #019529 -pink/purple #ef1de7 -dark navy #000435 -greeny blue #42b395 -light plum #9d5783 -pinkish grey #c8aca9 -dirty orange #c87606 -rust red #aa2704 -pale lilac #e4cbff -orangey red #fa4224 -primary blue #0804f9 -kermit green #5cb200 -brownish purple #76424e -murky green #6c7a0e -wheat #fbdd7e -very dark purple #2a0134 -bottle green #044a05 -watermelon #fd4659 -deep sky blue #0d75f8 -fire engine red #fe0002 -yellow ochre #cb9d06 -pumpkin orange #fb7d07 -pale olive #b9cc81 -light lilac #edc8ff -lightish green #61e160 -carolina blue #8ab8fe -mulberry #920a4e -shocking pink #fe02a2 -auburn #9a3001 -bright lime green #65fe08 -celadon #befdb7 -pinkish brown #b17261 -poo brown #885f01 -bright sky blue #02ccfe -celery #c1fd95 -dirt brown #836539 -strawberry #fb2943 -dark lime #84b701 -copper #b66325 -medium brown #7f5112 -muted green #5fa052 -robin's egg #6dedfd -bright aqua #0bf9ea -bright lavender #c760ff -ivory #ffffcb -very light purple #f6cefc -light navy #155084 -pink red #f5054f -olive brown #645403 -poop brown #7a5901 -mustard green #a8b504 -ocean green #3d9973 -very dark blue #000133 -dusty green #76a973 -light navy blue #2e5a88 -minty green #0bf77d -adobe #bd6c48 -barney #ac1db8 -jade green #2baf6a -bright light blue #26f7fd -light lime #aefd6c -dark khaki #9b8f55 -orange yellow #ffad01 -ocre #c69c04 -maize #f4d054 -faded pink #de9dac -british racing green #05480d -sandstone #c9ae74 -mud brown #60460f -light sea green #98f6b0 -robin egg blue #8af1fe -aqua marine #2ee8bb -dark sea green #11875d -soft pink #fdb0c0 -orangey brown #b16002 -cherry red #f7022a -burnt yellow #d5ab09 -brownish grey #86775f -camel #c69f59 -purplish grey #7a687f -marine #042e60 -greyish pink #c88d94 -pale turquoise #a5fbd5 -pastel yellow #fffe71 -bluey purple #6241c7 -canary yellow #fffe40 -faded red #d3494e -sepia #985e2b -coffee #a6814c -bright magenta #ff08e8 -mocha #9d7651 -ecru #feffca -purpleish #98568d -cranberry #9e003a -darkish green #287c37 -brown orange #b96902 -dusky rose #ba6873 -melon #ff7855 -sickly green #94b21c -silver #c5c9c7 -purply blue #661aee -purpleish blue #6140ef -hospital green #9be5aa -shit brown #7b5804 -mid blue #276ab3 -amber #feb308 -easter green #8cfd7e -soft blue #6488ea -cerulean blue #056eee -golden brown #b27a01 -bright turquoise #0ffef9 -red pink #fa2a55 -red purple #820747 -greyish brown #7a6a4f -vermillion #f4320c -russet #a13905 -steel grey #6f828a -lighter purple #a55af4 -bright violet #ad0afd -prussian blue #004577 -slate green #658d6d -dirty pink #ca7b80 -dark blue green #005249 -pine #2b5d34 -yellowy green #bff128 -dark gold #b59410 -bluish #2976bb -darkish blue #014182 -dull red #bb3f3f -pinky red #fc2647 -bronze #a87900 -pale teal #82cbb2 -military green #667c3e -barbie pink #fe46a5 -bubblegum pink #fe83cc -pea soup green #94a617 -dark mustard #a88905 -shit #7f5f00 -medium purple #9e43a2 -very dark green #062e03 -dirt #8a6e45 -dusky pink #cc7a8b -red violet #9e0168 -lemon yellow #fdff38 -pistachio #c0fa8b -dull yellow #eedc5b -dark lime green #7ebd01 -denim blue #3b5b92 -teal blue #01889f -lightish blue #3d7afd -purpley blue #5f34e7 -light indigo #6d5acf -swamp green #748500 -brown green #706c11 -dark maroon #3c0008 -hot purple #cb00f5 -dark forest green #002d04 -faded blue #658cbb -drab green #749551 -light lime green #b9ff66 -snot green #9dc100 -yellowish #faee66 -light blue green #7efbb3 -bordeaux #7b002c -light mauve #c292a1 -ocean #017b92 -marigold #fcc006 -muddy green #657432 -dull orange #d8863b -steel #738595 -electric purple #aa23ff -fluorescent green #08ff08 -yellowish brown #9b7a01 -blush #f29e8e -soft green #6fc276 -bright orange #ff5b00 -lemon #fdff52 -purple grey #866f85 -acid green #8ffe09 -pale lavender #eecffe -violet blue #510ac9 -light forest green #4f9153 -burnt red #9f2305 -khaki green #728639 -cerise #de0c62 -faded purple #916e99 -apricot #ffb16d -dark olive green #3c4d03 -grey brown #7f7053 -green grey #77926f -true blue #010fcc -pale violet #ceaefa -periwinkle blue #8f99fb -light sky blue #c6fcff -blurple #5539cc -green brown #544e03 -bluegreen #017a79 -bright teal #01f9c6 -brownish yellow #c9b003 -pea soup #929901 -forest #0b5509 -barney purple #a00498 -ultramarine #2000b1 -purplish #94568c -puke yellow #c2be0e -bluish grey #748b97 -dark periwinkle #665fd1 -dark lilac #9c6da5 -reddish #c44240 -light maroon #a24857 -dusty purple #825f87 -terra cotta #c9643b -avocado #90b134 -marine blue #01386a -teal green #25a36f -slate grey #59656d -lighter green #75fd63 -electric green #21fc0d -dusty blue #5a86ad -golden yellow #fec615 -bright yellow #fffd01 -light lavender #dfc5fe -umber #b26400 -poop #7f5e00 -dark peach #de7e5d -jungle green #048243 -eggshell #ffffd4 -denim #3b638c -yellow brown #b79400 -dull purple #84597e -chocolate brown #411900 -wine red #7b0323 -neon blue #04d9ff -dirty green #667e2c -light tan #fbeeac -ice blue #d7fffe -cadet blue #4e7496 -dark mauve #874c62 -very light blue #d5ffff -grey purple #826d8c -pastel pink #ffbacd -very light green #d1ffbd -dark sky blue #448ee4 -evergreen #05472a -dull pink #d5869d -aubergine #3d0734 -mahogany #4a0100 -reddish orange #f8481c -deep green #02590f -vomit green #89a203 -purple pink #e03fd8 -dusty pink #d58a94 -faded green #7bb274 -camo green #526525 -pinky purple #c94cbe -pink purple #db4bda -brownish red #9e3623 -dark rose #b5485d -mud #735c12 -brownish #9c6d57 -emerald green #028f1e -pale brown #b1916e -dull blue #49759c -burnt umber #a0450e -medium green #39ad48 -clay #b66a50 -light aqua #8cffdb -light olive green #a4be5c -brownish orange #cb7723 -dark aqua #05696b -purplish pink #ce5dae -dark salmon #c85a53 -greenish grey #96ae8d -jade #1fa774 -ugly green #7a9703 -dark beige #ac9362 -emerald #01a049 -pale red #d9544d -light magenta #fa5ff7 -sky #82cafc -light cyan #acfffc -yellow orange #fcb001 -reddish purple #910951 -reddish pink #fe2c54 -orchid #c875c4 -dirty yellow #cdc50a -orange red #fd411e -deep red #9a0200 -orange brown #be6400 -cobalt blue #030aa7 -neon pink #fe019a -rose pink #f7879a -greyish purple #887191 -raspberry #b00149 -aqua green #12e193 -salmon pink #fe7b7c -tangerine #ff9408 -brownish green #6a6e09 -red brown #8b2e16 -greenish brown #696112 -pumpkin #e17701 -pine green #0a481e -charcoal #343837 -baby pink #ffb7ce -cornflower #6a79f7 -blue violet #5d06e9 -chocolate #3d1c02 -greyish green #82a67d -scarlet #be0119 -green yellow #c9ff27 -dark olive #373e02 -sienna #a9561e -pastel purple #caa0ff -terracotta #ca6641 -aqua blue #02d8e9 -sage green #88b378 -blood red #980002 -deep pink #cb0162 -grass #5cac2d -moss #769958 -pastel blue #a2bffe -bluish green #10a674 -green blue #06b48b -dark tan #af884a -greenish blue #0b8b87 -pale orange #ffa756 -vomit #a2a415 -forrest green #154406 -dark lavender #856798 -dark violet #34013f -purple blue #632de9 -dark cyan #0a888a -olive drab #6f7632 -pinkish #d46a7e -cobalt #1e488f -neon purple #bc13fe -light turquoise #7ef4cc -apple green #76cd26 -dull green #74a662 -wine #80013f -powder blue #b1d1fc -off white #ffffe4 -electric blue #0652ff -dark turquoise #045c5a -blue purple #5729ce -azure #069af3 -bright red #ff000d -pinkish red #f10c45 -cornflower blue #5170d7 -light olive #acbf69 -grape #6c3461 -greyish blue #5e819d -purplish blue #601ef9 -yellowish green #b0dd16 -greenish yellow #cdfd02 -medium blue #2c6fbb -dusty rose #c0737a -light violet #d6b4fc -midnight blue #020035 -bluish purple #703be7 -red orange #fd3c06 -dark magenta #960056 -greenish #40a368 -ocean blue #03719c -coral #fc5a50 -cream #ffffc2 -reddish brown #7f2b0a -burnt sienna #b04e0f -brick #a03623 -sage #87ae73 -grey green #789b73 -white #ffffff -robin's egg blue #98eff9 -moss green #658b38 -steel blue #5a7d9a -eggplant #380835 -light yellow #fffe7a -leaf green #5ca904 -light grey #d8dcd6 -puke #a5a502 -pinkish purple #d648d7 -sea blue #047495 -pale purple #b790d4 -slate blue #5b7c99 -blue grey #607c8e -hunter green #0b4008 -fuchsia #ed0dd9 -crimson #8c000f -pale yellow #ffff84 -ochre #bf9005 -mustard yellow #d2bd0a -light red #ff474c -cerulean #0485d1 -pale pink #ffcfdc -deep blue #040273 -rust #a83c09 -light teal #90e4c1 -slate #516572 -goldenrod #fac205 -dark yellow #d5b60a -dark grey #363737 -army green #4b5d16 -grey blue #6b8ba4 -seafoam #80f9ad -puce #a57e52 -spring green #a9f971 -dark orange #c65102 -sand #e2ca76 -pastel green #b0ff9d -mint #9ffeb0 -light orange #fdaa48 -bright pink #fe01b1 -chartreuse #c1f80a -deep purple #36013f -dark brown #341c02 -taupe #b9a281 -pea green #8eab12 -puke green #9aae07 -kelly green #02ab2e -seafoam green #7af9ab -blue green #137e6d -khaki #aaa662 -burgundy #610023 -dark teal #014d4e -brick red #8f1402 -royal purple #4b006e -plum #580f41 -mint green #8fff9f -gold #dbb40c -baby blue #a2cffe -yellow green #c0fb2d -bright purple #be03fd -dark red #840000 -pale blue #d0fefe -grass green #3f9b0b -navy #01153e -aquamarine #04d8b2 -burnt orange #c04e01 -neon green #0cff0c -bright blue #0165fc -rose #cf6275 -light pink #ffd1df -mustard #ceb301 -indigo #380282 -lime #aaff32 -sea green #53fca1 -periwinkle #8e82fe -dark pink #cb416b -olive green #677a04 -peach #ffb07c -pale green #c7fdb5 -light brown #ad8150 -hot pink #ff028d -black #000000 -lilac #cea2fd -navy blue #001146 -royal blue #0504aa -beige #e6daa6 -salmon #ff796c -olive #6e750e -maroon #650021 -bright green #01ff07 -dark purple #35063e -mauve #ae7181 -forest green #06470c -aqua #13eac9 -cyan #00ffff -tan #d1b26f -dark blue #00035b -lavender #c79fef -turquoise #06c2ac -dark green #033500 -violet #9a0eea -light purple #bf77f6 -lime green #89fe05 -grey #929591 -sky blue #75bbfd -yellow #ffff14 -magenta #c20078 -light green #96f97b -orange #f97306 -teal #029386 -light blue #95d0fc -red #e50000 -brown #653700 -pink #ff81c0 -blue #0343df -green #15b01a -purple #7e1e9c