Compare commits

..

5 Commits

Author SHA1 Message Date
John McCardle 4144cdf067 draft lessons 2025-07-10 00:14:56 -04:00
John McCardle 665689c550 hotfix: Windows build attempt 2025-07-09 23:33:09 -04:00
John McCardle d11f76ac43 Squashed commit of 53 Commits: [alpha_streamline_2]
* Field of View, Pathing courtesy of libtcod
* python-tcod emulation at `mcrfpy.libtcod` - partial implementation
* documentation, tutorial drafts: in middling to good shape

┌────────────┬────────────────────┬───────────┬────────────┬───────────────┬────────────────┬────────────────┬─────────────┐
│ Date       │ Models             │     Input │     Output │  Cache Create │     Cache Read │   Total Tokens │  Cost (USD) │
├────────────┼────────────────────┼───────────┼────────────┼───────────────┼────────────────┼────────────────┼─────────────┤
│ 2025-07-05 │ - opus-4           │    13,630 │    159,500 │     3,854,900 │     84,185,034 │     88,213,064 │     $210.72 │
├────────────┼────────────────────┼───────────┼────────────┼───────────────┼────────────────┼────────────────┼─────────────┤
│ 2025-07-06 │ - opus-4           │     5,814 │    113,190 │     4,242,407 │    150,191,183 │    154,552,594 │     $313.41 │
├────────────┼────────────────────┼───────────┼────────────┼───────────────┼────────────────┼────────────────┼─────────────┤
│ 2025-07-07 │ - opus-4           │     7,244 │    104,599 │     3,894,453 │     81,781,179 │     85,787,475 │     $184.46 │
│            │ - sonnet-4         │           │            │               │                │                │             │
├────────────┼────────────────────┼───────────┼────────────┼───────────────┼────────────────┼────────────────┼─────────────┤
│ 2025-07-08 │ - opus-4           │    50,312 │    158,599 │     5,021,189 │     60,028,561 │     65,258,661 │     $167.05 │
│            │ - sonnet-4         │           │            │               │                │                │             │
├────────────┼────────────────────┼───────────┼────────────┼───────────────┼────────────────┼────────────────┼─────────────┤
│ 2025-07-09 │ - opus-4           │     6,311 │    109,653 │     4,171,140 │     80,092,875 │     84,379,979 │     $193.09 │
│            │ - sonnet-4         │           │            │               │                │                │             │
└────────────┴────────────────────┴───────────┴────────────┴───────────────┴────────────────┴────────────────┴─────────────┘

    🤖 Generated with [Claude Code](https://claude.ai/code)
    Co-Authored-By: Claude <noreply@anthropic.com>

Author: John McCardle <mccardle.john@gmail.com>

    Draft tutorials

Author: John McCardle <mccardle.john@gmail.com>

    docs: update ROADMAP with FOV, A* pathfinding, and GUI text widget completions

    - Mark TCOD Integration Sprint as complete
    - Document FOV system with perspective rendering implementation
    - Update UIEntity pathfinding status to complete with A* and caching
    - Add comprehensive achievement entry for July 10 work
    - Reflect current engine capabilities accurately

    The engine now has all core roguelike mechanics:
    - Field of View with per-entity visibility
    - Pathfinding (both Dijkstra and A*)
    - Text input for in-game consoles
    - Performance optimizations throughout

Author: John McCardle <mccardle.john@gmail.com>

    feat(engine): implement perspective FOV, pathfinding, and GUI text widgets

    Major Engine Enhancements:
    - Complete FOV (Field of View) system with perspective rendering
      - UIGrid.perspective property for entity-based visibility
      - Three-layer overlay colors (unexplored, explored, visible)
      - Per-entity visibility state tracking
      - Perfect knowledge updates only for explored areas

    - Advanced Pathfinding Integration
      - A* pathfinding implementation in UIGrid
      - Entity.path_to() method for direct pathfinding
      - Dijkstra maps for multi-target pathfinding
      - Path caching for performance optimization

    - GUI Text Input Widgets
      - TextInputWidget class with cursor, selection, scrolling
      - Improved widget with proper text rendering and input handling
      - Example showcase of multiple text input fields
      - Foundation for in-game console and chat systems

    - Performance & Architecture Improvements
      - PyTexture copy operations optimized
      - GameEngine update cycle refined
      - UIEntity property handling enhanced
      - UITestScene modernized

    Test Suite:
    - Interactive visibility demos showing FOV in action
    - Pathfinding comparison (A* vs Dijkstra)
    - Debug utilities for visibility and empty path handling
    - Sizzle reel demo combining pathfinding and vision
    - Multiple text input test scenarios

    This commit brings McRogueFace closer to a complete roguelike engine
    with essential features like line-of-sight, intelligent pathfinding,
    and interactive text input capabilities.

Author: John McCardle <mccardle.john@gmail.com>

    feat(demos): enhance interactive pathfinding demos with entity.path_to()

    - dijkstra_interactive_enhanced.py: Animation along paths with smooth movement
      - M key to start movement animation
      - P to pause/resume
      - R to reset positions
      - Visual path gradient for better clarity

    - pathfinding_showcase.py: Advanced multi-entity behaviors
      - Chase mode: enemies pursue player
      - Flee mode: enemies avoid player
      - Patrol mode: entities follow waypoints
      - WASD player movement
      - Dijkstra distance field visualization (D key)
      - Larger dungeon map with multiple rooms

    - Both demos use new entity.path_to() method
    - Smooth interpolated movement animations
    - Real-time pathfinding recalculation
    - Comprehensive test coverage

    These demos showcase the power of integrated pathfinding for game AI.

Author: John McCardle <mccardle.john@gmail.com>

    feat(entity): implement path_to() method for entity pathfinding

    - Add path_to(target_x, target_y) method to UIEntity class
    - Uses existing Dijkstra pathfinding implementation from UIGrid
    - Returns list of (x, y) coordinate tuples for complete path
    - Supports both positional and keyword argument formats
    - Proper error handling for out-of-bounds and no-grid scenarios
    - Comprehensive test suite covering normal and edge cases

    Part of TCOD integration sprint - gives entities immediate pathfinding capabilities.

Author: John McCardle <mccardle.john@gmail.com>

    docs: update roadmap with Dijkstra pathfinding progress

    - Mark UIGrid TCOD Integration as completed
    - Document critical PyArg bug fix achievement
    - Update UIEntity Pathfinding to 50% complete
    - Add detailed progress notes for July 9 sprint work

Author: John McCardle <mccardle.john@gmail.com>

    feat(tcod): complete Dijkstra pathfinding implementation with critical PyArg fix

    - Add complete Dijkstra pathfinding to UIGrid class
      - compute_dijkstra(), get_dijkstra_distance(), get_dijkstra_path()
      - Full TCODMap and TCODDijkstra integration
      - Proper memory management in constructors/destructors

    - Create mcrfpy.libtcod submodule with Python bindings
      - dijkstra_compute(), dijkstra_get_distance(), dijkstra_get_path()
      - line() function for drawing corridors
      - Foundation for future FOV and pathfinding algorithms

    - Fix critical PyArg bug in UIGridPoint color setter
      - PyObject_to_sfColor() now handles both Color objects and tuples
      - Prevents "SystemError: new style getargs format but argument is not a tuple"
      - Proper error handling and exception propagation

    - Add comprehensive test suite
      - test_dijkstra_simple.py validates all pathfinding operations
      - dijkstra_test.py for headless testing with screenshots
      - dijkstra_interactive.py for full user interaction demos

    - Consolidate and clean up test files
      - Removed 6 duplicate/broken demo attempts
      - Two clean versions: headless test + interactive demo

    Part of TCOD integration sprint for RoguelikeDev Tutorial Event.

Author: John McCardle <mccardle.john@gmail.com>

    Roguelike Tutorial Planning + Prep

Author: John McCardle <mccardle.john@gmail.com>

    feat(docs): complete markdown API documentation export

    - Created comprehensive markdown documentation matching HTML completeness
    - Documented all 75 functions, 20 classes, 56 methods, and 20 automation methods
    - Zero ellipsis instances - complete coverage with no missing documentation
    - Added proper markdown formatting with code blocks and navigation
    - Included full parameter documentation, return values, and examples

    Key features:
    - 23KB GitHub-compatible markdown documentation
    - 47 argument sections with detailed parameters
    - 35 return value specifications
    - 23 code examples with syntax highlighting
    - 38 explanatory notes and 10 exception specifications
    - Full table of contents with anchor links
    - Professional markdown formatting

    Both export formats now available:
    - HTML: docs/api_reference_complete.html (54KB, rich styling)
    - Markdown: docs/API_REFERENCE_COMPLETE.md (23KB, GitHub-compatible)

Author: John McCardle <mccardle.john@gmail.com>

    feat(docs): complete API documentation with zero missing methods

    - Eliminated ALL ellipsis instances (0 remaining)
    - Documented 40 functions with complete signatures and examples
    - Documented 21 classes with full method and property documentation
    - Added 56 method descriptions with detailed parameters and return values
    - Included 15 complete property specifications
    - Added 24 code examples and 38 explanatory notes
    - Comprehensive coverage of all collection methods, system classes, and functions

    Key highlights:
    - EntityCollection/UICollection: Complete method docs (append, remove, extend, count, index)
    - Animation: Full property and method documentation with examples
    - Color: All manipulation methods (from_hex, to_hex, lerp) with examples
    - Vector: Complete mathematical operations (magnitude, normalize, dot, distance_to, angle, copy)
    - Scene: All management methods including register_keyboard
    - Timer: Complete control methods (pause, resume, cancel, restart)
    - Window: All management methods (get, center, screenshot)
    - System functions: Complete audio, scene, UI, and system function documentation

Author: John McCardle <mccardle.john@gmail.com>

    feat(docs): create professional HTML API documentation

    - Fixed all formatting issues from original HTML output
    - Added comprehensive constructor documentation for all classes
    - Enhanced visual design with modern styling and typography
    - Fixed literal newline display and markdown link conversion
    - Added proper semantic HTML structure and navigation
    - Includes detailed documentation for Entity, collections, and system types

Author: John McCardle <mccardle.john@gmail.com>

    feat: complete API reference generator and finish Phase 7 documentation

    Implemented comprehensive API documentation generator that:
    - Introspects live mcrfpy module for accurate documentation
    - Generates organized Markdown reference (docs/API_REFERENCE.md)
    - Categorizes classes and functions by type
    - Includes full automation module documentation
    - Provides summary statistics

    Results:
    - 20 classes documented
    - 19 module functions documented
    - 20 automation methods documented
    - 100% coverage of public API
    - Clean, readable Markdown output

    Phase 7 Summary:
    - Completed 4/5 tasks (1 cancelled as architecturally inappropriate)
    - All documentation tasks successful
    - Type stubs, docstrings, and API reference all complete

Author: John McCardle <mccardle.john@gmail.com>

    docs: cancel PyPI wheel task and add future vision for Python extension architecture

    Task #70 Analysis:
    - Discovered fundamental incompatibility with PyPI distribution
    - McRogueFace embeds CPython rather than being loaded by it
    - Traditional wheels expect to extend existing Python interpreter
    - Current architecture is application-with-embedded-Python

    Decisions:
    - Cancelled PyPI wheel preparation as out of scope for Alpha
    - Cleaned up attempted packaging files (pyproject.toml, setup.py, etc.)
    - Identified better distribution methods (installers, package managers)

    Added Future Vision:
    - Comprehensive plan for pure Python extension architecture
    - Would allow true "pip install mcrogueface" experience
    - Requires major refactoring to invert control flow
    - Python would drive main loop with C++ performance extensions
    - Unscheduled but documented as long-term possibility

    This clarifies the architectural boundaries and sets realistic
    expectations for distribution methods while preserving the vision
    of what McRogueFace could become with significant rework.

Author: John McCardle <mccardle.john@gmail.com>

    feat: generate comprehensive .pyi type stubs for IDE support (#108)

    Created complete type stub files for the mcrfpy module to enable:
    - Full IntelliSense/autocomplete in IDEs
    - Static type checking with mypy/pyright
    - Better documentation tooltips
    - Parameter hints and return types

    Implementation details:
    - Manually crafted stubs for accuracy (15KB, 533 lines)
    - Complete coverage: 19 classes, 112 functions/methods
    - Proper type annotations using typing module
    - @overload decorators for multiple signatures
    - Type aliases for common patterns (UIElement union)
    - Preserved all docstrings for IDE help
    - Automation module fully typed
    - PEP 561 compliant with py.typed marker

    Testing:
    - Validated Python syntax with ast.parse()
    - Verified all expected classes and functions
    - Confirmed type annotations are well-formed
    - Checked docstring preservation (80 docstrings)

    Usage:
    - VS Code: Add stubs/ to python.analysis.extraPaths
    - PyCharm: Mark stubs/ directory as Sources Root
    - Other IDEs will auto-detect .pyi files

    This significantly improves the developer experience when using
    McRogueFace as a Python game engine.

Author: John McCardle <mccardle.john@gmail.com>

    docs: add comprehensive parameter documentation to all API methods (#86)

    Enhanced documentation for the mcrfpy module with:
    - Detailed docstrings for all API methods
    - Type hints in documentation (name: type format)
    - Return type specifications
    - Exception documentation where applicable
    - Usage examples for complex methods
    - Module-level documentation with overview and example code

    Specific improvements:
    - Audio API: Added parameter types and return values
    - Scene API: Documented transition types and error conditions
    - Timer API: Clarified handler signature and runtime parameter
    - UI Search: Added wildcard pattern examples for findAll()
    - Metrics API: Documented all dictionary keys returned

    Also fixed method signatures:
    - Changed METH_VARARGS to METH_NOARGS for parameterless methods
    - Ensures proper Python calling conventions

    Test coverage included - all documentation is accessible via Python's
    __doc__ attributes and shows correctly formatted information.

Author: John McCardle <mccardle.john@gmail.com>

    docs: mark issue #85 as completed in Phase 7

Author: John McCardle <mccardle.john@gmail.com>

    docs: replace all 'docstring' placeholders with comprehensive documentation (#85)

    Added proper Python docstrings for all UI component classes:

    UIFrame:
    - Container element that can hold child drawables
    - Documents position, size, colors, outline, and clip_children
    - Includes constructor signature with all parameters

    UICaption:
    - Text display element with font and styling
    - Documents text content, position, font, colors, outline
    - Notes that w/h are computed from text content

    UISprite:
    - Texture/sprite display element
    - Documents position, texture, sprite_index, scale
    - Notes that w/h are computed from texture and scale

    UIGrid:
    - Tile-based grid for game worlds
    - Documents grid dimensions, tile size, texture atlas
    - Includes entities collection and background_color

    All docstrings follow consistent format:
    - Constructor signature with defaults
    - Brief description
    - Args section with types and defaults
    - Attributes section with all properties

    This completes Phase 7 task #85 for documentation improvements.

Author: John McCardle <mccardle.john@gmail.com>

    docs: update ROADMAP with PyArgHelpers infrastructure completion

Author: John McCardle <mccardle.john@gmail.com>

    refactor: implement PyArgHelpers for standardized Python argument parsing

    This major refactoring standardizes how position, size, and other arguments
    are parsed across all UI components. PyArgHelpers provides consistent handling
    for various argument patterns:

    - Position as (x, y) tuple or separate x, y args
    - Size as (w, h) tuple or separate width, height args
    - Grid position and size with proper validation
    - Color parsing with PyColorObject support

    Changes across UI components:
    - UICaption: Migrated to PyArgHelpers, improved resize() for future multiline support
    - UIFrame: Uses standardized position parsing
    - UISprite: Consistent position handling
    - UIGrid: Grid-specific position/size helpers
    - UIEntity: Unified argument parsing

    Also includes:
    - Improved error messages for type mismatches (int or float accepted)
    - Reduced code duplication across constructors
    - Better handling of keyword/positional argument conflicts
    - Maintains backward compatibility with existing API

    This addresses the inconsistent argument handling patterns discovered during
    the inheritance hierarchy work and prepares for Phase 7 documentation.

Author: John McCardle <mccardle.john@gmail.com>

    feat(Python): establish proper inheritance hierarchy for UI types

    All UIDrawable-derived Python types now properly inherit from the Drawable
    base class in Python, matching the C++ inheritance structure.

    Changes:
    - Add Py_TPFLAGS_BASETYPE to PyDrawableType to allow inheritance
    - Set tp_base = &mcrfpydef::PyDrawableType for all UI types
    - Add PyDrawable.h include to UI type headers
    - Rename _Drawable to Drawable and update error message

    This enables proper Python inheritance: Frame, Caption, Sprite, Grid,
    and Entity all inherit from Drawable, allowing shared functionality
    and isinstance() checks.

Author: John McCardle <mccardle.john@gmail.com>

    refactor: move position property to UIDrawable base class (UISprite)

    - Update UISprite to use base class position instead of sprite position
    - Synchronize sprite position with base class position for rendering
    - Implement onPositionChanged() for position synchronization
    - Update all UISprite methods to use base position consistently
    - Add comprehensive test coverage for UISprite position handling

    This is part 3 of moving position to the base class. UIGrid is the final
    class that needs to be updated.

Author: John McCardle <mccardle.john@gmail.com>

    refactor: move position property to UIDrawable base class (UICaption)

    - Update UICaption to use base class position instead of text position
    - Synchronize text position with base class position for rendering
    - Add onPositionChanged() virtual method for position synchronization
    - Update all UICaption methods to use base position consistently
    - Add comprehensive test coverage for UICaption position handling

    This is part 2 of moving position to the base class. UISprite and UIGrid
    will be updated in subsequent commits.

Author: John McCardle <mccardle.john@gmail.com>

    refactor: move position property to UIDrawable base class (UIFrame)

    - Add position member to UIDrawable base class
    - Add common position getters/setters (x, y, pos) to base class
    - Update UIFrame to use base class position instead of box position
    - Synchronize box position with base class position for rendering
    - Update all UIFrame methods to use base position consistently
    - Add comprehensive test coverage for UIFrame position handling

    This is part 1 of moving position to the base class. Other derived classes
    (UICaption, UISprite, UIGrid) will be updated in subsequent commits.

Author: John McCardle <mccardle.john@gmail.com>

    refactor: remove UIEntity collision_pos field

    - Remove redundant collision_pos field from UIEntity
    - Update position getters/setters to use integer-cast position when needed
    - Remove all collision_pos synchronization code
    - Simplify entity position handling to use single float position field
    - Add comprehensive test coverage proving functionality is preserved

    This removes technical debt and simplifies the codebase without changing API behavior.

Author: John McCardle <mccardle.john@gmail.com>

    feat: add PyArgHelpers infrastructure for standardized argument parsing

    - Create PyArgHelpers.h with parsing functions for position, size, grid coordinates, and color
    - Support tuple-based vector arguments with conflict detection
    - Provide consistent error messages and validation
    - Add comprehensive test coverage for infrastructure

    This sets the foundation for standardizing all Python API constructors.

Author: John McCardle <mccardle.john@gmail.com>

    docs: mark Phase 6 (Rendering Revolution) as complete

    Phase 6 is now complete with all core rendering features implemented:

    Completed Features:
    - Grid background colors (#50) - customizable backgrounds with animation
    - RenderTexture overhaul (#6) - UIFrame clipping with opt-in architecture
    - Viewport-based rendering (#8) - three scaling modes with coordinate transform

    Strategic Decisions:
    - UIGrid already has optimal RenderTexture implementation for its viewport needs
    - UICaption/UISprite clipping deemed unnecessary (no children to clip)
    - Effects/Shader/Particle systems deferred to post-Phase 7 for focused delivery

    The rendering foundation is now solid and ready for Phase 7: Documentation & Distribution.

Author: John McCardle <mccardle.john@gmail.com>

    feat(viewport): complete viewport-based rendering system (#8)

    Implements a comprehensive viewport system that allows fixed game resolution
    with flexible window scaling, addressing the primary wishes for issues #34, #49, and #8.

    Key Features:
    - Fixed game resolution independent of window size (window.game_resolution property)
    - Three scaling modes accessible via window.scaling_mode:
      - "center": 1:1 pixels, viewport centered in window
      - "stretch": viewport fills window, ignores aspect ratio
      - "fit": maintains aspect ratio with black bars
    - Automatic window-to-game coordinate transformation for mouse input
    - Full Python API integration with PyWindow properties

    Technical Implementation:
    - GameEngine::ViewportMode enum with Center, Stretch, Fit modes
    - SFML View system for efficient GPU-based viewport scaling
    - updateViewport() recalculates on window resize or mode change
    - windowToGameCoords() transforms mouse coordinates correctly
    - PyScene mouse input automatically uses transformed coordinates

    Tests:
    - test_viewport_simple.py: Basic API functionality
    - test_viewport_visual.py: Visual verification with screenshots
    - test_viewport_scaling.py: Interactive mode switching and resizing

    This completes the viewport-based rendering task and provides the foundation
    for resolution-independent game development as requested for Crypt of Sokoban.

Author: John McCardle <mccardle.john@gmail.com>

    docs: update ROADMAP for Phase 6 progress

    - Marked Phase 6 as IN PROGRESS
    - Updated RenderTexture overhaul (#6) as PARTIALLY COMPLETE
    - Marked Grid background colors (#50) as COMPLETED
    - Added technical notes from implementation experience
    - Identified viewport rendering (#8) as next priority

Author: John McCardle <mccardle.john@gmail.com>

    feat(rendering): implement RenderTexture base infrastructure and UIFrame clipping (#6)

    - Added RenderTexture support to UIDrawable base class
      - std::unique_ptr<sf::RenderTexture> for opt-in rendering
      - Dirty flag system for optimization
      - enableRenderTexture() and markDirty() methods

    - Implemented clip_children property for UIFrame
      - Python-accessible boolean property
      - Automatic RenderTexture creation when enabled
      - Proper coordinate transformation for nested frames

    - Updated UIFrame::render() for clipping support
      - Renders to RenderTexture when clip_children=true
      - Handles nested clipping correctly
      - Only re-renders when dirty flag is set

    - Added comprehensive dirty flag propagation
      - All property setters mark frame as dirty
      - Size changes recreate RenderTexture
      - Animation system integration

    - Created tests for clipping functionality
      - Basic clipping test with visual verification
      - Advanced nested clipping test
      - Dynamic resize handling test

    This is Phase 1 of the RenderTexture overhaul, providing the foundation
    for advanced rendering effects like blur, glow, and viewport rendering.

Author: John McCardle <mccardle.john@gmail.com>

    docs: create RenderTexture overhaul design document

    - Comprehensive design for Issue #6 implementation
    - Opt-in architecture to maintain backward compatibility
    - Phased implementation plan with clear milestones
    - Performance considerations and risk mitigation
    - API design for clipping and future effects

    Also includes Grid background color test

Author: John McCardle <mccardle.john@gmail.com>

    feat(Grid): add customizable background_color property (#50)

    - Added sf::Color background_color member with default dark gray
    - Python property getter/setter for background_color
    - Animation support for individual color components (r/g/b/a)
    - Replaces hardcoded clear color in render method
    - Test demonstrates color changes and property access

    Closes #50

Author: John McCardle <mccardle.john@gmail.com>

    docs: update roadmap for Phase 6 preparation

    - Mark Phase 5 (Window/Scene Architecture) as complete
    - Update issue statuses (#34, #61, #1, #105 completed)
    - Add Phase 6 implementation strategy for RenderTexture overhaul
    - Archive Phase 5 test files to .archive/
    - Identify quick wins and technical approach for rendering work

Author: John McCardle <mccardle.john@gmail.com>

    feat(Phase 5): Complete Window/Scene Architecture

    - Window singleton with properties (resolution, fullscreen, vsync, title)
    - OOP Scene support with lifecycle methods (on_enter, on_exit, on_keypress, update)
    - Window resize events trigger scene.on_resize callbacks
    - Scene transitions (fade, slide_left/right/up/down) with smooth animations
    - Full integration of Python Scene objects with C++ engine

    All Phase 5 tasks (#34, #1, #61, #105) completed successfully.

Author: John McCardle <mccardle.john@gmail.com>

    research: SFML 3.0 migration analysis

    - Analyzed SFML 3.0 breaking changes (event system, scoped enums, C++17)
    - Assessed migration impact on McRogueFace (40+ files affected)
    - Evaluated timing relative to mcrfpy.sfml module plans
    - Recommended deferring migration until after mcrfpy.sfml implementation
    - Created SFML_3_MIGRATION_RESEARCH.md with comprehensive strategy

Author: John McCardle <mccardle.john@gmail.com>

    research: SFML exposure options analysis (#14)

    - Analyzed current SFML 2.6.1 usage throughout codebase
    - Evaluated python-sfml (abandoned, only supports SFML 2.3.2)
    - Recommended direct integration as mcrfpy.sfml module
    - Created comprehensive SFML_EXPOSURE_RESEARCH.md with implementation plan
    - Identified opportunity to provide modern SFML 2.6+ Python bindings

Author: John McCardle <mccardle.john@gmail.com>

    feat: add basic profiling/metrics system (#104)

    - Add ProfilingMetrics struct to track performance data
    - Track frame time (current and 60-frame rolling average)
    - Calculate FPS from average frame time
    - Count draw calls, UI elements, and visible elements per frame
    - Track total runtime and current frame number
    - PyScene counts elements during render
    - Expose metrics via mcrfpy.getMetrics() returning dict

    This provides basic performance monitoring capabilities for
    identifying bottlenecks and optimizing rendering performance.

Author: John McCardle <mccardle.john@gmail.com>

    fix: improve click handling with proper z-order and coordinate transforms

    - UIFrame: Fix coordinate transformation (subtract parent pos, not add)
    - UIFrame: Check children in reverse order (highest z-index first)
    - UIFrame: Skip invisible elements entirely
    - PyScene: Sort elements by z-index before checking clicks
    - PyScene: Stop at first element that handles the click
    - UIGrid: Implement entity click detection with grid coordinate transform
    - UIGrid: Check entities in reverse order, return sprite as target

    Click events now correctly respect z-order (top elements get priority),
    handle coordinate transforms for nested frames, and support clicking
    on grid entities. Elements without click handlers are transparent to
    clicks, allowing elements below to receive them.

    Note: Click testing requires non-headless mode due to PyScene limitation.

    feat: implement name system for finding UI elements (#39/40/41)

    - Add 'name' property to UIDrawable base class
    - All UI elements (Frame, Caption, Sprite, Grid, Entity) support .name
    - Entity delegates name to its sprite member
    - Add find(name, scene=None) function for exact match search
    - Add findAll(pattern, scene=None) with wildcard support (* matches any sequence)
    - Both functions search recursively through Frame children and Grid entities
    - Comprehensive test coverage for all functionality

    This provides a simple way to find UI elements by name in Python scripts,
    supporting both exact matches and wildcard patterns.

Author: John McCardle <mccardle.john@gmail.com>

    fix: prevent segfault when closing window via X button

    - Add cleanup() method to GameEngine to clear Python references before destruction
    - Clear timers and McRFPy_API references in proper order
    - Call cleanup() at end of run loop and in destructor
    - Ensure cleanup is only called once per GameEngine instance

    Also includes:
    - Fix audio ::stop() calls (already in place, OpenAL warning is benign)
    - Add Caption support for x, y keywords (e.g. Caption("text", x=5, y=10))
    - Refactor UIDrawable_methods.h into UIBase.h for better organization
    - Move UIEntity-specific implementations to UIEntityPyMethods.h

Author: John McCardle <mccardle.john@gmail.com>

    feat: stabilize test suite and add UIDrawable methods

    - Add visible, opacity properties to all UI classes (#87, #88)
    - Add get_bounds(), move(), resize() methods to UIDrawable (#89, #98)
    - Create UIDrawable_methods.h with template implementations
    - Fix test termination issues - all tests now exit properly
    - Fix test_sprite_texture_swap.py click handler signature
    - Fix test_drawable_base.py segfault in headless mode
    - Convert audio objects to pointers for cleanup (OpenAL warning persists)
    - Remove debug print statements from UICaption
    - Special handling for UIEntity to delegate drawable methods to sprite

    All test files are now "airtight" - they complete successfully,
    terminate on their own, and handle edge cases properly.

Author: John McCardle <mccardle.john@gmail.com>

    docs: add Phase 1-3 completion summary

    - Document all completed tasks across three phases
    - Show before/after API improvements
    - Highlight technical achievements
    - Outline next steps for Phase 4-7

Author: John McCardle <mccardle.john@gmail.com>

    feat: implement mcrfpy.Timer object with pause/resume/cancel capabilities closes #103

    - Created PyTimer.h/cpp with object-oriented timer interface
    - Enhanced PyTimerCallable with pause/resume state tracking
    - Added timer control methods: pause(), resume(), cancel(), restart()
    - Added timer properties: interval, remaining, paused, active, callback
    - Fixed timing logic to prevent rapid catch-up after resume
    - Timer objects automatically register with game engine
    - Added comprehensive test demonstrating all functionality

Author: John McCardle <mccardle.john@gmail.com>

    feat(Color): add helper methods from_hex, to_hex, lerp closes #94

    - Add Color.from_hex(hex_string) class method for creating colors from hex
    - Support formats: #RRGGBB, RRGGBB, #RRGGBBAA, RRGGBBAA
    - Add color.to_hex() to convert Color to hex string
    - Add color.lerp(other, t) for smooth color interpolation
    - Comprehensive test coverage for all methods

Author: John McCardle <mccardle.john@gmail.com>

    fix: properly configure UTF-8 encoding for Python stdio

    - Use PyConfig to set stdio_encoding="UTF-8" during initialization
    - Set stdio_errors="surrogateescape" for robust handling
    - Configure in both init_python() and init_python_with_config()
    - Cleaner solution than wrapping streams after initialization
    - Fixes UnicodeEncodeError when printing unicode characters

Author: John McCardle <mccardle.john@gmail.com>

    feat(Vector): implement arithmetic operations closes #93

    - Add PyNumberMethods with add, subtract, multiply, divide, negate, absolute
    - Add rich comparison for equality/inequality checks
    - Add boolean check (zero vector is False)
    - Implement vector methods: magnitude(), normalize(), dot(), distance_to(), angle(), copy()
    - Fix UIDrawable::get_click() segfault when click_callable is null
    - Comprehensive test coverage for all arithmetic operations

Author: John McCardle <mccardle.john@gmail.com>

    feat: Complete position argument standardization for all UI classes

    - Frame and Sprite now support pos keyword override
    - Entity now accepts x,y arguments (was pos-only before)
    - All UI classes now consistently support:
      - (x, y) positional
      - ((x, y)) tuple
      - x=x, y=y keywords
      - pos=(x,y) keyword
      - pos=Vector keyword
    - Improves API consistency and flexibility

Author: John McCardle <mccardle.john@gmail.com>

    feat: Standardize position arguments across all UI classes

    - Create PyPositionHelper for consistent position parsing
    - Grid.at() now accepts (x,y), ((x,y)), x=x, y=y, pos=(x,y)
    - Caption now accepts x,y args in addition to pos
    - Grid init fully supports keyword arguments
    - Maintain backward compatibility for all formats
    - Consistent error messages across classes

Author: John McCardle <mccardle.john@gmail.com>

    feat: Add Entity.die() method for lifecycle management closes #30

    - Remove entity from its grid's entity list
    - Clear grid reference after removal
    - Safe to call multiple times (no-op if not on grid)
    - Works with shared_ptr entity management

Author: John McCardle <mccardle.john@gmail.com>

    perf: Skip out-of-bounds entities during Grid rendering closes #52

    - Add visibility bounds check in entity render loop
    - Skip entities outside view with 1 cell margin
    - Improves performance for large grids with many entities
    - Bounds check considers zoom and pan settings

Author: John McCardle <mccardle.john@gmail.com>

    verify: Sprite texture swapping functionality closes #19

    - Texture property getter/setter already implemented
    - Position/scale preservation during swap confirmed
    - Type validation for texture assignment working
    - Tests verify functionality is complete

Author: John McCardle <mccardle.john@gmail.com>

    feat: Grid size tuple support closes #90

    - Add grid_size keyword parameter to Grid.__init__
    - Accept tuple or list of two integers
    - Override grid_x/grid_y if grid_size provided
    - Maintain backward compatibility
    - Add comprehensive test coverage

Author: John McCardle <mccardle.john@gmail.com>

    feat: Phase 1 - safe constructors and _Drawable foundation

    Closes #7 - Make all UI class constructors safe:
    - Added safe default constructors for UISprite, UIGrid, UIEntity, UICaption
    - Initialize all members to predictable values
    - Made Python init functions accept no arguments
    - Added x,y properties to UIEntity

    Closes #71 - Create _Drawable Python base class:
    - Created PyDrawable.h/cpp with base type (not yet inherited by UI types)
    - Registered in module initialization

    Closes #87 - Add visible property:
    - Added bool visible=true to UIDrawable base class
    - All render methods check visibility before drawing

    Closes #88 - Add opacity property:
    - Added float opacity=1.0 to UIDrawable base class
    - UICaption and UISprite apply opacity to alpha channel

    Closes #89 - Add get_bounds() method:
    - Virtual method returns sf::FloatRect(x,y,w,h)
    - Implemented in Frame, Caption, Sprite, Grid

    Closes #98 - Add move() and resize() methods:
    - move(dx,dy) for relative movement
    - resize(w,h) for absolute sizing
    - Caption resize is no-op (size controlled by font)

Author: John McCardle <mccardle.john@gmail.com>

    docs: comprehensive alpha_streamline_2 plan and strategic vision

    - Add 7-phase development plan for alpha_streamline_2 branch
    - Define architectural dependencies and critical path
    - Identify new issues needed (Timer objects, event system, etc.)
    - Add strategic vision document with 3 transformative directions
    - Timeline: 10-12 weeks to solid Beta foundation

Author: John McCardle <mccardle.john@gmail.com>

    feat(Grid): flexible at() method arguments

    - Support tuple argument: grid.at((x, y))
    - Support keyword arguments: grid.at(x=5, y=3)
    - Support pos keyword: grid.at(pos=(2, 8))
    - Maintain backward compatibility with grid.at(x, y)
    - Add comprehensive error handling for invalid arguments

    Improves API ergonomics and Python-like flexibility
2025-07-09 22:41:15 -04:00
John McCardle cd0bd5468b Squashed commit of the following: [alpha_streamline_1]
the low-hanging fruit of pre-existing issues and standardizing the
Python interfaces

Special thanks to Claude Code, ~100k output tokens for this merge

    🤖 Generated with [Claude Code](https://claude.ai/code)
    Co-Authored-By: Claude <noreply@anthropic.com>

commit 99f301e3a0
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 16:25:32 2025 -0400

    Add position tuple support and pos property to UI elements

    closes #83, closes #84

    - Issue #83: Add position tuple support to constructors
      - Frame and Sprite now accept both (x, y) and ((x, y)) forms
      - Also accept Vector objects as position arguments
      - Caption and Entity already supported tuple/Vector forms
      - Uses PyVector::from_arg for flexible position parsing

    - Issue #84: Add pos property to Frame and Sprite
      - Added pos getter that returns a Vector
      - Added pos setter that accepts Vector or tuple
      - Provides consistency with Caption and Entity which already had pos properties
      - All UI elements now have a uniform way to get/set positions as Vectors

    Both features improve API consistency and make it easier to work with positions.

commit 2f2b488fb5
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 16:18:10 2025 -0400

    Standardize sprite_index property and add scale_x/scale_y to UISprite

    closes #81, closes #82

    - Issue #81: Standardized property name to sprite_index across UISprite and UIEntity
      - Added sprite_index as the primary property name
      - Kept sprite_number as a deprecated alias for backward compatibility
      - Updated repr() methods to use sprite_index
      - Updated animation system to recognize both names

    - Issue #82: Added scale_x and scale_y properties to UISprite
      - Enables non-uniform scaling of sprites
      - scale property still works for uniform scaling
      - Both properties work with the animation system

    All existing code using sprite_number continues to work due to backward compatibility.

commit 5a003a9aa5
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 16:09:52 2025 -0400

    Fix multiple low priority issues

    closes #12, closes #80, closes #95, closes #96, closes #99

    - Issue #12: Set tp_new to NULL for GridPoint and GridPointState to prevent instantiation from Python
    - Issue #80: Renamed Caption.size to Caption.font_size for semantic clarity
    - Issue #95: Fixed UICollection repr to show actual derived types instead of generic UIDrawable
    - Issue #96: Added extend() method to UICollection for API consistency with UIEntityCollection
    - Issue #99: Exposed read-only properties for Texture (sprite_width, sprite_height, sheet_width, sheet_height, sprite_count, source) and Font (family, source)

    All issues have corresponding tests that verify the fixes work correctly.

commit e5affaf317
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 15:50:09 2025 -0400

    Fix critical issues: script loading, entity types, and color properties

    - Issue #37: Fix Windows scripts subdirectory not checked
      - Updated executeScript() to use executable_path() from platform.h
      - Scripts now load correctly when working directory differs from executable

    - Issue #76: Fix UIEntityCollection returns wrong type
      - Updated UIEntityCollectionIter::next() to check for stored Python object
      - Derived Entity classes now preserve their type when retrieved from collections

    - Issue #9: Recreate RenderTexture when resized (already fixed)
      - Confirmed RenderTexture recreation already implemented in set_size() and set_float_member()
      - Uses 1.5x padding and 4096 max size limit

    - Issue #79: Fix Color r, g, b, a properties return None
      - Implemented get_member() and set_member() in PyColor.cpp
      - Color component properties now work correctly with proper validation

    - Additional fix: Grid.at() method signature
      - Changed from METH_O to METH_VARARGS to accept two arguments

    All fixes include comprehensive tests to verify functionality.

    closes #37, closes #76, closes #9, closes #79
2025-07-05 18:56:02 -04:00
John McCardle e6dbb2d560 Squashed commit of the following: [interpreter_mode]
closes #63
closes #69
closes #59
closes #47
closes #2
closes #3
closes #33
closes #27
closes #73
closes #74
closes #78

  I'd like to thank Claude Code for ~200-250M total tokens and 500-700k output tokens

    🤖 Generated with [Claude Code](https://claude.ai/code)
    Co-Authored-By: Claude <noreply@anthropic.com>

commit 9bd1561bfc
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 11:20:07 2025 -0400

    Alpha 0.1 release
    - Move RenderTexture (#6) out of alpha requirements, I don't need it
      that badly
    - alpha blockers resolved:
      * Animation system (#59)
      * Z-order rendering (#63)
      * Python Sequence Protocol (#69)
      * New README (#47)
      * Removed deprecated methods (#2, #3)

    🍾 McRogueFace 0.1.0

commit 43321487eb
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 10:36:09 2025 -0400

    Issue #63 (z-order rendering) complete
    - Archive z-order test files

commit 90c318104b
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 10:34:06 2025 -0400

    Fix Issue #63: Implement z-order rendering with dirty flag optimization

    - Add dirty flags to PyScene and UIFrame to track when sorting is needed
    - Implement lazy sorting - only sort when z_index changes or elements are added/removed
    - Make Frame children respect z_index (previously rendered in insertion order only)
    - Update UIDrawable::set_int to notify when z_index changes
    - Mark collections dirty on append, remove, setitem, and slice operations
    - Remove per-frame vector copy in PyScene::render for better performance

commit e4482e7189
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 01:58:03 2025 -0400

    Implement complete Python Sequence Protocol for collections (closes #69)

    Major implementation of the full sequence protocol for both UICollection
    and UIEntityCollection, making them behave like proper Python sequences.

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

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

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

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

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

commit 70cf44f8f0
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 00:56:42 2025 -0400

    Implement comprehensive animation system (closes #59)

    - Add Animation class with 30+ easing functions (linear, ease in/out, quad, cubic, elastic, bounce, etc.)
    - Add property system to all UI classes for animation support:
      - UIFrame: position, size, colors (including individual r/g/b/a components)
      - UICaption: position, size, text, colors
      - UISprite: position, scale, sprite_number (with sequence support)
      - UIGrid: position, size, camera center, zoom
      - UIEntity: position, sprite properties
    - Create AnimationManager singleton for frame-based updates
    - Add Python bindings through PyAnimation wrapper
    - Support for delta animations (relative values)
    - Fix segfault when running scripts directly (mcrf_module initialization)
    - Fix headless/windowed mode behavior to respect --headless flag
    - Animations run purely in C++ without Python callbacks per frame

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

commit 05bddae511
Author: John McCardle <mccardle.john@gmail.com>
Date:   Fri Jul 4 06:59:02 2025 -0400

    Update comprehensive documentation for Alpha release (Issue #47)

    - Completely rewrote README.md to reflect current features
    - Updated GitHub Pages documentation site with:
      - Modern landing page highlighting Crypt of Sokoban
      - Comprehensive API reference (2700+ lines) with exhaustive examples
      - Updated getting-started guide with installation and first game tutorial
      - 8 detailed tutorials covering all major game systems
      - Quick reference cheat sheet for common operations
    - Generated documentation screenshots showing UI elements
    - Fixed deprecated API references and added new features
    - Added automation API documentation
    - Included Python 3.12 requirement and platform-specific instructions

    Note: Text rendering in headless mode has limitations for screenshots

commit af6a5e090b
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:43:58 2025 -0400

    Update ROADMAP.md to reflect completion of Issues #2 and #3

    - Marked both issues as completed with the removal of deprecated action system
    - Updated open issue count from ~50 to ~48
    - These were both Alpha blockers, bringing us closer to release

commit 281800cd23
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:43:22 2025 -0400

    Remove deprecated registerPyAction/registerInputAction system (closes #2, closes #3)

    This is our largest net-negative commit yet\! Removed the entire deprecated
    action registration system that provided unnecessary two-step indirection:
    keyboard → action string → Python callback

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

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

commit cc8a7d20e8
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:13:59 2025 -0400

    Clean up temporary test files

commit ff83fd8bb1
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:13:46 2025 -0400

    Update ROADMAP.md to reflect massive progress today

    - Fixed 12+ critical bugs in a single session
    - Implemented 3 missing features (Entity.index, EntityCollection.extend, sprite validation)
    - Updated Phase 1 progress showing 11 of 12 items complete
    - Added detailed summary of today's achievements with issue numbers
    - Emphasized test-driven development approach used throughout

commit dae400031f
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:12:29 2025 -0400

    Remove deprecated player_input and turn-based functions for Issue #3

    Removed the commented-out player_input(), computerTurn(), and playerTurn()
    functions that were part of the old turn-based system. These are no longer
    needed as input is now handled through Scene callbacks.

    Partial fix for #3

commit cb0130b46e
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:09:06 2025 -0400

    Implement sprite index validation for Issue #33

    Added validation to prevent setting sprite indices outside the valid
    range for a texture. The implementation:
    - Adds getSpriteCount() method to PyTexture to expose total sprites
    - Validates sprite_number setter to ensure index is within bounds
    - Provides clear error messages showing valid range
    - Works for both Sprite and Entity objects

    closes #33

commit 1e7f5e9e7e
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:05:47 2025 -0400

    Implement EntityCollection.extend() method for Issue #27

    Added extend() method to EntityCollection that accepts any iterable
    of Entity objects and adds them all to the collection. The method:
    - Accepts lists, tuples, generators, or any iterable
    - Validates all items are Entity objects
    - Sets the grid association for each added entity
    - Properly handles errors and empty iterables

    closes #27

commit 923350137d
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:02:14 2025 -0400

    Implement Entity.index() method for Issue #73

    Added index() method to Entity class that returns the entity's
    position in its parent grid's entity collection. This enables
    proper entity removal patterns using entity.index().

commit 6134869371
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 20:41:03 2025 -0400

    Add validation to keypressScene() for non-callable arguments

    Added PyCallable_Check validation to ensure keypressScene() only
    accepts callable objects. Now properly raises TypeError with a
    clear error message when passed non-callable arguments like
    strings, numbers, None, or dicts.

commit 4715356b5e
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 20:31:36 2025 -0400

    Fix Sprite texture setter 'error return without exception set'

    Implemented the missing UISprite::set_texture method to properly:
    - Validate the input is a Texture instance
    - Update the sprite's texture using setTexture()
    - Return appropriate error messages for invalid inputs

    The setter now works correctly and no longer returns -1 without
    setting an exception.

commit 6dd1cec600
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 20:27:32 2025 -0400

    Fix Entity property setters and PyVector implementation

    Fixed the 'new style getargs format' error in Entity property setters by:
    - Implementing PyObject_to_sfVector2f/2i using PyVector::from_arg
    - Adding proper error checking in Entity::set_position
    - Implementing PyVector get_member/set_member for x/y properties
    - Fixing PyVector::from_arg to handle non-tuple arguments correctly

    Now Entity.pos and Entity.sprite_number setters work correctly with
    proper type validation.

commit f82b861bcd
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 19:48:33 2025 -0400

    Fix Issue #74: Add missing Grid.grid_y property

    Added individual grid_x and grid_y getter properties to the Grid class
    to complement the existing grid_size property. This allows direct access
    to grid dimensions and fixes error messages that referenced these
    properties before they existed.

    closes #74

commit 59e6f8d53d
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 19:42:32 2025 -0400

    Fix Issue #78: Middle mouse click no longer sends 'C' keyboard event

    The bug was caused by accessing event.key.code on a mouse event without
    checking the event type first. Since SFML uses a union for events, this
    read garbage data. The middle mouse button value (2) coincidentally matched
    the keyboard 'C' value (2), causing the spurious keyboard event.

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

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

    Closes #78

commit 1c71d8d4f7
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 19:36:15 2025 -0400

    Fix Grid to support None/null texture and fix error message bug

    - Allow Grid to be created with None as texture parameter
    - Use default cell dimensions (16x16) when no texture provided
    - Skip sprite rendering when texture is null, but still render colors
    - Fix issue #77: Corrected copy/paste error in Grid.at() error messages
    - Grid now functional for color-only rendering and entity positioning

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

    Closes #77

commit 18cfe93a44
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 19:25:49 2025 -0400

    Fix --exec interactive prompt bug and create comprehensive test suite

    Major fixes:
    - Fixed --exec entering Python REPL instead of game loop
    - Resolved screenshot transparency issue (requires timer callbacks)
    - Added debug output to trace Python initialization

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

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

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

    The Grid segfault is now the highest priority as it blocks all Grid-based functionality.

commit 9ad0b6850d
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 15:55:24 2025 -0400

    Update ROADMAP.md to reflect Python interpreter and automation API progress

    - Mark #32 (Python interpreter behavior) as 90% complete
      - All major Python flags implemented: -h, -V, -c, -m, -i
      - Script execution with proper sys.argv handling works
      - Only stdin (-) support missing

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

    - Flag issues #53 and #45 as potentially aided by automation API

commit 7ec4698653
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 14:57:59 2025 -0400

    Update ROADMAP.md to remove closed issues

    - Remove #72 (iterator improvements - closed)
    - Remove #51 (UIEntity derive from UIDrawable - closed)
    - Update issue counts: 64 open issues from original 78
    - Update dependencies and references to reflect closed issues
    - Clarify that core iterators are complete, only grid points remain

commit 68c1a016b0
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 14:27:01 2025 -0400

    Implement --exec flag and PyAutoGUI-compatible automation API

    - Add --exec flag to execute multiple scripts before main program
    - Scripts are executed in order and share Python interpreter state
    - Implement full PyAutoGUI-compatible automation API in McRFPy_Automation
    - Add screenshot, mouse control, keyboard input capabilities
    - Fix Python initialization issues when multiple scripts are loaded
    - Update CommandLineParser to handle --exec with proper sys.argv management
    - Add comprehensive examples and documentation

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

    Closes #32 partially (Python interpreter emulation)
    References automation testing requirements

commit 763fa201f0
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 10:43:17 2025 -0400

    Python command emulation

commit a44b8c93e9
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 09:42:46 2025 -0400

    Prep: Cleanup for interpreter mode
2025-07-05 17:23:09 -04:00
133 changed files with 24435 additions and 2853 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

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

3
.gitignore vendored
View File

@ -9,6 +9,7 @@ obj
build
lib
obj
__pycache__
.cache/
7DRL2025 Release/
@ -27,3 +28,5 @@ forest_fire_CA.py
mcrogueface.github.io
scripts/
test_*
tcod_reference

1093
ALPHA_STREAMLINE_WORKLOG.md Normal file

File diff suppressed because it is too large Load Diff

283
CLAUDE.md
View File

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

View File

@ -1,23 +1,24 @@
# McRogueFace
*Blame my wife for the name*
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
**Latest Release**: Successfully completed 7DRL 2025 with *"Crypt of Sokoban"* - a unique roguelike that blends Sokoban puzzle mechanics with dungeon crawling!
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
## Features
## Tenets
- **Python-First Design**: Write your game logic in Python while leveraging C++ performance
- **Rich UI System**: Sprites, Grids, Frames, and Captions with full animation support
- **Entity-Component Architecture**: Flexible game object system with Python integration
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
- **Automation API**: PyAutoGUI-compatible testing and demo recording
- **Interactive Development**: Python REPL integration for live game debugging
- **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
```bash
# Clone and build
git clone https://github.com/jmcb/McRogueFace.git
git clone <wherever you found this repo>
cd McRogueFace
make
@ -35,9 +36,9 @@ import mcrfpy
mcrfpy.createScene("intro")
# Add a text caption
caption = mcrfpy.Caption(50, 50, "Welcome to McRogueFace!")
caption.font = mcrfpy.default_font
caption.font_color = (255, 255, 255)
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
caption.size = 48
caption.fill_color = (255, 255, 255)
# Add to scene
mcrfpy.sceneUI("intro").append(caption)
@ -72,7 +73,9 @@ McRogueFace/
## Contributing
McRogueFace is under active development. Check the [ROADMAP.md](ROADMAP.md) for current priorities and open issues.
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.
The project has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests.
## License
@ -80,6 +83,6 @@ This project is licensed under the MIT License - see LICENSE file for details.
## Acknowledgments
- Developed for 7-Day Roguelike Challenge 2025
- Developed for 7-Day Roguelike 2023, 2024, 2025 - here's to many more
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
- Inspired by David Churchill's COMP4300 game engine lectures

View File

@ -1,362 +0,0 @@
# McRogueFace - Development Roadmap
## Project Status: 🎉 ALPHA 0.1 RELEASE! 🎉
**Current State**: Alpha release achieved! All critical blockers resolved!
**Latest Update**: Moved RenderTexture (#6) to Beta - Alpha is READY! (2025-07-05)
**Branch**: interpreter_mode (ready for alpha release merge)
**Open Issues**: ~46 remaining (non-blocking quality-of-life improvements)
---
## Recent Achievements
### 2025-07-05: ALPHA 0.1 ACHIEVED! 🎊🍾
**All Alpha Blockers Resolved!**
- Z-order rendering with performance optimization (Issue #63)
- Python Sequence Protocol for collections (Issue #69)
- Comprehensive Animation System (Issue #59)
- Moved RenderTexture to Beta (not needed for Alpha)
- **McRogueFace is ready for Alpha release!**
### 2025-07-05: Z-order Rendering Complete! 🎉
**Issue #63 Resolved**: Consistent z-order rendering with performance optimization
- Dirty flag pattern prevents unnecessary per-frame sorting
- Lazy sorting for both Scene elements and Frame children
- Frame children now respect z_index (fixed inconsistency)
- Automatic dirty marking on z_index changes and collection modifications
- Performance: O(1) check for static scenes vs O(n log n) every frame
### 2025-07-05: Python Sequence Protocol Complete! 🎉
**Issue #69 Resolved**: Full sequence protocol implementation for collections
- Complete __setitem__, __delitem__, __contains__ support
- Slice operations with extended slice support (step != 1)
- Concatenation (+) and in-place concatenation (+=) with validation
- Negative indexing throughout, index() and count() methods
- Type safety: UICollection (Frame/Caption/Sprite/Grid), EntityCollection (Entity only)
- Default value support: None for texture/font parameters uses engine defaults
### 2025-07-05: Animation System Complete! 🎉
**Issue #59 Resolved**: Comprehensive animation system with 30+ easing functions
- Property-based animations for all UI classes (Frame, Caption, Sprite, Grid, Entity)
- Individual color component animation (r/g/b/a)
- Sprite sequence animation and text typewriter effects
- Pure C++ execution without Python callbacks
- Delta animation support for relative values
### 2025-01-03: Major Stability Update
**Major Cleanup**: Removed deprecated registerPyAction system (-180 lines)
**Bug Fixes**: 12 critical issues including Grid segfault, Issue #78 (middle click), Entity setters
**New Features**: Entity.index() (#73), EntityCollection.extend() (#27), Sprite validation (#33)
**Test Coverage**: Comprehensive test suite with timer callback pattern established
---
## 🔧 CURRENT WORK: Python Interpreter Mode & Automation API
### Branch: interpreter_mode
**Status**: Actively implementing Python interpreter emulation features
#### Completed Features:
- [x] **--exec flag implementation** - Execute multiple scripts before main program
- Scripts execute in order and share Python interpreter state
- Proper sys.argv handling for main script execution
- Compatible with -i (interactive), -c (command), and -m (module) modes
- [x] **PyAutoGUI-compatible Automation API** - Full automation testing capability
- Screenshot capture: `automation.screenshot(filename)`
- Mouse control: `click()`, `moveTo()`, `dragTo()`, `scroll()`
- Keyboard input: `typewrite()`, `hotkey()`, `keyDown()`, `keyUp()`
- Event injection into SFML render loop
- **Enables**: Automated UI testing, demo recording/playback, accessibility testing
#### Architectural Decisions:
1. **Single-threaded design maintained** - All Python runs in main thread between frames
2. **Honor system for scripts** - Scripts must return control to C++ render loop
3. **Shared Python state** - All --exec scripts share the same interpreter
4. **No threading complexity** - Chose simplicity over parallelism (see THREADING_FOOTGUNS.md)
5. **Animation system in pure C++** - All interpolation happens in C++ for performance
6. **Property-based animation** - Unified interface for all UI element properties
#### Key Files Created:
- `src/McRFPy_Automation.h/cpp` - Complete automation API implementation
- `EXEC_FLAG_DOCUMENTATION.md` - Usage guide and examples
- `AUTOMATION_ARCHITECTURE_REPORT.md` - Design analysis and alternatives
- Multiple example scripts demonstrating automation patterns
#### Addresses:
- **#32** - Executable behave like `python` command (90% complete - all major Python interpreter flags implemented)
#### Test Suite Results (2025-07-03):
Created comprehensive test suite with 13 tests covering all Python-exposed methods:
**✅ Fixed Issues:**
- Fixed `--exec` Python interactive prompt bug (was entering REPL instead of game loop)
- Resolved screenshot transparency issue (must use timer callbacks for rendered content)
- Updated CLAUDE.md with testing guidelines and patterns
**❌ Critical Bugs Found:**
1. **SEGFAULT**: Grid class crashes on instantiation (blocks all Grid functionality)
2. **#78 CONFIRMED**: Middle mouse click sends 'C' keyboard event
3. **Entity property setters**: "new style getargs format" error
4. **Sprite texture setter**: Returns "error return without exception set"
5. **keypressScene()**: Segfaults on non-callable arguments
**📋 Missing Features Confirmed:**
- #73: Entity.index() method
- #27: EntityCollection.extend() method
- #41: UICollection.find(name) method
- #38: Frame 'children' constructor parameter
- #33: Sprite index validation
---
## 🚀 NEXT PHASE: Beta Features & Polish
### Alpha Complete! Moving to Beta Priorities:
1. ~~**#69** - Python Sequence Protocol for collections~~ - *Completed! (2025-07-05)*
2. ~~**#63** - Z-order rendering for UIDrawables~~ - *Completed! (2025-07-05)*
3. ~~**#59** - Animation system~~ - *Completed! (2025-07-05)*
4. **#6** - RenderTexture concept - *Extensive Overhaul*
5. ~~**#47** - New README.md for Alpha release~~ - *Completed*
- [x] **#78** - Middle Mouse Click sends "C" keyboard event - *Fixed*
- [x] **#77** - Fix error message copy/paste bug - *Fixed*
- [x] **#74** - Add missing `Grid.grid_y` property - *Fixed*
- [ ] **#37** - Fix Windows build module import from "scripts" directory - *Isolated Fix*
- [x] **Entity Property Setters** - Fix "new style getargs format" error - *Fixed*
- [x] **Sprite Texture Setter** - Fix "error return without exception set" - *Fixed*
- [x] **keypressScene() Validation** - Add proper error handling - *Fixed*
### 🔄 Complete Iterator System
**Status**: Core iterators complete (#72 closed), Grid point iterators still pending
- [ ] **Grid Point Iterator Implementation** - Complete the remaining grid iteration work
- [x] **#73** - Add `entity.index()` method for collection removal - *Fixed*
- [x] **#69** ⚠️ **Alpha Blocker** - Refactor all collections to use Python Sequence Protocol - *Completed! (2025-07-05)*
**Dependencies**: Grid point iterators → #73 entity.index() → #69 Sequence Protocol overhaul
---
## ✅ ALPHA 0.1 RELEASE ACHIEVED! (All Blockers Complete)
### ✅ All Alpha Requirements Complete!
- [x] **#69** - Collections use Python Sequence Protocol - *Completed! (2025-07-05)*
- [x] **#63** - Z-order rendering for UIDrawables - *Completed! (2025-07-05)*
- [x] **#59** - Animation system for arbitrary UIDrawable fields - *Completed! (2025-07-05)*
- [x] **#47** - New README.md for Alpha release - *Completed*
- [x] **#3** - Remove deprecated `McRFPy_API::player_input` - *Completed*
- [x] **#2** - Remove `registerPyAction` system - *Completed*
### 📋 Moved to Beta:
- [ ] **#6** - RenderTexture concept - *Moved to Beta (not needed for Alpha)*
---
## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues)
### 🎮 Core Engine Systems
#### Iterator/Collection System (2 issues)
- [x] **#73** - Entity index() method for removal - *Fixed*
- [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)*
#### Python/C++ Integration (7 issues)
- [ ] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations*
- [ ] **#71** - Drawable base class hierarchy - *Extensive Overhaul*
- [ ] **#70** - PyPI wheel distribution - *Extensive Overhaul*
- [~] **#32** - Executable behave like `python` command - *Extensive Overhaul* *(90% Complete: -h, -V, -c, -m, -i, script execution, sys.argv, --exec all implemented. Only stdin (-) support missing)*
- [ ] **#35** - TCOD as built-in module - *Extensive Overhaul*
- [ ] **#14** - Expose SFML as built-in module - *Extensive Overhaul*
- [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations*
#### UI/Rendering System (12 issues)
- [ ] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations*
- [x] **#59** ⚠️ **Alpha Blocker** - Animation system - *Completed! (2025-07-05)*
- [ ] **#6** ⚠️ **Alpha Blocker** - RenderTexture for all UIDrawables - *Extensive Overhaul*
- [ ] **#10** - UIDrawable visibility/AABB system - *Extensive Overhaul*
- [ ] **#8** - UIGrid RenderTexture viewport sizing - *Multiple Integrations*
- [ ] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations*
- [ ] **#52** - UIGrid skip out-of-bounds entities - *Isolated Fix*
- [ ] **#50** - UIGrid background color field - *Isolated Fix*
- [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations*
- [ ] **#17** - Move UISprite position into sf::Sprite - *Isolated Fix*
- [x] **#33** - Sprite index validation against texture range - *Fixed*
#### Grid/Entity System (6 issues)
- [ ] **#30** - Entity/Grid association management (.die() method) - *Extensive Overhaul*
- [ ] **#16** - Grid strict mode for entity knowledge/visibility - *Extensive Overhaul*
- [ ] **#67** - Grid stitching for infinite worlds - *Extensive Overhaul*
- [ ] **#15** - UIGridPointState cleanup and standardization - *Multiple Integrations*
- [ ] **#20** - UIGrid get_grid_size standardization - *Multiple Integrations*
- [ ] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix*
#### Scene/Window Management (5 issues)
- [ ] **#61** - Scene object encapsulating key callbacks - *Extensive Overhaul*
- [ ] **#34** - Window object for resolution/scaling - *Extensive Overhaul*
- [ ] **#62** - Multiple windows support - *Extensive Overhaul*
- [ ] **#49** - Window resolution & viewport controls - *Multiple Integrations*
- [ ] **#1** - Scene resize event handling - *Isolated Fix*
### 🔧 Quality of Life Features
#### UI Enhancement Features (8 issues)
- [ ] **#39** - Name field on UIDrawables - *Multiple Integrations*
- [ ] **#40** - `only_one` arg for unique naming - *Multiple Integrations*
- [ ] **#41** - `.find(name)` method for collections - *Multiple Integrations*
- [ ] **#38** - `children` arg for Frame initialization - *Isolated Fix*
- [ ] **#42** - Click callback arg for UIDrawable init - *Isolated Fix*
- [x] **#27** - UIEntityCollection.extend() method - *Fixed*
- [ ] **#28** - UICollectionIter for scene ui iteration - *Isolated Fix*
- [ ] **#26** - UIEntityCollectionIter implementation - *Isolated Fix*
### 🧹 Refactoring & Cleanup
#### Code Cleanup (7 issues)
- [x] **#3** ⚠️ **Alpha Blocker** - Remove `McRFPy_API::player_input` - *Completed*
- [x] **#2** ⚠️ **Alpha Blocker** - Review `registerPyAction` necessity - *Completed*
- [ ] **#7** - Remove unsafe no-argument constructors - *Multiple Integrations*
- [ ] **#21** - PyUIGrid dealloc cleanup - *Isolated Fix*
- [ ] **#75** - REPL thread separation from SFML window - *Multiple Integrations*
### 📚 Demo & Documentation
#### Documentation (2 issues)
- [ ] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix*
- [ ] **#48** - Dependency compilation documentation - *Isolated Fix*
#### Demo Projects (6 issues)
- [ ] **#54** - Jupyter notebook integration demo - *Multiple Integrations*
- [ ] **#55** - Hunt the Wumpus AI demo - *Multiple Integrations*
- [ ] **#53** - Web interface input demo - *Multiple Integrations* *(New automation API could help)*
- [ ] **#45** - Accessibility mode demos - *Multiple Integrations* *(New automation API could help test)*
- [ ] **#36** - Dear ImGui integration tests - *Extensive Overhaul*
- [ ] **#65** - Python Explorer scene (replaces uitest) - *Extensive Overhaul*
---
## 🎯 RECOMMENDED TRIAGE SEQUENCE
### Phase 1: Foundation Stabilization (1-2 weeks)
```
✅ COMPLETE AS OF 2025-01-03:
1. ✅ Fix Grid Segfault - Grid now supports None/null textures
2. ✅ Fix #78 Middle Mouse Click bug - Event type checking added
3. ✅ Fix Entity/Sprite property setters - PyVector conversion fixed
4. ✅ Fix #77 - Error message copy/paste bug fixed
5. ✅ Fix #74 - Grid.grid_y property added
6. ✅ Fix keypressScene() validation - Now rejects non-callable
7. ✅ Fix Sprite texture setter - No longer returns error without exception
8. ✅ Fix PyVector x/y properties - Were returning None
REMAINING IN PHASE 1:
9. ✅ Fix #73 - Entity.index() method for removal
10. ✅ Fix #27 - EntityCollection.extend() method
11. ✅ Fix #33 - Sprite index validation
12. Alpha Blockers (#3, #2) - Remove deprecated methods
```
### Phase 2: Alpha Release Preparation (4-6 weeks)
```
1. Collections Sequence Protocol (#69) - Major refactor, alpha blocker
2. Z-order rendering (#63) - Essential UI improvement, alpha blocker
3. RenderTexture overhaul (#6) - Core rendering improvement, alpha blocker
4. ✅ Animation system (#59) - COMPLETE! 30+ easing functions, all UI properties
5. ✅ Documentation (#47) - README.md complete, #48 dependency docs remaining
```
### Phase 3: Engine Architecture (6-8 weeks)
```
1. Drawable base class (#71) - Clean up inheritance patterns
2. Entity/Grid associations (#30) - Proper lifecycle management
3. Window object (#34) - Scene/window architecture
4. UIDrawable visibility (#10) - Rendering optimization
```
### Phase 4: Advanced Features (8-12 weeks)
```
1. Grid strict mode (#16) - Entity knowledge/visibility system
2. SFML/TCOD integration (#14, #35) - Expose native libraries
3. Scene object refactor (#61) - Better input handling
4. Name-based finding (#39, #40, #41) - UI element management
5. Demo projects (#54, #55, #36) - Showcase capabilities
```
### Ongoing/Low Priority
```
- PyPI distribution (#70) - Community access
- Multiple windows (#62) - Advanced use cases
- Grid stitching (#67) - Infinite world support
- Accessibility (#45) - Important but not blocking
- Subinterpreter tests (#46) - Performance research
```
---
## 📊 DIFFICULTY ASSESSMENT SUMMARY
**Isolated Fixes (24 issues)**: Single file/function changes
- Bugfixes: #77, #74, #37, #78
- Simple features: #73, #52, #50, #33, #17, #38, #42, #27, #28, #26, #12, #1
- Cleanup: #3, #2, #21, #47, #48
**Multiple Integrations (28 issues)**: Cross-system changes
- UI/Rendering: #63, #8, #9, #19, #39, #40, #41
- Grid/Entity: #15, #20, #76, #46, #49, #75
- Features: #54, #55, #53, #45, #7
**Extensive Overhauls (26 issues)**: Major architectural changes
- Core Systems: #69, #59, #6, #10, #30, #16, #67, #61, #34, #62
- Integration: #71, #70, #32, #35, #14
- Advanced: #36, #65
---
## 🎮 STRATEGIC DIRECTION
### Engine Philosophy Maintained
- **C++ First**: Performance-critical code stays in C++
- **Python Close Behind**: Rich scripting without frame-rate impact
- **Game-Ready**: Each improvement should benefit actual game development
### Architecture Goals
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
3. **Resource Management**: RAII everywhere, proper lifecycle handling
4. **Multi-Platform**: Windows/Linux feature parity maintained
### Success Metrics for Alpha 0.1
- [ ] All Alpha Blocker issues resolved (5 of 7 complete: #69, #59, #47, #3, #2)
- [ ] Grid point iteration complete and tested
- [ ] Clean build on Windows and Linux
- [ ] Documentation sufficient for external developers
- [ ] At least one compelling demo (Wumpus or Jupyter integration)
---
## 📚 REFERENCES & CONTEXT
**Issue Dependencies** (Key Chains):
- Iterator System: Grid points → #73#69 (Alpha Blocker)
- UI Hierarchy: #71#63 (Alpha Blocker)
- Rendering: #6 (Alpha Blocker) → #8, #9#10
- Entity System: #30#16#67
- Window Management: #34#49, #61#62
**Commit References**:
- 167636c: Iterator improvements (UICollection/UIEntityCollection complete)
- Recent work: 7DRL 2025 completion, RPATH updates, console improvements
**Architecture Files**:
- Iterator patterns: src/UICollection.cpp, src/UIGrid.cpp
- Python integration: src/McRFPy_API.cpp, src/PyObjectUtils.h
- Game implementation: src/scripts/ (Crypt of Sokoban complete game)
---
*Last Updated: 2025-07-05*
*Total Open Issues: 62* (from original 78)
*Alpha Status: 🎉 COMPLETE! All blockers resolved!*
*Achievement Unlocked: Alpha 0.1 Release Ready*
*Next Phase: Beta features including RenderTexture (#6), advanced UI patterns, and platform polish*

View File

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

View File

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

36
build_windows.bat Normal file
View File

@ -0,0 +1,36 @@
@echo off
REM Windows build script for McRogueFace
REM Run this over SSH without Visual Studio GUI
echo Building McRogueFace for Windows...
REM Clean previous build
if exist build_win rmdir /s /q build_win
mkdir build_win
cd build_win
REM Generate Visual Studio project files with CMake
REM Use -G to specify generator, -A for architecture
REM Visual Studio 2022 = "Visual Studio 17 2022"
REM Visual Studio 2019 = "Visual Studio 16 2019"
cmake -G "Visual Studio 17 2022" -A x64 ..
if errorlevel 1 (
echo CMake configuration failed!
exit /b 1
)
REM Build using MSBuild (comes with Visual Studio)
REM You can also use cmake --build . --config Release
msbuild McRogueFace.sln /p:Configuration=Release /p:Platform=x64 /m
if errorlevel 1 (
echo Build failed!
exit /b 1
)
echo Build completed successfully!
echo Executable location: build_win\Release\mcrogueface.exe
REM Alternative: Using cmake to build (works with any generator)
REM cmake --build . --config Release --parallel
cd ..

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,189 +0,0 @@
// Example implementation of --exec flag for McRogueFace
// This shows the minimal changes needed to support multiple script execution
// === In McRogueFaceConfig.h ===
struct McRogueFaceConfig {
// ... existing fields ...
// Scripts to execute after main script (McRogueFace style)
std::vector<std::filesystem::path> exec_scripts;
};
// === In CommandLineParser.cpp ===
CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) {
// ... existing parsing code ...
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
// ... existing flag handling ...
else if (arg == "--exec") {
// Add script to exec list
if (i + 1 < argc) {
config.exec_scripts.push_back(argv[++i]);
} else {
std::cerr << "Error: --exec requires a script path\n";
return {true, 1};
}
}
}
}
// === In GameEngine.cpp ===
GameEngine::GameEngine(const McRogueFaceConfig& cfg) : config(cfg) {
// ... existing initialization ...
// Only load game.py if no custom script/command/module is specified
bool should_load_game = config.script_path.empty() &&
config.python_command.empty() &&
config.python_module.empty() &&
!config.interactive_mode &&
!config.python_mode &&
config.exec_scripts.empty(); // Add this check
if (should_load_game) {
if (!Py_IsInitialized()) {
McRFPy_API::api_init();
}
McRFPy_API::executePyString("import mcrfpy");
McRFPy_API::executeScript("scripts/game.py");
}
// Execute any --exec scripts
for (const auto& exec_script : config.exec_scripts) {
std::cout << "Executing script: " << exec_script << std::endl;
McRFPy_API::executeScript(exec_script.string());
}
}
// === Usage Examples ===
// Example 1: Run game with automation
// ./mcrogueface game.py --exec automation.py
// Example 2: Run game with multiple automation scripts
// ./mcrogueface game.py --exec test_suite.py --exec monitor.py --exec logger.py
// Example 3: Run only automation (no game)
// ./mcrogueface --exec standalone_test.py
// Example 4: Headless automation
// ./mcrogueface --headless game.py --exec automation.py
// === Python Script Example (automation.py) ===
/*
import mcrfpy
from mcrfpy import automation
def periodic_test():
"""Run automated tests every 5 seconds"""
# Take screenshot
automation.screenshot(f"test_{mcrfpy.getFrame()}.png")
# Check game state
scene = mcrfpy.currentScene()
if scene == "main_menu":
# Click start button
automation.click(400, 300)
elif scene == "game":
# Perform game tests
automation.hotkey("i") # Open inventory
print(f"Test completed at frame {mcrfpy.getFrame()}")
# Register timer for periodic testing
mcrfpy.setTimer("automation_test", periodic_test, 5000)
print("Automation script loaded - tests will run every 5 seconds")
# Script returns here - giving control back to C++
*/
// === Advanced Example: Event-Driven Automation ===
/*
# automation_advanced.py
import mcrfpy
from mcrfpy import automation
import json
class AutomationFramework:
def __init__(self):
self.test_queue = []
self.results = []
self.load_test_suite()
def load_test_suite(self):
"""Load test definitions from JSON"""
with open("test_suite.json") as f:
self.test_queue = json.load(f)["tests"]
def run_next_test(self):
"""Execute next test in queue"""
if not self.test_queue:
self.finish_testing()
return
test = self.test_queue.pop(0)
try:
if test["type"] == "click":
automation.click(test["x"], test["y"])
elif test["type"] == "key":
automation.keyDown(test["key"])
automation.keyUp(test["key"])
elif test["type"] == "screenshot":
automation.screenshot(test["filename"])
elif test["type"] == "wait":
# Re-queue this test for later
self.test_queue.insert(0, test)
return
self.results.append({"test": test, "status": "pass"})
except Exception as e:
self.results.append({"test": test, "status": "fail", "error": str(e)})
def finish_testing(self):
"""Save test results and cleanup"""
with open("test_results.json", "w") as f:
json.dump(self.results, f, indent=2)
print(f"Testing complete: {len(self.results)} tests executed")
mcrfpy.delTimer("automation_framework")
# Create and start automation
framework = AutomationFramework()
mcrfpy.setTimer("automation_framework", framework.run_next_test, 100)
*/
// === Thread Safety Considerations ===
// The --exec approach requires NO thread safety changes because:
// 1. All scripts run in the same Python interpreter
// 2. Scripts execute sequentially during initialization
// 3. After initialization, only callbacks run (timer/input based)
// 4. C++ maintains control of the render loop
// This is the "honor system" - scripts must:
// - Set up their callbacks/timers
// - Return control to C++
// - Not block or run infinite loops
// - Use timers for periodic tasks
// === Future Extensions ===
// 1. Script communication via shared Python modules
// game.py:
// import mcrfpy
// mcrfpy.game_state = {"level": 1, "score": 0}
//
// automation.py:
// import mcrfpy
// if mcrfpy.game_state["level"] == 1:
// # Test level 1 specific features
// 2. Priority-based script execution
// ./mcrogueface game.py --exec-priority high:critical.py --exec-priority low:logging.py
// 3. Conditional execution
// ./mcrogueface game.py --exec-if-scene menu:menu_test.py --exec-if-scene game:game_test.py

482
generate_api_docs.py Normal file
View File

@ -0,0 +1,482 @@
#!/usr/bin/env python3
"""Generate API reference documentation for McRogueFace.
This script generates comprehensive API documentation in multiple formats:
- Markdown for GitHub/documentation sites
- HTML for local browsing
- RST for Sphinx integration (future)
"""
import os
import sys
import inspect
import datetime
from typing import Dict, List, Any, Optional
from pathlib import Path
# We need to run this with McRogueFace as the interpreter
# so mcrfpy is available
import mcrfpy
def escape_markdown(text: str) -> str:
"""Escape special markdown characters."""
if not text:
return ""
# Escape backticks in inline code
return text.replace("`", "\\`")
def format_signature(name: str, doc: str) -> str:
"""Extract and format function signature from docstring."""
if not doc:
return f"{name}(...)"
lines = doc.strip().split('\n')
if lines and '(' in lines[0]:
# First line contains signature
return lines[0].split('->')[0].strip()
return f"{name}(...)"
def get_class_info(cls: type) -> Dict[str, Any]:
"""Extract comprehensive information about a class."""
info = {
'name': cls.__name__,
'doc': cls.__doc__ or "",
'methods': [],
'properties': [],
'bases': [base.__name__ for base in cls.__bases__ if base.__name__ != 'object'],
}
# Get all attributes
for attr_name in sorted(dir(cls)):
if attr_name.startswith('_') and not attr_name.startswith('__'):
continue
try:
attr = getattr(cls, attr_name)
if isinstance(attr, property):
prop_info = {
'name': attr_name,
'doc': (attr.fget.__doc__ if attr.fget else "") or "",
'readonly': attr.fset is None
}
info['properties'].append(prop_info)
elif callable(attr) and not attr_name.startswith('__'):
method_info = {
'name': attr_name,
'doc': attr.__doc__ or "",
'signature': format_signature(attr_name, attr.__doc__)
}
info['methods'].append(method_info)
except:
pass
return info
def get_function_info(func: Any, name: str) -> Dict[str, Any]:
"""Extract information about a function."""
return {
'name': name,
'doc': func.__doc__ or "",
'signature': format_signature(name, func.__doc__)
}
def generate_markdown_class(cls_info: Dict[str, Any]) -> List[str]:
"""Generate markdown documentation for a class."""
lines = []
# Class header
lines.append(f"### class `{cls_info['name']}`")
if cls_info['bases']:
lines.append(f"*Inherits from: {', '.join(cls_info['bases'])}*")
lines.append("")
# Class description
if cls_info['doc']:
doc_lines = cls_info['doc'].strip().split('\n')
# First line is usually the constructor signature
if doc_lines and '(' in doc_lines[0]:
lines.append(f"```python")
lines.append(doc_lines[0])
lines.append("```")
lines.append("")
# Rest is description
if len(doc_lines) > 2:
lines.extend(doc_lines[2:])
lines.append("")
else:
lines.extend(doc_lines)
lines.append("")
# Properties
if cls_info['properties']:
lines.append("#### Properties")
lines.append("")
for prop in cls_info['properties']:
readonly = " *(readonly)*" if prop['readonly'] else ""
lines.append(f"- **`{prop['name']}`**{readonly}")
if prop['doc']:
lines.append(f" - {prop['doc'].strip()}")
lines.append("")
# Methods
if cls_info['methods']:
lines.append("#### Methods")
lines.append("")
for method in cls_info['methods']:
lines.append(f"##### `{method['signature']}`")
if method['doc']:
# Parse docstring for better formatting
doc_lines = method['doc'].strip().split('\n')
# Skip the signature line if it's repeated
start = 1 if doc_lines and method['name'] in doc_lines[0] else 0
for line in doc_lines[start:]:
lines.append(line)
lines.append("")
lines.append("---")
lines.append("")
return lines
def generate_markdown_function(func_info: Dict[str, Any]) -> List[str]:
"""Generate markdown documentation for a function."""
lines = []
lines.append(f"### `{func_info['signature']}`")
lines.append("")
if func_info['doc']:
doc_lines = func_info['doc'].strip().split('\n')
# Skip signature line if present
start = 1 if doc_lines and func_info['name'] in doc_lines[0] else 0
# Process documentation sections
in_section = None
for line in doc_lines[start:]:
if line.strip() in ['Args:', 'Returns:', 'Raises:', 'Note:', 'Example:']:
in_section = line.strip()
lines.append(f"**{in_section}**")
elif in_section and line.strip():
# Indent content under sections
lines.append(f"{line}")
else:
lines.append(line)
lines.append("")
lines.append("---")
lines.append("")
return lines
def generate_markdown_docs() -> str:
"""Generate complete markdown API documentation."""
lines = []
# Header
lines.append("# McRogueFace API Reference")
lines.append("")
lines.append(f"*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
lines.append("")
# Module description
if mcrfpy.__doc__:
lines.append("## Overview")
lines.append("")
lines.extend(mcrfpy.__doc__.strip().split('\n'))
lines.append("")
# Table of contents
lines.append("## Table of Contents")
lines.append("")
lines.append("- [Classes](#classes)")
lines.append("- [Functions](#functions)")
lines.append("- [Automation Module](#automation-module)")
lines.append("")
# Collect all components
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))
# Document classes
lines.append("## Classes")
lines.append("")
# Group classes by category
ui_classes = []
collection_classes = []
system_classes = []
other_classes = []
for name, cls in classes:
if name in ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']:
ui_classes.append((name, cls))
elif 'Collection' in name:
collection_classes.append((name, cls))
elif name in ['Color', 'Vector', 'Texture', 'Font']:
system_classes.append((name, cls))
else:
other_classes.append((name, cls))
# UI Classes
if ui_classes:
lines.append("### UI Components")
lines.append("")
for name, cls in ui_classes:
lines.extend(generate_markdown_class(get_class_info(cls)))
# Collections
if collection_classes:
lines.append("### Collections")
lines.append("")
for name, cls in collection_classes:
lines.extend(generate_markdown_class(get_class_info(cls)))
# System Classes
if system_classes:
lines.append("### System Types")
lines.append("")
for name, cls in system_classes:
lines.extend(generate_markdown_class(get_class_info(cls)))
# Other Classes
if other_classes:
lines.append("### Other Classes")
lines.append("")
for name, cls in other_classes:
lines.extend(generate_markdown_class(get_class_info(cls)))
# Document functions
lines.append("## Functions")
lines.append("")
# Group functions by category
scene_funcs = []
audio_funcs = []
ui_funcs = []
system_funcs = []
for name, func in functions:
if 'scene' in name.lower() or name in ['createScene', 'setScene']:
scene_funcs.append((name, func))
elif any(x in name.lower() for x in ['sound', 'music', 'volume']):
audio_funcs.append((name, func))
elif name in ['find', 'findAll']:
ui_funcs.append((name, func))
else:
system_funcs.append((name, func))
# Scene Management
if scene_funcs:
lines.append("### Scene Management")
lines.append("")
for name, func in scene_funcs:
lines.extend(generate_markdown_function(get_function_info(func, name)))
# Audio
if audio_funcs:
lines.append("### Audio")
lines.append("")
for name, func in audio_funcs:
lines.extend(generate_markdown_function(get_function_info(func, name)))
# UI Utilities
if ui_funcs:
lines.append("### UI Utilities")
lines.append("")
for name, func in ui_funcs:
lines.extend(generate_markdown_function(get_function_info(func, name)))
# System
if system_funcs:
lines.append("### System")
lines.append("")
for name, func in system_funcs:
lines.extend(generate_markdown_function(get_function_info(func, name)))
# Automation module
if hasattr(mcrfpy, 'automation'):
lines.append("## Automation Module")
lines.append("")
lines.append("The `mcrfpy.automation` module provides testing and automation capabilities.")
lines.append("")
automation = mcrfpy.automation
auto_funcs = []
for name in sorted(dir(automation)):
if not name.startswith('_'):
obj = getattr(automation, name)
if callable(obj):
auto_funcs.append((name, obj))
for name, func in auto_funcs:
# Format as static method
func_info = get_function_info(func, name)
lines.append(f"### `automation.{func_info['signature']}`")
lines.append("")
if func_info['doc']:
lines.append(func_info['doc'])
lines.append("")
lines.append("---")
lines.append("")
return '\n'.join(lines)
def generate_html_docs(markdown_content: str) -> str:
"""Convert markdown to HTML."""
# Simple conversion - in production use a proper markdown parser
html = ['<!DOCTYPE html>']
html.append('<html><head>')
html.append('<meta charset="UTF-8">')
html.append('<title>McRogueFace API Reference</title>')
html.append('<style>')
html.append('''
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.6; color: #333; max-width: 900px; margin: 0 auto; padding: 20px; }
h1, h2, h3, h4, h5 { color: #2c3e50; margin-top: 24px; }
h1 { border-bottom: 2px solid #3498db; padding-bottom: 10px; }
h2 { border-bottom: 1px solid #ecf0f1; padding-bottom: 8px; }
code { background: #f4f4f4; padding: 2px 4px; border-radius: 3px; font-size: 90%; }
pre { background: #f4f4f4; padding: 12px; border-radius: 5px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { border-left: 4px solid #3498db; margin: 0; padding-left: 16px; color: #7f8c8d; }
hr { border: none; border-top: 1px solid #ecf0f1; margin: 24px 0; }
a { color: #3498db; text-decoration: none; }
a:hover { text-decoration: underline; }
.property { color: #27ae60; }
.method { color: #2980b9; }
.class-name { color: #8e44ad; font-weight: bold; }
ul { padding-left: 24px; }
li { margin: 4px 0; }
''')
html.append('</style>')
html.append('</head><body>')
# Very basic markdown to HTML conversion
lines = markdown_content.split('\n')
in_code_block = False
in_list = False
for line in lines:
stripped = line.strip()
if stripped.startswith('```'):
if in_code_block:
html.append('</code></pre>')
in_code_block = False
else:
lang = stripped[3:] or 'python'
html.append(f'<pre><code class="language-{lang}">')
in_code_block = True
continue
if in_code_block:
html.append(line)
continue
# Headers
if stripped.startswith('#'):
level = len(stripped.split()[0])
text = stripped[level:].strip()
html.append(f'<h{level}>{text}</h{level}>')
# Lists
elif stripped.startswith('- '):
if not in_list:
html.append('<ul>')
in_list = True
html.append(f'<li>{stripped[2:]}</li>')
# Horizontal rule
elif stripped == '---':
if in_list:
html.append('</ul>')
in_list = False
html.append('<hr>')
# Emphasis
elif stripped.startswith('*') and stripped.endswith('*') and len(stripped) > 2:
html.append(f'<em>{stripped[1:-1]}</em>')
# Bold
elif stripped.startswith('**') and stripped.endswith('**'):
html.append(f'<strong>{stripped[2:-2]}</strong>')
# Regular paragraph
elif stripped:
if in_list:
html.append('</ul>')
in_list = False
# Convert inline code
text = stripped
if '`' in text:
import re
text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
html.append(f'<p>{text}</p>')
else:
if in_list:
html.append('</ul>')
in_list = False
# Empty line
html.append('')
if in_list:
html.append('</ul>')
if in_code_block:
html.append('</code></pre>')
html.append('</body></html>')
return '\n'.join(html)
def main():
"""Generate API documentation in multiple formats."""
print("Generating McRogueFace API Documentation...")
# Create docs directory
docs_dir = Path("docs")
docs_dir.mkdir(exist_ok=True)
# Generate markdown documentation
print("- Generating Markdown documentation...")
markdown_content = generate_markdown_docs()
# Write markdown
md_path = docs_dir / "API_REFERENCE.md"
with open(md_path, 'w') as f:
f.write(markdown_content)
print(f" ✓ Written to {md_path}")
# Generate HTML
print("- Generating HTML documentation...")
html_content = generate_html_docs(markdown_content)
# Write HTML
html_path = docs_dir / "api_reference.html"
with open(html_path, 'w') as f:
f.write(html_content)
print(f" ✓ Written to {html_path}")
# Summary statistics
lines = markdown_content.split('\n')
class_count = markdown_content.count('### class')
func_count = len([l for l in lines if l.strip().startswith('### `') and 'class' not in l])
print("\nDocumentation Statistics:")
print(f"- Classes documented: {class_count}")
print(f"- Functions documented: {func_count}")
print(f"- Total lines: {len(lines)}")
print(f"- File size: {len(markdown_content):,} bytes")
print("\nAPI documentation generated successfully!")
if __name__ == '__main__':
main()

1602
generate_api_docs_html.py Normal file

File diff suppressed because it is too large Load Diff

119
generate_api_docs_simple.py Normal file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Generate API reference documentation for McRogueFace - Simple version."""
import os
import sys
import datetime
from pathlib import Path
import mcrfpy
def generate_markdown_docs():
"""Generate markdown API documentation."""
lines = []
# Header
lines.append("# McRogueFace API Reference")
lines.append("")
lines.append("*Generated on {}*".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
lines.append("")
# Module description
if mcrfpy.__doc__:
lines.append("## Overview")
lines.append("")
lines.extend(mcrfpy.__doc__.strip().split('\n'))
lines.append("")
# Collect all components
classes = []
functions = []
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))
# Document classes
lines.append("## Classes")
lines.append("")
for name, cls in classes:
lines.append("### class {}".format(name))
if cls.__doc__:
doc_lines = cls.__doc__.strip().split('\n')
for line in doc_lines[:5]: # First 5 lines
lines.append(line)
lines.append("")
lines.append("---")
lines.append("")
# Document functions
lines.append("## Functions")
lines.append("")
for name, func in functions:
lines.append("### {}".format(name))
if func.__doc__:
doc_lines = func.__doc__.strip().split('\n')
for line in doc_lines[:5]: # First 5 lines
lines.append(line)
lines.append("")
lines.append("---")
lines.append("")
# Automation module
if hasattr(mcrfpy, 'automation'):
lines.append("## Automation Module")
lines.append("")
automation = mcrfpy.automation
for name in sorted(dir(automation)):
if not name.startswith('_'):
obj = getattr(automation, name)
if callable(obj):
lines.append("### automation.{}".format(name))
if obj.__doc__:
lines.append(obj.__doc__.strip().split('\n')[0])
lines.append("")
return '\n'.join(lines)
def main():
"""Generate API documentation."""
print("Generating McRogueFace API Documentation...")
# Create docs directory
docs_dir = Path("docs")
docs_dir.mkdir(exist_ok=True)
# Generate markdown
markdown_content = generate_markdown_docs()
# Write markdown
md_path = docs_dir / "API_REFERENCE.md"
with open(md_path, 'w') as f:
f.write(markdown_content)
print("Written to {}".format(md_path))
# Summary
lines = markdown_content.split('\n')
class_count = markdown_content.count('### class')
func_count = markdown_content.count('### ') - class_count - markdown_content.count('### automation.')
print("\nDocumentation Statistics:")
print("- Classes documented: {}".format(class_count))
print("- Functions documented: {}".format(func_count))
print("- Total lines: {}".format(len(lines)))
print("\nAPI documentation generated successfully!")
sys.exit(0)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,960 @@
#!/usr/bin/env python3
"""Generate COMPLETE HTML API reference documentation for McRogueFace with NO missing methods."""
import os
import sys
import datetime
import html
from pathlib import Path
import mcrfpy
def escape_html(text: str) -> str:
"""Escape HTML special characters."""
return html.escape(text) if text else ""
def get_complete_method_documentation():
"""Return complete documentation for ALL methods across all classes."""
return {
# 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': 'For Caption and Sprite, this may not change actual size if determined by content.'
}
},
# Entity-specific methods
'Entity': {
'at': {
'signature': 'at(x, y)',
'description': 'Check if this entity is at the specified grid coordinates.',
'args': [
('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'
},
'die': {
'signature': 'die()',
'description': 'Remove this entity from its parent grid.',
'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 parent grid\'s entity list.',
'returns': 'int: Index position, or -1 if not in a grid'
}
},
# Grid-specific methods
'Grid': {
'at': {
'signature': 'at(x, y)',
'description': 'Get the GridPoint at the specified grid coordinates.',
'args': [
('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'
}
},
# Collection methods
'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')],
'raises': 'ValueError: If entity is not in collection'
},
'extend': {
'signature': 'extend(iterable)',
'description': 'Add all entities from an iterable to the collection.',
'args': [('iterable', 'Iterable[Entity]', 'Entities to add')]
},
'count': {
'signature': 'count(entity)',
'description': 'Count the number of occurrences of an entity in the collection.',
'args': [('entity', 'Entity', 'The entity to count')],
'returns': 'int: Number of times entity appears in collection'
},
'index': {
'signature': 'index(entity)',
'description': 'Find the index of the first occurrence of an entity.',
'args': [('entity', 'Entity', 'The entity to find')],
'returns': 'int: Index of entity in collection',
'raises': 'ValueError: If entity is not in collection'
}
},
'UICollection': {
'append': {
'signature': 'append(drawable)',
'description': 'Add a drawable element to the end of the collection.',
'args': [('drawable', 'UIDrawable', 'The drawable element to add')]
},
'remove': {
'signature': 'remove(drawable)',
'description': 'Remove the first occurrence of a drawable from the collection.',
'args': [('drawable', 'UIDrawable', 'The drawable to remove')],
'raises': 'ValueError: If drawable is not in collection'
},
'extend': {
'signature': 'extend(iterable)',
'description': 'Add all drawables from an iterable to the collection.',
'args': [('iterable', 'Iterable[UIDrawable]', 'Drawables to add')]
},
'count': {
'signature': 'count(drawable)',
'description': 'Count the number of occurrences of a drawable in the collection.',
'args': [('drawable', 'UIDrawable', 'The drawable to count')],
'returns': 'int: Number of times drawable appears in collection'
},
'index': {
'signature': 'index(drawable)',
'description': 'Find the index of the first occurrence of a drawable.',
'args': [('drawable', 'UIDrawable', 'The drawable to find')],
'returns': 'int: Index of drawable in collection',
'raises': 'ValueError: If drawable is not in collection'
}
},
# Animation methods
'Animation': {
'get_current_value': {
'signature': 'get_current_value()',
'description': 'Get the current interpolated value of the animation.',
'returns': 'float: Current animation value between start and end'
},
'start': {
'signature': 'start(target)',
'description': 'Start the animation on a target UI element.',
'args': [('target', 'UIDrawable', 'The UI element to animate')],
'note': 'The target must have the property specified in the animation constructor.'
},
'update': {
'signature': 'update(delta_time)',
'description': 'Update the animation by the given time delta.',
'args': [('delta_time', 'float', 'Time elapsed since last update in seconds')],
'returns': 'bool: True if animation is still running, False if finished'
}
},
# Color methods
'Color': {
'from_hex': {
'signature': 'from_hex(hex_string)',
'description': 'Create a Color from a hexadecimal color string.',
'args': [('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")'
},
'to_hex': {
'signature': 'to_hex()',
'description': 'Convert this Color to a hexadecimal string.',
'returns': 'str: Hex color string in format "#RRGGBB"',
'example': 'hex_str = color.to_hex() # Returns "#FF0000"'
},
'lerp': {
'signature': 'lerp(other, t)',
'description': 'Linearly interpolate between this color and another.',
'args': [
('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'
}
},
# Vector methods
'Vector': {
'magnitude': {
'signature': 'magnitude()',
'description': 'Calculate the length/magnitude of this vector.',
'returns': 'float: The magnitude of the vector',
'example': 'length = vector.magnitude()'
},
'magnitude_squared': {
'signature': 'magnitude_squared()',
'description': '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.'
},
'normalize': {
'signature': 'normalize()',
'description': 'Return a unit vector in the same direction.',
'returns': 'Vector: New normalized vector with magnitude 1.0',
'raises': 'ValueError: If vector has zero magnitude'
},
'dot': {
'signature': 'dot(other)',
'description': 'Calculate the dot product with another vector.',
'args': [('other', 'Vector', 'The other vector')],
'returns': 'float: Dot product of the two vectors'
},
'distance_to': {
'signature': 'distance_to(other)',
'description': 'Calculate the distance to another vector.',
'args': [('other', 'Vector', 'The other vector')],
'returns': 'float: Distance between the two vectors'
},
'angle': {
'signature': 'angle()',
'description': 'Get the angle of this vector in radians.',
'returns': 'float: Angle in radians from positive x-axis'
},
'copy': {
'signature': 'copy()',
'description': 'Create a copy of this vector.',
'returns': 'Vector: New Vector object with same x and y values'
}
},
# Scene methods
'Scene': {
'activate': {
'signature': 'activate()',
'description': 'Make this scene the active scene.',
'note': 'Equivalent to calling setScene() with this scene\'s name.'
},
'get_ui': {
'signature': 'get_ui()',
'description': 'Get the UI element collection for this scene.',
'returns': 'UICollection: Collection of all UI elements in this scene'
},
'keypress': {
'signature': 'keypress(handler)',
'description': 'Register a keyboard handler function for this scene.',
'args': [('handler', 'callable', 'Function that takes (key_name: str, is_pressed: bool)')],
'note': 'Alternative to overriding the on_keypress method.'
},
'register_keyboard': {
'signature': 'register_keyboard(callable)',
'description': 'Register a keyboard event handler function for the scene.',
'args': [('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)'''
}
},
# Timer methods
'Timer': {
'pause': {
'signature': 'pause()',
'description': 'Pause the timer, stopping its callback execution.',
'note': 'Use resume() to continue the timer from where it was paused.'
},
'resume': {
'signature': 'resume()',
'description': 'Resume a paused timer.',
'note': 'Has no effect if timer is not paused.'
},
'cancel': {
'signature': 'cancel()',
'description': 'Cancel the timer and remove it from the system.',
'note': 'After cancelling, the timer object cannot be reused.'
},
'restart': {
'signature': 'restart()',
'description': 'Restart the timer from the beginning.',
'note': 'Resets the timer\'s internal clock to zero.'
}
},
# Window methods
'Window': {
'get': {
'signature': 'get()',
'description': 'Get the Window singleton instance.',
'returns': 'Window: The singleton window object',
'note': 'This is a static method that returns the same instance every time.'
},
'center': {
'signature': 'center()',
'description': 'Center the window on the screen.',
'note': 'Only works if the window is not fullscreen.'
},
'screenshot': {
'signature': 'screenshot(filename)',
'description': 'Take a screenshot and save it to a file.',
'args': [('filename', 'str', 'Path where to save the screenshot')],
'note': 'Supports PNG, JPG, and BMP formats based on file extension.'
}
}
}
def get_complete_function_documentation():
"""Return complete documentation for ALL module functions."""
return {
# Scene Management
'createScene': {
'signature': 'createScene(name: str) -> None',
'description': 'Create a new empty scene with the given name.',
'args': [('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': {
'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None',
'description': 'Switch to a different scene with optional transition effect.',
'args': [
('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': {
'signature': 'currentScene() -> str',
'description': 'Get the name of the currently active scene.',
'returns': 'str: Name of the current scene',
'example': 'scene_name = mcrfpy.currentScene()'
},
'sceneUI': {
'signature': 'sceneUI(scene: str = None) -> UICollection',
'description': 'Get all UI elements for a scene.',
'args': [('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': {
'signature': 'keypressScene(handler: callable) -> None',
'description': 'Set the keyboard event handler for the current scene.',
'args': [('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 Functions
'createSoundBuffer': {
'signature': 'createSoundBuffer(filename: str) -> int',
'description': 'Load a sound effect from a file and return its buffer ID.',
'args': [('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': {
'signature': 'loadMusic(filename: str, loop: bool = True) -> None',
'description': 'Load and immediately play background music from a file.',
'args': [
('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': {
'signature': 'playSound(buffer_id: int) -> None',
'description': 'Play a sound effect using a previously loaded buffer.',
'args': [('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')],
'raises': 'RuntimeError: If the buffer ID is invalid',
'example': 'mcrfpy.playSound(jump_sound)'
},
'getMusicVolume': {
'signature': 'getMusicVolume() -> int',
'description': 'Get the current music volume level.',
'returns': 'int: Current volume (0-100)',
'example': 'current_volume = mcrfpy.getMusicVolume()'
},
'getSoundVolume': {
'signature': 'getSoundVolume() -> int',
'description': 'Get the current sound effects volume level.',
'returns': 'int: Current volume (0-100)',
'example': 'current_volume = mcrfpy.getSoundVolume()'
},
'setMusicVolume': {
'signature': 'setMusicVolume(volume: int) -> None',
'description': 'Set the global music volume.',
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
'example': 'mcrfpy.setMusicVolume(50) # Set to 50% volume'
},
'setSoundVolume': {
'signature': 'setSoundVolume(volume: int) -> None',
'description': 'Set the global sound effects volume.',
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
'example': 'mcrfpy.setSoundVolume(75) # Set to 75% volume'
},
# UI Utilities
'find': {
'signature': 'find(name: str, scene: str = None) -> UIDrawable | None',
'description': 'Find the first UI element with the specified name.',
'args': [
('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': {
'signature': 'findAll(pattern: str, scene: str = None) -> list',
'description': 'Find all UI elements matching a name pattern.',
'args': [
('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 Functions
'exit': {
'signature': 'exit() -> None',
'description': 'Cleanly shut down the game engine and exit the application.',
'note': 'This immediately closes the window and terminates the program.',
'example': 'mcrfpy.exit()'
},
'getMetrics': {
'signature': 'getMetrics() -> dict',
'description': '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': {
'signature': 'setTimer(name: str, handler: callable, interval: int) -> None',
'description': 'Create or update a recurring timer.',
'args': [
('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': {
'signature': 'delTimer(name: str) -> None',
'description': 'Stop and remove a timer.',
'args': [('name', 'str', 'Timer identifier to remove')],
'note': 'No error is raised if the timer doesn\'t exist.',
'example': 'mcrfpy.delTimer("score_update")'
},
'setScale': {
'signature': 'setScale(multiplier: float) -> None',
'description': 'Scale the game window size.',
'args': [('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'
}
}
def get_complete_property_documentation():
"""Return complete documentation for ALL properties."""
return {
'Animation': {
'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)'
},
'GridPoint': {
'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': {
'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'
}
}
def generate_complete_html_documentation():
"""Generate complete HTML documentation with NO missing methods."""
# Get all documentation data
method_docs = get_complete_method_documentation()
function_docs = get_complete_function_documentation()
property_docs = get_complete_property_documentation()
html_parts = []
# HTML header with enhanced styling
html_parts.append('''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>McRogueFace API Reference - Complete Documentation</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f8f9fa;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
border-bottom: 3px solid #3498db;
padding-bottom: 15px;
margin-bottom: 30px;
}
h2 {
color: #34495e;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
margin-top: 40px;
}
h3 {
color: #2c3e50;
margin-top: 30px;
}
h4 {
color: #34495e;
margin-top: 20px;
font-size: 1.1em;
}
h5 {
color: #555;
margin-top: 15px;
font-size: 1em;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
font-size: 0.9em;
}
pre {
background: #f8f8f8;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 16px;
overflow-x: auto;
margin: 15px 0;
}
pre code {
background: none;
padding: 0;
font-size: 0.875em;
line-height: 1.45;
}
.class-name {
color: #8e44ad;
font-weight: bold;
}
.property {
color: #27ae60;
font-weight: 600;
}
.method {
color: #2980b9;
font-weight: 600;
}
.function-signature {
color: #d73a49;
font-weight: 600;
}
.method-section {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #3498db;
}
.arg-list {
margin: 10px 0;
}
.arg-item {
margin: 8px 0;
padding: 8px;
background: #fff;
border-radius: 4px;
border: 1px solid #e1e4e8;
}
.arg-name {
color: #d73a49;
font-weight: 600;
}
.arg-type {
color: #6f42c1;
font-style: italic;
}
.returns {
background: #e8f5e8;
padding: 10px;
border-radius: 4px;
border-left: 4px solid #28a745;
margin: 10px 0;
}
.note {
background: #fff3cd;
padding: 10px;
border-radius: 4px;
border-left: 4px solid #ffc107;
margin: 10px 0;
}
.example {
background: #e7f3ff;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #0366d6;
margin: 15px 0;
}
.toc {
background: #f8f9fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 20px;
margin: 20px 0;
}
.toc ul {
list-style: none;
padding-left: 0;
}
.toc li {
margin: 8px 0;
}
.toc a {
color: #3498db;
text-decoration: none;
font-weight: 500;
}
.toc a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
''')
# Title and overview
html_parts.append('<h1>McRogueFace API Reference - Complete Documentation</h1>')
html_parts.append(f'<p><em>Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</em></p>')
# Table of contents
html_parts.append('<div class="toc">')
html_parts.append('<h2>Table of Contents</h2>')
html_parts.append('<ul>')
html_parts.append('<li><a href="#functions">Functions</a></li>')
html_parts.append('<li><a href="#classes">Classes</a></li>')
html_parts.append('<li><a href="#automation">Automation Module</a></li>')
html_parts.append('</ul>')
html_parts.append('</div>')
# Functions section
html_parts.append('<h2 id="functions">Functions</h2>')
# Group functions by category
categories = {
'Scene Management': ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'],
'Audio': ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'],
'UI Utilities': ['find', 'findAll'],
'System': ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale']
}
for category, functions in categories.items():
html_parts.append(f'<h3>{category}</h3>')
for func_name in functions:
if func_name in function_docs:
html_parts.append(format_function_html(func_name, function_docs[func_name]))
# Classes section
html_parts.append('<h2 id="classes">Classes</h2>')
# Get all classes from mcrfpy
classes = []
for name in sorted(dir(mcrfpy)):
if not name.startswith('_'):
obj = getattr(mcrfpy, name)
if isinstance(obj, type):
classes.append((name, obj))
# Generate class documentation
for class_name, cls in classes:
html_parts.append(format_class_html_complete(class_name, cls, method_docs, property_docs))
# Automation section
if hasattr(mcrfpy, 'automation'):
html_parts.append('<h2 id="automation">Automation Module</h2>')
html_parts.append('<p>The <code>mcrfpy.automation</code> module provides testing and automation capabilities.</p>')
automation = mcrfpy.automation
for name in sorted(dir(automation)):
if not name.startswith('_'):
obj = getattr(automation, name)
if callable(obj):
html_parts.append(f'<div class="method-section">')
html_parts.append(f'<h4><code class="function-signature">automation.{name}</code></h4>')
if obj.__doc__:
doc_parts = obj.__doc__.split(' - ')
if len(doc_parts) > 1:
html_parts.append(f'<p>{escape_html(doc_parts[1])}</p>')
else:
html_parts.append(f'<p>{escape_html(obj.__doc__)}</p>')
html_parts.append('</div>')
html_parts.append('</div>')
html_parts.append('</body>')
html_parts.append('</html>')
return '\n'.join(html_parts)
def format_function_html(func_name, func_doc):
"""Format a function with complete documentation."""
html_parts = []
html_parts.append('<div class="method-section">')
html_parts.append(f'<h4><code class="function-signature">{func_doc["signature"]}</code></h4>')
html_parts.append(f'<p>{escape_html(func_doc["description"])}</p>')
# Arguments
if 'args' in func_doc:
html_parts.append('<div class="arg-list">')
html_parts.append('<h5>Arguments:</h5>')
for arg in func_doc['args']:
html_parts.append('<div class="arg-item">')
html_parts.append(f'<span class="arg-name">{arg[0]}</span> ')
html_parts.append(f'<span class="arg-type">({arg[1]})</span>: ')
html_parts.append(f'{escape_html(arg[2])}')
html_parts.append('</div>')
html_parts.append('</div>')
# Returns
if 'returns' in func_doc:
html_parts.append('<div class="returns">')
html_parts.append(f'<strong>Returns:</strong> {escape_html(func_doc["returns"])}')
html_parts.append('</div>')
# Raises
if 'raises' in func_doc:
html_parts.append('<div class="note">')
html_parts.append(f'<strong>Raises:</strong> {escape_html(func_doc["raises"])}')
html_parts.append('</div>')
# Note
if 'note' in func_doc:
html_parts.append('<div class="note">')
html_parts.append(f'<strong>Note:</strong> {escape_html(func_doc["note"])}')
html_parts.append('</div>')
# Example
if 'example' in func_doc:
html_parts.append('<div class="example">')
html_parts.append('<h5>Example:</h5>')
html_parts.append('<pre><code>')
html_parts.append(escape_html(func_doc['example']))
html_parts.append('</code></pre>')
html_parts.append('</div>')
html_parts.append('</div>')
return '\n'.join(html_parts)
def format_class_html_complete(class_name, cls, method_docs, property_docs):
"""Format a class with complete documentation."""
html_parts = []
html_parts.append('<div class="method-section">')
html_parts.append(f'<h3><span class="class-name">{class_name}</span></h3>')
# Class description
if cls.__doc__:
html_parts.append(f'<p>{escape_html(cls.__doc__)}</p>')
# Properties
if class_name in property_docs:
html_parts.append('<h4>Properties:</h4>')
for prop_name, prop_desc in property_docs[class_name].items():
html_parts.append(f'<div class="arg-item">')
html_parts.append(f'<span class="property">{prop_name}</span>: {escape_html(prop_desc)}')
html_parts.append('</div>')
# Methods
methods_to_document = []
# Add inherited methods for UI classes
if any(base.__name__ == 'Drawable' for base in cls.__bases__ if hasattr(base, '__name__')):
methods_to_document.extend(['get_bounds', 'move', 'resize'])
# Add class-specific methods
if class_name in method_docs:
methods_to_document.extend(method_docs[class_name].keys())
# Add methods from introspection
for attr_name in dir(cls):
if not attr_name.startswith('_') and callable(getattr(cls, attr_name)):
if attr_name not in methods_to_document:
methods_to_document.append(attr_name)
if methods_to_document:
html_parts.append('<h4>Methods:</h4>')
for method_name in set(methods_to_document):
# Get method documentation
method_doc = None
if class_name in method_docs and method_name in method_docs[class_name]:
method_doc = method_docs[class_name][method_name]
elif method_name in method_docs.get('Drawable', {}):
method_doc = method_docs['Drawable'][method_name]
if method_doc:
html_parts.append(format_method_html(method_name, method_doc))
else:
# Basic method with no documentation
html_parts.append(f'<div class="arg-item">')
html_parts.append(f'<span class="method">{method_name}(...)</span>')
html_parts.append('</div>')
html_parts.append('</div>')
return '\n'.join(html_parts)
def format_method_html(method_name, method_doc):
"""Format a method with complete documentation."""
html_parts = []
html_parts.append('<div style="margin-left: 20px; margin-bottom: 15px;">')
html_parts.append(f'<h5><code class="method">{method_doc["signature"]}</code></h5>')
html_parts.append(f'<p>{escape_html(method_doc["description"])}</p>')
# Arguments
if 'args' in method_doc:
for arg in method_doc['args']:
html_parts.append(f'<div style="margin-left: 20px;">')
html_parts.append(f'<span class="arg-name">{arg[0]}</span> ')
html_parts.append(f'<span class="arg-type">({arg[1]})</span>: ')
html_parts.append(f'{escape_html(arg[2])}')
html_parts.append('</div>')
# Returns
if 'returns' in method_doc:
html_parts.append(f'<div style="margin-left: 20px; color: #28a745;">')
html_parts.append(f'<strong>Returns:</strong> {escape_html(method_doc["returns"])}')
html_parts.append('</div>')
# Note
if 'note' in method_doc:
html_parts.append(f'<div style="margin-left: 20px; color: #856404;">')
html_parts.append(f'<strong>Note:</strong> {escape_html(method_doc["note"])}')
html_parts.append('</div>')
# Example
if 'example' in method_doc:
html_parts.append(f'<div style="margin-left: 20px;">')
html_parts.append('<strong>Example:</strong>')
html_parts.append('<pre><code>')
html_parts.append(escape_html(method_doc['example']))
html_parts.append('</code></pre>')
html_parts.append('</div>')
html_parts.append('</div>')
return '\n'.join(html_parts)
def main():
"""Generate complete HTML documentation with zero missing methods."""
print("Generating COMPLETE HTML API documentation...")
# Generate HTML
html_content = generate_complete_html_documentation()
# Write to file
output_path = Path("docs/api_reference_complete.html")
output_path.parent.mkdir(exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✓ Generated {output_path}")
print(f" File size: {len(html_content):,} bytes")
# Count "..." instances
ellipsis_count = html_content.count('...')
print(f" Ellipsis instances: {ellipsis_count}")
if ellipsis_count == 0:
print("✅ SUCCESS: No missing documentation found!")
else:
print(f"❌ WARNING: {ellipsis_count} methods still need documentation")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,821 @@
#!/usr/bin/env python3
"""Generate COMPLETE Markdown API reference documentation for McRogueFace with NO missing methods."""
import os
import sys
import datetime
from pathlib import Path
import mcrfpy
def get_complete_method_documentation():
"""Return complete documentation for ALL methods across all classes."""
return {
# 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': 'For Caption and Sprite, this may not change actual size if determined by content.'
}
},
# Entity-specific methods
'Entity': {
'at': {
'signature': 'at(x, y)',
'description': 'Check if this entity is at the specified grid coordinates.',
'args': [
('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'
},
'die': {
'signature': 'die()',
'description': 'Remove this entity from its parent grid.',
'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 parent grid\'s entity list.',
'returns': 'int: Index position, or -1 if not in a grid'
}
},
# Grid-specific methods
'Grid': {
'at': {
'signature': 'at(x, y)',
'description': 'Get the GridPoint at the specified grid coordinates.',
'args': [
('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'
}
},
# Collection methods
'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')],
'raises': 'ValueError: If entity is not in collection'
},
'extend': {
'signature': 'extend(iterable)',
'description': 'Add all entities from an iterable to the collection.',
'args': [('iterable', 'Iterable[Entity]', 'Entities to add')]
},
'count': {
'signature': 'count(entity)',
'description': 'Count the number of occurrences of an entity in the collection.',
'args': [('entity', 'Entity', 'The entity to count')],
'returns': 'int: Number of times entity appears in collection'
},
'index': {
'signature': 'index(entity)',
'description': 'Find the index of the first occurrence of an entity.',
'args': [('entity', 'Entity', 'The entity to find')],
'returns': 'int: Index of entity in collection',
'raises': 'ValueError: If entity is not in collection'
}
},
'UICollection': {
'append': {
'signature': 'append(drawable)',
'description': 'Add a drawable element to the end of the collection.',
'args': [('drawable', 'UIDrawable', 'The drawable element to add')]
},
'remove': {
'signature': 'remove(drawable)',
'description': 'Remove the first occurrence of a drawable from the collection.',
'args': [('drawable', 'UIDrawable', 'The drawable to remove')],
'raises': 'ValueError: If drawable is not in collection'
},
'extend': {
'signature': 'extend(iterable)',
'description': 'Add all drawables from an iterable to the collection.',
'args': [('iterable', 'Iterable[UIDrawable]', 'Drawables to add')]
},
'count': {
'signature': 'count(drawable)',
'description': 'Count the number of occurrences of a drawable in the collection.',
'args': [('drawable', 'UIDrawable', 'The drawable to count')],
'returns': 'int: Number of times drawable appears in collection'
},
'index': {
'signature': 'index(drawable)',
'description': 'Find the index of the first occurrence of a drawable.',
'args': [('drawable', 'UIDrawable', 'The drawable to find')],
'returns': 'int: Index of drawable in collection',
'raises': 'ValueError: If drawable is not in collection'
}
},
# Animation methods
'Animation': {
'get_current_value': {
'signature': 'get_current_value()',
'description': 'Get the current interpolated value of the animation.',
'returns': 'float: Current animation value between start and end'
},
'start': {
'signature': 'start(target)',
'description': 'Start the animation on a target UI element.',
'args': [('target', 'UIDrawable', 'The UI element to animate')],
'note': 'The target must have the property specified in the animation constructor.'
},
'update': {
'signature': 'update(delta_time)',
'description': 'Update the animation by the given time delta.',
'args': [('delta_time', 'float', 'Time elapsed since last update in seconds')],
'returns': 'bool: True if animation is still running, False if finished'
}
},
# Color methods
'Color': {
'from_hex': {
'signature': 'from_hex(hex_string)',
'description': 'Create a Color from a hexadecimal color string.',
'args': [('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")'
},
'to_hex': {
'signature': 'to_hex()',
'description': 'Convert this Color to a hexadecimal string.',
'returns': 'str: Hex color string in format "#RRGGBB"',
'example': 'hex_str = color.to_hex() # Returns "#FF0000"'
},
'lerp': {
'signature': 'lerp(other, t)',
'description': 'Linearly interpolate between this color and another.',
'args': [
('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'
}
},
# Vector methods
'Vector': {
'magnitude': {
'signature': 'magnitude()',
'description': 'Calculate the length/magnitude of this vector.',
'returns': 'float: The magnitude of the vector'
},
'magnitude_squared': {
'signature': 'magnitude_squared()',
'description': '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.'
},
'normalize': {
'signature': 'normalize()',
'description': 'Return a unit vector in the same direction.',
'returns': 'Vector: New normalized vector with magnitude 1.0',
'raises': 'ValueError: If vector has zero magnitude'
},
'dot': {
'signature': 'dot(other)',
'description': 'Calculate the dot product with another vector.',
'args': [('other', 'Vector', 'The other vector')],
'returns': 'float: Dot product of the two vectors'
},
'distance_to': {
'signature': 'distance_to(other)',
'description': 'Calculate the distance to another vector.',
'args': [('other', 'Vector', 'The other vector')],
'returns': 'float: Distance between the two vectors'
},
'angle': {
'signature': 'angle()',
'description': 'Get the angle of this vector in radians.',
'returns': 'float: Angle in radians from positive x-axis'
},
'copy': {
'signature': 'copy()',
'description': 'Create a copy of this vector.',
'returns': 'Vector: New Vector object with same x and y values'
}
},
# Scene methods
'Scene': {
'activate': {
'signature': 'activate()',
'description': 'Make this scene the active scene.',
'note': 'Equivalent to calling setScene() with this scene\'s name.'
},
'get_ui': {
'signature': 'get_ui()',
'description': 'Get the UI element collection for this scene.',
'returns': 'UICollection: Collection of all UI elements in this scene'
},
'keypress': {
'signature': 'keypress(handler)',
'description': 'Register a keyboard handler function for this scene.',
'args': [('handler', 'callable', 'Function that takes (key_name: str, is_pressed: bool)')],
'note': 'Alternative to overriding the on_keypress method.'
},
'register_keyboard': {
'signature': 'register_keyboard(callable)',
'description': 'Register a keyboard event handler function for the scene.',
'args': [('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}")
scene.register_keyboard(handle_keyboard)'''
}
},
# Timer methods
'Timer': {
'pause': {
'signature': 'pause()',
'description': 'Pause the timer, stopping its callback execution.',
'note': 'Use resume() to continue the timer from where it was paused.'
},
'resume': {
'signature': 'resume()',
'description': 'Resume a paused timer.',
'note': 'Has no effect if timer is not paused.'
},
'cancel': {
'signature': 'cancel()',
'description': 'Cancel the timer and remove it from the system.',
'note': 'After cancelling, the timer object cannot be reused.'
},
'restart': {
'signature': 'restart()',
'description': 'Restart the timer from the beginning.',
'note': 'Resets the timer\'s internal clock to zero.'
}
},
# Window methods
'Window': {
'get': {
'signature': 'get()',
'description': 'Get the Window singleton instance.',
'returns': 'Window: The singleton window object',
'note': 'This is a static method that returns the same instance every time.'
},
'center': {
'signature': 'center()',
'description': 'Center the window on the screen.',
'note': 'Only works if the window is not fullscreen.'
},
'screenshot': {
'signature': 'screenshot(filename)',
'description': 'Take a screenshot and save it to a file.',
'args': [('filename', 'str', 'Path where to save the screenshot')],
'note': 'Supports PNG, JPG, and BMP formats based on file extension.'
}
}
}
def get_complete_function_documentation():
"""Return complete documentation for ALL module functions."""
return {
# Scene Management
'createScene': {
'signature': 'createScene(name: str) -> None',
'description': 'Create a new empty scene with the given name.',
'args': [('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': {
'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None',
'description': 'Switch to a different scene with optional transition effect.',
'args': [
('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': {
'signature': 'currentScene() -> str',
'description': 'Get the name of the currently active scene.',
'returns': 'str: Name of the current scene',
'example': 'scene_name = mcrfpy.currentScene()'
},
'sceneUI': {
'signature': 'sceneUI(scene: str = None) -> UICollection',
'description': 'Get all UI elements for a scene.',
'args': [('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': {
'signature': 'keypressScene(handler: callable) -> None',
'description': 'Set the keyboard event handler for the current scene.',
'args': [('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 Functions
'createSoundBuffer': {
'signature': 'createSoundBuffer(filename: str) -> int',
'description': 'Load a sound effect from a file and return its buffer ID.',
'args': [('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': {
'signature': 'loadMusic(filename: str, loop: bool = True) -> None',
'description': 'Load and immediately play background music from a file.',
'args': [
('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': {
'signature': 'playSound(buffer_id: int) -> None',
'description': 'Play a sound effect using a previously loaded buffer.',
'args': [('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')],
'raises': 'RuntimeError: If the buffer ID is invalid',
'example': 'mcrfpy.playSound(jump_sound)'
},
'getMusicVolume': {
'signature': 'getMusicVolume() -> int',
'description': 'Get the current music volume level.',
'returns': 'int: Current volume (0-100)',
'example': 'current_volume = mcrfpy.getMusicVolume()'
},
'getSoundVolume': {
'signature': 'getSoundVolume() -> int',
'description': 'Get the current sound effects volume level.',
'returns': 'int: Current volume (0-100)',
'example': 'current_volume = mcrfpy.getSoundVolume()'
},
'setMusicVolume': {
'signature': 'setMusicVolume(volume: int) -> None',
'description': 'Set the global music volume.',
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
'example': 'mcrfpy.setMusicVolume(50) # Set to 50% volume'
},
'setSoundVolume': {
'signature': 'setSoundVolume(volume: int) -> None',
'description': 'Set the global sound effects volume.',
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
'example': 'mcrfpy.setSoundVolume(75) # Set to 75% volume'
},
# UI Utilities
'find': {
'signature': 'find(name: str, scene: str = None) -> UIDrawable | None',
'description': 'Find the first UI element with the specified name.',
'args': [
('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': {
'signature': 'findAll(pattern: str, scene: str = None) -> list',
'description': 'Find all UI elements matching a name pattern.',
'args': [
('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 Functions
'exit': {
'signature': 'exit() -> None',
'description': 'Cleanly shut down the game engine and exit the application.',
'note': 'This immediately closes the window and terminates the program.',
'example': 'mcrfpy.exit()'
},
'getMetrics': {
'signature': 'getMetrics() -> dict',
'description': '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': {
'signature': 'setTimer(name: str, handler: callable, interval: int) -> None',
'description': 'Create or update a recurring timer.',
'args': [
('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': {
'signature': 'delTimer(name: str) -> None',
'description': 'Stop and remove a timer.',
'args': [('name', 'str', 'Timer identifier to remove')],
'note': 'No error is raised if the timer doesn\'t exist.',
'example': 'mcrfpy.delTimer("score_update")'
},
'setScale': {
'signature': 'setScale(multiplier: float) -> None',
'description': 'Scale the game window size.',
'args': [('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'
}
}
def get_complete_property_documentation():
"""Return complete documentation for ALL properties."""
return {
'Animation': {
'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)'
},
'GridPoint': {
'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': {
'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'
}
}
def format_method_markdown(method_name, method_doc):
"""Format a method as markdown."""
lines = []
lines.append(f"#### `{method_doc['signature']}`")
lines.append("")
lines.append(method_doc['description'])
lines.append("")
# Arguments
if 'args' in method_doc:
lines.append("**Arguments:**")
for arg in method_doc['args']:
lines.append(f"- `{arg[0]}` (*{arg[1]}*): {arg[2]}")
lines.append("")
# Returns
if 'returns' in method_doc:
lines.append(f"**Returns:** {method_doc['returns']}")
lines.append("")
# Raises
if 'raises' in method_doc:
lines.append(f"**Raises:** {method_doc['raises']}")
lines.append("")
# Note
if 'note' in method_doc:
lines.append(f"**Note:** {method_doc['note']}")
lines.append("")
# Example
if 'example' in method_doc:
lines.append("**Example:**")
lines.append("```python")
lines.append(method_doc['example'])
lines.append("```")
lines.append("")
return lines
def format_function_markdown(func_name, func_doc):
"""Format a function as markdown."""
lines = []
lines.append(f"### `{func_doc['signature']}`")
lines.append("")
lines.append(func_doc['description'])
lines.append("")
# Arguments
if 'args' in func_doc:
lines.append("**Arguments:**")
for arg in func_doc['args']:
lines.append(f"- `{arg[0]}` (*{arg[1]}*): {arg[2]}")
lines.append("")
# Returns
if 'returns' in func_doc:
lines.append(f"**Returns:** {func_doc['returns']}")
lines.append("")
# Raises
if 'raises' in func_doc:
lines.append(f"**Raises:** {func_doc['raises']}")
lines.append("")
# Note
if 'note' in func_doc:
lines.append(f"**Note:** {func_doc['note']}")
lines.append("")
# Example
if 'example' in func_doc:
lines.append("**Example:**")
lines.append("```python")
lines.append(func_doc['example'])
lines.append("```")
lines.append("")
lines.append("---")
lines.append("")
return lines
def generate_complete_markdown_documentation():
"""Generate complete markdown documentation with NO missing methods."""
# Get all documentation data
method_docs = get_complete_method_documentation()
function_docs = get_complete_function_documentation()
property_docs = get_complete_property_documentation()
lines = []
# Header
lines.append("# McRogueFace API Reference")
lines.append("")
lines.append(f"*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
lines.append("")
# Overview
if mcrfpy.__doc__:
lines.append("## Overview")
lines.append("")
# Process the docstring properly
doc_text = mcrfpy.__doc__.replace('\\n', '\n')
lines.append(doc_text)
lines.append("")
# Table of Contents
lines.append("## Table of Contents")
lines.append("")
lines.append("- [Functions](#functions)")
lines.append(" - [Scene Management](#scene-management)")
lines.append(" - [Audio](#audio)")
lines.append(" - [UI Utilities](#ui-utilities)")
lines.append(" - [System](#system)")
lines.append("- [Classes](#classes)")
lines.append(" - [UI Components](#ui-components)")
lines.append(" - [Collections](#collections)")
lines.append(" - [System Types](#system-types)")
lines.append(" - [Other Classes](#other-classes)")
lines.append("- [Automation Module](#automation-module)")
lines.append("")
# Functions section
lines.append("## Functions")
lines.append("")
# Group functions by category
categories = {
'Scene Management': ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'],
'Audio': ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'],
'UI Utilities': ['find', 'findAll'],
'System': ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale']
}
for category, functions in categories.items():
lines.append(f"### {category}")
lines.append("")
for func_name in functions:
if func_name in function_docs:
lines.extend(format_function_markdown(func_name, function_docs[func_name]))
# Classes section
lines.append("## Classes")
lines.append("")
# Get all classes from mcrfpy
classes = []
for name in sorted(dir(mcrfpy)):
if not name.startswith('_'):
obj = getattr(mcrfpy, name)
if isinstance(obj, type):
classes.append((name, obj))
# Group classes
ui_classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']
collection_classes = ['EntityCollection', 'UICollection', 'UICollectionIter', 'UIEntityCollectionIter']
system_classes = ['Color', 'Vector', 'Texture', 'Font']
other_classes = [name for name, _ in classes if name not in ui_classes + collection_classes + system_classes]
# UI Components
lines.append("### UI Components")
lines.append("")
for class_name in ui_classes:
if any(name == class_name for name, _ in classes):
lines.extend(format_class_markdown(class_name, method_docs, property_docs))
# Collections
lines.append("### Collections")
lines.append("")
for class_name in collection_classes:
if any(name == class_name for name, _ in classes):
lines.extend(format_class_markdown(class_name, method_docs, property_docs))
# System Types
lines.append("### System Types")
lines.append("")
for class_name in system_classes:
if any(name == class_name for name, _ in classes):
lines.extend(format_class_markdown(class_name, method_docs, property_docs))
# Other Classes
lines.append("### Other Classes")
lines.append("")
for class_name in other_classes:
lines.extend(format_class_markdown(class_name, method_docs, property_docs))
# Automation section
if hasattr(mcrfpy, 'automation'):
lines.append("## Automation Module")
lines.append("")
lines.append("The `mcrfpy.automation` module provides testing and automation capabilities.")
lines.append("")
automation = mcrfpy.automation
for name in sorted(dir(automation)):
if not name.startswith('_'):
obj = getattr(automation, name)
if callable(obj):
lines.append(f"### `automation.{name}`")
lines.append("")
if obj.__doc__:
doc_parts = obj.__doc__.split(' - ')
if len(doc_parts) > 1:
lines.append(doc_parts[1])
else:
lines.append(obj.__doc__)
lines.append("")
lines.append("---")
lines.append("")
return '\n'.join(lines)
def format_class_markdown(class_name, method_docs, property_docs):
"""Format a class as markdown."""
lines = []
lines.append(f"### class `{class_name}`")
lines.append("")
# Class description from known info
class_descriptions = {
'Frame': 'A rectangular frame UI element that can contain other drawable elements.',
'Caption': 'A text display UI element with customizable font and styling.',
'Sprite': 'A sprite UI element that displays a texture or portion of a texture atlas.',
'Grid': 'A grid-based tilemap UI element for rendering tile-based levels and game worlds.',
'Entity': 'Game entity that can be placed in a Grid.',
'EntityCollection': 'Container for Entity objects in a Grid. Supports iteration and indexing.',
'UICollection': 'Container for UI drawable elements. Supports iteration and indexing.',
'UICollectionIter': 'Iterator for UICollection. Automatically created when iterating over a UICollection.',
'UIEntityCollectionIter': 'Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.',
'Color': 'RGBA color representation.',
'Vector': '2D vector for positions and directions.',
'Font': 'Font object for text rendering.',
'Texture': 'Texture object for image data.',
'Animation': 'Animate UI element properties over time.',
'GridPoint': 'Represents a single tile in a Grid.',
'GridPointState': 'State information for a GridPoint.',
'Scene': 'Base class for object-oriented scenes.',
'Timer': 'Timer object for scheduled callbacks.',
'Window': 'Window singleton for accessing and modifying the game window properties.',
'Drawable': 'Base class for all drawable UI elements.'
}
if class_name in class_descriptions:
lines.append(class_descriptions[class_name])
lines.append("")
# Properties
if class_name in property_docs:
lines.append("#### Properties")
lines.append("")
for prop_name, prop_desc in property_docs[class_name].items():
lines.append(f"- **`{prop_name}`**: {prop_desc}")
lines.append("")
# Methods
methods_to_document = []
# Add inherited methods for UI classes
if class_name in ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']:
methods_to_document.extend(['get_bounds', 'move', 'resize'])
# Add class-specific methods
if class_name in method_docs:
methods_to_document.extend(method_docs[class_name].keys())
if methods_to_document:
lines.append("#### Methods")
lines.append("")
for method_name in set(methods_to_document):
# Get method documentation
method_doc = None
if class_name in method_docs and method_name in method_docs[class_name]:
method_doc = method_docs[class_name][method_name]
elif method_name in method_docs.get('Drawable', {}):
method_doc = method_docs['Drawable'][method_name]
if method_doc:
lines.extend(format_method_markdown(method_name, method_doc))
lines.append("---")
lines.append("")
return lines
def main():
"""Generate complete markdown documentation with zero missing methods."""
print("Generating COMPLETE Markdown API documentation...")
# Generate markdown
markdown_content = generate_complete_markdown_documentation()
# Write to file
output_path = Path("docs/API_REFERENCE_COMPLETE.md")
output_path.parent.mkdir(exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(markdown_content)
print(f"✓ Generated {output_path}")
print(f" File size: {len(markdown_content):,} bytes")
# Count "..." instances
ellipsis_count = markdown_content.count('...')
print(f" Ellipsis instances: {ellipsis_count}")
if ellipsis_count == 0:
print("✅ SUCCESS: No missing documentation found!")
else:
print(f"❌ WARNING: {ellipsis_count} methods still need documentation")
if __name__ == '__main__':
main()

574
generate_stubs_v2.py Normal file
View File

@ -0,0 +1,574 @@
#!/usr/bin/env python3
"""Generate .pyi type stub files for McRogueFace Python API - Version 2.
This script creates properly formatted type stubs by manually defining
the API based on the documentation we've created.
"""
import os
import mcrfpy
def generate_mcrfpy_stub():
"""Generate the main mcrfpy.pyi stub file."""
return '''"""Type stubs for McRogueFace Python API.
Core game engine interface for creating roguelike games with Python.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
# Type aliases
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
Transition = Union[str, None]
# Classes
class Color:
"""SFML Color Object for RGBA colors."""
r: int
g: int
b: int
a: int
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
def from_hex(self, hex_string: str) -> 'Color':
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
...
def to_hex(self) -> str:
"""Convert color to hex string format."""
...
def lerp(self, other: 'Color', t: float) -> 'Color':
"""Linear interpolation between two colors."""
...
class Vector:
"""SFML Vector Object for 2D coordinates."""
x: float
y: float
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float, y: float) -> None: ...
def add(self, other: 'Vector') -> 'Vector': ...
def subtract(self, other: 'Vector') -> 'Vector': ...
def multiply(self, scalar: float) -> 'Vector': ...
def divide(self, scalar: float) -> 'Vector': ...
def distance(self, other: 'Vector') -> float: ...
def normalize(self) -> 'Vector': ...
def dot(self, other: 'Vector') -> float: ...
class Texture:
"""SFML Texture Object for images."""
def __init__(self, filename: str) -> None: ...
filename: str
width: int
height: int
sprite_count: int
class Font:
"""SFML Font Object for text rendering."""
def __init__(self, filename: str) -> None: ...
filename: str
family: str
class Drawable:
"""Base class for all drawable UI elements."""
x: float
y: float
visible: bool
z_index: int
name: str
pos: Vector
def get_bounds(self) -> Tuple[float, float, float, float]:
"""Get bounding box as (x, y, width, height)."""
...
def move(self, dx: float, dy: float) -> None:
"""Move by relative offset (dx, dy)."""
...
def resize(self, width: float, height: float) -> None:
"""Resize to new dimensions (width, height)."""
...
class Frame(Drawable):
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)
A rectangular frame UI element that can contain other drawable elements.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
outline: float = 0, click: Optional[Callable] = None,
children: Optional[List[UIElement]] = None) -> None: ...
w: float
h: float
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
children: 'UICollection'
clip_children: bool
class Caption(Drawable):
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)
A text display UI element with customizable font and styling.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, text: str = '', x: float = 0, y: float = 0,
font: Optional[Font] = None, fill_color: Optional[Color] = None,
outline_color: Optional[Color] = None, outline: float = 0,
click: Optional[Callable] = None) -> None: ...
text: str
font: Font
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from text
h: float # Read-only, computed from text
class Sprite(Drawable):
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)
A sprite UI element that displays a texture or portion of a texture atlas.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, scale: float = 1.0,
click: Optional[Callable] = None) -> None: ...
texture: Texture
sprite_index: int
scale: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from texture
h: float # Read-only, computed from texture
class Grid(Drawable):
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)
A grid-based tilemap UI element for rendering tile-based levels and game worlds.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
scale: float = 1.0, click: Optional[Callable] = None) -> None: ...
grid_size: Tuple[int, int]
tile_width: int
tile_height: int
texture: Texture
scale: float
points: List[List['GridPoint']]
entities: 'EntityCollection'
background_color: Color
click: Optional[Callable[[int, int, int], None]]
def at(self, x: int, y: int) -> 'GridPoint':
"""Get grid point at tile coordinates."""
...
class GridPoint:
"""Grid point representing a single tile."""
texture_index: int
solid: bool
color: Color
class GridPointState:
"""State information for a grid point."""
texture_index: int
color: Color
class Entity(Drawable):
"""Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='')
Game entity that lives within a Grid.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, name: str = '') -> None: ...
grid_x: float
grid_y: float
texture: Texture
sprite_index: int
grid: Optional[Grid]
def at(self, grid_x: float, grid_y: float) -> None:
"""Move entity to grid position."""
...
def die(self) -> None:
"""Remove entity from its grid."""
...
def index(self) -> int:
"""Get index in parent grid's entity collection."""
...
class UICollection:
"""Collection of UI drawable elements (Frame, Caption, Sprite, Grid)."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> UIElement: ...
def __setitem__(self, index: int, value: UIElement) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: UIElement) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'UICollection') -> 'UICollection': ...
def __iadd__(self, other: 'UICollection') -> 'UICollection': ...
def append(self, item: UIElement) -> None: ...
def extend(self, items: List[UIElement]) -> None: ...
def remove(self, item: UIElement) -> None: ...
def index(self, item: UIElement) -> int: ...
def count(self, item: UIElement) -> int: ...
class EntityCollection:
"""Collection of Entity objects."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> Entity: ...
def __setitem__(self, index: int, value: Entity) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: Entity) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def append(self, item: Entity) -> None: ...
def extend(self, items: List[Entity]) -> None: ...
def remove(self, item: Entity) -> None: ...
def index(self, item: Entity) -> int: ...
def count(self, item: Entity) -> int: ...
class Scene:
"""Base class for object-oriented scenes."""
name: str
def __init__(self, name: str) -> None: ...
def activate(self) -> None:
"""Called when scene becomes active."""
...
def deactivate(self) -> None:
"""Called when scene becomes inactive."""
...
def get_ui(self) -> UICollection:
"""Get UI elements collection."""
...
def on_keypress(self, key: str, pressed: bool) -> None:
"""Handle keyboard events."""
...
def on_click(self, x: float, y: float, button: int) -> None:
"""Handle mouse clicks."""
...
def on_enter(self) -> None:
"""Called when entering the scene."""
...
def on_exit(self) -> None:
"""Called when leaving the scene."""
...
def on_resize(self, width: int, height: int) -> None:
"""Handle window resize events."""
...
def update(self, dt: float) -> None:
"""Update scene logic."""
...
class Timer:
"""Timer object for scheduled callbacks."""
name: str
interval: int
active: bool
def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ...
def pause(self) -> None:
"""Pause the timer."""
...
def resume(self) -> None:
"""Resume the timer."""
...
def cancel(self) -> None:
"""Cancel and remove the timer."""
...
class Window:
"""Window singleton for managing the game window."""
resolution: Tuple[int, int]
fullscreen: bool
vsync: bool
title: str
fps_limit: int
game_resolution: Tuple[int, int]
scaling_mode: str
@staticmethod
def get() -> 'Window':
"""Get the window singleton instance."""
...
class Animation:
"""Animation object for animating UI properties."""
target: Any
property: str
duration: float
easing: str
loop: bool
on_complete: Optional[Callable]
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
duration: float, easing: str = 'linear', loop: bool = False,
on_complete: Optional[Callable] = None) -> None: ...
def start(self) -> None:
"""Start the animation."""
...
def update(self, dt: float) -> bool:
"""Update animation, returns True if still running."""
...
def get_current_value(self) -> Any:
"""Get the current interpolated value."""
...
# Module functions
def createSoundBuffer(filename: str) -> int:
"""Load a sound effect from a file and return its buffer ID."""
...
def loadMusic(filename: str) -> None:
"""Load and immediately play background music from a file."""
...
def setMusicVolume(volume: int) -> None:
"""Set the global music volume (0-100)."""
...
def setSoundVolume(volume: int) -> None:
"""Set the global sound effects volume (0-100)."""
...
def playSound(buffer_id: int) -> None:
"""Play a sound effect using a previously loaded buffer."""
...
def getMusicVolume() -> int:
"""Get the current music volume level (0-100)."""
...
def getSoundVolume() -> int:
"""Get the current sound effects volume level (0-100)."""
...
def sceneUI(scene: Optional[str] = None) -> UICollection:
"""Get all UI elements for a scene."""
...
def currentScene() -> str:
"""Get the name of the currently active scene."""
...
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
"""Switch to a different scene with optional transition effect."""
...
def createScene(name: str) -> None:
"""Create a new empty scene."""
...
def keypressScene(handler: Callable[[str, bool], None]) -> None:
"""Set the keyboard event handler for the current scene."""
...
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
"""Create or update a recurring timer."""
...
def delTimer(name: str) -> None:
"""Stop and remove a timer."""
...
def exit() -> None:
"""Cleanly shut down the game engine and exit the application."""
...
def setScale(multiplier: float) -> None:
"""Scale the game window size (deprecated - use Window.resolution)."""
...
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
"""Find the first UI element with the specified name."""
...
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
"""Find all UI elements matching a name pattern (supports * wildcards)."""
...
def getMetrics() -> Dict[str, Union[int, float]]:
"""Get current performance metrics."""
...
# Submodule
class automation:
"""Automation API for testing and scripting."""
@staticmethod
def screenshot(filename: str) -> bool:
"""Save a screenshot to the specified file."""
...
@staticmethod
def position() -> Tuple[int, int]:
"""Get current mouse position as (x, y) tuple."""
...
@staticmethod
def size() -> Tuple[int, int]:
"""Get screen size as (width, height) tuple."""
...
@staticmethod
def onScreen(x: int, y: int) -> bool:
"""Check if coordinates are within screen bounds."""
...
@staticmethod
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
"""Move mouse to absolute position."""
...
@staticmethod
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
"""Move mouse relative to current position."""
...
@staticmethod
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse to position."""
...
@staticmethod
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse relative to current position."""
...
@staticmethod
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
interval: float = 0.0, button: str = 'left') -> None:
"""Click mouse at position."""
...
@staticmethod
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Press mouse button down."""
...
@staticmethod
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Release mouse button."""
...
@staticmethod
def keyDown(key: str) -> None:
"""Press key down."""
...
@staticmethod
def keyUp(key: str) -> None:
"""Release key."""
...
@staticmethod
def press(key: str) -> None:
"""Press and release a key."""
...
@staticmethod
def typewrite(text: str, interval: float = 0.0) -> None:
"""Type text with optional interval between characters."""
...
'''
def main():
"""Generate type stubs."""
print("Generating comprehensive type stubs for McRogueFace...")
# Create stubs directory
os.makedirs('stubs', exist_ok=True)
# Write main stub file
with open('stubs/mcrfpy.pyi', 'w') as f:
f.write(generate_mcrfpy_stub())
print("Generated stubs/mcrfpy.pyi")
# Create py.typed marker
with open('stubs/py.typed', 'w') as f:
f.write('')
print("Created py.typed marker")
print("\nType stubs generated successfully!")
print("\nTo use in your IDE:")
print("1. Add the 'stubs' directory to your project")
print("2. Most IDEs will automatically detect the .pyi files")
print("3. For VS Code: add to python.analysis.extraPaths in settings.json")
print("4. For PyCharm: mark 'stubs' directory as Sources Root")
if __name__ == '__main__':
main()

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,253 @@
# Part 0 - Setting Up McRogueFace
Welcome to the McRogueFace Roguelike Tutorial! This tutorial will teach you how to create a complete roguelike game using the McRogueFace game engine. Unlike traditional Python libraries, McRogueFace is a complete, portable game engine that includes everything you need to make and distribute games.
## What is McRogueFace?
McRogueFace is a high-performance game engine with Python scripting support. Think of it like Unity or Godot, but specifically designed for roguelikes and 2D games. It includes:
- A complete Python 3.12 runtime (no installation needed!)
- High-performance C++ rendering and entity management
- Built-in UI components and scene management
- Integrated audio system
- Professional sprite-based graphics
- Easy distribution - your players don't need Python installed!
## Prerequisites
Before starting this tutorial, you should:
- Have basic Python knowledge (variables, functions, classes)
- Be comfortable editing text files
- Have a text editor (VS Code, Sublime Text, Notepad++, etc.)
That's it! Unlike other roguelike tutorials, you don't need Python installed - McRogueFace includes everything.
## Getting McRogueFace
### Step 1: Download the Engine
1. Visit the McRogueFace releases page
2. Download the version for your operating system:
- `McRogueFace-Windows.zip` for Windows
- `McRogueFace-MacOS.zip` for macOS
- `McRogueFace-Linux.zip` for Linux
### Step 2: Extract the Archive
Extract the downloaded archive to a folder where you want to develop your game. You should see this structure:
```
McRogueFace/
├── mcrogueface (or mcrogueface.exe on Windows)
├── scripts/
│ └── game.py
├── assets/
│ ├── sprites/
│ ├── fonts/
│ └── audio/
└── lib/
```
### Step 3: Run the Engine
Run the McRogueFace executable:
- **Windows**: Double-click `mcrogueface.exe`
- **Mac/Linux**: Open a terminal in the folder and run `./mcrogueface`
You should see a window open with the default McRogueFace demo. This shows the engine is working correctly!
## Your First McRogueFace Script
Let's modify the engine to display "Hello Roguelike!" instead of the default demo.
### Step 1: Open game.py
Open `scripts/game.py` in your text editor. You'll see the default demo code. Replace it entirely with:
```python
import mcrfpy
# Create a new scene called "hello"
mcrfpy.createScene("hello")
# Switch to our new scene
mcrfpy.setScene("hello")
# Get the UI container for our scene
ui = mcrfpy.sceneUI("hello")
# Create a text caption
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
caption.font_size = 32
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
# Add the caption to our scene
ui.append(caption)
# Create a smaller instruction caption
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
instruction.font_size = 16
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
ui.append(instruction)
# Set up a simple key handler
def handle_keys(key, state):
if state == "start" and key == "Escape":
mcrfpy.setScene(None) # This exits the game
mcrfpy.keypressScene(handle_keys)
print("Hello Roguelike is running!")
```
### Step 2: Save and Run
1. Save the file
2. If McRogueFace is still running, it will automatically reload!
3. If not, run the engine again
You should now see "Hello Roguelike!" displayed in the window.
### Step 3: Understanding the Code
Let's break down what we just wrote:
1. **Import mcrfpy**: This is McRogueFace's Python API
2. **Create a scene**: Scenes are like game states (menu, gameplay, inventory, etc.)
3. **UI elements**: We create Caption objects for text display
4. **Colors**: McRogueFace uses RGB colors (0-255 for each component)
5. **Input handling**: We set up a callback for keyboard input
6. **Scene switching**: Setting the scene to None exits the game
## Key Differences from Pure Python Development
### The Game Loop
Unlike typical Python scripts, McRogueFace runs your code inside its game loop:
1. The engine starts and loads `scripts/game.py`
2. Your script sets up scenes, UI elements, and callbacks
3. The engine runs at 60 FPS, handling rendering and input
4. Your callbacks are triggered by game events
### Hot Reloading
McRogueFace can reload your scripts while running! Just save your changes and the engine will reload automatically. This makes development incredibly fast.
### Asset Pipeline
McRogueFace includes a complete asset system:
- **Sprites**: Place images in `assets/sprites/`
- **Fonts**: TrueType fonts in `assets/fonts/`
- **Audio**: Sound effects and music in `assets/audio/`
We'll explore these in later lessons.
## Testing Your Setup
Let's create a more interactive test to ensure everything is working properly:
```python
import mcrfpy
# Create our test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# Create a background frame
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
ui.append(background)
# Title text
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
title.font_size = 36
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
ui.append(title)
# Status text that will update
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
status_text.font_size = 20
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
# Instructions
instructions = [
"Arrow Keys: Test movement input",
"Space: Test action input",
"Mouse Click: Test mouse input",
"ESC: Exit"
]
y_offset = 400
for instruction in instructions:
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
inst_caption.font_size = 16
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(inst_caption)
y_offset += 30
# Input handler
def handle_input(key, state):
if state != "start":
return
if key == "Escape":
mcrfpy.setScene(None)
else:
status_text.text = f"You pressed: {key}"
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
# Set up input handling
mcrfpy.keypressScene(handle_input)
print("Setup test is running! Try pressing different keys.")
```
## Troubleshooting
### Engine Won't Start
- **Windows**: Make sure you extracted all files, not just the .exe
- **Mac**: You may need to right-click and select "Open" the first time
- **Linux**: Make sure the file is executable: `chmod +x mcrogueface`
### Scripts Not Loading
- Ensure your script is named exactly `game.py` in the `scripts/` folder
- Check the console output for Python errors
- Make sure you're using Python 3 syntax
### Performance Issues
- McRogueFace should run smoothly at 60 FPS
- If not, check if your graphics drivers are updated
- The engine shows FPS in the window title
## What's Next?
Congratulations! You now have McRogueFace set up and running. You've learned:
- How to download and run the McRogueFace engine
- The basic structure of a McRogueFace project
- How to create scenes and UI elements
- How to handle keyboard input
- The development workflow with hot reloading
In Part 1, we'll create our player character and implement movement. We'll explore McRogueFace's entity system and learn how to create a game world.
## Why McRogueFace?
Before we continue, let's highlight why McRogueFace is excellent for roguelike development:
1. **No Installation Hassles**: Your players just download and run - no Python needed!
2. **Professional Performance**: C++ engine core means smooth gameplay even with hundreds of entities
3. **Built-in Features**: UI, audio, scenes, and animations are already there
4. **Easy Distribution**: Just zip your game folder and share it
5. **Rapid Development**: Hot reloading and Python scripting for quick iteration
Ready to make a roguelike? Let's continue to Part 1!

View File

@ -0,0 +1,33 @@
import mcrfpy
# Create a new scene called "hello"
mcrfpy.createScene("hello")
# Switch to our new scene
mcrfpy.setScene("hello")
# Get the UI container for our scene
ui = mcrfpy.sceneUI("hello")
# Create a text caption
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
caption.font_size = 32
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
# Add the caption to our scene
ui.append(caption)
# Create a smaller instruction caption
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
instruction.font_size = 16
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
ui.append(instruction)
# Set up a simple key handler
def handle_keys(key, state):
if state == "start" and key == "Escape":
mcrfpy.setScene(None) # This exits the game
mcrfpy.keypressScene(handle_keys)
print("Hello Roguelike is running!")

View File

@ -0,0 +1,55 @@
import mcrfpy
# Create our test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# Create a background frame
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
ui.append(background)
# Title text
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
title.font_size = 36
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
ui.append(title)
# Status text that will update
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
status_text.font_size = 20
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
# Instructions
instructions = [
"Arrow Keys: Test movement input",
"Space: Test action input",
"Mouse Click: Test mouse input",
"ESC: Exit"
]
y_offset = 400
for instruction in instructions:
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
inst_caption.font_size = 16
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(inst_caption)
y_offset += 30
# Input handler
def handle_input(key, state):
if state != "start":
return
if key == "Escape":
mcrfpy.setScene(None)
else:
status_text.text = f"You pressed: {key}"
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
# Set up input handling
mcrfpy.keypressScene(handle_input)
print("Setup test is running! Try pressing different keys.")

View File

@ -0,0 +1,457 @@
# Part 1 - Drawing the '@' Symbol and Moving It Around
In Part 0, we set up McRogueFace and created a simple "Hello Roguelike" scene. Now it's time to create the foundation of our game: a player character that can move around the screen.
In traditional roguelikes, the player is represented by the '@' symbol. We'll honor that tradition while taking advantage of McRogueFace's powerful sprite-based rendering system.
## Understanding McRogueFace's Architecture
Before we dive into code, let's understand two key concepts in McRogueFace:
### Grid - The Game World
A `Grid` represents your game world. It's a 2D array of tiles where each tile can be:
- **Walkable or not** (for collision detection)
- **Transparent or not** (for field of view, which we'll cover later)
- **Have a visual appearance** (sprite index and color)
Think of the Grid as the dungeon floor, walls, and other static elements.
### Entity - Things That Move
An `Entity` represents anything that can move around on the Grid:
- The player character
- Monsters
- Items (if you want them to be thrown or moved)
- Projectiles
Entities exist "on top of" the Grid and automatically handle smooth movement animation between tiles.
## Creating Our Game World
Let's start by creating a simple room for our player to move around in. Create a new `game.py`:
```python
import mcrfpy
# Define some constants for our tile types
FLOOR_TILE = 0
WALL_TILE = 1
PLAYER_SPRITE = 2
# Window configuration
mcrfpy.createScene("game")
mcrfpy.setScene("game")
# Configure window properties
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 1"
# Get the UI container for our scene
ui = mcrfpy.sceneUI("game")
# Create a dark background
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
ui.append(background)
```
Now we need to set up our tileset. For this tutorial, we'll use ASCII-style sprites. McRogueFace comes with a built-in ASCII tileset:
```python
# Load the ASCII tileset
# This tileset has characters mapped to sprite indices
# For example: @ = 64, # = 35, . = 46
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
# Create the game grid
# 50x30 tiles is a good size for a roguelike
GRID_WIDTH = 50
GRID_HEIGHT = 30
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
grid.position = (100, 100) # Position on screen
grid.size = (800, 480) # Size in pixels
# Add the grid to our UI
ui.append(grid)
```
## Initializing the Game World
Now let's fill our grid with a simple room:
```python
def create_room():
"""Create a room with walls around the edges"""
# Fill everything with floor tiles first
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.sprite_index = 46 # '.' character
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
# Create walls around the edges
for x in range(GRID_WIDTH):
# Top wall
cell = grid.at(x, 0)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
# Bottom wall
cell = grid.at(x, GRID_HEIGHT - 1)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
for y in range(GRID_HEIGHT):
# Left wall
cell = grid.at(0, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Right wall
cell = grid.at(GRID_WIDTH - 1, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Create the room
create_room()
```
## Creating the Player
Now let's add our player character:
```python
# Create the player entity
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
player.sprite_index = 64 # '@' character
player.color = mcrfpy.Color(255, 255, 255) # White
# The entity is automatically added to the grid when we pass grid= parameter
# This is equivalent to: grid.entities.append(player)
```
## Handling Input
McRogueFace uses a callback system for input. For a turn-based roguelike, we only care about key presses, not releases:
```python
def handle_input(key, state):
"""Handle keyboard input for player movement"""
# Only process key presses, not releases
if state != "start":
return
# Movement deltas
dx, dy = 0, 0
# Arrow keys
if key == "Up":
dy = -1
elif key == "Down":
dy = 1
elif key == "Left":
dx = -1
elif key == "Right":
dx = 1
# Numpad movement (for true roguelike feel!)
elif key == "Num7": # Northwest
dx, dy = -1, -1
elif key == "Num8": # North
dy = -1
elif key == "Num9": # Northeast
dx, dy = 1, -1
elif key == "Num4": # West
dx = -1
elif key == "Num6": # East
dx = 1
elif key == "Num1": # Southwest
dx, dy = -1, 1
elif key == "Num2": # South
dy = 1
elif key == "Num3": # Southeast
dx, dy = 1, 1
# Escape to quit
elif key == "Escape":
mcrfpy.setScene(None)
return
# If there's movement, try to move the player
if dx != 0 or dy != 0:
move_player(dx, dy)
# Register the input handler
mcrfpy.keypressScene(handle_input)
```
## Implementing Movement with Collision Detection
Now let's implement the movement function with proper collision detection:
```python
def move_player(dx, dy):
"""Move the player if the destination is walkable"""
# Calculate new position
new_x = player.x + dx
new_y = player.y + dy
# Check bounds
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
return
# Check if the destination is walkable
destination = grid.at(new_x, new_y)
if destination.walkable:
# Move the player
player.x = new_x
player.y = new_y
# The entity will automatically animate to the new position!
```
## Adding Visual Polish
Let's add some UI elements to make our game look more polished:
```python
# Add a title
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
ui.append(title)
# Add instructions
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
ui.append(instructions)
# Add a status line at the bottom
status = mcrfpy.Caption("@ You", 100, 600)
status.font_size = 18
status.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status)
```
## Complete Code
Here's the complete `game.py` for Part 1:
```python
import mcrfpy
# Window configuration
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 1"
# Get the UI container for our scene
ui = mcrfpy.sceneUI("game")
# Create a dark background
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
ui.append(background)
# Load the ASCII tileset
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
# Create the game grid
GRID_WIDTH = 50
GRID_HEIGHT = 30
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
grid.position = (100, 100)
grid.size = (800, 480)
ui.append(grid)
def create_room():
"""Create a room with walls around the edges"""
# Fill everything with floor tiles first
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.sprite_index = 46 # '.' character
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
# Create walls around the edges
for x in range(GRID_WIDTH):
# Top wall
cell = grid.at(x, 0)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
# Bottom wall
cell = grid.at(x, GRID_HEIGHT - 1)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
for y in range(GRID_HEIGHT):
# Left wall
cell = grid.at(0, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Right wall
cell = grid.at(GRID_WIDTH - 1, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Create the room
create_room()
# Create the player entity
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
player.sprite_index = 64 # '@' character
player.color = mcrfpy.Color(255, 255, 255) # White
def move_player(dx, dy):
"""Move the player if the destination is walkable"""
# Calculate new position
new_x = player.x + dx
new_y = player.y + dy
# Check bounds
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
return
# Check if the destination is walkable
destination = grid.at(new_x, new_y)
if destination.walkable:
# Move the player
player.x = new_x
player.y = new_y
def handle_input(key, state):
"""Handle keyboard input for player movement"""
# Only process key presses, not releases
if state != "start":
return
# Movement deltas
dx, dy = 0, 0
# Arrow keys
if key == "Up":
dy = -1
elif key == "Down":
dy = 1
elif key == "Left":
dx = -1
elif key == "Right":
dx = 1
# Numpad movement (for true roguelike feel!)
elif key == "Num7": # Northwest
dx, dy = -1, -1
elif key == "Num8": # North
dy = -1
elif key == "Num9": # Northeast
dx, dy = 1, -1
elif key == "Num4": # West
dx = -1
elif key == "Num6": # East
dx = 1
elif key == "Num1": # Southwest
dx, dy = -1, 1
elif key == "Num2": # South
dy = 1
elif key == "Num3": # Southeast
dx, dy = 1, 1
# Escape to quit
elif key == "Escape":
mcrfpy.setScene(None)
return
# If there's movement, try to move the player
if dx != 0 or dy != 0:
move_player(dx, dy)
# Register the input handler
mcrfpy.keypressScene(handle_input)
# Add UI elements
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(title)
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(instructions)
status = mcrfpy.Caption("@ You", 100, 600)
status.font_size = 18
status.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status)
print("Part 1: The @ symbol moves!")
```
## Understanding What We've Built
Let's review the key concepts we've implemented:
1. **Grid-Entity Architecture**: The Grid represents our static world (floors and walls), while the Entity (player) moves on top of it.
2. **Collision Detection**: By checking the `walkable` property of grid cells, we prevent the player from walking through walls.
3. **Turn-Based Input**: By only responding to key presses (not releases), we've created true turn-based movement.
4. **Visual Feedback**: The Entity system automatically animates movement between tiles, giving smooth visual feedback.
## Exercises
Try these modifications to deepen your understanding:
1. **Add More Rooms**: Create multiple rooms connected by corridors
2. **Different Tile Types**: Add doors (walkable but different appearance)
3. **Sprint Movement**: Hold Shift to move multiple tiles at once
4. **Mouse Support**: Click a tile to pathfind to it (we'll cover pathfinding properly later)
## ASCII Sprite Reference
Here are some useful ASCII character indices for the default tileset:
- @ (player): 64
- # (wall): 35
- . (floor): 46
- + (door): 43
- ~ (water): 126
- % (item): 37
- ! (potion): 33
## What's Next?
In Part 2, we'll expand our world with:
- A proper Entity system for managing multiple objects
- NPCs that can also move around
- A more interesting map layout
- The beginning of our game architecture
The foundation is set - you have a player character that can move around a world with collision detection. This is the core of any roguelike game!

View File

@ -0,0 +1,162 @@
import mcrfpy
# Window configuration
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 1"
# Get the UI container for our scene
ui = mcrfpy.sceneUI("game")
# Create a dark background
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
ui.append(background)
# Load the ASCII tileset
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
# Create the game grid
GRID_WIDTH = 50
GRID_HEIGHT = 30
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
grid.position = (100, 100)
grid.size = (800, 480)
ui.append(grid)
def create_room():
"""Create a room with walls around the edges"""
# Fill everything with floor tiles first
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.sprite_index = 46 # '.' character
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
# Create walls around the edges
for x in range(GRID_WIDTH):
# Top wall
cell = grid.at(x, 0)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
# Bottom wall
cell = grid.at(x, GRID_HEIGHT - 1)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
for y in range(GRID_HEIGHT):
# Left wall
cell = grid.at(0, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Right wall
cell = grid.at(GRID_WIDTH - 1, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Create the room
create_room()
# Create the player entity
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
player.sprite_index = 64 # '@' character
player.color = mcrfpy.Color(255, 255, 255) # White
def move_player(dx, dy):
"""Move the player if the destination is walkable"""
# Calculate new position
new_x = player.x + dx
new_y = player.y + dy
# Check bounds
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
return
# Check if the destination is walkable
destination = grid.at(new_x, new_y)
if destination.walkable:
# Move the player
player.x = new_x
player.y = new_y
def handle_input(key, state):
"""Handle keyboard input for player movement"""
# Only process key presses, not releases
if state != "start":
return
# Movement deltas
dx, dy = 0, 0
# Arrow keys
if key == "Up":
dy = -1
elif key == "Down":
dy = 1
elif key == "Left":
dx = -1
elif key == "Right":
dx = 1
# Numpad movement (for true roguelike feel!)
elif key == "Num7": # Northwest
dx, dy = -1, -1
elif key == "Num8": # North
dy = -1
elif key == "Num9": # Northeast
dx, dy = 1, -1
elif key == "Num4": # West
dx = -1
elif key == "Num6": # East
dx = 1
elif key == "Num1": # Southwest
dx, dy = -1, 1
elif key == "Num2": # South
dy = 1
elif key == "Num3": # Southeast
dx, dy = 1, 1
# Escape to quit
elif key == "Escape":
mcrfpy.setScene(None)
return
# If there's movement, try to move the player
if dx != 0 or dy != 0:
move_player(dx, dy)
# Register the input handler
mcrfpy.keypressScene(handle_input)
# Add UI elements
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(title)
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(instructions)
status = mcrfpy.Caption("@ You", 100, 600)
status.font_size = 18
status.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status)
print("Part 1: The @ symbol moves!")

View File

@ -0,0 +1,562 @@
# Part 2 - The Generic Entity, the Render Functions, and the Map
In Part 1, we created a player character that could move around a simple room. Now it's time to build a proper architecture for our roguelike. We'll create a flexible entity system, a proper map structure, and organize our code for future expansion.
## Understanding Game Architecture
Before diving into code, let's understand the architecture we're building:
1. **Entities**: Anything that can exist in the game world (player, monsters, items)
2. **Game Map**: The dungeon structure with tiles that can be walls or floors
3. **Game Engine**: Coordinates everything - entities, map, input, and rendering
In McRogueFace, we'll adapt these concepts to work with the engine's scene-based architecture.
## Creating a Flexible Entity System
While McRogueFace provides a built-in `Entity` class, we'll create a wrapper to add game-specific functionality:
```python
class GameObject:
"""Base class for all game objects (player, monsters, items)"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks # Does this entity block movement?
self._entity = None # The McRogueFace entity
self.grid = None # Reference to the grid
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = self.color
def move(self, dx, dy):
"""Move by the given amount if possible"""
if not self.grid:
return
new_x = self.x + dx
new_y = self.y + dy
# Update our position
self.x = new_x
self.y = new_y
# Update the visual entity
if self._entity:
self._entity.x = new_x
self._entity.y = new_y
def destroy(self):
"""Remove this entity from the game"""
if self._entity and self.grid:
# Find and remove from grid's entity list
for i, entity in enumerate(self.grid.entities):
if entity == self._entity:
del self.grid.entities[i]
break
self._entity = None
```
## Building the Game Map
Let's create a proper map class that manages our dungeon:
```python
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = [] # List of GameObjects
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Initialize all tiles as walls
self.fill_with_walls()
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def create_room(self, x1, y1, x2, y2):
"""Carve out a room in the map"""
# Make sure coordinates are in the right order
x1, x2 = min(x1, x2), max(x1, x2)
y1, y2 = min(y1, y2), max(y1, y2)
# Carve out floor tiles
for y in range(y1, y2 + 1):
for x in range(x1, x2 + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_h(self, x1, x2, y):
"""Create a horizontal tunnel"""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_v(self, y1, y2, x):
"""Create a vertical tunnel"""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
# Check map boundaries
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
# Check if tile is walkable
if not self.grid.at(x, y).walkable:
return True
# Check if any blocking entity is at this position
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
```
## Creating the Game Engine
Now let's build our game engine to tie everything together:
```python
class Engine:
"""Main game engine that manages game state"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
# Create the game scene
mcrfpy.createScene("game")
mcrfpy.setScene("game")
# Configure window
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 2"
# Get UI container
self.ui = mcrfpy.sceneUI("game")
# Add background
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
# Load tileset
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
# Create the game world
self.setup_game()
# Setup input handling
self.setup_input()
# Add UI elements
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
# Create the map
self.game_map = GameMap(50, 30)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create some rooms
self.game_map.create_room(10, 10, 20, 20)
self.game_map.create_room(30, 15, 40, 25)
self.game_map.create_room(15, 22, 25, 28)
# Connect rooms with tunnels
self.game_map.create_tunnel_h(20, 30, 15)
self.game_map.create_tunnel_v(20, 22, 20)
# Create player
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
self.game_map.add_entity(self.player)
# Create an NPC
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
self.game_map.add_entity(npc)
self.entities.append(npc)
# Create some items (non-blocking)
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
self.game_map.add_entity(potion)
self.entities.append(potion)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
# Check if movement is blocked
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
else:
# Check if we bumped into an entity
target = self.game_map.get_blocking_entity_at(new_x, new_y)
if target:
print(f"You bump into the {target.name}!")
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
# Movement keys
movement = {
"Up": (0, -1),
"Down": (0, 1),
"Left": (-1, 0),
"Right": (1, 0),
"Num7": (-1, -1),
"Num8": (0, -1),
"Num9": (1, -1),
"Num4": (-1, 0),
"Num6": (1, 0),
"Num1": (-1, 1),
"Num2": (0, 1),
"Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
# Title
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
# Instructions
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
```
## Putting It All Together
Here's the complete `game.py` file:
```python
import mcrfpy
class GameObject:
"""Base class for all game objects (player, monsters, items)"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount if possible"""
if not self.grid:
return
new_x = self.x + dx
new_y = self.y + dy
self.x = new_x
self.y = new_y
if self._entity:
self._entity.x = new_x
self._entity.y = new_y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
self.fill_with_walls()
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def create_room(self, x1, y1, x2, y2):
"""Carve out a room in the map"""
x1, x2 = min(x1, x2), max(x1, x2)
y1, y2 = min(y1, y2), max(y1, y2)
for y in range(y1, y2 + 1):
for x in range(x1, x2 + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_h(self, x1, x2, y):
"""Create a horizontal tunnel"""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_v(self, y1, y2, x):
"""Create a vertical tunnel"""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
class Engine:
"""Main game engine that manages game state"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 2"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(50, 30)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
self.game_map.create_room(10, 10, 20, 20)
self.game_map.create_room(30, 15, 40, 25)
self.game_map.create_room(15, 22, 25, 28)
self.game_map.create_tunnel_h(20, 30, 15)
self.game_map.create_tunnel_v(20, 22, 20)
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
self.game_map.add_entity(self.player)
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
self.game_map.add_entity(npc)
self.entities.append(npc)
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
self.game_map.add_entity(potion)
self.entities.append(potion)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
else:
target = self.game_map.get_blocking_entity_at(new_x, new_y)
if target:
print(f"You bump into the {target.name}!")
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 2: Entities and Maps!")
```
## Understanding the Architecture
### GameObject Class
Our `GameObject` class wraps McRogueFace's `Entity` and adds:
- Game logic properties (name, blocking)
- Position tracking independent of the visual entity
- Easy attachment/detachment from grids
### GameMap Class
The `GameMap` manages:
- The McRogueFace `Grid` for visual representation
- A list of all entities in the map
- Collision detection including entity blocking
- Map generation utilities (rooms, tunnels)
### Engine Class
The `Engine` coordinates everything:
- Scene and UI setup
- Game state management
- Input handling
- Entity-map interactions
## Key Improvements from Part 1
1. **Proper Entity Management**: Multiple entities can exist and interact
2. **Blocking Entities**: Some entities block movement, others don't
3. **Map Generation**: Tools for creating rooms and tunnels
4. **Collision System**: Checks both tiles and entities
5. **Organized Code**: Clear separation of concerns
## Exercises
1. **Add More Entity Types**: Create different sprites for monsters, items, and NPCs
2. **Entity Interactions**: Make items disappear when walked over
3. **Random Map Generation**: Place rooms and tunnels randomly
4. **Entity Properties**: Add health, damage, or other attributes to GameObjects
## What's Next?
In Part 3, we'll implement proper dungeon generation with:
- Procedurally generated rooms
- Smart tunnel routing
- Entity spawning
- The beginning of a real roguelike dungeon!
We now have a solid foundation with proper entity management and map structure. This architecture will serve us well as we add more complex features to our roguelike!

View File

@ -0,0 +1,217 @@
import mcrfpy
class GameObject:
"""Base class for all game objects (player, monsters, items)"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount if possible"""
if not self.grid:
return
new_x = self.x + dx
new_y = self.y + dy
self.x = new_x
self.y = new_y
if self._entity:
self._entity.x = new_x
self._entity.y = new_y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
self.fill_with_walls()
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def create_room(self, x1, y1, x2, y2):
"""Carve out a room in the map"""
x1, x2 = min(x1, x2), max(x1, x2)
y1, y2 = min(y1, y2), max(y1, y2)
for y in range(y1, y2 + 1):
for x in range(x1, x2 + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_h(self, x1, x2, y):
"""Create a horizontal tunnel"""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_v(self, y1, y2, x):
"""Create a vertical tunnel"""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
class Engine:
"""Main game engine that manages game state"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 2"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(50, 30)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
self.game_map.create_room(10, 10, 20, 20)
self.game_map.create_room(30, 15, 40, 25)
self.game_map.create_room(15, 22, 25, 28)
self.game_map.create_tunnel_h(20, 30, 15)
self.game_map.create_tunnel_v(20, 22, 20)
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
self.game_map.add_entity(self.player)
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
self.game_map.add_entity(npc)
self.entities.append(npc)
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
self.game_map.add_entity(potion)
self.entities.append(potion)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
else:
target = self.game_map.get_blocking_entity_at(new_x, new_y)
if target:
print(f"You bump into the {target.name}!")
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 2: Entities and Maps!")

View File

@ -0,0 +1,548 @@
# Part 3 - Generating a Dungeon
In Parts 1 and 2, we created a player that could move around and interact with a hand-crafted dungeon. Now it's time to generate dungeons procedurally - a core feature of any roguelike game!
## The Plan
We'll create a dungeon generator that:
1. Places rectangular rooms randomly
2. Ensures rooms don't overlap
3. Connects rooms with tunnels
4. Places the player in the first room
This is a classic approach used by many roguelikes, and it creates interesting, playable dungeons.
## Creating a Room Class
First, let's create a class to represent rectangular rooms:
```python
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
"""Return the center coordinates of the room"""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
"""Return the inner area of the room as a tuple of slices
This property returns the area inside the walls.
We'll add 1 to min coordinates and subtract 1 from max coordinates.
"""
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
"""Return True if this room overlaps with another RectangularRoom"""
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
```
## Implementing Tunnel Generation
Since McRogueFace doesn't include line-drawing algorithms, let's implement simple L-shaped tunnels:
```python
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
# Randomly decide whether to go horizontal first or vertical first
if random.random() < 0.5:
# Horizontal, then vertical
corner_x = x2
corner_y = y1
else:
# Vertical, then horizontal
corner_x = x1
corner_y = y2
# Generate the coordinates
# First line: from start to corner
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
# Second line: from corner to end
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
```
## The Dungeon Generator
Now let's update our GameMap class to generate dungeons:
```python
import random
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = [] # Keep track of rooms for game logic
def generate_dungeon(
self,
max_rooms,
room_min_size,
room_max_size,
player
):
"""Generate a new dungeon map"""
# Start with everything as walls
self.fill_with_walls()
for r in range(max_rooms):
# Random width and height
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
# Random position without going out of bounds
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
# Create the room
new_room = RectangularRoom(x, y, room_width, room_height)
# Check if it intersects with any existing room
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue # This room intersects, so go to the next attempt
# If we get here, it's a valid room
# Carve out this room
self.carve_room(new_room)
# Place the player in the center of the first room
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All rooms after the first:
# Tunnel between this room and the previous one
self.carve_tunnel(self.rooms[-1].center, new_room.center)
# Finally, append the new room to the list
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(30, 30, 40)) # Slightly different color for tunnels
```
## Complete Code
Here's the complete `game.py` with procedural dungeon generation:
```python
import mcrfpy
import random
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
"""Return the center coordinates of the room"""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
"""Return the inner area of the room"""
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
"""Return True if this room overlaps with another"""
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
# Generate the coordinates
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(30, 30, 40))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 3"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player (before dungeon generation)
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add some monsters in random rooms
for i in range(5):
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
room = self.game_map.rooms[i + 1]
x, y = room.center
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "Space":
# Regenerate the dungeon
self.regenerate_dungeon()
mcrfpy.keypressScene(handle_keys)
def regenerate_dungeon(self):
"""Generate a new dungeon"""
# Clear existing entities
self.game_map.entities.clear()
self.game_map.rooms.clear()
self.entities.clear()
# Clear the entity list in the grid
if self.game_map.grid:
self.game_map.grid.entities.clear()
# Regenerate
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Re-add player
self.game_map.add_entity(self.player)
# Add new monsters
for i in range(5):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 3: Procedural Dungeon Generation!")
print("Press SPACE to generate a new dungeon")
```
## Understanding the Algorithm
Our dungeon generation algorithm is simple but effective:
1. **Start with solid walls** - The entire map begins filled with wall tiles
2. **Try to place rooms** - Generate random rooms and check for overlaps
3. **Connect with tunnels** - Each new room connects to the previous one
4. **Place entities** - The player starts in the first room, monsters in others
### Room Placement
The algorithm attempts to place `max_rooms` rooms, but may place fewer if many attempts result in overlapping rooms. This is called "rejection sampling" - we generate random rooms and reject ones that don't fit.
### Tunnel Design
Our L-shaped tunnels are simple but effective. They either go:
- Horizontal first, then vertical
- Vertical first, then horizontal
This creates variety while ensuring all rooms are connected.
## Experimenting with Parameters
Try adjusting these parameters to create different dungeon styles:
```python
# Sparse dungeon with large rooms
self.game_map.generate_dungeon(
max_rooms=10,
room_min_size=10,
room_max_size=15,
player=self.player
)
# Dense dungeon with small rooms
self.game_map.generate_dungeon(
max_rooms=50,
room_min_size=4,
room_max_size=6,
player=self.player
)
```
## Visual Enhancements
Notice how we gave tunnels a slightly different color:
- Rooms: `color=(50, 50, 50)` - Medium gray
- Tunnels: `color=(30, 30, 40)` - Darker with blue tint
This subtle difference helps players understand the dungeon layout.
## Exercises
1. **Different Room Shapes**: Create circular or cross-shaped rooms
2. **Better Tunnel Routing**: Implement A* pathfinding for more natural tunnels
3. **Room Types**: Create special rooms (treasure rooms, trap rooms)
4. **Dungeon Themes**: Use different tile sets and colors for different dungeon levels
## What's Next?
In Part 4, we'll implement Field of View (FOV) so the player can only see parts of the dungeon they've explored. This will add mystery and atmosphere to our procedurally generated dungeons!
Our dungeon generator is now creating unique, playable levels every time. The foundation of a true roguelike is taking shape!

View File

@ -0,0 +1,312 @@
import mcrfpy
import random
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
"""Return the center coordinates of the room"""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
"""Return the inner area of the room"""
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
"""Return True if this room overlaps with another"""
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
# Generate the coordinates
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(30, 30, 40))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 3"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player (before dungeon generation)
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add some monsters in random rooms
for i in range(5):
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
room = self.game_map.rooms[i + 1]
x, y = room.center
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "Space":
# Regenerate the dungeon
self.regenerate_dungeon()
mcrfpy.keypressScene(handle_keys)
def regenerate_dungeon(self):
"""Generate a new dungeon"""
# Clear existing entities
self.game_map.entities.clear()
self.game_map.rooms.clear()
self.entities.clear()
# Clear the entity list in the grid
if self.game_map.grid:
self.game_map.grid.entities.clear()
# Regenerate
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Re-add player
self.game_map.add_entity(self.player)
# Add new monsters
for i in range(5):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 3: Procedural Dungeon Generation!")
print("Press SPACE to generate a new dungeon")

View File

@ -0,0 +1,520 @@
# Part 4 - Field of View
One of the defining features of roguelikes is exploration and discovery. In Part 3, we could see the entire dungeon at once. Now we'll implement Field of View (FOV) so players can only see what their character can actually see, adding mystery and tactical depth to our game.
## Understanding Field of View
Field of View creates three distinct visibility states for each tile:
1. **Visible**: Currently in the player's line of sight
2. **Explored**: Previously seen but not currently visible
3. **Unexplored**: Never seen (completely hidden)
This creates the classic "fog of war" effect where you remember the layout of areas you've explored, but can't see current enemy positions unless they're in your view.
## McRogueFace's FOV System
Good news! McRogueFace includes built-in FOV support through its C++ engine. We just need to enable and configure it. The engine uses an efficient shadowcasting algorithm that provides smooth, realistic line-of-sight calculations.
Let's update our code to use FOV:
```python
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
```
## Configuring Visibility Rendering
McRogueFace automatically handles the rendering of visible/explored/unexplored tiles. We need to set up our grid to use perspective-based rendering:
```python
class GameMap:
"""Manages the game world"""
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering (0 = first entity = player)
self.grid.perspective = 0
return self.grid
```
## Visual Appearance Configuration
Let's define how our tiles look in different visibility states:
```python
# Color configurations for visibility states
COLORS_VISIBLE = {
'wall': (100, 100, 100), # Light gray
'floor': (50, 50, 50), # Dark gray
'tunnel': (30, 30, 40), # Dark blue-gray
}
COLORS_EXPLORED = {
'wall': (50, 50, 70), # Darker, bluish
'floor': (20, 20, 30), # Very dark
'tunnel': (15, 15, 25), # Almost black
}
# Update the tile-setting methods to store the tile type
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
# Store both visible and explored colors
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
# The engine will automatically darken explored tiles
```
## Complete Implementation
Here's the complete updated `game.py` with FOV:
```python
import mcrfpy
import random
# Color configurations for visibility
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering (0 = first entity = player)
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.fov_radius = 8
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 4"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add monsters in random rooms
for i in range(10):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
# Randomly offset from center
x += random.randint(-2, 2)
y += random.randint(-2, 2)
# Make sure position is walkable
if self.game_map.grid.at(x, y).walkable:
if i % 2 == 0:
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
else:
# Create a troll
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
self.game_map.add_entity(troll)
self.entities.append(troll)
# Initial FOV calculation
self.player.update_fov()
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "v":
# Toggle FOV on/off
if self.game_map.grid.perspective == 0:
self.game_map.grid.perspective = -1 # Omniscient
print("FOV disabled - omniscient view")
else:
self.game_map.grid.perspective = 0 # Player perspective
print("FOV enabled - player perspective")
elif key == "Plus" or key == "Equals":
# Increase FOV radius
self.fov_radius = min(self.fov_radius + 1, 20)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
elif key == "Minus":
# Decrease FOV radius
self.fov_radius = max(self.fov_radius - 1, 3)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Field of View", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# FOV indicator
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
self.fov_text.font_size = 14
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
self.ui.append(self.fov_text)
# Create and run the game
engine = Engine()
print("Part 4: Field of View!")
print("Press V to toggle FOV on/off")
print("Press +/- to adjust FOV radius")
```
## How FOV Works
McRogueFace's built-in FOV system uses a shadowcasting algorithm that:
1. **Casts rays** from the player's position to tiles within the radius
2. **Checks transparency** along each ray path
3. **Marks tiles as visible** if the ray reaches them unobstructed
4. **Remembers explored tiles** automatically
The engine handles all the complex calculations in C++ for optimal performance.
## Visibility States in Detail
### Visible Tiles
- Currently in the player's line of sight
- Rendered at full brightness
- Show current entity positions
### Explored Tiles
- Previously seen but not currently visible
- Rendered darker/muted
- Show remembered terrain but not entities
### Unexplored Tiles
- Never been in the player's FOV
- Rendered as black/invisible
- Complete mystery to the player
## FOV Parameters
You can customize FOV behavior:
```python
# Basic FOV update
entity.update_fov(radius=8)
# The grid's perspective property controls rendering:
grid.perspective = 0 # Use first entity's FOV (player)
grid.perspective = 1 # Use second entity's FOV
grid.perspective = -1 # Omniscient (no FOV, see everything)
```
## Performance Considerations
McRogueFace's C++ FOV implementation is highly optimized:
- Uses efficient shadowcasting algorithm
- Only recalculates when needed
- Handles large maps smoothly
- Automatically culls entities outside FOV
## Visual Polish
The engine automatically handles visual transitions:
- Smooth color changes between visibility states
- Entities fade in/out of view
- Explored areas remain visible but dimmed
## Exercises
1. **Variable Vision**: Give different entities different FOV radii
2. **Light Sources**: Create torches that expand local FOV
3. **Blind Spots**: Add pillars that create interesting shadows
4. **X-Ray Vision**: Temporary power-up to see through walls
## What's Next?
In Part 5, we'll place enemies throughout the dungeon and implement basic interactions. With FOV in place, enemies will appear and disappear as you explore, creating tension and surprise!
Field of View transforms our dungeon from a tactical puzzle into a mysterious world to explore. The fog of war adds atmosphere and gameplay depth that's essential to the roguelike experience.

View File

@ -0,0 +1,334 @@
import mcrfpy
import random
# Color configurations for visibility
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering (0 = first entity = player)
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.fov_radius = 8
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 4"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add monsters in random rooms
for i in range(10):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
# Randomly offset from center
x += random.randint(-2, 2)
y += random.randint(-2, 2)
# Make sure position is walkable
if self.game_map.grid.at(x, y).walkable:
if i % 2 == 0:
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
else:
# Create a troll
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
self.game_map.add_entity(troll)
self.entities.append(troll)
# Initial FOV calculation
self.player.update_fov()
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "v":
# Toggle FOV on/off
if self.game_map.grid.perspective == 0:
self.game_map.grid.perspective = -1 # Omniscient
print("FOV disabled - omniscient view")
else:
self.game_map.grid.perspective = 0 # Player perspective
print("FOV enabled - player perspective")
elif key == "Plus" or key == "Equals":
# Increase FOV radius
self.fov_radius = min(self.fov_radius + 1, 20)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
elif key == "Minus":
# Decrease FOV radius
self.fov_radius = max(self.fov_radius - 1, 3)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Field of View", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# FOV indicator
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
self.fov_text.font_size = 14
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
self.ui.append(self.fov_text)
# Create and run the game
engine = Engine()
print("Part 4: Field of View!")
print("Press V to toggle FOV on/off")
print("Press +/- to adjust FOV radius")

View File

@ -0,0 +1,570 @@
# Part 5 - Placing Enemies and Kicking Them (Harmlessly)
Now that we have Field of View working, it's time to populate our dungeon with enemies! In this part, we'll:
- Place enemies randomly in rooms
- Implement entity-to-entity collision detection
- Create basic interactions (bumping into enemies)
- Set the stage for combat in Part 6
## Enemy Spawning System
First, let's create a system to spawn enemies in our dungeon rooms. We'll avoid placing them in the first room (where the player starts) to give players a safe starting area.
```python
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
import random
number_of_enemies = random.randint(0, max_enemies)
for i in range(number_of_enemies):
# Try to find a valid position
attempts = 10
while attempts > 0:
# Random position within room bounds
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
# Check if position is valid
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
else:
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
game_map.add_entity(enemy)
break
attempts -= 1
```
## Enhanced Collision Detection
We need to improve our collision detection to check for entities, not just walls:
```python
class GameMap:
"""Manages the game world"""
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
# Check boundaries
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
# Check walls
if not self.grid.at(x, y).walkable:
return True
# Check entities
if self.get_blocking_entity_at(x, y):
return True
return False
```
## Action System Introduction
Let's create a simple action system to handle different types of interactions:
```python
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class BumpAction(Action):
"""Action for bumping into something"""
def __init__(self, dx, dy, target=None):
self.dx = dx
self.dy = dy
self.target = target
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
```
## Handling Player Actions
Now let's update our movement handling to support bumping into enemies:
```python
def handle_player_turn(self, action):
"""Process the player's action"""
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# We bumped into something!
print(f"You kick the {target.name} in the shins, much to its annoyance!")
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
# Update message
self.status_text.text = "Exploring the dungeon..."
else:
# Bumped into a wall
self.status_text.text = "Ouch! You bump into a wall."
elif isinstance(action, WaitAction):
self.status_text.text = "You wait..."
```
## Complete Updated Code
Here's the complete `game.py` with enemy placement and interactions:
```python
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
# Try to find a valid position
attempts = 10
while attempts > 0:
# Random position within room bounds
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
# Check if position is valid
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
else:
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 5"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
def handle_player_turn(self, action):
"""Process the player's action"""
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# We bumped into something!
print(f"You kick the {target.name} in the shins, much to its annoyance!")
self.status_text.text = f"You kick the {target.name}!"
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
self.status_text.text = ""
else:
# Bumped into a wall
self.status_text.text = "Blocked!"
elif isinstance(action, WaitAction):
self.status_text.text = "You wait..."
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Placing Enemies", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Status text
self.status_text = mcrfpy.Caption("", 512, 600)
self.status_text.font_size = 18
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
self.ui.append(self.status_text)
# Entity count
entity_count = len(self.entities)
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
count_text.font_size = 14
count_text.fill_color = mcrfpy.Color(150, 150, 255)
self.ui.append(count_text)
# Create and run the game
engine = Engine()
print("Part 5: Placing Enemies!")
print("Try bumping into enemies - combat coming in Part 6!")
```
## Understanding Entity Interactions
### Collision Detection
Our system now checks three things when the player tries to move:
1. **Map boundaries** - Can't move outside the map
2. **Wall tiles** - Can't walk through walls
3. **Blocking entities** - Can't walk through enemies
### The Action System
We've introduced a simple action system that will grow in Part 6:
- `Action` - Base class for all actions
- `MovementAction` - Represents attempted movement
- `WaitAction` - Skip a turn (important for turn-based games)
### Entity Spawning
Enemies are placed randomly in rooms with these rules:
- Never in the first room (player's starting room)
- Random number between 0 and max per room
- 80% orcs, 20% trolls
- Must be placed on walkable, unoccupied tiles
## Visual Feedback
With FOV enabled, enemies will appear and disappear as you explore:
- Enemies in sight are fully visible
- Enemies in explored but dark areas are hidden
- Creates tension and surprise encounters
## Exercises
1. **More Enemy Types**: Add different sprites and names (goblins, skeletons)
2. **Enemy Density**: Adjust spawn rates based on dungeon depth
3. **Special Rooms**: Create rooms with guaranteed enemies or treasures
4. **Better Feedback**: Add sound effects or visual effects for bumping
## What's Next?
In Part 6, we'll transform those harmless kicks into a real combat system! We'll add:
- Health points for all entities
- Damage calculations
- Death and corpses
- Combat messages
- The beginning of a real roguelike!
Right now our enemies are just obstacles. Soon they'll fight back!

View File

@ -0,0 +1,388 @@
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
# Try to find a valid position
attempts = 10
while attempts > 0:
# Random position within room bounds
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
# Check if position is valid
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
else:
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 5"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
def handle_player_turn(self, action):
"""Process the player's action"""
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# We bumped into something!
print(f"You kick the {target.name} in the shins, much to its annoyance!")
self.status_text.text = f"You kick the {target.name}!"
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
self.status_text.text = ""
else:
# Bumped into a wall
self.status_text.text = "Blocked!"
elif isinstance(action, WaitAction):
self.status_text.text = "You wait..."
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Placing Enemies", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Status text
self.status_text = mcrfpy.Caption("", 512, 600)
self.status_text.font_size = 18
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
self.ui.append(self.status_text)
# Entity count
entity_count = len(self.entities)
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
count_text.font_size = 14
count_text.fill_color = mcrfpy.Color(150, 150, 255)
self.ui.append(count_text)
# Create and run the game
engine = Engine()
print("Part 5: Placing Enemies!")
print("Try bumping into enemies - combat coming in Part 6!")

View File

@ -0,0 +1,743 @@
# Part 6 - Doing (and Taking) Some Damage
It's time to turn our harmless kicks into real combat! In this part, we'll implement:
- Health points for all entities
- A damage calculation system
- Death and corpse mechanics
- Combat feedback messages
- The foundation of tactical roguelike combat
## Adding Combat Stats
First, let's enhance our GameObject class with combat capabilities:
```python
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name,
blocks=False, hp=0, defense=0, power=0):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
# Combat stats
self.max_hp = hp
self.hp = hp
self.defense = defense
self.power = power
@property
def is_alive(self):
"""Returns True if this entity can act"""
return self.hp > 0
def take_damage(self, amount):
"""Apply damage to this entity"""
damage = amount - self.defense
if damage > 0:
self.hp -= damage
# Check for death
if self.hp <= 0 and self.hp + damage > 0:
self.die()
return damage
def die(self):
"""Handle entity death"""
if self.name == "Player":
# Player death is special - we'll handle it differently
self.sprite_index = 64 # Stay as @ but change color
self.color = (127, 0, 0) # Dark red
if self._entity:
self._entity.color = mcrfpy.Color(127, 0, 0)
print("You have died!")
else:
# Enemy death
self.sprite_index = 37 # % character for corpse
self.color = (127, 0, 0) # Dark red
self.blocks = False # Corpses don't block
self.name = f"remains of {self.name}"
if self._entity:
self._entity.sprite_index = 37
self._entity.color = mcrfpy.Color(127, 0, 0)
```
## The Combat System
Now let's implement actual combat when entities bump into each other:
```python
class MeleeAction(Action):
"""Action for melee attacks"""
def __init__(self, attacker, target):
self.attacker = attacker
self.target = target
def perform(self):
"""Execute the attack"""
if not self.target.is_alive:
return # Can't attack the dead
damage = self.attacker.power - self.target.defense
if damage > 0:
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
self.target.take_damage(damage)
else:
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
return attack_desc
```
## Entity Factories
Let's create factory functions for consistent entity creation:
```python
def create_player(x, y):
"""Create the player entity"""
return GameObject(
x=x, y=y,
sprite_index=64, # @
color=(255, 255, 255),
name="Player",
blocks=True,
hp=30,
defense=2,
power=5
)
def create_orc(x, y):
"""Create an orc enemy"""
return GameObject(
x=x, y=y,
sprite_index=111, # o
color=(63, 127, 63),
name="Orc",
blocks=True,
hp=10,
defense=0,
power=3
)
def create_troll(x, y):
"""Create a troll enemy"""
return GameObject(
x=x, y=y,
sprite_index=84, # T
color=(0, 127, 0),
name="Troll",
blocks=True,
hp=16,
defense=1,
power=4
)
```
## The Message Log
Combat needs feedback! Let's create a simple message log:
```python
class MessageLog:
"""Manages game messages"""
def __init__(self, max_messages=5):
self.messages = []
self.max_messages = max_messages
def add_message(self, text, color=(255, 255, 255)):
"""Add a message to the log"""
self.messages.append((text, color))
# Keep only recent messages
if len(self.messages) > self.max_messages:
self.messages.pop(0)
def render(self, ui, x, y, line_height=20):
"""Render messages to the UI"""
for i, (text, color) in enumerate(self.messages):
caption = mcrfpy.Caption(text, x, y + i * line_height)
caption.font_size = 14
caption.fill_color = mcrfpy.Color(*color)
ui.append(caption)
```
## Complete Implementation
Here's the complete `game.py` with combat:
```python
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Message colors
COLOR_PLAYER_ATK = (230, 230, 230)
COLOR_ENEMY_ATK = (255, 200, 200)
COLOR_PLAYER_DIE = (255, 100, 100)
COLOR_ENEMY_DIE = (255, 165, 0)
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class MeleeAction(Action):
"""Action for melee attacks"""
def __init__(self, attacker, target):
self.attacker = attacker
self.target = target
def perform(self):
"""Execute the attack"""
if not self.target.is_alive:
return None
damage = self.attacker.power - self.target.defense
if damage > 0:
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
self.target.take_damage(damage)
# Choose color based on attacker
if self.attacker.name == "Player":
color = COLOR_PLAYER_ATK
else:
color = COLOR_ENEMY_ATK
return attack_desc, color
else:
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
return attack_desc, (150, 150, 150)
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name,
blocks=False, hp=0, defense=0, power=0):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
# Combat stats
self.max_hp = hp
self.hp = hp
self.defense = defense
self.power = power
@property
def is_alive(self):
"""Returns True if this entity can act"""
return self.hp > 0
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
def take_damage(self, amount):
"""Apply damage to this entity"""
self.hp -= amount
# Check for death
if self.hp <= 0:
self.die()
def die(self):
"""Handle entity death"""
if self.name == "Player":
# Player death
self.sprite_index = 64 # Stay as @
self.color = (127, 0, 0) # Dark red
if self._entity:
self._entity.color = mcrfpy.Color(127, 0, 0)
else:
# Enemy death
self.sprite_index = 37 # % character for corpse
self.color = (127, 0, 0) # Dark red
self.blocks = False # Corpses don't block
self.name = f"remains of {self.name}"
if self._entity:
self._entity.sprite_index = 37
self._entity.color = mcrfpy.Color(127, 0, 0)
# Entity factories
def create_player(x, y):
"""Create the player entity"""
return GameObject(
x=x, y=y,
sprite_index=64, # @
color=(255, 255, 255),
name="Player",
blocks=True,
hp=30,
defense=2,
power=5
)
def create_orc(x, y):
"""Create an orc enemy"""
return GameObject(
x=x, y=y,
sprite_index=111, # o
color=(63, 127, 63),
name="Orc",
blocks=True,
hp=10,
defense=0,
power=3
)
def create_troll(x, y):
"""Create a troll enemy"""
return GameObject(
x=x, y=y,
sprite_index=84, # T
color=(0, 127, 0),
name="Troll",
blocks=True,
hp=16,
defense=1,
power=4
)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
attempts = 10
while attempts > 0:
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = create_orc(x, y)
else:
enemy = create_troll(x, y)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.messages = [] # Simple message log
self.max_messages = 5
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 6"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def add_message(self, text, color=(255, 255, 255)):
"""Add a message to the log"""
self.messages.append((text, color))
if len(self.messages) > self.max_messages:
self.messages.pop(0)
self.update_message_display()
def update_message_display(self):
"""Update the message display"""
# Clear old messages
for caption in self.message_captions:
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
caption.text = ""
# Display current messages
for i, (text, color) in enumerate(self.messages):
if i < len(self.message_captions):
self.message_captions[i].text = text
self.message_captions[i].fill_color = mcrfpy.Color(*color)
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = create_player(0, 0)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
# Welcome message
self.add_message("Welcome to the dungeon!", (100, 100, 255))
def handle_player_turn(self, action):
"""Process the player's action"""
if not self.player.is_alive:
return
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# Attack!
attack = MeleeAction(self.player, target)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if target died
if not target.is_alive:
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
self.add_message(death_msg, COLOR_ENEMY_DIE)
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
elif isinstance(action, WaitAction):
pass # Do nothing
# Enemy turns
self.handle_enemy_turns()
def handle_enemy_turns(self):
"""Let all enemies take their turn"""
for entity in self.entities:
if entity.is_alive:
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
dx = entity.x - self.player.x
dy = entity.y - self.player.y
distance = abs(dx) + abs(dy)
if distance == 1: # Adjacent to player
attack = MeleeAction(entity, self.player)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if player died
if not self.player.is_alive:
self.add_message("You have died!", COLOR_PLAYER_DIE)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Combat System", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Player stats
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
self.hp_text.font_size = 18
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
self.ui.append(self.hp_text)
# Message log
self.message_captions = []
for i in range(self.max_messages):
caption = mcrfpy.Caption("", 50, 620 + i * 20)
caption.font_size = 14
caption.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(caption)
self.message_captions.append(caption)
# Timer to update HP display
def update_stats(dt):
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
if self.player.hp <= 0:
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
elif self.player.hp < self.player.max_hp // 3:
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
else:
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
mcrfpy.setTimer("update_stats", update_stats, 100)
# Create and run the game
engine = Engine()
print("Part 6: Combat System!")
print("Attack enemies to defeat them, but watch your HP!")

View File

@ -0,0 +1,568 @@
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Message colors
COLOR_PLAYER_ATK = (230, 230, 230)
COLOR_ENEMY_ATK = (255, 200, 200)
COLOR_PLAYER_DIE = (255, 100, 100)
COLOR_ENEMY_DIE = (255, 165, 0)
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class MeleeAction(Action):
"""Action for melee attacks"""
def __init__(self, attacker, target):
self.attacker = attacker
self.target = target
def perform(self):
"""Execute the attack"""
if not self.target.is_alive:
return None
damage = self.attacker.power - self.target.defense
if damage > 0:
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
self.target.take_damage(damage)
# Choose color based on attacker
if self.attacker.name == "Player":
color = COLOR_PLAYER_ATK
else:
color = COLOR_ENEMY_ATK
return attack_desc, color
else:
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
return attack_desc, (150, 150, 150)
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name,
blocks=False, hp=0, defense=0, power=0):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
# Combat stats
self.max_hp = hp
self.hp = hp
self.defense = defense
self.power = power
@property
def is_alive(self):
"""Returns True if this entity can act"""
return self.hp > 0
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
def take_damage(self, amount):
"""Apply damage to this entity"""
self.hp -= amount
# Check for death
if self.hp <= 0:
self.die()
def die(self):
"""Handle entity death"""
if self.name == "Player":
# Player death
self.sprite_index = 64 # Stay as @
self.color = (127, 0, 0) # Dark red
if self._entity:
self._entity.color = mcrfpy.Color(127, 0, 0)
else:
# Enemy death
self.sprite_index = 37 # % character for corpse
self.color = (127, 0, 0) # Dark red
self.blocks = False # Corpses don't block
self.name = f"remains of {self.name}"
if self._entity:
self._entity.sprite_index = 37
self._entity.color = mcrfpy.Color(127, 0, 0)
# Entity factories
def create_player(x, y):
"""Create the player entity"""
return GameObject(
x=x, y=y,
sprite_index=64, # @
color=(255, 255, 255),
name="Player",
blocks=True,
hp=30,
defense=2,
power=5
)
def create_orc(x, y):
"""Create an orc enemy"""
return GameObject(
x=x, y=y,
sprite_index=111, # o
color=(63, 127, 63),
name="Orc",
blocks=True,
hp=10,
defense=0,
power=3
)
def create_troll(x, y):
"""Create a troll enemy"""
return GameObject(
x=x, y=y,
sprite_index=84, # T
color=(0, 127, 0),
name="Troll",
blocks=True,
hp=16,
defense=1,
power=4
)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
attempts = 10
while attempts > 0:
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = create_orc(x, y)
else:
enemy = create_troll(x, y)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.messages = [] # Simple message log
self.max_messages = 5
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 6"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def add_message(self, text, color=(255, 255, 255)):
"""Add a message to the log"""
self.messages.append((text, color))
if len(self.messages) > self.max_messages:
self.messages.pop(0)
self.update_message_display()
def update_message_display(self):
"""Update the message display"""
# Clear old messages
for caption in self.message_captions:
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
caption.text = ""
# Display current messages
for i, (text, color) in enumerate(self.messages):
if i < len(self.message_captions):
self.message_captions[i].text = text
self.message_captions[i].fill_color = mcrfpy.Color(*color)
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = create_player(0, 0)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
# Welcome message
self.add_message("Welcome to the dungeon!", (100, 100, 255))
def handle_player_turn(self, action):
"""Process the player's action"""
if not self.player.is_alive:
return
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# Attack!
attack = MeleeAction(self.player, target)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if target died
if not target.is_alive:
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
self.add_message(death_msg, COLOR_ENEMY_DIE)
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
elif isinstance(action, WaitAction):
pass # Do nothing
# Enemy turns
self.handle_enemy_turns()
def handle_enemy_turns(self):
"""Let all enemies take their turn"""
for entity in self.entities:
if entity.is_alive:
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
dx = entity.x - self.player.x
dy = entity.y - self.player.y
distance = abs(dx) + abs(dy)
if distance == 1: # Adjacent to player
attack = MeleeAction(entity, self.player)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if player died
if not self.player.is_alive:
self.add_message("You have died!", COLOR_PLAYER_DIE)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Combat System", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Player stats
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
self.hp_text.font_size = 18
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
self.ui.append(self.hp_text)
# Message log
self.message_captions = []
for i in range(self.max_messages):
caption = mcrfpy.Caption("", 50, 620 + i * 20)
caption.font_size = 14
caption.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(caption)
self.message_captions.append(caption)
# Timer to update HP display
def update_stats(dt):
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
if self.player.hp <= 0:
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
elif self.player.hp < self.player.max_hp // 3:
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
else:
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
mcrfpy.setTimer("update_stats", update_stats, 100)
# Create and run the game
engine = Engine()
print("Part 6: Combat System!")
print("Attack enemies to defeat them, but watch your HP!")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -90,8 +90,8 @@ void Animation::startEntity(UIEntity* target) {
}
}
else if constexpr (std::is_same_v<T, int>) {
// For entities, we might need to handle sprite_number differently
if (targetProperty == "sprite_number") {
// For entities, we might need to handle sprite_index differently
if (targetProperty == "sprite_index" || targetProperty == "sprite_number") {
startValue = target->sprite.getSpriteIndex();
}
}

View File

@ -5,6 +5,7 @@
#include "UITestScene.h"
#include "Resources.h"
#include "Animation.h"
#include <cmath>
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
{
@ -26,12 +27,18 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
render_target = &headless_renderer->getRenderTarget();
} else {
window = std::make_unique<sf::RenderWindow>();
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close);
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize);
window->setFramerateLimit(60);
render_target = window.get();
}
visible = render_target->getDefaultView();
// Initialize the game view
gameView.setSize(static_cast<float>(gameResolution.x), static_cast<float>(gameResolution.y));
// Use integer center coordinates for pixel-perfect rendering
gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f));
updateViewport();
scene = "uitest";
scenes["uitest"] = new UITestScene(this);
@ -73,19 +80,81 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
GameEngine::~GameEngine()
{
cleanup();
for (auto& [name, scene] : scenes) {
delete scene;
}
}
void GameEngine::cleanup()
{
if (cleaned_up) return;
cleaned_up = true;
// Clear Python references before destroying C++ objects
// Clear all timers (they hold Python callables)
timers.clear();
// Clear McRFPy_API's reference to this game engine
if (McRFPy_API::game == this) {
McRFPy_API::game = nullptr;
}
// Force close the window if it's still open
if (window && window->isOpen()) {
window->close();
}
}
Scene* GameEngine::currentScene() { return scenes[scene]; }
void GameEngine::changeScene(std::string s)
{
/*std::cout << "Current scene is now '" << s << "'\n";*/
if (scenes.find(s) != scenes.end())
scene = s;
changeScene(s, TransitionType::None, 0.0f);
}
void GameEngine::changeScene(std::string sceneName, TransitionType transitionType, float duration)
{
if (scenes.find(sceneName) == scenes.end())
{
std::cout << "Attempted to change to a scene that doesn't exist (`" << sceneName << "`)" << std::endl;
return;
}
if (transitionType == TransitionType::None || duration <= 0.0f)
{
// Immediate scene change
std::string old_scene = scene;
scene = sceneName;
// Trigger Python scene lifecycle events
McRFPy_API::triggerSceneChange(old_scene, sceneName);
}
else
std::cout << "Attempted to change to a scene that doesn't exist (`" << s << "`)" << std::endl;
{
// Start transition
transition.start(transitionType, scene, sceneName, duration);
// Render current scene to texture
sf::RenderTarget* original_target = render_target;
render_target = transition.oldSceneTexture.get();
transition.oldSceneTexture->clear();
currentScene()->render();
transition.oldSceneTexture->display();
// Change to new scene
std::string old_scene = scene;
scene = sceneName;
// Render new scene to texture
render_target = transition.newSceneTexture.get();
transition.newSceneTexture->clear();
currentScene()->render();
transition.newSceneTexture->display();
// Restore original render target and scene
render_target = original_target;
scene = old_scene;
}
}
void GameEngine::quit() { running = false; }
void GameEngine::setPause(bool p) { paused = p; }
@ -106,9 +175,9 @@ void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); }
void GameEngine::setWindowScale(float multiplier)
{
if (!headless && window) {
window->setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling
window->setSize(sf::Vector2u(gameResolution.x * multiplier, gameResolution.y * multiplier));
updateViewport();
}
//window.create(sf::VideoMode(1024 * multiplier, 768 * multiplier), window_title, sf::Style::Titlebar | sf::Style::Close);
}
void GameEngine::run()
@ -119,9 +188,15 @@ void GameEngine::run()
clock.restart();
while (running)
{
// Reset per-frame metrics
metrics.resetPerFrame();
currentScene()->update();
testTimers();
// Update Python scenes
McRFPy_API::updatePythonScenes(frameTime);
// Update animations (only if frameTime is valid)
if (frameTime > 0.0f && frameTime < 1.0f) {
AnimationManager::getInstance().update(frameTime);
@ -133,7 +208,33 @@ void GameEngine::run()
if (!paused)
{
}
currentScene()->render();
// Handle scene transitions
if (transition.type != TransitionType::None)
{
transition.update(frameTime);
if (transition.isComplete())
{
// Transition complete - finalize scene change
scene = transition.toScene;
transition.type = TransitionType::None;
// Trigger Python scene lifecycle events
McRFPy_API::triggerSceneChange(transition.fromScene, transition.toScene);
}
else
{
// Render transition
render_target->clear();
transition.render(*render_target);
}
}
else
{
// Normal scene rendering
currentScene()->render();
}
// Display the frame
if (headless) {
@ -150,8 +251,12 @@ void GameEngine::run()
currentFrame++;
frameTime = clock.restart().asSeconds();
fps = 1 / frameTime;
int whole_fps = (int)fps;
int tenth_fps = int(fps * 100) % 10;
// Update profiling metrics
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
int whole_fps = metrics.fps;
int tenth_fps = (metrics.fps * 10) % 10;
if (!headless && window) {
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
@ -162,6 +267,18 @@ void GameEngine::run()
running = false;
}
}
// Clean up before exiting the run loop
cleanup();
}
std::shared_ptr<PyTimerCallable> GameEngine::getTimer(const std::string& name)
{
auto it = timers.find(name);
if (it != timers.end()) {
return it->second;
}
return nullptr;
}
void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
@ -208,9 +325,13 @@ void GameEngine::processEvent(const sf::Event& event)
int actionCode = 0;
if (event.type == sf::Event::Closed) { running = false; return; }
// TODO: add resize event to Scene to react; call it after constructor too, maybe
// Handle window resize events
else if (event.type == sf::Event::Resized) {
return; // 7DRL short circuit. Resizing manually disabled
// Update the viewport to handle the new window size
updateViewport();
// Notify Python scenes about the resize
McRFPy_API::triggerResize(event.size.width, event.size.height);
}
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
@ -270,3 +391,123 @@ std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(s
if (scenes.count(target) == 0) return NULL;
return scenes[target]->ui_elements;
}
void GameEngine::setWindowTitle(const std::string& title)
{
window_title = title;
if (!headless && window) {
window->setTitle(title);
}
}
void GameEngine::setVSync(bool enabled)
{
vsync_enabled = enabled;
if (!headless && window) {
window->setVerticalSyncEnabled(enabled);
}
}
void GameEngine::setFramerateLimit(unsigned int limit)
{
framerate_limit = limit;
if (!headless && window) {
window->setFramerateLimit(limit);
}
}
void GameEngine::setGameResolution(unsigned int width, unsigned int height) {
gameResolution = sf::Vector2u(width, height);
gameView.setSize(static_cast<float>(width), static_cast<float>(height));
// Use integer center coordinates for pixel-perfect rendering
gameView.setCenter(std::floor(width / 2.0f), std::floor(height / 2.0f));
updateViewport();
}
void GameEngine::setViewportMode(ViewportMode mode) {
viewportMode = mode;
updateViewport();
}
std::string GameEngine::getViewportModeString() const {
switch (viewportMode) {
case ViewportMode::Center: return "center";
case ViewportMode::Stretch: return "stretch";
case ViewportMode::Fit: return "fit";
}
return "unknown";
}
void GameEngine::updateViewport() {
if (!render_target) return;
auto windowSize = render_target->getSize();
switch (viewportMode) {
case ViewportMode::Center: {
// 1:1 pixels, centered in window
float viewportWidth = std::min(static_cast<float>(gameResolution.x), static_cast<float>(windowSize.x));
float viewportHeight = std::min(static_cast<float>(gameResolution.y), static_cast<float>(windowSize.y));
// Floor offsets to ensure integer pixel alignment
float offsetX = std::floor((windowSize.x - viewportWidth) / 2.0f);
float offsetY = std::floor((windowSize.y - viewportHeight) / 2.0f);
gameView.setViewport(sf::FloatRect(
offsetX / windowSize.x,
offsetY / windowSize.y,
viewportWidth / windowSize.x,
viewportHeight / windowSize.y
));
break;
}
case ViewportMode::Stretch: {
// Fill entire window, ignore aspect ratio
gameView.setViewport(sf::FloatRect(0, 0, 1, 1));
break;
}
case ViewportMode::Fit: {
// Maintain aspect ratio with black bars
float windowAspect = static_cast<float>(windowSize.x) / windowSize.y;
float gameAspect = static_cast<float>(gameResolution.x) / gameResolution.y;
float viewportWidth, viewportHeight;
float offsetX = 0, offsetY = 0;
if (windowAspect > gameAspect) {
// Window is wider - black bars on sides
// Calculate viewport size in pixels and floor for pixel-perfect scaling
float pixelHeight = static_cast<float>(windowSize.y);
float pixelWidth = std::floor(pixelHeight * gameAspect);
viewportHeight = 1.0f;
viewportWidth = pixelWidth / windowSize.x;
offsetX = (1.0f - viewportWidth) / 2.0f;
} else {
// Window is taller - black bars on top/bottom
// Calculate viewport size in pixels and floor for pixel-perfect scaling
float pixelWidth = static_cast<float>(windowSize.x);
float pixelHeight = std::floor(pixelWidth / gameAspect);
viewportWidth = 1.0f;
viewportHeight = pixelHeight / windowSize.y;
offsetY = (1.0f - viewportHeight) / 2.0f;
}
gameView.setViewport(sf::FloatRect(offsetX, offsetY, viewportWidth, viewportHeight));
break;
}
}
// Apply the view
render_target->setView(gameView);
}
sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const {
if (!render_target) return windowPos;
// Convert window coordinates to game coordinates using the view
return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView);
}

View File

@ -8,10 +8,20 @@
#include "PyCallable.h"
#include "McRogueFaceConfig.h"
#include "HeadlessRenderer.h"
#include "SceneTransition.h"
#include <memory>
class GameEngine
{
public:
// Viewport modes (moved here so private section can use it)
enum class ViewportMode {
Center, // 1:1 pixels, viewport centered in window
Stretch, // viewport size = window size, doesn't respect aspect ratio
Fit // maintains original aspect ratio, leaves black bars
};
private:
std::unique_ptr<sf::RenderWindow> window;
std::unique_ptr<HeadlessRenderer> headless_renderer;
sf::RenderTarget* render_target;
@ -28,19 +38,70 @@ class GameEngine
bool headless = false;
McRogueFaceConfig config;
bool cleaned_up = false;
// Window state tracking
bool vsync_enabled = false;
unsigned int framerate_limit = 60;
// Scene transition state
SceneTransition transition;
// Viewport system
sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution
sf::View gameView; // View for the game content
ViewportMode viewportMode = ViewportMode::Fit;
void updateViewport();
sf::Clock runtime;
//std::map<std::string, Timer> timers;
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
void testTimers();
public:
sf::Clock runtime;
//std::map<std::string, Timer> timers;
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
std::string scene;
// Profiling metrics
struct ProfilingMetrics {
float frameTime = 0.0f; // Current frame time in milliseconds
float avgFrameTime = 0.0f; // Average frame time over last N frames
int fps = 0; // Frames per second
int drawCalls = 0; // Draw calls per frame
int uiElements = 0; // Number of UI elements rendered
int visibleElements = 0; // Number of visible elements
// Frame time history for averaging
static constexpr int HISTORY_SIZE = 60;
float frameTimeHistory[HISTORY_SIZE] = {0};
int historyIndex = 0;
void updateFrameTime(float deltaMs) {
frameTime = deltaMs;
frameTimeHistory[historyIndex] = deltaMs;
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
// Calculate average
float sum = 0.0f;
for (int i = 0; i < HISTORY_SIZE; ++i) {
sum += frameTimeHistory[i];
}
avgFrameTime = sum / HISTORY_SIZE;
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
}
void resetPerFrame() {
drawCalls = 0;
uiElements = 0;
visibleElements = 0;
}
} metrics;
GameEngine();
GameEngine(const McRogueFaceConfig& cfg);
~GameEngine();
Scene* currentScene();
void changeScene(std::string);
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
void createScene(std::string);
void quit();
void setPause(bool);
@ -50,14 +111,32 @@ public:
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
void run();
void sUserInput();
void cleanup(); // Clean up Python references before destruction
int getFrame() { return currentFrame; }
float getFrameTime() { return frameTime; }
sf::View getView() { return visible; }
void manageTimer(std::string, PyObject*, int);
std::shared_ptr<PyTimerCallable> getTimer(const std::string& name);
void setWindowScale(float);
bool isHeadless() const { return headless; }
void processEvent(const sf::Event& event);
// Window property accessors
const std::string& getWindowTitle() const { return window_title; }
void setWindowTitle(const std::string& title);
bool getVSync() const { return vsync_enabled; }
void setVSync(bool enabled);
unsigned int getFramerateLimit() const { return framerate_limit; }
void setFramerateLimit(unsigned int limit);
// Viewport system
void setGameResolution(unsigned int width, unsigned int height);
sf::Vector2u getGameResolution() const { return gameResolution; }
void setViewportMode(ViewportMode mode);
ViewportMode getViewportMode() const { return viewportMode; }
std::string getViewportModeString() const;
sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) const;
// global textures for scripts to access
std::vector<IndexTexture> textures;

View File

@ -1,17 +1,23 @@
#include "McRFPy_API.h"
#include "McRFPy_Automation.h"
#include "McRFPy_Libtcod.h"
#include "platform.h"
#include "PyAnimation.h"
#include "PyDrawable.h"
#include "PyTimer.h"
#include "PyWindow.h"
#include "PySceneObject.h"
#include "GameEngine.h"
#include "UI.h"
#include "Resources.h"
#include "PyScene.h"
#include <filesystem>
#include <cstring>
#include <libtcod.h>
std::vector<sf::SoundBuffer> McRFPy_API::soundbuffers;
sf::Music McRFPy_API::music;
sf::Sound McRFPy_API::sfx;
std::vector<sf::SoundBuffer>* McRFPy_API::soundbuffers = nullptr;
sf::Music* McRFPy_API::music = nullptr;
sf::Sound* McRFPy_API::sfx = nullptr;
std::shared_ptr<PyFont> McRFPy_API::default_font;
std::shared_ptr<PyTexture> McRFPy_API::default_texture;
@ -20,32 +26,189 @@ PyObject* McRFPy_API::mcrf_module;
static PyMethodDef mcrfpyMethods[] = {
{"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS, "(filename)"},
{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS, "(filename)"},
{"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS, "(int)"},
{"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS, "(int)"},
{"playSound", McRFPy_API::_playSound, METH_VARARGS, "(int)"},
{"getMusicVolume", McRFPy_API::_getMusicVolume, METH_VARARGS, ""},
{"getSoundVolume", McRFPy_API::_getSoundVolume, METH_VARARGS, ""},
{"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS,
"createSoundBuffer(filename: str) -> int\n\n"
"Load a sound effect from a file and return its buffer ID.\n\n"
"Args:\n"
" filename: Path to the sound file (WAV, OGG, FLAC)\n\n"
"Returns:\n"
" int: Buffer ID for use with playSound()\n\n"
"Raises:\n"
" RuntimeError: If the file cannot be loaded"},
{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS,
"loadMusic(filename: str) -> None\n\n"
"Load and immediately play background music from a file.\n\n"
"Args:\n"
" filename: Path to the music file (WAV, OGG, FLAC)\n\n"
"Note:\n"
" Only one music track can play at a time. Loading new music stops the current track."},
{"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS,
"setMusicVolume(volume: int) -> None\n\n"
"Set the global music volume.\n\n"
"Args:\n"
" volume: Volume level from 0 (silent) to 100 (full volume)"},
{"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS,
"setSoundVolume(volume: int) -> None\n\n"
"Set the global sound effects volume.\n\n"
"Args:\n"
" volume: Volume level from 0 (silent) to 100 (full volume)"},
{"playSound", McRFPy_API::_playSound, METH_VARARGS,
"playSound(buffer_id: int) -> None\n\n"
"Play a sound effect using a previously loaded buffer.\n\n"
"Args:\n"
" buffer_id: Sound buffer ID returned by createSoundBuffer()\n\n"
"Raises:\n"
" RuntimeError: If the buffer ID is invalid"},
{"getMusicVolume", McRFPy_API::_getMusicVolume, METH_NOARGS,
"getMusicVolume() -> int\n\n"
"Get the current music volume level.\n\n"
"Returns:\n"
" int: Current volume (0-100)"},
{"getSoundVolume", McRFPy_API::_getSoundVolume, METH_NOARGS,
"getSoundVolume() -> int\n\n"
"Get the current sound effects volume level.\n\n"
"Returns:\n"
" int: Current volume (0-100)"},
{"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS, "sceneUI(scene) - Returns a list of UI elements"},
{"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS,
"sceneUI(scene: str = None) -> list\n\n"
"Get all UI elements for a scene.\n\n"
"Args:\n"
" scene: Scene name. If None, uses current scene\n\n"
"Returns:\n"
" list: All UI elements (Frame, Caption, Sprite, Grid) in the scene\n\n"
"Raises:\n"
" KeyError: If the specified scene doesn't exist"},
{"currentScene", McRFPy_API::_currentScene, METH_VARARGS, "currentScene() - Current scene's name. Returns a string"},
{"setScene", McRFPy_API::_setScene, METH_VARARGS, "setScene(scene) - transition to a different scene"},
{"createScene", McRFPy_API::_createScene, METH_VARARGS, "createScene(scene) - create a new blank scene with given name"},
{"keypressScene", McRFPy_API::_keypressScene, METH_VARARGS, "keypressScene(callable) - assign a callable object to the current scene receive keypress events"},
{"currentScene", McRFPy_API::_currentScene, METH_NOARGS,
"currentScene() -> str\n\n"
"Get the name of the currently active scene.\n\n"
"Returns:\n"
" str: Name of the current scene"},
{"setScene", McRFPy_API::_setScene, METH_VARARGS,
"setScene(scene: str, transition: str = None, duration: float = 0.0) -> None\n\n"
"Switch to a different scene with optional transition effect.\n\n"
"Args:\n"
" scene: Name of the scene to switch to\n"
" transition: Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')\n"
" duration: Transition duration in seconds (default: 0.0 for instant)\n\n"
"Raises:\n"
" KeyError: If the scene doesn't exist\n"
" ValueError: If the transition type is invalid"},
{"createScene", McRFPy_API::_createScene, METH_VARARGS,
"createScene(name: str) -> None\n\n"
"Create a new empty scene.\n\n"
"Args:\n"
" name: Unique name for the new scene\n\n"
"Raises:\n"
" ValueError: If a scene with this name already exists\n\n"
"Note:\n"
" The scene is created but not made active. Use setScene() to switch to it."},
{"keypressScene", McRFPy_API::_keypressScene, METH_VARARGS,
"keypressScene(handler: callable) -> None\n\n"
"Set the keyboard event handler for the current scene.\n\n"
"Args:\n"
" handler: Callable that receives (key_name: str, is_pressed: bool)\n\n"
"Example:\n"
" def on_key(key, pressed):\n"
" if key == 'A' and pressed:\n"
" print('A key pressed')\n"
" mcrfpy.keypressScene(on_key)"},
{"setTimer", McRFPy_API::_setTimer, METH_VARARGS,
"setTimer(name: str, handler: callable, interval: int) -> None\n\n"
"Create or update a recurring timer.\n\n"
"Args:\n"
" name: Unique identifier for the timer\n"
" handler: Function called with (runtime: float) parameter\n"
" interval: Time between calls in milliseconds\n\n"
"Note:\n"
" If a timer with this name exists, it will be replaced.\n"
" The handler receives the total runtime in seconds as its argument."},
{"delTimer", McRFPy_API::_delTimer, METH_VARARGS,
"delTimer(name: str) -> None\n\n"
"Stop and remove a timer.\n\n"
"Args:\n"
" name: Timer identifier to remove\n\n"
"Note:\n"
" No error is raised if the timer doesn't exist."},
{"exit", McRFPy_API::_exit, METH_NOARGS,
"exit() -> None\n\n"
"Cleanly shut down the game engine and exit the application.\n\n"
"Note:\n"
" This immediately closes the window and terminates the program."},
{"setScale", McRFPy_API::_setScale, METH_VARARGS,
"setScale(multiplier: float) -> None\n\n"
"Scale the game window size.\n\n"
"Args:\n"
" multiplier: Scale factor (e.g., 2.0 for double size)\n\n"
"Note:\n"
" The internal resolution remains 1024x768, but the window is scaled.\n"
" This is deprecated - use Window.resolution instead."},
{"find", McRFPy_API::_find, METH_VARARGS,
"find(name: str, scene: str = None) -> UIDrawable | None\n\n"
"Find the first UI element with the specified name.\n\n"
"Args:\n"
" name: Exact name to search for\n"
" scene: Scene to search in (default: current scene)\n\n"
"Returns:\n"
" Frame, Caption, Sprite, Grid, or Entity if found; None otherwise\n\n"
"Note:\n"
" Searches scene UI elements and entities within grids."},
{"findAll", McRFPy_API::_findAll, METH_VARARGS,
"findAll(pattern: str, scene: str = None) -> list\n\n"
"Find all UI elements matching a name pattern.\n\n"
"Args:\n"
" pattern: Name pattern with optional wildcards (* matches any characters)\n"
" scene: Scene to search in (default: current scene)\n\n"
"Returns:\n"
" list: All matching UI elements and entities\n\n"
"Example:\n"
" findAll('enemy*') # Find all elements starting with 'enemy'\n"
" findAll('*_button') # Find all elements ending with '_button'"},
{"getMetrics", McRFPy_API::_getMetrics, METH_NOARGS,
"getMetrics() -> dict\n\n"
"Get current performance metrics.\n\n"
"Returns:\n"
" dict: Performance data with keys:\n"
" - frame_time: Last frame duration in seconds\n"
" - avg_frame_time: Average frame time\n"
" - fps: Frames per second\n"
" - draw_calls: Number of draw calls\n"
" - ui_elements: Total UI element count\n"
" - visible_elements: Visible element count\n"
" - current_frame: Frame counter\n"
" - runtime: Total runtime in seconds"},
{"setTimer", McRFPy_API::_setTimer, METH_VARARGS, "setTimer(name:str, callable:object, interval:int) - callable will be called with args (runtime:float) every `interval` milliseconds"},
{"delTimer", McRFPy_API::_delTimer, METH_VARARGS, "delTimer(name:str) - stop calling the timer labelled with `name`"},
{"exit", McRFPy_API::_exit, METH_VARARGS, "exit() - close down the game engine"},
{"setScale", McRFPy_API::_setScale, METH_VARARGS, "setScale(multiplier:float) - resize the window (still 1024x768, but bigger)"},
{NULL, NULL, 0, NULL}
};
static PyModuleDef mcrfpyModule = {
PyModuleDef_HEAD_INIT, /* m_base - Always initialize this member to PyModuleDef_HEAD_INIT. */
"mcrfpy", /* m_name */
NULL, /* m_doc - Docstring for the module; usually a docstring variable created with PyDoc_STRVAR is used. */
PyDoc_STR("McRogueFace Python API\\n\\n"
"Core game engine interface for creating roguelike games with Python.\\n\\n"
"This 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\\n"
"Example:\\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"),
-1, /* m_size - Setting m_size to -1 means that the module does not support sub-interpreters, because it has global state. */
mcrfpyMethods, /* m_methods */
NULL, /* m_slots - An array of slot definitions ... When using single-phase initialization, m_slots must be NULL. */
@ -69,6 +232,9 @@ PyObject* PyInit_mcrfpy()
/*SFML exposed types*/
&PyColorType, /*&PyLinkedColorType,*/ &PyFontType, &PyTextureType, &PyVectorType,
/*Base classes*/
&PyDrawableType,
/*UI widgets*/
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
@ -81,7 +247,26 @@ PyObject* PyInit_mcrfpy()
/*animation*/
&PyAnimationType,
/*timer*/
&PyTimerType,
/*window singleton*/
&PyWindowType,
/*scene class*/
&PySceneType,
nullptr};
// Set up PyWindowType methods and getsetters before PyType_Ready
PyWindowType.tp_methods = PyWindow::methods;
PyWindowType.tp_getset = PyWindow::getsetters;
// Set up PySceneType methods and getsetters
PySceneType.tp_methods = PySceneClass::methods;
PySceneType.tp_getset = PySceneClass::getsetters;
int i = 0;
auto t = pytypes[i];
while (t != nullptr)
@ -100,11 +285,25 @@ PyObject* PyInit_mcrfpy()
// Add default_font and default_texture to module
McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf");
McRFPy_API::default_texture = std::make_shared<PyTexture>("assets/kenney_tinydungeon.png", 16, 16);
//PyModule_AddObject(m, "default_font", McRFPy_API::default_font->pyObject());
//PyModule_AddObject(m, "default_texture", McRFPy_API::default_texture->pyObject());
// These will be set later when the window is created
PyModule_AddObject(m, "default_font", Py_None);
PyModule_AddObject(m, "default_texture", Py_None);
// Add TCOD FOV algorithm constants
PyModule_AddIntConstant(m, "FOV_BASIC", FOV_BASIC);
PyModule_AddIntConstant(m, "FOV_DIAMOND", FOV_DIAMOND);
PyModule_AddIntConstant(m, "FOV_SHADOW", FOV_SHADOW);
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0);
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1);
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2);
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3);
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4);
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5);
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6);
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7);
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8);
PyModule_AddIntConstant(m, "FOV_RESTRICTIVE", FOV_RESTRICTIVE);
// Add automation submodule
PyObject* automation_module = McRFPy_Automation::init_automation_module();
if (automation_module != NULL) {
@ -115,6 +314,16 @@ PyObject* PyInit_mcrfpy()
PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module);
}
// Add libtcod submodule
PyObject* libtcod_module = McRFPy_Libtcod::init_libtcod_module();
if (libtcod_module != NULL) {
PyModule_AddObject(m, "libtcod", libtcod_module);
// Also add to sys.modules for proper import behavior
PyObject* sys_modules = PyImport_GetModuleDict();
PyDict_SetItemString(sys_modules, "mcrfpy.libtcod", libtcod_module);
}
//McRFPy_API::mcrf_module = m;
return m;
}
@ -138,6 +347,11 @@ PyStatus init_python(const char *program_name)
PyConfig_InitIsolatedConfig(&config);
config.dev_mode = 0;
// Configure UTF-8 for stdio
PyConfig_SetString(&config, &config.stdio_encoding, L"UTF-8");
PyConfig_SetString(&config, &config.stdio_errors, L"surrogateescape");
config.configure_c_stdio = 1;
PyConfig_SetBytesString(&config, &config.home,
narrow_string(executable_path() + L"/lib/Python").c_str());
@ -184,6 +398,11 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, in
PyConfig pyconfig;
PyConfig_InitIsolatedConfig(&pyconfig);
// Configure UTF-8 for stdio
PyConfig_SetString(&pyconfig, &pyconfig.stdio_encoding, L"UTF-8");
PyConfig_SetString(&pyconfig, &pyconfig.stdio_errors, L"surrogateescape");
pyconfig.configure_c_stdio = 1;
// CRITICAL: Pass actual command line arguments to Python
status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
if (PyStatus_Exception(status)) {
@ -313,17 +532,49 @@ void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv
void McRFPy_API::executeScript(std::string filename)
{
FILE* PScriptFile = fopen(filename.c_str(), "r");
std::filesystem::path script_path(filename);
// If the path is relative and the file doesn't exist, try resolving it relative to the executable
if (script_path.is_relative() && !std::filesystem::exists(script_path)) {
// Get the directory where the executable is located using platform-specific function
std::wstring exe_dir_w = executable_path();
std::filesystem::path exe_dir(exe_dir_w);
// Try the script path relative to the executable directory
std::filesystem::path resolved_path = exe_dir / script_path;
if (std::filesystem::exists(resolved_path)) {
script_path = resolved_path;
}
}
FILE* PScriptFile = fopen(script_path.string().c_str(), "r");
if(PScriptFile) {
std::cout << "Before PyRun_SimpleFile" << std::endl;
PyRun_SimpleFile(PScriptFile, filename.c_str());
std::cout << "After PyRun_SimpleFile" << std::endl;
PyRun_SimpleFile(PScriptFile, script_path.string().c_str());
fclose(PScriptFile);
} else {
std::cout << "Failed to open script: " << script_path.string() << std::endl;
}
}
void McRFPy_API::api_shutdown()
{
// Clean up audio resources in correct order
if (sfx) {
sfx->stop();
delete sfx;
sfx = nullptr;
}
if (music) {
music->stop();
delete music;
music = nullptr;
}
if (soundbuffers) {
soundbuffers->clear();
delete soundbuffers;
soundbuffers = nullptr;
}
Py_Finalize();
}
@ -358,25 +609,29 @@ PyObject* McRFPy_API::_refreshFov(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_createSoundBuffer(PyObject* self, PyObject* args) {
const char *fn_cstr;
if (!PyArg_ParseTuple(args, "s", &fn_cstr)) return NULL;
// Initialize soundbuffers if needed
if (!McRFPy_API::soundbuffers) {
McRFPy_API::soundbuffers = new std::vector<sf::SoundBuffer>();
}
auto b = sf::SoundBuffer();
b.loadFromFile(fn_cstr);
McRFPy_API::soundbuffers.push_back(b);
McRFPy_API::soundbuffers->push_back(b);
Py_INCREF(Py_None);
return Py_None;
}
PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
const char *fn_cstr;
PyObject* loop_obj;
PyObject* loop_obj = Py_False;
if (!PyArg_ParseTuple(args, "s|O", &fn_cstr, &loop_obj)) return NULL;
McRFPy_API::music.stop();
// get params for sf::Music initialization
//sf::InputSoundFile file;
//file.openFromFile(fn_cstr);
McRFPy_API::music.openFromFile(fn_cstr);
McRFPy_API::music.setLoop(PyObject_IsTrue(loop_obj));
//McRFPy_API::music.initialize(file.getChannelCount(), file.getSampleRate());
McRFPy_API::music.play();
// Initialize music if needed
if (!McRFPy_API::music) {
McRFPy_API::music = new sf::Music();
}
McRFPy_API::music->stop();
McRFPy_API::music->openFromFile(fn_cstr);
McRFPy_API::music->setLoop(PyObject_IsTrue(loop_obj));
McRFPy_API::music->play();
Py_INCREF(Py_None);
return Py_None;
}
@ -384,7 +639,10 @@ PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) {
int vol;
if (!PyArg_ParseTuple(args, "i", &vol)) return NULL;
McRFPy_API::music.setVolume(vol);
if (!McRFPy_API::music) {
McRFPy_API::music = new sf::Music();
}
McRFPy_API::music->setVolume(vol);
Py_INCREF(Py_None);
return Py_None;
}
@ -392,7 +650,10 @@ PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) {
float vol;
if (!PyArg_ParseTuple(args, "f", &vol)) return NULL;
McRFPy_API::sfx.setVolume(vol);
if (!McRFPy_API::sfx) {
McRFPy_API::sfx = new sf::Sound();
}
McRFPy_API::sfx->setVolume(vol);
Py_INCREF(Py_None);
return Py_None;
}
@ -400,20 +661,29 @@ PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_playSound(PyObject* self, PyObject* args) {
float index;
if (!PyArg_ParseTuple(args, "f", &index)) return NULL;
if (index >= McRFPy_API::soundbuffers.size()) return NULL;
McRFPy_API::sfx.stop();
McRFPy_API::sfx.setBuffer(McRFPy_API::soundbuffers[index]);
McRFPy_API::sfx.play();
if (!McRFPy_API::soundbuffers || index >= McRFPy_API::soundbuffers->size()) return NULL;
if (!McRFPy_API::sfx) {
McRFPy_API::sfx = new sf::Sound();
}
McRFPy_API::sfx->stop();
McRFPy_API::sfx->setBuffer((*McRFPy_API::soundbuffers)[index]);
McRFPy_API::sfx->play();
Py_INCREF(Py_None);
return Py_None;
}
PyObject* McRFPy_API::_getMusicVolume(PyObject* self, PyObject* args) {
return Py_BuildValue("f", McRFPy_API::music.getVolume());
if (!McRFPy_API::music) {
return Py_BuildValue("f", 0.0f);
}
return Py_BuildValue("f", McRFPy_API::music->getVolume());
}
PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) {
return Py_BuildValue("f", McRFPy_API::sfx.getVolume());
if (!McRFPy_API::sfx) {
return Py_BuildValue("f", 0.0f);
}
return Py_BuildValue("f", McRFPy_API::sfx->getVolume());
}
// Removed deprecated player_input, computerTurn, playerTurn functions
@ -466,8 +736,24 @@ PyObject* McRFPy_API::_currentScene(PyObject* self, PyObject* args) {
PyObject* McRFPy_API::_setScene(PyObject* self, PyObject* args) {
const char* newscene;
if (!PyArg_ParseTuple(args, "s", &newscene)) return NULL;
game->changeScene(newscene);
const char* transition_str = nullptr;
float duration = 0.0f;
// Parse arguments: scene name, optional transition type, optional duration
if (!PyArg_ParseTuple(args, "s|sf", &newscene, &transition_str, &duration)) return NULL;
// Map transition string to enum
TransitionType transition_type = TransitionType::None;
if (transition_str) {
std::string trans(transition_str);
if (trans == "fade") transition_type = TransitionType::Fade;
else if (trans == "slide_left") transition_type = TransitionType::SlideLeft;
else if (trans == "slide_right") transition_type = TransitionType::SlideRight;
else if (trans == "slide_up") transition_type = TransitionType::SlideUp;
else if (trans == "slide_down") transition_type = TransitionType::SlideDown;
}
game->changeScene(newscene, transition_type, duration);
Py_INCREF(Py_None);
return Py_None;
}
@ -552,3 +838,283 @@ void McRFPy_API::markSceneNeedsSort() {
}
}
}
// Helper function to check if a name matches a pattern with wildcards
static bool name_matches_pattern(const std::string& name, const std::string& pattern) {
if (pattern.find('*') == std::string::npos) {
// No wildcards, exact match
return name == pattern;
}
// Simple wildcard matching - * matches any sequence
size_t name_pos = 0;
size_t pattern_pos = 0;
while (pattern_pos < pattern.length() && name_pos < name.length()) {
if (pattern[pattern_pos] == '*') {
// Skip consecutive stars
while (pattern_pos < pattern.length() && pattern[pattern_pos] == '*') {
pattern_pos++;
}
if (pattern_pos == pattern.length()) {
// Pattern ends with *, matches rest of name
return true;
}
// Find next non-star character in pattern
char next_char = pattern[pattern_pos];
while (name_pos < name.length() && name[name_pos] != next_char) {
name_pos++;
}
} else if (pattern[pattern_pos] == name[name_pos]) {
pattern_pos++;
name_pos++;
} else {
return false;
}
}
// Skip trailing stars in pattern
while (pattern_pos < pattern.length() && pattern[pattern_pos] == '*') {
pattern_pos++;
}
return pattern_pos == pattern.length() && name_pos == name.length();
}
// Helper to recursively search a collection for named elements
static void find_in_collection(std::vector<std::shared_ptr<UIDrawable>>* collection, const std::string& pattern,
bool find_all, PyObject* results) {
if (!collection) return;
for (auto& drawable : *collection) {
if (!drawable) continue;
// Check this element's name
if (name_matches_pattern(drawable->name, pattern)) {
// Convert to Python object using RET_PY_INSTANCE logic
PyObject* py_obj = nullptr;
switch (drawable->derived_type()) {
case PyObjectsEnum::UIFRAME: {
auto frame = std::static_pointer_cast<UIFrame>(drawable);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (o) {
o->data = frame;
py_obj = (PyObject*)o;
}
break;
}
case PyObjectsEnum::UICAPTION: {
auto caption = std::static_pointer_cast<UICaption>(drawable);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
auto o = (PyUICaptionObject*)type->tp_alloc(type, 0);
if (o) {
o->data = caption;
py_obj = (PyObject*)o;
}
break;
}
case PyObjectsEnum::UISPRITE: {
auto sprite = std::static_pointer_cast<UISprite>(drawable);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
auto o = (PyUISpriteObject*)type->tp_alloc(type, 0);
if (o) {
o->data = sprite;
py_obj = (PyObject*)o;
}
break;
}
case PyObjectsEnum::UIGRID: {
auto grid = std::static_pointer_cast<UIGrid>(drawable);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
auto o = (PyUIGridObject*)type->tp_alloc(type, 0);
if (o) {
o->data = grid;
py_obj = (PyObject*)o;
}
break;
}
default:
break;
}
if (py_obj) {
if (find_all) {
PyList_Append(results, py_obj);
Py_DECREF(py_obj);
} else {
// For find (not findAll), we store in results and return early
PyList_Append(results, py_obj);
Py_DECREF(py_obj);
return;
}
}
}
// Recursively search in Frame children
if (drawable->derived_type() == PyObjectsEnum::UIFRAME) {
auto frame = std::static_pointer_cast<UIFrame>(drawable);
find_in_collection(frame->children.get(), pattern, find_all, results);
if (!find_all && PyList_Size(results) > 0) {
return; // Found one, stop searching
}
}
}
}
// Also search Grid entities
static void find_in_grid_entities(UIGrid* grid, const std::string& pattern,
bool find_all, PyObject* results) {
if (!grid || !grid->entities) return;
for (auto& entity : *grid->entities) {
if (!entity) continue;
// Entities delegate name to their sprite
if (name_matches_pattern(entity->sprite.name, pattern)) {
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
if (o) {
o->data = entity;
PyObject* py_obj = (PyObject*)o;
if (find_all) {
PyList_Append(results, py_obj);
Py_DECREF(py_obj);
} else {
PyList_Append(results, py_obj);
Py_DECREF(py_obj);
return;
}
}
}
}
}
PyObject* McRFPy_API::_find(PyObject* self, PyObject* args) {
const char* name;
const char* scene_name = nullptr;
if (!PyArg_ParseTuple(args, "s|s", &name, &scene_name)) {
return NULL;
}
PyObject* results = PyList_New(0);
// Get the UI elements to search
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;
if (scene_name) {
// Search specific scene
ui_elements = game->scene_ui(scene_name);
if (!ui_elements) {
PyErr_Format(PyExc_ValueError, "Scene '%s' not found", scene_name);
Py_DECREF(results);
return NULL;
}
} else {
// Search current scene
Scene* current = game->currentScene();
if (!current) {
PyErr_SetString(PyExc_RuntimeError, "No current scene");
Py_DECREF(results);
return NULL;
}
ui_elements = current->ui_elements;
}
// Search the scene's UI elements
find_in_collection(ui_elements.get(), name, false, results);
// Also search all grids in the scene for entities
if (PyList_Size(results) == 0 && ui_elements) {
for (auto& drawable : *ui_elements) {
if (drawable && drawable->derived_type() == PyObjectsEnum::UIGRID) {
auto grid = std::static_pointer_cast<UIGrid>(drawable);
find_in_grid_entities(grid.get(), name, false, results);
if (PyList_Size(results) > 0) break;
}
}
}
// Return the first result or None
if (PyList_Size(results) > 0) {
PyObject* result = PyList_GetItem(results, 0);
Py_INCREF(result);
Py_DECREF(results);
return result;
}
Py_DECREF(results);
Py_RETURN_NONE;
}
PyObject* McRFPy_API::_findAll(PyObject* self, PyObject* args) {
const char* pattern;
const char* scene_name = nullptr;
if (!PyArg_ParseTuple(args, "s|s", &pattern, &scene_name)) {
return NULL;
}
PyObject* results = PyList_New(0);
// Get the UI elements to search
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;
if (scene_name) {
// Search specific scene
ui_elements = game->scene_ui(scene_name);
if (!ui_elements) {
PyErr_Format(PyExc_ValueError, "Scene '%s' not found", scene_name);
Py_DECREF(results);
return NULL;
}
} else {
// Search current scene
Scene* current = game->currentScene();
if (!current) {
PyErr_SetString(PyExc_RuntimeError, "No current scene");
Py_DECREF(results);
return NULL;
}
ui_elements = current->ui_elements;
}
// Search the scene's UI elements
find_in_collection(ui_elements.get(), pattern, true, results);
// Also search all grids in the scene for entities
if (ui_elements) {
for (auto& drawable : *ui_elements) {
if (drawable && drawable->derived_type() == PyObjectsEnum::UIGRID) {
auto grid = std::static_pointer_cast<UIGrid>(drawable);
find_in_grid_entities(grid.get(), pattern, true, results);
}
}
}
return results;
}
PyObject* McRFPy_API::_getMetrics(PyObject* self, PyObject* args) {
// Create a dictionary with metrics
PyObject* dict = PyDict_New();
if (!dict) return NULL;
// Add frame time metrics
PyDict_SetItemString(dict, "frame_time", PyFloat_FromDouble(game->metrics.frameTime));
PyDict_SetItemString(dict, "avg_frame_time", PyFloat_FromDouble(game->metrics.avgFrameTime));
PyDict_SetItemString(dict, "fps", PyLong_FromLong(game->metrics.fps));
// Add draw call metrics
PyDict_SetItemString(dict, "draw_calls", PyLong_FromLong(game->metrics.drawCalls));
PyDict_SetItemString(dict, "ui_elements", PyLong_FromLong(game->metrics.uiElements));
PyDict_SetItemString(dict, "visible_elements", PyLong_FromLong(game->metrics.visibleElements));
// Add general metrics
PyDict_SetItemString(dict, "current_frame", PyLong_FromLong(game->getFrame()));
PyDict_SetItemString(dict, "runtime", PyFloat_FromDouble(game->runtime.getElapsedTime().asSeconds()));
return dict;
}

View File

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

324
src/McRFPy_Libtcod.cpp Normal file
View File

@ -0,0 +1,324 @@
#include "McRFPy_Libtcod.h"
#include "McRFPy_API.h"
#include "UIGrid.h"
#include <vector>
// Helper function to get UIGrid from Python object
static UIGrid* get_grid_from_pyobject(PyObject* obj) {
auto grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
if (!grid_type) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Grid type");
return nullptr;
}
if (!PyObject_IsInstance(obj, (PyObject*)grid_type)) {
Py_DECREF(grid_type);
PyErr_SetString(PyExc_TypeError, "First argument must be a Grid object");
return nullptr;
}
Py_DECREF(grid_type);
PyUIGridObject* pygrid = (PyUIGridObject*)obj;
return pygrid->data.get();
}
// Field of View computation
static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y, radius;
int light_walls = 1;
int algorithm = FOV_BASIC;
if (!PyArg_ParseTuple(args, "Oiii|ii", &grid_obj, &x, &y, &radius,
&light_walls, &algorithm)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// Compute FOV using grid's method
grid->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
// Return list of visible cells
PyObject* visible_list = PyList_New(0);
for (int gy = 0; gy < grid->grid_y; gy++) {
for (int gx = 0; gx < grid->grid_x; gx++) {
if (grid->isInFOV(gx, gy)) {
PyObject* pos = Py_BuildValue("(ii)", gx, gy);
PyList_Append(visible_list, pos);
Py_DECREF(pos);
}
}
}
return visible_list;
}
// A* Pathfinding
static PyObject* McRFPy_Libtcod::find_path(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x1, y1, x2, y2;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTuple(args, "Oiiii|f", &grid_obj, &x1, &y1, &x2, &y2, &diagonal_cost)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// Get path from grid
std::vector<std::pair<int, int>> path = grid->findPath(x1, y1, x2, y2, diagonal_cost);
// Convert to Python list
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // steals reference
}
return path_list;
}
// Line drawing algorithm
static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) {
int x1, y1, x2, y2;
if (!PyArg_ParseTuple(args, "iiii", &x1, &y1, &x2, &y2)) {
return NULL;
}
// Use TCOD's line algorithm
TCODLine::init(x1, y1, x2, y2);
PyObject* line_list = PyList_New(0);
int x, y;
// Step through line
while (!TCODLine::step(&x, &y)) {
PyObject* pos = Py_BuildValue("(ii)", x, y);
PyList_Append(line_list, pos);
Py_DECREF(pos);
}
return line_list;
}
// Line iterator (generator-like function)
static PyObject* McRFPy_Libtcod::line_iter(PyObject* self, PyObject* args) {
// For simplicity, just call line() for now
// A proper implementation would create an iterator object
return line(self, args);
}
// Dijkstra pathfinding
static PyObject* McRFPy_Libtcod::dijkstra_new(PyObject* self, PyObject* args) {
PyObject* grid_obj;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTuple(args, "O|f", &grid_obj, &diagonal_cost)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// For now, just return the grid object since Dijkstra is part of the grid
Py_INCREF(grid_obj);
return grid_obj;
}
static PyObject* McRFPy_Libtcod::dijkstra_compute(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int root_x, root_y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &root_x, &root_y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
grid->computeDijkstra(root_x, root_y);
Py_RETURN_NONE;
}
static PyObject* McRFPy_Libtcod::dijkstra_get_distance(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
float distance = grid->getDijkstraDistance(x, y);
if (distance < 0) {
Py_RETURN_NONE;
}
return PyFloat_FromDouble(distance);
}
static PyObject* McRFPy_Libtcod::dijkstra_path_to(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
std::vector<std::pair<int, int>> path = grid->getDijkstraPath(x, y);
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // steals reference
}
return path_list;
}
// Add FOV algorithm constants to module
static PyObject* McRFPy_Libtcod::add_fov_constants(PyObject* module) {
// FOV algorithms
PyModule_AddIntConstant(module, "FOV_BASIC", FOV_BASIC);
PyModule_AddIntConstant(module, "FOV_DIAMOND", FOV_DIAMOND);
PyModule_AddIntConstant(module, "FOV_SHADOW", FOV_SHADOW);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8);
PyModule_AddIntConstant(module, "FOV_RESTRICTIVE", FOV_RESTRICTIVE);
PyModule_AddIntConstant(module, "FOV_SYMMETRIC_SHADOWCAST", FOV_SYMMETRIC_SHADOWCAST);
return module;
}
// Method definitions
static PyMethodDef libtcodMethods[] = {
{"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS,
"compute_fov(grid, x, y, radius, light_walls=True, algorithm=FOV_BASIC)\n\n"
"Compute field of view from a position.\n\n"
"Args:\n"
" grid: Grid object to compute FOV on\n"
" x, y: Origin position\n"
" radius: Maximum sight radius\n"
" light_walls: Whether walls are lit when in FOV\n"
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_SHADOW, etc.)\n\n"
"Returns:\n"
" List of (x, y) tuples for visible cells"},
{"find_path", McRFPy_Libtcod::find_path, METH_VARARGS,
"find_path(grid, x1, y1, x2, y2, diagonal_cost=1.41)\n\n"
"Find shortest path between two points using A*.\n\n"
"Args:\n"
" grid: Grid object to pathfind on\n"
" x1, y1: Starting position\n"
" x2, y2: Target position\n"
" diagonal_cost: Cost of diagonal movement\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, or empty list if no path exists"},
{"line", McRFPy_Libtcod::line, METH_VARARGS,
"line(x1, y1, x2, y2)\n\n"
"Get cells along a line using Bresenham's algorithm.\n\n"
"Args:\n"
" x1, y1: Starting position\n"
" x2, y2: Ending position\n\n"
"Returns:\n"
" List of (x, y) tuples along the line"},
{"line_iter", McRFPy_Libtcod::line_iter, METH_VARARGS,
"line_iter(x1, y1, x2, y2)\n\n"
"Iterate over cells along a line.\n\n"
"Args:\n"
" x1, y1: Starting position\n"
" x2, y2: Ending position\n\n"
"Returns:\n"
" Iterator of (x, y) tuples along the line"},
{"dijkstra_new", McRFPy_Libtcod::dijkstra_new, METH_VARARGS,
"dijkstra_new(grid, diagonal_cost=1.41)\n\n"
"Create a Dijkstra pathfinding context for a grid.\n\n"
"Args:\n"
" grid: Grid object to use for pathfinding\n"
" diagonal_cost: Cost of diagonal movement\n\n"
"Returns:\n"
" Grid object configured for Dijkstra pathfinding"},
{"dijkstra_compute", McRFPy_Libtcod::dijkstra_compute, METH_VARARGS,
"dijkstra_compute(grid, root_x, root_y)\n\n"
"Compute Dijkstra distance map from root position.\n\n"
"Args:\n"
" grid: Grid object with Dijkstra context\n"
" root_x, root_y: Root position to compute distances from"},
{"dijkstra_get_distance", McRFPy_Libtcod::dijkstra_get_distance, METH_VARARGS,
"dijkstra_get_distance(grid, x, y)\n\n"
"Get distance from root to a position.\n\n"
"Args:\n"
" grid: Grid object with computed Dijkstra map\n"
" x, y: Position to get distance for\n\n"
"Returns:\n"
" Float distance or None if position is invalid/unreachable"},
{"dijkstra_path_to", McRFPy_Libtcod::dijkstra_path_to, METH_VARARGS,
"dijkstra_path_to(grid, x, y)\n\n"
"Get shortest path from position to Dijkstra root.\n\n"
"Args:\n"
" grid: Grid object with computed Dijkstra map\n"
" x, y: Starting position\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path to root"},
{NULL, NULL, 0, NULL}
};
// Module definition
static PyModuleDef libtcodModule = {
PyModuleDef_HEAD_INIT,
"mcrfpy.libtcod",
"TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n"
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
"FOV Algorithms:\n"
" FOV_BASIC - Basic circular FOV\n"
" FOV_SHADOW - Shadow casting (recommended)\n"
" FOV_DIAMOND - Diamond-shaped FOV\n"
" FOV_PERMISSIVE_0 through FOV_PERMISSIVE_8 - Permissive variants\n"
" FOV_RESTRICTIVE - Most restrictive FOV\n"
" FOV_SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
"Example:\n"
" import mcrfpy\n"
" from mcrfpy import libtcod\n\n"
" grid = mcrfpy.Grid(50, 50)\n"
" visible = libtcod.compute_fov(grid, 25, 25, 10)\n"
" path = libtcod.find_path(grid, 0, 0, 49, 49)",
-1,
libtcodMethods
};
// Module initialization
PyObject* McRFPy_Libtcod::init_libtcod_module() {
PyObject* m = PyModule_Create(&libtcodModule);
if (m == NULL) {
return NULL;
}
// Add FOV algorithm constants
add_fov_constants(m);
return m;
}

27
src/McRFPy_Libtcod.h Normal file
View File

@ -0,0 +1,27 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <libtcod.h>
namespace McRFPy_Libtcod
{
// Field of View algorithms
static PyObject* compute_fov(PyObject* self, PyObject* args);
// Pathfinding
static PyObject* find_path(PyObject* self, PyObject* args);
static PyObject* dijkstra_new(PyObject* self, PyObject* args);
static PyObject* dijkstra_compute(PyObject* self, PyObject* args);
static PyObject* dijkstra_get_distance(PyObject* self, PyObject* args);
static PyObject* dijkstra_path_to(PyObject* self, PyObject* args);
// Line algorithms
static PyObject* line(PyObject* self, PyObject* args);
static PyObject* line_iter(PyObject* self, PyObject* args);
// FOV algorithm constants
static PyObject* add_fov_constants(PyObject* module);
// Module initialization
PyObject* init_libtcod_module();
}

410
src/PyArgHelpers.h Normal file
View File

@ -0,0 +1,410 @@
#pragma once
#include "Python.h"
#include "PyVector.h"
#include "PyColor.h"
#include <SFML/Graphics.hpp>
#include <string>
// Unified argument parsing helpers for Python API consistency
namespace PyArgHelpers {
// Position in pixels (float)
struct PositionResult {
float x, y;
bool valid;
const char* error;
};
// Size in pixels (float)
struct SizeResult {
float w, h;
bool valid;
const char* error;
};
// Grid position in tiles (float - for animation)
struct GridPositionResult {
float grid_x, grid_y;
bool valid;
const char* error;
};
// Grid size in tiles (int - can't have fractional tiles)
struct GridSizeResult {
int grid_w, grid_h;
bool valid;
const char* error;
};
// Color parsing
struct ColorResult {
sf::Color color;
bool valid;
const char* error;
};
// Helper to check if a keyword conflicts with positional args
static bool hasConflict(PyObject* kwds, const char* key, bool has_positional) {
if (!kwds || !has_positional) return false;
PyObject* value = PyDict_GetItemString(kwds, key);
return value != nullptr;
}
// Parse position with conflict detection
static PositionResult parsePosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
PositionResult result = {0.0f, 0.0f, false, nullptr};
int start_idx = next_arg ? *next_arg : 0;
bool has_positional = false;
// Check for positional tuple argument first
if (args && PyTuple_Size(args) > start_idx) {
PyObject* first = PyTuple_GetItem(args, start_idx);
// Is it a tuple/Vector?
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
// Extract from tuple
PyObject* x_obj = PyTuple_GetItem(first, 0);
PyObject* y_obj = PyTuple_GetItem(first, 1);
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
}
} else if (PyObject_TypeCheck(first, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
// It's a Vector object
PyVectorObject* vec = (PyVectorObject*)first;
result.x = vec->data.x;
result.y = vec->data.y;
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
}
}
// Check for keyword conflicts
if (has_positional) {
if (hasConflict(kwds, "pos", true) || hasConflict(kwds, "x", true) || hasConflict(kwds, "y", true)) {
result.valid = false;
result.error = "position specified both positionally and by keyword";
return result;
}
}
// If no positional, try keywords
if (!has_positional && kwds) {
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
// Check for conflicts between pos and x/y
if (pos_obj && (x_obj || y_obj)) {
result.valid = false;
result.error = "pos and x/y cannot both be specified";
return result;
}
if (pos_obj) {
// Parse pos keyword
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
result.x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
result.y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
result.valid = true;
}
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
PyVectorObject* vec = (PyVectorObject*)pos_obj;
result.x = vec->data.x;
result.y = vec->data.y;
result.valid = true;
}
} else if (x_obj && y_obj) {
// Parse x, y keywords
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.valid = true;
}
}
}
return result;
}
// Parse size with conflict detection
static SizeResult parseSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
SizeResult result = {0.0f, 0.0f, false, nullptr};
int start_idx = next_arg ? *next_arg : 0;
bool has_positional = false;
// Check for positional tuple argument
if (args && PyTuple_Size(args) > start_idx) {
PyObject* first = PyTuple_GetItem(args, start_idx);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* w_obj = PyTuple_GetItem(first, 0);
PyObject* h_obj = PyTuple_GetItem(first, 1);
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
}
}
}
// Check for keyword conflicts
if (has_positional) {
if (hasConflict(kwds, "size", true) || hasConflict(kwds, "w", true) || hasConflict(kwds, "h", true)) {
result.valid = false;
result.error = "size specified both positionally and by keyword";
return result;
}
}
// If no positional, try keywords
if (!has_positional && kwds) {
PyObject* size_obj = PyDict_GetItemString(kwds, "size");
PyObject* w_obj = PyDict_GetItemString(kwds, "w");
PyObject* h_obj = PyDict_GetItemString(kwds, "h");
// Check for conflicts between size and w/h
if (size_obj && (w_obj || h_obj)) {
result.valid = false;
result.error = "size and w/h cannot both be specified";
return result;
}
if (size_obj) {
// Parse size keyword
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
result.w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
result.h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
result.valid = true;
}
}
} else if (w_obj && h_obj) {
// Parse w, h keywords
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
result.valid = true;
}
}
}
return result;
}
// Parse grid position (float for smooth animation)
static GridPositionResult parseGridPosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
GridPositionResult result = {0.0f, 0.0f, false, nullptr};
int start_idx = next_arg ? *next_arg : 0;
bool has_positional = false;
// Check for positional tuple argument
if (args && PyTuple_Size(args) > start_idx) {
PyObject* first = PyTuple_GetItem(args, start_idx);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* x_obj = PyTuple_GetItem(first, 0);
PyObject* y_obj = PyTuple_GetItem(first, 1);
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.grid_x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.grid_y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
}
}
}
// Check for keyword conflicts
if (has_positional) {
if (hasConflict(kwds, "grid_pos", true) || hasConflict(kwds, "grid_x", true) || hasConflict(kwds, "grid_y", true)) {
result.valid = false;
result.error = "grid position specified both positionally and by keyword";
return result;
}
}
// If no positional, try keywords
if (!has_positional && kwds) {
PyObject* grid_pos_obj = PyDict_GetItemString(kwds, "grid_pos");
PyObject* grid_x_obj = PyDict_GetItemString(kwds, "grid_x");
PyObject* grid_y_obj = PyDict_GetItemString(kwds, "grid_y");
// Check for conflicts between grid_pos and grid_x/grid_y
if (grid_pos_obj && (grid_x_obj || grid_y_obj)) {
result.valid = false;
result.error = "grid_pos and grid_x/grid_y cannot both be specified";
return result;
}
if (grid_pos_obj) {
// Parse grid_pos keyword
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
result.grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
result.grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
result.valid = true;
}
}
} else if (grid_x_obj && grid_y_obj) {
// Parse grid_x, grid_y keywords
if ((PyFloat_Check(grid_x_obj) || PyLong_Check(grid_x_obj)) &&
(PyFloat_Check(grid_y_obj) || PyLong_Check(grid_y_obj))) {
result.grid_x = PyFloat_Check(grid_x_obj) ? PyFloat_AsDouble(grid_x_obj) : PyLong_AsLong(grid_x_obj);
result.grid_y = PyFloat_Check(grid_y_obj) ? PyFloat_AsDouble(grid_y_obj) : PyLong_AsLong(grid_y_obj);
result.valid = true;
}
}
}
return result;
}
// Parse grid size (int - no fractional tiles)
static GridSizeResult parseGridSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
GridSizeResult result = {0, 0, false, nullptr};
int start_idx = next_arg ? *next_arg : 0;
bool has_positional = false;
// Check for positional tuple argument
if (args && PyTuple_Size(args) > start_idx) {
PyObject* first = PyTuple_GetItem(args, start_idx);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* w_obj = PyTuple_GetItem(first, 0);
PyObject* h_obj = PyTuple_GetItem(first, 1);
if (PyLong_Check(w_obj) && PyLong_Check(h_obj)) {
result.grid_w = PyLong_AsLong(w_obj);
result.grid_h = PyLong_AsLong(h_obj);
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
} else {
result.valid = false;
result.error = "grid size must be specified with integers";
return result;
}
}
}
// Check for keyword conflicts
if (has_positional) {
if (hasConflict(kwds, "grid_size", true) || hasConflict(kwds, "grid_w", true) || hasConflict(kwds, "grid_h", true)) {
result.valid = false;
result.error = "grid size specified both positionally and by keyword";
return result;
}
}
// If no positional, try keywords
if (!has_positional && kwds) {
PyObject* grid_size_obj = PyDict_GetItemString(kwds, "grid_size");
PyObject* grid_w_obj = PyDict_GetItemString(kwds, "grid_w");
PyObject* grid_h_obj = PyDict_GetItemString(kwds, "grid_h");
// Check for conflicts between grid_size and grid_w/grid_h
if (grid_size_obj && (grid_w_obj || grid_h_obj)) {
result.valid = false;
result.error = "grid_size and grid_w/grid_h cannot both be specified";
return result;
}
if (grid_size_obj) {
// Parse grid_size keyword
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
PyObject* w_val = PyTuple_GetItem(grid_size_obj, 0);
PyObject* h_val = PyTuple_GetItem(grid_size_obj, 1);
if (PyLong_Check(w_val) && PyLong_Check(h_val)) {
result.grid_w = PyLong_AsLong(w_val);
result.grid_h = PyLong_AsLong(h_val);
result.valid = true;
} else {
result.valid = false;
result.error = "grid size must be specified with integers";
return result;
}
}
} else if (grid_w_obj && grid_h_obj) {
// Parse grid_w, grid_h keywords
if (PyLong_Check(grid_w_obj) && PyLong_Check(grid_h_obj)) {
result.grid_w = PyLong_AsLong(grid_w_obj);
result.grid_h = PyLong_AsLong(grid_h_obj);
result.valid = true;
} else {
result.valid = false;
result.error = "grid size must be specified with integers";
return result;
}
}
}
return result;
}
// Parse color using existing PyColor infrastructure
static ColorResult parseColor(PyObject* obj, const char* param_name = nullptr) {
ColorResult result = {sf::Color::White, false, nullptr};
if (!obj) {
return result;
}
// Use existing PyColor::from_arg which handles tuple/Color conversion
auto py_color = PyColor::from_arg(obj);
if (py_color) {
result.color = py_color->data;
result.valid = true;
} else {
result.valid = false;
std::string error_msg = param_name
? std::string(param_name) + " must be a color tuple (r,g,b) or (r,g,b,a)"
: "Invalid color format - expected tuple (r,g,b) or (r,g,b,a)";
result.error = error_msg.c_str();
}
return result;
}
// Helper to validate a texture object
static bool isValidTexture(PyObject* obj) {
if (!obj) return false;
PyObject* texture_type = PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Texture");
bool is_texture = PyObject_IsInstance(obj, texture_type);
Py_DECREF(texture_type);
return is_texture;
}
// Helper to validate a click handler
static bool isValidClickHandler(PyObject* obj) {
return obj && PyCallable_Check(obj);
}
}

View File

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

View File

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

View File

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

View File

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

179
src/PyDrawable.cpp Normal file
View File

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

15
src/PyDrawable.h Normal file
View File

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

View File

@ -61,3 +61,19 @@ PyObject* PyFont::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
return (PyObject*)type->tp_alloc(type, 0);
}
PyObject* PyFont::get_family(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->font.getInfo().family.c_str());
}
PyObject* PyFont::get_source(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->source.c_str());
}
PyGetSetDef PyFont::getsetters[] = {
{"family", (getter)PyFont::get_family, NULL, "Font family name", NULL},
{"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL},
{NULL} // Sentinel
};

View File

@ -21,6 +21,12 @@ public:
static Py_hash_t hash(PyObject*);
static int init(PyFontObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
// Getters for properties
static PyObject* get_family(PyFontObject* self, void* closure);
static PyObject* get_source(PyFontObject* self, void* closure);
static PyGetSetDef getsetters[];
};
namespace mcrfpydef {
@ -33,6 +39,7 @@ namespace mcrfpydef {
//.tp_hash = PyFont::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Font Object"),
.tp_getset = PyFont::getsetters,
//.tp_base = &PyBaseObject_Type,
.tp_init = (initproc)PyFont::init,
.tp_new = PyType_GenericNew, //PyFont::pynew,

164
src/PyPositionHelper.h Normal file
View File

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

View File

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

268
src/PySceneObject.cpp Normal file
View File

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

63
src/PySceneObject.h Normal file
View File

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

View File

@ -2,10 +2,15 @@
#include "McRFPy_API.h"
PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h)
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0)
{
texture = sf::Texture();
texture.loadFromFile(source);
if (!texture.loadFromFile(source)) {
// Failed to load texture - leave sheet dimensions as 0
// This will be checked in init()
return;
}
texture.setSmooth(false); // Disable smoothing for pixel art
auto size = texture.getSize();
sheet_width = (size.x / sprite_width);
sheet_height = (size.y / sprite_height);
@ -18,6 +23,12 @@ PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
{
// Protect against division by zero if texture failed to load
if (sheet_width == 0 || sheet_height == 0) {
// Return an empty sprite
return sf::Sprite();
}
int tx = index % sheet_width, ty = index / sheet_width;
auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height);
auto sprite = sf::Sprite(texture, ir);
@ -71,7 +82,16 @@ int PyTexture::init(PyTextureObject* self, PyObject* args, PyObject* kwds)
int sprite_width, sprite_height;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast<char**>(keywords), &filename, &sprite_width, &sprite_height))
return -1;
// Create the texture object
self->data = std::make_shared<PyTexture>(filename, sprite_width, sprite_height);
// Check if the texture failed to load (sheet dimensions will be 0)
if (self->data->sheet_width == 0 || self->data->sheet_height == 0) {
PyErr_Format(PyExc_IOError, "Failed to load texture from file: %s", filename);
return -1;
}
return 0;
}
@ -79,3 +99,43 @@ PyObject* PyTexture::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
return (PyObject*)type->tp_alloc(type, 0);
}
PyObject* PyTexture::get_sprite_width(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sprite_width);
}
PyObject* PyTexture::get_sprite_height(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sprite_height);
}
PyObject* PyTexture::get_sheet_width(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sheet_width);
}
PyObject* PyTexture::get_sheet_height(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sheet_height);
}
PyObject* PyTexture::get_sprite_count(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->getSpriteCount());
}
PyObject* PyTexture::get_source(PyTextureObject* self, void* closure)
{
return PyUnicode_FromString(self->data->source.c_str());
}
PyGetSetDef PyTexture::getsetters[] = {
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL, "Width of each sprite in pixels", NULL},
{"sprite_height", (getter)PyTexture::get_sprite_height, NULL, "Height of each sprite in pixels", NULL},
{"sheet_width", (getter)PyTexture::get_sheet_width, NULL, "Number of sprite columns in the texture", NULL},
{"sheet_height", (getter)PyTexture::get_sheet_height, NULL, "Number of sprite rows in the texture", NULL},
{"sprite_count", (getter)PyTexture::get_sprite_count, NULL, "Total number of sprites in the texture", NULL},
{"source", (getter)PyTexture::get_source, NULL, "Source filename of the texture", NULL},
{NULL} // Sentinel
};

View File

@ -26,6 +26,16 @@ public:
static Py_hash_t hash(PyObject*);
static int init(PyTextureObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
// Getters for properties
static PyObject* get_sprite_width(PyTextureObject* self, void* closure);
static PyObject* get_sprite_height(PyTextureObject* self, void* closure);
static PyObject* get_sheet_width(PyTextureObject* self, void* closure);
static PyObject* get_sheet_height(PyTextureObject* self, void* closure);
static PyObject* get_sprite_count(PyTextureObject* self, void* closure);
static PyObject* get_source(PyTextureObject* self, void* closure);
static PyGetSetDef getsetters[];
};
namespace mcrfpydef {
@ -38,6 +48,7 @@ namespace mcrfpydef {
.tp_hash = PyTexture::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Texture Object"),
.tp_getset = PyTexture::getsetters,
//.tp_base = &PyBaseObject_Type,
.tp_init = (initproc)PyTexture::init,
.tp_new = PyType_GenericNew, //PyTexture::pynew,

271
src/PyTimer.cpp Normal file
View File

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

58
src/PyTimer.h Normal file
View File

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

View File

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

View File

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

514
src/PyWindow.cpp Normal file
View File

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

69
src/PyWindow.h Normal file
View File

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

85
src/SceneTransition.cpp Normal file
View File

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

42
src/SceneTransition.h Normal file
View File

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

View File

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

View File

@ -3,8 +3,22 @@
#include "PyColor.h"
#include "PyVector.h"
#include "PyFont.h"
#include "PyArgHelpers.h"
// UIDrawable methods now in UIBase.h
#include <algorithm>
UICaption::UICaption()
{
// Initialize text with safe defaults
text.setString("");
position = sf::Vector2f(0.0f, 0.0f); // Set base class position
text.setPosition(position); // Sync text position
text.setCharacterSize(12);
text.setFillColor(sf::Color::White);
text.setOutlineColor(sf::Color::Black);
text.setOutlineThickness(0.0f);
}
UIDrawable* UICaption::click_at(sf::Vector2f point)
{
if (click_callable)
@ -16,10 +30,22 @@ UIDrawable* UICaption::click_at(sf::Vector2f point)
void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
{
// Check visibility
if (!visible) return;
// Apply opacity
auto color = text.getFillColor();
color.a = static_cast<sf::Uint8>(255 * opacity);
text.setFillColor(color);
text.move(offset);
//Resources::game->getWindow().draw(text);
target.draw(text);
text.move(-offset);
// Restore original alpha
color.a = 255;
text.setFillColor(color);
}
PyObjectsEnum UICaption::derived_type()
@ -27,6 +53,47 @@ PyObjectsEnum UICaption::derived_type()
return PyObjectsEnum::UICAPTION;
}
// Phase 1 implementations
sf::FloatRect UICaption::get_bounds() const
{
return text.getGlobalBounds();
}
void UICaption::move(float dx, float dy)
{
position.x += dx;
position.y += dy;
text.setPosition(position); // Keep text in sync
}
void UICaption::resize(float w, float h)
{
// Implement multiline text support by setting bounds
// Width constraint enables automatic word wrapping in SFML
if (w > 0) {
// Store the requested width for word wrapping
// Note: SFML doesn't have direct width constraint, but we can
// implement basic word wrapping by inserting newlines
// For now, we'll store the constraint for future use
// A full implementation would need to:
// 1. Split text into words
// 2. Measure each word's width
// 3. Insert newlines where needed
// This is a placeholder that at least acknowledges the resize request
// TODO: Implement proper word wrapping algorithm
// For now, just mark that resize was called
markDirty();
}
}
void UICaption::onPositionChanged()
{
// Sync text position with base class position
text.setPosition(position);
}
PyObject* UICaption::get_float_member(PyUICaptionObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
@ -59,7 +126,7 @@ int UICaption::set_float_member(PyUICaptionObject* self, PyObject* value, void*
}
else
{
PyErr_SetString(PyExc_TypeError, "Value must be an integer.");
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
return -1;
}
if (member_ptr == 0) //x
@ -122,7 +189,6 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void*
// get value from mcrfpy.Color instance
auto c = ((PyColorObject*)value)->data;
r = c.r; g = c.g; b = c.b; a = c.a;
std::cout << "got " << int(r) << ", " << int(g) << ", " << int(b) << ", " << int(a) << std::endl;
}
else if (!PyTuple_Check(value) || PyTuple_Size(value) < 3 || PyTuple_Size(value) > 4)
{
@ -167,6 +233,15 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void*
}
// Define the PyObjectType alias for the macros
typedef PyUICaptionObject PyObjectType;
// Method definitions
PyMethodDef UICaption_methods[] = {
UIDRAWABLE_METHODS,
{NULL} // Sentinel
};
//TODO: evaluate use of Resources::caption_buffer... can't I do this with a std::string?
PyObject* UICaption::get_text(PyUICaptionObject* self, void* closure)
{
@ -187,9 +262,9 @@ int UICaption::set_text(PyUICaptionObject* self, PyObject* value, void* closure)
}
PyGetSetDef UICaption::getsetters[] = {
{"x", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "X coordinate of top-left corner", (void*)0},
{"y", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Y coordinate of top-left corner", (void*)1},
{"pos", (getter)UICaption::get_vec_member, (setter)UICaption::set_vec_member, "(x, y) vector", (void*)0},
{"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "X coordinate of top-left corner", (void*)((intptr_t)PyObjectsEnum::UICAPTION << 8 | 0)},
{"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "Y coordinate of top-left corner", (void*)((intptr_t)PyObjectsEnum::UICAPTION << 8 | 1)},
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "(x, y) vector", (void*)PyObjectsEnum::UICAPTION},
//{"w", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "width of the rectangle", (void*)2},
//{"h", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "height of the rectangle", (void*)3},
{"outline", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Thickness of the border", (void*)4},
@ -197,9 +272,11 @@ PyGetSetDef UICaption::getsetters[] = {
{"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Outline color of the text", (void*)1},
//{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL},
{"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL},
{"size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Text size (integer) in points", (void*)5},
{"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION},
UIDRAWABLE_GETSETTERS,
{NULL}
};
@ -225,30 +302,126 @@ PyObject* UICaption::repr(PyUICaptionObject* self)
int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
{
using namespace mcrfpydef;
// Constructor switch to Vector position
//static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", nullptr };
//float x = 0.0f, y = 0.0f, outline = 0.0f;
static const char* keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", nullptr };
PyObject* pos;
float outline = 0.0f;
char* text;
PyObject* font=NULL, *fill_color=NULL, *outline_color=NULL;
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf",
// const_cast<char**>(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oz|OOOf",
const_cast<char**>(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline))
{
return -1;
// Try parsing with PyArgHelpers
int arg_idx = 0;
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
// Default values
float x = 0.0f, y = 0.0f, outline = 0.0f;
char* text = nullptr;
PyObject* font = nullptr;
PyObject* fill_color = nullptr;
PyObject* outline_color = nullptr;
PyObject* click_handler = nullptr;
// Case 1: Got position from helpers (tuple format)
if (pos_result.valid) {
x = pos_result.x;
y = pos_result.y;
// Parse remaining arguments
static const char* remaining_keywords[] = {
"text", "font", "fill_color", "outline_color", "outline", "click", nullptr
};
// Create new tuple with remaining args
Py_ssize_t total_args = PyTuple_Size(args);
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|zOOOfO",
const_cast<char**>(remaining_keywords),
&text, &font, &fill_color, &outline_color,
&outline, &click_handler)) {
Py_DECREF(remaining_args);
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
return -1;
}
Py_DECREF(remaining_args);
}
// Case 2: Traditional format
else {
PyErr_Clear(); // Clear any errors from helpers
// First check if this is the old (text, x, y, ...) format
PyObject* first_arg = args && PyTuple_Size(args) > 0 ? PyTuple_GetItem(args, 0) : nullptr;
bool text_first = first_arg && PyUnicode_Check(first_arg);
if (text_first) {
// Pattern: (text, x, y, ...)
static const char* text_first_keywords[] = {
"text", "x", "y", "font", "fill_color", "outline_color",
"outline", "click", "pos", nullptr
};
PyObject* pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO",
const_cast<char**>(text_first_keywords),
&text, &x, &y, &font, &fill_color, &outline_color,
&outline, &click_handler, &pos_obj)) {
return -1;
}
// Handle pos keyword override
if (pos_obj && pos_obj != Py_None) {
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
}
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Vector"))) {
PyVectorObject* vec = (PyVectorObject*)pos_obj;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
}
}
} else {
// Pattern: (x, y, text, ...)
static const char* xy_keywords[] = {
"x", "y", "text", "font", "fill_color", "outline_color",
"outline", "click", "pos", nullptr
};
PyObject* pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO",
const_cast<char**>(xy_keywords),
&x, &y, &text, &font, &fill_color, &outline_color,
&outline, &click_handler, &pos_obj)) {
return -1;
}
// Handle pos keyword override
if (pos_obj && pos_obj != Py_None) {
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
}
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Vector"))) {
PyVectorObject* vec = (PyVectorObject*)pos_obj;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
}
}
}
}
PyVectorObject* pos_result = PyVector::from_arg(pos);
if (!pos_result)
{
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
}
self->data->text.setPosition(pos_result->data);
self->data->position = sf::Vector2f(x, y); // Set base class position
self->data->text.setPosition(self->data->position); // Sync text position
// check types for font, fill_color, outline_color
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
@ -275,7 +448,12 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
}
}
self->data->text.setString((std::string)text);
// Handle text - default to empty string if not provided
if (text && text != NULL) {
self->data->text.setString((std::string)text);
} else {
self->data->text.setString("");
}
self->data->text.setOutlineThickness(outline);
if (fill_color) {
auto fc = PyColor::from_arg(fill_color);
@ -301,20 +479,31 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
self->data->text.setOutlineColor(sf::Color(128,128,128,255));
}
// Process click handler if provided
if (click_handler && click_handler != Py_None) {
if (!PyCallable_Check(click_handler)) {
PyErr_SetString(PyExc_TypeError, "click must be callable");
return -1;
}
self->data->click_register(click_handler);
}
return 0;
}
// Property system implementation for animations
bool UICaption::setProperty(const std::string& name, float value) {
if (name == "x") {
text.setPosition(sf::Vector2f(value, text.getPosition().y));
position.x = value;
text.setPosition(position); // Keep text in sync
return true;
}
else if (name == "y") {
text.setPosition(sf::Vector2f(text.getPosition().x, value));
position.y = value;
text.setPosition(position); // Keep text in sync
return true;
}
else if (name == "size") {
else if (name == "font_size" || name == "size") { // Support both for backward compatibility
text.setCharacterSize(static_cast<unsigned int>(value));
return true;
}
@ -399,14 +588,14 @@ bool UICaption::setProperty(const std::string& name, const std::string& value) {
bool UICaption::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = text.getPosition().x;
value = position.x;
return true;
}
else if (name == "y") {
value = text.getPosition().y;
value = position.y;
return true;
}
else if (name == "size") {
else if (name == "font_size" || name == "size") { // Support both for backward compatibility
value = static_cast<float>(text.getCharacterSize());
return true;
}

View File

@ -2,15 +2,23 @@
#include "Common.h"
#include "Python.h"
#include "UIDrawable.h"
#include "PyDrawable.h"
class UICaption: public UIDrawable
{
public:
sf::Text text;
UICaption(); // Default constructor with safe initialization
void render(sf::Vector2f, sf::RenderTarget&) override final;
PyObjectsEnum derived_type() override final;
virtual UIDrawable* click_at(sf::Vector2f point) override final;
// Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override;
void resize(float w, float h) override;
void onPositionChanged() override;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Color& value) override;
@ -34,6 +42,8 @@ public:
};
extern PyMethodDef UICaption_methods[];
namespace mcrfpydef {
static PyTypeObject PyUICaptionType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -55,11 +65,31 @@ namespace mcrfpydef {
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
//.tp_methods = PyUIFrame_methods,
.tp_doc = PyDoc_STR("Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)\n\n"
"A text display UI element with customizable font and styling.\n\n"
"Args:\n"
" text (str): The text content to display. Default: ''\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" font (Font): Font object for text rendering. Default: engine default font\n"
" fill_color (Color): Text fill color. Default: (255, 255, 255, 255)\n"
" outline_color (Color): Text outline color. Default: (0, 0, 0, 255)\n"
" outline (float): Text outline thickness. Default: 0\n"
" click (callable): Click event handler. Default: None\n\n"
"Attributes:\n"
" text (str): The displayed text content\n"
" x, y (float): Position in pixels\n"
" font (Font): Font used for rendering\n"
" fill_color, outline_color (Color): Text appearance\n"
" outline (float): Outline thickness\n"
" click (callable): Click event handler\n"
" visible (bool): Visibility state\n"
" z_index (int): Rendering order\n"
" w, h (float): Read-only computed size based on text and font"),
.tp_methods = UICaption_methods,
//.tp_members = PyUIFrame_members,
.tp_getset = UICaption::getsetters,
//.tp_base = NULL,
.tp_base = &mcrfpydef::PyDrawableType,
.tp_init = (initproc)UICaption::init,
// TODO - move tp_new to .cpp file as a static function (UICaption::new)
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*

View File

@ -615,6 +615,88 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
return Py_None;
}
PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
{
// Accept any iterable of UIDrawable objects
PyObject* iterator = PyObject_GetIter(iterable);
if (iterator == NULL) {
PyErr_SetString(PyExc_TypeError, "UICollection.extend requires an iterable");
return NULL;
}
// Ensure module is initialized
if (!McRFPy_API::mcrf_module) {
Py_DECREF(iterator);
PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized");
return NULL;
}
// Get current highest z_index
int current_z_index = 0;
if (!self->data->empty()) {
current_z_index = self->data->back()->z_index;
}
PyObject* item;
while ((item = PyIter_Next(iterator)) != NULL) {
// Check if item is a UIDrawable subclass
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
{
Py_DECREF(item);
Py_DECREF(iterator);
PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, or Grid objects");
return NULL;
}
// Increment z_index for each new element
if (current_z_index <= INT_MAX - 10) {
current_z_index += 10;
} else {
current_z_index = INT_MAX;
}
// Add the item based on its type
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
PyUIFrameObject* frame = (PyUIFrameObject*)item;
frame->data->z_index = current_z_index;
self->data->push_back(frame->data);
}
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
PyUICaptionObject* caption = (PyUICaptionObject*)item;
caption->data->z_index = current_z_index;
self->data->push_back(caption->data);
}
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
PyUISpriteObject* sprite = (PyUISpriteObject*)item;
sprite->data->z_index = current_z_index;
self->data->push_back(sprite->data);
}
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyUIGridObject* grid = (PyUIGridObject*)item;
grid->data->z_index = current_z_index;
self->data->push_back(grid->data);
}
Py_DECREF(item);
}
Py_DECREF(iterator);
// Check if iteration ended due to an error
if (PyErr_Occurred()) {
return NULL;
}
// Mark scene as needing resort after adding elements
McRFPy_API::markSceneNeedsSort();
Py_INCREF(Py_None);
return Py_None;
}
PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
{
if (!PyLong_Check(o))
@ -734,7 +816,7 @@ PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) {
PyMethodDef UICollection::methods[] = {
{"append", (PyCFunction)UICollection::append, METH_O},
//{"extend", (PyCFunction)PyUICollection_extend, METH_O}, // TODO
{"extend", (PyCFunction)UICollection::extend, METH_O},
{"remove", (PyCFunction)UICollection::remove, METH_O},
{"index", (PyCFunction)UICollection::index_method, METH_O},
{"count", (PyCFunction)UICollection::count, METH_O},
@ -746,7 +828,47 @@ PyObject* UICollection::repr(PyUICollectionObject* self)
std::ostringstream ss;
if (!self->data) ss << "<UICollection (invalid internal object)>";
else {
ss << "<UICollection (" << self->data->size() << " child objects)>";
ss << "<UICollection (" << self->data->size() << " objects: ";
// Count each type
int frame_count = 0, caption_count = 0, sprite_count = 0, grid_count = 0, other_count = 0;
for (auto& item : *self->data) {
switch(item->derived_type()) {
case PyObjectsEnum::UIFRAME: frame_count++; break;
case PyObjectsEnum::UICAPTION: caption_count++; break;
case PyObjectsEnum::UISPRITE: sprite_count++; break;
case PyObjectsEnum::UIGRID: grid_count++; break;
default: other_count++; break;
}
}
// Build type summary
bool first = true;
if (frame_count > 0) {
ss << frame_count << " Frame" << (frame_count > 1 ? "s" : "");
first = false;
}
if (caption_count > 0) {
if (!first) ss << ", ";
ss << caption_count << " Caption" << (caption_count > 1 ? "s" : "");
first = false;
}
if (sprite_count > 0) {
if (!first) ss << ", ";
ss << sprite_count << " Sprite" << (sprite_count > 1 ? "s" : "");
first = false;
}
if (grid_count > 0) {
if (!first) ss << ", ";
ss << grid_count << " Grid" << (grid_count > 1 ? "s" : "");
first = false;
}
if (other_count > 0) {
if (!first) ss << ", ";
ss << other_count << " UIDrawable" << (other_count > 1 ? "s" : "");
}
ss << ")>";
}
std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");

View File

@ -28,6 +28,7 @@ public:
static PyObject* subscript(PyUICollectionObject* self, PyObject* key);
static int ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value);
static PyObject* append(PyUICollectionObject* self, PyObject* o);
static PyObject* extend(PyUICollectionObject* self, PyObject* iterable);
static PyObject* remove(PyUICollectionObject* self, PyObject* o);
static PyObject* index_method(PyUICollectionObject* self, PyObject* value);
static PyObject* count(PyUICollectionObject* self, PyObject* value);

82
src/UIContainerBase.h Normal file
View File

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

View File

@ -6,7 +6,7 @@
#include "GameEngine.h"
#include "McRFPy_API.h"
UIDrawable::UIDrawable() { click_callable = NULL; }
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
void UIDrawable::click_unregister()
{
@ -25,16 +25,28 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
switch (objtype)
{
case PyObjectsEnum::UIFRAME:
ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow();
if (((PyUIFrameObject*)self)->data->click_callable)
ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
case PyObjectsEnum::UICAPTION:
ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow();
if (((PyUICaptionObject*)self)->data->click_callable)
ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
case PyObjectsEnum::UISPRITE:
ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow();
if (((PyUISpriteObject*)self)->data->click_callable)
ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
case PyObjectsEnum::UIGRID:
ptr = ((PyUIGridObject*)self)->data->click_callable->borrow();
if (((PyUIGridObject*)self)->data->click_callable)
ptr = ((PyUIGridObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
default:
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click");
@ -163,3 +175,307 @@ void UIDrawable::notifyZIndexChanged() {
// For now, Frame children will need manual sorting or collection modification
// to trigger a resort
}
PyObject* UIDrawable::get_name(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return NULL;
}
return PyUnicode_FromString(drawable->name.c_str());
}
int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return -1;
}
if (value == NULL || value == Py_None) {
drawable->name = "";
return 0;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "name must be a string");
return -1;
}
const char* name_str = PyUnicode_AsUTF8(value);
if (!name_str) {
return -1;
}
drawable->name = name_str;
return 0;
}
void UIDrawable::enableRenderTexture(unsigned int width, unsigned int height) {
// Create or recreate RenderTexture if size changed
if (!render_texture || render_texture->getSize().x != width || render_texture->getSize().y != height) {
render_texture = std::make_unique<sf::RenderTexture>();
if (!render_texture->create(width, height)) {
render_texture.reset();
use_render_texture = false;
return;
}
render_sprite.setTexture(render_texture->getTexture());
}
use_render_texture = true;
render_dirty = true;
}
void UIDrawable::updateRenderTexture() {
if (!use_render_texture || !render_texture) {
return;
}
// Clear the RenderTexture
render_texture->clear(sf::Color::Transparent);
// Render content to RenderTexture
// This will be overridden by derived classes
// For now, just display the texture
render_texture->display();
// Update the sprite
render_sprite.setTexture(render_texture->getTexture());
}
PyObject* UIDrawable::get_float_member(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure) >> 8);
int member = reinterpret_cast<intptr_t>(closure) & 0xFF;
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return NULL;
}
switch (member) {
case 0: // x
return PyFloat_FromDouble(drawable->position.x);
case 1: // y
return PyFloat_FromDouble(drawable->position.y);
case 2: // w (width) - delegate to get_bounds
return PyFloat_FromDouble(drawable->get_bounds().width);
case 3: // h (height) - delegate to get_bounds
return PyFloat_FromDouble(drawable->get_bounds().height);
default:
PyErr_SetString(PyExc_AttributeError, "Invalid float member");
return NULL;
}
}
int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure) >> 8);
int member = reinterpret_cast<intptr_t>(closure) & 0xFF;
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return -1;
}
float val = 0.0f;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = static_cast<float>(PyLong_AsLong(value));
} else {
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
return -1;
}
switch (member) {
case 0: // x
drawable->position.x = val;
drawable->onPositionChanged();
break;
case 1: // y
drawable->position.y = val;
drawable->onPositionChanged();
break;
case 2: // w
case 3: // h
{
sf::FloatRect bounds = drawable->get_bounds();
if (member == 2) {
drawable->resize(val, bounds.height);
} else {
drawable->resize(bounds.width, val);
}
}
break;
default:
PyErr_SetString(PyExc_AttributeError, "Invalid float member");
return -1;
}
return 0;
}
PyObject* UIDrawable::get_pos(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return NULL;
}
// Create a Python Vector object from position
PyObject* module = PyImport_ImportModule("mcrfpy");
if (!module) return NULL;
PyObject* vector_type = PyObject_GetAttrString(module, "Vector");
Py_DECREF(module);
if (!vector_type) return NULL;
PyObject* args = Py_BuildValue("(ff)", drawable->position.x, drawable->position.y);
PyObject* result = PyObject_CallObject(vector_type, args);
Py_DECREF(vector_type);
Py_DECREF(args);
return result;
}
int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return -1;
}
// Accept tuple or Vector
float x, y;
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
PyObject* x_obj = PyTuple_GetItem(value, 0);
PyObject* y_obj = PyTuple_GetItem(value, 1);
if (PyFloat_Check(x_obj) || PyLong_Check(x_obj)) {
x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : static_cast<float>(PyLong_AsLong(x_obj));
} else {
PyErr_SetString(PyExc_TypeError, "Position x must be a number");
return -1;
}
if (PyFloat_Check(y_obj) || PyLong_Check(y_obj)) {
y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : static_cast<float>(PyLong_AsLong(y_obj));
} else {
PyErr_SetString(PyExc_TypeError, "Position y must be a number");
return -1;
}
} else {
// Try to get as Vector
PyObject* module = PyImport_ImportModule("mcrfpy");
if (!module) return -1;
PyObject* vector_type = PyObject_GetAttrString(module, "Vector");
Py_DECREF(module);
if (!vector_type) return -1;
int is_vector = PyObject_IsInstance(value, vector_type);
Py_DECREF(vector_type);
if (is_vector) {
PyVectorObject* vec = (PyVectorObject*)value;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "Position must be a tuple (x, y) or Vector");
return -1;
}
}
drawable->position = sf::Vector2f(x, y);
drawable->onPositionChanged();
return 0;
}

View File

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

View File

@ -1,15 +1,60 @@
#include "UIEntity.h"
#include "UIGrid.h"
#include "McRFPy_API.h"
#include <algorithm>
#include "PyObjectUtils.h"
#include "PyVector.h"
#include "PyArgHelpers.h"
// UIDrawable methods now in UIBase.h
#include "UIEntityPyMethods.h"
UIEntity::UIEntity() {} // this will not work lol. TODO remove default constructor by finding the shared pointer inits that use it
UIEntity::UIEntity(UIGrid& grid)
: gridstate(grid.grid_x * grid.grid_y)
UIEntity::UIEntity()
: self(nullptr), grid(nullptr), position(0.0f, 0.0f)
{
// Initialize sprite with safe defaults (sprite has its own safe constructor now)
// gridstate vector starts empty - will be lazily initialized when needed
}
// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead
void UIEntity::updateVisibility()
{
if (!grid) return;
// Lazy initialize gridstate if needed
if (gridstate.size() == 0) {
gridstate.resize(grid->grid_x * grid->grid_y);
// Initialize all cells as not visible/discovered
for (auto& state : gridstate) {
state.visible = false;
state.discovered = false;
}
}
// First, mark all cells as not visible
for (auto& state : gridstate) {
state.visible = false;
}
// Compute FOV from entity's position
int x = static_cast<int>(position.x);
int y = static_cast<int>(position.y);
// Use default FOV radius of 10 (can be made configurable later)
grid->computeFOV(x, y, 10);
// Update visible cells based on FOV computation
for (int gy = 0; gy < grid->grid_y; gy++) {
for (int gx = 0; gx < grid->grid_x; gx++) {
int idx = gy * grid->grid_x + gx;
if (grid->isInFOV(gx, gy)) {
gridstate[idx].visible = true;
gridstate[idx].discovered = true; // Once seen, always discovered
}
}
}
}
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
@ -23,17 +68,29 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid");
return NULL;
}
/*
PyUIGridPointStateObject* obj = (PyUIGridPointStateObject*)((&mcrfpydef::PyUIGridPointStateType)->tp_alloc(&mcrfpydef::PyUIGridPointStateType, 0));
*/
// Lazy initialize gridstate if needed
if (self->data->gridstate.size() == 0) {
self->data->gridstate.resize(self->data->grid->grid_x * self->data->grid->grid_y);
// Initialize all cells as not visible/discovered
for (auto& state : self->data->gridstate) {
state.visible = false;
state.discovered = false;
}
}
// Bounds check
if (x < 0 || x >= self->data->grid->grid_x || y < 0 || y >= self->data->grid->grid_y) {
PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of bounds", x, y);
return NULL;
}
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState");
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
//auto target = std::static_pointer_cast<UIEntity>(target);
obj->data = &(self->data->gridstate[y + self->data->grid->grid_x * x]);
obj->data = &(self->data->gridstate[y * self->data->grid->grid_x + x]);
obj->grid = self->data->grid;
obj->entity = self->data;
return (PyObject*)obj;
}
PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) {
@ -64,28 +121,70 @@ PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
}
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
//static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr };
//float x = 0.0f, y = 0.0f, scale = 1.0f;
static const char* keywords[] = { "pos", "texture", "sprite_index", "grid", nullptr };
PyObject* pos;
float scale = 1.0f;
int sprite_index = -1;
PyObject* texture = NULL;
PyObject* grid = NULL;
// Try parsing with PyArgHelpers for grid position
int arg_idx = 0;
auto grid_pos_result = PyArgHelpers::parseGridPosition(args, kwds, &arg_idx);
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O",
// const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &grid))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OiO",
const_cast<char**>(keywords), &pos, &texture, &sprite_index, &grid))
{
return -1;
// Default values
float grid_x = 0.0f, grid_y = 0.0f;
int sprite_index = 0;
PyObject* texture = nullptr;
PyObject* grid_obj = nullptr;
// Case 1: Got grid position from helpers (tuple format)
if (grid_pos_result.valid) {
grid_x = grid_pos_result.grid_x;
grid_y = grid_pos_result.grid_y;
// Parse remaining arguments
static const char* remaining_keywords[] = {
"texture", "sprite_index", "grid", nullptr
};
// Create new tuple with remaining args
Py_ssize_t total_args = PyTuple_Size(args);
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OiO",
const_cast<char**>(remaining_keywords),
&texture, &sprite_index, &grid_obj)) {
Py_DECREF(remaining_args);
if (grid_pos_result.error) PyErr_SetString(PyExc_TypeError, grid_pos_result.error);
return -1;
}
Py_DECREF(remaining_args);
}
// Case 2: Traditional format
else {
PyErr_Clear(); // Clear any errors from helpers
PyVectorObject* pos_result = PyVector::from_arg(pos);
if (!pos_result)
{
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
static const char* keywords[] = {
"grid_x", "grid_y", "texture", "sprite_index", "grid", "grid_pos", nullptr
};
PyObject* grid_pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO",
const_cast<char**>(keywords),
&grid_x, &grid_y, &texture, &sprite_index,
&grid_obj, &grid_pos_obj)) {
return -1;
}
// Handle grid_pos keyword override
if (grid_pos_obj && grid_pos_obj != Py_None) {
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
}
} else {
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
return -1;
}
}
}
// check types for texture
@ -104,29 +203,43 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
texture_ptr = McRFPy_API::default_texture;
}
if (!texture_ptr) {
PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
return -1;
}
// Allow creation without texture for testing purposes
// if (!texture_ptr) {
// PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
// return -1;
// }
if (grid != NULL && !PyObject_IsInstance(grid, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
if (grid_obj != NULL && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
return -1;
}
if (grid == NULL)
self->data = std::make_shared<UIEntity>();
else
self->data = std::make_shared<UIEntity>(*((PyUIGridObject*)grid)->data);
// Always use default constructor for lazy initialization
self->data = std::make_shared<UIEntity>();
// Store reference to Python object
self->data->self = (PyObject*)self;
Py_INCREF(self);
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
self->data->position = pos_result->data;
if (grid != NULL) {
PyUIGridObject* pygrid = (PyUIGridObject*)grid;
if (texture_ptr) {
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
} else {
// Create an empty sprite for testing
self->data->sprite = UISprite();
}
// Set position using grid coordinates
self->data->position = sf::Vector2f(grid_x, grid_y);
if (grid_obj != NULL) {
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
self->data->grid = pygrid->data;
// todone - on creation of Entity with Grid assignment, also append it to the entity list
pygrid->data->entities->push_back(self->data);
// Don't initialize gridstate here - lazy initialization to support large numbers of entities
// gridstate will be initialized when visibility is updated or accessed
}
return 0;
}
@ -173,11 +286,26 @@ sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) {
return sf::Vector2i(static_cast<int>(vec->data.x), static_cast<int>(vec->data.y));
}
// TODO - deprecate / remove this helper
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) {
// This function is incomplete - it creates an empty object without setting state data
// Should use PyObjectUtils::createGridPointState() instead
return PyObjectUtils::createPyObjectGeneric("GridPointState");
// Create a new GridPointState Python object
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState");
if (!type) {
return NULL;
}
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
if (!obj) {
Py_DECREF(type);
return NULL;
}
// Allocate new data and copy values
obj->data = new UIGridPointState();
obj->data->visible = state.visible;
obj->data->discovered = state.discovered;
Py_DECREF(type);
return (PyObject*)obj;
}
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec) {
@ -200,7 +328,10 @@ PyObject* UIEntity::get_position(PyUIEntityObject* self, void* closure) {
if (reinterpret_cast<long>(closure) == 0) {
return sfVector2f_to_PyObject(self->data->position);
} else {
return sfVector2i_to_PyObject(self->data->collision_pos);
// Return integer-cast position for grid coordinates
sf::Vector2i int_pos(static_cast<int>(self->data->position.x),
static_cast<int>(self->data->position.y));
return sfVector2i_to_PyObject(int_pos);
}
}
@ -212,11 +343,13 @@ int UIEntity::set_position(PyUIEntityObject* self, PyObject* value, void* closur
}
self->data->position = vec;
} else {
// For integer position, convert to float and set position
sf::Vector2i vec = PyObject_to_sfVector2i(value);
if (PyErr_Occurred()) {
return -1; // Error already set by PyObject_to_sfVector2i
}
self->data->collision_pos = vec;
self->data->position = sf::Vector2f(static_cast<float>(vec.x),
static_cast<float>(vec.y));
}
return 0;
}
@ -232,7 +365,7 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl
val = PyLong_AsLong(value);
else
{
PyErr_SetString(PyExc_TypeError, "Value must be an integer.");
PyErr_SetString(PyExc_TypeError, "sprite_index must be an integer");
return -1;
}
//self->data->sprite.sprite_index = val;
@ -240,17 +373,171 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl
return 0;
}
PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
if (member_ptr == 0) // x
return PyFloat_FromDouble(self->data->position.x);
else if (member_ptr == 1) // y
return PyFloat_FromDouble(self->data->position.y);
else
{
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
return nullptr;
}
}
int UIEntity::set_float_member(PyUIEntityObject* self, PyObject* value, void* closure)
{
float val;
auto member_ptr = reinterpret_cast<long>(closure);
if (PyFloat_Check(value))
{
val = PyFloat_AsDouble(value);
}
else if (PyLong_Check(value))
{
val = PyLong_AsLong(value);
}
else
{
PyErr_SetString(PyExc_TypeError, "Position must be a number (int or float)");
return -1;
}
if (member_ptr == 0) // x
{
self->data->position.x = val;
}
else if (member_ptr == 1) // y
{
self->data->position.y = val;
}
return 0;
}
PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
{
// Check if entity has a grid
if (!self->data || !self->data->grid) {
Py_RETURN_NONE; // Entity not on a grid, nothing to do
}
// Remove entity from grid's entity list
auto grid = self->data->grid;
auto& entities = grid->entities;
// Find and remove this entity from the list
auto it = std::find_if(entities->begin(), entities->end(),
[self](const std::shared_ptr<UIEntity>& e) {
return e.get() == self->data.get();
});
if (it != entities->end()) {
entities->erase(it);
// Clear the grid reference
self->data->grid.reset();
}
Py_RETURN_NONE;
}
PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"target_x", "target_y", "x", "y", nullptr};
int target_x = -1, target_y = -1;
// Parse arguments - support both target_x/target_y and x/y parameter names
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(keywords),
&target_x, &target_y)) {
PyErr_Clear();
// Try alternative parameter names
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiii", const_cast<char**>(keywords),
&target_x, &target_y, &target_x, &target_y)) {
PyErr_SetString(PyExc_TypeError, "path_to() requires target_x and target_y integer arguments");
return NULL;
}
}
// Check if entity has a grid
if (!self->data || !self->data->grid) {
PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to compute paths");
return NULL;
}
// Get current position
int current_x = static_cast<int>(self->data->position.x);
int current_y = static_cast<int>(self->data->position.y);
// Validate target position
auto grid = self->data->grid;
if (target_x < 0 || target_x >= grid->grid_x || target_y < 0 || target_y >= grid->grid_y) {
PyErr_Format(PyExc_ValueError, "Target position (%d, %d) is out of grid bounds (0-%d, 0-%d)",
target_x, target_y, grid->grid_x - 1, grid->grid_y - 1);
return NULL;
}
// Use the grid's Dijkstra implementation
grid->computeDijkstra(current_x, current_y);
auto path = grid->getDijkstraPath(target_x, target_y);
// Convert path to Python list of tuples
PyObject* path_list = PyList_New(path.size());
if (!path_list) return PyErr_NoMemory();
for (size_t i = 0; i < path.size(); ++i) {
PyObject* coord_tuple = PyTuple_New(2);
if (!coord_tuple) {
Py_DECREF(path_list);
return PyErr_NoMemory();
}
PyTuple_SetItem(coord_tuple, 0, PyLong_FromLong(path[i].first));
PyTuple_SetItem(coord_tuple, 1, PyLong_FromLong(path[i].second));
PyList_SetItem(path_list, i, coord_tuple);
}
return path_list;
}
PyObject* UIEntity::update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
{
self->data->updateVisibility();
Py_RETURN_NONE;
}
PyMethodDef UIEntity::methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, "Find path from entity to target position using Dijkstra pathfinding"},
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "Update entity's visibility state based on current FOV"},
{NULL, NULL, 0, NULL}
};
// Define the PyObjectType alias for the macros
typedef PyUIEntityObject PyObjectType;
// Combine base methods with entity-specific methods
PyMethodDef UIEntity_all_methods[] = {
UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, "Find path from entity to target position using Dijkstra pathfinding"},
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "Update entity's visibility state based on current FOV"},
{NULL} // Sentinel
};
PyGetSetDef UIEntity::getsetters[] = {
{"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (graphically)", (void*)0},
{"pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (integer grid coordinates)", (void*)1},
{"gridstate", (getter)UIEntity::get_gridstate, NULL, "Grid point states for the entity", NULL},
{"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite number (index) on the texture on the display", NULL},
{"sprite_index", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display", NULL},
{"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index (DEPRECATED: use sprite_index instead)", NULL},
{"x", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity x position", (void*)0},
{"y", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity y position", (void*)1},
{"visible", (getter)UIEntity_get_visible, (setter)UIEntity_set_visible, "Visibility flag", NULL},
{"opacity", (getter)UIEntity_get_opacity, (setter)UIEntity_set_opacity, "Opacity (0.0 = transparent, 1.0 = opaque)", NULL},
{"name", (getter)UIEntity_get_name, (setter)UIEntity_set_name, "Name for finding elements", NULL},
{NULL} /* Sentinel */
};
@ -259,7 +546,7 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) {
if (!self->data) ss << "<Entity (invalid internal object)>";
else {
auto ent = self->data;
ss << "<Entity (x=" << self->data->position.x << ", y=" << self->data->position.y << ", sprite_number=" << self->data->sprite.getSpriteIndex() <<
ss << "<Entity (x=" << self->data->position.x << ", y=" << self->data->position.y << ", sprite_index=" << self->data->sprite.getSpriteIndex() <<
")>";
}
std::string repr_str = ss.str();
@ -270,17 +557,12 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) {
bool UIEntity::setProperty(const std::string& name, float value) {
if (name == "x") {
position.x = value;
collision_pos.x = static_cast<int>(value);
// Update sprite position based on grid position
// Note: This is a simplified version - actual grid-to-pixel conversion depends on grid properties
sprite.setPosition(sf::Vector2f(position.x, position.y));
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
return true;
}
else if (name == "y") {
position.y = value;
collision_pos.y = static_cast<int>(value);
// Update sprite position based on grid position
sprite.setPosition(sf::Vector2f(position.x, position.y));
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
return true;
}
else if (name == "sprite_scale") {
@ -291,7 +573,7 @@ bool UIEntity::setProperty(const std::string& name, float value) {
}
bool UIEntity::setProperty(const std::string& name, int value) {
if (name == "sprite_number") {
if (name == "sprite_index" || name == "sprite_number") {
sprite.setSpriteIndex(value);
return true;
}

View File

@ -8,6 +8,7 @@
#include "PyCallable.h"
#include "PyTexture.h"
#include "PyDrawable.h"
#include "PyColor.h"
#include "PyVector.h"
#include "PyFont.h"
@ -26,33 +27,42 @@ class UIGrid;
//} PyUIEntityObject;
// helper methods with no namespace requirement
static PyObject* sfVector2f_to_PyObject(sf::Vector2f vector);
static sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
static PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
static PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
PyObject* sfVector2f_to_PyObject(sf::Vector2f vector);
sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
// TODO: make UIEntity a drawable
class UIEntity//: public UIDrawable
{
public:
//PyObject* self;
PyObject* self = nullptr; // Reference to the Python object (if created from Python)
std::shared_ptr<UIGrid> grid;
std::vector<UIGridPointState> gridstate;
UISprite sprite;
sf::Vector2f position; //(x,y) in grid coordinates; float for animation
sf::Vector2i collision_pos; //(x, y) in grid coordinates: int for collision
//void render(sf::Vector2f); //override final;
UIEntity();
UIEntity(UIGrid&);
// Visibility methods
void updateVisibility(); // Update gridstate from current FOV
// Property system for animations
bool setProperty(const std::string& name, float value);
bool setProperty(const std::string& name, int value);
bool getProperty(const std::string& name, float& value) const;
// Methods that delegate to sprite
sf::FloatRect get_bounds() const { return sprite.get_bounds(); }
void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; }
void resize(float w, float h) { /* Entities don't support direct resizing */ }
static PyObject* at(PyUIEntityObject* self, PyObject* o);
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* get_position(PyUIEntityObject* self, void* closure);
@ -60,11 +70,16 @@ public:
static PyObject* get_gridstate(PyUIEntityObject* self, void* closure);
static PyObject* get_spritenumber(PyUIEntityObject* self, void* closure);
static int set_spritenumber(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_float_member(PyUIEntityObject* self, void* closure);
static int set_float_member(PyUIEntityObject* self, PyObject* value, void* closure);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIEntityObject* self);
};
// Forward declaration of methods array
extern PyMethodDef UIEntity_all_methods[];
namespace mcrfpydef {
static PyTypeObject PyUIEntityType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -74,8 +89,9 @@ namespace mcrfpydef {
.tp_repr = (reprfunc)UIEntity::repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = "UIEntity objects",
.tp_methods = UIEntity::methods,
.tp_methods = UIEntity_all_methods,
.tp_getset = UIEntity::getsetters,
.tp_base = &mcrfpydef::PyDrawableType,
.tp_init = (initproc)UIEntity::init,
.tp_new = PyType_GenericNew,
};

75
src/UIEntityPyMethods.h Normal file
View File

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

View File

@ -1,35 +1,57 @@
#include "UIFrame.h"
#include "UICollection.h"
#include "GameEngine.h"
#include "PyVector.h"
#include "UICaption.h"
#include "UISprite.h"
#include "UIGrid.h"
#include "McRFPy_API.h"
#include "PyArgHelpers.h"
// UIDrawable methods now in UIBase.h
UIDrawable* UIFrame::click_at(sf::Vector2f point)
{
for (auto e: *children)
{
auto p = e->click_at(point + box.getPosition());
if (p)
return p;
// Check bounds first (optimization)
float x = position.x, y = position.y, w = box.getSize().x, h = box.getSize().y;
if (point.x < x || point.y < y || point.x >= x+w || point.y >= y+h) {
return nullptr;
}
if (click_callable)
{
float x = box.getPosition().x, y = box.getPosition().y, w = box.getSize().x, h = box.getSize().y;
if (point.x > x && point.y > y && point.x < x+w && point.y < y+h) return this;
// Transform to local coordinates for children
sf::Vector2f localPoint = point - position;
// Check children in reverse order (top to bottom, highest z-index first)
for (auto it = children->rbegin(); it != children->rend(); ++it) {
auto& child = *it;
if (!child->visible) continue;
if (auto target = child->click_at(localPoint)) {
return target;
}
}
return NULL;
// No child handled it, check if we have a handler
if (click_callable) {
return this;
}
return nullptr;
}
UIFrame::UIFrame()
: outline(0)
{
children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
box.setPosition(0, 0);
position = sf::Vector2f(0, 0); // Set base class position
box.setPosition(position); // Sync box position
box.setSize(sf::Vector2f(0, 0));
}
UIFrame::UIFrame(float _x, float _y, float _w, float _h)
: outline(0)
{
box.setPosition(_x, _y);
position = sf::Vector2f(_x, _y); // Set base class position
box.setPosition(position); // Sync box position
box.setSize(sf::Vector2f(_w, _h));
children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
}
@ -44,24 +66,102 @@ PyObjectsEnum UIFrame::derived_type()
return PyObjectsEnum::UIFRAME;
}
// Phase 1 implementations
sf::FloatRect UIFrame::get_bounds() const
{
auto size = box.getSize();
return sf::FloatRect(position.x, position.y, size.x, size.y);
}
void UIFrame::move(float dx, float dy)
{
position.x += dx;
position.y += dy;
box.setPosition(position); // Keep box in sync
}
void UIFrame::resize(float w, float h)
{
box.setSize(sf::Vector2f(w, h));
}
void UIFrame::onPositionChanged()
{
// Sync box position with base class position
box.setPosition(position);
}
void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
{
box.move(offset);
//Resources::game->getWindow().draw(box);
target.draw(box);
box.move(-offset);
// Check visibility
if (!visible) return;
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
// TODO: Apply opacity when SFML supports it on shapes
for (auto drawable : *children) {
drawable->render(offset + box.getPosition(), target);
// Check if we need to use RenderTexture for clipping
if (clip_children && !children->empty()) {
// Enable RenderTexture if not already enabled
if (!use_render_texture) {
auto size = box.getSize();
enableRenderTexture(static_cast<unsigned int>(size.x),
static_cast<unsigned int>(size.y));
}
// Update RenderTexture if dirty
if (use_render_texture && render_dirty) {
// Clear the RenderTexture
render_texture->clear(sf::Color::Transparent);
// Draw the frame box to RenderTexture
box.setPosition(0, 0); // Render at origin in texture
render_texture->draw(box);
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
// Render children to RenderTexture at local coordinates
for (auto drawable : *children) {
drawable->render(sf::Vector2f(0, 0), *render_texture);
}
// Finalize the RenderTexture
render_texture->display();
// Update sprite
render_sprite.setTexture(render_texture->getTexture());
render_dirty = false;
}
// Draw the RenderTexture sprite
if (use_render_texture) {
render_sprite.setPosition(offset + box.getPosition());
target.draw(render_sprite);
}
} else {
// Standard rendering without clipping
box.move(offset);
target.draw(box);
box.move(-offset);
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
for (auto drawable : *children) {
drawable->render(offset + box.getPosition(), target);
}
}
}
@ -111,19 +211,39 @@ int UIFrame::set_float_member(PyUIFrameObject* self, PyObject* value, void* clos
}
else
{
PyErr_SetString(PyExc_TypeError, "Value must be an integer.");
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
return -1;
}
if (member_ptr == 0) //x
if (member_ptr == 0) { //x
self->data->box.setPosition(val, self->data->box.getPosition().y);
else if (member_ptr == 1) //y
self->data->markDirty();
}
else if (member_ptr == 1) { //y
self->data->box.setPosition(self->data->box.getPosition().x, val);
else if (member_ptr == 2) //w
self->data->markDirty();
}
else if (member_ptr == 2) { //w
self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y));
else if (member_ptr == 3) //h
if (self->data->use_render_texture) {
// Need to recreate RenderTexture with new size
self->data->enableRenderTexture(static_cast<unsigned int>(self->data->box.getSize().x),
static_cast<unsigned int>(self->data->box.getSize().y));
}
self->data->markDirty();
}
else if (member_ptr == 3) { //h
self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val));
else if (member_ptr == 4) //outline
if (self->data->use_render_texture) {
// Need to recreate RenderTexture with new size
self->data->enableRenderTexture(static_cast<unsigned int>(self->data->box.getSize().x),
static_cast<unsigned int>(self->data->box.getSize().y));
}
self->data->markDirty();
}
else if (member_ptr == 4) { //outline
self->data->box.setOutlineThickness(val);
self->data->markDirty();
}
return 0;
}
@ -200,10 +320,12 @@ int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* clos
if (member_ptr == 0)
{
self->data->box.setFillColor(sf::Color(r, g, b, a));
self->data->markDirty();
}
else if (member_ptr == 1)
{
self->data->box.setOutlineColor(sf::Color(r, g, b, a));
self->data->markDirty();
}
else
{
@ -214,17 +336,74 @@ int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* clos
return 0;
}
PyObject* UIFrame::get_pos(PyUIFrameObject* self, void* closure)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
if (obj) {
auto pos = self->data->box.getPosition();
obj->data = sf::Vector2f(pos.x, pos.y);
}
return (PyObject*)obj;
}
int UIFrame::set_pos(PyUIFrameObject* self, PyObject* value, void* closure)
{
PyVectorObject* vec = PyVector::from_arg(value);
if (!vec) {
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or convertible to Vector");
return -1;
}
self->data->box.setPosition(vec->data);
self->data->markDirty();
return 0;
}
PyObject* UIFrame::get_clip_children(PyUIFrameObject* self, void* closure)
{
return PyBool_FromLong(self->data->clip_children);
}
int UIFrame::set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "clip_children must be a boolean");
return -1;
}
bool new_clip = PyObject_IsTrue(value);
if (new_clip != self->data->clip_children) {
self->data->clip_children = new_clip;
self->data->markDirty(); // Mark as needing redraw
}
return 0;
}
// Define the PyObjectType alias for the macros
typedef PyUIFrameObject PyObjectType;
// Method definitions
PyMethodDef UIFrame_methods[] = {
UIDRAWABLE_METHODS,
{NULL} // Sentinel
};
PyGetSetDef UIFrame::getsetters[] = {
{"x", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "X coordinate of top-left corner", (void*)0},
{"y", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Y coordinate of top-left corner", (void*)1},
{"w", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "width of the rectangle", (void*)2},
{"h", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "height of the rectangle", (void*)3},
{"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "X coordinate of top-left corner", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 0)},
{"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "Y coordinate of top-left corner", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 1)},
{"w", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "width of the rectangle", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 2)},
{"h", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "height of the rectangle", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 3)},
{"outline", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Thickness of the border", (void*)4},
{"fill_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Fill color of the rectangle", (void*)0},
{"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1},
{"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIFRAME},
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UIFRAME},
{"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL},
UIDRAWABLE_GETSETTERS,
{NULL}
};
@ -250,18 +429,108 @@ PyObject* UIFrame::repr(PyUIFrameObject* self)
int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
{
//std::cout << "Init called\n";
const char* keywords[] = { "x", "y", "w", "h", "fill_color", "outline_color", "outline", nullptr };
float x = 0.0f, y = 0.0f, w = 0.0f, h=0.0f, outline=0.0f;
PyObject* fill_color = 0;
PyObject* outline_color = 0;
// Initialize children first
self->data->children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffff|OOf", const_cast<char**>(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline))
{
return -1;
// Try parsing with PyArgHelpers
int arg_idx = 0;
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx);
// Default values
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f, outline = 0.0f;
PyObject* fill_color = nullptr;
PyObject* outline_color = nullptr;
PyObject* children_arg = nullptr;
PyObject* click_handler = nullptr;
// Case 1: Got position and size from helpers (tuple format)
if (pos_result.valid && size_result.valid) {
x = pos_result.x;
y = pos_result.y;
w = size_result.w;
h = size_result.h;
// Parse remaining arguments
static const char* remaining_keywords[] = {
"fill_color", "outline_color", "outline", "children", "click", nullptr
};
// Create new tuple with remaining args
Py_ssize_t total_args = PyTuple_Size(args);
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OOfOO",
const_cast<char**>(remaining_keywords),
&fill_color, &outline_color, &outline,
&children_arg, &click_handler)) {
Py_DECREF(remaining_args);
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
else if (size_result.error) PyErr_SetString(PyExc_TypeError, size_result.error);
return -1;
}
Py_DECREF(remaining_args);
}
// Case 2: Traditional format (x, y, w, h, ...)
else {
PyErr_Clear(); // Clear any errors from helpers
static const char* keywords[] = {
"x", "y", "w", "h", "fill_color", "outline_color", "outline",
"children", "click", "pos", "size", nullptr
};
PyObject* pos_obj = nullptr;
PyObject* size_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOOO",
const_cast<char**>(keywords),
&x, &y, &w, &h, &fill_color, &outline_color,
&outline, &children_arg, &click_handler,
&pos_obj, &size_obj)) {
return -1;
}
// Handle pos keyword override
if (pos_obj && pos_obj != Py_None) {
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
}
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Vector"))) {
PyVectorObject* vec = (PyVectorObject*)pos_obj;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
}
}
// Handle size keyword override
if (size_obj && size_obj != Py_None) {
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
}
} else {
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
return -1;
}
}
}
self->data->box.setPosition(sf::Vector2f(x, y));
self->data->position = sf::Vector2f(x, y); // Set base class position
self->data->box.setPosition(self->data->position); // Sync box position
self->data->box.setSize(sf::Vector2f(w, h));
self->data->box.setOutlineThickness(outline);
// getsetter abuse because I haven't standardized Color object parsing (TODO)
@ -272,65 +541,154 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1);
else self->data->box.setOutlineColor(sf::Color(128,128,128,255));
if (err_val) return err_val;
// Process children argument if provided
if (children_arg && children_arg != Py_None) {
if (!PySequence_Check(children_arg)) {
PyErr_SetString(PyExc_TypeError, "children must be a sequence");
return -1;
}
Py_ssize_t len = PySequence_Length(children_arg);
for (Py_ssize_t i = 0; i < len; i++) {
PyObject* child = PySequence_GetItem(children_arg, i);
if (!child) return -1;
// Check if it's a UIDrawable (Frame, Caption, Sprite, or Grid)
PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
if (!PyObject_IsInstance(child, frame_type) &&
!PyObject_IsInstance(child, caption_type) &&
!PyObject_IsInstance(child, sprite_type) &&
!PyObject_IsInstance(child, grid_type)) {
Py_DECREF(child);
PyErr_SetString(PyExc_TypeError, "children must contain only Frame, Caption, Sprite, or Grid objects");
return -1;
}
// Get the shared_ptr and add to children
std::shared_ptr<UIDrawable> drawable = nullptr;
if (PyObject_IsInstance(child, frame_type)) {
drawable = ((PyUIFrameObject*)child)->data;
} else if (PyObject_IsInstance(child, caption_type)) {
drawable = ((PyUICaptionObject*)child)->data;
} else if (PyObject_IsInstance(child, sprite_type)) {
drawable = ((PyUISpriteObject*)child)->data;
} else if (PyObject_IsInstance(child, grid_type)) {
drawable = ((PyUIGridObject*)child)->data;
}
// Clean up type references
Py_DECREF(frame_type);
Py_DECREF(caption_type);
Py_DECREF(sprite_type);
Py_DECREF(grid_type);
if (drawable) {
self->data->children->push_back(drawable);
self->data->children_need_sort = true;
}
Py_DECREF(child);
}
}
// Process click handler if provided
if (click_handler && click_handler != Py_None) {
if (!PyCallable_Check(click_handler)) {
PyErr_SetString(PyExc_TypeError, "click must be callable");
return -1;
}
self->data->click_register(click_handler);
}
return 0;
}
// Animation property system implementation
bool UIFrame::setProperty(const std::string& name, float value) {
if (name == "x") {
box.setPosition(sf::Vector2f(value, box.getPosition().y));
position.x = value;
box.setPosition(position); // Keep box in sync
markDirty();
return true;
} else if (name == "y") {
box.setPosition(sf::Vector2f(box.getPosition().x, value));
position.y = value;
box.setPosition(position); // Keep box in sync
markDirty();
return true;
} else if (name == "w") {
box.setSize(sf::Vector2f(value, box.getSize().y));
if (use_render_texture) {
// Need to recreate RenderTexture with new size
enableRenderTexture(static_cast<unsigned int>(box.getSize().x),
static_cast<unsigned int>(box.getSize().y));
}
markDirty();
return true;
} else if (name == "h") {
box.setSize(sf::Vector2f(box.getSize().x, value));
if (use_render_texture) {
// Need to recreate RenderTexture with new size
enableRenderTexture(static_cast<unsigned int>(box.getSize().x),
static_cast<unsigned int>(box.getSize().y));
}
markDirty();
return true;
} else if (name == "outline") {
box.setOutlineThickness(value);
markDirty();
return true;
} else if (name == "fill_color.r") {
auto color = box.getFillColor();
color.r = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "fill_color.g") {
auto color = box.getFillColor();
color.g = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "fill_color.b") {
auto color = box.getFillColor();
color.b = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "fill_color.a") {
auto color = box.getFillColor();
color.a = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
markDirty();
return true;
} else if (name == "outline_color.r") {
auto color = box.getOutlineColor();
color.r = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
} else if (name == "outline_color.g") {
auto color = box.getOutlineColor();
color.g = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
} else if (name == "outline_color.b") {
auto color = box.getOutlineColor();
color.b = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
} else if (name == "outline_color.a") {
auto color = box.getOutlineColor();
color.a = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
markDirty();
return true;
}
return false;
@ -339,9 +697,11 @@ bool UIFrame::setProperty(const std::string& name, float value) {
bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
if (name == "fill_color") {
box.setFillColor(value);
markDirty();
return true;
} else if (name == "outline_color") {
box.setOutlineColor(value);
markDirty();
return true;
}
return false;
@ -349,10 +709,18 @@ bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "position") {
box.setPosition(value);
position = value;
box.setPosition(position); // Keep box in sync
markDirty();
return true;
} else if (name == "size") {
box.setSize(value);
if (use_render_texture) {
// Need to recreate RenderTexture with new size
enableRenderTexture(static_cast<unsigned int>(value.x),
static_cast<unsigned int>(value.y));
}
markDirty();
return true;
}
return false;
@ -360,10 +728,10 @@ bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
bool UIFrame::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = box.getPosition().x;
value = position.x;
return true;
} else if (name == "y") {
value = box.getPosition().y;
value = position.y;
return true;
} else if (name == "w") {
value = box.getSize().x;
@ -415,7 +783,7 @@ bool UIFrame::getProperty(const std::string& name, sf::Color& value) const {
bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const {
if (name == "position") {
value = box.getPosition();
value = position;
return true;
} else if (name == "size") {
value = box.getSize();

View File

@ -8,6 +8,7 @@
#include "PyCallable.h"
#include "PyColor.h"
#include "PyDrawable.h"
#include "PyVector.h"
#include "UIDrawable.h"
#include "UIBase.h"
@ -29,17 +30,28 @@ public:
float outline;
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
bool children_need_sort = true; // Dirty flag for z_index sorting optimization
bool clip_children = false; // Whether to clip children to frame bounds
void render(sf::Vector2f, sf::RenderTarget&) override final;
void move(sf::Vector2f);
PyObjectsEnum derived_type() override final;
virtual UIDrawable* click_at(sf::Vector2f point) override final;
// Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override;
void resize(float w, float h) override;
void onPositionChanged() override;
static PyObject* get_children(PyUIFrameObject* self, void* closure);
static PyObject* get_float_member(PyUIFrameObject* self, void* closure);
static int set_float_member(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_color_member(PyUIFrameObject* self, void* closure);
static int set_color_member(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_pos(PyUIFrameObject* self, void* closure);
static int set_pos(PyUIFrameObject* self, PyObject* value, void* closure);
static PyObject* get_clip_children(PyUIFrameObject* self, void* closure);
static int set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIFrameObject* self);
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);
@ -54,6 +66,9 @@ public:
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
};
// Forward declaration of methods array
extern PyMethodDef UIFrame_methods[];
namespace mcrfpydef {
static PyTypeObject PyUIFrameType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -71,11 +86,32 @@ namespace mcrfpydef {
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
//.tp_methods = PyUIFrame_methods,
.tp_doc = PyDoc_STR("Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)\n\n"
"A rectangular frame UI element that can contain other drawable elements.\n\n"
"Args:\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" w (float): Width in pixels. Default: 0\n"
" h (float): Height in pixels. Default: 0\n"
" fill_color (Color): Background fill color. Default: (0, 0, 0, 128)\n"
" outline_color (Color): Border outline color. Default: (255, 255, 255, 255)\n"
" outline (float): Border outline thickness. Default: 0\n"
" click (callable): Click event handler. Default: None\n"
" children (list): Initial list of child drawable elements. Default: None\n\n"
"Attributes:\n"
" x, y (float): Position in pixels\n"
" w, h (float): Size in pixels\n"
" fill_color, outline_color (Color): Visual appearance\n"
" outline (float): Border thickness\n"
" click (callable): Click event handler\n"
" children (list): Collection of child drawable elements\n"
" visible (bool): Visibility state\n"
" z_index (int): Rendering order\n"
" clip_children (bool): Whether to clip children to frame bounds"),
.tp_methods = UIFrame_methods,
//.tp_members = PyUIFrame_members,
.tp_getset = UIFrame::getsetters,
//.tp_base = NULL,
.tp_base = &mcrfpydef::PyDrawableType,
.tp_init = (initproc)UIFrame::init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{

File diff suppressed because it is too large Load Diff

View File

@ -5,9 +5,11 @@
#include "IndexTexture.h"
#include "Resources.h"
#include <list>
#include <libtcod.h>
#include "PyCallable.h"
#include "PyTexture.h"
#include "PyDrawable.h"
#include "PyColor.h"
#include "PyVector.h"
#include "PyFont.h"
@ -24,10 +26,15 @@ private:
// Default cell dimensions when no texture is provided
static constexpr int DEFAULT_CELL_WIDTH = 16;
static constexpr int DEFAULT_CELL_HEIGHT = 16;
TCODMap* tcod_map; // TCOD map for FOV and pathfinding
TCODDijkstra* tcod_dijkstra; // Dijkstra pathfinding
TCODPath* tcod_path; // A* pathfinding
public:
UIGrid();
//UIGrid(int, int, IndexTexture*, float, float, float, float);
UIGrid(int, int, std::shared_ptr<PyTexture>, sf::Vector2f, sf::Vector2f);
~UIGrid(); // Destructor to clean up TCOD map
void update();
void render(sf::Vector2f, sf::RenderTarget&) override final;
UIGridPoint& at(int, int);
@ -35,6 +42,27 @@ public:
//void setSprite(int);
virtual UIDrawable* click_at(sf::Vector2f point) override final;
// TCOD integration methods
void syncTCODMap(); // Sync entire map with current grid state
void syncTCODMapCell(int x, int y); // Sync a single cell to TCOD map
void computeFOV(int x, int y, int radius, bool light_walls = true, TCOD_fov_algorithm_t algo = FOV_BASIC);
bool isInFOV(int x, int y) const;
// Pathfinding methods
std::vector<std::pair<int, int>> findPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f);
void computeDijkstra(int rootX, int rootY, float diagonalCost = 1.41f);
float getDijkstraDistance(int x, int y) const;
std::vector<std::pair<int, int>> getDijkstraPath(int x, int y) const;
// A* pathfinding methods
std::vector<std::pair<int, int>> computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f);
// Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override;
void resize(float w, float h) override;
void onPositionChanged() override;
int grid_x, grid_y;
//int grid_size; // grid sizes are implied by IndexTexture now
sf::RectangleShape box;
@ -46,6 +74,12 @@ public:
std::vector<UIGridPoint> points;
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
// Background rendering
sf::Color fill_color;
// Perspective system - which entity's view to render (-1 = omniscient/default)
int perspective;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
@ -65,7 +99,18 @@ public:
static PyObject* get_float_member(PyUIGridObject* self, void* closure);
static int set_float_member(PyUIGridObject* self, PyObject* value, void* closure);
static PyObject* get_texture(PyUIGridObject* self, void* closure);
static PyObject* py_at(PyUIGridObject* self, PyObject* o);
static PyObject* get_fill_color(PyUIGridObject* self, void* closure);
static int set_fill_color(PyUIGridObject* self, PyObject* value, void* closure);
static PyObject* get_perspective(PyUIGridObject* self, void* closure);
static int set_perspective(PyUIGridObject* self, PyObject* value, void* closure);
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args);
static PyObject* py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args);
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args);
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
static PyObject* get_children(PyUIGridObject* self, void* closure);
@ -118,6 +163,9 @@ public:
};
// Forward declaration of methods array
extern PyMethodDef UIGrid_all_methods[];
namespace mcrfpydef {
static PyTypeObject PyUIGridType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
@ -136,11 +184,33 @@ namespace mcrfpydef {
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("docstring"),
.tp_methods = UIGrid::methods,
.tp_doc = PyDoc_STR("Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)\n\n"
"A grid-based tilemap UI element for rendering tile-based levels and game worlds.\n\n"
"Args:\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)\n"
" texture (Texture): Texture atlas containing tile sprites. Default: None\n"
" tile_width (int): Width of each tile in pixels. Default: 16\n"
" tile_height (int): Height of each tile in pixels. Default: 16\n"
" scale (float): Grid scaling factor. Default: 1.0\n"
" click (callable): Click event handler. Default: None\n\n"
"Attributes:\n"
" x, y (float): Position in pixels\n"
" grid_size (tuple): Grid dimensions (width, height) in tiles\n"
" tile_width, tile_height (int): Tile dimensions in pixels\n"
" texture (Texture): Tile texture atlas\n"
" scale (float): Scale multiplier\n"
" points (list): 2D array of GridPoint objects for tile data\n"
" entities (list): Collection of Entity objects in the grid\n"
" background_color (Color): Grid background color\n"
" click (callable): Click event handler\n"
" visible (bool): Visibility state\n"
" z_index (int): Rendering order"),
.tp_methods = UIGrid_all_methods,
//.tp_members = UIGrid::members,
.tp_getset = UIGrid::getsetters,
//.tp_base = NULL,
.tp_base = &mcrfpydef::PyDrawableType,
.tp_init = (initproc)UIGrid::init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{

View File

@ -1,19 +1,51 @@
#include "UIGridPoint.h"
#include "UIGrid.h"
UIGridPoint::UIGridPoint()
: color(1.0f, 1.0f, 1.0f), color_overlay(0.0f, 0.0f, 0.0f), walkable(false), transparent(false),
tilesprite(-1), tile_overlay(-1), uisprite(-1)
tilesprite(-1), tile_overlay(-1), uisprite(-1), grid_x(-1), grid_y(-1), parent_grid(nullptr)
{}
// Utility function to convert sf::Color to PyObject*
PyObject* sfColor_to_PyObject(sf::Color color) {
// For now, keep returning tuples to avoid breaking existing code
return Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a);
}
// Utility function to convert PyObject* to sf::Color
sf::Color PyObject_to_sfColor(PyObject* obj) {
// Get the mcrfpy module and Color type
PyObject* module = PyImport_ImportModule("mcrfpy");
if (!module) {
PyErr_SetString(PyExc_RuntimeError, "Failed to import mcrfpy module");
return sf::Color();
}
PyObject* color_type = PyObject_GetAttrString(module, "Color");
Py_DECREF(module);
if (!color_type) {
PyErr_SetString(PyExc_RuntimeError, "Failed to get Color type from mcrfpy module");
return sf::Color();
}
// Check if it's a mcrfpy.Color object
int is_color = PyObject_IsInstance(obj, color_type);
Py_DECREF(color_type);
if (is_color == 1) {
PyColorObject* color_obj = (PyColorObject*)obj;
return color_obj->data;
} else if (is_color == -1) {
// Error occurred in PyObject_IsInstance
return sf::Color();
}
// Otherwise try to parse as tuple
int r, g, b, a = 255; // Default alpha to fully opaque if not specified
if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) {
PyErr_Clear(); // Clear the error from failed tuple parsing
PyErr_SetString(PyExc_TypeError, "color must be a Color object or a tuple of (r, g, b[, a])");
return sf::Color(); // Return default color on parse error
}
return sf::Color(r, g, b, a);
@ -29,6 +61,11 @@ PyObject* UIGridPoint::get_color(PyUIGridPointObject* self, void* closure) {
int UIGridPoint::set_color(PyUIGridPointObject* self, PyObject* value, void* closure) {
sf::Color color = PyObject_to_sfColor(value);
// Check if an error occurred during conversion
if (PyErr_Occurred()) {
return -1;
}
if (reinterpret_cast<long>(closure) == 0) { // color
self->data->color = color;
} else { // color_overlay
@ -62,6 +99,12 @@ int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, voi
PyErr_SetString(PyExc_ValueError, "Expected a boolean value");
return -1;
}
// Sync with TCOD map if parent grid exists
if (self->data->parent_grid && self->data->grid_x >= 0 && self->data->grid_y >= 0) {
self->data->parent_grid->syncTCODMapCell(self->data->grid_x, self->data->grid_y);
}
return 0;
}

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