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.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..9cdf3fa --- /dev/null +++ b/docs/api_reference_dynamic.html @@ -0,0 +1,923 @@ + + + + + + McRogueFace API Reference + + + +
+

McRogueFace API Reference

+

Generated on 2025-07-10 01:13:53

+

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:

+ +
+
get_current_value(...)
+

Get the current interpolated value

+
+ +
+
start(...)
+

Start the animation on a target UIDrawable

+
+ +
+
updateUpdate the animation by deltaTime (returns True if still running)
+
+
+ +
+

Caption

+

Inherits from: 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. + +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_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

+

Inherits from: Drawable

+

UIEntity objects

+

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(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_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(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:

+ +
+
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(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_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 object for scheduled callbacks

+

Methods:

+ +
+
cancel(...)
+

Cancel the timer and remove it from the system

+
+ +
+
pause(...)
+

Pause the timer

+
+ +
+
restart(...)
+

Restart the timer from the current time

+
+ +
+
resume(...)
+

Resume a paused timer

+
+
+ +
+

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..39353c3 --- /dev/null +++ b/docs/stubs/mcrfpy.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/__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/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/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..d3b1e20 --- /dev/null +++ b/tests/demos/animation_sizzle_reel.py @@ -0,0 +1,615 @@ +#!/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 = Grid(100, 150, grid_size=(20, 15), texture=texture, + tile_width=24, tile_height=24) + 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, sprite_index=8) + 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, sprite_index=12) + 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..e12f9bc --- /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(ui[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..d24cc1a --- /dev/null +++ b/tests/demos/animation_sizzle_reel_working.py @@ -0,0 +1,318 @@ +#!/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"]: + if not mcrfpy.getTimer(timer): + continue + 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.py b/tests/demos/exhaustive_api_demo.py new file mode 100644 index 0000000..76d36cc --- /dev/null +++ b/tests/demos/exhaustive_api_demo.py @@ -0,0 +1,1204 @@ +#!/usr/bin/env python3 +""" +McRogueFace Exhaustive API Demonstration +======================================== + +This script demonstrates EVERY constructor variant and EVERY method +for EVERY UI object type in McRogueFace. It serves as both a test +suite and a comprehensive API reference with working examples. + +The script is organized by UI object type, showing: +1. All constructor variants (empty, partial args, full args) +2. All properties (get and set) +3. All methods with different parameter combinations +4. Special behaviors and edge cases + +Author: Claude +Purpose: Complete API demonstration and validation +""" + +import mcrfpy +from mcrfpy import Color, Vector, Font, Texture, Frame, Caption, Sprite, Grid, Entity +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 = Color() + print_test(f"Color() = ({c1.r}, {c1.g}, {c1.b}, {c1.a})") + + # Single value (grayscale) + c2 = Color(128) + print_test(f"Color(128) = ({c2.r}, {c2.g}, {c2.b}, {c2.a})") + + # RGB only (alpha defaults to 255) + c3 = Color(255, 128, 0) + print_test(f"Color(255, 128, 0) = ({c3.r}, {c3.g}, {c3.b}, {c3.a})") + + # Full RGBA + c4 = Color(100, 150, 200, 128) + print_test(f"Color(100, 150, 200, 128) = ({c4.r}, {c4.g}, {c4.b}, {c4.a})") + + # From hex string + c5 = Color.from_hex("#FF8800") + print_test(f"Color.from_hex('#FF8800') = ({c5.r}, {c5.g}, {c5.b}, {c5.a})") + + c6 = Color.from_hex("#FF8800AA") + print_test(f"Color.from_hex('#FF8800AA') = ({c6.r}, {c6.g}, {c6.b}, {c6.a})") + + # Methods + print("\n Methods:") + + # to_hex + hex_str = c4.to_hex() + print_test(f"Color(100, 150, 200, 128).to_hex() = '{hex_str}'") + + # lerp (linear interpolation) + c_start = Color(0, 0, 0) + c_end = Color(255, 255, 255) + c_mid = c_start.lerp(c_end, 0.5) + print_test(f"Black.lerp(White, 0.5) = ({c_mid.r}, {c_mid.g}, {c_mid.b}, {c_mid.a})") + + # Property access + print("\n Properties:") + c = 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_vector_api(): + """Test all Vector constructors and methods""" + print_section("VECTOR API TESTS") + + # Constructor variants + print("\n Constructors:") + + # Empty constructor + v1 = Vector() + print_test(f"Vector() = ({v1.x}, {v1.y})") + + # Single value (both x and y) + v2 = Vector(5.0) + print_test(f"Vector(5.0) = ({v2.x}, {v2.y})") + + # Full x, y + v3 = Vector(10.5, 20.3) + print_test(f"Vector(10.5, 20.3) = ({v3.x}, {v3.y})") + + # Methods + print("\n Methods:") + + # magnitude + v = Vector(3, 4) + mag = v.magnitude() + print_test(f"Vector(3, 4).magnitude() = {mag}") + + # normalize + v_norm = v.normalize() + print_test(f"Vector(3, 4).normalize() = ({v_norm.x:.3f}, {v_norm.y:.3f})") + + # dot product + v_a = Vector(2, 3) + v_b = Vector(4, 5) + dot = v_a.dot(v_b) + print_test(f"Vector(2, 3).dot(Vector(4, 5)) = {dot}") + + # distance_to + dist = v_a.distance_to(v_b) + print_test(f"Vector(2, 3).distance_to(Vector(4, 5)) = {dist:.3f}") + + # Operators + print("\n Operators:") + + # Addition + v_sum = v_a + v_b + print_test(f"Vector(2, 3) + Vector(4, 5) = ({v_sum.x}, {v_sum.y})") + + # Subtraction + v_diff = v_b - v_a + print_test(f"Vector(4, 5) - Vector(2, 3) = ({v_diff.x}, {v_diff.y})") + + # Multiplication (scalar) + v_mult = v_a * 2.5 + print_test(f"Vector(2, 3) * 2.5 = ({v_mult.x}, {v_mult.y})") + + # Division (scalar) + v_div = v_b / 2.0 + print_test(f"Vector(4, 5) / 2.0 = ({v_div.x}, {v_div.y})") + + # Comparison + v_eq1 = Vector(1, 2) + v_eq2 = Vector(1, 2) + v_neq = Vector(3, 4) + print_test(f"Vector(1, 2) == Vector(1, 2) = {v_eq1 == v_eq2}") + print_test(f"Vector(1, 2) != Vector(3, 4) = {v_eq1 != v_neq}") + + 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 = Frame() + print_test(f"Frame() - pos=({f1.x}, {f1.y}), size=({f1.w}, {f1.h})") + ui.append(f1) + + # Position only + f2 = 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 = 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 = Frame(300, 200, 200, 100, + fill_color=Color(100, 100, 200), + outline_color=Color(255, 255, 0), + outline=3) + print_test("Frame with all parameters") + ui.append(f4) + + # With click handler + def on_click(x, y, button): + print(f" Frame clicked at ({x}, {y}) with button {button}") + + f5 = Frame(500, 300, 100, 100, click=on_click) + print_test("Frame with click handler") + ui.append(f5) + + # Properties + print("\n Properties:") + + # Position and size + f = 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 = Color(255, 0, 0, 128) + f.outline_color = 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 = Frame(5, 5, 20, 20) + child2 = Frame(30, 5, 20, 20) + f.children.append(child1) + f.children.append(child2) + print_test(f"children.count = {len(f.children)}") + + # Clip children + f.clip_children = True + print_test(f"clip_children={f.clip_children}") + + # Methods + print("\n Methods:") + + # get_bounds + bounds = f.get_bounds() + print_test(f"get_bounds() = {bounds}") + + # move + old_pos = (f.x, f.y) + f.move(10, 15) + new_pos = (f.x, f.y) + print_test(f"move(10, 15): {old_pos} -> {new_pos}") + + # resize + old_size = (f.w, f.h) + f.resize(100, 120) + new_size = (f.w, f.h) + print_test(f"resize(100, 120): {old_size} -> {new_size}") + + # Position tuple property + f.pos = (150, 175) + print_test(f"pos property: ({f.x}, {f.y})") + + # Children collection methods + print("\n Children Collection:") + + # Clear and test + f.children.extend([Frame(0, 0, 10, 10) for _ in range(3)]) + print_test(f"extend() - count = {len(f.children)}") + + # Index access + first_child = f.children[0] + print_test(f"children[0] = Frame at ({first_child.x}, {first_child.y})") + + # Remove + f.children.remove(first_child) + print_test(f"remove() - count = {len(f.children)}") + + # Iteration + count = 0 + for child in f.children: + count += 1 + print_test(f"iteration - counted {count} 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 = Caption() + print_test(f"Caption() - text='{c1.text}', pos=({c1.x}, {c1.y})") + ui.append(c1) + + # Text only + c2 = Caption("Hello World") + print_test(f"Caption('Hello World') - pos=({c2.x}, {c2.y})") + ui.append(c2) + + # Text and position + c3 = Caption("Positioned Text", 100, 50) + print_test(f"Caption('Positioned Text', 100, 50)") + ui.append(c3) + + # With font (would need Font object) + # font = Font("assets/fonts/arial.ttf", 16) + # c4 = Caption("Custom Font", 200, 100, font) + + # Full constructor + c5 = Caption("Styled Text", 300, 150, + fill_color=Color(255, 255, 0), + outline_color=Color(255, 0, 0), + outline=2) + print_test("Caption with all style parameters") + ui.append(c5) + + # With click handler + def caption_click(x, y, button): + print(f" Caption clicked at ({x}, {y})") + + c6 = Caption("Clickable", 400, 200, click=caption_click) + print_test("Caption with click handler") + ui.append(c6) + + # Properties + print("\n Properties:") + + c = 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 = Color(0, 255, 255) + c.outline_color = 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})") + + # Common properties + c.visible = True + c.opacity = 0.8 + c.z_index = 5 + print_test(f"visible={c.visible}, opacity={c.opacity}, z_index={c.z_index}") + + # Methods + print("\n Methods:") + + # get_bounds + bounds = c.get_bounds() + print_test(f"get_bounds() = {bounds}") + + # move + c.move(25, 30) + print_test(f"move(25, 30) - new pos = ({c.x}, {c.y})") + + # Special text behaviors + print("\n Text Behaviors:") + + # Empty text + c.text = "" + print_test(f"Empty text - size = ({c.w}, {c.h})") + + # Multiline text + c.text = "Line 1\nLine 2\nLine 3" + print_test(f"Multiline text - size = ({c.w}, {c.h})") + + # Very long text + c.text = "A" * 100 + print_test(f"Long text (100 chars) - size = ({c.w}, {c.h})") + + return True + +def test_sprite_api(): + """Test all Sprite constructors and methods""" + print_section("SPRITE API TESTS") + + ui = mcrfpy.sceneUI("api_test") + + # Try to load a texture for testing + texture = None + try: + texture = Texture("assets/sprites/player.png", grid_size=(32, 32)) + print_test("Texture loaded successfully") + except: + print_test("Texture load failed - using None", False) + + # Constructor variants + print("\n Constructors:") + + # Empty constructor + s1 = Sprite() + print_test(f"Sprite() - pos=({s1.x}, {s1.y}), sprite_index={s1.sprite_index}") + ui.append(s1) + + # Position only + s2 = Sprite(100, 50) + print_test(f"Sprite(100, 50)") + ui.append(s2) + + # Position and texture + s3 = Sprite(200, 100, texture) + print_test(f"Sprite(200, 100, texture)") + ui.append(s3) + + # Full constructor + s4 = Sprite(300, 150, texture, sprite_index=5, scale=2.0) + print_test(f"Sprite with texture, index=5, scale=2.0") + ui.append(s4) + + # With click handler + def sprite_click(x, y, button): + print(f" Sprite clicked!") + + s5 = Sprite(400, 200, texture, click=sprite_click) + print_test("Sprite with click handler") + ui.append(s5) + + # Properties + print("\n Properties:") + + s = Sprite(10, 20, texture) + + # Position + s.x = 50 + s.y = 60 + print_test(f"position = ({s.x}, {s.y})") + + # Position tuple + s.pos = (75, 85) + print_test(f"pos tuple = ({s.x}, {s.y})") + + # Sprite index + s.sprite_index = 10 + print_test(f"sprite_index = {s.sprite_index}") + + # Scale + s.scale = 1.5 + print_test(f"scale = {s.scale}") + + # Size (computed from texture and scale) + print_test(f"size (computed) = ({s.w}, {s.h})") + + # Texture + s.texture = texture # Can reassign texture + print_test("Texture reassigned") + + # Common properties + s.visible = True + s.opacity = 0.9 + s.z_index = 3 + print_test(f"visible={s.visible}, opacity={s.opacity}, z_index={s.z_index}") + + # Methods + print("\n Methods:") + + # get_bounds + bounds = s.get_bounds() + print_test(f"get_bounds() = {bounds}") + + # move + old_pos = (s.x, s.y) + s.move(15, 20) + new_pos = (s.x, s.y) + print_test(f"move(15, 20): {old_pos} -> {new_pos}") + + # Sprite animation test + print("\n Sprite Animation:") + + # Test different sprite indices + for i in range(5): + s.sprite_index = i + print_test(f"Set sprite_index to {i}") + + return True + +def test_grid_api(): + """Test all Grid constructors and methods""" + print_section("GRID API TESTS") + + ui = mcrfpy.sceneUI("api_test") + + # Load texture for grid + texture = None + try: + texture = Texture("assets/sprites/tiles.png", grid_size=(16, 16)) + print_test("Tile texture loaded") + except: + print_test("Tile texture load failed", False) + + # Constructor variants + print("\n Constructors:") + + # Empty constructor + g1 = Grid() + print_test(f"Grid() - pos=({g1.x}, {g1.y}), grid_size={g1.grid_size}") + ui.append(g1) + + # Position only + g2 = Grid(100, 50) + print_test(f"Grid(100, 50)") + ui.append(g2) + + # Position and grid size + g3 = Grid(200, 100, grid_size=(30, 20)) + print_test(f"Grid with size (30, 20)") + ui.append(g3) + + # With texture + g4 = Grid(300, 150, grid_size=(25, 15), texture=texture) + print_test("Grid with texture") + ui.append(g4) + + # Full constructor + g5 = Grid(400, 200, grid_size=(20, 10), texture=texture, + tile_width=24, tile_height=24, scale=1.5) + print_test("Grid with all parameters") + ui.append(g5) + + # With click handler + def grid_click(x, y, button): + print(f" Grid clicked at ({x}, {y})") + + g6 = Grid(500, 250, click=grid_click) + print_test("Grid with click handler") + ui.append(g6) + + # Properties + print("\n Properties:") + + g = Grid(10, 20, grid_size=(40, 30)) + + # Position + g.x = 50 + g.y = 60 + print_test(f"position = ({g.x}, {g.y})") + + # Grid dimensions + print_test(f"grid_size = {g.grid_size}") + print_test(f"grid_x = {g.grid_x}, grid_y = {g.grid_y}") + + # Tile dimensions + g.tile_width = 20 + g.tile_height = 20 + print_test(f"tile size = ({g.tile_width}, {g.tile_height})") + + # Scale + g.scale = 2.0 + print_test(f"scale = {g.scale}") + + # Texture + g.texture = texture + print_test("Texture assigned") + + # Fill color + g.fill_color = Color(30, 30, 50) + print_test("Fill color set") + + # Camera properties + g.center = (20.0, 15.0) + print_test(f"center (camera) = {g.center}") + + g.zoom = 1.5 + print_test(f"zoom = {g.zoom}") + + # Common properties + g.visible = True + g.opacity = 0.95 + g.z_index = 1 + print_test(f"visible={g.visible}, opacity={g.opacity}, z_index={g.z_index}") + + # Grid point access + print("\n Grid Points:") + + # Access grid point + point = g.at(5, 5) + print_test(f"at(5, 5) returned GridPoint") + + # Modify grid point + point.tilesprite = 10 + point.tile_overlay = 2 + point.walkable = False + point.transparent = True + point.color = Color(255, 0, 0, 128) + print_test("GridPoint properties modified") + + # Check modifications + print_test(f" tilesprite = {point.tilesprite}") + print_test(f" walkable = {point.walkable}") + print_test(f" transparent = {point.transparent}") + + # Entity collection + print("\n Entity Collection:") + + # Create entities + if texture: + e1 = Entity(10.5, 10.5, texture, sprite_index=5) + e2 = Entity(15.0, 12.0, texture, sprite_index=8) + + g.entities.append(e1) + g.entities.append(e2) + print_test(f"Added 2 entities, count = {len(g.entities)}") + + # Access entities + first = g.entities[0] + print_test(f"entities[0] at ({first.x}, {first.y})") + + # Iterate entities + count = 0 + for entity in g.entities: + count += 1 + print_test(f"Iterated {count} entities") + + # Methods + print("\n Methods:") + + # get_bounds + bounds = g.get_bounds() + print_test(f"get_bounds() = {bounds}") + + # move + g.move(20, 25) + print_test(f"move(20, 25) - new pos = ({g.x}, {g.y})") + + # Points array access + print("\n Points Array:") + + # The points property is a 2D array + all_points = g.points + print_test(f"points array dimensions: {len(all_points)}x{len(all_points[0]) if all_points else 0}") + + # Modify multiple points + for y in range(5): + for x in range(5): + pt = g.at(x, y) + pt.tilesprite = x + y * 5 + pt.color = Color(x * 50, y * 50, 100) + print_test("Modified 5x5 area of grid") + + return True + +def test_entity_api(): + """Test all Entity constructors and methods""" + print_section("ENTITY API TESTS") + + # Entities need to be in a grid + ui = mcrfpy.sceneUI("api_test") + + # Create grid and texture + texture = None + try: + texture = Texture("assets/sprites/entities.png", grid_size=(32, 32)) + print_test("Entity texture loaded") + except: + print_test("Entity texture load failed", False) + + grid = Grid(50, 50, grid_size=(30, 30), texture=texture) + ui.append(grid) + + # Constructor variants + print("\n Constructors:") + + # Empty constructor + e1 = Entity() + print_test(f"Entity() - pos=({e1.x}, {e1.y}), sprite_index={e1.sprite_index}") + grid.entities.append(e1) + + # Position only + e2 = Entity(5.5, 3.5) + print_test(f"Entity(5.5, 3.5)") + grid.entities.append(e2) + + # Position and texture + e3 = Entity(10.0, 8.0, texture) + print_test("Entity with texture") + grid.entities.append(e3) + + # Full constructor + e4 = Entity(15.5, 12.5, texture, sprite_index=7, scale=1.5) + print_test("Entity with all parameters") + grid.entities.append(e4) + + # Properties + print("\n Properties:") + + e = Entity(20.0, 15.0, texture, sprite_index=3) + grid.entities.append(e) + + # Position (float coordinates in grid space) + e.x = 22.5 + e.y = 16.5 + print_test(f"position = ({e.x}, {e.y})") + + # Position tuple + e.position = (24.0, 18.0) + print_test(f"position tuple = {e.position}") + + # Sprite index + e.sprite_index = 12 + print_test(f"sprite_index = {e.sprite_index}") + + # Scale + e.scale = 2.0 + print_test(f"scale = {e.scale}") + + # Methods + print("\n Methods:") + + # index() - get position in entity collection + idx = e.index() + print_test(f"index() in collection = {idx}") + + # Gridstate (visibility per grid cell) + print("\n Grid State:") + + # Access gridstate + if len(e.gridstate) > 0: + state = e.gridstate[0] + print_test(f"gridstate[0] - visible={state.visible}, discovered={state.discovered}") + + # Modify visibility + state.visible = True + state.discovered = True + print_test("Modified gridstate visibility") + + # at() method - check if entity occupies a grid point + # This would need a GridPointState object + # occupied = e.at(some_gridpoint_state) + + # die() method - remove from grid + print("\n Entity Lifecycle:") + + # Create temporary entity + temp_entity = Entity(25.0, 25.0, texture) + grid.entities.append(temp_entity) + count_before = len(grid.entities) + + # Remove it + temp_entity.die() + count_after = len(grid.entities) + print_test(f"die() - entity count: {count_before} -> {count_after}") + + # Entity movement + print("\n Entity Movement:") + + # Test fractional positions (entities can be between grid cells) + e.position = (10.0, 10.0) + print_test(f"Integer position: {e.position}") + + e.position = (10.5, 10.5) + print_test(f"Center of cell: {e.position}") + + e.position = (10.25, 10.75) + print_test(f"Fractional position: {e.position}") + + return True + +def test_collections(): + """Test UICollection and EntityCollection behaviors""" + print_section("COLLECTION API TESTS") + + ui = mcrfpy.sceneUI("api_test") + + # Test UICollection (scene UI and frame children) + print("\n UICollection (Scene UI):") + + # Clear scene + while len(ui) > 0: + ui.remove(ui[0]) + print_test(f"Cleared - length = {len(ui)}") + + # append + f1 = Frame(10, 10, 50, 50) + ui.append(f1) + print_test(f"append() - length = {len(ui)}") + + # extend + frames = [Frame(x * 60, 10, 50, 50) for x in range(1, 4)] + ui.extend(frames) + print_test(f"extend() with 3 items - length = {len(ui)}") + + # index access + item = ui[0] + print_test(f"ui[0] = Frame at ({item.x}, {item.y})") + + # slice access + slice_items = ui[1:3] + print_test(f"ui[1:3] returned {len(slice_items)} items") + + # index() method + idx = ui.index(f1) + print_test(f"index(frame) = {idx}") + + # count() method + cnt = ui.count(f1) + print_test(f"count(frame) = {cnt}") + + # in operator + contains = f1 in ui + print_test(f"frame in ui = {contains}") + + # iteration + count = 0 + for item in ui: + count += 1 + print_test(f"Iteration counted {count} items") + + # remove + ui.remove(f1) + print_test(f"remove() - length = {len(ui)}") + + # Test Frame.children collection + print("\n UICollection (Frame Children):") + + parent = Frame(100, 100, 300, 200) + ui.append(parent) + + # Add children + child1 = Caption("Child 1", 10, 10) + child2 = Caption("Child 2", 10, 30) + child3 = Frame(10, 50, 50, 50) + + parent.children.append(child1) + parent.children.append(child2) + parent.children.append(child3) + print_test(f"Added 3 children - count = {len(parent.children)}") + + # Mixed types in collection + has_caption = any(isinstance(child, Caption) for child in parent.children) + has_frame = any(isinstance(child, Frame) for child in parent.children) + print_test(f"Mixed types: has Caption = {has_caption}, has Frame = {has_frame}") + + # Test EntityCollection + print("\n EntityCollection (Grid Entities):") + + texture = None + try: + texture = Texture("assets/sprites/entities.png", grid_size=(32, 32)) + except: + pass + + grid = Grid(400, 100, grid_size=(20, 20), texture=texture) + ui.append(grid) + + # Add entities + entities = [] + for i in range(5): + e = Entity(float(i * 2), float(i * 2), texture, sprite_index=i) + grid.entities.append(e) + entities.append(e) + + print_test(f"Added 5 entities - count = {len(grid.entities)}") + + # Access and iteration + first_entity = grid.entities[0] + print_test(f"entities[0] at ({first_entity.x}, {first_entity.y})") + + # Remove entity + grid.entities.remove(first_entity) + print_test(f"Removed entity - count = {len(grid.entities)}") + + return True + +def test_animation_api(): + """Test Animation class API""" + print_section("ANIMATION API TESTS") + + ui = mcrfpy.sceneUI("api_test") + + # Import Animation + from mcrfpy import Animation + + print("\n Animation Constructors:") + + # Basic animation + anim1 = Animation("x", 100.0, 2.0) + print_test("Animation('x', 100.0, 2.0)") + + # With easing + anim2 = Animation("y", 200.0, 3.0, "easeInOut") + print_test("Animation with easing='easeInOut'") + + # Delta mode + anim3 = Animation("w", 50.0, 1.5, "linear", delta=True) + print_test("Animation with delta=True") + + # Color animation + anim4 = Animation("fill_color", Color(255, 0, 0), 2.0) + print_test("Animation with Color target") + + # Vector animation + anim5 = Animation("position", (10.0, 20.0), 2.5, "easeOutBounce") + print_test("Animation with position tuple") + + # Sprite sequence + anim6 = 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 = Frame(50, 50, 100, 100) + frame.fill_color = Color(100, 100, 100) + ui.append(frame) + + # Start animation + anim1.start(frame) + print_test("start() called on frame") + + # Get current value (before update) + current = anim1.get_current_value() + print_test(f"get_current_value() = {current}") + + # Manual update (usually automatic) + anim1.update(0.5) # 0.5 seconds + print_test("update(0.5) called") + + # Check elapsed time + print_test(f"elapsed after update = {anim1.elapsed}") + + # All easing functions + print("\n Available Easing Functions:") + easings = [ + "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" + ] + + # Test creating animation with each easing + for easing in easings[:10]: # Test first 10 + try: + test_anim = Animation("x", 100.0, 1.0, easing) + print_test(f"Easing '{easing}' ✓") + except: + print_test(f"Easing '{easing}' failed", False) + + return True + +def test_scene_api(): + """Test scene-related API functions""" + print_section("SCENE API TESTS") + + print("\n Scene Management:") + + # Create scene + mcrfpy.createScene("test_scene_1") + print_test("createScene('test_scene_1')") + + mcrfpy.createScene("test_scene_2") + print_test("createScene('test_scene_2')") + + # Set active scene + mcrfpy.setScene("test_scene_1") + print_test("setScene('test_scene_1')") + + # Get scene UI + ui1 = mcrfpy.sceneUI("test_scene_1") + print_test(f"sceneUI('test_scene_1') - collection size = {len(ui1)}") + + ui2 = mcrfpy.sceneUI("test_scene_2") + print_test(f"sceneUI('test_scene_2') - collection size = {len(ui2)}") + + # Add content to scenes + ui1.append(Frame(10, 10, 100, 100)) + ui1.append(Caption("Scene 1", 10, 120)) + print_test(f"Added content to scene 1 - size = {len(ui1)}") + + ui2.append(Frame(20, 20, 150, 150)) + ui2.append(Caption("Scene 2", 20, 180)) + print_test(f"Added content to scene 2 - size = {len(ui2)}") + + # Scene transitions + print("\n Scene Transitions:") + + # Note: Actual transition types would need to be tested visually + # TransitionType enum: None, Fade, SlideLeft, SlideRight, SlideUp, SlideDown + + # Keypress handling + print("\n Input Handling:") + + def test_keypress(scene_name, keycode): + print(f" Key pressed in {scene_name}: {keycode}") + + mcrfpy.keypressScene("test_scene_1", test_keypress) + print_test("keypressScene() handler registered") + + return True + +def test_audio_api(): + """Test audio-related API functions""" + print_section("AUDIO API TESTS") + + print("\n Sound Functions:") + + # Create sound buffer + try: + mcrfpy.createSoundBuffer("test_sound", "assets/audio/click.wav") + print_test("createSoundBuffer('test_sound', 'click.wav')") + + # Play sound + mcrfpy.playSound("test_sound") + print_test("playSound('test_sound')") + + # Set volume + mcrfpy.setVolume("test_sound", 0.5) + print_test("setVolume('test_sound', 0.5)") + + except Exception as e: + print_test(f"Audio functions failed: {e}", False) + + return True + +def test_timer_api(): + """Test timer API functions""" + print_section("TIMER API TESTS") + + print("\n Timer Functions:") + + # Timer callback + def timer_callback(runtime): + print(f" Timer fired at runtime: {runtime}") + + # Set timer + mcrfpy.setTimer("test_timer", timer_callback, 1000) # 1 second + print_test("setTimer('test_timer', callback, 1000)") + + # Delete timer + mcrfpy.delTimer("test_timer") + print_test("delTimer('test_timer')") + + # Multiple timers + mcrfpy.setTimer("timer1", lambda r: print(f" Timer 1: {r}"), 500) + mcrfpy.setTimer("timer2", lambda r: print(f" Timer 2: {r}"), 750) + mcrfpy.setTimer("timer3", lambda r: print(f" Timer 3: {r}"), 1000) + print_test("Set 3 timers with different intervals") + + # Clean up + mcrfpy.delTimer("timer1") + mcrfpy.delTimer("timer2") + mcrfpy.delTimer("timer3") + print_test("Cleaned up all timers") + + return True + +def test_edge_cases(): + """Test edge cases and error conditions""" + print_section("EDGE CASES AND ERROR HANDLING") + + ui = mcrfpy.sceneUI("api_test") + + print("\n Boundary Values:") + + # Negative positions + f = Frame(-100, -50, 50, 50) + print_test(f"Negative position: ({f.x}, {f.y})") + + # Zero size + f2 = Frame(0, 0, 0, 0) + print_test(f"Zero size: ({f2.w}, {f2.h})") + + # Very large values + f3 = Frame(10000, 10000, 5000, 5000) + print_test(f"Large values: pos=({f3.x}, {f3.y}), size=({f3.w}, {f3.h})") + + # Opacity bounds + f.opacity = -0.5 + print_test(f"Opacity below 0: {f.opacity}") + + f.opacity = 2.0 + print_test(f"Opacity above 1: {f.opacity}") + + # Color component bounds + c = Color(300, -50, 1000, 128) + print_test(f"Color out of bounds: ({c.r}, {c.g}, {c.b}, {c.a})") + + print("\n Empty Collections:") + + # Empty children + frame = Frame(0, 0, 100, 100) + print_test(f"Empty children collection: {len(frame.children)}") + + # Access empty collection + try: + item = frame.children[0] + print_test("Accessing empty collection[0]", False) + except IndexError: + print_test("Accessing empty collection[0] raises IndexError") + + print("\n Invalid Operations:") + + # Grid without texture + g = Grid(0, 0, grid_size=(10, 10)) + point = g.at(5, 5) + point.tilesprite = 10 # No texture to reference + print_test("Set tilesprite without texture") + + # Entity without grid + e = Entity(5.0, 5.0) + # e.die() would fail if not in a grid + print_test("Created entity without grid") + + return True + +def run_all_tests(): + """Run all API tests""" + print("\n" + "="*60) + print(" McRogueFace Exhaustive API Test Suite") + print(" Testing every constructor and method...") + print("="*60) + + # Run each test category + test_functions = [ + test_color_api, + test_vector_api, + test_frame_api, + test_caption_api, + test_sprite_api, + test_grid_api, + test_entity_api, + test_collections, + test_animation_api, + test_scene_api, + test_audio_api, + test_timer_api, + test_edge_cases + ] + + 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) + + # Visual test scene + print("\n Visual elements are displayed in the 'api_test' scene.") + print(" The test is complete. Press ESC to exit.") + +def handle_exit(scene_name, keycode): + """Handle ESC key to exit""" + if keycode == 256: # ESC + print("\nExiting API test suite...") + sys.exit(0) + +# Set up exit handler +mcrfpy.keypressScene("api_test", handle_exit) + +# Run after short delay to ensure scene is ready +def start_tests(runtime): + run_all_tests() + +mcrfpy.setTimer("start_tests", start_tests, 100) + +print("Starting McRogueFace Exhaustive API Demo...") +print("This will test EVERY constructor and method.") +print("Press ESC to exit at any time.") \ 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..d4e082f --- /dev/null +++ b/tests/demos/pathfinding_showcase.py @@ -0,0 +1,373 @@ +#!/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 + +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) + player.sprite_index = 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) + enemy.sprite_index = 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) + treasure.sprite_index = TREASURE + grid.entities.append(treasure) + treasures.append(treasure) + + # Spawn patrol entities + patrol = mcrfpy.Entity(10, 10) + patrol.sprite_index = PATROL + patrol.waypoints = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol + patrol.waypoint_index = 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 not hasattr(patrol, 'waypoints'): + continue + + # Get current waypoint + target_x, target_y = patrol.waypoints[patrol.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 + patrol.waypoint_index = (patrol.waypoint_index + 1) % len(patrol.waypoints) + target_x, target_y = patrol.waypoints[patrol.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.") \ No newline at end of file diff --git a/tests/demos/simple_text_input.py b/tests/demos/simple_text_input.py new file mode 100644 index 0000000..e775670 --- /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.color = (255, 255, 255, 255) + + # Text display + self.text_caption = mcrfpy.Caption("", self.x + 4, self.y + 4) + self.text_caption.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.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.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..8251498 --- /dev/null +++ b/tests/demos/sizzle_reel_final.py @@ -0,0 +1,177 @@ +#!/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. +""" + +import mcrfpy + +# Configuration +DEMO_DURATION = 4.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 + 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) + 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 + 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) + 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(ui[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)) + else: + subtitle.text = "Demo Complete!" + +# Initialize +print("Starting Animation Sizzle Reel...") +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..5e5de6a --- /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", font_size=24) + title.color = (255, 255, 255, 255) + scene.append(title) + + # Instructions + instructions = mcrfpy.Caption(10, 50, "This will automatically test the text input system", font_size=14) + 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...", font_size=14) + 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..fa6fe81 --- /dev/null +++ b/tests/demos/text_input_standalone.py @@ -0,0 +1,322 @@ +#!/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, + font_size=self.font_size + ) + self.label_text.color = (255, 255, 255, 255) + + # Text display + self.text_display = mcrfpy.Caption( + self.x + 4, + self.y + 4, + "", + font_size=self.font_size + ) + 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", font_size=24) + 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", font_size=14) + 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...", font_size=14) + 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..0986a7c --- /dev/null +++ b/tests/demos/text_input_widget.py @@ -0,0 +1,322 @@ +#!/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, + font_size=self.font_size + ) + self.label_text.color = (255, 255, 255, 255) + + # Text display + self.text_display = mcrfpy.Caption( + self.x + 4, + self.y + 4, + "", + font_size=self.font_size + ) + 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", font_size=24) + 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", font_size=14) + 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...", font_size=14) + 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/run_all_tests.sh b/tests/run_all_tests.sh new file mode 100755 index 0000000..85e7c7f --- /dev/null +++ b/tests/run_all_tests.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Run all tests and check for failures + +TESTS=( + "test_click_init.py" + "test_drawable_base.py" + "test_frame_children.py" + "test_sprite_texture_swap.py" + "test_timer_object.py" + "test_timer_object_fixed.py" +) + +echo "Running all tests..." +echo "====================" + +failed=0 +passed=0 + +for test in "${TESTS[@]}"; do + echo -n "Running $test... " + if timeout 5 ./mcrogueface --headless --exec ../tests/$test > /tmp/test_output.txt 2>&1; then + if grep -q "FAIL\|✗" /tmp/test_output.txt; then + echo "FAILED" + echo "Output:" + cat /tmp/test_output.txt | grep -E "✗|FAIL|Error|error" | head -10 + ((failed++)) + else + echo "PASSED" + ((passed++)) + fi + else + echo "TIMEOUT/CRASH" + ((failed++)) + fi +done + +echo "====================" +echo "Total: $((passed + failed)) tests" +echo "Passed: $passed" +echo "Failed: $failed" + +exit $failed \ No newline at end of file diff --git a/tests/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/gitea_issues.py b/tools/gitea_issues.py new file mode 100644 index 0000000..9ba8bd9 --- /dev/null +++ b/tools/gitea_issues.py @@ -0,0 +1,102 @@ +import json +from time import time +#with open("/home/john/issues.json", "r") as f: +# data = json.loads(f.read()) +#with open("/home/john/issues2.json", "r") as f: +# data.extend(json.loads(f.read())) + +print("Fetching issues...", end='') +start = time() +from gitea import Gitea, Repository, Issue +g = Gitea("https://gamedev.ffwf.net/gitea", token_text="3b450f66e21d62c22bb9fa1c8b975049a5d0c38d") +repo = Repository.request(g, "john", "McRogueFace") +issues = repo.get_issues() +dur = time() - start +print(f"({dur:.1f}s)") +print("Gitea Version: " + g.get_version()) +print("API-Token belongs to user: " + g.get_user().username) + +data = [ + { + "labels": i.labels, + "body": i.body, + "number": i.number, + } + for i in issues + ] + +input() + +def front_number(txt): + if not txt[0].isdigit(): return None + number = "" + for c in txt: + if not c.isdigit(): + break + number += c + return int(number) + +def split_any(txt, splitters): + tokens = [] + txt = [txt] + for s in splitters: + for t in txt: + tokens.extend(t.split(s)) + txt = tokens + tokens = [] + return txt + +def find_refs(txt): + tokens = [tok for tok in split_any(txt, ' ,;\t\r\n') if tok.startswith('#')] + return [front_number(tok[1:]) for tok in tokens] + +from collections import defaultdict +issue_relations = defaultdict(list) + +nodes = set() + +for issue in data: + #refs = issue['body'].split('#')[1::2] + + #refs = [front_number(r) for r in refs if front_number(r) is not None] + refs = find_refs(issue['body']) + print(issue['number'], ':', refs) + issue_relations[issue['number']].extend(refs) + nodes.add(issue['number']) + for r in refs: + nodes.add(r) + issue_relations[r].append(issue['number']) + + +# Find issue labels +issue_labels = {} +for d in data: + labels = [l['name'] for l in d['labels']] + #print(d['number'], labels) + issue_labels[d['number']] = labels + +import networkx as nx +import matplotlib.pyplot as plt + +relations = nx.Graph() + +for k in issue_relations: + relations.add_node(k) + for r in issue_relations[k]: + relations.add_edge(k, r) + relations.add_edge(r, k) + +#nx.draw_networkx(relations) + +pos = nx.spring_layout(relations) +nx.draw_networkx_nodes(relations, pos, + nodelist = [n for n in issue_labels if 'Alpha Release Requirement' in issue_labels[n]], + node_color="tab:red") +nx.draw_networkx_nodes(relations, pos, + nodelist = [n for n in issue_labels if 'Alpha Release Requirement' not in issue_labels[n]], + node_color="tab:blue") +nx.draw_networkx_edges(relations, pos, + edgelist = relations.edges() + ) +nx.draw_networkx_labels(relations, pos, {i: str(i) for i in relations.nodes()}) +plt.show() \ No newline at end of file diff --git a/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