Author: John McCardle <mccardle.john@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
commit dc47f2474c7b2642d368f9772894aed857527807
the UIEntity rant
commit 673ca8e1b089ea670257fc04ae1a676ed95a40ed
I forget when these tests were written, but I want them in the squash merge
commit 70c71565c684fa96e222179271ecb13a156d80ad
Fix UI object segfault by switching from managed to manual weakref management
The UI types (Frame, Caption, Sprite, Grid, Entity) were using
Py_TPFLAGS_MANAGED_WEAKREF while also trying to manually create weakrefs
for the PythonObjectCache. This is fundamentally incompatible - when
Python manages weakrefs internally, PyWeakref_NewRef() cannot access the
weakref list properly, causing segfaults.
Changed all UI types to use manual weakref management (like PyTimer):
- Restored weakreflist field in all UI type structures
- Removed Py_TPFLAGS_MANAGED_WEAKREF from all UI type flags
- Added tp_weaklistoffset for all UI types in module initialization
- Initialize weakreflist=NULL in tp_new and init methods
- Call PyObject_ClearWeakRefs() in dealloc functions
This allows the PythonObjectCache to continue working correctly,
maintaining Python object identity for C++ objects across the boundary.
Fixes segfault when creating UI objects (e.g., Caption, Grid) that was
preventing tutorial scripts from running.
This is the bulk of the required behavior for Issue #126.
that issure isn't ready for closure yet; several other sub-issues left.
closes #110
mention issue #109 - resolves some __init__ related nuisances
commit 3dce3ec539ae99e32d869007bf3f49d03e4e2f89
Refactor timer system for cleaner architecture and enhanced functionality
Major improvements to the timer system:
- Unified all timer logic in the Timer class (C++)
- Removed PyTimerCallable subclass, now using PyCallable directly
- Timer objects are now passed to callbacks as first argument
- Added 'once' parameter for one-shot timers that auto-stop
- Implemented proper PythonObjectCache integration with weakref support
API enhancements:
- New callback signature: callback(timer, runtime) instead of just (runtime)
- Timer objects expose: name, interval, remaining, paused, active, once properties
- Methods: pause(), resume(), cancel(), restart()
- Comprehensive documentation with examples
- Enhanced repr showing timer state (active/paused/once/remaining time)
This cleanup follows the UIEntity/PyUIEntity pattern and makes the timer
system more Pythonic while maintaining backward compatibility through
the legacy setTimer/delTimer API.
closes #121
commit 145834cfc31b8dabc4cb3591b9cb4ed99fc8b964
Implement Python object cache to preserve derived types in collections
Add a global cache system that maintains weak references to Python objects,
ensuring that derived Python classes maintain their identity when stored in
and retrieved from C++ collections.
Key changes:
- Add PythonObjectCache singleton with serial number system
- Each cacheable object (UIDrawable, UIEntity, Timer, Animation) gets unique ID
- Cache stores weak references to prevent circular reference memory leaks
- Update all UI type definitions to support weak references (Py_TPFLAGS_MANAGED_WEAKREF)
- Enable subclassing for all UI types (Py_TPFLAGS_BASETYPE)
- Collections check cache before creating new Python wrappers
- Register objects in cache during __init__ methods
- Clean up cache entries in C++ destructors
This ensures that Python code like:
```python
class MyFrame(mcrfpy.Frame):
def __init__(self):
super().__init__()
self.custom_data = "preserved"
frame = MyFrame()
scene.ui.append(frame)
retrieved = scene.ui[0] # Same MyFrame instance with custom_data intact
```
Works correctly, with retrieved maintaining the derived type and custom attributes.
Closes #112
commit c5e7e8e298
Update test demos for new Python API and entity system
- Update all text input demos to use new Entity constructor signature
- Fix pathfinding showcase to work with new entity position handling
- Remove entity_waypoints tracking in favor of simplified movement
- Delete obsolete exhaustive_api_demo.py (superseded by newer demos)
- Adjust entity creation calls to match Entity((x, y), texture, sprite_index) pattern
commit 6d29652ae7
Update animation demo suite with crash fixes and improvements
- Add warnings about AnimationManager segfault bug in sizzle_reel_final.py
- Create sizzle_reel_final_fixed.py that works around the crash by hiding objects instead of removing them
- Increase font sizes for better visibility in demos
- Extend demo durations for better showcase of animations
- Remove debug prints from animation_sizzle_reel_working.py
- Minor cleanup and improvements to all animation demos
commit a010e5fa96
Update game scripts for new Python API
- Convert entity position access from tuple to x/y properties
- Update caption size property to font_size
- Fix grid boundary checks to use grid_size instead of exceptions
- Clean up demo timer on menu exit to prevent callbacks
These changes adapt the game scripts to work with the new standardized
Python API constructors and property names.
commit 9c8d6c4591
Fix click event z-order handling in PyScene
Changed click detection to properly respect z-index by:
- Sorting ui_elements in-place when needed (same as render order)
- Using reverse iterators to check highest z-index elements first
- This ensures top-most elements receive clicks before lower ones
commit dcd1b0ca33
Add roguelike tutorial implementation files
Implement Parts 0-2 of the classic roguelike tutorial adapted for McRogueFace:
- Part 0: Basic grid setup and tile rendering
- Part 1: Drawing '@' symbol and basic movement
- Part 1b: Variant with sprite-based player
- Part 2: Entity system and NPC implementation with three movement variants:
- part_2.py: Standard implementation
- part_2-naive.py: Naive movement approach
- part_2-onemovequeued.py: Queued movement system
Includes tutorial assets:
- tutorial2.png: Tileset for dungeon tiles
- tutorial_hero.png: Player sprite sheet
commit 6813fb5129
Standardize Python API constructors and remove PyArgHelpers
- Remove PyArgHelpers.h and all macro-based argument parsing
- Convert all UI class constructors to use PyArg_ParseTupleAndKeywords
- Standardize constructor signatures across UICaption, UIEntity, UIFrame, UIGrid, and UISprite
- Replace PYARGHELPER_SINGLE/MULTI macros with explicit argument parsing
- Improve error messages and argument validation
- Maintain backward compatibility with existing Python code
This change improves code maintainability and consistency across the Python API.
commit 6f67fbb51e
Fix animation callback crashes from iterator invalidation (#119)
Resolved segfaults caused by creating new animations from within
animation callbacks. The issue was iterator invalidation in
AnimationManager::update() when callbacks modified the active
animations vector.
Changes:
- Add deferred animation queue to AnimationManager
- New animations created during update are queued and added after
- Set isUpdating flag to track when in update loop
- Properly handle Animation destructor during callback execution
- Add clearCallback() method for safe cleanup scenarios
This fixes the "free(): invalid pointer" and "malloc(): unaligned
fastbin chunk detected" errors that occurred with rapid animation
creation in callbacks.
commit eb88c7b3aa
Add animation completion callbacks (#119)
Implement callbacks that fire when animations complete, enabling direct
causality between animation end and game state changes. This eliminates
race conditions from parallel timer workarounds.
- Add optional callback parameter to Animation constructor
- Callbacks execute synchronously when animation completes
- Proper Python reference counting with GIL safety
- Callbacks receive (anim, target) parameters (currently None)
- Exception handling prevents crashes from Python errors
Example usage:
```python
def on_complete(anim, target):
player_moving = False
anim = mcrfpy.Animation("x", 300.0, 1.0, "easeOut", callback=on_complete)
anim.start(player)
```
closes #119
commit 9fb428dd01
Update ROADMAP with GitHub issue numbers (#111-#125)
Added issue numbers from GitHub tracker to roadmap items:
- #111: Grid Click Events Broken in Headless
- #112: Object Splitting Bug (Python type preservation)
- #113: Batch Operations for Grid
- #114: CellView API
- #115: SpatialHash Implementation
- #116: Dirty Flag System
- #117: Memory Pool for Entities
- #118: Scene as Drawable
- #119: Animation Completion Callbacks
- #120: Animation Property Locking
- #121: Timer Object System
- #122: Parent-Child UI System
- #123: Grid Subgrid System
- #124: Grid Point Animation
- #125: GitHub Issues Automation
Also updated existing references:
- #101/#110: Constructor standardization
- #109: Vector class indexing
Note: Tutorial-specific items and Python-implementable features
(input queue, collision reservation) are not tracked as engine issues.
commit 062e4dadc4
Fix animation segfaults with RAII weak_ptr implementation
Resolved two critical segmentation faults in AnimationManager:
1. Race condition when creating multiple animations in timer callbacks
2. Exit crash when animations outlive their target objects
Changes:
- Replace raw pointers with std::weak_ptr for automatic target invalidation
- Add Animation::complete() to jump animations to final value
- Add Animation::hasValidTarget() to check if target still exists
- Update AnimationManager to auto-remove invalid animations
- Add AnimationManager::clear() call to GameEngine::cleanup()
- Update Python bindings to pass shared_ptr instead of raw pointers
This ensures animations can never reference destroyed objects, following
proper RAII principles. Tested with sizzle_reel_final.py and stress
tests creating/destroying hundreds of animated objects.
commit 98fc49a978
Directory structure cleanup and organization overhaul
1602 lines
No EOL
60 KiB
Python
1602 lines
No EOL
60 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate high-quality HTML API reference documentation for McRogueFace."""
|
|
|
|
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 format_docstring_as_html(docstring: str) -> str:
|
|
"""Convert docstring to properly formatted HTML."""
|
|
if not docstring:
|
|
return ""
|
|
|
|
# Split and process lines
|
|
lines = docstring.strip().split('\n')
|
|
result = []
|
|
in_code_block = False
|
|
|
|
for line in lines:
|
|
# Convert \n to actual newlines
|
|
line = line.replace('\\n', '\n')
|
|
|
|
# Handle code blocks
|
|
if line.strip().startswith('```'):
|
|
if in_code_block:
|
|
result.append('</pre></code>')
|
|
in_code_block = False
|
|
else:
|
|
result.append('<code><pre>')
|
|
in_code_block = True
|
|
continue
|
|
|
|
# Convert markdown-style code to HTML
|
|
if '`' in line and not in_code_block:
|
|
import re
|
|
line = re.sub(r'`([^`]+)`', r'<code>\1</code>', line)
|
|
|
|
if in_code_block:
|
|
result.append(escape_html(line))
|
|
else:
|
|
result.append(escape_html(line) + '<br>')
|
|
|
|
if in_code_block:
|
|
result.append('</pre></code>')
|
|
|
|
return '\n'.join(result)
|
|
|
|
def get_class_details(cls):
|
|
"""Get detailed information about a class."""
|
|
info = {
|
|
'name': cls.__name__,
|
|
'doc': cls.__doc__ or "",
|
|
'methods': {},
|
|
'properties': {},
|
|
'bases': []
|
|
}
|
|
|
|
# Get real base classes (excluding object)
|
|
for base in cls.__bases__:
|
|
if base.__name__ != 'object':
|
|
info['bases'].append(base.__name__)
|
|
|
|
# Special handling for Entity which doesn't inherit from Drawable
|
|
if cls.__name__ == 'Entity' and 'Drawable' in info['bases']:
|
|
info['bases'].remove('Drawable')
|
|
|
|
# Get methods and properties
|
|
for attr_name in dir(cls):
|
|
if attr_name.startswith('__') and attr_name != '__init__':
|
|
continue
|
|
|
|
try:
|
|
attr = getattr(cls, attr_name)
|
|
|
|
if isinstance(attr, property):
|
|
info['properties'][attr_name] = {
|
|
'doc': (attr.fget.__doc__ if attr.fget else "") or "",
|
|
'readonly': attr.fset is None
|
|
}
|
|
elif callable(attr) and not attr_name.startswith('_'):
|
|
info['methods'][attr_name] = attr.__doc__ or ""
|
|
except:
|
|
pass
|
|
|
|
return info
|
|
|
|
def generate_class_init_docs(class_name):
|
|
"""Generate initialization documentation for specific classes."""
|
|
init_docs = {
|
|
'Entity': {
|
|
'signature': 'Entity(x=0, y=0, sprite_id=0)',
|
|
'description': 'Game entity that can be placed in a Grid.',
|
|
'args': [
|
|
('x', 'int', 'Grid x coordinate. Default: 0'),
|
|
('y', 'int', 'Grid y coordinate. Default: 0'),
|
|
('sprite_id', 'int', 'Sprite index for rendering. Default: 0')
|
|
],
|
|
'example': '''entity = mcrfpy.Entity(5, 10, 42)
|
|
entity.move(1, 0) # Move right one tile'''
|
|
},
|
|
'Color': {
|
|
'signature': 'Color(r=255, g=255, b=255, a=255)',
|
|
'description': 'RGBA color representation.',
|
|
'args': [
|
|
('r', 'int', 'Red component (0-255). Default: 255'),
|
|
('g', 'int', 'Green component (0-255). Default: 255'),
|
|
('b', 'int', 'Blue component (0-255). Default: 255'),
|
|
('a', 'int', 'Alpha component (0-255). Default: 255')
|
|
],
|
|
'example': 'red = mcrfpy.Color(255, 0, 0)'
|
|
},
|
|
'Font': {
|
|
'signature': 'Font(filename)',
|
|
'description': 'Load a font from file.',
|
|
'args': [
|
|
('filename', 'str', 'Path to font file (TTF/OTF)')
|
|
]
|
|
},
|
|
'Texture': {
|
|
'signature': 'Texture(filename)',
|
|
'description': 'Load a texture from file.',
|
|
'args': [
|
|
('filename', 'str', 'Path to image file (PNG/JPG/BMP)')
|
|
]
|
|
},
|
|
'Vector': {
|
|
'signature': 'Vector(x=0.0, y=0.0)',
|
|
'description': '2D vector for positions and directions.',
|
|
'args': [
|
|
('x', 'float', 'X component. Default: 0.0'),
|
|
('y', 'float', 'Y component. Default: 0.0')
|
|
]
|
|
},
|
|
'Animation': {
|
|
'signature': 'Animation(property_name, start_value, end_value, duration, transition="linear", loop=False)',
|
|
'description': 'Animate UI element properties over time.',
|
|
'args': [
|
|
('property_name', 'str', 'Property to animate (e.g., "x", "y", "scale")'),
|
|
('start_value', 'float', 'Starting value'),
|
|
('end_value', 'float', 'Ending value'),
|
|
('duration', 'float', 'Duration in seconds'),
|
|
('transition', 'str', 'Easing function. Default: "linear"'),
|
|
('loop', 'bool', 'Whether to loop. Default: False')
|
|
],
|
|
'properties': ['current_value', 'elapsed_time', 'is_running', 'is_finished']
|
|
},
|
|
'GridPoint': {
|
|
'description': 'Represents a single tile in a Grid.',
|
|
'properties': ['x', 'y', 'texture_index', 'solid', 'transparent', 'color']
|
|
},
|
|
'GridPointState': {
|
|
'description': 'State information for a GridPoint.',
|
|
'properties': ['visible', 'discovered', 'custom_flags']
|
|
},
|
|
'Timer': {
|
|
'signature': 'Timer(name, callback, interval_ms)',
|
|
'description': 'Create a recurring timer.',
|
|
'args': [
|
|
('name', 'str', 'Unique timer identifier'),
|
|
('callback', 'callable', 'Function to call'),
|
|
('interval_ms', 'int', 'Interval in milliseconds')
|
|
]
|
|
}
|
|
}
|
|
|
|
return init_docs.get(class_name, {})
|
|
|
|
def generate_method_docs(method_name, class_name):
|
|
"""Generate documentation for specific methods."""
|
|
method_docs = {
|
|
# Base Drawable methods (inherited by all UI elements)
|
|
'Drawable': {
|
|
'get_bounds': {
|
|
'signature': 'get_bounds()',
|
|
'description': 'Get the bounding rectangle of this drawable element.',
|
|
'returns': 'tuple: (x, y, width, height) representing the element\'s bounds',
|
|
'note': 'The bounds are in screen coordinates and account for current position and size.'
|
|
},
|
|
'move': {
|
|
'signature': 'move(dx, dy)',
|
|
'description': 'Move the element by a relative offset.',
|
|
'args': [
|
|
('dx', 'float', 'Horizontal offset in pixels'),
|
|
('dy', 'float', 'Vertical offset in pixels')
|
|
],
|
|
'note': 'This modifies the x and y position properties by the given amounts.'
|
|
},
|
|
'resize': {
|
|
'signature': 'resize(width, height)',
|
|
'description': 'Resize the element to new dimensions.',
|
|
'args': [
|
|
('width', 'float', 'New width in pixels'),
|
|
('height', 'float', 'New height in pixels')
|
|
],
|
|
'note': 'Behavior varies by element type. Some elements may ignore or constrain dimensions.'
|
|
}
|
|
},
|
|
|
|
# Caption-specific methods
|
|
'Caption': {
|
|
'get_bounds': {
|
|
'signature': 'get_bounds()',
|
|
'description': 'Get the bounding rectangle of the text.',
|
|
'returns': 'tuple: (x, y, width, height) based on text content and font size',
|
|
'note': 'Bounds are automatically calculated from the rendered text dimensions.'
|
|
},
|
|
'move': {
|
|
'signature': 'move(dx, dy)',
|
|
'description': 'Move the caption by a relative offset.',
|
|
'args': [
|
|
('dx', 'float', 'Horizontal offset in pixels'),
|
|
('dy', 'float', 'Vertical offset in pixels')
|
|
]
|
|
},
|
|
'resize': {
|
|
'signature': 'resize(width, height)',
|
|
'description': 'Set text wrapping bounds (limited support).',
|
|
'args': [
|
|
('width', 'float', 'Maximum width for text wrapping'),
|
|
('height', 'float', 'Currently unused')
|
|
],
|
|
'note': 'Full text wrapping is not yet implemented. This prepares for future multiline support.'
|
|
}
|
|
},
|
|
|
|
# Entity-specific methods
|
|
'Entity': {
|
|
'at': {
|
|
'signature': 'at(x, y)',
|
|
'description': 'Get the GridPointState at the specified grid coordinates relative to this entity.',
|
|
'args': [
|
|
('x', 'int', 'Grid x offset from entity position'),
|
|
('y', 'int', 'Grid y offset from entity position')
|
|
],
|
|
'returns': 'GridPointState: State of the grid point at the specified position',
|
|
'note': 'Requires entity to be associated with a grid. Raises ValueError if not.'
|
|
},
|
|
'die': {
|
|
'signature': 'die()',
|
|
'description': 'Remove this entity from its parent grid.',
|
|
'returns': 'None',
|
|
'note': 'The entity object remains valid but is no longer rendered or updated.'
|
|
},
|
|
'index': {
|
|
'signature': 'index()',
|
|
'description': 'Get the index of this entity in its grid\'s entity collection.',
|
|
'returns': 'int: Zero-based index in the parent grid\'s entity list',
|
|
'note': 'Raises RuntimeError if not associated with a grid, ValueError if not found.'
|
|
},
|
|
'get_bounds': {
|
|
'signature': 'get_bounds()',
|
|
'description': 'Get the bounding rectangle of the entity\'s sprite.',
|
|
'returns': 'tuple: (x, y, width, height) of the sprite bounds',
|
|
'note': 'Delegates to the internal sprite\'s get_bounds method.'
|
|
},
|
|
'move': {
|
|
'signature': 'move(dx, dy)',
|
|
'description': 'Move the entity by a relative offset in pixels.',
|
|
'args': [
|
|
('dx', 'float', 'Horizontal offset in pixels'),
|
|
('dy', 'float', 'Vertical offset in pixels')
|
|
],
|
|
'note': 'Updates both sprite position and entity grid position.'
|
|
},
|
|
'resize': {
|
|
'signature': 'resize(width, height)',
|
|
'description': 'Entities do not support direct resizing.',
|
|
'args': [
|
|
('width', 'float', 'Ignored'),
|
|
('height', 'float', 'Ignored')
|
|
],
|
|
'note': 'This method exists for interface compatibility but has no effect.'
|
|
}
|
|
},
|
|
|
|
# Frame-specific methods
|
|
'Frame': {
|
|
'get_bounds': {
|
|
'signature': 'get_bounds()',
|
|
'description': 'Get the bounding rectangle of the frame.',
|
|
'returns': 'tuple: (x, y, width, height) representing the frame bounds'
|
|
},
|
|
'move': {
|
|
'signature': 'move(dx, dy)',
|
|
'description': 'Move the frame and all its children by a relative offset.',
|
|
'args': [
|
|
('dx', 'float', 'Horizontal offset in pixels'),
|
|
('dy', 'float', 'Vertical offset in pixels')
|
|
],
|
|
'note': 'Child elements maintain their relative positions within the frame.'
|
|
},
|
|
'resize': {
|
|
'signature': 'resize(width, height)',
|
|
'description': 'Resize the frame to new dimensions.',
|
|
'args': [
|
|
('width', 'float', 'New width in pixels'),
|
|
('height', 'float', 'New height in pixels')
|
|
],
|
|
'note': 'Does not automatically resize children. Set clip_children=True to clip overflow.'
|
|
}
|
|
},
|
|
|
|
# Grid-specific methods
|
|
'Grid': {
|
|
'at': {
|
|
'signature': 'at(x, y) or at((x, y))',
|
|
'description': 'Get the GridPoint at the specified grid coordinates.',
|
|
'args': [
|
|
('x', 'int', 'Grid x coordinate (0-based)'),
|
|
('y', 'int', 'Grid y coordinate (0-based)')
|
|
],
|
|
'returns': 'GridPoint: The grid point at (x, y)',
|
|
'note': 'Raises IndexError if coordinates are out of range. Accepts either two arguments or a tuple.',
|
|
'example': 'point = grid.at(5, 3) # or grid.at((5, 3))'
|
|
},
|
|
'get_bounds': {
|
|
'signature': 'get_bounds()',
|
|
'description': 'Get the bounding rectangle of the entire grid.',
|
|
'returns': 'tuple: (x, y, width, height) of the grid\'s display area'
|
|
},
|
|
'move': {
|
|
'signature': 'move(dx, dy)',
|
|
'description': 'Move the grid display by a relative offset.',
|
|
'args': [
|
|
('dx', 'float', 'Horizontal offset in pixels'),
|
|
('dy', 'float', 'Vertical offset in pixels')
|
|
],
|
|
'note': 'Moves the entire grid viewport. Use center property to pan within the grid.'
|
|
},
|
|
'resize': {
|
|
'signature': 'resize(width, height)',
|
|
'description': 'Resize the grid\'s display viewport.',
|
|
'args': [
|
|
('width', 'float', 'New viewport width in pixels'),
|
|
('height', 'float', 'New viewport height in pixels')
|
|
],
|
|
'note': 'Changes the visible area, not the grid dimensions. Use zoom to scale content.'
|
|
}
|
|
},
|
|
|
|
# Sprite-specific methods
|
|
'Sprite': {
|
|
'get_bounds': {
|
|
'signature': 'get_bounds()',
|
|
'description': 'Get the bounding rectangle of the sprite.',
|
|
'returns': 'tuple: (x, y, width, height) based on texture size and scale',
|
|
'note': 'Bounds account for current scale. Returns (x, y, 0, 0) if no texture.'
|
|
},
|
|
'move': {
|
|
'signature': 'move(dx, dy)',
|
|
'description': 'Move the sprite by a relative offset.',
|
|
'args': [
|
|
('dx', 'float', 'Horizontal offset in pixels'),
|
|
('dy', 'float', 'Vertical offset in pixels')
|
|
]
|
|
},
|
|
'resize': {
|
|
'signature': 'resize(width, height)',
|
|
'description': 'Resize the sprite by adjusting its scale.',
|
|
'args': [
|
|
('width', 'float', 'Target width in pixels'),
|
|
('height', 'float', 'Target height in pixels')
|
|
],
|
|
'note': 'Calculates and applies uniform scale to best fit the target dimensions.'
|
|
}
|
|
},
|
|
|
|
'Animation': {
|
|
'get_current_value': {
|
|
'signature': 'get_current_value()',
|
|
'description': 'Get the current interpolated value.',
|
|
'returns': 'float: Current animation value'
|
|
},
|
|
'start': {
|
|
'signature': 'start(target)',
|
|
'description': 'Start the animation on a target UI element.',
|
|
'args': [('target', 'UIDrawable', 'The element to animate')]
|
|
}
|
|
},
|
|
|
|
# Collection methods (shared by EntityCollection and UICollection)
|
|
'EntityCollection': {
|
|
'append': {
|
|
'signature': 'append(entity)',
|
|
'description': 'Add an entity to the end of the collection.',
|
|
'args': [
|
|
('entity', 'Entity', 'The entity to add')
|
|
]
|
|
},
|
|
'remove': {
|
|
'signature': 'remove(entity)',
|
|
'description': 'Remove the first occurrence of an entity from the collection.',
|
|
'args': [
|
|
('entity', 'Entity', 'The entity to remove')
|
|
],
|
|
'note': 'Raises ValueError if entity is not found.'
|
|
},
|
|
'extend': {
|
|
'signature': 'extend(iterable)',
|
|
'description': 'Add multiple entities from an iterable.',
|
|
'args': [
|
|
('iterable', 'iterable', 'An iterable of Entity objects')
|
|
]
|
|
},
|
|
'count': {
|
|
'signature': 'count(entity)',
|
|
'description': 'Count occurrences of an entity in the collection.',
|
|
'args': [
|
|
('entity', 'Entity', 'The entity to count')
|
|
],
|
|
'returns': 'int: Number of times the entity appears'
|
|
},
|
|
'index': {
|
|
'signature': 'index(entity)',
|
|
'description': 'Find the index of the first occurrence of an entity.',
|
|
'args': [
|
|
('entity', 'Entity', 'The entity to find')
|
|
],
|
|
'returns': 'int: Zero-based index of the entity',
|
|
'note': 'Raises ValueError if entity is not found.'
|
|
}
|
|
},
|
|
|
|
'UICollection': {
|
|
'append': {
|
|
'signature': 'append(drawable)',
|
|
'description': 'Add a drawable element to the end of the collection.',
|
|
'args': [
|
|
('drawable', 'Drawable', 'Any UI element (Frame, Caption, Sprite, Grid)')
|
|
]
|
|
},
|
|
'remove': {
|
|
'signature': 'remove(drawable)',
|
|
'description': 'Remove the first occurrence of a drawable from the collection.',
|
|
'args': [
|
|
('drawable', 'Drawable', 'The drawable to remove')
|
|
],
|
|
'note': 'Raises ValueError if drawable is not found.'
|
|
},
|
|
'extend': {
|
|
'signature': 'extend(iterable)',
|
|
'description': 'Add multiple drawables from an iterable.',
|
|
'args': [
|
|
('iterable', 'iterable', 'An iterable of Drawable objects')
|
|
]
|
|
},
|
|
'count': {
|
|
'signature': 'count(drawable)',
|
|
'description': 'Count occurrences of a drawable in the collection.',
|
|
'args': [
|
|
('drawable', 'Drawable', 'The drawable to count')
|
|
],
|
|
'returns': 'int: Number of times the drawable appears'
|
|
},
|
|
'index': {
|
|
'signature': 'index(drawable)',
|
|
'description': 'Find the index of the first occurrence of a drawable.',
|
|
'args': [
|
|
('drawable', 'Drawable', 'The drawable to find')
|
|
],
|
|
'returns': 'int: Zero-based index of the drawable',
|
|
'note': 'Raises ValueError if drawable is not found.'
|
|
}
|
|
}
|
|
}
|
|
|
|
return method_docs.get(class_name, {}).get(method_name, {})
|
|
|
|
def generate_function_docs():
|
|
"""Generate documentation for all mcrfpy module functions."""
|
|
function_docs = {
|
|
# Scene Management
|
|
'createScene': {
|
|
'signature': 'createScene(name: str) -> None',
|
|
'description': 'Create a new empty scene.',
|
|
'args': [
|
|
('name', 'str', 'Unique name for the new scene')
|
|
],
|
|
'returns': 'None',
|
|
'exceptions': [
|
|
('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")
|
|
mcrfpy.createScene("menu")
|
|
mcrfpy.setScene("game")'''
|
|
},
|
|
|
|
'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"). Default: None'),
|
|
('duration', 'float', 'Transition duration in seconds. Default: 0.0 for instant')
|
|
],
|
|
'returns': 'None',
|
|
'exceptions': [
|
|
('KeyError', 'If the scene doesn\'t exist'),
|
|
('ValueError', 'If the transition type is invalid')
|
|
],
|
|
'example': '''mcrfpy.setScene("menu")
|
|
mcrfpy.setScene("game", "fade", 0.5)
|
|
mcrfpy.setScene("credits", "slide_left", 1.0)'''
|
|
},
|
|
|
|
'currentScene': {
|
|
'signature': 'currentScene() -> str',
|
|
'description': 'Get the name of the currently active scene.',
|
|
'args': [],
|
|
'returns': 'str: Name of the current scene',
|
|
'example': '''scene = mcrfpy.currentScene()
|
|
print(f"Currently in scene: {scene}")'''
|
|
},
|
|
|
|
'sceneUI': {
|
|
'signature': 'sceneUI(scene: str = None) -> list',
|
|
'description': 'Get all UI elements for a scene.',
|
|
'args': [
|
|
('scene', 'str', 'Scene name. If None, uses current scene. Default: None')
|
|
],
|
|
'returns': 'list: All UI elements (Frame, Caption, Sprite, Grid) in the scene',
|
|
'exceptions': [
|
|
('KeyError', 'If the specified scene doesn\'t exist')
|
|
],
|
|
'example': '''# Get UI for current scene
|
|
ui_elements = mcrfpy.sceneUI()
|
|
|
|
# Get UI for specific scene
|
|
menu_ui = mcrfpy.sceneUI("menu")
|
|
for element in menu_ui:
|
|
print(f"{element.name}: {type(element).__name__}")'''
|
|
},
|
|
|
|
'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)')
|
|
],
|
|
'returns': 'None',
|
|
'note': 'The handler is called for every key press and release event. Key names are single characters (e.g., "A", "1") or special keys (e.g., "Space", "Enter", "Escape").',
|
|
'example': '''def on_key(key, pressed):
|
|
if pressed:
|
|
if key == "Space":
|
|
player.jump()
|
|
elif key == "Escape":
|
|
mcrfpy.setScene("pause_menu")
|
|
else:
|
|
# Handle key release
|
|
if key in ["A", "D"]:
|
|
player.stop_moving()
|
|
|
|
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()',
|
|
'exceptions': [
|
|
('RuntimeError', 'If the file cannot be loaded')
|
|
],
|
|
'note': 'Sound buffers are stored in memory for fast playback. Load sound effects once and reuse the buffer ID.',
|
|
'example': '''# Load sound effects
|
|
jump_sound = mcrfpy.createSoundBuffer("assets/sounds/jump.wav")
|
|
coin_sound = mcrfpy.createSoundBuffer("assets/sounds/coin.ogg")
|
|
|
|
# Play later
|
|
mcrfpy.playSound(jump_sound)'''
|
|
},
|
|
|
|
'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')
|
|
],
|
|
'returns': 'None',
|
|
'note': 'Only one music track can play at a time. Loading new music stops the current track.',
|
|
'example': '''# Play looping background music
|
|
mcrfpy.loadMusic("assets/music/theme.ogg")
|
|
|
|
# Play music once without looping
|
|
mcrfpy.loadMusic("assets/music/victory.ogg", loop=False)'''
|
|
},
|
|
|
|
'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()')
|
|
],
|
|
'returns': 'None',
|
|
'exceptions': [
|
|
('RuntimeError', 'If the buffer ID is invalid')
|
|
],
|
|
'note': 'Multiple sounds can play simultaneously. Each call creates a new sound instance.',
|
|
'example': '''# Load once
|
|
explosion_sound = mcrfpy.createSoundBuffer("explosion.wav")
|
|
|
|
# Play multiple times
|
|
for enemy in destroyed_enemies:
|
|
mcrfpy.playSound(explosion_sound)'''
|
|
},
|
|
|
|
'getMusicVolume': {
|
|
'signature': 'getMusicVolume() -> int',
|
|
'description': 'Get the current music volume level.',
|
|
'args': [],
|
|
'returns': 'int: Current volume (0-100)',
|
|
'example': '''volume = mcrfpy.getMusicVolume()
|
|
print(f"Music volume: {volume}%")'''
|
|
},
|
|
|
|
'getSoundVolume': {
|
|
'signature': 'getSoundVolume() -> int',
|
|
'description': 'Get the current sound effects volume level.',
|
|
'args': [],
|
|
'returns': 'int: Current volume (0-100)',
|
|
'example': '''volume = mcrfpy.getSoundVolume()
|
|
print(f"Sound effects volume: {volume}%")'''
|
|
},
|
|
|
|
'setMusicVolume': {
|
|
'signature': 'setMusicVolume(volume: int) -> None',
|
|
'description': 'Set the global music volume.',
|
|
'args': [
|
|
('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')
|
|
],
|
|
'returns': 'None',
|
|
'example': '''# Mute music
|
|
mcrfpy.setMusicVolume(0)
|
|
|
|
# Half volume
|
|
mcrfpy.setMusicVolume(50)
|
|
|
|
# Full volume
|
|
mcrfpy.setMusicVolume(100)'''
|
|
},
|
|
|
|
'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)')
|
|
],
|
|
'returns': 'None',
|
|
'example': '''# Audio settings from options menu
|
|
mcrfpy.setSoundVolume(sound_slider.value)
|
|
mcrfpy.setMusicVolume(music_slider.value)'''
|
|
},
|
|
|
|
# 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': 'Frame, Caption, Sprite, Grid, or Entity if found; None otherwise',
|
|
'note': 'Searches scene UI elements and entities within grids. Returns the first match found.',
|
|
'example': '''# Find in current scene
|
|
player = mcrfpy.find("player")
|
|
if player:
|
|
player.x = 100
|
|
|
|
# Find in specific scene
|
|
menu_button = mcrfpy.find("start_button", "main_menu")'''
|
|
},
|
|
|
|
'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',
|
|
'note': 'Supports wildcard patterns for flexible searching.',
|
|
'example': '''# Find all enemies
|
|
enemies = mcrfpy.findAll("enemy*")
|
|
for enemy in enemies:
|
|
enemy.sprite_id = 0 # Reset sprite
|
|
|
|
# Find all buttons
|
|
buttons = mcrfpy.findAll("*_button")
|
|
for btn in buttons:
|
|
btn.visible = True
|
|
|
|
# Find exact matches
|
|
health_bars = mcrfpy.findAll("health_bar") # No wildcards = exact match'''
|
|
},
|
|
|
|
# System Functions
|
|
'exit': {
|
|
'signature': 'exit() -> None',
|
|
'description': 'Cleanly shut down the game engine and exit the application.',
|
|
'args': [],
|
|
'returns': 'None',
|
|
'note': 'This immediately closes the window and terminates the program. Ensure any necessary cleanup is done before calling.',
|
|
'example': '''def quit_game():
|
|
# Save game state
|
|
save_progress()
|
|
|
|
# Exit
|
|
mcrfpy.exit()'''
|
|
},
|
|
|
|
'getMetrics': {
|
|
'signature': 'getMetrics() -> dict',
|
|
'description': 'Get current performance metrics.',
|
|
'args': [],
|
|
'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()
|
|
print(f"FPS: {metrics['fps']}")
|
|
print(f"Frame time: {metrics['frame_time']*1000:.1f}ms")
|
|
print(f"Draw calls: {metrics['draw_calls']}")
|
|
print(f"Runtime: {metrics['runtime']:.1f}s")
|
|
|
|
# Performance monitoring
|
|
if metrics['fps'] < 30:
|
|
print("Performance warning: FPS below 30")'''
|
|
},
|
|
|
|
'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')
|
|
],
|
|
'returns': 'None',
|
|
'note': 'If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.',
|
|
'example': '''# Simple repeating timer
|
|
def spawn_enemy(runtime):
|
|
enemy = mcrfpy.Entity()
|
|
enemy.x = random.randint(0, 800)
|
|
grid.entities.append(enemy)
|
|
|
|
mcrfpy.setTimer("enemy_spawner", spawn_enemy, 2000) # Every 2 seconds
|
|
|
|
# Timer with runtime check
|
|
def update_timer(runtime):
|
|
time_left = 60 - runtime
|
|
timer_text.text = f"Time: {int(time_left)}"
|
|
if time_left <= 0:
|
|
mcrfpy.delTimer("game_timer")
|
|
game_over()
|
|
|
|
mcrfpy.setTimer("game_timer", update_timer, 100) # Update every 100ms'''
|
|
},
|
|
|
|
'delTimer': {
|
|
'signature': 'delTimer(name: str) -> None',
|
|
'description': 'Stop and remove a timer.',
|
|
'args': [
|
|
('name', 'str', 'Timer identifier to remove')
|
|
],
|
|
'returns': 'None',
|
|
'note': 'No error is raised if the timer doesn\'t exist.',
|
|
'example': '''# Stop spawning enemies
|
|
mcrfpy.delTimer("enemy_spawner")
|
|
|
|
# Clean up all game timers
|
|
for timer_name in ["enemy_spawner", "powerup_timer", "score_updater"]:
|
|
mcrfpy.delTimer(timer_name)'''
|
|
},
|
|
|
|
'setScale': {
|
|
'signature': 'setScale(multiplier: float) -> None',
|
|
'description': 'Scale the game window size.',
|
|
'args': [
|
|
('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)')
|
|
],
|
|
'returns': 'None',
|
|
'exceptions': [
|
|
('ValueError', 'If multiplier is not between 0.2 and 4.0')
|
|
],
|
|
'note': 'The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.',
|
|
'example': '''# Double the window size
|
|
mcrfpy.setScale(2.0)
|
|
|
|
# Half size window
|
|
mcrfpy.setScale(0.5)
|
|
|
|
# Better approach (not deprecated):
|
|
mcrfpy.Window.resolution = (1920, 1080)'''
|
|
}
|
|
}
|
|
|
|
return function_docs
|
|
|
|
def generate_collection_docs(class_name):
|
|
"""Generate documentation for collection classes."""
|
|
collection_docs = {
|
|
'EntityCollection': {
|
|
'description': 'Container for Entity objects in a Grid. Supports iteration and indexing.',
|
|
'methods': {
|
|
'append': 'Add an entity to the collection',
|
|
'remove': 'Remove an entity from the collection',
|
|
'extend': 'Add multiple entities from an iterable',
|
|
'count': 'Count occurrences of an entity',
|
|
'index': 'Find the index of an entity'
|
|
}
|
|
},
|
|
'UICollection': {
|
|
'description': 'Container for UI drawable elements. Supports iteration and indexing.',
|
|
'methods': {
|
|
'append': 'Add a UI element to the collection',
|
|
'remove': 'Remove a UI element from the collection',
|
|
'extend': 'Add multiple UI elements from an iterable',
|
|
'count': 'Count occurrences of a UI element',
|
|
'index': 'Find the index of a UI element'
|
|
}
|
|
},
|
|
'UICollectionIter': {
|
|
'description': 'Iterator for UICollection. Automatically created when iterating over a UICollection.'
|
|
},
|
|
'UIEntityCollectionIter': {
|
|
'description': 'Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.'
|
|
}
|
|
}
|
|
|
|
return collection_docs.get(class_name, {})
|
|
|
|
def format_class_html(cls_info, class_name):
|
|
"""Format a class as HTML with proper structure."""
|
|
html_parts = []
|
|
|
|
# Class header
|
|
html_parts.append(f'<div class="class-section" id="class-{class_name}">')
|
|
html_parts.append(f'<h3>class <span class="class-name">{class_name}</span></h3>')
|
|
|
|
# Inheritance
|
|
if cls_info['bases']:
|
|
html_parts.append(f'<p class="inheritance"><em>Inherits from: {", ".join(cls_info["bases"])}</em></p>')
|
|
|
|
# Get additional documentation
|
|
init_info = generate_class_init_docs(class_name)
|
|
collection_info = generate_collection_docs(class_name)
|
|
|
|
# Constructor signature for classes with __init__
|
|
if init_info.get('signature'):
|
|
html_parts.append('<div class="constructor">')
|
|
html_parts.append('<pre><code class="language-python">')
|
|
html_parts.append(escape_html(init_info['signature']))
|
|
html_parts.append('</code></pre>')
|
|
html_parts.append('</div>')
|
|
|
|
# Description
|
|
description = ""
|
|
if collection_info.get('description'):
|
|
description = collection_info['description']
|
|
elif init_info.get('description'):
|
|
description = init_info['description']
|
|
elif cls_info['doc']:
|
|
# Parse description from docstring
|
|
doc_lines = cls_info['doc'].strip().split('\n')
|
|
# Skip constructor line if present
|
|
start_idx = 1 if doc_lines and '(' in doc_lines[0] else 0
|
|
if start_idx < len(doc_lines):
|
|
description = '\n'.join(doc_lines[start_idx:]).strip()
|
|
|
|
if description:
|
|
html_parts.append('<div class="description">')
|
|
html_parts.append(f'<p>{format_docstring_as_html(description)}</p>')
|
|
html_parts.append('</div>')
|
|
|
|
# Constructor arguments
|
|
if init_info.get('args'):
|
|
html_parts.append('<div class="arguments">')
|
|
html_parts.append('<h4>Arguments:</h4>')
|
|
html_parts.append('<dl>')
|
|
for arg_name, arg_type, arg_desc in init_info['args']:
|
|
html_parts.append(f'<dt><code>{arg_name}</code> (<em>{arg_type}</em>)</dt>')
|
|
html_parts.append(f'<dd>{escape_html(arg_desc)}</dd>')
|
|
html_parts.append('</dl>')
|
|
html_parts.append('</div>')
|
|
|
|
# Properties/Attributes
|
|
props = cls_info.get('properties', {})
|
|
if props or init_info.get('properties'):
|
|
html_parts.append('<div class="properties">')
|
|
html_parts.append('<h4>Attributes:</h4>')
|
|
html_parts.append('<dl>')
|
|
|
|
# Add documented properties from init_info
|
|
if init_info.get('properties'):
|
|
for prop_name in init_info['properties']:
|
|
html_parts.append(f'<dt><code class="property">{prop_name}</code></dt>')
|
|
html_parts.append(f'<dd>Property of {class_name}</dd>')
|
|
|
|
# Add actual properties
|
|
for prop_name, prop_info in props.items():
|
|
readonly = ' <em>(read-only)</em>' if prop_info.get('readonly') else ''
|
|
html_parts.append(f'<dt><code class="property">{prop_name}</code>{readonly}</dt>')
|
|
if prop_info.get('doc'):
|
|
html_parts.append(f'<dd>{escape_html(prop_info["doc"])}</dd>')
|
|
|
|
html_parts.append('</dl>')
|
|
html_parts.append('</div>')
|
|
|
|
# Methods
|
|
methods = cls_info.get('methods', {})
|
|
collection_methods = collection_info.get('methods', {})
|
|
|
|
if methods or collection_methods:
|
|
html_parts.append('<div class="methods">')
|
|
html_parts.append('<h4>Methods:</h4>')
|
|
|
|
for method_name, method_doc in {**collection_methods, **methods}.items():
|
|
if method_name == '__init__':
|
|
continue
|
|
|
|
html_parts.append('<div class="method">')
|
|
|
|
# Get specific method documentation
|
|
method_info = generate_method_docs(method_name, class_name)
|
|
|
|
if method_info:
|
|
# Use detailed documentation
|
|
html_parts.append(f'<h5><code class="method">{method_info["signature"]}</code></h5>')
|
|
html_parts.append(f'<p>{escape_html(method_info["description"])}</p>')
|
|
|
|
if method_info.get('args'):
|
|
html_parts.append('<p><strong>Arguments:</strong></p>')
|
|
html_parts.append('<ul>')
|
|
for arg in method_info['args']:
|
|
if len(arg) == 3:
|
|
html_parts.append(f'<li><code>{arg[0]}</code> ({arg[1]}): {arg[2]}</li>')
|
|
else:
|
|
html_parts.append(f'<li><code>{arg[0]}</code> ({arg[1]})</li>')
|
|
html_parts.append('</ul>')
|
|
|
|
if method_info.get('returns'):
|
|
html_parts.append(f'<p><strong>Returns:</strong> {escape_html(method_info["returns"])}</p>')
|
|
|
|
if method_info.get('note'):
|
|
html_parts.append(f'<p><strong>Note:</strong> {escape_html(method_info["note"])}</p>')
|
|
else:
|
|
# Use docstring
|
|
html_parts.append(f'<h5><code class="method">{method_name}(...)</code></h5>')
|
|
if isinstance(method_doc, str) and method_doc:
|
|
html_parts.append(f'<p>{escape_html(method_doc)}</p>')
|
|
|
|
html_parts.append('</div>')
|
|
|
|
html_parts.append('</div>')
|
|
|
|
# Example
|
|
if init_info.get('example'):
|
|
html_parts.append('<div class="example">')
|
|
html_parts.append('<h4>Example:</h4>')
|
|
html_parts.append('<pre><code class="language-python">')
|
|
html_parts.append(escape_html(init_info['example']))
|
|
html_parts.append('</code></pre>')
|
|
html_parts.append('</div>')
|
|
|
|
html_parts.append('</div>')
|
|
html_parts.append('<hr>')
|
|
|
|
return '\n'.join(html_parts)
|
|
|
|
def generate_html_documentation():
|
|
"""Generate complete HTML API documentation."""
|
|
html_parts = []
|
|
|
|
# HTML header
|
|
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</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;
|
|
}
|
|
|
|
.inheritance {
|
|
color: #7f8c8d;
|
|
font-size: 0.9em;
|
|
margin-top: -10px;
|
|
}
|
|
|
|
.toc {
|
|
background: #f8f9fa;
|
|
border: 1px solid #e1e4e8;
|
|
border-radius: 6px;
|
|
padding: 20px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.toc h2 {
|
|
margin-top: 0;
|
|
border: none;
|
|
padding: 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;
|
|
}
|
|
|
|
.class-section, .function-section {
|
|
margin: 30px 0;
|
|
padding: 20px;
|
|
background: #fafbfc;
|
|
border-radius: 6px;
|
|
border: 1px solid #e1e4e8;
|
|
}
|
|
|
|
.description {
|
|
margin: 15px 0;
|
|
color: #4a5568;
|
|
}
|
|
|
|
.arguments, .properties, .methods {
|
|
margin: 20px 0;
|
|
}
|
|
|
|
dl {
|
|
margin: 10px 0;
|
|
}
|
|
|
|
dt {
|
|
font-weight: 600;
|
|
margin-top: 10px;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
dd {
|
|
margin-left: 20px;
|
|
margin-bottom: 10px;
|
|
color: #555;
|
|
}
|
|
|
|
.example {
|
|
margin: 20px 0;
|
|
padding: 15px;
|
|
background: #f0f7ff;
|
|
border-left: 4px solid #3498db;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.example h4 {
|
|
margin-top: 0;
|
|
color: #2980b9;
|
|
}
|
|
|
|
hr {
|
|
border: none;
|
|
border-top: 1px solid #e1e4e8;
|
|
margin: 30px 0;
|
|
}
|
|
|
|
.timestamp {
|
|
color: #7f8c8d;
|
|
font-style: italic;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.overview {
|
|
background: #e8f4fd;
|
|
border-left: 4px solid #3498db;
|
|
padding: 20px;
|
|
margin: 20px 0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.overview pre {
|
|
background: white;
|
|
border: 1px solid #d6e9f5;
|
|
}
|
|
|
|
strong {
|
|
color: #2c3e50;
|
|
}
|
|
|
|
em {
|
|
color: #555;
|
|
}
|
|
|
|
.automation-section {
|
|
background: #f0f9ff;
|
|
border: 1px solid #b8daff;
|
|
border-radius: 6px;
|
|
padding: 20px;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.automation-section h2 {
|
|
color: #004085;
|
|
border-bottom: 2px solid #b8daff;
|
|
}
|
|
|
|
.function-signature {
|
|
font-family: "SF Mono", Monaco, monospace;
|
|
font-size: 1.1em;
|
|
color: #d73a49;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
''')
|
|
|
|
# Title and timestamp
|
|
html_parts.append('<h1>McRogueFace API Reference</h1>')
|
|
html_parts.append(f'<p class="timestamp">Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>')
|
|
|
|
# Overview
|
|
if mcrfpy.__doc__:
|
|
html_parts.append('<div class="overview">')
|
|
html_parts.append('<h2>Overview</h2>')
|
|
# Process the docstring properly
|
|
doc_lines = mcrfpy.__doc__.strip().split('\\n')
|
|
for line in doc_lines:
|
|
if line.strip().startswith('Example:'):
|
|
html_parts.append('<h4>Example:</h4>')
|
|
html_parts.append('<pre><code class="language-python">')
|
|
elif line.strip() and not line.startswith(' '):
|
|
html_parts.append(f'<p>{escape_html(line)}</p>')
|
|
elif line.strip():
|
|
# Code line
|
|
html_parts.append(escape_html(line))
|
|
html_parts.append('</code></pre>')
|
|
html_parts.append('</div>')
|
|
|
|
# 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="#classes">Classes</a>')
|
|
html_parts.append('<ul>')
|
|
html_parts.append('<li><a href="#ui-components">UI Components</a></li>')
|
|
html_parts.append('<li><a href="#collections">Collections</a></li>')
|
|
html_parts.append('<li><a href="#system-types">System Types</a></li>')
|
|
html_parts.append('<li><a href="#other-classes">Other Classes</a></li>')
|
|
html_parts.append('</ul>')
|
|
html_parts.append('</li>')
|
|
html_parts.append('<li><a href="#functions">Functions</a>')
|
|
html_parts.append('<ul>')
|
|
html_parts.append('<li><a href="#scene-management">Scene Management</a></li>')
|
|
html_parts.append('<li><a href="#audio">Audio</a></li>')
|
|
html_parts.append('<li><a href="#ui-utilities">UI Utilities</a></li>')
|
|
html_parts.append('<li><a href="#system">System</a></li>')
|
|
html_parts.append('</ul>')
|
|
html_parts.append('</li>')
|
|
html_parts.append('<li><a href="#automation">Automation Module</a></li>')
|
|
html_parts.append('</ul>')
|
|
html_parts.append('</div>')
|
|
|
|
# Collect all components
|
|
classes = {}
|
|
functions = {}
|
|
|
|
for name in sorted(dir(mcrfpy)):
|
|
if name.startswith('_'):
|
|
continue
|
|
|
|
obj = getattr(mcrfpy, name)
|
|
|
|
if isinstance(obj, type):
|
|
classes[name] = obj
|
|
elif callable(obj) and not isinstance(obj, type):
|
|
# Include built-in functions and other callables (but not classes)
|
|
functions[name] = obj
|
|
|
|
|
|
# Classes section
|
|
html_parts.append('<h2 id="classes">Classes</h2>')
|
|
|
|
# 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
|
|
html_parts.append('<h3 id="ui-components">UI Components</h3>')
|
|
for class_name in ui_classes:
|
|
if class_name in classes:
|
|
cls_info = get_class_details(classes[class_name])
|
|
html_parts.append(format_class_html(cls_info, class_name))
|
|
|
|
# Collections
|
|
html_parts.append('<h3 id="collections">Collections</h3>')
|
|
for class_name in collection_classes:
|
|
if class_name in classes:
|
|
cls_info = get_class_details(classes[class_name])
|
|
html_parts.append(format_class_html(cls_info, class_name))
|
|
|
|
# System Types
|
|
html_parts.append('<h3 id="system-types">System Types</h3>')
|
|
for class_name in system_classes:
|
|
if class_name in classes:
|
|
cls_info = get_class_details(classes[class_name])
|
|
html_parts.append(format_class_html(cls_info, class_name))
|
|
|
|
# Other Classes
|
|
html_parts.append('<h3 id="other-classes">Other Classes</h3>')
|
|
for class_name in other_classes:
|
|
if class_name in classes:
|
|
cls_info = get_class_details(classes[class_name])
|
|
html_parts.append(format_class_html(cls_info, class_name))
|
|
|
|
# Functions section
|
|
html_parts.append('<h2 id="functions">Functions</h2>')
|
|
|
|
# Group functions by category
|
|
scene_funcs = ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene']
|
|
audio_funcs = ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume',
|
|
'getSoundVolume', 'setMusicVolume', 'setSoundVolume']
|
|
ui_funcs = ['find', 'findAll']
|
|
system_funcs = ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale']
|
|
|
|
# Scene Management
|
|
html_parts.append('<h3 id="scene-management">Scene Management</h3>')
|
|
for func_name in scene_funcs:
|
|
if func_name in functions:
|
|
html_parts.append(format_function_html(func_name, functions[func_name]))
|
|
|
|
# Audio
|
|
html_parts.append('<h3 id="audio">Audio</h3>')
|
|
for func_name in audio_funcs:
|
|
if func_name in functions:
|
|
html_parts.append(format_function_html(func_name, functions[func_name]))
|
|
|
|
# UI Utilities
|
|
html_parts.append('<h3 id="ui-utilities">UI Utilities</h3>')
|
|
for func_name in ui_funcs:
|
|
if func_name in functions:
|
|
html_parts.append(format_function_html(func_name, functions[func_name]))
|
|
|
|
# System
|
|
html_parts.append('<h3 id="system">System</h3>')
|
|
for func_name in system_funcs:
|
|
if func_name in functions:
|
|
html_parts.append(format_function_html(func_name, functions[func_name]))
|
|
|
|
# Automation Module
|
|
if hasattr(mcrfpy, 'automation'):
|
|
html_parts.append('<div class="automation-section">')
|
|
html_parts.append('<h2 id="automation">Automation Module</h2>')
|
|
html_parts.append('<p>The <code>mcrfpy.automation</code> module provides testing and automation capabilities for simulating user input and capturing screenshots.</p>')
|
|
|
|
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:
|
|
html_parts.append('<div class="function-section">')
|
|
html_parts.append(f'<h4><code class="function-signature">automation.{name}</code></h4>')
|
|
if func.__doc__:
|
|
# Extract just the description, not the repeated signature
|
|
doc_lines = func.__doc__.strip().split(' - ')
|
|
if len(doc_lines) > 1:
|
|
description = doc_lines[1]
|
|
else:
|
|
description = func.__doc__.strip()
|
|
html_parts.append(f'<p>{escape_html(description)}</p>')
|
|
html_parts.append('</div>')
|
|
|
|
html_parts.append('</div>')
|
|
|
|
# Close HTML
|
|
html_parts.append('''
|
|
</div>
|
|
</body>
|
|
</html>''')
|
|
|
|
return '\n'.join(html_parts)
|
|
|
|
def format_function_html(func_name, func):
|
|
"""Format a function as HTML using enhanced documentation."""
|
|
html_parts = []
|
|
|
|
html_parts.append('<div class="function-section">')
|
|
|
|
# Get enhanced documentation
|
|
func_docs = generate_function_docs()
|
|
|
|
if func_name in func_docs:
|
|
doc_info = func_docs[func_name]
|
|
|
|
# Signature
|
|
signature = doc_info.get('signature', f'{func_name}(...)')
|
|
html_parts.append(f'<h4><code class="function-signature">{escape_html(signature)}</code></h4>')
|
|
|
|
# Description
|
|
if 'description' in doc_info:
|
|
html_parts.append(f'<p class="description">{escape_html(doc_info["description"])}</p>')
|
|
|
|
# Arguments
|
|
if 'args' in doc_info and doc_info['args']:
|
|
html_parts.append('<div class="arguments">')
|
|
html_parts.append('<h5>Arguments:</h5>')
|
|
html_parts.append('<dl>')
|
|
for arg_name, arg_type, arg_desc in doc_info['args']:
|
|
html_parts.append(f'<dt><code>{escape_html(arg_name)}</code> : <em>{escape_html(arg_type)}</em></dt>')
|
|
html_parts.append(f'<dd>{escape_html(arg_desc)}</dd>')
|
|
html_parts.append('</dl>')
|
|
html_parts.append('</div>')
|
|
|
|
# Returns
|
|
if 'returns' in doc_info and doc_info['returns']:
|
|
html_parts.append('<div class="returns">')
|
|
html_parts.append('<h5>Returns:</h5>')
|
|
html_parts.append(f'<p>{escape_html(doc_info["returns"])}</p>')
|
|
html_parts.append('</div>')
|
|
|
|
# Exceptions
|
|
if 'exceptions' in doc_info and doc_info['exceptions']:
|
|
html_parts.append('<div class="exceptions">')
|
|
html_parts.append('<h5>Raises:</h5>')
|
|
html_parts.append('<dl>')
|
|
for exc_type, exc_desc in doc_info['exceptions']:
|
|
html_parts.append(f'<dt><code>{escape_html(exc_type)}</code></dt>')
|
|
html_parts.append(f'<dd>{escape_html(exc_desc)}</dd>')
|
|
html_parts.append('</dl>')
|
|
html_parts.append('</div>')
|
|
|
|
# Note
|
|
if 'note' in doc_info:
|
|
html_parts.append('<div class="note">')
|
|
html_parts.append(f'<p><strong>Note:</strong> {escape_html(doc_info["note"])}</p>')
|
|
html_parts.append('</div>')
|
|
|
|
# Example
|
|
if 'example' in doc_info:
|
|
html_parts.append('<div class="example">')
|
|
html_parts.append('<h5>Example:</h5>')
|
|
html_parts.append('<pre><code class="language-python">')
|
|
html_parts.append(escape_html(doc_info['example']))
|
|
html_parts.append('</code></pre>')
|
|
html_parts.append('</div>')
|
|
else:
|
|
# Fallback to parsing docstring if not in enhanced docs
|
|
doc = func.__doc__ or ""
|
|
lines = doc.strip().split('\n') if doc else []
|
|
|
|
# Extract signature
|
|
signature = func_name + '(...)'
|
|
if lines and '(' in lines[0]:
|
|
signature = lines[0].strip()
|
|
|
|
html_parts.append(f'<h4><code class="function-signature">{escape_html(signature)}</code></h4>')
|
|
|
|
# Process rest of docstring
|
|
if len(lines) > 1:
|
|
in_section = None
|
|
for line in lines[1:]:
|
|
stripped = line.strip()
|
|
|
|
if stripped in ['Args:', 'Returns:', 'Raises:', 'Note:', 'Example:']:
|
|
in_section = stripped[:-1]
|
|
html_parts.append(f'<p><strong>{in_section}:</strong></p>')
|
|
elif in_section == 'Example':
|
|
if not stripped:
|
|
continue
|
|
if stripped.startswith('>>>') or (len(lines) > lines.index(line) + 1 and
|
|
lines[lines.index(line) + 1].strip().startswith('>>>')):
|
|
html_parts.append('<pre><code class="language-python">')
|
|
html_parts.append(escape_html(stripped))
|
|
# Get rest of example
|
|
idx = lines.index(line) + 1
|
|
while idx < len(lines) and lines[idx].strip():
|
|
html_parts.append(escape_html(lines[idx]))
|
|
idx += 1
|
|
html_parts.append('</code></pre>')
|
|
break
|
|
elif in_section and stripped:
|
|
if in_section == 'Args':
|
|
# Format arguments nicely
|
|
if ':' in stripped:
|
|
param, desc = stripped.split(':', 1)
|
|
html_parts.append(f'<p style="margin-left: 20px;"><code>{escape_html(param.strip())}</code>: {escape_html(desc.strip())}</p>')
|
|
else:
|
|
html_parts.append(f'<p style="margin-left: 20px;">{escape_html(stripped)}</p>')
|
|
else:
|
|
html_parts.append(f'<p style="margin-left: 20px;">{escape_html(stripped)}</p>')
|
|
elif stripped and not in_section:
|
|
html_parts.append(f'<p>{escape_html(stripped)}</p>')
|
|
|
|
html_parts.append('</div>')
|
|
html_parts.append('<hr>')
|
|
|
|
return '\n'.join(html_parts)
|
|
|
|
def main():
|
|
"""Generate improved HTML API documentation."""
|
|
print("Generating improved HTML API documentation...")
|
|
|
|
# Generate HTML
|
|
html_content = generate_html_documentation()
|
|
|
|
# Write to file
|
|
output_path = Path("docs/api_reference_improved.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")
|
|
|
|
# Also generate a test to verify the HTML
|
|
test_content = '''#!/usr/bin/env python3
|
|
"""Test the improved HTML API documentation."""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
def test_html_quality():
|
|
"""Test that the HTML documentation meets quality standards."""
|
|
html_path = Path("docs/api_reference_improved.html")
|
|
|
|
if not html_path.exists():
|
|
print("ERROR: HTML documentation not found")
|
|
return False
|
|
|
|
with open(html_path, 'r') as f:
|
|
content = f.read()
|
|
|
|
# Check for common issues
|
|
issues = []
|
|
|
|
# Check that \\n is not present literally
|
|
if '\\\\n' in content:
|
|
issues.append("Found literal \\\\n in HTML content")
|
|
|
|
# Check that markdown links are converted
|
|
if '[' in content and '](#' in content:
|
|
issues.append("Found unconverted markdown links")
|
|
|
|
# Check for proper HTML structure
|
|
if '<h4>Args:</h4>' in content:
|
|
issues.append("Args: should not be an H4 heading")
|
|
|
|
if '<h4>Attributes:</h4>' not in content:
|
|
issues.append("Missing proper Attributes: headings")
|
|
|
|
# Check for duplicate method descriptions
|
|
if content.count('Get bounding box as (x, y, width, height)') > 20:
|
|
issues.append("Too many duplicate method descriptions")
|
|
|
|
# Check specific improvements
|
|
if 'Entity' in content and 'Inherits from: Drawable' in content:
|
|
issues.append("Entity incorrectly shown as inheriting from Drawable")
|
|
|
|
if not issues:
|
|
print("✓ HTML documentation passes all quality checks")
|
|
return True
|
|
else:
|
|
print("Issues found:")
|
|
for issue in issues:
|
|
print(f" - {issue}")
|
|
return False
|
|
|
|
if __name__ == '__main__':
|
|
if test_html_quality():
|
|
print("PASS")
|
|
sys.exit(0)
|
|
else:
|
|
print("FAIL")
|
|
sys.exit(1)
|
|
'''
|
|
|
|
test_path = Path("tests/test_html_quality.py")
|
|
with open(test_path, 'w') as f:
|
|
f.write(test_content)
|
|
|
|
print(f"✓ Generated test at {test_path}")
|
|
|
|
if __name__ == '__main__':
|
|
main() |