Compare commits

...

16 Commits

Author SHA1 Message Date
John McCardle 621d719c25 docs: Phase 3 - Convert 19 module functions to MCRF_FUNCTION macros
Converted all module-level functions in McRFPy_API.cpp to use the MCRF_*
documentation macro system:

Audio functions (7):
- createSoundBuffer, loadMusic, setMusicVolume, setSoundVolume
- playSound, getMusicVolume, getSoundVolume

Scene functions (5):
- sceneUI, currentScene, setScene, createScene, keypressScene

Timer functions (2):
- setTimer, delTimer

Utility functions (5):
- exit, setScale, find, findAll, getMetrics

Each function now uses:
- MCRF_SIG for signatures
- MCRF_DESC for descriptions
- MCRF_ARG for parameters
- MCRF_RETURNS for return values
- MCRF_RAISES for exceptions
- MCRF_NOTE for additional details

Phase 4 assessment: PyCallable.cpp and PythonObjectCache.cpp contain only
internal C++ implementation with no Python API to document.

All conversions tested and verified with test_phase3_docs.py.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 19:38:22 -04:00
John McCardle 29aa6e62be docs: convert Phase 2 classes to documentation macros (Animation, Window, SceneObject)
Converted 3 files to use MCRF_* documentation macros:
- PyAnimation.cpp: 5 methods + 5 properties
- PyWindow.cpp: 3 methods + 8 properties
- PySceneObject.cpp: 3 methods + 2 properties

All conversions build successfully. Enhanced descriptions with implementation details.

Note: PyScene.cpp has no exposed methods/properties, so no conversion needed.

Progress: Phase 1 (4 files) + Phase 2 (3 files) = 7 new classes complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 17:03:28 -04:00
John McCardle 67aba5ef1f docs: convert Phase 1 classes to documentation macros (Color, Font, Texture, Timer)
Converted 4 files to use MCRF_* documentation macros:
- PyColor.cpp: 3 methods + 4 properties
- PyFont.cpp: 2 properties (read-only)
- PyTexture.cpp: 6 properties (read-only)
- PyTimer.cpp: 4 methods + 7 properties

All conversions verified with test_phase1_docs.py - 0 placeholders.
Documentation regenerated with enhanced descriptions.

Progress: 11/12 class files complete (92%)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 16:59:40 -04:00
John McCardle 6aa4625b76 fix: correct module docstring newline escaping
Fixed module-level docstring in PyModuleDef where double-backslash newlines
(\\n) were appearing as literal "\n" text in help(mcrfpy) output.

Changed from escaped newlines (\\n) to actual newlines (\n) so the C compiler
interprets them correctly.

Before: help(mcrfpy) showed "McRogueFace Python API\\n\\nCore game..."
After:  help(mcrfpy) shows proper formatting with line breaks

The issue was in the PyDoc_STR() macro call - it doesn't interpret escape
sequences, so the string literal itself needs to have proper newlines.
2025-10-30 15:57:17 -04:00
John McCardle 4c61bee512 docs: update CLAUDE.md with MCRF_* macro documentation system
Updated documentation guidelines to reflect the new macro-based system:
- Documented MCRF_METHOD and MCRF_PROPERTY usage
- Listed all available macros (MCRF_SIG, MCRF_DESC, MCRF_ARG, etc.)
- Added prose guidelines (concise C++, verbose external docs)
- Updated regeneration workflow (removed references to deleted scripts)
- Emphasized single source of truth and zero-drift architecture

Removed references to obsolete hardcoded documentation scripts that were
deleted in previous commits.

Related: #92 (Inline C++ documentation system)
2025-10-30 12:37:04 -04:00
John McCardle cc80964835 fix: update child class property overrides to use MCRF_PROPERTY macros
Fixes critical issue discovered in code review where PyDrawable property
docstrings were being overridden by child classes, making enhanced documentation
invisible to users.

Updated files:
- src/UIBase.h: UIDRAWABLE_GETSETTERS macro (visible, opacity)
- src/UIFrame.cpp: click and z_index properties
- src/UISprite.cpp: click and z_index properties
- src/UICaption.cpp: click and z_index properties
- src/UIGrid.cpp: click and z_index properties

All four UI class hierarchies (Frame, Sprite, Caption, Grid) now expose
consistent, enhanced property documentation to Python users.

Verification:
- tools/test_child_class_docstrings.py: All 16 property tests pass
- All 4 properties (click, z_index, visible, opacity) match across all 4 classes

Related: #92 (Inline C++ documentation system)
2025-10-30 12:33:27 -04:00
John McCardle 326b692908 feat: convert PyDrawable properties to documentation macros
All Drawable properties (click, z_index, visible, opacity) now
use MCRF_PROPERTY with enhanced descriptions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 12:22:00 -04:00
John McCardle dda5305256 feat: convert PyDrawable methods to documentation macros
Converts get_bounds, move, and resize to MCRF_METHOD.
These are inherited by all UI classes (Frame, Caption, Sprite, Grid).

Updated both PyDrawable.cpp and UIBase.h (UIDRAWABLE_METHODS macro).
All method docstrings now include complete Args, Returns, and Note sections.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 12:06:37 -04:00
John McCardle 1f6175bfa5 refactor: remove obsolete documentation generators
Removes ~3,979 lines of hardcoded Python documentation dictionaries.
Documentation is now generated from C++ docstrings via macros.

Deleted:
- generate_complete_api_docs.py (959 lines)
- generate_complete_markdown_docs.py (820 lines)
- generate_api_docs.py (481 lines)
- generate_api_docs_simple.py (118 lines)
- generate_api_docs_html.py (1,601 lines)

Addresses issue #97.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:51:21 -04:00
John McCardle 7f253da581 fix: escape HTML in descriptions before link transformation
Fixes HTML injection vulnerability in generate_dynamic_docs.py where
description text was not HTML-escaped before being inserted into HTML
output. Special characters like <, >, & could be interpreted as HTML.

Changes:
- Modified transform_doc_links() to escape all non-link text when
  format='html' or format='web'
- Link text and hrefs are also properly escaped
- Non-HTML formats (markdown, python) remain unchanged
- Added proper handling for descriptions with mixed plain text and links

The fix splits docstrings into link and non-link segments, escapes
non-link segments, and properly escapes content within link patterns.

Tested with comprehensive test suite covering:
- Basic HTML special characters
- Special chars with links
- Special chars in link text
- Multiple links with special chars

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:48:09 -04:00
John McCardle fac6a9a457 feat: add link transformation to documentation generator
Adds transform_doc_links() function that converts MCRF_LINK patterns
to appropriate format (HTML links, Markdown links, or plain text).
Addresses issue #97.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:39:54 -04:00
John McCardle a8a257eefc feat: convert PyVector properties to use macros
Properties x and y now use MCRF_PROPERTY for consistency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:33:49 -04:00
John McCardle 07e8207a08 feat: complete PyVector documentation macro conversion
All Vector methods now use MCRF_METHOD macros with complete
documentation including Args, Returns, and Notes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:27:50 -04:00
John McCardle 23d7882b93 fix: correct normalize() documentation to match implementation
The normalize() method's implementation returns a zero vector when
called on a zero-magnitude vector, rather than raising ValueError as
the documentation claimed. Updated the MCRF_RAISES to MCRF_NOTE to
accurately describe the actual behavior.

Also added test coverage in tools/test_vector_docs.py to verify the
normalize() docstring contains the correct Note section.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:25:43 -04:00
John McCardle 91461d0f87 feat: convert PyVector to use documentation macros
Converts magnitude, normalize, and dot methods to MCRF_METHOD macro.
Docstrings now include complete Args/Returns/Raises sections.
Addresses issue #92.
2025-10-30 11:20:48 -04:00
John McCardle a08003bda4 feat: add documentation macro system header
Adds C++ preprocessor macros for consistent API documentation.
Addresses issue #92.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:16:44 -04:00
29 changed files with 1033 additions and 4327 deletions

9
.mcp.json Normal file
View File

@ -0,0 +1,9 @@
{
"mcpServers": {
"gitea": {
"type": "stdio",
"command": "/home/john/Development/discord_for_claude/forgejo-mcp.linux.amd64",
"args": ["stdio", "--server", "https://gamedev.ffwf.net/gitea/", "--token", "f58ec698a5edee82db4960920b13d3f7d0d58d8e"]
}
}
}

103
CLAUDE.md
View File

@ -392,67 +392,102 @@ mcrfpy.setTimer("test", run_test, 100) # 0.1 seconds
## Documentation Guidelines
### Inline C++ Documentation Format
### Documentation Macro System
When adding new methods or modifying existing ones in C++ source files, use this documentation format in PyMethodDef arrays:
**As of 2025-10-30, McRogueFace uses a macro-based documentation system** (`src/McRFPy_Doc.h`) that ensures consistent, complete docstrings across all Python bindings.
#### Include the Header
```cpp
{"method_name", (PyCFunction)Class::method, METH_VARARGS | METH_KEYWORDS,
"method_name(arg1: type, arg2: type = default) -> return_type\n\n"
"Brief description of what the method does.\n\n"
"Args:\n"
" arg1: Description of first argument\n"
" arg2: Description of second argument (default: value)\n\n"
"Returns:\n"
" Description of return value\n\n"
"Example:\n"
" result = obj.method_name(value1, value2)\n\n"
"Note:\n"
" Any important notes or caveats"},
#include "McRFPy_Doc.h"
```
For properties in PyGetSetDef arrays:
#### Documenting Methods
For methods in PyMethodDef arrays, use `MCRF_METHOD`:
```cpp
{"method_name", (PyCFunction)Class::method, METH_VARARGS,
MCRF_METHOD(ClassName, method_name,
MCRF_SIG("(arg1: type, arg2: type)", "return_type"),
MCRF_DESC("Brief description of what the method does."),
MCRF_ARGS_START
MCRF_ARG("arg1", "Description of first argument")
MCRF_ARG("arg2", "Description of second argument")
MCRF_RETURNS("Description of return value")
MCRF_RAISES("ValueError", "Condition that raises this exception")
MCRF_NOTE("Important notes or caveats")
MCRF_LINK("docs/guide.md", "Related Documentation")
)},
```
#### Documenting Properties
For properties in PyGetSetDef arrays, use `MCRF_PROPERTY`:
```cpp
{"property_name", (getter)getter_func, (setter)setter_func,
MCRF_PROPERTY(property_name,
"Brief description of the property. "
"Additional details about valid values, side effects, etc.", NULL},
"Additional details about valid values, side effects, etc."
), NULL},
```
#### Available Macros
- `MCRF_SIG(params, ret)` - Method signature
- `MCRF_DESC(text)` - Description paragraph
- `MCRF_ARGS_START` - Begin arguments section
- `MCRF_ARG(name, desc)` - Individual argument
- `MCRF_RETURNS(text)` - Return value description
- `MCRF_RAISES(exception, condition)` - Exception documentation
- `MCRF_NOTE(text)` - Important notes
- `MCRF_LINK(path, text)` - Reference to external documentation
#### Documentation Prose Guidelines
**Keep C++ docstrings concise** (1-2 sentences per section). For complex topics, use `MCRF_LINK` to reference external guides:
```cpp
MCRF_LINK("docs/animation-guide.md", "Animation System Tutorial")
```
**External documentation** (in `docs/`) can be verbose with examples, tutorials, and design rationale.
### Regenerating Documentation
After modifying C++ inline documentation:
After modifying C++ inline documentation with MCRF_* macros:
1. **Rebuild the project**: `make -j$(nproc)`
2. **Generate stub files** (for IDE support):
2. **Generate documentation** (automatic from compiled module):
```bash
./build/mcrogueface --exec generate_stubs.py
```
3. **Generate dynamic documentation** (recommended):
```bash
./build/mcrogueface --exec generate_dynamic_docs.py
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
```
This creates:
- `docs/api_reference_dynamic.html`
- `docs/API_REFERENCE_DYNAMIC.md`
4. **Update hardcoded documentation** (if still using old system):
- `generate_complete_api_docs.py` - Update method dictionaries
- `generate_complete_markdown_docs.py` - Update method dictionaries
3. **Generate stub files** (optional, for IDE support):
```bash
./build/mcrogueface --headless --exec tools/generate_stubs.py
```
Creates `.pyi` stub files for type checking and autocompletion
### Important Notes
- **Single source of truth**: Documentation lives in C++ source files via MCRF_* macros
- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python
- **Use --exec flag**: `./build/mcrogueface --exec script.py` or `--headless --exec` for CI/automation
- **Dynamic is better**: The new `generate_dynamic_docs.py` extracts documentation directly from compiled module
- **Keep docstrings consistent**: Follow the format above for automatic parsing
- **Use --headless --exec**: For non-interactive documentation generation
- **Link transformation**: `MCRF_LINK` references are transformed to appropriate format (HTML, Markdown, etc.)
- **No manual dictionaries**: The old hardcoded documentation system has been removed
### Documentation Pipeline Architecture
1. **C++ Source** → PyMethodDef/PyGetSetDef arrays with docstrings
2. **Compilation**Docstrings embedded in compiled module
1. **C++ Source**MCRF_* macros in PyMethodDef/PyGetSetDef arrays
2. **Compilation**Macros expand to complete docstrings embedded in module
3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract
4. **Generation** → HTML/Markdown/Stub files created
4. **Generation** → HTML/Markdown/Stub files created with transformed links
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
The documentation is only as good as the C++ inline docstrings!
The macro system ensures complete, consistent documentation across all Python bindings.

View File

@ -108,7 +108,7 @@
<body>
<div class="container">
<h1>McRogueFace API Reference</h1>
<p><em>Generated on 2025-07-15 21:28:24</em></p>
<p><em>Generated on 2025-10-30 16:58:07</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
<div class="toc">
@ -424,15 +424,37 @@ Attributes:
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
<h5><code class="method-name">get_boundsget_bounds() -> tuple</code></h5>
<p>Get the bounding rectangle of this drawable element.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> tuple: (x, y, width, height) representing the element&#x27;s bounds The bounds are in screen coordinates and account for current position and size.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
<h5><code class="method-name">movemove(dx: float, dy: float) -> None</code></h5>
<p>Move the element by a relative offset.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>dx</span>: Horizontal offset in pixels</div>
<div><span class='arg-name'>dy</span>: Vertical offset in pixels</div>
</div>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
<h5><code class="method-name">resizeresize(width: float, height: float) -> None</code></h5>
<p>Resize the element to new dimensions.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>width</span>: New width in pixels</div>
<div><span class='arg-name'>height</span>: New height in pixels</div>
</div>
</div>
</div>
@ -442,17 +464,38 @@ Attributes:
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">from_hexCreate Color from hex string (e.g., '#FF0000' or 'FF0000')</code></h5>
<h5><code class="method-name">from_hexfrom_hex(hex_string: str) -> Color</code></h5>
<p>Create a Color from a hexadecimal string.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>hex_string</span>: Hex color string (e.g., &#x27;#FF0000&#x27;, &#x27;FF0000&#x27;, &#x27;#AABBCCDD&#x27; for RGBA)</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Color: New Color object with values from hex string ValueError: If hex string is not 6 or 8 characters (RGB or RGBA) This is a class method. Call as Color.from_hex(&#x27;#FF0000&#x27;)</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">lerp(...)</code></h5>
<p>Linearly interpolate between this color and another</p>
<h5><code class="method-name">lerplerp(other: Color, t: float) -> Color</code></h5>
<p>Linearly interpolate between this color and another.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>other</span>: The target Color to interpolate towards</div>
<div><span class='arg-name'>t</span>: Interpolation factor (0.0 = this color, 1.0 = other color). Automatically clamped to [0.0, 1.0]</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Color: New Color representing the interpolated value All components (r, g, b, a) are interpolated independently</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">to_hex(...)</code></h5>
<p>Convert Color to hex string</p>
<h5><code class="method-name">to_hexto_hex() -> str</code></h5>
<p>Convert this Color to a hexadecimal string.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> str: Hex string in format &#x27;#RRGGBB&#x27; or &#x27;#RRGGBBAA&#x27; (if alpha &lt; 255) Alpha component is only included if not fully opaque (&lt; 255)</p>
</div>
</div>
@ -462,15 +505,37 @@ Attributes:
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
<h5><code class="method-name">get_boundsget_bounds() -> tuple</code></h5>
<p>Get the bounding rectangle of this drawable element.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> tuple: (x, y, width, height) representing the element&#x27;s bounds The bounds are in screen coordinates and account for current position and size.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
<h5><code class="method-name">movemove(dx: float, dy: float) -> None</code></h5>
<p>Move the element by a relative offset.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>dx</span>: Horizontal offset in pixels</div>
<div><span class='arg-name'>dy</span>: Vertical offset in pixels</div>
</div>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
<h5><code class="method-name">resizeresize(width: float, height: float) -> None</code></h5>
<p>Resize the element to new dimensions.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>width</span>: New width in pixels</div>
<div><span class='arg-name'>height</span>: New height in pixels</div>
</div>
</div>
</div>
@ -514,7 +579,13 @@ Attributes:
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
<h5><code class="method-name">get_boundsget_bounds() -> tuple</code></h5>
<p>Get the bounding rectangle of this drawable element.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> tuple: (x, y, width, height) representing the element&#x27;s bounds The bounds are in screen coordinates and account for current position and size.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -523,7 +594,15 @@ Attributes:
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
<h5><code class="method-name">movemove(dx: float, dy: float) -> None</code></h5>
<p>Move the element by a relative offset.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>dx</span>: Horizontal offset in pixels</div>
<div><span class='arg-name'>dy</span>: Vertical offset in pixels</div>
</div>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -537,7 +616,15 @@ Attributes:
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
<h5><code class="method-name">resizeresize(width: float, height: float) -> None</code></h5>
<p>Resize the element to new dimensions.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>width</span>: New width in pixels</div>
<div><span class='arg-name'>height</span>: New height in pixels</div>
</div>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -625,15 +712,37 @@ Attributes:
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
<h5><code class="method-name">get_boundsget_bounds() -> tuple</code></h5>
<p>Get the bounding rectangle of this drawable element.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> tuple: (x, y, width, height) representing the element&#x27;s bounds The bounds are in screen coordinates and account for current position and size.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
<h5><code class="method-name">movemove(dx: float, dy: float) -> None</code></h5>
<p>Move the element by a relative offset.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>dx</span>: Horizontal offset in pixels</div>
<div><span class='arg-name'>dy</span>: Vertical offset in pixels</div>
</div>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
<h5><code class="method-name">resizeresize(width: float, height: float) -> None</code></h5>
<p>Resize the element to new dimensions.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>width</span>: New width in pixels</div>
<div><span class='arg-name'>height</span>: New height in pixels</div>
</div>
</div>
</div>
@ -717,8 +826,8 @@ Attributes:
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">compute_fovcompute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None</code></h5>
<p>Compute field of view from a position.</p>
<h5><code class="method-name">compute_fovcompute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]</code></h5>
<p>Compute field of view from a position and return visible cells.</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>x</span>: X coordinate of the viewer</div>
<div><span class='arg-name'>y</span>: Y coordinate of the viewer</div>
@ -726,6 +835,7 @@ Attributes:
<div><span class='arg-name'>light_walls</span>: Whether walls are lit when visible</div>
<div><span class='arg-name'>algorithm</span>: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> List of tuples (x, y, visible, discovered) for all visible cells: - x, y: Grid coordinates - visible: True (all returned cells are visible) - discovered: True (FOV implies discovery)</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -742,7 +852,13 @@ Attributes:
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
<h5><code class="method-name">get_boundsget_bounds() -> tuple</code></h5>
<p>Get the bounding rectangle of this drawable element.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> tuple: (x, y, width, height) representing the element&#x27;s bounds The bounds are in screen coordinates and account for current position and size.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -776,11 +892,27 @@ Attributes:
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
<h5><code class="method-name">movemove(dx: float, dy: float) -> None</code></h5>
<p>Move the element by a relative offset.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>dx</span>: Horizontal offset in pixels</div>
<div><span class='arg-name'>dy</span>: Vertical offset in pixels</div>
</div>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
<h5><code class="method-name">resizeresize(width: float, height: float) -> None</code></h5>
<p>Resize the element to new dimensions.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>width</span>: New width in pixels</div>
<div><span class='arg-name'>height</span>: New height in pixels</div>
</div>
</div>
</div>
@ -856,15 +988,37 @@ Attributes:
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
<h5><code class="method-name">get_boundsget_bounds() -> tuple</code></h5>
<p>Get the bounding rectangle of this drawable element.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> tuple: (x, y, width, height) representing the element&#x27;s bounds The bounds are in screen coordinates and account for current position and size.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
<h5><code class="method-name">movemove(dx: float, dy: float) -> None</code></h5>
<p>Move the element by a relative offset.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>dx</span>: Horizontal offset in pixels</div>
<div><span class='arg-name'>dy</span>: Vertical offset in pixels</div>
</div>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
<h5><code class="method-name">resizeresize(width: float, height: float) -> None</code></h5>
<p>Resize the element to new dimensions.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>width</span>: New width in pixels</div>
<div><span class='arg-name'>height</span>: New height in pixels</div>
</div>
</div>
</div>
@ -915,25 +1069,41 @@ Example:
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">cancelcancel() -> None</code></h5>
<p>Cancel the timer and remove it from the timer system.
The timer will no longer fire and cannot be restarted.</p>
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None The timer will no longer fire and cannot be restarted. The callback will not be called again.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">pausepause() -> None</code></h5>
<p>Pause the timer, preserving the time remaining until next trigger.
The timer can be resumed later with resume().</p>
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None The timer can be resumed later with resume(). Time spent paused does not count toward the interval.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">restartrestart() -> None</code></h5>
<p>Restart the timer from the beginning.
Resets the timer to fire after a full interval from now.</p>
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None Resets the timer to fire after a full interval from now, regardless of remaining time.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">resumeresume() -> None</code></h5>
<p>Resume a paused timer from where it left off.
Has no effect if the timer is not paused.</p>
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.</p>
</div>
</div>
@ -981,38 +1151,59 @@ Has no effect if the timer is not paused.</p>
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">angle(...)</code></h5>
<p>Return the angle in radians from the positive X axis</p>
<h5><code class="method-name">angleangle() -> float</code></h5>
<p>Get the angle of this vector in radians.</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> float: Angle in radians from positive x-axis</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">copy(...)</code></h5>
<p>Return a copy of this vector</p>
<h5><code class="method-name">copycopy() -> Vector</code></h5>
<p>Create a copy of this vector.</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Vector: New Vector object with same x and y values</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">distance_to(...)</code></h5>
<p>Return the distance to another vector</p>
<h5><code class="method-name">distance_todistance_to(other: Vector) -> float</code></h5>
<p>Calculate the distance to another vector.</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>other</span>: The other vector</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> float: Distance between the two vectors</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">dot(...)</code></h5>
<p>Return the dot product with another vector</p>
<h5><code class="method-name">dotdot(other: Vector) -> float</code></h5>
<p>Calculate the dot product with another vector.</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>other</span>: The other vector</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> float: Dot product of the two vectors</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">magnitude(...)</code></h5>
<p>Return the length of the vector</p>
<h5><code class="method-name">magnitudemagnitude() -> float</code></h5>
<p>Calculate the length/magnitude of this vector.</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> float: The magnitude of the vector</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">magnitude_squared(...)</code></h5>
<p>Return the squared length of the vector</p>
<h5><code class="method-name">magnitude_squaredmagnitude_squared() -> float</code></h5>
<p>Calculate the squared magnitude of this vector.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> float: The squared magnitude (faster than magnitude()) Use this for comparisons to avoid expensive square root calculation.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">normalize(...)</code></h5>
<p>Return a unit vector in the same direction</p>
<h5><code class="method-name">normalizenormalize() -> Vector</code></h5>
<p>Return a unit vector in the same direction.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Vector: New normalized vector with magnitude 1.0 For zero vectors (magnitude 0.0), returns a zero vector rather than raising an exception</p>
</div>
</div>

BIN
forgejo-mcp.linux.amd64 Executable file

Binary file not shown.

View File

@ -373,7 +373,7 @@ class Engine:
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background = mcrfpy.Frame((0, 0), (1024, 768))
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)

View File

@ -1,6 +1,7 @@
#include "McRFPy_API.h"
#include "McRFPy_Automation.h"
#include "McRFPy_Libtcod.h"
#include "McRFPy_Doc.h"
#include "platform.h"
#include "PyAnimation.h"
#include "PyDrawable.h"
@ -27,160 +28,173 @@ PyObject* McRFPy_API::mcrf_module;
static PyMethodDef mcrfpyMethods[] = {
{"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS,
"createSoundBuffer(filename: str) -> int\n\n"
"Load a sound effect from a file and return its buffer ID.\n\n"
"Args:\n"
" filename: Path to the sound file (WAV, OGG, FLAC)\n\n"
"Returns:\n"
" int: Buffer ID for use with playSound()\n\n"
"Raises:\n"
" RuntimeError: If the file cannot be loaded"},
MCRF_FUNCTION(createSoundBuffer,
MCRF_SIG("(filename: str)", "int"),
MCRF_DESC("Load a sound effect from a file and return its buffer ID."),
MCRF_ARGS_START
MCRF_ARG("filename", "Path to the sound file (WAV, OGG, FLAC)")
MCRF_RETURNS("int: Buffer ID for use with playSound()")
MCRF_RAISES("RuntimeError", "If the file cannot be loaded")
)},
{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS,
"loadMusic(filename: str) -> None\n\n"
"Load and immediately play background music from a file.\n\n"
"Args:\n"
" filename: Path to the music file (WAV, OGG, FLAC)\n\n"
"Note:\n"
" Only one music track can play at a time. Loading new music stops the current track."},
MCRF_FUNCTION(loadMusic,
MCRF_SIG("(filename: str)", "None"),
MCRF_DESC("Load and immediately play background music from a file."),
MCRF_ARGS_START
MCRF_ARG("filename", "Path to the music file (WAV, OGG, FLAC)")
MCRF_RETURNS("None")
MCRF_NOTE("Only one music track can play at a time. Loading new music stops the current track.")
)},
{"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS,
"setMusicVolume(volume: int) -> None\n\n"
"Set the global music volume.\n\n"
"Args:\n"
" volume: Volume level from 0 (silent) to 100 (full volume)"},
MCRF_FUNCTION(setMusicVolume,
MCRF_SIG("(volume: int)", "None"),
MCRF_DESC("Set the global music volume."),
MCRF_ARGS_START
MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)")
MCRF_RETURNS("None")
)},
{"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS,
"setSoundVolume(volume: int) -> None\n\n"
"Set the global sound effects volume.\n\n"
"Args:\n"
" volume: Volume level from 0 (silent) to 100 (full volume)"},
MCRF_FUNCTION(setSoundVolume,
MCRF_SIG("(volume: int)", "None"),
MCRF_DESC("Set the global sound effects volume."),
MCRF_ARGS_START
MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)")
MCRF_RETURNS("None")
)},
{"playSound", McRFPy_API::_playSound, METH_VARARGS,
"playSound(buffer_id: int) -> None\n\n"
"Play a sound effect using a previously loaded buffer.\n\n"
"Args:\n"
" buffer_id: Sound buffer ID returned by createSoundBuffer()\n\n"
"Raises:\n"
" RuntimeError: If the buffer ID is invalid"},
MCRF_FUNCTION(playSound,
MCRF_SIG("(buffer_id: int)", "None"),
MCRF_DESC("Play a sound effect using a previously loaded buffer."),
MCRF_ARGS_START
MCRF_ARG("buffer_id", "Sound buffer ID returned by createSoundBuffer()")
MCRF_RETURNS("None")
MCRF_RAISES("RuntimeError", "If the buffer ID is invalid")
)},
{"getMusicVolume", McRFPy_API::_getMusicVolume, METH_NOARGS,
"getMusicVolume() -> int\n\n"
"Get the current music volume level.\n\n"
"Returns:\n"
" int: Current volume (0-100)"},
MCRF_FUNCTION(getMusicVolume,
MCRF_SIG("()", "int"),
MCRF_DESC("Get the current music volume level."),
MCRF_RETURNS("int: Current volume (0-100)")
)},
{"getSoundVolume", McRFPy_API::_getSoundVolume, METH_NOARGS,
"getSoundVolume() -> int\n\n"
"Get the current sound effects volume level.\n\n"
"Returns:\n"
" int: Current volume (0-100)"},
MCRF_FUNCTION(getSoundVolume,
MCRF_SIG("()", "int"),
MCRF_DESC("Get the current sound effects volume level."),
MCRF_RETURNS("int: Current volume (0-100)")
)},
{"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS,
"sceneUI(scene: str = None) -> list\n\n"
"Get all UI elements for a scene.\n\n"
"Args:\n"
" scene: Scene name. If None, uses current scene\n\n"
"Returns:\n"
" list: All UI elements (Frame, Caption, Sprite, Grid) in the scene\n\n"
"Raises:\n"
" KeyError: If the specified scene doesn't exist"},
MCRF_FUNCTION(sceneUI,
MCRF_SIG("(scene: str = None)", "list"),
MCRF_DESC("Get all UI elements for a scene."),
MCRF_ARGS_START
MCRF_ARG("scene", "Scene name. If None, uses current scene")
MCRF_RETURNS("list: All UI elements (Frame, Caption, Sprite, Grid) in the scene")
MCRF_RAISES("KeyError", "If the specified scene doesn't exist")
)},
{"currentScene", McRFPy_API::_currentScene, METH_NOARGS,
"currentScene() -> str\n\n"
"Get the name of the currently active scene.\n\n"
"Returns:\n"
" str: Name of the current scene"},
MCRF_FUNCTION(currentScene,
MCRF_SIG("()", "str"),
MCRF_DESC("Get the name of the currently active scene."),
MCRF_RETURNS("str: Name of the current scene")
)},
{"setScene", McRFPy_API::_setScene, METH_VARARGS,
"setScene(scene: str, transition: str = None, duration: float = 0.0) -> None\n\n"
"Switch to a different scene with optional transition effect.\n\n"
"Args:\n"
" scene: Name of the scene to switch to\n"
" transition: Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')\n"
" duration: Transition duration in seconds (default: 0.0 for instant)\n\n"
"Raises:\n"
" KeyError: If the scene doesn't exist\n"
" ValueError: If the transition type is invalid"},
MCRF_FUNCTION(setScene,
MCRF_SIG("(scene: str, transition: str = None, duration: float = 0.0)", "None"),
MCRF_DESC("Switch to a different scene with optional transition effect."),
MCRF_ARGS_START
MCRF_ARG("scene", "Name of the scene to switch to")
MCRF_ARG("transition", "Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')")
MCRF_ARG("duration", "Transition duration in seconds (default: 0.0 for instant)")
MCRF_RETURNS("None")
MCRF_RAISES("KeyError", "If the scene doesn't exist")
MCRF_RAISES("ValueError", "If the transition type is invalid")
)},
{"createScene", McRFPy_API::_createScene, METH_VARARGS,
"createScene(name: str) -> None\n\n"
"Create a new empty scene.\n\n"
"Args:\n"
" name: Unique name for the new scene\n\n"
"Raises:\n"
" ValueError: If a scene with this name already exists\n\n"
"Note:\n"
" The scene is created but not made active. Use setScene() to switch to it."},
MCRF_FUNCTION(createScene,
MCRF_SIG("(name: str)", "None"),
MCRF_DESC("Create a new empty scene."),
MCRF_ARGS_START
MCRF_ARG("name", "Unique name for the new scene")
MCRF_RETURNS("None")
MCRF_RAISES("ValueError", "If a scene with this name already exists")
MCRF_NOTE("The scene is created but not made active. Use setScene() to switch to it.")
)},
{"keypressScene", McRFPy_API::_keypressScene, METH_VARARGS,
"keypressScene(handler: callable) -> None\n\n"
"Set the keyboard event handler for the current scene.\n\n"
"Args:\n"
" handler: Callable that receives (key_name: str, is_pressed: bool)\n\n"
"Example:\n"
" def on_key(key, pressed):\n"
" if key == 'A' and pressed:\n"
" print('A key pressed')\n"
" mcrfpy.keypressScene(on_key)"},
MCRF_FUNCTION(keypressScene,
MCRF_SIG("(handler: callable)", "None"),
MCRF_DESC("Set the keyboard event handler for the current scene."),
MCRF_ARGS_START
MCRF_ARG("handler", "Callable that receives (key_name: str, is_pressed: bool)")
MCRF_RETURNS("None")
MCRF_NOTE("Example: def on_key(key, pressed): if key == 'A' and pressed: print('A key pressed') mcrfpy.keypressScene(on_key)")
)},
{"setTimer", McRFPy_API::_setTimer, METH_VARARGS,
"setTimer(name: str, handler: callable, interval: int) -> None\n\n"
"Create or update a recurring timer.\n\n"
"Args:\n"
" name: Unique identifier for the timer\n"
" handler: Function called with (runtime: float) parameter\n"
" interval: Time between calls in milliseconds\n\n"
"Note:\n"
" If a timer with this name exists, it will be replaced.\n"
" The handler receives the total runtime in seconds as its argument."},
MCRF_FUNCTION(setTimer,
MCRF_SIG("(name: str, handler: callable, interval: int)", "None"),
MCRF_DESC("Create or update a recurring timer."),
MCRF_ARGS_START
MCRF_ARG("name", "Unique identifier for the timer")
MCRF_ARG("handler", "Function called with (runtime: float) parameter")
MCRF_ARG("interval", "Time between calls in milliseconds")
MCRF_RETURNS("None")
MCRF_NOTE("If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.")
)},
{"delTimer", McRFPy_API::_delTimer, METH_VARARGS,
"delTimer(name: str) -> None\n\n"
"Stop and remove a timer.\n\n"
"Args:\n"
" name: Timer identifier to remove\n\n"
"Note:\n"
" No error is raised if the timer doesn't exist."},
MCRF_FUNCTION(delTimer,
MCRF_SIG("(name: str)", "None"),
MCRF_DESC("Stop and remove a timer."),
MCRF_ARGS_START
MCRF_ARG("name", "Timer identifier to remove")
MCRF_RETURNS("None")
MCRF_NOTE("No error is raised if the timer doesn't exist.")
)},
{"exit", McRFPy_API::_exit, METH_NOARGS,
"exit() -> None\n\n"
"Cleanly shut down the game engine and exit the application.\n\n"
"Note:\n"
" This immediately closes the window and terminates the program."},
MCRF_FUNCTION(exit,
MCRF_SIG("()", "None"),
MCRF_DESC("Cleanly shut down the game engine and exit the application."),
MCRF_RETURNS("None")
MCRF_NOTE("This immediately closes the window and terminates the program.")
)},
{"setScale", McRFPy_API::_setScale, METH_VARARGS,
"setScale(multiplier: float) -> None\n\n"
"Scale the game window size.\n\n"
"Args:\n"
" multiplier: Scale factor (e.g., 2.0 for double size)\n\n"
"Note:\n"
" The internal resolution remains 1024x768, but the window is scaled.\n"
" This is deprecated - use Window.resolution instead."},
MCRF_FUNCTION(setScale,
MCRF_SIG("(multiplier: float)", "None"),
MCRF_DESC("Scale the game window size."),
MCRF_ARGS_START
MCRF_ARG("multiplier", "Scale factor (e.g., 2.0 for double size)")
MCRF_RETURNS("None")
MCRF_NOTE("The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.")
)},
{"find", McRFPy_API::_find, METH_VARARGS,
"find(name: str, scene: str = None) -> UIDrawable | None\n\n"
"Find the first UI element with the specified name.\n\n"
"Args:\n"
" name: Exact name to search for\n"
" scene: Scene to search in (default: current scene)\n\n"
"Returns:\n"
" Frame, Caption, Sprite, Grid, or Entity if found; None otherwise\n\n"
"Note:\n"
" Searches scene UI elements and entities within grids."},
MCRF_FUNCTION(find,
MCRF_SIG("(name: str, scene: str = None)", "UIDrawable | None"),
MCRF_DESC("Find the first UI element with the specified name."),
MCRF_ARGS_START
MCRF_ARG("name", "Exact name to search for")
MCRF_ARG("scene", "Scene to search in (default: current scene)")
MCRF_RETURNS("Frame, Caption, Sprite, Grid, or Entity if found; None otherwise")
MCRF_NOTE("Searches scene UI elements and entities within grids.")
)},
{"findAll", McRFPy_API::_findAll, METH_VARARGS,
"findAll(pattern: str, scene: str = None) -> list\n\n"
"Find all UI elements matching a name pattern.\n\n"
"Args:\n"
" pattern: Name pattern with optional wildcards (* matches any characters)\n"
" scene: Scene to search in (default: current scene)\n\n"
"Returns:\n"
" list: All matching UI elements and entities\n\n"
"Example:\n"
" findAll('enemy*') # Find all elements starting with 'enemy'\n"
" findAll('*_button') # Find all elements ending with '_button'"},
MCRF_FUNCTION(findAll,
MCRF_SIG("(pattern: str, scene: str = None)", "list"),
MCRF_DESC("Find all UI elements matching a name pattern."),
MCRF_ARGS_START
MCRF_ARG("pattern", "Name pattern with optional wildcards (* matches any characters)")
MCRF_ARG("scene", "Scene to search in (default: current scene)")
MCRF_RETURNS("list: All matching UI elements and entities")
MCRF_NOTE("Example: findAll('enemy*') finds all elements starting with 'enemy', findAll('*_button') finds all elements ending with '_button'")
)},
{"getMetrics", McRFPy_API::_getMetrics, METH_NOARGS,
"getMetrics() -> dict\n\n"
"Get current performance metrics.\n\n"
"Returns:\n"
" dict: Performance data with keys:\n"
" - frame_time: Last frame duration in seconds\n"
" - avg_frame_time: Average frame time\n"
" - fps: Frames per second\n"
" - draw_calls: Number of draw calls\n"
" - ui_elements: Total UI element count\n"
" - visible_elements: Visible element count\n"
" - current_frame: Frame counter\n"
" - runtime: Total runtime in seconds"},
MCRF_FUNCTION(getMetrics,
MCRF_SIG("()", "dict"),
MCRF_DESC("Get current performance metrics."),
MCRF_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)")
)},
{NULL, NULL, 0, NULL}
};
@ -188,27 +202,27 @@ static PyMethodDef mcrfpyMethods[] = {
static PyModuleDef mcrfpyModule = {
PyModuleDef_HEAD_INIT, /* m_base - Always initialize this member to PyModuleDef_HEAD_INIT. */
"mcrfpy", /* m_name */
PyDoc_STR("McRogueFace Python API\\n\\n"
"Core game engine interface for creating roguelike games with Python.\\n\\n"
"This module provides:\\n"
"- Scene management (createScene, setScene, currentScene)\\n"
"- UI components (Frame, Caption, Sprite, Grid)\\n"
"- Entity system for game objects\\n"
"- Audio playback (sound effects and music)\\n"
"- Timer system for scheduled events\\n"
"- Input handling\\n"
"- Performance metrics\\n\\n"
"Example:\\n"
" import mcrfpy\\n"
" \\n"
" # Create a new scene\\n"
" mcrfpy.createScene('game')\\n"
" mcrfpy.setScene('game')\\n"
" \\n"
" # Add UI elements\\n"
" frame = mcrfpy.Frame(10, 10, 200, 100)\\n"
" caption = mcrfpy.Caption('Hello World', 50, 50)\\n"
" mcrfpy.sceneUI().extend([frame, caption])\\n"),
PyDoc_STR("McRogueFace Python API\n\n"
"Core game engine interface for creating roguelike games with Python.\n\n"
"This module provides:\n"
"- Scene management (createScene, setScene, currentScene)\n"
"- UI components (Frame, Caption, Sprite, Grid)\n"
"- Entity system for game objects\n"
"- Audio playback (sound effects and music)\n"
"- Timer system for scheduled events\n"
"- Input handling\n"
"- Performance metrics\n\n"
"Example:\n"
" import mcrfpy\n"
" \n"
" # Create a new scene\n"
" mcrfpy.createScene('game')\n"
" mcrfpy.setScene('game')\n"
" \n"
" # Add UI elements\n"
" frame = mcrfpy.Frame(10, 10, 200, 100)\n"
" caption = mcrfpy.Caption('Hello World', 50, 50)\n"
" mcrfpy.sceneUI().extend([frame, caption])\n"),
-1, /* m_size - Setting m_size to -1 means that the module does not support sub-interpreters, because it has global state. */
mcrfpyMethods, /* m_methods */
NULL, /* m_slots - An array of slot definitions ... When using single-phase initialization, m_slots must be NULL. */

31
src/McRFPy_Doc.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef MCRFPY_DOC_H
#define MCRFPY_DOC_H
// Section builders for documentation
#define MCRF_SIG(params, ret) params " -> " ret "\n\n"
#define MCRF_DESC(text) text "\n\n"
#define MCRF_ARGS_START "Args:\n"
#define MCRF_ARG(name, desc) " " name ": " desc "\n"
#define MCRF_RETURNS(text) "\nReturns:\n " text "\n"
#define MCRF_RAISES(exc, desc) "\nRaises:\n " exc ": " desc "\n"
#define MCRF_NOTE(text) "\nNote:\n " text "\n"
// Link to external documentation
// Format: MCRF_LINK("docs/file.md", "Link Text")
// Parsers detect this pattern and format per output type
#define MCRF_LINK(ref, text) "\nSee also: " text " (" ref ")\n"
// Main documentation macros
#define MCRF_METHOD_DOC(name, sig, desc, ...) \
name sig desc __VA_ARGS__
#define MCRF_FUNCTION(name, ...) \
MCRF_METHOD_DOC(#name, __VA_ARGS__)
#define MCRF_METHOD(cls, name, ...) \
MCRF_METHOD_DOC(#name, __VA_ARGS__)
#define MCRF_PROPERTY(name, desc) \
desc
#endif // MCRFPY_DOC_H

View File

@ -1,5 +1,6 @@
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include "UIDrawable.h"
#include "UIFrame.h"
#include "UICaption.h"
@ -261,33 +262,58 @@ PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args)
}
PyGetSetDef PyAnimation::getsetters[] = {
{"property", (getter)get_property, NULL, "Target property name", NULL},
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
{"elapsed", (getter)get_elapsed, NULL, "Elapsed time in seconds", NULL},
{"is_complete", (getter)get_is_complete, NULL, "Whether animation is complete", NULL},
{"is_delta", (getter)get_is_delta, NULL, "Whether animation uses delta mode", NULL},
{"property", (getter)get_property, NULL,
MCRF_PROPERTY(property, "Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index')."), NULL},
{"duration", (getter)get_duration, NULL,
MCRF_PROPERTY(duration, "Animation duration in seconds (float, read-only). Total time for the animation to complete."), NULL},
{"elapsed", (getter)get_elapsed, NULL,
MCRF_PROPERTY(elapsed, "Elapsed time in seconds (float, read-only). Time since the animation started."), NULL},
{"is_complete", (getter)get_is_complete, NULL,
MCRF_PROPERTY(is_complete, "Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called."), NULL},
{"is_delta", (getter)get_is_delta, NULL,
MCRF_PROPERTY(is_delta, "Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value."), NULL},
{NULL}
};
PyMethodDef PyAnimation::methods[] = {
{"start", (PyCFunction)start, METH_VARARGS,
"start(target) -> None\n\n"
"Start the animation on a target UI element.\n\n"
"Args:\n"
" target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)\n\n"
"Note:\n"
" The animation will automatically stop if the target is destroyed."},
MCRF_METHOD(Animation, start,
MCRF_SIG("(target: UIDrawable)", "None"),
MCRF_DESC("Start the animation on a target UI element."),
MCRF_ARGS_START
MCRF_ARG("target", "The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)")
MCRF_RETURNS("None")
MCRF_NOTE("The animation will automatically stop if the target is destroyed. Call AnimationManager.update(delta_time) each frame to progress animations.")
)},
{"update", (PyCFunction)update, METH_VARARGS,
"Update the animation by deltaTime (returns True if still running)"},
MCRF_METHOD(Animation, update,
MCRF_SIG("(delta_time: float)", "bool"),
MCRF_DESC("Update the animation by the given time delta."),
MCRF_ARGS_START
MCRF_ARG("delta_time", "Time elapsed since last update in seconds")
MCRF_RETURNS("bool: True if animation is still running, False if complete")
MCRF_NOTE("Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.")
)},
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
"Get the current interpolated value"},
MCRF_METHOD(Animation, get_current_value,
MCRF_SIG("()", "Any"),
MCRF_DESC("Get the current interpolated value of the animation."),
MCRF_RETURNS("Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str)")
MCRF_NOTE("Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).")
)},
{"complete", (PyCFunction)complete, METH_NOARGS,
"complete() -> None\n\n"
"Complete the animation immediately by jumping to the final value."},
MCRF_METHOD(Animation, complete,
MCRF_SIG("()", "None"),
MCRF_DESC("Complete the animation immediately by jumping to the final value."),
MCRF_RETURNS("None")
MCRF_NOTE("Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.")
)},
{"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS,
"hasValidTarget() -> bool\n\n"
"Check if the animation still has a valid target.\n\n"
"Returns:\n"
" True if the target still exists, False if it was destroyed."},
MCRF_METHOD(Animation, hasValidTarget,
MCRF_SIG("()", "bool"),
MCRF_DESC("Check if the animation still has a valid target."),
MCRF_RETURNS("bool: True if the target still exists, False if it was destroyed")
MCRF_NOTE("Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.")
)},
{NULL}
};

View File

@ -2,21 +2,50 @@
#include "McRFPy_API.h"
#include "PyObjectUtils.h"
#include "PyRAII.h"
#include "McRFPy_Doc.h"
#include <string>
#include <cstdio>
PyGetSetDef PyColor::getsetters[] = {
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0},
{"g", (getter)PyColor::get_member, (setter)PyColor::set_member, "Green component", (void*)1},
{"b", (getter)PyColor::get_member, (setter)PyColor::set_member, "Blue component", (void*)2},
{"a", (getter)PyColor::get_member, (setter)PyColor::set_member, "Alpha component", (void*)3},
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member,
MCRF_PROPERTY(r, "Red component (0-255). Automatically clamped to valid range."), (void*)0},
{"g", (getter)PyColor::get_member, (setter)PyColor::set_member,
MCRF_PROPERTY(g, "Green component (0-255). Automatically clamped to valid range."), (void*)1},
{"b", (getter)PyColor::get_member, (setter)PyColor::set_member,
MCRF_PROPERTY(b, "Blue component (0-255). Automatically clamped to valid range."), (void*)2},
{"a", (getter)PyColor::get_member, (setter)PyColor::set_member,
MCRF_PROPERTY(a, "Alpha component (0-255, where 0=transparent, 255=opaque). Automatically clamped to valid range."), (void*)3},
{NULL}
};
PyMethodDef PyColor::methods[] = {
{"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS, "Create Color from hex string (e.g., '#FF0000' or 'FF0000')"},
{"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS, "Convert Color to hex string"},
{"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS, "Linearly interpolate between this color and another"},
{"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS,
MCRF_METHOD(Color, from_hex,
MCRF_SIG("(hex_string: str)", "Color"),
MCRF_DESC("Create a Color from a hexadecimal string."),
MCRF_ARGS_START
MCRF_ARG("hex_string", "Hex color string (e.g., '#FF0000', 'FF0000', '#AABBCCDD' for RGBA)")
MCRF_RETURNS("Color: New Color object with values from hex string")
MCRF_RAISES("ValueError", "If hex string is not 6 or 8 characters (RGB or RGBA)")
MCRF_NOTE("This is a class method. Call as Color.from_hex('#FF0000')")
)},
{"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS,
MCRF_METHOD(Color, to_hex,
MCRF_SIG("()", "str"),
MCRF_DESC("Convert this Color to a hexadecimal string."),
MCRF_RETURNS("str: Hex string in format '#RRGGBB' or '#RRGGBBAA' (if alpha < 255)")
MCRF_NOTE("Alpha component is only included if not fully opaque (< 255)")
)},
{"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS,
MCRF_METHOD(Color, lerp,
MCRF_SIG("(other: Color, t: float)", "Color"),
MCRF_DESC("Linearly interpolate between this color and another."),
MCRF_ARGS_START
MCRF_ARG("other", "The target Color to interpolate towards")
MCRF_ARG("t", "Interpolation factor (0.0 = this color, 1.0 = other color). Automatically clamped to [0.0, 1.0]")
MCRF_RETURNS("Color: New Color representing the interpolated value")
MCRF_NOTE("All components (r, g, b, a) are interpolated independently")
)},
{NULL}
};

View File

@ -1,5 +1,6 @@
#include "PyDrawable.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
// Click property getter
static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
@ -99,13 +100,25 @@ static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void*
// GetSetDef array for properties
static PyGetSetDef PyDrawable_getsetters[] = {
{"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
"Callable executed when object is clicked", NULL},
MCRF_PROPERTY(click,
"Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click."
), NULL},
{"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index,
"Z-order for rendering (lower values rendered first)", NULL},
MCRF_PROPERTY(z_index,
"Z-order for rendering (lower values rendered first). "
"Automatically triggers scene resort when changed."
), NULL},
{"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible,
"Whether the object is visible", NULL},
MCRF_PROPERTY(visible,
"Whether the object is visible (bool). "
"Invisible objects are not rendered or clickable."
), NULL},
{"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity,
"Opacity level (0.0 = transparent, 1.0 = opaque)", NULL},
MCRF_PROPERTY(opacity,
"Opacity level (0.0 = transparent, 1.0 = opaque). "
"Automatically clamped to valid range [0.0, 1.0]."
), NULL},
{NULL} // Sentinel
};
@ -143,11 +156,30 @@ static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args)
// Method definitions
static PyMethodDef PyDrawable_methods[] = {
{"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS,
"Get bounding box as (x, y, width, height)"},
MCRF_METHOD(Drawable, get_bounds,
MCRF_SIG("()", "tuple"),
MCRF_DESC("Get the bounding rectangle of this drawable element."),
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds")
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.")
)},
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS,
"Move by relative offset (dx, dy)"},
MCRF_METHOD(Drawable, move,
MCRF_SIG("(dx: float, dy: float)", "None"),
MCRF_DESC("Move the element by a relative offset."),
MCRF_ARGS_START
MCRF_ARG("dx", "Horizontal offset in pixels")
MCRF_ARG("dy", "Vertical offset in pixels")
MCRF_NOTE("This modifies the x and y position properties by the given amounts.")
)},
{"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS,
"Resize to new dimensions (width, height)"},
MCRF_METHOD(Drawable, resize,
MCRF_SIG("(width: float, height: float)", "None"),
MCRF_DESC("Resize the element to new dimensions."),
MCRF_ARGS_START
MCRF_ARG("width", "New width in pixels")
MCRF_ARG("height", "New height in pixels")
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.")
)},
{NULL} // Sentinel
};

View File

@ -1,5 +1,6 @@
#include "PyFont.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
PyFont::PyFont(std::string filename)
@ -73,7 +74,9 @@ PyObject* PyFont::get_source(PyFontObject* self, void* closure)
}
PyGetSetDef PyFont::getsetters[] = {
{"family", (getter)PyFont::get_family, NULL, "Font family name", NULL},
{"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL},
{"family", (getter)PyFont::get_family, NULL,
MCRF_PROPERTY(family, "Font family name (str, read-only). Retrieved from font metadata."), NULL},
{"source", (getter)PyFont::get_source, NULL,
MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this font."), NULL},
{NULL} // Sentinel
};

View File

@ -2,6 +2,7 @@
#include "PyScene.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include <iostream>
// Static map to store Python scene objects by name
@ -213,19 +214,38 @@ void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
// Properties
PyGetSetDef PySceneClass::getsetters[] = {
{"name", (getter)get_name, NULL, "Scene name", NULL},
{"active", (getter)get_active, NULL, "Whether this scene is currently active", NULL},
{"name", (getter)get_name, NULL,
MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL},
{"active", (getter)get_active, NULL,
MCRF_PROPERTY(active, "Whether this scene is currently active (bool, read-only). Only one scene can be active at a time."), NULL},
{NULL}
};
// Methods
PyMethodDef PySceneClass::methods[] = {
{"activate", (PyCFunction)activate, METH_NOARGS,
"Make this the active scene"},
MCRF_METHOD(SceneClass, activate,
MCRF_SIG("()", "None"),
MCRF_DESC("Make this the active scene."),
MCRF_RETURNS("None")
MCRF_NOTE("Deactivates the current scene and activates this one. Scene transitions and lifecycle callbacks are triggered.")
)},
{"get_ui", (PyCFunction)get_ui, METH_NOARGS,
"Get the UI element collection for this scene"},
MCRF_METHOD(SceneClass, get_ui,
MCRF_SIG("()", "UICollection"),
MCRF_DESC("Get the UI element collection for this scene."),
MCRF_RETURNS("UICollection: Collection of UI elements (Frames, Captions, Sprites, Grids) in this scene")
MCRF_NOTE("Use to add, remove, or iterate over UI elements. Changes are reflected immediately.")
)},
{"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS,
"Register a keyboard handler function (alternative to overriding on_keypress)"},
MCRF_METHOD(SceneClass, register_keyboard,
MCRF_SIG("(callback: callable)", "None"),
MCRF_DESC("Register a keyboard event handler function."),
MCRF_ARGS_START
MCRF_ARG("callback", "Function that receives (key: str, pressed: bool) when keyboard events occur")
MCRF_RETURNS("None")
MCRF_NOTE("Alternative to overriding on_keypress() method. Handler is called for both key press and release events.")
)},
{NULL}
};

View File

@ -1,5 +1,6 @@
#include "PyTexture.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0)
@ -131,11 +132,17 @@ PyObject* PyTexture::get_source(PyTextureObject* self, void* closure)
}
PyGetSetDef PyTexture::getsetters[] = {
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL, "Width of each sprite in pixels", NULL},
{"sprite_height", (getter)PyTexture::get_sprite_height, NULL, "Height of each sprite in pixels", NULL},
{"sheet_width", (getter)PyTexture::get_sheet_width, NULL, "Number of sprite columns in the texture", NULL},
{"sheet_height", (getter)PyTexture::get_sheet_height, NULL, "Number of sprite rows in the texture", NULL},
{"sprite_count", (getter)PyTexture::get_sprite_count, NULL, "Total number of sprites in the texture", NULL},
{"source", (getter)PyTexture::get_source, NULL, "Source filename of the texture", NULL},
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL,
MCRF_PROPERTY(sprite_width, "Width of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL},
{"sprite_height", (getter)PyTexture::get_sprite_height, NULL,
MCRF_PROPERTY(sprite_height, "Height of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL},
{"sheet_width", (getter)PyTexture::get_sheet_width, NULL,
MCRF_PROPERTY(sheet_width, "Number of sprite columns in the texture sheet (int, read-only). Calculated as texture_width / sprite_width."), NULL},
{"sheet_height", (getter)PyTexture::get_sheet_height, NULL,
MCRF_PROPERTY(sheet_height, "Number of sprite rows in the texture sheet (int, read-only). Calculated as texture_height / sprite_height."), NULL},
{"sprite_count", (getter)PyTexture::get_sprite_count, NULL,
MCRF_PROPERTY(sprite_count, "Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height."), NULL},
{"source", (getter)PyTexture::get_source, NULL,
MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this texture."), NULL},
{NULL} // Sentinel
};

View File

@ -3,6 +3,7 @@
#include "GameEngine.h"
#include "Resources.h"
#include "PythonObjectCache.h"
#include "McRFPy_Doc.h"
#include <sstream>
PyObject* PyTimer::repr(PyObject* self) {
@ -307,38 +308,50 @@ PyObject* PyTimer::get_name(PyTimerObject* self, void* closure) {
PyGetSetDef PyTimer::getsetters[] = {
{"name", (getter)PyTimer::get_name, NULL,
"Timer name (read-only)", NULL},
MCRF_PROPERTY(name, "Timer name (str, read-only). Unique identifier for this timer."), NULL},
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
"Timer interval in milliseconds", NULL},
MCRF_PROPERTY(interval, "Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running."), NULL},
{"remaining", (getter)PyTimer::get_remaining, NULL,
"Time remaining until next trigger in milliseconds", NULL},
MCRF_PROPERTY(remaining, "Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused."), NULL},
{"paused", (getter)PyTimer::get_paused, NULL,
"Whether the timer is paused", NULL},
MCRF_PROPERTY(paused, "Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time."), NULL},
{"active", (getter)PyTimer::get_active, NULL,
"Whether the timer is active and not paused", NULL},
MCRF_PROPERTY(active, "Whether the timer is active and not paused (bool, read-only). False if cancelled or paused."), NULL},
{"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback,
"The callback function to be called", NULL},
MCRF_PROPERTY(callback, "The callback function to be called when timer fires (callable). Can be changed while timer is running."), NULL},
{"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once,
"Whether the timer stops after firing once", NULL},
MCRF_PROPERTY(once, "Whether the timer stops after firing once (bool). If False, timer repeats indefinitely."), NULL},
{NULL}
};
PyMethodDef PyTimer::methods[] = {
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
"pause() -> None\n\n"
"Pause the timer, preserving the time remaining until next trigger.\n"
"The timer can be resumed later with resume()."},
MCRF_METHOD(Timer, pause,
MCRF_SIG("()", "None"),
MCRF_DESC("Pause the timer, preserving the time remaining until next trigger."),
MCRF_RETURNS("None")
MCRF_NOTE("The timer can be resumed later with resume(). Time spent paused does not count toward the interval.")
)},
{"resume", (PyCFunction)PyTimer::resume, METH_NOARGS,
"resume() -> None\n\n"
"Resume a paused timer from where it left off.\n"
"Has no effect if the timer is not paused."},
MCRF_METHOD(Timer, resume,
MCRF_SIG("()", "None"),
MCRF_DESC("Resume a paused timer from where it left off."),
MCRF_RETURNS("None")
MCRF_NOTE("Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.")
)},
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
"cancel() -> None\n\n"
"Cancel the timer and remove it from the timer system.\n"
"The timer will no longer fire and cannot be restarted."},
MCRF_METHOD(Timer, cancel,
MCRF_SIG("()", "None"),
MCRF_DESC("Cancel the timer and remove it from the timer system."),
MCRF_RETURNS("None")
MCRF_NOTE("The timer will no longer fire and cannot be restarted. The callback will not be called again.")
)},
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
"restart() -> None\n\n"
"Restart the timer from the beginning.\n"
"Resets the timer to fire after a full interval from now."},
MCRF_METHOD(Timer, restart,
MCRF_SIG("()", "None"),
MCRF_DESC("Restart the timer from the beginning."),
MCRF_RETURNS("None")
MCRF_NOTE("Resets the timer to fire after a full interval from now, regardless of remaining time.")
)},
{NULL}
};

View File

@ -1,21 +1,65 @@
#include "PyVector.h"
#include "PyObjectUtils.h"
#include "McRFPy_Doc.h"
#include <cmath>
PyGetSetDef PyVector::getsetters[] = {
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member, "X/horizontal component", (void*)0},
{"y", (getter)PyVector::get_member, (setter)PyVector::set_member, "Y/vertical component", (void*)1},
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member,
MCRF_PROPERTY(x, "X coordinate of the vector (float)"), (void*)0},
{"y", (getter)PyVector::get_member, (setter)PyVector::set_member,
MCRF_PROPERTY(y, "Y coordinate of the vector (float)"), (void*)1},
{NULL}
};
PyMethodDef PyVector::methods[] = {
{"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS, "Return the length of the vector"},
{"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS, "Return the squared length of the vector"},
{"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS, "Return a unit vector in the same direction"},
{"dot", (PyCFunction)PyVector::dot, METH_O, "Return the dot product with another vector"},
{"distance_to", (PyCFunction)PyVector::distance_to, METH_O, "Return the distance to another vector"},
{"angle", (PyCFunction)PyVector::angle, METH_NOARGS, "Return the angle in radians from the positive X axis"},
{"copy", (PyCFunction)PyVector::copy, METH_NOARGS, "Return a copy of this vector"},
{"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS,
MCRF_METHOD(Vector, magnitude,
MCRF_SIG("()", "float"),
MCRF_DESC("Calculate the length/magnitude of this vector."),
MCRF_RETURNS("float: The magnitude of the vector")
)},
{"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS,
MCRF_METHOD(Vector, magnitude_squared,
MCRF_SIG("()", "float"),
MCRF_DESC("Calculate the squared magnitude of this vector."),
MCRF_RETURNS("float: The squared magnitude (faster than magnitude())")
MCRF_NOTE("Use this for comparisons to avoid expensive square root calculation.")
)},
{"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS,
MCRF_METHOD(Vector, normalize,
MCRF_SIG("()", "Vector"),
MCRF_DESC("Return a unit vector in the same direction."),
MCRF_RETURNS("Vector: New normalized vector with magnitude 1.0")
MCRF_NOTE("For zero vectors (magnitude 0.0), returns a zero vector rather than raising an exception")
)},
{"dot", (PyCFunction)PyVector::dot, METH_O,
MCRF_METHOD(Vector, dot,
MCRF_SIG("(other: Vector)", "float"),
MCRF_DESC("Calculate the dot product with another vector."),
MCRF_ARGS_START
MCRF_ARG("other", "The other vector")
MCRF_RETURNS("float: Dot product of the two vectors")
)},
{"distance_to", (PyCFunction)PyVector::distance_to, METH_O,
MCRF_METHOD(Vector, distance_to,
MCRF_SIG("(other: Vector)", "float"),
MCRF_DESC("Calculate the distance to another vector."),
MCRF_ARGS_START
MCRF_ARG("other", "The other vector")
MCRF_RETURNS("float: Distance between the two vectors")
)},
{"angle", (PyCFunction)PyVector::angle, METH_NOARGS,
MCRF_METHOD(Vector, angle,
MCRF_SIG("()", "float"),
MCRF_DESC("Get the angle of this vector in radians."),
MCRF_RETURNS("float: Angle in radians from positive x-axis")
)},
{"copy", (PyCFunction)PyVector::copy, METH_NOARGS,
MCRF_METHOD(Vector, copy,
MCRF_SIG("()", "Vector"),
MCRF_DESC("Create a copy of this vector."),
MCRF_RETURNS("Vector: New Vector object with same x and y values")
)},
{NULL}
};

View File

@ -1,6 +1,7 @@
#include "PyWindow.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include <SFML/Graphics.hpp>
#include <cstring>
@ -484,31 +485,48 @@ int PyWindow::set_scaling_mode(PyWindowObject* self, PyObject* value, void* clos
// Property definitions
PyGetSetDef PyWindow::getsetters[] = {
{"resolution", (getter)get_resolution, (setter)set_resolution,
"Window resolution as (width, height) tuple", NULL},
MCRF_PROPERTY(resolution, "Window resolution as (width, height) tuple. Setting this recreates the window."), NULL},
{"fullscreen", (getter)get_fullscreen, (setter)set_fullscreen,
"Window fullscreen state", NULL},
MCRF_PROPERTY(fullscreen, "Window fullscreen state (bool). Setting this recreates the window."), NULL},
{"vsync", (getter)get_vsync, (setter)set_vsync,
"Vertical sync enabled state", NULL},
MCRF_PROPERTY(vsync, "Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate."), NULL},
{"title", (getter)get_title, (setter)set_title,
"Window title string", NULL},
MCRF_PROPERTY(title, "Window title string (str). Displayed in the window title bar."), NULL},
{"visible", (getter)get_visible, (setter)set_visible,
"Window visibility state", NULL},
MCRF_PROPERTY(visible, "Window visibility state (bool). Hidden windows still process events."), NULL},
{"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit,
"Frame rate limit (0 for unlimited)", NULL},
MCRF_PROPERTY(framerate_limit, "Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate."), NULL},
{"game_resolution", (getter)get_game_resolution, (setter)set_game_resolution,
"Fixed game resolution as (width, height) tuple", NULL},
MCRF_PROPERTY(game_resolution, "Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling."), NULL},
{"scaling_mode", (getter)get_scaling_mode, (setter)set_scaling_mode,
"Viewport scaling mode: 'center', 'stretch', or 'fit'", NULL},
MCRF_PROPERTY(scaling_mode, "Viewport scaling mode (str): 'center' (no scaling), 'stretch' (fill window), or 'fit' (maintain aspect ratio)."), NULL},
{NULL}
};
// Method definitions
PyMethodDef PyWindow::methods[] = {
{"get", (PyCFunction)PyWindow::get, METH_VARARGS | METH_CLASS,
"Get the Window singleton instance"},
MCRF_METHOD(Window, get,
MCRF_SIG("()", "Window"),
MCRF_DESC("Get the Window singleton instance."),
MCRF_RETURNS("Window: The global window object")
MCRF_NOTE("This is a class method. Call as Window.get(). There is only one window instance per application.")
)},
{"center", (PyCFunction)PyWindow::center, METH_NOARGS,
"Center the window on the screen"},
MCRF_METHOD(Window, center,
MCRF_SIG("()", "None"),
MCRF_DESC("Center the window on the screen."),
MCRF_RETURNS("None")
MCRF_NOTE("Only works in windowed mode. Has no effect when fullscreen or in headless mode.")
)},
{"screenshot", (PyCFunction)PyWindow::screenshot, METH_VARARGS | METH_KEYWORDS,
"Take a screenshot. Pass filename to save to file, or get raw bytes if no filename."},
MCRF_METHOD(Window, screenshot,
MCRF_SIG("(filename: str = None)", "bytes | None"),
MCRF_DESC("Take a screenshot of the current window contents."),
MCRF_ARGS_START
MCRF_ARG("filename", "Optional path to save screenshot. If omitted, returns raw RGBA bytes.")
MCRF_RETURNS("bytes | None: Raw RGBA pixel data if no filename given, otherwise None after saving")
MCRF_NOTE("Screenshot is taken at the actual window resolution. Use after render loop update for current frame.")
)},
{NULL}
};

View File

@ -1,5 +1,6 @@
#pragma once
#include "Python.h"
#include "McRFPy_Doc.h"
#include <memory>
class UIEntity;
@ -78,11 +79,30 @@ static PyObject* UIDrawable_resize(T* self, PyObject* args)
// Macro to add common UIDrawable methods to a method array
#define UIDRAWABLE_METHODS \
{"get_bounds", (PyCFunction)UIDrawable_get_bounds<PyObjectType>, METH_NOARGS, \
"Get bounding box as (x, y, width, height)"}, \
MCRF_METHOD(Drawable, get_bounds, \
MCRF_SIG("()", "tuple"), \
MCRF_DESC("Get the bounding rectangle of this drawable element."), \
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") \
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") \
)}, \
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS, \
"Move by relative offset (dx, dy)"}, \
MCRF_METHOD(Drawable, move, \
MCRF_SIG("(dx: float, dy: float)", "None"), \
MCRF_DESC("Move the element by a relative offset."), \
MCRF_ARGS_START \
MCRF_ARG("dx", "Horizontal offset in pixels") \
MCRF_ARG("dy", "Vertical offset in pixels") \
MCRF_NOTE("This modifies the x and y position properties by the given amounts.") \
)}, \
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS, \
"Resize to new dimensions (width, height)"}
MCRF_METHOD(Drawable, resize, \
MCRF_SIG("(width: float, height: float)", "None"), \
MCRF_DESC("Resize the element to new dimensions."), \
MCRF_ARGS_START \
MCRF_ARG("width", "New width in pixels") \
MCRF_ARG("height", "New height in pixels") \
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") \
)}
// Property getters/setters for visible and opacity
template<typename T>
@ -132,8 +152,14 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
// Macro to add common UIDrawable properties to a getsetters array
#define UIDRAWABLE_GETSETTERS \
{"visible", (getter)UIDrawable_get_visible<PyObjectType>, (setter)UIDrawable_set_visible<PyObjectType>, \
"Visibility flag", NULL}, \
MCRF_PROPERTY(visible, \
"Whether the object is visible (bool). " \
"Invisible objects are not rendered or clickable." \
), NULL}, \
{"opacity", (getter)UIDrawable_get_opacity<PyObjectType>, (setter)UIDrawable_set_opacity<PyObjectType>, \
"Opacity (0.0 = transparent, 1.0 = opaque)", NULL}
MCRF_PROPERTY(opacity, \
"Opacity level (0.0 = transparent, 1.0 = opaque). " \
"Automatically clamped to valid range [0.0, 1.0]." \
), NULL}
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete

View File

@ -273,8 +273,16 @@ 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, "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},
{"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},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION},
UIDRAWABLE_GETSETTERS,
{NULL}

View File

@ -398,8 +398,16 @@ 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, "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},
{"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},
{"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},

View File

@ -1418,7 +1418,11 @@ 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, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID},
{"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},
{"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},
@ -1428,7 +1432,11 @@ 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, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
{"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},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
UIDRAWABLE_GETSETTERS,
{NULL} /* Sentinel */

View File

@ -339,8 +339,16 @@ 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, "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},
{"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},
{"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,

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,960 +0,0 @@
#!/usr/bin/env python3
"""Generate COMPLETE HTML API reference documentation for McRogueFace with NO missing methods."""
import os
import sys
import datetime
import html
from pathlib import Path
import mcrfpy
def escape_html(text: str) -> str:
"""Escape HTML special characters."""
return html.escape(text) if text else ""
def get_complete_method_documentation():
"""Return complete documentation for ALL methods across all classes."""
return {
# Base Drawable methods (inherited by all UI elements)
'Drawable': {
'get_bounds': {
'signature': 'get_bounds()',
'description': 'Get the bounding rectangle of this drawable element.',
'returns': 'tuple: (x, y, width, height) representing the element\'s bounds',
'note': 'The bounds are in screen coordinates and account for current position and size.'
},
'move': {
'signature': 'move(dx, dy)',
'description': 'Move the element by a relative offset.',
'args': [
('dx', 'float', 'Horizontal offset in pixels'),
('dy', 'float', 'Vertical offset in pixels')
],
'note': 'This modifies the x and y position properties by the given amounts.'
},
'resize': {
'signature': 'resize(width, height)',
'description': 'Resize the element to new dimensions.',
'args': [
('width', 'float', 'New width in pixels'),
('height', 'float', 'New height in pixels')
],
'note': 'For Caption and Sprite, this may not change actual size if determined by content.'
}
},
# Entity-specific methods
'Entity': {
'at': {
'signature': 'at(x, y)',
'description': 'Check if this entity is at the specified grid coordinates.',
'args': [
('x', 'int', 'Grid x coordinate to check'),
('y', 'int', 'Grid y coordinate to check')
],
'returns': 'bool: True if entity is at position (x, y), False otherwise'
},
'die': {
'signature': 'die()',
'description': 'Remove this entity from its parent grid.',
'note': 'The entity object remains valid but is no longer rendered or updated.'
},
'index': {
'signature': 'index()',
'description': 'Get the index of this entity in its parent grid\'s entity list.',
'returns': 'int: Index position, or -1 if not in a grid'
}
},
# Grid-specific methods
'Grid': {
'at': {
'signature': 'at(x, y)',
'description': 'Get the GridPoint at the specified grid coordinates.',
'args': [
('x', 'int', 'Grid x coordinate'),
('y', 'int', 'Grid y coordinate')
],
'returns': 'GridPoint or None: The grid point at (x, y), or None if out of bounds'
}
},
# Collection methods
'EntityCollection': {
'append': {
'signature': 'append(entity)',
'description': 'Add an entity to the end of the collection.',
'args': [('entity', 'Entity', 'The entity to add')]
},
'remove': {
'signature': 'remove(entity)',
'description': 'Remove the first occurrence of an entity from the collection.',
'args': [('entity', 'Entity', 'The entity to remove')],
'raises': 'ValueError: If entity is not in collection'
},
'extend': {
'signature': 'extend(iterable)',
'description': 'Add all entities from an iterable to the collection.',
'args': [('iterable', 'Iterable[Entity]', 'Entities to add')]
},
'count': {
'signature': 'count(entity)',
'description': 'Count the number of occurrences of an entity in the collection.',
'args': [('entity', 'Entity', 'The entity to count')],
'returns': 'int: Number of times entity appears in collection'
},
'index': {
'signature': 'index(entity)',
'description': 'Find the index of the first occurrence of an entity.',
'args': [('entity', 'Entity', 'The entity to find')],
'returns': 'int: Index of entity in collection',
'raises': 'ValueError: If entity is not in collection'
}
},
'UICollection': {
'append': {
'signature': 'append(drawable)',
'description': 'Add a drawable element to the end of the collection.',
'args': [('drawable', 'UIDrawable', 'The drawable element to add')]
},
'remove': {
'signature': 'remove(drawable)',
'description': 'Remove the first occurrence of a drawable from the collection.',
'args': [('drawable', 'UIDrawable', 'The drawable to remove')],
'raises': 'ValueError: If drawable is not in collection'
},
'extend': {
'signature': 'extend(iterable)',
'description': 'Add all drawables from an iterable to the collection.',
'args': [('iterable', 'Iterable[UIDrawable]', 'Drawables to add')]
},
'count': {
'signature': 'count(drawable)',
'description': 'Count the number of occurrences of a drawable in the collection.',
'args': [('drawable', 'UIDrawable', 'The drawable to count')],
'returns': 'int: Number of times drawable appears in collection'
},
'index': {
'signature': 'index(drawable)',
'description': 'Find the index of the first occurrence of a drawable.',
'args': [('drawable', 'UIDrawable', 'The drawable to find')],
'returns': 'int: Index of drawable in collection',
'raises': 'ValueError: If drawable is not in collection'
}
},
# Animation methods
'Animation': {
'get_current_value': {
'signature': 'get_current_value()',
'description': 'Get the current interpolated value of the animation.',
'returns': 'float: Current animation value between start and end'
},
'start': {
'signature': 'start(target)',
'description': 'Start the animation on a target UI element.',
'args': [('target', 'UIDrawable', 'The UI element to animate')],
'note': 'The target must have the property specified in the animation constructor.'
},
'update': {
'signature': 'update(delta_time)',
'description': 'Update the animation by the given time delta.',
'args': [('delta_time', 'float', 'Time elapsed since last update in seconds')],
'returns': 'bool: True if animation is still running, False if finished'
}
},
# Color methods
'Color': {
'from_hex': {
'signature': 'from_hex(hex_string)',
'description': 'Create a Color from a hexadecimal color string.',
'args': [('hex_string', 'str', 'Hex color string (e.g., "#FF0000" or "FF0000")')],
'returns': 'Color: New Color object from hex string',
'example': 'red = Color.from_hex("#FF0000")'
},
'to_hex': {
'signature': 'to_hex()',
'description': 'Convert this Color to a hexadecimal string.',
'returns': 'str: Hex color string in format "#RRGGBB"',
'example': 'hex_str = color.to_hex() # Returns "#FF0000"'
},
'lerp': {
'signature': 'lerp(other, t)',
'description': 'Linearly interpolate between this color and another.',
'args': [
('other', 'Color', 'The color to interpolate towards'),
('t', 'float', 'Interpolation factor from 0.0 to 1.0')
],
'returns': 'Color: New interpolated Color object',
'example': 'mixed = red.lerp(blue, 0.5) # 50% between red and blue'
}
},
# Vector methods
'Vector': {
'magnitude': {
'signature': 'magnitude()',
'description': 'Calculate the length/magnitude of this vector.',
'returns': 'float: The magnitude of the vector',
'example': 'length = vector.magnitude()'
},
'magnitude_squared': {
'signature': 'magnitude_squared()',
'description': 'Calculate the squared magnitude of this vector.',
'returns': 'float: The squared magnitude (faster than magnitude())',
'note': 'Use this for comparisons to avoid expensive square root calculation.'
},
'normalize': {
'signature': 'normalize()',
'description': 'Return a unit vector in the same direction.',
'returns': 'Vector: New normalized vector with magnitude 1.0',
'raises': 'ValueError: If vector has zero magnitude'
},
'dot': {
'signature': 'dot(other)',
'description': 'Calculate the dot product with another vector.',
'args': [('other', 'Vector', 'The other vector')],
'returns': 'float: Dot product of the two vectors'
},
'distance_to': {
'signature': 'distance_to(other)',
'description': 'Calculate the distance to another vector.',
'args': [('other', 'Vector', 'The other vector')],
'returns': 'float: Distance between the two vectors'
},
'angle': {
'signature': 'angle()',
'description': 'Get the angle of this vector in radians.',
'returns': 'float: Angle in radians from positive x-axis'
},
'copy': {
'signature': 'copy()',
'description': 'Create a copy of this vector.',
'returns': 'Vector: New Vector object with same x and y values'
}
},
# Scene methods
'Scene': {
'activate': {
'signature': 'activate()',
'description': 'Make this scene the active scene.',
'note': 'Equivalent to calling setScene() with this scene\'s name.'
},
'get_ui': {
'signature': 'get_ui()',
'description': 'Get the UI element collection for this scene.',
'returns': 'UICollection: Collection of all UI elements in this scene'
},
'keypress': {
'signature': 'keypress(handler)',
'description': 'Register a keyboard handler function for this scene.',
'args': [('handler', 'callable', 'Function that takes (key_name: str, is_pressed: bool)')],
'note': 'Alternative to overriding the on_keypress method.'
},
'register_keyboard': {
'signature': 'register_keyboard(callable)',
'description': 'Register a keyboard event handler function for the scene.',
'args': [('callable', 'callable', 'Function that takes (key: str, action: str) parameters')],
'note': 'Alternative to overriding the on_keypress method when subclassing Scene objects.',
'example': '''def handle_keyboard(key, action):
print(f"Key '{key}' was {action}")
if key == "q" and action == "press":
# Handle quit
pass
scene.register_keyboard(handle_keyboard)'''
}
},
# Timer methods
'Timer': {
'pause': {
'signature': 'pause()',
'description': 'Pause the timer, stopping its callback execution.',
'note': 'Use resume() to continue the timer from where it was paused.'
},
'resume': {
'signature': 'resume()',
'description': 'Resume a paused timer.',
'note': 'Has no effect if timer is not paused.'
},
'cancel': {
'signature': 'cancel()',
'description': 'Cancel the timer and remove it from the system.',
'note': 'After cancelling, the timer object cannot be reused.'
},
'restart': {
'signature': 'restart()',
'description': 'Restart the timer from the beginning.',
'note': 'Resets the timer\'s internal clock to zero.'
}
},
# Window methods
'Window': {
'get': {
'signature': 'get()',
'description': 'Get the Window singleton instance.',
'returns': 'Window: The singleton window object',
'note': 'This is a static method that returns the same instance every time.'
},
'center': {
'signature': 'center()',
'description': 'Center the window on the screen.',
'note': 'Only works if the window is not fullscreen.'
},
'screenshot': {
'signature': 'screenshot(filename)',
'description': 'Take a screenshot and save it to a file.',
'args': [('filename', 'str', 'Path where to save the screenshot')],
'note': 'Supports PNG, JPG, and BMP formats based on file extension.'
}
}
}
def get_complete_function_documentation():
"""Return complete documentation for ALL module functions."""
return {
# Scene Management
'createScene': {
'signature': 'createScene(name: str) -> None',
'description': 'Create a new empty scene with the given name.',
'args': [('name', 'str', 'Unique name for the new scene')],
'raises': 'ValueError: If a scene with this name already exists',
'note': 'The scene is created but not made active. Use setScene() to switch to it.',
'example': 'mcrfpy.createScene("game_over")'
},
'setScene': {
'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None',
'description': 'Switch to a different scene with optional transition effect.',
'args': [
('scene', 'str', 'Name of the scene to switch to'),
('transition', 'str', 'Transition type: "fade", "slide_left", "slide_right", "slide_up", "slide_down"'),
('duration', 'float', 'Transition duration in seconds (default: 0.0 for instant)')
],
'raises': 'KeyError: If the scene doesn\'t exist',
'example': 'mcrfpy.setScene("game", "fade", 0.5)'
},
'currentScene': {
'signature': 'currentScene() -> str',
'description': 'Get the name of the currently active scene.',
'returns': 'str: Name of the current scene',
'example': 'scene_name = mcrfpy.currentScene()'
},
'sceneUI': {
'signature': 'sceneUI(scene: str = None) -> UICollection',
'description': 'Get all UI elements for a scene.',
'args': [('scene', 'str', 'Scene name. If None, uses current scene')],
'returns': 'UICollection: All UI elements in the scene',
'raises': 'KeyError: If the specified scene doesn\'t exist',
'example': 'ui_elements = mcrfpy.sceneUI("game")'
},
'keypressScene': {
'signature': 'keypressScene(handler: callable) -> None',
'description': 'Set the keyboard event handler for the current scene.',
'args': [('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)')],
'example': '''def on_key(key, pressed):
if key == "SPACE" and pressed:
player.jump()
mcrfpy.keypressScene(on_key)'''
},
# Audio Functions
'createSoundBuffer': {
'signature': 'createSoundBuffer(filename: str) -> int',
'description': 'Load a sound effect from a file and return its buffer ID.',
'args': [('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)')],
'returns': 'int: Buffer ID for use with playSound()',
'raises': 'RuntimeError: If the file cannot be loaded',
'example': 'jump_sound = mcrfpy.createSoundBuffer("assets/jump.wav")'
},
'loadMusic': {
'signature': 'loadMusic(filename: str, loop: bool = True) -> None',
'description': 'Load and immediately play background music from a file.',
'args': [
('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'),
('loop', 'bool', 'Whether to loop the music (default: True)')
],
'note': 'Only one music track can play at a time. Loading new music stops the current track.',
'example': 'mcrfpy.loadMusic("assets/background.ogg", True)'
},
'playSound': {
'signature': 'playSound(buffer_id: int) -> None',
'description': 'Play a sound effect using a previously loaded buffer.',
'args': [('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')],
'raises': 'RuntimeError: If the buffer ID is invalid',
'example': 'mcrfpy.playSound(jump_sound)'
},
'getMusicVolume': {
'signature': 'getMusicVolume() -> int',
'description': 'Get the current music volume level.',
'returns': 'int: Current volume (0-100)',
'example': 'current_volume = mcrfpy.getMusicVolume()'
},
'getSoundVolume': {
'signature': 'getSoundVolume() -> int',
'description': 'Get the current sound effects volume level.',
'returns': 'int: Current volume (0-100)',
'example': 'current_volume = mcrfpy.getSoundVolume()'
},
'setMusicVolume': {
'signature': 'setMusicVolume(volume: int) -> None',
'description': 'Set the global music volume.',
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
'example': 'mcrfpy.setMusicVolume(50) # Set to 50% volume'
},
'setSoundVolume': {
'signature': 'setSoundVolume(volume: int) -> None',
'description': 'Set the global sound effects volume.',
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
'example': 'mcrfpy.setSoundVolume(75) # Set to 75% volume'
},
# UI Utilities
'find': {
'signature': 'find(name: str, scene: str = None) -> UIDrawable | None',
'description': 'Find the first UI element with the specified name.',
'args': [
('name', 'str', 'Exact name to search for'),
('scene', 'str', 'Scene to search in (default: current scene)')
],
'returns': 'UIDrawable or None: The found element, or None if not found',
'note': 'Searches scene UI elements and entities within grids.',
'example': 'button = mcrfpy.find("start_button")'
},
'findAll': {
'signature': 'findAll(pattern: str, scene: str = None) -> list',
'description': 'Find all UI elements matching a name pattern.',
'args': [
('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'),
('scene', 'str', 'Scene to search in (default: current scene)')
],
'returns': 'list: All matching UI elements and entities',
'example': 'enemies = mcrfpy.findAll("enemy_*")'
},
# System Functions
'exit': {
'signature': 'exit() -> None',
'description': 'Cleanly shut down the game engine and exit the application.',
'note': 'This immediately closes the window and terminates the program.',
'example': 'mcrfpy.exit()'
},
'getMetrics': {
'signature': 'getMetrics() -> dict',
'description': 'Get current performance metrics.',
'returns': '''dict: Performance data with keys:
- frame_time: Last frame duration in seconds
- avg_frame_time: Average frame time
- fps: Frames per second
- draw_calls: Number of draw calls
- ui_elements: Total UI element count
- visible_elements: Visible element count
- current_frame: Frame counter
- runtime: Total runtime in seconds''',
'example': 'metrics = mcrfpy.getMetrics()'
},
'setTimer': {
'signature': 'setTimer(name: str, handler: callable, interval: int) -> None',
'description': 'Create or update a recurring timer.',
'args': [
('name', 'str', 'Unique identifier for the timer'),
('handler', 'callable', 'Function called with (runtime: float) parameter'),
('interval', 'int', 'Time between calls in milliseconds')
],
'note': 'If a timer with this name exists, it will be replaced.',
'example': '''def update_score(runtime):
score += 1
mcrfpy.setTimer("score_update", update_score, 1000)'''
},
'delTimer': {
'signature': 'delTimer(name: str) -> None',
'description': 'Stop and remove a timer.',
'args': [('name', 'str', 'Timer identifier to remove')],
'note': 'No error is raised if the timer doesn\'t exist.',
'example': 'mcrfpy.delTimer("score_update")'
},
'setScale': {
'signature': 'setScale(multiplier: float) -> None',
'description': 'Scale the game window size.',
'args': [('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)')],
'note': 'The internal resolution remains 1024x768, but the window is scaled.',
'example': 'mcrfpy.setScale(2.0) # Double the window size'
}
}
def get_complete_property_documentation():
"""Return complete documentation for ALL properties."""
return {
'Animation': {
'property': 'str: Name of the property being animated (e.g., "x", "y", "scale")',
'duration': 'float: Total duration of the animation in seconds',
'elapsed_time': 'float: Time elapsed since animation started (read-only)',
'current_value': 'float: Current interpolated value of the animation (read-only)',
'is_running': 'bool: True if animation is currently running (read-only)',
'is_finished': 'bool: True if animation has completed (read-only)'
},
'GridPoint': {
'x': 'int: Grid x coordinate of this point',
'y': 'int: Grid y coordinate of this point',
'texture_index': 'int: Index of the texture/sprite to display at this point',
'solid': 'bool: Whether this point blocks movement',
'transparent': 'bool: Whether this point allows light/vision through',
'color': 'Color: Color tint applied to the texture at this point'
},
'GridPointState': {
'visible': 'bool: Whether this point is currently visible to the player',
'discovered': 'bool: Whether this point has been discovered/explored',
'custom_flags': 'int: Bitfield for custom game-specific flags'
}
}
def generate_complete_html_documentation():
"""Generate complete HTML documentation with NO missing methods."""
# Get all documentation data
method_docs = get_complete_method_documentation()
function_docs = get_complete_function_documentation()
property_docs = get_complete_property_documentation()
html_parts = []
# HTML header with enhanced styling
html_parts.append('''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>McRogueFace API Reference - Complete Documentation</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f8f9fa;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
border-bottom: 3px solid #3498db;
padding-bottom: 15px;
margin-bottom: 30px;
}
h2 {
color: #34495e;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
margin-top: 40px;
}
h3 {
color: #2c3e50;
margin-top: 30px;
}
h4 {
color: #34495e;
margin-top: 20px;
font-size: 1.1em;
}
h5 {
color: #555;
margin-top: 15px;
font-size: 1em;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
font-size: 0.9em;
}
pre {
background: #f8f8f8;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 16px;
overflow-x: auto;
margin: 15px 0;
}
pre code {
background: none;
padding: 0;
font-size: 0.875em;
line-height: 1.45;
}
.class-name {
color: #8e44ad;
font-weight: bold;
}
.property {
color: #27ae60;
font-weight: 600;
}
.method {
color: #2980b9;
font-weight: 600;
}
.function-signature {
color: #d73a49;
font-weight: 600;
}
.method-section {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #3498db;
}
.arg-list {
margin: 10px 0;
}
.arg-item {
margin: 8px 0;
padding: 8px;
background: #fff;
border-radius: 4px;
border: 1px solid #e1e4e8;
}
.arg-name {
color: #d73a49;
font-weight: 600;
}
.arg-type {
color: #6f42c1;
font-style: italic;
}
.returns {
background: #e8f5e8;
padding: 10px;
border-radius: 4px;
border-left: 4px solid #28a745;
margin: 10px 0;
}
.note {
background: #fff3cd;
padding: 10px;
border-radius: 4px;
border-left: 4px solid #ffc107;
margin: 10px 0;
}
.example {
background: #e7f3ff;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #0366d6;
margin: 15px 0;
}
.toc {
background: #f8f9fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 20px;
margin: 20px 0;
}
.toc ul {
list-style: none;
padding-left: 0;
}
.toc li {
margin: 8px 0;
}
.toc a {
color: #3498db;
text-decoration: none;
font-weight: 500;
}
.toc a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
''')
# Title and overview
html_parts.append('<h1>McRogueFace API Reference - Complete Documentation</h1>')
html_parts.append(f'<p><em>Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</em></p>')
# Table of contents
html_parts.append('<div class="toc">')
html_parts.append('<h2>Table of Contents</h2>')
html_parts.append('<ul>')
html_parts.append('<li><a href="#functions">Functions</a></li>')
html_parts.append('<li><a href="#classes">Classes</a></li>')
html_parts.append('<li><a href="#automation">Automation Module</a></li>')
html_parts.append('</ul>')
html_parts.append('</div>')
# Functions section
html_parts.append('<h2 id="functions">Functions</h2>')
# Group functions by category
categories = {
'Scene Management': ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'],
'Audio': ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'],
'UI Utilities': ['find', 'findAll'],
'System': ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale']
}
for category, functions in categories.items():
html_parts.append(f'<h3>{category}</h3>')
for func_name in functions:
if func_name in function_docs:
html_parts.append(format_function_html(func_name, function_docs[func_name]))
# Classes section
html_parts.append('<h2 id="classes">Classes</h2>')
# Get all classes from mcrfpy
classes = []
for name in sorted(dir(mcrfpy)):
if not name.startswith('_'):
obj = getattr(mcrfpy, name)
if isinstance(obj, type):
classes.append((name, obj))
# Generate class documentation
for class_name, cls in classes:
html_parts.append(format_class_html_complete(class_name, cls, method_docs, property_docs))
# Automation section
if hasattr(mcrfpy, 'automation'):
html_parts.append('<h2 id="automation">Automation Module</h2>')
html_parts.append('<p>The <code>mcrfpy.automation</code> module provides testing and automation capabilities.</p>')
automation = mcrfpy.automation
for name in sorted(dir(automation)):
if not name.startswith('_'):
obj = getattr(automation, name)
if callable(obj):
html_parts.append(f'<div class="method-section">')
html_parts.append(f'<h4><code class="function-signature">automation.{name}</code></h4>')
if obj.__doc__:
doc_parts = obj.__doc__.split(' - ')
if len(doc_parts) > 1:
html_parts.append(f'<p>{escape_html(doc_parts[1])}</p>')
else:
html_parts.append(f'<p>{escape_html(obj.__doc__)}</p>')
html_parts.append('</div>')
html_parts.append('</div>')
html_parts.append('</body>')
html_parts.append('</html>')
return '\n'.join(html_parts)
def format_function_html(func_name, func_doc):
"""Format a function with complete documentation."""
html_parts = []
html_parts.append('<div class="method-section">')
html_parts.append(f'<h4><code class="function-signature">{func_doc["signature"]}</code></h4>')
html_parts.append(f'<p>{escape_html(func_doc["description"])}</p>')
# Arguments
if 'args' in func_doc:
html_parts.append('<div class="arg-list">')
html_parts.append('<h5>Arguments:</h5>')
for arg in func_doc['args']:
html_parts.append('<div class="arg-item">')
html_parts.append(f'<span class="arg-name">{arg[0]}</span> ')
html_parts.append(f'<span class="arg-type">({arg[1]})</span>: ')
html_parts.append(f'{escape_html(arg[2])}')
html_parts.append('</div>')
html_parts.append('</div>')
# Returns
if 'returns' in func_doc:
html_parts.append('<div class="returns">')
html_parts.append(f'<strong>Returns:</strong> {escape_html(func_doc["returns"])}')
html_parts.append('</div>')
# Raises
if 'raises' in func_doc:
html_parts.append('<div class="note">')
html_parts.append(f'<strong>Raises:</strong> {escape_html(func_doc["raises"])}')
html_parts.append('</div>')
# Note
if 'note' in func_doc:
html_parts.append('<div class="note">')
html_parts.append(f'<strong>Note:</strong> {escape_html(func_doc["note"])}')
html_parts.append('</div>')
# Example
if 'example' in func_doc:
html_parts.append('<div class="example">')
html_parts.append('<h5>Example:</h5>')
html_parts.append('<pre><code>')
html_parts.append(escape_html(func_doc['example']))
html_parts.append('</code></pre>')
html_parts.append('</div>')
html_parts.append('</div>')
return '\n'.join(html_parts)
def format_class_html_complete(class_name, cls, method_docs, property_docs):
"""Format a class with complete documentation."""
html_parts = []
html_parts.append('<div class="method-section">')
html_parts.append(f'<h3><span class="class-name">{class_name}</span></h3>')
# Class description
if cls.__doc__:
html_parts.append(f'<p>{escape_html(cls.__doc__)}</p>')
# Properties
if class_name in property_docs:
html_parts.append('<h4>Properties:</h4>')
for prop_name, prop_desc in property_docs[class_name].items():
html_parts.append(f'<div class="arg-item">')
html_parts.append(f'<span class="property">{prop_name}</span>: {escape_html(prop_desc)}')
html_parts.append('</div>')
# Methods
methods_to_document = []
# Add inherited methods for UI classes
if any(base.__name__ == 'Drawable' for base in cls.__bases__ if hasattr(base, '__name__')):
methods_to_document.extend(['get_bounds', 'move', 'resize'])
# Add class-specific methods
if class_name in method_docs:
methods_to_document.extend(method_docs[class_name].keys())
# Add methods from introspection
for attr_name in dir(cls):
if not attr_name.startswith('_') and callable(getattr(cls, attr_name)):
if attr_name not in methods_to_document:
methods_to_document.append(attr_name)
if methods_to_document:
html_parts.append('<h4>Methods:</h4>')
for method_name in set(methods_to_document):
# Get method documentation
method_doc = None
if class_name in method_docs and method_name in method_docs[class_name]:
method_doc = method_docs[class_name][method_name]
elif method_name in method_docs.get('Drawable', {}):
method_doc = method_docs['Drawable'][method_name]
if method_doc:
html_parts.append(format_method_html(method_name, method_doc))
else:
# Basic method with no documentation
html_parts.append(f'<div class="arg-item">')
html_parts.append(f'<span class="method">{method_name}(...)</span>')
html_parts.append('</div>')
html_parts.append('</div>')
return '\n'.join(html_parts)
def format_method_html(method_name, method_doc):
"""Format a method with complete documentation."""
html_parts = []
html_parts.append('<div style="margin-left: 20px; margin-bottom: 15px;">')
html_parts.append(f'<h5><code class="method">{method_doc["signature"]}</code></h5>')
html_parts.append(f'<p>{escape_html(method_doc["description"])}</p>')
# Arguments
if 'args' in method_doc:
for arg in method_doc['args']:
html_parts.append(f'<div style="margin-left: 20px;">')
html_parts.append(f'<span class="arg-name">{arg[0]}</span> ')
html_parts.append(f'<span class="arg-type">({arg[1]})</span>: ')
html_parts.append(f'{escape_html(arg[2])}')
html_parts.append('</div>')
# Returns
if 'returns' in method_doc:
html_parts.append(f'<div style="margin-left: 20px; color: #28a745;">')
html_parts.append(f'<strong>Returns:</strong> {escape_html(method_doc["returns"])}')
html_parts.append('</div>')
# Note
if 'note' in method_doc:
html_parts.append(f'<div style="margin-left: 20px; color: #856404;">')
html_parts.append(f'<strong>Note:</strong> {escape_html(method_doc["note"])}')
html_parts.append('</div>')
# Example
if 'example' in method_doc:
html_parts.append(f'<div style="margin-left: 20px;">')
html_parts.append('<strong>Example:</strong>')
html_parts.append('<pre><code>')
html_parts.append(escape_html(method_doc['example']))
html_parts.append('</code></pre>')
html_parts.append('</div>')
html_parts.append('</div>')
return '\n'.join(html_parts)
def main():
"""Generate complete HTML documentation with zero missing methods."""
print("Generating COMPLETE HTML API documentation...")
# Generate HTML
html_content = generate_complete_html_documentation()
# Write to file
output_path = Path("docs/api_reference_complete.html")
output_path.parent.mkdir(exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✓ Generated {output_path}")
print(f" File size: {len(html_content):,} bytes")
# Count "..." instances
ellipsis_count = html_content.count('...')
print(f" Ellipsis instances: {ellipsis_count}")
if ellipsis_count == 0:
print("✅ SUCCESS: No missing documentation found!")
else:
print(f"❌ WARNING: {ellipsis_count} methods still need documentation")
if __name__ == '__main__':
main()

View File

@ -1,821 +0,0 @@
#!/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()

View File

@ -12,6 +12,67 @@ 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'<p class="see-also">See also: <a href="{href}">{text_escaped}</a></p>'
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'<p class="see-also">See also: <a href="{href}">{text_escaped}</a></p>'
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
@ -304,8 +365,10 @@ def generate_html_docs():
html_content += f"""
<div class="method-section">
<h3><code class="function-signature">{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}</code></h3>
<p>{html.escape(parsed['description'])}</p>
"""
if parsed['description']:
description = transform_doc_links(parsed['description'], format='html')
html_content += f" <p>{description}</p>\n"
if parsed['args']:
html_content += " <h4>Arguments:</h4>\n <ul>\n"
@ -363,7 +426,8 @@ def generate_html_docs():
"""
if parsed['description']:
html_content += f" <p>{html.escape(parsed['description'])}</p>\n"
description = transform_doc_links(parsed['description'], format='html')
html_content += f" <p>{description}</p>\n"
if parsed['args']:
html_content += " <div style='margin-left: 20px;'>\n"
@ -431,7 +495,8 @@ def generate_markdown_docs():
md_content += f"### `{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
if parsed['description']:
md_content += f"{parsed['description']}\n\n"
description = transform_doc_links(parsed['description'], format='markdown')
md_content += f"{description}\n\n"
if parsed['args']:
md_content += "**Arguments:**\n"
@ -481,7 +546,8 @@ def generate_markdown_docs():
md_content += f"#### `{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
if parsed['description']:
md_content += f"{parsed['description']}\n\n"
description = transform_doc_links(parsed['description'], format='markdown')
md_content += f"{description}\n\n"
if parsed['args']:
md_content += "**Arguments:**\n"

View File

@ -0,0 +1,55 @@
#!/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'<p class="see-also">See also: <a href="{href}">{text}</a></p>'
elif format == 'web':
# Link to hosted docs
web_path = ref.replace('docs/', '').replace('.md', '')
return f'<p class="see-also">See also: <a href="{base_url}/{web_path}">{text}</a></p>'
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 '<a href="guide.html">Tutorial Guide</a>' 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")

39
tools/test_vector_docs.py Normal file
View File

@ -0,0 +1,39 @@
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)