diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 7f71600..0000000 --- a/.mcp.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "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"] - } - } -} diff --git a/CLAUDE.md b/CLAUDE.md index 641417a..0dea84c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -392,102 +392,67 @@ mcrfpy.setTimer("test", run_test, 100) # 0.1 seconds ## Documentation Guidelines -### Documentation Macro System +### Inline C++ Documentation Format -**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 +When adding new methods or modifying existing ones in C++ source files, use this documentation format in PyMethodDef arrays: ```cpp -#include "McRFPy_Doc.h" +{"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"}, ``` -#### 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`: - +For properties in PyGetSetDef arrays: ```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}, + "Brief description of the property. " + "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 with MCRF_* macros: +After modifying C++ inline documentation: 1. **Rebuild the project**: `make -j$(nproc)` -2. **Generate documentation** (automatic from compiled module): +2. **Generate stub files** (for IDE support): ```bash - ./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py + ./build/mcrogueface --exec generate_stubs.py + ``` + +3. **Generate dynamic documentation** (recommended): + ```bash + ./build/mcrogueface --exec generate_dynamic_docs.py ``` This creates: - `docs/api_reference_dynamic.html` - `docs/API_REFERENCE_DYNAMIC.md` -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 +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 ### 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 --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 +- **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 ### Documentation Pipeline Architecture -1. **C++ Source** → MCRF_* macros in PyMethodDef/PyGetSetDef arrays -2. **Compilation** → Macros expand to complete docstrings embedded in module +1. **C++ Source** → PyMethodDef/PyGetSetDef arrays with docstrings +2. **Compilation** → Docstrings embedded in compiled module 3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract -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! +4. **Generation** → HTML/Markdown/Stub files created -The macro system ensures complete, consistent documentation across all Python bindings. \ No newline at end of file +The documentation is only as good as the C++ inline docstrings! \ No newline at end of file diff --git a/docs/api_reference_dynamic.html b/docs/api_reference_dynamic.html index faa33e5..82c247d 100644 --- a/docs/api_reference_dynamic.html +++ b/docs/api_reference_dynamic.html @@ -108,7 +108,7 @@

McRogueFace API Reference

-

Generated on 2025-10-30 16:58:07

+

Generated on 2025-07-15 21:28:24

This documentation was dynamically generated from the compiled module.

@@ -424,37 +424,15 @@ Attributes:

Methods:

-
get_boundsget_bounds() -> tuple
-

Get the bounding rectangle of this drawable element. - - - -Note:

-

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

+
get_boundsGet bounding box as (x, y, width, height)
-
movemove(dx: float, dy: float) -> None
-

Move the element by a relative offset. - - -Note:

-
-
dx: Horizontal offset in pixels
-
dy: Vertical offset in pixels
-
+
moveMove by relative offset (dx, dy)
-
resizeresize(width: float, height: float) -> None
-

Resize the element to new dimensions. - - -Note:

-
-
width: New width in pixels
-
height: New height in pixels
-
+
resizeResize to new dimensions (width, height)
@@ -464,38 +442,17 @@ Note:

Methods:

-
from_hexfrom_hex(hex_string: str) -> Color
-

Create a Color from a hexadecimal string. - - -Note:

-
-
hex_string: Hex color string (e.g., '#FF0000', 'FF0000', '#AABBCCDD' for RGBA)
-
-

Returns: 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('#FF0000')

+
from_hexCreate Color from hex string (e.g., '#FF0000' or 'FF0000')
-
lerplerp(other: Color, t: float) -> Color
-

Linearly interpolate between this color and another. - - -Note:

-
-
other: The target Color to interpolate towards
-
t: Interpolation factor (0.0 = this color, 1.0 = other color). Automatically clamped to [0.0, 1.0]
-
-

Returns: Color: New Color representing the interpolated value All components (r, g, b, a) are interpolated independently

+
lerp(...)
+

Linearly interpolate between this color and another

-
to_hexto_hex() -> str
-

Convert this Color to a hexadecimal string. - - - -Note:

-

Returns: str: Hex string in format '#RRGGBB' or '#RRGGBBAA' (if alpha < 255) Alpha component is only included if not fully opaque (< 255)

+
to_hex(...)
+

Convert Color to hex string

@@ -505,37 +462,15 @@ Note:

Methods:

-
get_boundsget_bounds() -> tuple
-

Get the bounding rectangle of this drawable element. - - - -Note:

-

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

+
get_boundsGet bounding box as (x, y, width, height)
-
movemove(dx: float, dy: float) -> None
-

Move the element by a relative offset. - - -Note:

-
-
dx: Horizontal offset in pixels
-
dy: Vertical offset in pixels
-
+
moveMove by relative offset (dx, dy)
-
resizeresize(width: float, height: float) -> None
-

Resize the element to new dimensions. - - -Note:

-
-
width: New width in pixels
-
height: New height in pixels
-
+
resizeResize to new dimensions (width, height)
@@ -579,13 +514,7 @@ Attributes:
-
get_boundsget_bounds() -> tuple
-

Get the bounding rectangle of this drawable element. - - - -Note:

-

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

+
get_boundsGet bounding box as (x, y, width, height)
@@ -594,15 +523,7 @@ Note:

-
movemove(dx: float, dy: float) -> None
-

Move the element by a relative offset. - - -Note:

-
-
dx: Horizontal offset in pixels
-
dy: Vertical offset in pixels
-
+
moveMove by relative offset (dx, dy)
@@ -616,15 +537,7 @@ Note:

-
resizeresize(width: float, height: float) -> None
-

Resize the element to new dimensions. - - -Note:

-
-
width: New width in pixels
-
height: New height in pixels
-
+
resizeResize to new dimensions (width, height)
@@ -712,37 +625,15 @@ Attributes:

Methods:

-
get_boundsget_bounds() -> tuple
-

Get the bounding rectangle of this drawable element. - - - -Note:

-

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

+
get_boundsGet bounding box as (x, y, width, height)
-
movemove(dx: float, dy: float) -> None
-

Move the element by a relative offset. - - -Note:

-
-
dx: Horizontal offset in pixels
-
dy: Vertical offset in pixels
-
+
moveMove by relative offset (dx, dy)
-
resizeresize(width: float, height: float) -> None
-

Resize the element to new dimensions. - - -Note:

-
-
width: New width in pixels
-
height: New height in pixels
-
+
resizeResize to new dimensions (width, height)
@@ -826,8 +717,8 @@ Attributes:
-
compute_fovcompute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]
-

Compute field of view from a position and return visible cells.

+
compute_fovcompute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None
+

Compute field of view from a position.

x: X coordinate of the viewer
y: Y coordinate of the viewer
@@ -835,7 +726,6 @@ Attributes:
light_walls: Whether walls are lit when visible
algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)
-

Returns: 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)

@@ -852,13 +742,7 @@ Attributes:
-
get_boundsget_bounds() -> tuple
-

Get the bounding rectangle of this drawable element. - - - -Note:

-

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

+
get_boundsGet bounding box as (x, y, width, height)
@@ -892,27 +776,11 @@ Note:

-
movemove(dx: float, dy: float) -> None
-

Move the element by a relative offset. - - -Note:

-
-
dx: Horizontal offset in pixels
-
dy: Vertical offset in pixels
-
+
moveMove by relative offset (dx, dy)
-
resizeresize(width: float, height: float) -> None
-

Resize the element to new dimensions. - - -Note:

-
-
width: New width in pixels
-
height: New height in pixels
-
+
resizeResize to new dimensions (width, height)
@@ -988,37 +856,15 @@ Attributes:

Methods:

-
get_boundsget_bounds() -> tuple
-

Get the bounding rectangle of this drawable element. - - - -Note:

-

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

+
get_boundsGet bounding box as (x, y, width, height)
-
movemove(dx: float, dy: float) -> None
-

Move the element by a relative offset. - - -Note:

-
-
dx: Horizontal offset in pixels
-
dy: Vertical offset in pixels
-
+
moveMove by relative offset (dx, dy)
-
resizeresize(width: float, height: float) -> None
-

Resize the element to new dimensions. - - -Note:

-
-
width: New width in pixels
-
height: New height in pixels
-
+
resizeResize to new dimensions (width, height)
@@ -1069,41 +915,25 @@ Example:
cancelcancel() -> None

Cancel the timer and remove it from the timer system. - - - -Note:

-

Returns: None The timer will no longer fire and cannot be restarted. The callback will not be called again.

+The timer will no longer fire and cannot be restarted.

pausepause() -> None

Pause the timer, preserving the time remaining until next trigger. - - - -Note:

-

Returns: None The timer can be resumed later with resume(). Time spent paused does not count toward the interval.

+The timer can be resumed later with resume().

restartrestart() -> None

Restart the timer from the beginning. - - - -Note:

-

Returns: None Resets the timer to fire after a full interval from now, regardless of remaining time.

+Resets the timer to fire after a full interval from now.

resumeresume() -> None

Resume a paused timer from where it left off. - - - -Note:

-

Returns: None Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.

+Has no effect if the timer is not paused.

@@ -1151,59 +981,38 @@ Note:

Methods:

-
angleangle() -> float
-

Get the angle of this vector in radians.

-

Returns: float: Angle in radians from positive x-axis

+
angle(...)
+

Return the angle in radians from the positive X axis

-
copycopy() -> Vector
-

Create a copy of this vector.

-

Returns: Vector: New Vector object with same x and y values

+
copy(...)
+

Return a copy of this vector

-
distance_todistance_to(other: Vector) -> float
-

Calculate the distance to another vector.

-
-
other: The other vector
-
-

Returns: float: Distance between the two vectors

+
distance_to(...)
+

Return the distance to another vector

-
dotdot(other: Vector) -> float
-

Calculate the dot product with another vector.

-
-
other: The other vector
-
-

Returns: float: Dot product of the two vectors

+
dot(...)
+

Return the dot product with another vector

-
magnitudemagnitude() -> float
-

Calculate the length/magnitude of this vector.

-

Returns: float: The magnitude of the vector

+
magnitude(...)
+

Return the length of the vector

-
magnitude_squaredmagnitude_squared() -> float
-

Calculate the squared magnitude of this vector. - - - -Note:

-

Returns: float: The squared magnitude (faster than magnitude()) Use this for comparisons to avoid expensive square root calculation.

+
magnitude_squared(...)
+

Return the squared length of the vector

-
normalizenormalize() -> Vector
-

Return a unit vector in the same direction. - - - -Note:

-

Returns: Vector: New normalized vector with magnitude 1.0 For zero vectors (magnitude 0.0), returns a zero vector rather than raising an exception

+
normalize(...)
+

Return a unit vector in the same direction

diff --git a/forgejo-mcp.linux.amd64 b/forgejo-mcp.linux.amd64 deleted file mode 100755 index fe84e35..0000000 Binary files a/forgejo-mcp.linux.amd64 and /dev/null differ diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py index c83719b..b738dcc 100644 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py @@ -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) @@ -565,4 +565,4 @@ class Engine: # Create and run the game engine = Engine() print("Part 6: Combat System!") -print("Attack enemies to defeat them, but watch your HP!") +print("Attack enemies to defeat them, but watch your HP!") \ No newline at end of file diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 3947163..7ba99ab 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -1,7 +1,6 @@ #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" @@ -28,201 +27,188 @@ PyObject* McRFPy_API::mcrf_module; static PyMethodDef mcrfpyMethods[] = { {"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS, - 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") - )}, + "createSoundBuffer(filename: str) -> int\n\n" + "Load a sound effect from a file and return its buffer ID.\n\n" + "Args:\n" + " filename: Path to the sound file (WAV, OGG, FLAC)\n\n" + "Returns:\n" + " int: Buffer ID for use with playSound()\n\n" + "Raises:\n" + " RuntimeError: If the file cannot be loaded"}, {"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS, - 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.") - )}, + "loadMusic(filename: str) -> None\n\n" + "Load and immediately play background music from a file.\n\n" + "Args:\n" + " filename: Path to the music file (WAV, OGG, FLAC)\n\n" + "Note:\n" + " Only one music track can play at a time. Loading new music stops the current track."}, {"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS, - 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") - )}, + "setMusicVolume(volume: int) -> None\n\n" + "Set the global music volume.\n\n" + "Args:\n" + " volume: Volume level from 0 (silent) to 100 (full volume)"}, {"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS, - 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") - )}, + "setSoundVolume(volume: int) -> None\n\n" + "Set the global sound effects volume.\n\n" + "Args:\n" + " volume: Volume level from 0 (silent) to 100 (full volume)"}, {"playSound", McRFPy_API::_playSound, METH_VARARGS, - 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") - )}, + "playSound(buffer_id: int) -> None\n\n" + "Play a sound effect using a previously loaded buffer.\n\n" + "Args:\n" + " buffer_id: Sound buffer ID returned by createSoundBuffer()\n\n" + "Raises:\n" + " RuntimeError: If the buffer ID is invalid"}, {"getMusicVolume", McRFPy_API::_getMusicVolume, METH_NOARGS, - MCRF_FUNCTION(getMusicVolume, - MCRF_SIG("()", "int"), - MCRF_DESC("Get the current music volume level."), - MCRF_RETURNS("int: Current volume (0-100)") - )}, + "getMusicVolume() -> int\n\n" + "Get the current music volume level.\n\n" + "Returns:\n" + " int: Current volume (0-100)"}, {"getSoundVolume", McRFPy_API::_getSoundVolume, METH_NOARGS, - MCRF_FUNCTION(getSoundVolume, - MCRF_SIG("()", "int"), - MCRF_DESC("Get the current sound effects volume level."), - MCRF_RETURNS("int: Current volume (0-100)") - )}, + "getSoundVolume() -> int\n\n" + "Get the current sound effects volume level.\n\n" + "Returns:\n" + " int: Current volume (0-100)"}, {"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS, - 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") - )}, + "sceneUI(scene: str = None) -> list\n\n" + "Get all UI elements for a scene.\n\n" + "Args:\n" + " scene: Scene name. If None, uses current scene\n\n" + "Returns:\n" + " list: All UI elements (Frame, Caption, Sprite, Grid) in the scene\n\n" + "Raises:\n" + " KeyError: If the specified scene doesn't exist"}, {"currentScene", McRFPy_API::_currentScene, METH_NOARGS, - MCRF_FUNCTION(currentScene, - MCRF_SIG("()", "str"), - MCRF_DESC("Get the name of the currently active scene."), - MCRF_RETURNS("str: Name of the current scene") - )}, + "currentScene() -> str\n\n" + "Get the name of the currently active scene.\n\n" + "Returns:\n" + " str: Name of the current scene"}, {"setScene", McRFPy_API::_setScene, METH_VARARGS, - 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") - )}, + "setScene(scene: str, transition: str = None, duration: float = 0.0) -> None\n\n" + "Switch to a different scene with optional transition effect.\n\n" + "Args:\n" + " scene: Name of the scene to switch to\n" + " transition: Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')\n" + " duration: Transition duration in seconds (default: 0.0 for instant)\n\n" + "Raises:\n" + " KeyError: If the scene doesn't exist\n" + " ValueError: If the transition type is invalid"}, {"createScene", McRFPy_API::_createScene, METH_VARARGS, - 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.") - )}, + "createScene(name: str) -> None\n\n" + "Create a new empty scene.\n\n" + "Args:\n" + " name: Unique name for the new scene\n\n" + "Raises:\n" + " ValueError: If a scene with this name already exists\n\n" + "Note:\n" + " The scene is created but not made active. Use setScene() to switch to it."}, {"keypressScene", McRFPy_API::_keypressScene, METH_VARARGS, - 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)") - )}, + "keypressScene(handler: callable) -> None\n\n" + "Set the keyboard event handler for the current scene.\n\n" + "Args:\n" + " handler: Callable that receives (key_name: str, is_pressed: bool)\n\n" + "Example:\n" + " def on_key(key, pressed):\n" + " if key == 'A' and pressed:\n" + " print('A key pressed')\n" + " mcrfpy.keypressScene(on_key)"}, {"setTimer", McRFPy_API::_setTimer, METH_VARARGS, - 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.") - )}, + "setTimer(name: str, handler: callable, interval: int) -> None\n\n" + "Create or update a recurring timer.\n\n" + "Args:\n" + " name: Unique identifier for the timer\n" + " handler: Function called with (runtime: float) parameter\n" + " interval: Time between calls in milliseconds\n\n" + "Note:\n" + " If a timer with this name exists, it will be replaced.\n" + " The handler receives the total runtime in seconds as its argument."}, {"delTimer", McRFPy_API::_delTimer, METH_VARARGS, - 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.") - )}, + "delTimer(name: str) -> None\n\n" + "Stop and remove a timer.\n\n" + "Args:\n" + " name: Timer identifier to remove\n\n" + "Note:\n" + " No error is raised if the timer doesn't exist."}, {"exit", McRFPy_API::_exit, METH_NOARGS, - 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.") - )}, + "exit() -> None\n\n" + "Cleanly shut down the game engine and exit the application.\n\n" + "Note:\n" + " This immediately closes the window and terminates the program."}, {"setScale", McRFPy_API::_setScale, METH_VARARGS, - 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.") - )}, - + "setScale(multiplier: float) -> None\n\n" + "Scale the game window size.\n\n" + "Args:\n" + " multiplier: Scale factor (e.g., 2.0 for double size)\n\n" + "Note:\n" + " The internal resolution remains 1024x768, but the window is scaled.\n" + " This is deprecated - use Window.resolution instead."}, + {"find", McRFPy_API::_find, METH_VARARGS, - 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.") - )}, + "find(name: str, scene: str = None) -> UIDrawable | None\n\n" + "Find the first UI element with the specified name.\n\n" + "Args:\n" + " name: Exact name to search for\n" + " scene: Scene to search in (default: current scene)\n\n" + "Returns:\n" + " Frame, Caption, Sprite, Grid, or Entity if found; None otherwise\n\n" + "Note:\n" + " Searches scene UI elements and entities within grids."}, {"findAll", McRFPy_API::_findAll, METH_VARARGS, - 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'") - )}, - + "findAll(pattern: str, scene: str = None) -> list\n\n" + "Find all UI elements matching a name pattern.\n\n" + "Args:\n" + " pattern: Name pattern with optional wildcards (* matches any characters)\n" + " scene: Scene to search in (default: current scene)\n\n" + "Returns:\n" + " list: All matching UI elements and entities\n\n" + "Example:\n" + " findAll('enemy*') # Find all elements starting with 'enemy'\n" + " findAll('*_button') # Find all elements ending with '_button'"}, + {"getMetrics", McRFPy_API::_getMetrics, METH_NOARGS, - 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)") - )}, - + "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"}, + {NULL, NULL, 0, NULL} }; 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. */ diff --git a/src/McRFPy_Doc.h b/src/McRFPy_Doc.h deleted file mode 100644 index 22ecdea..0000000 --- a/src/McRFPy_Doc.h +++ /dev/null @@ -1,31 +0,0 @@ -#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 diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index 265646d..c81a2ea 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -1,6 +1,5 @@ #include "PyAnimation.h" #include "McRFPy_API.h" -#include "McRFPy_Doc.h" #include "UIDrawable.h" #include "UIFrame.h" #include "UICaption.h" @@ -262,58 +261,33 @@ PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) } PyGetSetDef PyAnimation::getsetters[] = { - {"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}, + {"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}, {NULL} }; PyMethodDef PyAnimation::methods[] = { {"start", (PyCFunction)start, METH_VARARGS, - 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.") - )}, + "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."}, {"update", (PyCFunction)update, METH_VARARGS, - 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.") - )}, + "Update the animation by deltaTime (returns True if still running)"}, {"get_current_value", (PyCFunction)get_current_value, METH_NOARGS, - 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).") - )}, + "Get the current interpolated value"}, {"complete", (PyCFunction)complete, METH_NOARGS, - 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.") - )}, + "complete() -> None\n\n" + "Complete the animation immediately by jumping to the final value."}, {"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS, - 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.") - )}, + "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."}, {NULL} }; \ No newline at end of file diff --git a/src/PyColor.cpp b/src/PyColor.cpp index 4fd2154..e1a0b1a 100644 --- a/src/PyColor.cpp +++ b/src/PyColor.cpp @@ -2,50 +2,21 @@ #include "McRFPy_API.h" #include "PyObjectUtils.h" #include "PyRAII.h" -#include "McRFPy_Doc.h" #include #include PyGetSetDef PyColor::getsetters[] = { - {"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}, + {"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}, {NULL} }; PyMethodDef PyColor::methods[] = { - {"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") - )}, + {"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS, "Create Color from hex string (e.g., '#FF0000' or 'FF0000')"}, + {"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS, "Convert Color to hex string"}, + {"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS, "Linearly interpolate between this color and another"}, {NULL} }; diff --git a/src/PyDrawable.cpp b/src/PyDrawable.cpp index 2fe0e93..7773a26 100644 --- a/src/PyDrawable.cpp +++ b/src/PyDrawable.cpp @@ -1,6 +1,5 @@ #include "PyDrawable.h" #include "McRFPy_API.h" -#include "McRFPy_Doc.h" // Click property getter static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure) @@ -99,26 +98,14 @@ 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, - MCRF_PROPERTY(click, - "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." - ), NULL}, + {"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click, + "Callable executed when object is clicked", NULL}, {"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index, - MCRF_PROPERTY(z_index, - "Z-order for rendering (lower values rendered first). " - "Automatically triggers scene resort when changed." - ), NULL}, + "Z-order for rendering (lower values rendered first)", NULL}, {"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible, - MCRF_PROPERTY(visible, - "Whether the object is visible (bool). " - "Invisible objects are not rendered or clickable." - ), NULL}, + "Whether the object is visible", NULL}, {"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity, - MCRF_PROPERTY(opacity, - "Opacity level (0.0 = transparent, 1.0 = opaque). " - "Automatically clamped to valid range [0.0, 1.0]." - ), NULL}, + "Opacity level (0.0 = transparent, 1.0 = opaque)", NULL}, {NULL} // Sentinel }; @@ -156,30 +143,11 @@ static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args) // Method definitions static PyMethodDef PyDrawable_methods[] = { {"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS, - 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.") - )}, + "Get bounding box as (x, y, width, height)"}, {"move", (PyCFunction)PyDrawable_move, METH_VARARGS, - 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.") - )}, + "Move by relative offset (dx, dy)"}, {"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS, - 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.") - )}, + "Resize to new dimensions (width, height)"}, {NULL} // Sentinel }; diff --git a/src/PyFont.cpp b/src/PyFont.cpp index 22ba217..157656e 100644 --- a/src/PyFont.cpp +++ b/src/PyFont.cpp @@ -1,6 +1,5 @@ #include "PyFont.h" #include "McRFPy_API.h" -#include "McRFPy_Doc.h" PyFont::PyFont(std::string filename) @@ -74,9 +73,7 @@ PyObject* PyFont::get_source(PyFontObject* self, void* closure) } PyGetSetDef PyFont::getsetters[] = { - {"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}, + {"family", (getter)PyFont::get_family, NULL, "Font family name", NULL}, + {"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL}, {NULL} // Sentinel }; diff --git a/src/PySceneObject.cpp b/src/PySceneObject.cpp index 43489a8..491024e 100644 --- a/src/PySceneObject.cpp +++ b/src/PySceneObject.cpp @@ -2,7 +2,6 @@ #include "PyScene.h" #include "GameEngine.h" #include "McRFPy_API.h" -#include "McRFPy_Doc.h" #include // Static map to store Python scene objects by name @@ -214,38 +213,19 @@ void PySceneClass::call_on_resize(PySceneObject* self, int width, int height) // Properties PyGetSetDef PySceneClass::getsetters[] = { - {"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}, + {"name", (getter)get_name, NULL, "Scene name", NULL}, + {"active", (getter)get_active, NULL, "Whether this scene is currently active", NULL}, {NULL} }; // Methods PyMethodDef PySceneClass::methods[] = { {"activate", (PyCFunction)activate, METH_NOARGS, - 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.") - )}, + "Make this the active scene"}, {"get_ui", (PyCFunction)get_ui, METH_NOARGS, - 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.") - )}, + "Get the UI element collection for this scene"}, {"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS, - 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.") - )}, + "Register a keyboard handler function (alternative to overriding on_keypress)"}, {NULL} }; diff --git a/src/PyTexture.cpp b/src/PyTexture.cpp index 1681d37..631d8af 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -1,6 +1,5 @@ #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) @@ -132,17 +131,11 @@ PyObject* PyTexture::get_source(PyTextureObject* self, void* closure) } PyGetSetDef PyTexture::getsetters[] = { - {"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}, + {"sprite_width", (getter)PyTexture::get_sprite_width, NULL, "Width of each sprite in pixels", NULL}, + {"sprite_height", (getter)PyTexture::get_sprite_height, NULL, "Height of each sprite in pixels", NULL}, + {"sheet_width", (getter)PyTexture::get_sheet_width, NULL, "Number of sprite columns in the texture", NULL}, + {"sheet_height", (getter)PyTexture::get_sheet_height, NULL, "Number of sprite rows in the texture", NULL}, + {"sprite_count", (getter)PyTexture::get_sprite_count, NULL, "Total number of sprites in the texture", NULL}, + {"source", (getter)PyTexture::get_source, NULL, "Source filename of the texture", NULL}, {NULL} // Sentinel }; diff --git a/src/PyTimer.cpp b/src/PyTimer.cpp index 95f619a..d1e89ec 100644 --- a/src/PyTimer.cpp +++ b/src/PyTimer.cpp @@ -3,7 +3,6 @@ #include "GameEngine.h" #include "Resources.h" #include "PythonObjectCache.h" -#include "McRFPy_Doc.h" #include PyObject* PyTimer::repr(PyObject* self) { @@ -308,50 +307,38 @@ PyObject* PyTimer::get_name(PyTimerObject* self, void* closure) { PyGetSetDef PyTimer::getsetters[] = { {"name", (getter)PyTimer::get_name, NULL, - MCRF_PROPERTY(name, "Timer name (str, read-only). Unique identifier for this timer."), NULL}, + "Timer name (read-only)", NULL}, {"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval, - MCRF_PROPERTY(interval, "Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running."), NULL}, + "Timer interval in milliseconds", NULL}, {"remaining", (getter)PyTimer::get_remaining, NULL, - MCRF_PROPERTY(remaining, "Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused."), NULL}, + "Time remaining until next trigger in milliseconds", NULL}, {"paused", (getter)PyTimer::get_paused, NULL, - MCRF_PROPERTY(paused, "Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time."), NULL}, + "Whether the timer is paused", NULL}, {"active", (getter)PyTimer::get_active, NULL, - MCRF_PROPERTY(active, "Whether the timer is active and not paused (bool, read-only). False if cancelled or paused."), NULL}, + "Whether the timer is active and not paused", NULL}, {"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback, - MCRF_PROPERTY(callback, "The callback function to be called when timer fires (callable). Can be changed while timer is running."), NULL}, + "The callback function to be called", NULL}, {"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once, - MCRF_PROPERTY(once, "Whether the timer stops after firing once (bool). If False, timer repeats indefinitely."), NULL}, + "Whether the timer stops after firing once", NULL}, {NULL} }; PyMethodDef PyTimer::methods[] = { {"pause", (PyCFunction)PyTimer::pause, METH_NOARGS, - 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.") - )}, + "pause() -> None\n\n" + "Pause the timer, preserving the time remaining until next trigger.\n" + "The timer can be resumed later with resume()."}, {"resume", (PyCFunction)PyTimer::resume, METH_NOARGS, - 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.") - )}, + "resume() -> None\n\n" + "Resume a paused timer from where it left off.\n" + "Has no effect if the timer is not paused."}, {"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS, - 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.") - )}, + "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."}, {"restart", (PyCFunction)PyTimer::restart, METH_NOARGS, - 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.") - )}, + "restart() -> None\n\n" + "Restart the timer from the beginning.\n" + "Resets the timer to fire after a full interval from now."}, {NULL} }; \ No newline at end of file diff --git a/src/PyVector.cpp b/src/PyVector.cpp index acb60c0..16acd51 100644 --- a/src/PyVector.cpp +++ b/src/PyVector.cpp @@ -1,65 +1,21 @@ #include "PyVector.h" #include "PyObjectUtils.h" -#include "McRFPy_Doc.h" #include PyGetSetDef PyVector::getsetters[] = { - {"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}, + {"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}, {NULL} }; PyMethodDef PyVector::methods[] = { - {"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") - )}, + {"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS, "Return the length of the vector"}, + {"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS, "Return the squared length of the vector"}, + {"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS, "Return a unit vector in the same direction"}, + {"dot", (PyCFunction)PyVector::dot, METH_O, "Return the dot product with another vector"}, + {"distance_to", (PyCFunction)PyVector::distance_to, METH_O, "Return the distance to another vector"}, + {"angle", (PyCFunction)PyVector::angle, METH_NOARGS, "Return the angle in radians from the positive X axis"}, + {"copy", (PyCFunction)PyVector::copy, METH_NOARGS, "Return a copy of this vector"}, {NULL} }; diff --git a/src/PyWindow.cpp b/src/PyWindow.cpp index 17fb1ba..c35f5c2 100644 --- a/src/PyWindow.cpp +++ b/src/PyWindow.cpp @@ -1,7 +1,6 @@ #include "PyWindow.h" #include "GameEngine.h" #include "McRFPy_API.h" -#include "McRFPy_Doc.h" #include #include @@ -484,49 +483,32 @@ int PyWindow::set_scaling_mode(PyWindowObject* self, PyObject* value, void* clos // Property definitions PyGetSetDef PyWindow::getsetters[] = { - {"resolution", (getter)get_resolution, (setter)set_resolution, - MCRF_PROPERTY(resolution, "Window resolution as (width, height) tuple. Setting this recreates the window."), NULL}, + {"resolution", (getter)get_resolution, (setter)set_resolution, + "Window resolution as (width, height) tuple", NULL}, {"fullscreen", (getter)get_fullscreen, (setter)set_fullscreen, - MCRF_PROPERTY(fullscreen, "Window fullscreen state (bool). Setting this recreates the window."), NULL}, + "Window fullscreen state", NULL}, {"vsync", (getter)get_vsync, (setter)set_vsync, - MCRF_PROPERTY(vsync, "Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate."), NULL}, + "Vertical sync enabled state", NULL}, {"title", (getter)get_title, (setter)set_title, - MCRF_PROPERTY(title, "Window title string (str). Displayed in the window title bar."), NULL}, + "Window title string", NULL}, {"visible", (getter)get_visible, (setter)set_visible, - MCRF_PROPERTY(visible, "Window visibility state (bool). Hidden windows still process events."), NULL}, + "Window visibility state", NULL}, {"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit, - MCRF_PROPERTY(framerate_limit, "Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate."), NULL}, + "Frame rate limit (0 for unlimited)", NULL}, {"game_resolution", (getter)get_game_resolution, (setter)set_game_resolution, - MCRF_PROPERTY(game_resolution, "Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling."), NULL}, + "Fixed game resolution as (width, height) tuple", NULL}, {"scaling_mode", (getter)get_scaling_mode, (setter)set_scaling_mode, - MCRF_PROPERTY(scaling_mode, "Viewport scaling mode (str): 'center' (no scaling), 'stretch' (fill window), or 'fit' (maintain aspect ratio)."), NULL}, + "Viewport scaling mode: 'center', 'stretch', or 'fit'", NULL}, {NULL} }; // Method definitions PyMethodDef PyWindow::methods[] = { {"get", (PyCFunction)PyWindow::get, METH_VARARGS | METH_CLASS, - 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.") - )}, + "Get the Window singleton instance"}, {"center", (PyCFunction)PyWindow::center, METH_NOARGS, - 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.") - )}, + "Center the window on the screen"}, {"screenshot", (PyCFunction)PyWindow::screenshot, METH_VARARGS | METH_KEYWORDS, - 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.") - )}, + "Take a screenshot. Pass filename to save to file, or get raw bytes if no filename."}, {NULL} }; \ No newline at end of file diff --git a/src/UIBase.h b/src/UIBase.h index f746168..d57e54c 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -1,6 +1,5 @@ #pragma once #include "Python.h" -#include "McRFPy_Doc.h" #include class UIEntity; @@ -79,30 +78,11 @@ 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, METH_NOARGS, \ - 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.") \ - )}, \ + "Get bounding box as (x, y, width, height)"}, \ {"move", (PyCFunction)UIDrawable_move, METH_VARARGS, \ - 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.") \ - )}, \ + "Move by relative offset (dx, dy)"}, \ {"resize", (PyCFunction)UIDrawable_resize, METH_VARARGS, \ - 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.") \ - )} + "Resize to new dimensions (width, height)"} // Property getters/setters for visible and opacity template @@ -152,14 +132,8 @@ 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, (setter)UIDrawable_set_visible, \ - MCRF_PROPERTY(visible, \ - "Whether the object is visible (bool). " \ - "Invisible objects are not rendered or clickable." \ - ), NULL}, \ + "Visibility flag", NULL}, \ {"opacity", (getter)UIDrawable_get_opacity, (setter)UIDrawable_set_opacity, \ - MCRF_PROPERTY(opacity, \ - "Opacity level (0.0 = transparent, 1.0 = opaque). " \ - "Automatically clamped to valid range [0.0, 1.0]." \ - ), NULL} + "Opacity (0.0 = transparent, 1.0 = opaque)", NULL} // UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 33cff43..6ac1adb 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -273,16 +273,8 @@ PyGetSetDef UICaption::getsetters[] = { //{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL}, {"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL}, {"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5}, - {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, - MCRF_PROPERTY(click, - "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." - ), (void*)PyObjectsEnum::UICAPTION}, - {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, - MCRF_PROPERTY(z_index, - "Z-order for rendering (lower values rendered first). " - "Automatically triggers scene resort when changed." - ), (void*)PyObjectsEnum::UICAPTION}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION}, UIDRAWABLE_GETSETTERS, {NULL} diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 8eb0522..4ceb8b8 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -398,16 +398,8 @@ PyGetSetDef UIFrame::getsetters[] = { {"fill_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Fill color of the rectangle", (void*)0}, {"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1}, {"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL}, - {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, - MCRF_PROPERTY(click, - "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." - ), (void*)PyObjectsEnum::UIFRAME}, - {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, - MCRF_PROPERTY(z_index, - "Z-order for rendering (lower values rendered first). " - "Automatically triggers scene resort when changed." - ), (void*)PyObjectsEnum::UIFRAME}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIFRAME}, {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UIFRAME}, {"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL}, diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 060c9c0..b07e596 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -1418,11 +1418,7 @@ PyGetSetDef UIGrid::getsetters[] = { {"center_y", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view Y-coordinate", (void*)5}, {"zoom", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "zoom factor for displaying the Grid", (void*)6}, - {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, - MCRF_PROPERTY(click, - "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." - ), (void*)PyObjectsEnum::UIGRID}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID}, {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 {"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL}, @@ -1432,11 +1428,7 @@ PyGetSetDef UIGrid::getsetters[] = { {"perspective_enabled", (getter)UIGrid::get_perspective_enabled, (setter)UIGrid::set_perspective_enabled, "Whether to use perspective-based FOV rendering. When True with no valid entity, " "all cells appear undiscovered.", NULL}, - {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, - MCRF_PROPERTY(z_index, - "Z-order for rendering (lower values rendered first). " - "Automatically triggers scene resort when changed." - ), (void*)PyObjectsEnum::UIGRID}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID}, UIDRAWABLE_GETSETTERS, {NULL} /* Sentinel */ diff --git a/src/UISprite.cpp b/src/UISprite.cpp index a1d697b..4be581c 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -339,16 +339,8 @@ PyGetSetDef UISprite::getsetters[] = { {"sprite_index", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL}, {"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Sprite index (DEPRECATED: use sprite_index instead)", NULL}, {"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL}, - {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, - MCRF_PROPERTY(click, - "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." - ), (void*)PyObjectsEnum::UISPRITE}, - {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, - MCRF_PROPERTY(z_index, - "Z-order for rendering (lower values rendered first). " - "Automatically triggers scene resort when changed." - ), (void*)PyObjectsEnum::UISPRITE}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UISPRITE}, {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UISPRITE}, UIDRAWABLE_GETSETTERS, diff --git a/tools/generate_api_docs.py b/tools/generate_api_docs.py new file mode 100644 index 0000000..d1e100f --- /dev/null +++ b/tools/generate_api_docs.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python3 +"""Generate API reference documentation for McRogueFace. + +This script generates comprehensive API documentation in multiple formats: +- Markdown for GitHub/documentation sites +- HTML for local browsing +- RST for Sphinx integration (future) +""" + +import os +import sys +import inspect +import datetime +from typing import Dict, List, Any, Optional +from pathlib import Path + +# We need to run this with McRogueFace as the interpreter +# so mcrfpy is available +import mcrfpy + +def escape_markdown(text: str) -> str: + """Escape special markdown characters.""" + if not text: + return "" + # Escape backticks in inline code + return text.replace("`", "\\`") + +def format_signature(name: str, doc: str) -> str: + """Extract and format function signature from docstring.""" + if not doc: + return f"{name}(...)" + + lines = doc.strip().split('\n') + if lines and '(' in lines[0]: + # First line contains signature + return lines[0].split('->')[0].strip() + + return f"{name}(...)" + +def get_class_info(cls: type) -> Dict[str, Any]: + """Extract comprehensive information about a class.""" + info = { + 'name': cls.__name__, + 'doc': cls.__doc__ or "", + 'methods': [], + 'properties': [], + 'bases': [base.__name__ for base in cls.__bases__ if base.__name__ != 'object'], + } + + # Get all attributes + for attr_name in sorted(dir(cls)): + if attr_name.startswith('_') and not attr_name.startswith('__'): + continue + + try: + attr = getattr(cls, attr_name) + + if isinstance(attr, property): + prop_info = { + 'name': attr_name, + 'doc': (attr.fget.__doc__ if attr.fget else "") or "", + 'readonly': attr.fset is None + } + info['properties'].append(prop_info) + elif callable(attr) and not attr_name.startswith('__'): + method_info = { + 'name': attr_name, + 'doc': attr.__doc__ or "", + 'signature': format_signature(attr_name, attr.__doc__) + } + info['methods'].append(method_info) + except: + pass + + return info + +def get_function_info(func: Any, name: str) -> Dict[str, Any]: + """Extract information about a function.""" + return { + 'name': name, + 'doc': func.__doc__ or "", + 'signature': format_signature(name, func.__doc__) + } + +def generate_markdown_class(cls_info: Dict[str, Any]) -> List[str]: + """Generate markdown documentation for a class.""" + lines = [] + + # Class header + lines.append(f"### class `{cls_info['name']}`") + if cls_info['bases']: + lines.append(f"*Inherits from: {', '.join(cls_info['bases'])}*") + lines.append("") + + # Class description + if cls_info['doc']: + doc_lines = cls_info['doc'].strip().split('\n') + # First line is usually the constructor signature + if doc_lines and '(' in doc_lines[0]: + lines.append(f"```python") + lines.append(doc_lines[0]) + lines.append("```") + lines.append("") + # Rest is description + if len(doc_lines) > 2: + lines.extend(doc_lines[2:]) + lines.append("") + else: + lines.extend(doc_lines) + lines.append("") + + # Properties + if cls_info['properties']: + lines.append("#### Properties") + lines.append("") + for prop in cls_info['properties']: + readonly = " *(readonly)*" if prop['readonly'] else "" + lines.append(f"- **`{prop['name']}`**{readonly}") + if prop['doc']: + lines.append(f" - {prop['doc'].strip()}") + lines.append("") + + # Methods + if cls_info['methods']: + lines.append("#### Methods") + lines.append("") + for method in cls_info['methods']: + lines.append(f"##### `{method['signature']}`") + if method['doc']: + # Parse docstring for better formatting + doc_lines = method['doc'].strip().split('\n') + # Skip the signature line if it's repeated + start = 1 if doc_lines and method['name'] in doc_lines[0] else 0 + for line in doc_lines[start:]: + lines.append(line) + lines.append("") + + lines.append("---") + lines.append("") + return lines + +def generate_markdown_function(func_info: Dict[str, Any]) -> List[str]: + """Generate markdown documentation for a function.""" + lines = [] + + lines.append(f"### `{func_info['signature']}`") + lines.append("") + + if func_info['doc']: + doc_lines = func_info['doc'].strip().split('\n') + # Skip signature line if present + start = 1 if doc_lines and func_info['name'] in doc_lines[0] else 0 + + # Process documentation sections + in_section = None + for line in doc_lines[start:]: + if line.strip() in ['Args:', 'Returns:', 'Raises:', 'Note:', 'Example:']: + in_section = line.strip() + lines.append(f"**{in_section}**") + elif in_section and line.strip(): + # Indent content under sections + lines.append(f"{line}") + else: + lines.append(line) + lines.append("") + + lines.append("---") + lines.append("") + return lines + +def generate_markdown_docs() -> str: + """Generate complete markdown API documentation.""" + lines = [] + + # Header + lines.append("# McRogueFace API Reference") + lines.append("") + lines.append(f"*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*") + lines.append("") + + # Module description + if mcrfpy.__doc__: + lines.append("## Overview") + lines.append("") + lines.extend(mcrfpy.__doc__.strip().split('\n')) + lines.append("") + + # Table of contents + lines.append("## Table of Contents") + lines.append("") + lines.append("- [Classes](#classes)") + lines.append("- [Functions](#functions)") + lines.append("- [Automation Module](#automation-module)") + lines.append("") + + # Collect all components + classes = [] + functions = [] + constants = [] + + for name in sorted(dir(mcrfpy)): + if name.startswith('_'): + continue + + obj = getattr(mcrfpy, name) + + if isinstance(obj, type): + classes.append((name, obj)) + elif callable(obj): + functions.append((name, obj)) + elif not inspect.ismodule(obj): + constants.append((name, obj)) + + # Document classes + lines.append("## Classes") + lines.append("") + + # Group classes by category + ui_classes = [] + collection_classes = [] + system_classes = [] + other_classes = [] + + for name, cls in classes: + if name in ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']: + ui_classes.append((name, cls)) + elif 'Collection' in name: + collection_classes.append((name, cls)) + elif name in ['Color', 'Vector', 'Texture', 'Font']: + system_classes.append((name, cls)) + else: + other_classes.append((name, cls)) + + # UI Classes + if ui_classes: + lines.append("### UI Components") + lines.append("") + for name, cls in ui_classes: + lines.extend(generate_markdown_class(get_class_info(cls))) + + # Collections + if collection_classes: + lines.append("### Collections") + lines.append("") + for name, cls in collection_classes: + lines.extend(generate_markdown_class(get_class_info(cls))) + + # System Classes + if system_classes: + lines.append("### System Types") + lines.append("") + for name, cls in system_classes: + lines.extend(generate_markdown_class(get_class_info(cls))) + + # Other Classes + if other_classes: + lines.append("### Other Classes") + lines.append("") + for name, cls in other_classes: + lines.extend(generate_markdown_class(get_class_info(cls))) + + # Document functions + lines.append("## Functions") + lines.append("") + + # Group functions by category + scene_funcs = [] + audio_funcs = [] + ui_funcs = [] + system_funcs = [] + + for name, func in functions: + if 'scene' in name.lower() or name in ['createScene', 'setScene']: + scene_funcs.append((name, func)) + elif any(x in name.lower() for x in ['sound', 'music', 'volume']): + audio_funcs.append((name, func)) + elif name in ['find', 'findAll']: + ui_funcs.append((name, func)) + else: + system_funcs.append((name, func)) + + # Scene Management + if scene_funcs: + lines.append("### Scene Management") + lines.append("") + for name, func in scene_funcs: + lines.extend(generate_markdown_function(get_function_info(func, name))) + + # Audio + if audio_funcs: + lines.append("### Audio") + lines.append("") + for name, func in audio_funcs: + lines.extend(generate_markdown_function(get_function_info(func, name))) + + # UI Utilities + if ui_funcs: + lines.append("### UI Utilities") + lines.append("") + for name, func in ui_funcs: + lines.extend(generate_markdown_function(get_function_info(func, name))) + + # System + if system_funcs: + lines.append("### System") + lines.append("") + for name, func in system_funcs: + lines.extend(generate_markdown_function(get_function_info(func, name))) + + # Automation module + if hasattr(mcrfpy, 'automation'): + lines.append("## Automation Module") + lines.append("") + lines.append("The `mcrfpy.automation` module provides testing and automation capabilities.") + lines.append("") + + automation = mcrfpy.automation + auto_funcs = [] + + for name in sorted(dir(automation)): + if not name.startswith('_'): + obj = getattr(automation, name) + if callable(obj): + auto_funcs.append((name, obj)) + + for name, func in auto_funcs: + # Format as static method + func_info = get_function_info(func, name) + lines.append(f"### `automation.{func_info['signature']}`") + lines.append("") + if func_info['doc']: + lines.append(func_info['doc']) + lines.append("") + lines.append("---") + lines.append("") + + return '\n'.join(lines) + +def generate_html_docs(markdown_content: str) -> str: + """Convert markdown to HTML.""" + # Simple conversion - in production use a proper markdown parser + html = [''] + html.append('') + html.append('') + html.append('McRogueFace API Reference') + html.append('') + html.append('') + + # Very basic markdown to HTML conversion + lines = markdown_content.split('\n') + in_code_block = False + in_list = False + + for line in lines: + stripped = line.strip() + + if stripped.startswith('```'): + if in_code_block: + html.append('') + in_code_block = False + else: + lang = stripped[3:] or 'python' + html.append(f'
')
+                in_code_block = True
+            continue
+        
+        if in_code_block:
+            html.append(line)
+            continue
+        
+        # Headers
+        if stripped.startswith('#'):
+            level = len(stripped.split()[0])
+            text = stripped[level:].strip()
+            html.append(f'{text}')
+        # Lists
+        elif stripped.startswith('- '):
+            if not in_list:
+                html.append('
    ') + in_list = True + html.append(f'
  • {stripped[2:]}
  • ') + # Horizontal rule + elif stripped == '---': + if in_list: + html.append('
') + in_list = False + html.append('
') + # Emphasis + elif stripped.startswith('*') and stripped.endswith('*') and len(stripped) > 2: + html.append(f'{stripped[1:-1]}') + # Bold + elif stripped.startswith('**') and stripped.endswith('**'): + html.append(f'{stripped[2:-2]}') + # Regular paragraph + elif stripped: + if in_list: + html.append('') + in_list = False + # Convert inline code + text = stripped + if '`' in text: + import re + text = re.sub(r'`([^`]+)`', r'\1', text) + html.append(f'

{text}

') + else: + if in_list: + html.append('') + in_list = False + # Empty line + html.append('') + + if in_list: + html.append('') + if in_code_block: + html.append('
') + + html.append('') + return '\n'.join(html) + +def main(): + """Generate API documentation in multiple formats.""" + print("Generating McRogueFace API Documentation...") + + # Create docs directory + docs_dir = Path("docs") + docs_dir.mkdir(exist_ok=True) + + # Generate markdown documentation + print("- Generating Markdown documentation...") + markdown_content = generate_markdown_docs() + + # Write markdown + md_path = docs_dir / "API_REFERENCE.md" + with open(md_path, 'w') as f: + f.write(markdown_content) + print(f" ✓ Written to {md_path}") + + # Generate HTML + print("- Generating HTML documentation...") + html_content = generate_html_docs(markdown_content) + + # Write HTML + html_path = docs_dir / "api_reference.html" + with open(html_path, 'w') as f: + f.write(html_content) + print(f" ✓ Written to {html_path}") + + # Summary statistics + lines = markdown_content.split('\n') + class_count = markdown_content.count('### class') + func_count = len([l for l in lines if l.strip().startswith('### `') and 'class' not in l]) + + print("\nDocumentation Statistics:") + print(f"- Classes documented: {class_count}") + print(f"- Functions documented: {func_count}") + print(f"- Total lines: {len(lines)}") + print(f"- File size: {len(markdown_content):,} bytes") + + print("\nAPI documentation generated successfully!") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tools/generate_api_docs_html.py b/tools/generate_api_docs_html.py new file mode 100644 index 0000000..fe3cf08 --- /dev/null +++ b/tools/generate_api_docs_html.py @@ -0,0 +1,1602 @@ +#!/usr/bin/env python3 +"""Generate high-quality HTML API reference documentation for McRogueFace.""" + +import os +import sys +import datetime +import html +from pathlib import Path +import mcrfpy + +def escape_html(text: str) -> str: + """Escape HTML special characters.""" + return html.escape(text) if text else "" + +def format_docstring_as_html(docstring: str) -> str: + """Convert docstring to properly formatted HTML.""" + if not docstring: + return "" + + # Split and process lines + lines = docstring.strip().split('\n') + result = [] + in_code_block = False + + for line in lines: + # Convert \n to actual newlines + line = line.replace('\\n', '\n') + + # Handle code blocks + if line.strip().startswith('```'): + if in_code_block: + result.append('') + in_code_block = False + else: + result.append('
')
+                in_code_block = True
+            continue
+            
+        # Convert markdown-style code to HTML
+        if '`' in line and not in_code_block:
+            import re
+            line = re.sub(r'`([^`]+)`', r'\1', line)
+        
+        if in_code_block:
+            result.append(escape_html(line))
+        else:
+            result.append(escape_html(line) + '
') + + if in_code_block: + result.append('
') + + return '\n'.join(result) + +def get_class_details(cls): + """Get detailed information about a class.""" + info = { + 'name': cls.__name__, + 'doc': cls.__doc__ or "", + 'methods': {}, + 'properties': {}, + 'bases': [] + } + + # Get real base classes (excluding object) + for base in cls.__bases__: + if base.__name__ != 'object': + info['bases'].append(base.__name__) + + # Special handling for Entity which doesn't inherit from Drawable + if cls.__name__ == 'Entity' and 'Drawable' in info['bases']: + info['bases'].remove('Drawable') + + # Get methods and properties + for attr_name in dir(cls): + if attr_name.startswith('__') and attr_name != '__init__': + continue + + try: + attr = getattr(cls, attr_name) + + if isinstance(attr, property): + info['properties'][attr_name] = { + 'doc': (attr.fget.__doc__ if attr.fget else "") or "", + 'readonly': attr.fset is None + } + elif callable(attr) and not attr_name.startswith('_'): + info['methods'][attr_name] = attr.__doc__ or "" + except: + pass + + return info + +def generate_class_init_docs(class_name): + """Generate initialization documentation for specific classes.""" + init_docs = { + 'Entity': { + 'signature': 'Entity(x=0, y=0, sprite_id=0)', + 'description': 'Game entity that can be placed in a Grid.', + 'args': [ + ('x', 'int', 'Grid x coordinate. Default: 0'), + ('y', 'int', 'Grid y coordinate. Default: 0'), + ('sprite_id', 'int', 'Sprite index for rendering. Default: 0') + ], + 'example': '''entity = mcrfpy.Entity(5, 10, 42) +entity.move(1, 0) # Move right one tile''' + }, + 'Color': { + 'signature': 'Color(r=255, g=255, b=255, a=255)', + 'description': 'RGBA color representation.', + 'args': [ + ('r', 'int', 'Red component (0-255). Default: 255'), + ('g', 'int', 'Green component (0-255). Default: 255'), + ('b', 'int', 'Blue component (0-255). Default: 255'), + ('a', 'int', 'Alpha component (0-255). Default: 255') + ], + 'example': 'red = mcrfpy.Color(255, 0, 0)' + }, + 'Font': { + 'signature': 'Font(filename)', + 'description': 'Load a font from file.', + 'args': [ + ('filename', 'str', 'Path to font file (TTF/OTF)') + ] + }, + 'Texture': { + 'signature': 'Texture(filename)', + 'description': 'Load a texture from file.', + 'args': [ + ('filename', 'str', 'Path to image file (PNG/JPG/BMP)') + ] + }, + 'Vector': { + 'signature': 'Vector(x=0.0, y=0.0)', + 'description': '2D vector for positions and directions.', + 'args': [ + ('x', 'float', 'X component. Default: 0.0'), + ('y', 'float', 'Y component. Default: 0.0') + ] + }, + 'Animation': { + 'signature': 'Animation(property_name, start_value, end_value, duration, transition="linear", loop=False)', + 'description': 'Animate UI element properties over time.', + 'args': [ + ('property_name', 'str', 'Property to animate (e.g., "x", "y", "scale")'), + ('start_value', 'float', 'Starting value'), + ('end_value', 'float', 'Ending value'), + ('duration', 'float', 'Duration in seconds'), + ('transition', 'str', 'Easing function. Default: "linear"'), + ('loop', 'bool', 'Whether to loop. Default: False') + ], + 'properties': ['current_value', 'elapsed_time', 'is_running', 'is_finished'] + }, + 'GridPoint': { + 'description': 'Represents a single tile in a Grid.', + 'properties': ['x', 'y', 'texture_index', 'solid', 'transparent', 'color'] + }, + 'GridPointState': { + 'description': 'State information for a GridPoint.', + 'properties': ['visible', 'discovered', 'custom_flags'] + }, + 'Timer': { + 'signature': 'Timer(name, callback, interval_ms)', + 'description': 'Create a recurring timer.', + 'args': [ + ('name', 'str', 'Unique timer identifier'), + ('callback', 'callable', 'Function to call'), + ('interval_ms', 'int', 'Interval in milliseconds') + ] + } + } + + return init_docs.get(class_name, {}) + +def generate_method_docs(method_name, class_name): + """Generate documentation for specific methods.""" + method_docs = { + # Base Drawable methods (inherited by all UI elements) + 'Drawable': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of this drawable element.', + 'returns': 'tuple: (x, y, width, height) representing the element\'s bounds', + 'note': 'The bounds are in screen coordinates and account for current position and size.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the element by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'This modifies the x and y position properties by the given amounts.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the element to new dimensions.', + 'args': [ + ('width', 'float', 'New width in pixels'), + ('height', 'float', 'New height in pixels') + ], + 'note': 'Behavior varies by element type. Some elements may ignore or constrain dimensions.' + } + }, + + # Caption-specific methods + 'Caption': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the text.', + 'returns': 'tuple: (x, y, width, height) based on text content and font size', + 'note': 'Bounds are automatically calculated from the rendered text dimensions.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the caption by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ] + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Set text wrapping bounds (limited support).', + 'args': [ + ('width', 'float', 'Maximum width for text wrapping'), + ('height', 'float', 'Currently unused') + ], + 'note': 'Full text wrapping is not yet implemented. This prepares for future multiline support.' + } + }, + + # Entity-specific methods + 'Entity': { + 'at': { + 'signature': 'at(x, y)', + 'description': 'Get the GridPointState at the specified grid coordinates relative to this entity.', + 'args': [ + ('x', 'int', 'Grid x offset from entity position'), + ('y', 'int', 'Grid y offset from entity position') + ], + 'returns': 'GridPointState: State of the grid point at the specified position', + 'note': 'Requires entity to be associated with a grid. Raises ValueError if not.' + }, + 'die': { + 'signature': 'die()', + 'description': 'Remove this entity from its parent grid.', + 'returns': 'None', + 'note': 'The entity object remains valid but is no longer rendered or updated.' + }, + 'index': { + 'signature': 'index()', + 'description': 'Get the index of this entity in its grid\'s entity collection.', + 'returns': 'int: Zero-based index in the parent grid\'s entity list', + 'note': 'Raises RuntimeError if not associated with a grid, ValueError if not found.' + }, + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the entity\'s sprite.', + 'returns': 'tuple: (x, y, width, height) of the sprite bounds', + 'note': 'Delegates to the internal sprite\'s get_bounds method.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the entity by a relative offset in pixels.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'Updates both sprite position and entity grid position.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Entities do not support direct resizing.', + 'args': [ + ('width', 'float', 'Ignored'), + ('height', 'float', 'Ignored') + ], + 'note': 'This method exists for interface compatibility but has no effect.' + } + }, + + # Frame-specific methods + 'Frame': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the frame.', + 'returns': 'tuple: (x, y, width, height) representing the frame bounds' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the frame and all its children by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'Child elements maintain their relative positions within the frame.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the frame to new dimensions.', + 'args': [ + ('width', 'float', 'New width in pixels'), + ('height', 'float', 'New height in pixels') + ], + 'note': 'Does not automatically resize children. Set clip_children=True to clip overflow.' + } + }, + + # Grid-specific methods + 'Grid': { + 'at': { + 'signature': 'at(x, y) or at((x, y))', + 'description': 'Get the GridPoint at the specified grid coordinates.', + 'args': [ + ('x', 'int', 'Grid x coordinate (0-based)'), + ('y', 'int', 'Grid y coordinate (0-based)') + ], + 'returns': 'GridPoint: The grid point at (x, y)', + 'note': 'Raises IndexError if coordinates are out of range. Accepts either two arguments or a tuple.', + 'example': 'point = grid.at(5, 3) # or grid.at((5, 3))' + }, + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the entire grid.', + 'returns': 'tuple: (x, y, width, height) of the grid\'s display area' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the grid display by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'Moves the entire grid viewport. Use center property to pan within the grid.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the grid\'s display viewport.', + 'args': [ + ('width', 'float', 'New viewport width in pixels'), + ('height', 'float', 'New viewport height in pixels') + ], + 'note': 'Changes the visible area, not the grid dimensions. Use zoom to scale content.' + } + }, + + # Sprite-specific methods + 'Sprite': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of the sprite.', + 'returns': 'tuple: (x, y, width, height) based on texture size and scale', + 'note': 'Bounds account for current scale. Returns (x, y, 0, 0) if no texture.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the sprite by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ] + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the sprite by adjusting its scale.', + 'args': [ + ('width', 'float', 'Target width in pixels'), + ('height', 'float', 'Target height in pixels') + ], + 'note': 'Calculates and applies uniform scale to best fit the target dimensions.' + } + }, + + 'Animation': { + 'get_current_value': { + 'signature': 'get_current_value()', + 'description': 'Get the current interpolated value.', + 'returns': 'float: Current animation value' + }, + 'start': { + 'signature': 'start(target)', + 'description': 'Start the animation on a target UI element.', + 'args': [('target', 'UIDrawable', 'The element to animate')] + } + }, + + # Collection methods (shared by EntityCollection and UICollection) + 'EntityCollection': { + 'append': { + 'signature': 'append(entity)', + 'description': 'Add an entity to the end of the collection.', + 'args': [ + ('entity', 'Entity', 'The entity to add') + ] + }, + 'remove': { + 'signature': 'remove(entity)', + 'description': 'Remove the first occurrence of an entity from the collection.', + 'args': [ + ('entity', 'Entity', 'The entity to remove') + ], + 'note': 'Raises ValueError if entity is not found.' + }, + 'extend': { + 'signature': 'extend(iterable)', + 'description': 'Add multiple entities from an iterable.', + 'args': [ + ('iterable', 'iterable', 'An iterable of Entity objects') + ] + }, + 'count': { + 'signature': 'count(entity)', + 'description': 'Count occurrences of an entity in the collection.', + 'args': [ + ('entity', 'Entity', 'The entity to count') + ], + 'returns': 'int: Number of times the entity appears' + }, + 'index': { + 'signature': 'index(entity)', + 'description': 'Find the index of the first occurrence of an entity.', + 'args': [ + ('entity', 'Entity', 'The entity to find') + ], + 'returns': 'int: Zero-based index of the entity', + 'note': 'Raises ValueError if entity is not found.' + } + }, + + 'UICollection': { + 'append': { + 'signature': 'append(drawable)', + 'description': 'Add a drawable element to the end of the collection.', + 'args': [ + ('drawable', 'Drawable', 'Any UI element (Frame, Caption, Sprite, Grid)') + ] + }, + 'remove': { + 'signature': 'remove(drawable)', + 'description': 'Remove the first occurrence of a drawable from the collection.', + 'args': [ + ('drawable', 'Drawable', 'The drawable to remove') + ], + 'note': 'Raises ValueError if drawable is not found.' + }, + 'extend': { + 'signature': 'extend(iterable)', + 'description': 'Add multiple drawables from an iterable.', + 'args': [ + ('iterable', 'iterable', 'An iterable of Drawable objects') + ] + }, + 'count': { + 'signature': 'count(drawable)', + 'description': 'Count occurrences of a drawable in the collection.', + 'args': [ + ('drawable', 'Drawable', 'The drawable to count') + ], + 'returns': 'int: Number of times the drawable appears' + }, + 'index': { + 'signature': 'index(drawable)', + 'description': 'Find the index of the first occurrence of a drawable.', + 'args': [ + ('drawable', 'Drawable', 'The drawable to find') + ], + 'returns': 'int: Zero-based index of the drawable', + 'note': 'Raises ValueError if drawable is not found.' + } + } + } + + return method_docs.get(class_name, {}).get(method_name, {}) + +def generate_function_docs(): + """Generate documentation for all mcrfpy module functions.""" + function_docs = { + # Scene Management + 'createScene': { + 'signature': 'createScene(name: str) -> None', + 'description': 'Create a new empty scene.', + 'args': [ + ('name', 'str', 'Unique name for the new scene') + ], + 'returns': 'None', + 'exceptions': [ + ('ValueError', 'If a scene with this name already exists') + ], + 'note': 'The scene is created but not made active. Use setScene() to switch to it.', + 'example': '''mcrfpy.createScene("game") +mcrfpy.createScene("menu") +mcrfpy.setScene("game")''' + }, + + 'setScene': { + 'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None', + 'description': 'Switch to a different scene with optional transition effect.', + 'args': [ + ('scene', 'str', 'Name of the scene to switch to'), + ('transition', 'str', 'Transition type ("fade", "slide_left", "slide_right", "slide_up", "slide_down"). Default: None'), + ('duration', 'float', 'Transition duration in seconds. Default: 0.0 for instant') + ], + 'returns': 'None', + 'exceptions': [ + ('KeyError', 'If the scene doesn\'t exist'), + ('ValueError', 'If the transition type is invalid') + ], + 'example': '''mcrfpy.setScene("menu") +mcrfpy.setScene("game", "fade", 0.5) +mcrfpy.setScene("credits", "slide_left", 1.0)''' + }, + + 'currentScene': { + 'signature': 'currentScene() -> str', + 'description': 'Get the name of the currently active scene.', + 'args': [], + 'returns': 'str: Name of the current scene', + 'example': '''scene = mcrfpy.currentScene() +print(f"Currently in scene: {scene}")''' + }, + + 'sceneUI': { + 'signature': 'sceneUI(scene: str = None) -> list', + 'description': 'Get all UI elements for a scene.', + 'args': [ + ('scene', 'str', 'Scene name. If None, uses current scene. Default: None') + ], + 'returns': 'list: All UI elements (Frame, Caption, Sprite, Grid) in the scene', + 'exceptions': [ + ('KeyError', 'If the specified scene doesn\'t exist') + ], + 'example': '''# Get UI for current scene +ui_elements = mcrfpy.sceneUI() + +# Get UI for specific scene +menu_ui = mcrfpy.sceneUI("menu") +for element in menu_ui: + print(f"{element.name}: {type(element).__name__}")''' + }, + + 'keypressScene': { + 'signature': 'keypressScene(handler: callable) -> None', + 'description': 'Set the keyboard event handler for the current scene.', + 'args': [ + ('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)') + ], + 'returns': 'None', + 'note': 'The handler is called for every key press and release event. Key names are single characters (e.g., "A", "1") or special keys (e.g., "Space", "Enter", "Escape").', + 'example': '''def on_key(key, pressed): + if pressed: + if key == "Space": + player.jump() + elif key == "Escape": + mcrfpy.setScene("pause_menu") + else: + # Handle key release + if key in ["A", "D"]: + player.stop_moving() + +mcrfpy.keypressScene(on_key)''' + }, + + # Audio Functions + 'createSoundBuffer': { + 'signature': 'createSoundBuffer(filename: str) -> int', + 'description': 'Load a sound effect from a file and return its buffer ID.', + 'args': [ + ('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)') + ], + 'returns': 'int: Buffer ID for use with playSound()', + 'exceptions': [ + ('RuntimeError', 'If the file cannot be loaded') + ], + 'note': 'Sound buffers are stored in memory for fast playback. Load sound effects once and reuse the buffer ID.', + 'example': '''# Load sound effects +jump_sound = mcrfpy.createSoundBuffer("assets/sounds/jump.wav") +coin_sound = mcrfpy.createSoundBuffer("assets/sounds/coin.ogg") + +# Play later +mcrfpy.playSound(jump_sound)''' + }, + + 'loadMusic': { + 'signature': 'loadMusic(filename: str, loop: bool = True) -> None', + 'description': 'Load and immediately play background music from a file.', + 'args': [ + ('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'), + ('loop', 'bool', 'Whether to loop the music. Default: True') + ], + 'returns': 'None', + 'note': 'Only one music track can play at a time. Loading new music stops the current track.', + 'example': '''# Play looping background music +mcrfpy.loadMusic("assets/music/theme.ogg") + +# Play music once without looping +mcrfpy.loadMusic("assets/music/victory.ogg", loop=False)''' + }, + + 'playSound': { + 'signature': 'playSound(buffer_id: int) -> None', + 'description': 'Play a sound effect using a previously loaded buffer.', + 'args': [ + ('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()') + ], + 'returns': 'None', + 'exceptions': [ + ('RuntimeError', 'If the buffer ID is invalid') + ], + 'note': 'Multiple sounds can play simultaneously. Each call creates a new sound instance.', + 'example': '''# Load once +explosion_sound = mcrfpy.createSoundBuffer("explosion.wav") + +# Play multiple times +for enemy in destroyed_enemies: + mcrfpy.playSound(explosion_sound)''' + }, + + 'getMusicVolume': { + 'signature': 'getMusicVolume() -> int', + 'description': 'Get the current music volume level.', + 'args': [], + 'returns': 'int: Current volume (0-100)', + 'example': '''volume = mcrfpy.getMusicVolume() +print(f"Music volume: {volume}%")''' + }, + + 'getSoundVolume': { + 'signature': 'getSoundVolume() -> int', + 'description': 'Get the current sound effects volume level.', + 'args': [], + 'returns': 'int: Current volume (0-100)', + 'example': '''volume = mcrfpy.getSoundVolume() +print(f"Sound effects volume: {volume}%")''' + }, + + 'setMusicVolume': { + 'signature': 'setMusicVolume(volume: int) -> None', + 'description': 'Set the global music volume.', + 'args': [ + ('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)') + ], + 'returns': 'None', + 'example': '''# Mute music +mcrfpy.setMusicVolume(0) + +# Half volume +mcrfpy.setMusicVolume(50) + +# Full volume +mcrfpy.setMusicVolume(100)''' + }, + + 'setSoundVolume': { + 'signature': 'setSoundVolume(volume: int) -> None', + 'description': 'Set the global sound effects volume.', + 'args': [ + ('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)') + ], + 'returns': 'None', + 'example': '''# Audio settings from options menu +mcrfpy.setSoundVolume(sound_slider.value) +mcrfpy.setMusicVolume(music_slider.value)''' + }, + + # UI Utilities + 'find': { + 'signature': 'find(name: str, scene: str = None) -> UIDrawable | None', + 'description': 'Find the first UI element with the specified name.', + 'args': [ + ('name', 'str', 'Exact name to search for'), + ('scene', 'str', 'Scene to search in. Default: current scene') + ], + 'returns': 'Frame, Caption, Sprite, Grid, or Entity if found; None otherwise', + 'note': 'Searches scene UI elements and entities within grids. Returns the first match found.', + 'example': '''# Find in current scene +player = mcrfpy.find("player") +if player: + player.x = 100 + +# Find in specific scene +menu_button = mcrfpy.find("start_button", "main_menu")''' + }, + + 'findAll': { + 'signature': 'findAll(pattern: str, scene: str = None) -> list', + 'description': 'Find all UI elements matching a name pattern.', + 'args': [ + ('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'), + ('scene', 'str', 'Scene to search in. Default: current scene') + ], + 'returns': 'list: All matching UI elements and entities', + 'note': 'Supports wildcard patterns for flexible searching.', + 'example': '''# Find all enemies +enemies = mcrfpy.findAll("enemy*") +for enemy in enemies: + enemy.sprite_id = 0 # Reset sprite + +# Find all buttons +buttons = mcrfpy.findAll("*_button") +for btn in buttons: + btn.visible = True + +# Find exact matches +health_bars = mcrfpy.findAll("health_bar") # No wildcards = exact match''' + }, + + # System Functions + 'exit': { + 'signature': 'exit() -> None', + 'description': 'Cleanly shut down the game engine and exit the application.', + 'args': [], + 'returns': 'None', + 'note': 'This immediately closes the window and terminates the program. Ensure any necessary cleanup is done before calling.', + 'example': '''def quit_game(): + # Save game state + save_progress() + + # Exit + mcrfpy.exit()''' + }, + + 'getMetrics': { + 'signature': 'getMetrics() -> dict', + 'description': 'Get current performance metrics.', + 'args': [], + 'returns': '''dict: Performance data with keys: + - frame_time: Last frame duration in seconds + - avg_frame_time: Average frame time + - fps: Frames per second + - draw_calls: Number of draw calls + - ui_elements: Total UI element count + - visible_elements: Visible element count + - current_frame: Frame counter + - runtime: Total runtime in seconds''', + 'example': '''metrics = mcrfpy.getMetrics() +print(f"FPS: {metrics['fps']}") +print(f"Frame time: {metrics['frame_time']*1000:.1f}ms") +print(f"Draw calls: {metrics['draw_calls']}") +print(f"Runtime: {metrics['runtime']:.1f}s") + +# Performance monitoring +if metrics['fps'] < 30: + print("Performance warning: FPS below 30")''' + }, + + 'setTimer': { + 'signature': 'setTimer(name: str, handler: callable, interval: int) -> None', + 'description': 'Create or update a recurring timer.', + 'args': [ + ('name', 'str', 'Unique identifier for the timer'), + ('handler', 'callable', 'Function called with (runtime: float) parameter'), + ('interval', 'int', 'Time between calls in milliseconds') + ], + 'returns': 'None', + 'note': 'If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.', + 'example': '''# Simple repeating timer +def spawn_enemy(runtime): + enemy = mcrfpy.Entity() + enemy.x = random.randint(0, 800) + grid.entities.append(enemy) + +mcrfpy.setTimer("enemy_spawner", spawn_enemy, 2000) # Every 2 seconds + +# Timer with runtime check +def update_timer(runtime): + time_left = 60 - runtime + timer_text.text = f"Time: {int(time_left)}" + if time_left <= 0: + mcrfpy.delTimer("game_timer") + game_over() + +mcrfpy.setTimer("game_timer", update_timer, 100) # Update every 100ms''' + }, + + 'delTimer': { + 'signature': 'delTimer(name: str) -> None', + 'description': 'Stop and remove a timer.', + 'args': [ + ('name', 'str', 'Timer identifier to remove') + ], + 'returns': 'None', + 'note': 'No error is raised if the timer doesn\'t exist.', + 'example': '''# Stop spawning enemies +mcrfpy.delTimer("enemy_spawner") + +# Clean up all game timers +for timer_name in ["enemy_spawner", "powerup_timer", "score_updater"]: + mcrfpy.delTimer(timer_name)''' + }, + + 'setScale': { + 'signature': 'setScale(multiplier: float) -> None', + 'description': 'Scale the game window size.', + 'args': [ + ('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)') + ], + 'returns': 'None', + 'exceptions': [ + ('ValueError', 'If multiplier is not between 0.2 and 4.0') + ], + 'note': 'The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.', + 'example': '''# Double the window size +mcrfpy.setScale(2.0) + +# Half size window +mcrfpy.setScale(0.5) + +# Better approach (not deprecated): +mcrfpy.Window.resolution = (1920, 1080)''' + } + } + + return function_docs + +def generate_collection_docs(class_name): + """Generate documentation for collection classes.""" + collection_docs = { + 'EntityCollection': { + 'description': 'Container for Entity objects in a Grid. Supports iteration and indexing.', + 'methods': { + 'append': 'Add an entity to the collection', + 'remove': 'Remove an entity from the collection', + 'extend': 'Add multiple entities from an iterable', + 'count': 'Count occurrences of an entity', + 'index': 'Find the index of an entity' + } + }, + 'UICollection': { + 'description': 'Container for UI drawable elements. Supports iteration and indexing.', + 'methods': { + 'append': 'Add a UI element to the collection', + 'remove': 'Remove a UI element from the collection', + 'extend': 'Add multiple UI elements from an iterable', + 'count': 'Count occurrences of a UI element', + 'index': 'Find the index of a UI element' + } + }, + 'UICollectionIter': { + 'description': 'Iterator for UICollection. Automatically created when iterating over a UICollection.' + }, + 'UIEntityCollectionIter': { + 'description': 'Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.' + } + } + + return collection_docs.get(class_name, {}) + +def format_class_html(cls_info, class_name): + """Format a class as HTML with proper structure.""" + html_parts = [] + + # Class header + html_parts.append(f'
') + html_parts.append(f'

class {class_name}

') + + # Inheritance + if cls_info['bases']: + html_parts.append(f'

Inherits from: {", ".join(cls_info["bases"])}

') + + # Get additional documentation + init_info = generate_class_init_docs(class_name) + collection_info = generate_collection_docs(class_name) + + # Constructor signature for classes with __init__ + if init_info.get('signature'): + html_parts.append('
') + html_parts.append('
')
+        html_parts.append(escape_html(init_info['signature']))
+        html_parts.append('
') + html_parts.append('
') + + # Description + description = "" + if collection_info.get('description'): + description = collection_info['description'] + elif init_info.get('description'): + description = init_info['description'] + elif cls_info['doc']: + # Parse description from docstring + doc_lines = cls_info['doc'].strip().split('\n') + # Skip constructor line if present + start_idx = 1 if doc_lines and '(' in doc_lines[0] else 0 + if start_idx < len(doc_lines): + description = '\n'.join(doc_lines[start_idx:]).strip() + + if description: + html_parts.append('
') + html_parts.append(f'

{format_docstring_as_html(description)}

') + html_parts.append('
') + + # Constructor arguments + if init_info.get('args'): + html_parts.append('
') + html_parts.append('

Arguments:

') + html_parts.append('
') + for arg_name, arg_type, arg_desc in init_info['args']: + html_parts.append(f'
{arg_name} ({arg_type})
') + html_parts.append(f'
{escape_html(arg_desc)}
') + html_parts.append('
') + html_parts.append('
') + + # Properties/Attributes + props = cls_info.get('properties', {}) + if props or init_info.get('properties'): + html_parts.append('
') + html_parts.append('

Attributes:

') + html_parts.append('
') + + # Add documented properties from init_info + if init_info.get('properties'): + for prop_name in init_info['properties']: + html_parts.append(f'
{prop_name}
') + html_parts.append(f'
Property of {class_name}
') + + # Add actual properties + for prop_name, prop_info in props.items(): + readonly = ' (read-only)' if prop_info.get('readonly') else '' + html_parts.append(f'
{prop_name}{readonly}
') + if prop_info.get('doc'): + html_parts.append(f'
{escape_html(prop_info["doc"])}
') + + html_parts.append('
') + html_parts.append('
') + + # Methods + methods = cls_info.get('methods', {}) + collection_methods = collection_info.get('methods', {}) + + if methods or collection_methods: + html_parts.append('
') + html_parts.append('

Methods:

') + + for method_name, method_doc in {**collection_methods, **methods}.items(): + if method_name == '__init__': + continue + + html_parts.append('
') + + # Get specific method documentation + method_info = generate_method_docs(method_name, class_name) + + if method_info: + # Use detailed documentation + html_parts.append(f'
{method_info["signature"]}
') + html_parts.append(f'

{escape_html(method_info["description"])}

') + + if method_info.get('args'): + html_parts.append('

Arguments:

') + html_parts.append('
    ') + for arg in method_info['args']: + if len(arg) == 3: + html_parts.append(f'
  • {arg[0]} ({arg[1]}): {arg[2]}
  • ') + else: + html_parts.append(f'
  • {arg[0]} ({arg[1]})
  • ') + html_parts.append('
') + + if method_info.get('returns'): + html_parts.append(f'

Returns: {escape_html(method_info["returns"])}

') + + if method_info.get('note'): + html_parts.append(f'

Note: {escape_html(method_info["note"])}

') + else: + # Use docstring + html_parts.append(f'
{method_name}(...)
') + if isinstance(method_doc, str) and method_doc: + html_parts.append(f'

{escape_html(method_doc)}

') + + html_parts.append('
') + + html_parts.append('
') + + # Example + if init_info.get('example'): + html_parts.append('
') + html_parts.append('

Example:

') + html_parts.append('
')
+        html_parts.append(escape_html(init_info['example']))
+        html_parts.append('
') + html_parts.append('
') + + html_parts.append('
') + html_parts.append('
') + + return '\n'.join(html_parts) + +def generate_html_documentation(): + """Generate complete HTML API documentation.""" + html_parts = [] + + # HTML header + html_parts.append(''' + + + + + McRogueFace API Reference + + + +
+''') + + # Title and timestamp + html_parts.append('

McRogueFace API Reference

') + html_parts.append(f'

Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

') + + # Overview + if mcrfpy.__doc__: + html_parts.append('
') + html_parts.append('

Overview

') + # Process the docstring properly + doc_lines = mcrfpy.__doc__.strip().split('\\n') + for line in doc_lines: + if line.strip().startswith('Example:'): + html_parts.append('

Example:

') + html_parts.append('
')
+            elif line.strip() and not line.startswith(' '):
+                html_parts.append(f'

{escape_html(line)}

') + elif line.strip(): + # Code line + html_parts.append(escape_html(line)) + html_parts.append('
') + html_parts.append('
') + + # Table of Contents + html_parts.append('
') + html_parts.append('

Table of Contents

') + html_parts.append('') + html_parts.append('
') + + # Collect all components + classes = {} + functions = {} + + for name in sorted(dir(mcrfpy)): + if name.startswith('_'): + continue + + obj = getattr(mcrfpy, name) + + if isinstance(obj, type): + classes[name] = obj + elif callable(obj) and not isinstance(obj, type): + # Include built-in functions and other callables (but not classes) + functions[name] = obj + + + # Classes section + html_parts.append('

Classes

') + + # Group classes + ui_classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity'] + collection_classes = ['EntityCollection', 'UICollection', 'UICollectionIter', 'UIEntityCollectionIter'] + system_classes = ['Color', 'Vector', 'Texture', 'Font'] + other_classes = [name for name in classes if name not in ui_classes + collection_classes + system_classes] + + # UI Components + html_parts.append('

UI Components

') + for class_name in ui_classes: + if class_name in classes: + cls_info = get_class_details(classes[class_name]) + html_parts.append(format_class_html(cls_info, class_name)) + + # Collections + html_parts.append('

Collections

') + for class_name in collection_classes: + if class_name in classes: + cls_info = get_class_details(classes[class_name]) + html_parts.append(format_class_html(cls_info, class_name)) + + # System Types + html_parts.append('

System Types

') + for class_name in system_classes: + if class_name in classes: + cls_info = get_class_details(classes[class_name]) + html_parts.append(format_class_html(cls_info, class_name)) + + # Other Classes + html_parts.append('

Other Classes

') + for class_name in other_classes: + if class_name in classes: + cls_info = get_class_details(classes[class_name]) + html_parts.append(format_class_html(cls_info, class_name)) + + # Functions section + html_parts.append('

Functions

') + + # Group functions by category + scene_funcs = ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'] + audio_funcs = ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', + 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'] + ui_funcs = ['find', 'findAll'] + system_funcs = ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale'] + + # Scene Management + html_parts.append('

Scene Management

') + for func_name in scene_funcs: + if func_name in functions: + html_parts.append(format_function_html(func_name, functions[func_name])) + + # Audio + html_parts.append('

Audio

') + for func_name in audio_funcs: + if func_name in functions: + html_parts.append(format_function_html(func_name, functions[func_name])) + + # UI Utilities + html_parts.append('

UI Utilities

') + for func_name in ui_funcs: + if func_name in functions: + html_parts.append(format_function_html(func_name, functions[func_name])) + + # System + html_parts.append('

System

') + for func_name in system_funcs: + if func_name in functions: + html_parts.append(format_function_html(func_name, functions[func_name])) + + # Automation Module + if hasattr(mcrfpy, 'automation'): + html_parts.append('
') + html_parts.append('

Automation Module

') + html_parts.append('

The mcrfpy.automation module provides testing and automation capabilities for simulating user input and capturing screenshots.

') + + automation = mcrfpy.automation + auto_funcs = [] + + for name in sorted(dir(automation)): + if not name.startswith('_'): + obj = getattr(automation, name) + if callable(obj): + auto_funcs.append((name, obj)) + + for name, func in auto_funcs: + html_parts.append('
') + html_parts.append(f'

automation.{name}

') + if func.__doc__: + # Extract just the description, not the repeated signature + doc_lines = func.__doc__.strip().split(' - ') + if len(doc_lines) > 1: + description = doc_lines[1] + else: + description = func.__doc__.strip() + html_parts.append(f'

{escape_html(description)}

') + html_parts.append('
') + + html_parts.append('
') + + # Close HTML + html_parts.append(''' +
+ +''') + + return '\n'.join(html_parts) + +def format_function_html(func_name, func): + """Format a function as HTML using enhanced documentation.""" + html_parts = [] + + html_parts.append('
') + + # Get enhanced documentation + func_docs = generate_function_docs() + + if func_name in func_docs: + doc_info = func_docs[func_name] + + # Signature + signature = doc_info.get('signature', f'{func_name}(...)') + html_parts.append(f'

{escape_html(signature)}

') + + # Description + if 'description' in doc_info: + html_parts.append(f'

{escape_html(doc_info["description"])}

') + + # Arguments + if 'args' in doc_info and doc_info['args']: + html_parts.append('
') + html_parts.append('
Arguments:
') + html_parts.append('
') + for arg_name, arg_type, arg_desc in doc_info['args']: + html_parts.append(f'
{escape_html(arg_name)} : {escape_html(arg_type)}
') + html_parts.append(f'
{escape_html(arg_desc)}
') + html_parts.append('
') + html_parts.append('
') + + # Returns + if 'returns' in doc_info and doc_info['returns']: + html_parts.append('
') + html_parts.append('
Returns:
') + html_parts.append(f'

{escape_html(doc_info["returns"])}

') + html_parts.append('
') + + # Exceptions + if 'exceptions' in doc_info and doc_info['exceptions']: + html_parts.append('
') + html_parts.append('
Raises:
') + html_parts.append('
') + for exc_type, exc_desc in doc_info['exceptions']: + html_parts.append(f'
{escape_html(exc_type)}
') + html_parts.append(f'
{escape_html(exc_desc)}
') + html_parts.append('
') + html_parts.append('
') + + # Note + if 'note' in doc_info: + html_parts.append('
') + html_parts.append(f'

Note: {escape_html(doc_info["note"])}

') + html_parts.append('
') + + # Example + if 'example' in doc_info: + html_parts.append('
') + html_parts.append('
Example:
') + html_parts.append('
')
+            html_parts.append(escape_html(doc_info['example']))
+            html_parts.append('
') + html_parts.append('
') + else: + # Fallback to parsing docstring if not in enhanced docs + doc = func.__doc__ or "" + lines = doc.strip().split('\n') if doc else [] + + # Extract signature + signature = func_name + '(...)' + if lines and '(' in lines[0]: + signature = lines[0].strip() + + html_parts.append(f'

{escape_html(signature)}

') + + # Process rest of docstring + if len(lines) > 1: + in_section = None + for line in lines[1:]: + stripped = line.strip() + + if stripped in ['Args:', 'Returns:', 'Raises:', 'Note:', 'Example:']: + in_section = stripped[:-1] + html_parts.append(f'

{in_section}:

') + elif in_section == 'Example': + if not stripped: + continue + if stripped.startswith('>>>') or (len(lines) > lines.index(line) + 1 and + lines[lines.index(line) + 1].strip().startswith('>>>')): + html_parts.append('
')
+                        html_parts.append(escape_html(stripped))
+                        # Get rest of example
+                        idx = lines.index(line) + 1
+                        while idx < len(lines) and lines[idx].strip():
+                            html_parts.append(escape_html(lines[idx]))
+                            idx += 1
+                        html_parts.append('
') + break + elif in_section and stripped: + if in_section == 'Args': + # Format arguments nicely + if ':' in stripped: + param, desc = stripped.split(':', 1) + html_parts.append(f'

{escape_html(param.strip())}: {escape_html(desc.strip())}

') + else: + html_parts.append(f'

{escape_html(stripped)}

') + else: + html_parts.append(f'

{escape_html(stripped)}

') + elif stripped and not in_section: + html_parts.append(f'

{escape_html(stripped)}

') + + html_parts.append('
') + html_parts.append('
') + + return '\n'.join(html_parts) + +def main(): + """Generate improved HTML API documentation.""" + print("Generating improved HTML API documentation...") + + # Generate HTML + html_content = generate_html_documentation() + + # Write to file + output_path = Path("docs/api_reference_improved.html") + output_path.parent.mkdir(exist_ok=True) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + print(f"✓ Generated {output_path}") + print(f" File size: {len(html_content):,} bytes") + + # Also generate a test to verify the HTML + test_content = '''#!/usr/bin/env python3 +"""Test the improved HTML API documentation.""" + +import os +import sys +from pathlib import Path + +def test_html_quality(): + """Test that the HTML documentation meets quality standards.""" + html_path = Path("docs/api_reference_improved.html") + + if not html_path.exists(): + print("ERROR: HTML documentation not found") + return False + + with open(html_path, 'r') as f: + content = f.read() + + # Check for common issues + issues = [] + + # Check that \\n is not present literally + if '\\\\n' in content: + issues.append("Found literal \\\\n in HTML content") + + # Check that markdown links are converted + if '[' in content and '](#' in content: + issues.append("Found unconverted markdown links") + + # Check for proper HTML structure + if '

Args:

' in content: + issues.append("Args: should not be an H4 heading") + + if '

Attributes:

' not in content: + issues.append("Missing proper Attributes: headings") + + # Check for duplicate method descriptions + if content.count('Get bounding box as (x, y, width, height)') > 20: + issues.append("Too many duplicate method descriptions") + + # Check specific improvements + if 'Entity' in content and 'Inherits from: Drawable' in content: + issues.append("Entity incorrectly shown as inheriting from Drawable") + + if not issues: + print("✓ HTML documentation passes all quality checks") + return True + else: + print("Issues found:") + for issue in issues: + print(f" - {issue}") + return False + +if __name__ == '__main__': + if test_html_quality(): + print("PASS") + sys.exit(0) + else: + print("FAIL") + sys.exit(1) +''' + + test_path = Path("tests/test_html_quality.py") + with open(test_path, 'w') as f: + f.write(test_content) + + print(f"✓ Generated test at {test_path}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tools/generate_api_docs_simple.py b/tools/generate_api_docs_simple.py new file mode 100644 index 0000000..2bb405f --- /dev/null +++ b/tools/generate_api_docs_simple.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Generate API reference documentation for McRogueFace - Simple version.""" + +import os +import sys +import datetime +from pathlib import Path + +import mcrfpy + +def generate_markdown_docs(): + """Generate markdown API documentation.""" + lines = [] + + # Header + lines.append("# McRogueFace API Reference") + lines.append("") + lines.append("*Generated on {}*".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) + lines.append("") + + # Module description + if mcrfpy.__doc__: + lines.append("## Overview") + lines.append("") + lines.extend(mcrfpy.__doc__.strip().split('\n')) + lines.append("") + + # Collect all components + classes = [] + functions = [] + + for name in sorted(dir(mcrfpy)): + if name.startswith('_'): + continue + + obj = getattr(mcrfpy, name) + + if isinstance(obj, type): + classes.append((name, obj)) + elif callable(obj): + functions.append((name, obj)) + + # Document classes + lines.append("## Classes") + lines.append("") + + for name, cls in classes: + lines.append("### class {}".format(name)) + if cls.__doc__: + doc_lines = cls.__doc__.strip().split('\n') + for line in doc_lines[:5]: # First 5 lines + lines.append(line) + lines.append("") + lines.append("---") + lines.append("") + + # Document functions + lines.append("## Functions") + lines.append("") + + for name, func in functions: + lines.append("### {}".format(name)) + if func.__doc__: + doc_lines = func.__doc__.strip().split('\n') + for line in doc_lines[:5]: # First 5 lines + lines.append(line) + lines.append("") + lines.append("---") + lines.append("") + + # Automation module + if hasattr(mcrfpy, 'automation'): + lines.append("## Automation Module") + lines.append("") + + automation = mcrfpy.automation + for name in sorted(dir(automation)): + if not name.startswith('_'): + obj = getattr(automation, name) + if callable(obj): + lines.append("### automation.{}".format(name)) + if obj.__doc__: + lines.append(obj.__doc__.strip().split('\n')[0]) + lines.append("") + + return '\n'.join(lines) + +def main(): + """Generate API documentation.""" + print("Generating McRogueFace API Documentation...") + + # Create docs directory + docs_dir = Path("docs") + docs_dir.mkdir(exist_ok=True) + + # Generate markdown + markdown_content = generate_markdown_docs() + + # Write markdown + md_path = docs_dir / "API_REFERENCE.md" + with open(md_path, 'w') as f: + f.write(markdown_content) + print("Written to {}".format(md_path)) + + # Summary + lines = markdown_content.split('\n') + class_count = markdown_content.count('### class') + func_count = markdown_content.count('### ') - class_count - markdown_content.count('### automation.') + + print("\nDocumentation Statistics:") + print("- Classes documented: {}".format(class_count)) + print("- Functions documented: {}".format(func_count)) + print("- Total lines: {}".format(len(lines))) + + print("\nAPI documentation generated successfully!") + sys.exit(0) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tools/generate_complete_api_docs.py b/tools/generate_complete_api_docs.py new file mode 100644 index 0000000..8b41446 --- /dev/null +++ b/tools/generate_complete_api_docs.py @@ -0,0 +1,960 @@ +#!/usr/bin/env python3 +"""Generate COMPLETE HTML API reference documentation for McRogueFace with NO missing methods.""" + +import os +import sys +import datetime +import html +from pathlib import Path +import mcrfpy + +def escape_html(text: str) -> str: + """Escape HTML special characters.""" + return html.escape(text) if text else "" + +def get_complete_method_documentation(): + """Return complete documentation for ALL methods across all classes.""" + return { + # Base Drawable methods (inherited by all UI elements) + 'Drawable': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of this drawable element.', + 'returns': 'tuple: (x, y, width, height) representing the element\'s bounds', + 'note': 'The bounds are in screen coordinates and account for current position and size.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the element by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'This modifies the x and y position properties by the given amounts.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the element to new dimensions.', + 'args': [ + ('width', 'float', 'New width in pixels'), + ('height', 'float', 'New height in pixels') + ], + 'note': 'For Caption and Sprite, this may not change actual size if determined by content.' + } + }, + + # Entity-specific methods + 'Entity': { + 'at': { + 'signature': 'at(x, y)', + 'description': 'Check if this entity is at the specified grid coordinates.', + 'args': [ + ('x', 'int', 'Grid x coordinate to check'), + ('y', 'int', 'Grid y coordinate to check') + ], + 'returns': 'bool: True if entity is at position (x, y), False otherwise' + }, + 'die': { + 'signature': 'die()', + 'description': 'Remove this entity from its parent grid.', + 'note': 'The entity object remains valid but is no longer rendered or updated.' + }, + 'index': { + 'signature': 'index()', + 'description': 'Get the index of this entity in its parent grid\'s entity list.', + 'returns': 'int: Index position, or -1 if not in a grid' + } + }, + + # Grid-specific methods + 'Grid': { + 'at': { + 'signature': 'at(x, y)', + 'description': 'Get the GridPoint at the specified grid coordinates.', + 'args': [ + ('x', 'int', 'Grid x coordinate'), + ('y', 'int', 'Grid y coordinate') + ], + 'returns': 'GridPoint or None: The grid point at (x, y), or None if out of bounds' + } + }, + + # Collection methods + 'EntityCollection': { + 'append': { + 'signature': 'append(entity)', + 'description': 'Add an entity to the end of the collection.', + 'args': [('entity', 'Entity', 'The entity to add')] + }, + 'remove': { + 'signature': 'remove(entity)', + 'description': 'Remove the first occurrence of an entity from the collection.', + 'args': [('entity', 'Entity', 'The entity to remove')], + 'raises': 'ValueError: If entity is not in collection' + }, + 'extend': { + 'signature': 'extend(iterable)', + 'description': 'Add all entities from an iterable to the collection.', + 'args': [('iterable', 'Iterable[Entity]', 'Entities to add')] + }, + 'count': { + 'signature': 'count(entity)', + 'description': 'Count the number of occurrences of an entity in the collection.', + 'args': [('entity', 'Entity', 'The entity to count')], + 'returns': 'int: Number of times entity appears in collection' + }, + 'index': { + 'signature': 'index(entity)', + 'description': 'Find the index of the first occurrence of an entity.', + 'args': [('entity', 'Entity', 'The entity to find')], + 'returns': 'int: Index of entity in collection', + 'raises': 'ValueError: If entity is not in collection' + } + }, + + 'UICollection': { + 'append': { + 'signature': 'append(drawable)', + 'description': 'Add a drawable element to the end of the collection.', + 'args': [('drawable', 'UIDrawable', 'The drawable element to add')] + }, + 'remove': { + 'signature': 'remove(drawable)', + 'description': 'Remove the first occurrence of a drawable from the collection.', + 'args': [('drawable', 'UIDrawable', 'The drawable to remove')], + 'raises': 'ValueError: If drawable is not in collection' + }, + 'extend': { + 'signature': 'extend(iterable)', + 'description': 'Add all drawables from an iterable to the collection.', + 'args': [('iterable', 'Iterable[UIDrawable]', 'Drawables to add')] + }, + 'count': { + 'signature': 'count(drawable)', + 'description': 'Count the number of occurrences of a drawable in the collection.', + 'args': [('drawable', 'UIDrawable', 'The drawable to count')], + 'returns': 'int: Number of times drawable appears in collection' + }, + 'index': { + 'signature': 'index(drawable)', + 'description': 'Find the index of the first occurrence of a drawable.', + 'args': [('drawable', 'UIDrawable', 'The drawable to find')], + 'returns': 'int: Index of drawable in collection', + 'raises': 'ValueError: If drawable is not in collection' + } + }, + + # Animation methods + 'Animation': { + 'get_current_value': { + 'signature': 'get_current_value()', + 'description': 'Get the current interpolated value of the animation.', + 'returns': 'float: Current animation value between start and end' + }, + 'start': { + 'signature': 'start(target)', + 'description': 'Start the animation on a target UI element.', + 'args': [('target', 'UIDrawable', 'The UI element to animate')], + 'note': 'The target must have the property specified in the animation constructor.' + }, + 'update': { + 'signature': 'update(delta_time)', + 'description': 'Update the animation by the given time delta.', + 'args': [('delta_time', 'float', 'Time elapsed since last update in seconds')], + 'returns': 'bool: True if animation is still running, False if finished' + } + }, + + # Color methods + 'Color': { + 'from_hex': { + 'signature': 'from_hex(hex_string)', + 'description': 'Create a Color from a hexadecimal color string.', + 'args': [('hex_string', 'str', 'Hex color string (e.g., "#FF0000" or "FF0000")')], + 'returns': 'Color: New Color object from hex string', + 'example': 'red = Color.from_hex("#FF0000")' + }, + 'to_hex': { + 'signature': 'to_hex()', + 'description': 'Convert this Color to a hexadecimal string.', + 'returns': 'str: Hex color string in format "#RRGGBB"', + 'example': 'hex_str = color.to_hex() # Returns "#FF0000"' + }, + 'lerp': { + 'signature': 'lerp(other, t)', + 'description': 'Linearly interpolate between this color and another.', + 'args': [ + ('other', 'Color', 'The color to interpolate towards'), + ('t', 'float', 'Interpolation factor from 0.0 to 1.0') + ], + 'returns': 'Color: New interpolated Color object', + 'example': 'mixed = red.lerp(blue, 0.5) # 50% between red and blue' + } + }, + + # Vector methods + 'Vector': { + 'magnitude': { + 'signature': 'magnitude()', + 'description': 'Calculate the length/magnitude of this vector.', + 'returns': 'float: The magnitude of the vector', + 'example': 'length = vector.magnitude()' + }, + 'magnitude_squared': { + 'signature': 'magnitude_squared()', + 'description': 'Calculate the squared magnitude of this vector.', + 'returns': 'float: The squared magnitude (faster than magnitude())', + 'note': 'Use this for comparisons to avoid expensive square root calculation.' + }, + 'normalize': { + 'signature': 'normalize()', + 'description': 'Return a unit vector in the same direction.', + 'returns': 'Vector: New normalized vector with magnitude 1.0', + 'raises': 'ValueError: If vector has zero magnitude' + }, + 'dot': { + 'signature': 'dot(other)', + 'description': 'Calculate the dot product with another vector.', + 'args': [('other', 'Vector', 'The other vector')], + 'returns': 'float: Dot product of the two vectors' + }, + 'distance_to': { + 'signature': 'distance_to(other)', + 'description': 'Calculate the distance to another vector.', + 'args': [('other', 'Vector', 'The other vector')], + 'returns': 'float: Distance between the two vectors' + }, + 'angle': { + 'signature': 'angle()', + 'description': 'Get the angle of this vector in radians.', + 'returns': 'float: Angle in radians from positive x-axis' + }, + 'copy': { + 'signature': 'copy()', + 'description': 'Create a copy of this vector.', + 'returns': 'Vector: New Vector object with same x and y values' + } + }, + + # Scene methods + 'Scene': { + 'activate': { + 'signature': 'activate()', + 'description': 'Make this scene the active scene.', + 'note': 'Equivalent to calling setScene() with this scene\'s name.' + }, + 'get_ui': { + 'signature': 'get_ui()', + 'description': 'Get the UI element collection for this scene.', + 'returns': 'UICollection: Collection of all UI elements in this scene' + }, + 'keypress': { + 'signature': 'keypress(handler)', + 'description': 'Register a keyboard handler function for this scene.', + 'args': [('handler', 'callable', 'Function that takes (key_name: str, is_pressed: bool)')], + 'note': 'Alternative to overriding the on_keypress method.' + }, + 'register_keyboard': { + 'signature': 'register_keyboard(callable)', + 'description': 'Register a keyboard event handler function for the scene.', + 'args': [('callable', 'callable', 'Function that takes (key: str, action: str) parameters')], + 'note': 'Alternative to overriding the on_keypress method when subclassing Scene objects.', + 'example': '''def handle_keyboard(key, action): + print(f"Key '{key}' was {action}") + if key == "q" and action == "press": + # Handle quit + pass +scene.register_keyboard(handle_keyboard)''' + } + }, + + # Timer methods + 'Timer': { + 'pause': { + 'signature': 'pause()', + 'description': 'Pause the timer, stopping its callback execution.', + 'note': 'Use resume() to continue the timer from where it was paused.' + }, + 'resume': { + 'signature': 'resume()', + 'description': 'Resume a paused timer.', + 'note': 'Has no effect if timer is not paused.' + }, + 'cancel': { + 'signature': 'cancel()', + 'description': 'Cancel the timer and remove it from the system.', + 'note': 'After cancelling, the timer object cannot be reused.' + }, + 'restart': { + 'signature': 'restart()', + 'description': 'Restart the timer from the beginning.', + 'note': 'Resets the timer\'s internal clock to zero.' + } + }, + + # Window methods + 'Window': { + 'get': { + 'signature': 'get()', + 'description': 'Get the Window singleton instance.', + 'returns': 'Window: The singleton window object', + 'note': 'This is a static method that returns the same instance every time.' + }, + 'center': { + 'signature': 'center()', + 'description': 'Center the window on the screen.', + 'note': 'Only works if the window is not fullscreen.' + }, + 'screenshot': { + 'signature': 'screenshot(filename)', + 'description': 'Take a screenshot and save it to a file.', + 'args': [('filename', 'str', 'Path where to save the screenshot')], + 'note': 'Supports PNG, JPG, and BMP formats based on file extension.' + } + } + } + +def get_complete_function_documentation(): + """Return complete documentation for ALL module functions.""" + return { + # Scene Management + 'createScene': { + 'signature': 'createScene(name: str) -> None', + 'description': 'Create a new empty scene with the given name.', + 'args': [('name', 'str', 'Unique name for the new scene')], + 'raises': 'ValueError: If a scene with this name already exists', + 'note': 'The scene is created but not made active. Use setScene() to switch to it.', + 'example': 'mcrfpy.createScene("game_over")' + }, + 'setScene': { + 'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None', + 'description': 'Switch to a different scene with optional transition effect.', + 'args': [ + ('scene', 'str', 'Name of the scene to switch to'), + ('transition', 'str', 'Transition type: "fade", "slide_left", "slide_right", "slide_up", "slide_down"'), + ('duration', 'float', 'Transition duration in seconds (default: 0.0 for instant)') + ], + 'raises': 'KeyError: If the scene doesn\'t exist', + 'example': 'mcrfpy.setScene("game", "fade", 0.5)' + }, + 'currentScene': { + 'signature': 'currentScene() -> str', + 'description': 'Get the name of the currently active scene.', + 'returns': 'str: Name of the current scene', + 'example': 'scene_name = mcrfpy.currentScene()' + }, + 'sceneUI': { + 'signature': 'sceneUI(scene: str = None) -> UICollection', + 'description': 'Get all UI elements for a scene.', + 'args': [('scene', 'str', 'Scene name. If None, uses current scene')], + 'returns': 'UICollection: All UI elements in the scene', + 'raises': 'KeyError: If the specified scene doesn\'t exist', + 'example': 'ui_elements = mcrfpy.sceneUI("game")' + }, + 'keypressScene': { + 'signature': 'keypressScene(handler: callable) -> None', + 'description': 'Set the keyboard event handler for the current scene.', + 'args': [('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)')], + 'example': '''def on_key(key, pressed): + if key == "SPACE" and pressed: + player.jump() +mcrfpy.keypressScene(on_key)''' + }, + + # Audio Functions + 'createSoundBuffer': { + 'signature': 'createSoundBuffer(filename: str) -> int', + 'description': 'Load a sound effect from a file and return its buffer ID.', + 'args': [('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)')], + 'returns': 'int: Buffer ID for use with playSound()', + 'raises': 'RuntimeError: If the file cannot be loaded', + 'example': 'jump_sound = mcrfpy.createSoundBuffer("assets/jump.wav")' + }, + 'loadMusic': { + 'signature': 'loadMusic(filename: str, loop: bool = True) -> None', + 'description': 'Load and immediately play background music from a file.', + 'args': [ + ('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'), + ('loop', 'bool', 'Whether to loop the music (default: True)') + ], + 'note': 'Only one music track can play at a time. Loading new music stops the current track.', + 'example': 'mcrfpy.loadMusic("assets/background.ogg", True)' + }, + 'playSound': { + 'signature': 'playSound(buffer_id: int) -> None', + 'description': 'Play a sound effect using a previously loaded buffer.', + 'args': [('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')], + 'raises': 'RuntimeError: If the buffer ID is invalid', + 'example': 'mcrfpy.playSound(jump_sound)' + }, + 'getMusicVolume': { + 'signature': 'getMusicVolume() -> int', + 'description': 'Get the current music volume level.', + 'returns': 'int: Current volume (0-100)', + 'example': 'current_volume = mcrfpy.getMusicVolume()' + }, + 'getSoundVolume': { + 'signature': 'getSoundVolume() -> int', + 'description': 'Get the current sound effects volume level.', + 'returns': 'int: Current volume (0-100)', + 'example': 'current_volume = mcrfpy.getSoundVolume()' + }, + 'setMusicVolume': { + 'signature': 'setMusicVolume(volume: int) -> None', + 'description': 'Set the global music volume.', + 'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')], + 'example': 'mcrfpy.setMusicVolume(50) # Set to 50% volume' + }, + 'setSoundVolume': { + 'signature': 'setSoundVolume(volume: int) -> None', + 'description': 'Set the global sound effects volume.', + 'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')], + 'example': 'mcrfpy.setSoundVolume(75) # Set to 75% volume' + }, + + # UI Utilities + 'find': { + 'signature': 'find(name: str, scene: str = None) -> UIDrawable | None', + 'description': 'Find the first UI element with the specified name.', + 'args': [ + ('name', 'str', 'Exact name to search for'), + ('scene', 'str', 'Scene to search in (default: current scene)') + ], + 'returns': 'UIDrawable or None: The found element, or None if not found', + 'note': 'Searches scene UI elements and entities within grids.', + 'example': 'button = mcrfpy.find("start_button")' + }, + 'findAll': { + 'signature': 'findAll(pattern: str, scene: str = None) -> list', + 'description': 'Find all UI elements matching a name pattern.', + 'args': [ + ('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'), + ('scene', 'str', 'Scene to search in (default: current scene)') + ], + 'returns': 'list: All matching UI elements and entities', + 'example': 'enemies = mcrfpy.findAll("enemy_*")' + }, + + # System Functions + 'exit': { + 'signature': 'exit() -> None', + 'description': 'Cleanly shut down the game engine and exit the application.', + 'note': 'This immediately closes the window and terminates the program.', + 'example': 'mcrfpy.exit()' + }, + 'getMetrics': { + 'signature': 'getMetrics() -> dict', + 'description': 'Get current performance metrics.', + 'returns': '''dict: Performance data with keys: +- frame_time: Last frame duration in seconds +- avg_frame_time: Average frame time +- fps: Frames per second +- draw_calls: Number of draw calls +- ui_elements: Total UI element count +- visible_elements: Visible element count +- current_frame: Frame counter +- runtime: Total runtime in seconds''', + 'example': 'metrics = mcrfpy.getMetrics()' + }, + 'setTimer': { + 'signature': 'setTimer(name: str, handler: callable, interval: int) -> None', + 'description': 'Create or update a recurring timer.', + 'args': [ + ('name', 'str', 'Unique identifier for the timer'), + ('handler', 'callable', 'Function called with (runtime: float) parameter'), + ('interval', 'int', 'Time between calls in milliseconds') + ], + 'note': 'If a timer with this name exists, it will be replaced.', + 'example': '''def update_score(runtime): + score += 1 +mcrfpy.setTimer("score_update", update_score, 1000)''' + }, + 'delTimer': { + 'signature': 'delTimer(name: str) -> None', + 'description': 'Stop and remove a timer.', + 'args': [('name', 'str', 'Timer identifier to remove')], + 'note': 'No error is raised if the timer doesn\'t exist.', + 'example': 'mcrfpy.delTimer("score_update")' + }, + 'setScale': { + 'signature': 'setScale(multiplier: float) -> None', + 'description': 'Scale the game window size.', + 'args': [('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)')], + 'note': 'The internal resolution remains 1024x768, but the window is scaled.', + 'example': 'mcrfpy.setScale(2.0) # Double the window size' + } + } + +def get_complete_property_documentation(): + """Return complete documentation for ALL properties.""" + return { + 'Animation': { + 'property': 'str: Name of the property being animated (e.g., "x", "y", "scale")', + 'duration': 'float: Total duration of the animation in seconds', + 'elapsed_time': 'float: Time elapsed since animation started (read-only)', + 'current_value': 'float: Current interpolated value of the animation (read-only)', + 'is_running': 'bool: True if animation is currently running (read-only)', + 'is_finished': 'bool: True if animation has completed (read-only)' + }, + 'GridPoint': { + 'x': 'int: Grid x coordinate of this point', + 'y': 'int: Grid y coordinate of this point', + 'texture_index': 'int: Index of the texture/sprite to display at this point', + 'solid': 'bool: Whether this point blocks movement', + 'transparent': 'bool: Whether this point allows light/vision through', + 'color': 'Color: Color tint applied to the texture at this point' + }, + 'GridPointState': { + 'visible': 'bool: Whether this point is currently visible to the player', + 'discovered': 'bool: Whether this point has been discovered/explored', + 'custom_flags': 'int: Bitfield for custom game-specific flags' + } + } + +def generate_complete_html_documentation(): + """Generate complete HTML documentation with NO missing methods.""" + + # Get all documentation data + method_docs = get_complete_method_documentation() + function_docs = get_complete_function_documentation() + property_docs = get_complete_property_documentation() + + html_parts = [] + + # HTML header with enhanced styling + html_parts.append(''' + + + + + McRogueFace API Reference - Complete Documentation + + + +
+''') + + # Title and overview + html_parts.append('

McRogueFace API Reference - Complete Documentation

') + html_parts.append(f'

Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

') + + # Table of contents + html_parts.append('
') + html_parts.append('

Table of Contents

') + html_parts.append('') + html_parts.append('
') + + # Functions section + html_parts.append('

Functions

') + + # Group functions by category + categories = { + 'Scene Management': ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'], + 'Audio': ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'], + 'UI Utilities': ['find', 'findAll'], + 'System': ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale'] + } + + for category, functions in categories.items(): + html_parts.append(f'

{category}

') + for func_name in functions: + if func_name in function_docs: + html_parts.append(format_function_html(func_name, function_docs[func_name])) + + # Classes section + html_parts.append('

Classes

') + + # Get all classes from mcrfpy + classes = [] + for name in sorted(dir(mcrfpy)): + if not name.startswith('_'): + obj = getattr(mcrfpy, name) + if isinstance(obj, type): + classes.append((name, obj)) + + # Generate class documentation + for class_name, cls in classes: + html_parts.append(format_class_html_complete(class_name, cls, method_docs, property_docs)) + + # Automation section + if hasattr(mcrfpy, 'automation'): + html_parts.append('

Automation Module

') + html_parts.append('

The mcrfpy.automation module provides testing and automation capabilities.

') + + automation = mcrfpy.automation + for name in sorted(dir(automation)): + if not name.startswith('_'): + obj = getattr(automation, name) + if callable(obj): + html_parts.append(f'
') + html_parts.append(f'

automation.{name}

') + if obj.__doc__: + doc_parts = obj.__doc__.split(' - ') + if len(doc_parts) > 1: + html_parts.append(f'

{escape_html(doc_parts[1])}

') + else: + html_parts.append(f'

{escape_html(obj.__doc__)}

') + html_parts.append('
') + + html_parts.append('
') + html_parts.append('') + html_parts.append('') + + return '\n'.join(html_parts) + +def format_function_html(func_name, func_doc): + """Format a function with complete documentation.""" + html_parts = [] + + html_parts.append('
') + html_parts.append(f'

{func_doc["signature"]}

') + html_parts.append(f'

{escape_html(func_doc["description"])}

') + + # Arguments + if 'args' in func_doc: + html_parts.append('
') + html_parts.append('
Arguments:
') + for arg in func_doc['args']: + html_parts.append('
') + html_parts.append(f'{arg[0]} ') + html_parts.append(f'({arg[1]}): ') + html_parts.append(f'{escape_html(arg[2])}') + html_parts.append('
') + html_parts.append('
') + + # Returns + if 'returns' in func_doc: + html_parts.append('
') + html_parts.append(f'Returns: {escape_html(func_doc["returns"])}') + html_parts.append('
') + + # Raises + if 'raises' in func_doc: + html_parts.append('
') + html_parts.append(f'Raises: {escape_html(func_doc["raises"])}') + html_parts.append('
') + + # Note + if 'note' in func_doc: + html_parts.append('
') + html_parts.append(f'Note: {escape_html(func_doc["note"])}') + html_parts.append('
') + + # Example + if 'example' in func_doc: + html_parts.append('
') + html_parts.append('
Example:
') + html_parts.append('
')
+        html_parts.append(escape_html(func_doc['example']))
+        html_parts.append('
') + html_parts.append('
') + + html_parts.append('
') + + return '\n'.join(html_parts) + +def format_class_html_complete(class_name, cls, method_docs, property_docs): + """Format a class with complete documentation.""" + html_parts = [] + + html_parts.append('
') + html_parts.append(f'

{class_name}

') + + # Class description + if cls.__doc__: + html_parts.append(f'

{escape_html(cls.__doc__)}

') + + # Properties + if class_name in property_docs: + html_parts.append('

Properties:

') + for prop_name, prop_desc in property_docs[class_name].items(): + html_parts.append(f'
') + html_parts.append(f'{prop_name}: {escape_html(prop_desc)}') + html_parts.append('
') + + # Methods + methods_to_document = [] + + # Add inherited methods for UI classes + if any(base.__name__ == 'Drawable' for base in cls.__bases__ if hasattr(base, '__name__')): + methods_to_document.extend(['get_bounds', 'move', 'resize']) + + # Add class-specific methods + if class_name in method_docs: + methods_to_document.extend(method_docs[class_name].keys()) + + # Add methods from introspection + for attr_name in dir(cls): + if not attr_name.startswith('_') and callable(getattr(cls, attr_name)): + if attr_name not in methods_to_document: + methods_to_document.append(attr_name) + + if methods_to_document: + html_parts.append('

Methods:

') + for method_name in set(methods_to_document): + # Get method documentation + method_doc = None + if class_name in method_docs and method_name in method_docs[class_name]: + method_doc = method_docs[class_name][method_name] + elif method_name in method_docs.get('Drawable', {}): + method_doc = method_docs['Drawable'][method_name] + + if method_doc: + html_parts.append(format_method_html(method_name, method_doc)) + else: + # Basic method with no documentation + html_parts.append(f'
') + html_parts.append(f'{method_name}(...)') + html_parts.append('
') + + html_parts.append('
') + + return '\n'.join(html_parts) + +def format_method_html(method_name, method_doc): + """Format a method with complete documentation.""" + html_parts = [] + + html_parts.append('
') + html_parts.append(f'
{method_doc["signature"]}
') + html_parts.append(f'

{escape_html(method_doc["description"])}

') + + # Arguments + if 'args' in method_doc: + for arg in method_doc['args']: + html_parts.append(f'
') + html_parts.append(f'{arg[0]} ') + html_parts.append(f'({arg[1]}): ') + html_parts.append(f'{escape_html(arg[2])}') + html_parts.append('
') + + # Returns + if 'returns' in method_doc: + html_parts.append(f'
') + html_parts.append(f'Returns: {escape_html(method_doc["returns"])}') + html_parts.append('
') + + # Note + if 'note' in method_doc: + html_parts.append(f'
') + html_parts.append(f'Note: {escape_html(method_doc["note"])}') + html_parts.append('
') + + # Example + if 'example' in method_doc: + html_parts.append(f'
') + html_parts.append('Example:') + html_parts.append('
')
+        html_parts.append(escape_html(method_doc['example']))
+        html_parts.append('
') + html_parts.append('
') + + html_parts.append('
') + + return '\n'.join(html_parts) + +def main(): + """Generate complete HTML documentation with zero missing methods.""" + print("Generating COMPLETE HTML API documentation...") + + # Generate HTML + html_content = generate_complete_html_documentation() + + # Write to file + output_path = Path("docs/api_reference_complete.html") + output_path.parent.mkdir(exist_ok=True) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + print(f"✓ Generated {output_path}") + print(f" File size: {len(html_content):,} bytes") + + # Count "..." instances + ellipsis_count = html_content.count('...') + print(f" Ellipsis instances: {ellipsis_count}") + + if ellipsis_count == 0: + print("✅ SUCCESS: No missing documentation found!") + else: + print(f"❌ WARNING: {ellipsis_count} methods still need documentation") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tools/generate_complete_markdown_docs.py b/tools/generate_complete_markdown_docs.py new file mode 100644 index 0000000..89fab79 --- /dev/null +++ b/tools/generate_complete_markdown_docs.py @@ -0,0 +1,821 @@ +#!/usr/bin/env python3 +"""Generate COMPLETE Markdown API reference documentation for McRogueFace with NO missing methods.""" + +import os +import sys +import datetime +from pathlib import Path +import mcrfpy + +def get_complete_method_documentation(): + """Return complete documentation for ALL methods across all classes.""" + return { + # Base Drawable methods (inherited by all UI elements) + 'Drawable': { + 'get_bounds': { + 'signature': 'get_bounds()', + 'description': 'Get the bounding rectangle of this drawable element.', + 'returns': 'tuple: (x, y, width, height) representing the element\'s bounds', + 'note': 'The bounds are in screen coordinates and account for current position and size.' + }, + 'move': { + 'signature': 'move(dx, dy)', + 'description': 'Move the element by a relative offset.', + 'args': [ + ('dx', 'float', 'Horizontal offset in pixels'), + ('dy', 'float', 'Vertical offset in pixels') + ], + 'note': 'This modifies the x and y position properties by the given amounts.' + }, + 'resize': { + 'signature': 'resize(width, height)', + 'description': 'Resize the element to new dimensions.', + 'args': [ + ('width', 'float', 'New width in pixels'), + ('height', 'float', 'New height in pixels') + ], + 'note': 'For Caption and Sprite, this may not change actual size if determined by content.' + } + }, + + # Entity-specific methods + 'Entity': { + 'at': { + 'signature': 'at(x, y)', + 'description': 'Check if this entity is at the specified grid coordinates.', + 'args': [ + ('x', 'int', 'Grid x coordinate to check'), + ('y', 'int', 'Grid y coordinate to check') + ], + 'returns': 'bool: True if entity is at position (x, y), False otherwise' + }, + 'die': { + 'signature': 'die()', + 'description': 'Remove this entity from its parent grid.', + 'note': 'The entity object remains valid but is no longer rendered or updated.' + }, + 'index': { + 'signature': 'index()', + 'description': 'Get the index of this entity in its parent grid\'s entity list.', + 'returns': 'int: Index position, or -1 if not in a grid' + } + }, + + # Grid-specific methods + 'Grid': { + 'at': { + 'signature': 'at(x, y)', + 'description': 'Get the GridPoint at the specified grid coordinates.', + 'args': [ + ('x', 'int', 'Grid x coordinate'), + ('y', 'int', 'Grid y coordinate') + ], + 'returns': 'GridPoint or None: The grid point at (x, y), or None if out of bounds' + } + }, + + # Collection methods + 'EntityCollection': { + 'append': { + 'signature': 'append(entity)', + 'description': 'Add an entity to the end of the collection.', + 'args': [('entity', 'Entity', 'The entity to add')] + }, + 'remove': { + 'signature': 'remove(entity)', + 'description': 'Remove the first occurrence of an entity from the collection.', + 'args': [('entity', 'Entity', 'The entity to remove')], + 'raises': 'ValueError: If entity is not in collection' + }, + 'extend': { + 'signature': 'extend(iterable)', + 'description': 'Add all entities from an iterable to the collection.', + 'args': [('iterable', 'Iterable[Entity]', 'Entities to add')] + }, + 'count': { + 'signature': 'count(entity)', + 'description': 'Count the number of occurrences of an entity in the collection.', + 'args': [('entity', 'Entity', 'The entity to count')], + 'returns': 'int: Number of times entity appears in collection' + }, + 'index': { + 'signature': 'index(entity)', + 'description': 'Find the index of the first occurrence of an entity.', + 'args': [('entity', 'Entity', 'The entity to find')], + 'returns': 'int: Index of entity in collection', + 'raises': 'ValueError: If entity is not in collection' + } + }, + + 'UICollection': { + 'append': { + 'signature': 'append(drawable)', + 'description': 'Add a drawable element to the end of the collection.', + 'args': [('drawable', 'UIDrawable', 'The drawable element to add')] + }, + 'remove': { + 'signature': 'remove(drawable)', + 'description': 'Remove the first occurrence of a drawable from the collection.', + 'args': [('drawable', 'UIDrawable', 'The drawable to remove')], + 'raises': 'ValueError: If drawable is not in collection' + }, + 'extend': { + 'signature': 'extend(iterable)', + 'description': 'Add all drawables from an iterable to the collection.', + 'args': [('iterable', 'Iterable[UIDrawable]', 'Drawables to add')] + }, + 'count': { + 'signature': 'count(drawable)', + 'description': 'Count the number of occurrences of a drawable in the collection.', + 'args': [('drawable', 'UIDrawable', 'The drawable to count')], + 'returns': 'int: Number of times drawable appears in collection' + }, + 'index': { + 'signature': 'index(drawable)', + 'description': 'Find the index of the first occurrence of a drawable.', + 'args': [('drawable', 'UIDrawable', 'The drawable to find')], + 'returns': 'int: Index of drawable in collection', + 'raises': 'ValueError: If drawable is not in collection' + } + }, + + # Animation methods + 'Animation': { + 'get_current_value': { + 'signature': 'get_current_value()', + 'description': 'Get the current interpolated value of the animation.', + 'returns': 'float: Current animation value between start and end' + }, + 'start': { + 'signature': 'start(target)', + 'description': 'Start the animation on a target UI element.', + 'args': [('target', 'UIDrawable', 'The UI element to animate')], + 'note': 'The target must have the property specified in the animation constructor.' + }, + 'update': { + 'signature': 'update(delta_time)', + 'description': 'Update the animation by the given time delta.', + 'args': [('delta_time', 'float', 'Time elapsed since last update in seconds')], + 'returns': 'bool: True if animation is still running, False if finished' + } + }, + + # Color methods + 'Color': { + 'from_hex': { + 'signature': 'from_hex(hex_string)', + 'description': 'Create a Color from a hexadecimal color string.', + 'args': [('hex_string', 'str', 'Hex color string (e.g., "#FF0000" or "FF0000")')], + 'returns': 'Color: New Color object from hex string', + 'example': 'red = Color.from_hex("#FF0000")' + }, + 'to_hex': { + 'signature': 'to_hex()', + 'description': 'Convert this Color to a hexadecimal string.', + 'returns': 'str: Hex color string in format "#RRGGBB"', + 'example': 'hex_str = color.to_hex() # Returns "#FF0000"' + }, + 'lerp': { + 'signature': 'lerp(other, t)', + 'description': 'Linearly interpolate between this color and another.', + 'args': [ + ('other', 'Color', 'The color to interpolate towards'), + ('t', 'float', 'Interpolation factor from 0.0 to 1.0') + ], + 'returns': 'Color: New interpolated Color object', + 'example': 'mixed = red.lerp(blue, 0.5) # 50% between red and blue' + } + }, + + # Vector methods + 'Vector': { + 'magnitude': { + 'signature': 'magnitude()', + 'description': 'Calculate the length/magnitude of this vector.', + 'returns': 'float: The magnitude of the vector' + }, + 'magnitude_squared': { + 'signature': 'magnitude_squared()', + 'description': 'Calculate the squared magnitude of this vector.', + 'returns': 'float: The squared magnitude (faster than magnitude())', + 'note': 'Use this for comparisons to avoid expensive square root calculation.' + }, + 'normalize': { + 'signature': 'normalize()', + 'description': 'Return a unit vector in the same direction.', + 'returns': 'Vector: New normalized vector with magnitude 1.0', + 'raises': 'ValueError: If vector has zero magnitude' + }, + 'dot': { + 'signature': 'dot(other)', + 'description': 'Calculate the dot product with another vector.', + 'args': [('other', 'Vector', 'The other vector')], + 'returns': 'float: Dot product of the two vectors' + }, + 'distance_to': { + 'signature': 'distance_to(other)', + 'description': 'Calculate the distance to another vector.', + 'args': [('other', 'Vector', 'The other vector')], + 'returns': 'float: Distance between the two vectors' + }, + 'angle': { + 'signature': 'angle()', + 'description': 'Get the angle of this vector in radians.', + 'returns': 'float: Angle in radians from positive x-axis' + }, + 'copy': { + 'signature': 'copy()', + 'description': 'Create a copy of this vector.', + 'returns': 'Vector: New Vector object with same x and y values' + } + }, + + # Scene methods + 'Scene': { + 'activate': { + 'signature': 'activate()', + 'description': 'Make this scene the active scene.', + 'note': 'Equivalent to calling setScene() with this scene\'s name.' + }, + 'get_ui': { + 'signature': 'get_ui()', + 'description': 'Get the UI element collection for this scene.', + 'returns': 'UICollection: Collection of all UI elements in this scene' + }, + 'keypress': { + 'signature': 'keypress(handler)', + 'description': 'Register a keyboard handler function for this scene.', + 'args': [('handler', 'callable', 'Function that takes (key_name: str, is_pressed: bool)')], + 'note': 'Alternative to overriding the on_keypress method.' + }, + 'register_keyboard': { + 'signature': 'register_keyboard(callable)', + 'description': 'Register a keyboard event handler function for the scene.', + 'args': [('callable', 'callable', 'Function that takes (key: str, action: str) parameters')], + 'note': 'Alternative to overriding the on_keypress method when subclassing Scene objects.', + 'example': '''def handle_keyboard(key, action): + print(f"Key '{key}' was {action}") +scene.register_keyboard(handle_keyboard)''' + } + }, + + # Timer methods + 'Timer': { + 'pause': { + 'signature': 'pause()', + 'description': 'Pause the timer, stopping its callback execution.', + 'note': 'Use resume() to continue the timer from where it was paused.' + }, + 'resume': { + 'signature': 'resume()', + 'description': 'Resume a paused timer.', + 'note': 'Has no effect if timer is not paused.' + }, + 'cancel': { + 'signature': 'cancel()', + 'description': 'Cancel the timer and remove it from the system.', + 'note': 'After cancelling, the timer object cannot be reused.' + }, + 'restart': { + 'signature': 'restart()', + 'description': 'Restart the timer from the beginning.', + 'note': 'Resets the timer\'s internal clock to zero.' + } + }, + + # Window methods + 'Window': { + 'get': { + 'signature': 'get()', + 'description': 'Get the Window singleton instance.', + 'returns': 'Window: The singleton window object', + 'note': 'This is a static method that returns the same instance every time.' + }, + 'center': { + 'signature': 'center()', + 'description': 'Center the window on the screen.', + 'note': 'Only works if the window is not fullscreen.' + }, + 'screenshot': { + 'signature': 'screenshot(filename)', + 'description': 'Take a screenshot and save it to a file.', + 'args': [('filename', 'str', 'Path where to save the screenshot')], + 'note': 'Supports PNG, JPG, and BMP formats based on file extension.' + } + } + } + +def get_complete_function_documentation(): + """Return complete documentation for ALL module functions.""" + return { + # Scene Management + 'createScene': { + 'signature': 'createScene(name: str) -> None', + 'description': 'Create a new empty scene with the given name.', + 'args': [('name', 'str', 'Unique name for the new scene')], + 'raises': 'ValueError: If a scene with this name already exists', + 'note': 'The scene is created but not made active. Use setScene() to switch to it.', + 'example': 'mcrfpy.createScene("game_over")' + }, + 'setScene': { + 'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None', + 'description': 'Switch to a different scene with optional transition effect.', + 'args': [ + ('scene', 'str', 'Name of the scene to switch to'), + ('transition', 'str', 'Transition type: "fade", "slide_left", "slide_right", "slide_up", "slide_down"'), + ('duration', 'float', 'Transition duration in seconds (default: 0.0 for instant)') + ], + 'raises': 'KeyError: If the scene doesn\'t exist', + 'example': 'mcrfpy.setScene("game", "fade", 0.5)' + }, + 'currentScene': { + 'signature': 'currentScene() -> str', + 'description': 'Get the name of the currently active scene.', + 'returns': 'str: Name of the current scene', + 'example': 'scene_name = mcrfpy.currentScene()' + }, + 'sceneUI': { + 'signature': 'sceneUI(scene: str = None) -> UICollection', + 'description': 'Get all UI elements for a scene.', + 'args': [('scene', 'str', 'Scene name. If None, uses current scene')], + 'returns': 'UICollection: All UI elements in the scene', + 'raises': 'KeyError: If the specified scene doesn\'t exist', + 'example': 'ui_elements = mcrfpy.sceneUI("game")' + }, + 'keypressScene': { + 'signature': 'keypressScene(handler: callable) -> None', + 'description': 'Set the keyboard event handler for the current scene.', + 'args': [('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)')], + 'example': '''def on_key(key, pressed): + if key == "SPACE" and pressed: + player.jump() +mcrfpy.keypressScene(on_key)''' + }, + + # Audio Functions + 'createSoundBuffer': { + 'signature': 'createSoundBuffer(filename: str) -> int', + 'description': 'Load a sound effect from a file and return its buffer ID.', + 'args': [('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)')], + 'returns': 'int: Buffer ID for use with playSound()', + 'raises': 'RuntimeError: If the file cannot be loaded', + 'example': 'jump_sound = mcrfpy.createSoundBuffer("assets/jump.wav")' + }, + 'loadMusic': { + 'signature': 'loadMusic(filename: str, loop: bool = True) -> None', + 'description': 'Load and immediately play background music from a file.', + 'args': [ + ('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'), + ('loop', 'bool', 'Whether to loop the music (default: True)') + ], + 'note': 'Only one music track can play at a time. Loading new music stops the current track.', + 'example': 'mcrfpy.loadMusic("assets/background.ogg", True)' + }, + 'playSound': { + 'signature': 'playSound(buffer_id: int) -> None', + 'description': 'Play a sound effect using a previously loaded buffer.', + 'args': [('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')], + 'raises': 'RuntimeError: If the buffer ID is invalid', + 'example': 'mcrfpy.playSound(jump_sound)' + }, + 'getMusicVolume': { + 'signature': 'getMusicVolume() -> int', + 'description': 'Get the current music volume level.', + 'returns': 'int: Current volume (0-100)', + 'example': 'current_volume = mcrfpy.getMusicVolume()' + }, + 'getSoundVolume': { + 'signature': 'getSoundVolume() -> int', + 'description': 'Get the current sound effects volume level.', + 'returns': 'int: Current volume (0-100)', + 'example': 'current_volume = mcrfpy.getSoundVolume()' + }, + 'setMusicVolume': { + 'signature': 'setMusicVolume(volume: int) -> None', + 'description': 'Set the global music volume.', + 'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')], + 'example': 'mcrfpy.setMusicVolume(50) # Set to 50% volume' + }, + 'setSoundVolume': { + 'signature': 'setSoundVolume(volume: int) -> None', + 'description': 'Set the global sound effects volume.', + 'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')], + 'example': 'mcrfpy.setSoundVolume(75) # Set to 75% volume' + }, + + # UI Utilities + 'find': { + 'signature': 'find(name: str, scene: str = None) -> UIDrawable | None', + 'description': 'Find the first UI element with the specified name.', + 'args': [ + ('name', 'str', 'Exact name to search for'), + ('scene', 'str', 'Scene to search in (default: current scene)') + ], + 'returns': 'UIDrawable or None: The found element, or None if not found', + 'note': 'Searches scene UI elements and entities within grids.', + 'example': 'button = mcrfpy.find("start_button")' + }, + 'findAll': { + 'signature': 'findAll(pattern: str, scene: str = None) -> list', + 'description': 'Find all UI elements matching a name pattern.', + 'args': [ + ('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'), + ('scene', 'str', 'Scene to search in (default: current scene)') + ], + 'returns': 'list: All matching UI elements and entities', + 'example': 'enemies = mcrfpy.findAll("enemy_*")' + }, + + # System Functions + 'exit': { + 'signature': 'exit() -> None', + 'description': 'Cleanly shut down the game engine and exit the application.', + 'note': 'This immediately closes the window and terminates the program.', + 'example': 'mcrfpy.exit()' + }, + 'getMetrics': { + 'signature': 'getMetrics() -> dict', + 'description': 'Get current performance metrics.', + 'returns': '''dict: Performance data with keys: +- frame_time: Last frame duration in seconds +- avg_frame_time: Average frame time +- fps: Frames per second +- draw_calls: Number of draw calls +- ui_elements: Total UI element count +- visible_elements: Visible element count +- current_frame: Frame counter +- runtime: Total runtime in seconds''', + 'example': 'metrics = mcrfpy.getMetrics()' + }, + 'setTimer': { + 'signature': 'setTimer(name: str, handler: callable, interval: int) -> None', + 'description': 'Create or update a recurring timer.', + 'args': [ + ('name', 'str', 'Unique identifier for the timer'), + ('handler', 'callable', 'Function called with (runtime: float) parameter'), + ('interval', 'int', 'Time between calls in milliseconds') + ], + 'note': 'If a timer with this name exists, it will be replaced.', + 'example': '''def update_score(runtime): + score += 1 +mcrfpy.setTimer("score_update", update_score, 1000)''' + }, + 'delTimer': { + 'signature': 'delTimer(name: str) -> None', + 'description': 'Stop and remove a timer.', + 'args': [('name', 'str', 'Timer identifier to remove')], + 'note': 'No error is raised if the timer doesn\'t exist.', + 'example': 'mcrfpy.delTimer("score_update")' + }, + 'setScale': { + 'signature': 'setScale(multiplier: float) -> None', + 'description': 'Scale the game window size.', + 'args': [('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)')], + 'note': 'The internal resolution remains 1024x768, but the window is scaled.', + 'example': 'mcrfpy.setScale(2.0) # Double the window size' + } + } + +def get_complete_property_documentation(): + """Return complete documentation for ALL properties.""" + return { + 'Animation': { + 'property': 'str: Name of the property being animated (e.g., "x", "y", "scale")', + 'duration': 'float: Total duration of the animation in seconds', + 'elapsed_time': 'float: Time elapsed since animation started (read-only)', + 'current_value': 'float: Current interpolated value of the animation (read-only)', + 'is_running': 'bool: True if animation is currently running (read-only)', + 'is_finished': 'bool: True if animation has completed (read-only)' + }, + 'GridPoint': { + 'x': 'int: Grid x coordinate of this point', + 'y': 'int: Grid y coordinate of this point', + 'texture_index': 'int: Index of the texture/sprite to display at this point', + 'solid': 'bool: Whether this point blocks movement', + 'transparent': 'bool: Whether this point allows light/vision through', + 'color': 'Color: Color tint applied to the texture at this point' + }, + 'GridPointState': { + 'visible': 'bool: Whether this point is currently visible to the player', + 'discovered': 'bool: Whether this point has been discovered/explored', + 'custom_flags': 'int: Bitfield for custom game-specific flags' + } + } + +def format_method_markdown(method_name, method_doc): + """Format a method as markdown.""" + lines = [] + + lines.append(f"#### `{method_doc['signature']}`") + lines.append("") + lines.append(method_doc['description']) + lines.append("") + + # Arguments + if 'args' in method_doc: + lines.append("**Arguments:**") + for arg in method_doc['args']: + lines.append(f"- `{arg[0]}` (*{arg[1]}*): {arg[2]}") + lines.append("") + + # Returns + if 'returns' in method_doc: + lines.append(f"**Returns:** {method_doc['returns']}") + lines.append("") + + # Raises + if 'raises' in method_doc: + lines.append(f"**Raises:** {method_doc['raises']}") + lines.append("") + + # Note + if 'note' in method_doc: + lines.append(f"**Note:** {method_doc['note']}") + lines.append("") + + # Example + if 'example' in method_doc: + lines.append("**Example:**") + lines.append("```python") + lines.append(method_doc['example']) + lines.append("```") + lines.append("") + + return lines + +def format_function_markdown(func_name, func_doc): + """Format a function as markdown.""" + lines = [] + + lines.append(f"### `{func_doc['signature']}`") + lines.append("") + lines.append(func_doc['description']) + lines.append("") + + # Arguments + if 'args' in func_doc: + lines.append("**Arguments:**") + for arg in func_doc['args']: + lines.append(f"- `{arg[0]}` (*{arg[1]}*): {arg[2]}") + lines.append("") + + # Returns + if 'returns' in func_doc: + lines.append(f"**Returns:** {func_doc['returns']}") + lines.append("") + + # Raises + if 'raises' in func_doc: + lines.append(f"**Raises:** {func_doc['raises']}") + lines.append("") + + # Note + if 'note' in func_doc: + lines.append(f"**Note:** {func_doc['note']}") + lines.append("") + + # Example + if 'example' in func_doc: + lines.append("**Example:**") + lines.append("```python") + lines.append(func_doc['example']) + lines.append("```") + lines.append("") + + lines.append("---") + lines.append("") + + return lines + +def generate_complete_markdown_documentation(): + """Generate complete markdown documentation with NO missing methods.""" + + # Get all documentation data + method_docs = get_complete_method_documentation() + function_docs = get_complete_function_documentation() + property_docs = get_complete_property_documentation() + + lines = [] + + # Header + lines.append("# McRogueFace API Reference") + lines.append("") + lines.append(f"*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*") + lines.append("") + + # Overview + if mcrfpy.__doc__: + lines.append("## Overview") + lines.append("") + # Process the docstring properly + doc_text = mcrfpy.__doc__.replace('\\n', '\n') + lines.append(doc_text) + lines.append("") + + # Table of Contents + lines.append("## Table of Contents") + lines.append("") + lines.append("- [Functions](#functions)") + lines.append(" - [Scene Management](#scene-management)") + lines.append(" - [Audio](#audio)") + lines.append(" - [UI Utilities](#ui-utilities)") + lines.append(" - [System](#system)") + lines.append("- [Classes](#classes)") + lines.append(" - [UI Components](#ui-components)") + lines.append(" - [Collections](#collections)") + lines.append(" - [System Types](#system-types)") + lines.append(" - [Other Classes](#other-classes)") + lines.append("- [Automation Module](#automation-module)") + lines.append("") + + # Functions section + lines.append("## Functions") + lines.append("") + + # Group functions by category + categories = { + 'Scene Management': ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'], + 'Audio': ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'], + 'UI Utilities': ['find', 'findAll'], + 'System': ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale'] + } + + for category, functions in categories.items(): + lines.append(f"### {category}") + lines.append("") + for func_name in functions: + if func_name in function_docs: + lines.extend(format_function_markdown(func_name, function_docs[func_name])) + + # Classes section + lines.append("## Classes") + lines.append("") + + # Get all classes from mcrfpy + classes = [] + for name in sorted(dir(mcrfpy)): + if not name.startswith('_'): + obj = getattr(mcrfpy, name) + if isinstance(obj, type): + classes.append((name, obj)) + + # Group classes + ui_classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity'] + collection_classes = ['EntityCollection', 'UICollection', 'UICollectionIter', 'UIEntityCollectionIter'] + system_classes = ['Color', 'Vector', 'Texture', 'Font'] + other_classes = [name for name, _ in classes if name not in ui_classes + collection_classes + system_classes] + + # UI Components + lines.append("### UI Components") + lines.append("") + for class_name in ui_classes: + if any(name == class_name for name, _ in classes): + lines.extend(format_class_markdown(class_name, method_docs, property_docs)) + + # Collections + lines.append("### Collections") + lines.append("") + for class_name in collection_classes: + if any(name == class_name for name, _ in classes): + lines.extend(format_class_markdown(class_name, method_docs, property_docs)) + + # System Types + lines.append("### System Types") + lines.append("") + for class_name in system_classes: + if any(name == class_name for name, _ in classes): + lines.extend(format_class_markdown(class_name, method_docs, property_docs)) + + # Other Classes + lines.append("### Other Classes") + lines.append("") + for class_name in other_classes: + lines.extend(format_class_markdown(class_name, method_docs, property_docs)) + + # Automation section + if hasattr(mcrfpy, 'automation'): + lines.append("## Automation Module") + lines.append("") + lines.append("The `mcrfpy.automation` module provides testing and automation capabilities.") + lines.append("") + + automation = mcrfpy.automation + for name in sorted(dir(automation)): + if not name.startswith('_'): + obj = getattr(automation, name) + if callable(obj): + lines.append(f"### `automation.{name}`") + lines.append("") + if obj.__doc__: + doc_parts = obj.__doc__.split(' - ') + if len(doc_parts) > 1: + lines.append(doc_parts[1]) + else: + lines.append(obj.__doc__) + lines.append("") + lines.append("---") + lines.append("") + + return '\n'.join(lines) + +def format_class_markdown(class_name, method_docs, property_docs): + """Format a class as markdown.""" + lines = [] + + lines.append(f"### class `{class_name}`") + lines.append("") + + # Class description from known info + class_descriptions = { + 'Frame': 'A rectangular frame UI element that can contain other drawable elements.', + 'Caption': 'A text display UI element with customizable font and styling.', + 'Sprite': 'A sprite UI element that displays a texture or portion of a texture atlas.', + 'Grid': 'A grid-based tilemap UI element for rendering tile-based levels and game worlds.', + 'Entity': 'Game entity that can be placed in a Grid.', + 'EntityCollection': 'Container for Entity objects in a Grid. Supports iteration and indexing.', + 'UICollection': 'Container for UI drawable elements. Supports iteration and indexing.', + 'UICollectionIter': 'Iterator for UICollection. Automatically created when iterating over a UICollection.', + 'UIEntityCollectionIter': 'Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.', + 'Color': 'RGBA color representation.', + 'Vector': '2D vector for positions and directions.', + 'Font': 'Font object for text rendering.', + 'Texture': 'Texture object for image data.', + 'Animation': 'Animate UI element properties over time.', + 'GridPoint': 'Represents a single tile in a Grid.', + 'GridPointState': 'State information for a GridPoint.', + 'Scene': 'Base class for object-oriented scenes.', + 'Timer': 'Timer object for scheduled callbacks.', + 'Window': 'Window singleton for accessing and modifying the game window properties.', + 'Drawable': 'Base class for all drawable UI elements.' + } + + if class_name in class_descriptions: + lines.append(class_descriptions[class_name]) + lines.append("") + + # Properties + if class_name in property_docs: + lines.append("#### Properties") + lines.append("") + for prop_name, prop_desc in property_docs[class_name].items(): + lines.append(f"- **`{prop_name}`**: {prop_desc}") + lines.append("") + + # Methods + methods_to_document = [] + + # Add inherited methods for UI classes + if class_name in ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']: + methods_to_document.extend(['get_bounds', 'move', 'resize']) + + # Add class-specific methods + if class_name in method_docs: + methods_to_document.extend(method_docs[class_name].keys()) + + if methods_to_document: + lines.append("#### Methods") + lines.append("") + for method_name in set(methods_to_document): + # Get method documentation + method_doc = None + if class_name in method_docs and method_name in method_docs[class_name]: + method_doc = method_docs[class_name][method_name] + elif method_name in method_docs.get('Drawable', {}): + method_doc = method_docs['Drawable'][method_name] + + if method_doc: + lines.extend(format_method_markdown(method_name, method_doc)) + + lines.append("---") + lines.append("") + + return lines + +def main(): + """Generate complete markdown documentation with zero missing methods.""" + print("Generating COMPLETE Markdown API documentation...") + + # Generate markdown + markdown_content = generate_complete_markdown_documentation() + + # Write to file + output_path = Path("docs/API_REFERENCE_COMPLETE.md") + output_path.parent.mkdir(exist_ok=True) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write(markdown_content) + + print(f"✓ Generated {output_path}") + print(f" File size: {len(markdown_content):,} bytes") + + # Count "..." instances + ellipsis_count = markdown_content.count('...') + print(f" Ellipsis instances: {ellipsis_count}") + + if ellipsis_count == 0: + print("✅ SUCCESS: No missing documentation found!") + else: + print(f"❌ WARNING: {ellipsis_count} methods still need documentation") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tools/generate_dynamic_docs.py b/tools/generate_dynamic_docs.py index 426fdcd..92e65cc 100644 --- a/tools/generate_dynamic_docs.py +++ b/tools/generate_dynamic_docs.py @@ -12,67 +12,6 @@ import html import re from pathlib import Path -def transform_doc_links(docstring, format='html', base_url=''): - """Transform MCRF_LINK patterns based on output format. - - Detects pattern: "See also: TEXT (docs/path.md)" - Transforms to appropriate format for output type. - - For HTML/web formats, properly escapes content before inserting HTML tags. - """ - if not docstring: - return docstring - - link_pattern = r'See also: ([^(]+) \(([^)]+)\)' - - def replace_link(match): - text, ref = match.group(1).strip(), match.group(2).strip() - - if format == 'html': - # Convert docs/foo.md → foo.html and escape for safe HTML - href = html.escape(ref.replace('docs/', '').replace('.md', '.html'), quote=True) - text_escaped = html.escape(text) - return f'

See also: {text_escaped}

' - - elif format == 'web': - # Link to hosted docs and escape for safe HTML - web_path = ref.replace('docs/', '').replace('.md', '') - href = html.escape(f"{base_url}/{web_path}", quote=True) - text_escaped = html.escape(text) - return f'

See also: {text_escaped}

' - - elif format == 'markdown': - # Markdown link - return f'\n**See also:** [{text}]({ref})' - - else: # 'python' or default - # Keep as plain text for Python docstrings - return match.group(0) - - # For HTML formats, escape the entire docstring first, then process links - if format in ('html', 'web'): - # Split by the link pattern, escape non-link parts, then reassemble - parts = [] - last_end = 0 - - for match in re.finditer(link_pattern, docstring): - # Escape the text before this match - if match.start() > last_end: - parts.append(html.escape(docstring[last_end:match.start()])) - - # Process the link (replace_link handles escaping internally) - parts.append(replace_link(match)) - last_end = match.end() - - # Escape any remaining text after the last match - if last_end < len(docstring): - parts.append(html.escape(docstring[last_end:])) - - return ''.join(parts) - else: - # For non-HTML formats, just do simple replacement - return re.sub(link_pattern, replace_link, docstring) - # Must be run with McRogueFace as interpreter try: import mcrfpy @@ -365,10 +304,8 @@ def generate_html_docs(): html_content += f"""

{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}

+

{html.escape(parsed['description'])}

""" - if parsed['description']: - description = transform_doc_links(parsed['description'], format='html') - html_content += f"

{description}

\n" if parsed['args']: html_content += "

Arguments:

\n
    \n" @@ -424,10 +361,9 @@ def generate_html_docs():
    {method_name}{parsed['signature'] if parsed['signature'] else '(...)'}
    """ - + if parsed['description']: - description = transform_doc_links(parsed['description'], format='html') - html_content += f"

    {description}

    \n" + html_content += f"

    {html.escape(parsed['description'])}

    \n" if parsed['args']: html_content += "
    \n" @@ -493,10 +429,9 @@ def generate_markdown_docs(): parsed = func_info["parsed"] md_content += f"### `{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n" - + if parsed['description']: - description = transform_doc_links(parsed['description'], format='markdown') - md_content += f"{description}\n\n" + md_content += f"{parsed['description']}\n\n" if parsed['args']: md_content += "**Arguments:**\n" @@ -544,10 +479,9 @@ def generate_markdown_docs(): parsed = method_info['parsed'] md_content += f"#### `{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n" - + if parsed['description']: - description = transform_doc_links(parsed['description'], format='markdown') - md_content += f"{description}\n\n" + md_content += f"{parsed['description']}\n\n" if parsed['args']: md_content += "**Arguments:**\n" diff --git a/tools/test_link_transform.py b/tools/test_link_transform.py deleted file mode 100644 index 37e3b05..0000000 --- a/tools/test_link_transform.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -"""Test script for link transformation function.""" - -import re - -def transform_doc_links(docstring, format='html', base_url=''): - """Transform MCRF_LINK patterns based on output format. - - Detects pattern: "See also: TEXT (docs/path.md)" - Transforms to appropriate format for output type. - """ - if not docstring: - return docstring - - link_pattern = r'See also: ([^(]+) \(([^)]+)\)' - - def replace_link(match): - text, ref = match.group(1).strip(), match.group(2).strip() - - if format == 'html': - # Convert docs/foo.md → foo.html - href = ref.replace('docs/', '').replace('.md', '.html') - return f'

    See also: {text}

    ' - - elif format == 'web': - # Link to hosted docs - web_path = ref.replace('docs/', '').replace('.md', '') - return f'

    See also: {text}

    ' - - elif format == 'markdown': - # Markdown link - return f'\n**See also:** [{text}]({ref})' - - else: # 'python' or default - # Keep as plain text for Python docstrings - return match.group(0) - - return re.sub(link_pattern, replace_link, docstring) - -# Test cases -test_doc = "Description text.\n\nSee also: Tutorial Guide (docs/guide.md)\n\nMore text." - -html_result = transform_doc_links(test_doc, format='html') -print("HTML:", html_result) -assert 'Tutorial Guide' in html_result - -md_result = transform_doc_links(test_doc, format='markdown') -print("Markdown:", md_result) -assert '[Tutorial Guide](docs/guide.md)' in md_result - -plain_result = transform_doc_links(test_doc, format='python') -print("Python:", plain_result) -assert 'See also: Tutorial Guide (docs/guide.md)' in plain_result - -print("\nSUCCESS: All transformations work correctly") diff --git a/tools/test_vector_docs.py b/tools/test_vector_docs.py deleted file mode 100644 index bf07e47..0000000 --- a/tools/test_vector_docs.py +++ /dev/null @@ -1,39 +0,0 @@ -import mcrfpy -import sys - -# Check Vector.magnitude docstring -mag_doc = mcrfpy.Vector.magnitude.__doc__ -print("magnitude doc:", mag_doc) -assert "magnitude()" in mag_doc -assert "Calculate the length/magnitude" in mag_doc -assert "Returns:" in mag_doc - -# Check Vector.dot docstring -dot_doc = mcrfpy.Vector.dot.__doc__ -print("dot doc:", dot_doc) -assert "dot(other: Vector)" in dot_doc -assert "Args:" in dot_doc -assert "other:" in dot_doc - -# Check Vector.normalize docstring -normalize_doc = mcrfpy.Vector.normalize.__doc__ -print("normalize doc:", normalize_doc) -assert "normalize()" in normalize_doc -assert "Return a unit vector" in normalize_doc -assert "Returns:" in normalize_doc -assert "Note:" in normalize_doc - -# Check Vector.x property docstring -x_doc = mcrfpy.Vector.x.__doc__ -print("x property doc:", x_doc) -assert "X coordinate of the vector" in x_doc -assert "float" in x_doc - -# Check Vector.y property docstring -y_doc = mcrfpy.Vector.y.__doc__ -print("y property doc:", y_doc) -assert "Y coordinate of the vector" in y_doc -assert "float" in y_doc - -print("SUCCESS: All docstrings present and complete") -sys.exit(0)