, \
- MCRF_PROPERTY(opacity, \
- "Opacity level (0.0 = transparent, 1.0 = opaque). " \
- "Automatically clamped to valid range [0.0, 1.0]." \
- ), NULL}
+ "Opacity (0.0 = transparent, 1.0 = opaque)", NULL}
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete
diff --git a/src/UICaption.cpp b/src/UICaption.cpp
index 33cff43..6ac1adb 100644
--- a/src/UICaption.cpp
+++ b/src/UICaption.cpp
@@ -273,16 +273,8 @@ PyGetSetDef UICaption::getsetters[] = {
//{"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},
{"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,
- MCRF_PROPERTY(click,
- "Callable executed when object is clicked. "
- "Function receives (x, y) coordinates of click."
- ), (void*)PyObjectsEnum::UICAPTION},
- {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
- MCRF_PROPERTY(z_index,
- "Z-order for rendering (lower values rendered first). "
- "Automatically triggers scene resort when changed."
- ), (void*)PyObjectsEnum::UICAPTION},
+ {"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}
diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp
index 8eb0522..4ceb8b8 100644
--- a/src/UIFrame.cpp
+++ b/src/UIFrame.cpp
@@ -398,16 +398,8 @@ PyGetSetDef UIFrame::getsetters[] = {
{"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,
- MCRF_PROPERTY(click,
- "Callable executed when object is clicked. "
- "Function receives (x, y) coordinates of click."
- ), (void*)PyObjectsEnum::UIFRAME},
- {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
- MCRF_PROPERTY(z_index,
- "Z-order for rendering (lower values rendered first). "
- "Automatically triggers scene resort when changed."
- ), (void*)PyObjectsEnum::UIFRAME},
+ {"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},
diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp
index 060c9c0..b07e596 100644
--- a/src/UIGrid.cpp
+++ b/src/UIGrid.cpp
@@ -1418,11 +1418,7 @@ PyGetSetDef UIGrid::getsetters[] = {
{"center_y", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view Y-coordinate", (void*)5},
{"zoom", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "zoom factor for displaying the Grid", (void*)6},
- {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
- MCRF_PROPERTY(click,
- "Callable executed when object is clicked. "
- "Function receives (x, y) coordinates of click."
- ), (void*)PyObjectsEnum::UIGRID},
+ {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID},
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL},
@@ -1432,11 +1428,7 @@ PyGetSetDef UIGrid::getsetters[] = {
{"perspective_enabled", (getter)UIGrid::get_perspective_enabled, (setter)UIGrid::set_perspective_enabled,
"Whether to use perspective-based FOV rendering. When True with no valid entity, "
"all cells appear undiscovered.", NULL},
- {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
- MCRF_PROPERTY(z_index,
- "Z-order for rendering (lower values rendered first). "
- "Automatically triggers scene resort when changed."
- ), (void*)PyObjectsEnum::UIGRID},
+ {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
UIDRAWABLE_GETSETTERS,
{NULL} /* Sentinel */
diff --git a/src/UISprite.cpp b/src/UISprite.cpp
index a1d697b..4be581c 100644
--- a/src/UISprite.cpp
+++ b/src/UISprite.cpp
@@ -339,16 +339,8 @@ PyGetSetDef UISprite::getsetters[] = {
{"sprite_index", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL},
{"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Sprite index (DEPRECATED: use sprite_index instead)", NULL},
{"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL},
- {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
- MCRF_PROPERTY(click,
- "Callable executed when object is clicked. "
- "Function receives (x, y) coordinates of click."
- ), (void*)PyObjectsEnum::UISPRITE},
- {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
- MCRF_PROPERTY(z_index,
- "Z-order for rendering (lower values rendered first). "
- "Automatically triggers scene resort when changed."
- ), (void*)PyObjectsEnum::UISPRITE},
+ {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE},
+ {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UISPRITE},
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UISPRITE},
UIDRAWABLE_GETSETTERS,
diff --git a/tools/generate_api_docs.py b/tools/generate_api_docs.py
new file mode 100644
index 0000000..d1e100f
--- /dev/null
+++ b/tools/generate_api_docs.py
@@ -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 = ['']
+ html.append('')
+ html.append('')
+ html.append('McRogueFace API Reference')
+ html.append('')
+ html.append('')
+
+ # 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('')
+ in_code_block = False
+ else:
+ lang = stripped[3:] or 'python'
+ html.append(f'')
+ 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'{text}')
+ # Lists
+ elif stripped.startswith('- '):
+ if not in_list:
+ html.append('')
+ in_list = True
+ html.append(f'- {stripped[2:]}
')
+ # Horizontal rule
+ elif stripped == '---':
+ if in_list:
+ html.append('
')
+ in_list = False
+ html.append('
')
+ # Emphasis
+ elif stripped.startswith('*') and stripped.endswith('*') and len(stripped) > 2:
+ html.append(f'{stripped[1:-1]}')
+ # Bold
+ elif stripped.startswith('**') and stripped.endswith('**'):
+ html.append(f'{stripped[2:-2]}')
+ # Regular paragraph
+ elif stripped:
+ if in_list:
+ html.append('')
+ in_list = False
+ # Convert inline code
+ text = stripped
+ if '`' in text:
+ import re
+ text = re.sub(r'`([^`]+)`', r'\1', text)
+ html.append(f'{text}
')
+ else:
+ if in_list:
+ html.append('')
+ in_list = False
+ # Empty line
+ html.append('')
+
+ if in_list:
+ html.append('')
+ if in_code_block:
+ html.append('
')
+
+ html.append('')
+ 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()
\ No newline at end of file
diff --git a/tools/generate_api_docs_html.py b/tools/generate_api_docs_html.py
new file mode 100644
index 0000000..fe3cf08
--- /dev/null
+++ b/tools/generate_api_docs_html.py
@@ -0,0 +1,1602 @@
+#!/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('')
+ in_code_block = False
+ else:
+ result.append('')
+ 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'\1', line)
+
+ if in_code_block:
+ result.append(escape_html(line))
+ else:
+ result.append(escape_html(line) + '
')
+
+ if in_code_block:
+ result.append('
')
+
+ 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'')
+ html_parts.append(f'
class {class_name}
')
+
+ # Inheritance
+ if cls_info['bases']:
+ html_parts.append(f'
Inherits from: {", ".join(cls_info["bases"])}
')
+
+ # 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('
')
+ html_parts.append('
')
+ html_parts.append(escape_html(init_info['signature']))
+ html_parts.append('
')
+ html_parts.append('
')
+
+ # 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('
')
+ html_parts.append(f'
{format_docstring_as_html(description)}
')
+ html_parts.append('
')
+
+ # Constructor arguments
+ if init_info.get('args'):
+ html_parts.append('
')
+ html_parts.append('
Arguments:
')
+ html_parts.append('
')
+ for arg_name, arg_type, arg_desc in init_info['args']:
+ html_parts.append(f'{arg_name} ({arg_type}) ')
+ html_parts.append(f'- {escape_html(arg_desc)}
')
+ html_parts.append('
')
+ html_parts.append('
')
+
+ # Properties/Attributes
+ props = cls_info.get('properties', {})
+ if props or init_info.get('properties'):
+ html_parts.append('
')
+ html_parts.append('
Attributes:
')
+ html_parts.append('
')
+
+ # Add documented properties from init_info
+ if init_info.get('properties'):
+ for prop_name in init_info['properties']:
+ html_parts.append(f'{prop_name} ')
+ html_parts.append(f'- Property of {class_name}
')
+
+ # Add actual properties
+ for prop_name, prop_info in props.items():
+ readonly = ' (read-only)' if prop_info.get('readonly') else ''
+ html_parts.append(f'{prop_name}{readonly} ')
+ if prop_info.get('doc'):
+ html_parts.append(f'- {escape_html(prop_info["doc"])}
')
+
+ html_parts.append('
')
+ html_parts.append('
')
+
+ # Methods
+ methods = cls_info.get('methods', {})
+ collection_methods = collection_info.get('methods', {})
+
+ if methods or collection_methods:
+ html_parts.append('
')
+ html_parts.append('
Methods:
')
+
+ for method_name, method_doc in {**collection_methods, **methods}.items():
+ if method_name == '__init__':
+ continue
+
+ html_parts.append('
')
+
+ # Get specific method documentation
+ method_info = generate_method_docs(method_name, class_name)
+
+ if method_info:
+ # Use detailed documentation
+ html_parts.append(f'
{method_info["signature"]}
')
+ html_parts.append(f'
{escape_html(method_info["description"])}
')
+
+ if method_info.get('args'):
+ html_parts.append('
Arguments:
')
+ html_parts.append('
')
+ for arg in method_info['args']:
+ if len(arg) == 3:
+ html_parts.append(f'{arg[0]} ({arg[1]}): {arg[2]} ')
+ else:
+ html_parts.append(f'{arg[0]} ({arg[1]}) ')
+ html_parts.append('
')
+
+ if method_info.get('returns'):
+ html_parts.append(f'
Returns: {escape_html(method_info["returns"])}
')
+
+ if method_info.get('note'):
+ html_parts.append(f'
Note: {escape_html(method_info["note"])}
')
+ else:
+ # Use docstring
+ html_parts.append(f'
{method_name}(...)
')
+ if isinstance(method_doc, str) and method_doc:
+ html_parts.append(f'
{escape_html(method_doc)}
')
+
+ html_parts.append('
')
+
+ html_parts.append('
')
+
+ # Example
+ if init_info.get('example'):
+ html_parts.append('
')
+ html_parts.append('
Example:
')
+ html_parts.append('
')
+ html_parts.append(escape_html(init_info['example']))
+ html_parts.append('
')
+ html_parts.append('
')
+
+ html_parts.append('
')
+ html_parts.append('
')
+
+ return '\n'.join(html_parts)
+
+def generate_html_documentation():
+ """Generate complete HTML API documentation."""
+ html_parts = []
+
+ # HTML header
+ html_parts.append('''
+
+
+
+
+ McRogueFace API Reference
+
+
+
+
+''')
+
+ # Title and timestamp
+ html_parts.append('
McRogueFace API Reference
')
+ html_parts.append(f'
Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
')
+
+ # Overview
+ if mcrfpy.__doc__:
+ html_parts.append('
')
+ html_parts.append('
Overview
')
+ # Process the docstring properly
+ doc_lines = mcrfpy.__doc__.strip().split('\\n')
+ for line in doc_lines:
+ if line.strip().startswith('Example:'):
+ html_parts.append('
Example:
')
+ html_parts.append('
')
+ elif line.strip() and not line.startswith(' '):
+ html_parts.append(f'{escape_html(line)}
')
+ elif line.strip():
+ # Code line
+ html_parts.append(escape_html(line))
+ html_parts.append('
')
+ html_parts.append('
')
+
+ # Table of Contents
+ html_parts.append('
')
+ html_parts.append('
Table of Contents
')
+ html_parts.append('
')
+ html_parts.append('- Classes')
+ html_parts.append('')
+ html_parts.append('
')
+ html_parts.append('- Functions')
+ html_parts.append('')
+ html_parts.append('
')
+ html_parts.append('- Automation Module
')
+ html_parts.append('
')
+ html_parts.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[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('
Classes
')
+
+ # 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('
UI Components
')
+ 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('
Collections
')
+ 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('
System Types
')
+ 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('
Other Classes
')
+ 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('
Functions
')
+
+ # 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('
Scene Management
')
+ 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('
Audio
')
+ 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('
UI Utilities
')
+ 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('
System
')
+ 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('
')
+ html_parts.append('
Automation Module
')
+ html_parts.append('
The mcrfpy.automation module provides testing and automation capabilities for simulating user input and capturing screenshots.
')
+
+ 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('
')
+ html_parts.append(f'
automation.{name}
')
+ 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'
{escape_html(description)}
')
+ html_parts.append('
')
+
+ html_parts.append('
')
+
+ # Close HTML
+ html_parts.append('''
+
+
+''')
+
+ 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('')
+
+ # 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'
{escape_html(signature)}
')
+
+ # Description
+ if 'description' in doc_info:
+ html_parts.append(f'
{escape_html(doc_info["description"])}
')
+
+ # Arguments
+ if 'args' in doc_info and doc_info['args']:
+ html_parts.append('
')
+ html_parts.append('
Arguments:
')
+ html_parts.append('
')
+ for arg_name, arg_type, arg_desc in doc_info['args']:
+ html_parts.append(f'{escape_html(arg_name)} : {escape_html(arg_type)} ')
+ html_parts.append(f'- {escape_html(arg_desc)}
')
+ html_parts.append('
')
+ html_parts.append('
')
+
+ # Returns
+ if 'returns' in doc_info and doc_info['returns']:
+ html_parts.append('
')
+ html_parts.append('
Returns:
')
+ html_parts.append(f'
{escape_html(doc_info["returns"])}
')
+ html_parts.append('
')
+
+ # Exceptions
+ if 'exceptions' in doc_info and doc_info['exceptions']:
+ html_parts.append('
')
+ html_parts.append('
Raises:
')
+ html_parts.append('
')
+ for exc_type, exc_desc in doc_info['exceptions']:
+ html_parts.append(f'{escape_html(exc_type)} ')
+ html_parts.append(f'- {escape_html(exc_desc)}
')
+ html_parts.append('
')
+ html_parts.append('
')
+
+ # Note
+ if 'note' in doc_info:
+ html_parts.append('
')
+ html_parts.append(f'
Note: {escape_html(doc_info["note"])}
')
+ html_parts.append('
')
+
+ # Example
+ if 'example' in doc_info:
+ html_parts.append('
')
+ html_parts.append('
Example:
')
+ html_parts.append('
')
+ html_parts.append(escape_html(doc_info['example']))
+ html_parts.append('
')
+ html_parts.append('
')
+ 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'
{escape_html(signature)}
')
+
+ # 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'
{in_section}:
')
+ 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('
')
+ 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('
')
+ 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'
{escape_html(param.strip())}: {escape_html(desc.strip())}
')
+ else:
+ html_parts.append(f'
{escape_html(stripped)}
')
+ else:
+ html_parts.append(f'
{escape_html(stripped)}
')
+ elif stripped and not in_section:
+ html_parts.append(f'
{escape_html(stripped)}
')
+
+ html_parts.append('
')
+ html_parts.append('
')
+
+ 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 'Args:
' in content:
+ issues.append("Args: should not be an H4 heading")
+
+ if 'Attributes:
' 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()
\ No newline at end of file
diff --git a/tools/generate_api_docs_simple.py b/tools/generate_api_docs_simple.py
new file mode 100644
index 0000000..2bb405f
--- /dev/null
+++ b/tools/generate_api_docs_simple.py
@@ -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()
\ No newline at end of file
diff --git a/tools/generate_complete_api_docs.py b/tools/generate_complete_api_docs.py
new file mode 100644
index 0000000..8b41446
--- /dev/null
+++ b/tools/generate_complete_api_docs.py
@@ -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('''
+
+
+
+
+ McRogueFace API Reference - Complete Documentation
+
+
+
+
+''')
+
+ # Title and overview
+ html_parts.append('
McRogueFace API Reference - Complete Documentation
')
+ html_parts.append(f'
Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
')
+
+ # Table of contents
+ html_parts.append('
')
+ html_parts.append('
Table of Contents
')
+ html_parts.append('
')
+ html_parts.append('
')
+
+ # Functions section
+ html_parts.append('
Functions
')
+
+ # 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'
{category}
')
+ 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('
Classes
')
+
+ # 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('
Automation Module
')
+ html_parts.append('
The mcrfpy.automation module provides testing and automation capabilities.
')
+
+ automation = mcrfpy.automation
+ for name in sorted(dir(automation)):
+ if not name.startswith('_'):
+ obj = getattr(automation, name)
+ if callable(obj):
+ html_parts.append(f'
')
+ html_parts.append(f'
automation.{name}
')
+ if obj.__doc__:
+ doc_parts = obj.__doc__.split(' - ')
+ if len(doc_parts) > 1:
+ html_parts.append(f'
{escape_html(doc_parts[1])}
')
+ else:
+ html_parts.append(f'
{escape_html(obj.__doc__)}
')
+ html_parts.append('
')
+
+ html_parts.append('
')
+ html_parts.append('')
+ html_parts.append('')
+
+ return '\n'.join(html_parts)
+
+def format_function_html(func_name, func_doc):
+ """Format a function with complete documentation."""
+ html_parts = []
+
+ html_parts.append('')
+ html_parts.append(f'
{func_doc["signature"]}
')
+ html_parts.append(f'
{escape_html(func_doc["description"])}
')
+
+ # Arguments
+ if 'args' in func_doc:
+ html_parts.append('
')
+ html_parts.append('
Arguments:
')
+ for arg in func_doc['args']:
+ html_parts.append('
')
+ html_parts.append(f'{arg[0]} ')
+ html_parts.append(f'({arg[1]}): ')
+ html_parts.append(f'{escape_html(arg[2])}')
+ html_parts.append('
')
+ html_parts.append('
')
+
+ # Returns
+ if 'returns' in func_doc:
+ html_parts.append('
')
+ html_parts.append(f'Returns: {escape_html(func_doc["returns"])}')
+ html_parts.append('
')
+
+ # Raises
+ if 'raises' in func_doc:
+ html_parts.append('
')
+ html_parts.append(f'Raises: {escape_html(func_doc["raises"])}')
+ html_parts.append('
')
+
+ # Note
+ if 'note' in func_doc:
+ html_parts.append('
')
+ html_parts.append(f'Note: {escape_html(func_doc["note"])}')
+ html_parts.append('
')
+
+ # Example
+ if 'example' in func_doc:
+ html_parts.append('
')
+ html_parts.append('
Example:
')
+ html_parts.append('
')
+ html_parts.append(escape_html(func_doc['example']))
+ html_parts.append('
')
+ html_parts.append('
')
+
+ html_parts.append('
')
+
+ 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('')
+ html_parts.append(f'
{class_name}
')
+
+ # Class description
+ if cls.__doc__:
+ html_parts.append(f'
{escape_html(cls.__doc__)}
')
+
+ # Properties
+ if class_name in property_docs:
+ html_parts.append('
Properties:
')
+ for prop_name, prop_desc in property_docs[class_name].items():
+ html_parts.append(f'
')
+ html_parts.append(f'{prop_name}: {escape_html(prop_desc)}')
+ html_parts.append('
')
+
+ # 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('
Methods:
')
+ 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'
')
+ html_parts.append(f'{method_name}(...)')
+ html_parts.append('
')
+
+ html_parts.append('
')
+
+ return '\n'.join(html_parts)
+
+def format_method_html(method_name, method_doc):
+ """Format a method with complete documentation."""
+ html_parts = []
+
+ html_parts.append('')
+ html_parts.append(f'
{method_doc["signature"]}
')
+ html_parts.append(f'
{escape_html(method_doc["description"])}
')
+
+ # Arguments
+ if 'args' in method_doc:
+ for arg in method_doc['args']:
+ html_parts.append(f'
')
+ html_parts.append(f'{arg[0]} ')
+ html_parts.append(f'({arg[1]}): ')
+ html_parts.append(f'{escape_html(arg[2])}')
+ html_parts.append('
')
+
+ # Returns
+ if 'returns' in method_doc:
+ html_parts.append(f'
')
+ html_parts.append(f'Returns: {escape_html(method_doc["returns"])}')
+ html_parts.append('
')
+
+ # Note
+ if 'note' in method_doc:
+ html_parts.append(f'
')
+ html_parts.append(f'Note: {escape_html(method_doc["note"])}')
+ html_parts.append('
')
+
+ # Example
+ if 'example' in method_doc:
+ html_parts.append(f'
')
+ html_parts.append('
Example:')
+ html_parts.append('
')
+ html_parts.append(escape_html(method_doc['example']))
+ html_parts.append('
')
+ html_parts.append('
')
+
+ html_parts.append('
')
+
+ 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()
\ No newline at end of file
diff --git a/tools/generate_complete_markdown_docs.py b/tools/generate_complete_markdown_docs.py
new file mode 100644
index 0000000..89fab79
--- /dev/null
+++ b/tools/generate_complete_markdown_docs.py
@@ -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()
\ No newline at end of file
diff --git a/tools/generate_dynamic_docs.py b/tools/generate_dynamic_docs.py
index 426fdcd..92e65cc 100644
--- a/tools/generate_dynamic_docs.py
+++ b/tools/generate_dynamic_docs.py
@@ -12,67 +12,6 @@ import html
import re
from pathlib import Path
-def transform_doc_links(docstring, format='html', base_url=''):
- """Transform MCRF_LINK patterns based on output format.
-
- Detects pattern: "See also: TEXT (docs/path.md)"
- Transforms to appropriate format for output type.
-
- For HTML/web formats, properly escapes content before inserting HTML tags.
- """
- if not docstring:
- return docstring
-
- link_pattern = r'See also: ([^(]+) \(([^)]+)\)'
-
- def replace_link(match):
- text, ref = match.group(1).strip(), match.group(2).strip()
-
- if format == 'html':
- # Convert docs/foo.md → foo.html and escape for safe HTML
- href = html.escape(ref.replace('docs/', '').replace('.md', '.html'), quote=True)
- text_escaped = html.escape(text)
- return f'See also: {text_escaped}
'
-
- elif format == 'web':
- # Link to hosted docs and escape for safe HTML
- web_path = ref.replace('docs/', '').replace('.md', '')
- href = html.escape(f"{base_url}/{web_path}", quote=True)
- text_escaped = html.escape(text)
- return f'See also: {text_escaped}
'
-
- elif format == 'markdown':
- # Markdown link
- return f'\n**See also:** [{text}]({ref})'
-
- else: # 'python' or default
- # Keep as plain text for Python docstrings
- return match.group(0)
-
- # For HTML formats, escape the entire docstring first, then process links
- if format in ('html', 'web'):
- # Split by the link pattern, escape non-link parts, then reassemble
- parts = []
- last_end = 0
-
- for match in re.finditer(link_pattern, docstring):
- # Escape the text before this match
- if match.start() > last_end:
- parts.append(html.escape(docstring[last_end:match.start()]))
-
- # Process the link (replace_link handles escaping internally)
- parts.append(replace_link(match))
- last_end = match.end()
-
- # Escape any remaining text after the last match
- if last_end < len(docstring):
- parts.append(html.escape(docstring[last_end:]))
-
- return ''.join(parts)
- else:
- # For non-HTML formats, just do simple replacement
- return re.sub(link_pattern, replace_link, docstring)
-
# Must be run with McRogueFace as interpreter
try:
import mcrfpy
@@ -365,10 +304,8 @@ def generate_html_docs():
html_content += f"""
{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}
+
{html.escape(parsed['description'])}
"""
- if parsed['description']:
- description = transform_doc_links(parsed['description'], format='html')
- html_content += f"
{description}
\n"
if parsed['args']:
html_content += "
Arguments:
\n
\n"
@@ -424,10 +361,9 @@ def generate_html_docs():
{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}
"""
-
+
if parsed['description']:
- description = transform_doc_links(parsed['description'], format='html')
- html_content += f"
{description}
\n"
+ html_content += f"
{html.escape(parsed['description'])}
\n"
if parsed['args']:
html_content += "
\n"
@@ -493,10 +429,9 @@ def generate_markdown_docs():
parsed = func_info["parsed"]
md_content += f"### `{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
-
+
if parsed['description']:
- description = transform_doc_links(parsed['description'], format='markdown')
- md_content += f"{description}\n\n"
+ md_content += f"{parsed['description']}\n\n"
if parsed['args']:
md_content += "**Arguments:**\n"
@@ -544,10 +479,9 @@ def generate_markdown_docs():
parsed = method_info['parsed']
md_content += f"#### `{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
-
+
if parsed['description']:
- description = transform_doc_links(parsed['description'], format='markdown')
- md_content += f"{description}\n\n"
+ md_content += f"{parsed['description']}\n\n"
if parsed['args']:
md_content += "**Arguments:**\n"
diff --git a/tools/test_link_transform.py b/tools/test_link_transform.py
deleted file mode 100644
index 37e3b05..0000000
--- a/tools/test_link_transform.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env python3
-"""Test script for link transformation function."""
-
-import re
-
-def transform_doc_links(docstring, format='html', base_url=''):
- """Transform MCRF_LINK patterns based on output format.
-
- Detects pattern: "See also: TEXT (docs/path.md)"
- Transforms to appropriate format for output type.
- """
- if not docstring:
- return docstring
-
- link_pattern = r'See also: ([^(]+) \(([^)]+)\)'
-
- def replace_link(match):
- text, ref = match.group(1).strip(), match.group(2).strip()
-
- if format == 'html':
- # Convert docs/foo.md → foo.html
- href = ref.replace('docs/', '').replace('.md', '.html')
- return f'
See also: {text}
'
-
- elif format == 'web':
- # Link to hosted docs
- web_path = ref.replace('docs/', '').replace('.md', '')
- return f'
See also: {text}
'
-
- elif format == 'markdown':
- # Markdown link
- return f'\n**See also:** [{text}]({ref})'
-
- else: # 'python' or default
- # Keep as plain text for Python docstrings
- return match.group(0)
-
- return re.sub(link_pattern, replace_link, docstring)
-
-# Test cases
-test_doc = "Description text.\n\nSee also: Tutorial Guide (docs/guide.md)\n\nMore text."
-
-html_result = transform_doc_links(test_doc, format='html')
-print("HTML:", html_result)
-assert '
Tutorial Guide' in html_result
-
-md_result = transform_doc_links(test_doc, format='markdown')
-print("Markdown:", md_result)
-assert '[Tutorial Guide](docs/guide.md)' in md_result
-
-plain_result = transform_doc_links(test_doc, format='python')
-print("Python:", plain_result)
-assert 'See also: Tutorial Guide (docs/guide.md)' in plain_result
-
-print("\nSUCCESS: All transformations work correctly")
diff --git a/tools/test_vector_docs.py b/tools/test_vector_docs.py
deleted file mode 100644
index bf07e47..0000000
--- a/tools/test_vector_docs.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import mcrfpy
-import sys
-
-# Check Vector.magnitude docstring
-mag_doc = mcrfpy.Vector.magnitude.__doc__
-print("magnitude doc:", mag_doc)
-assert "magnitude()" in mag_doc
-assert "Calculate the length/magnitude" in mag_doc
-assert "Returns:" in mag_doc
-
-# Check Vector.dot docstring
-dot_doc = mcrfpy.Vector.dot.__doc__
-print("dot doc:", dot_doc)
-assert "dot(other: Vector)" in dot_doc
-assert "Args:" in dot_doc
-assert "other:" in dot_doc
-
-# Check Vector.normalize docstring
-normalize_doc = mcrfpy.Vector.normalize.__doc__
-print("normalize doc:", normalize_doc)
-assert "normalize()" in normalize_doc
-assert "Return a unit vector" in normalize_doc
-assert "Returns:" in normalize_doc
-assert "Note:" in normalize_doc
-
-# Check Vector.x property docstring
-x_doc = mcrfpy.Vector.x.__doc__
-print("x property doc:", x_doc)
-assert "X coordinate of the vector" in x_doc
-assert "float" in x_doc
-
-# Check Vector.y property docstring
-y_doc = mcrfpy.Vector.y.__doc__
-print("y property doc:", y_doc)
-assert "Y coordinate of the vector" in y_doc
-assert "float" in y_doc
-
-print("SUCCESS: All docstrings present and complete")
-sys.exit(0)