Compare commits

...

5 commits

Author SHA1 Message Date
f62362032e feat: Grid camera defaults to tile (0,0) at top-left + center_camera() method (#169)
Changes:
- Default Grid center now positions tile (0,0) at widget's top-left corner
- Added center_camera() method to center grid's middle tile at view center
- Added center_camera((tile_x, tile_y)) to position tile at top-left of widget
- Uses NaN as sentinel to detect if user provided center values in kwargs
- Animation-compatible: center_camera() just sets center property, no special state

Behavior:
- center_camera() → grid's center tile at view center
- center_camera((0, 0)) → tile (0,0) at top-left corner
- center_camera((5, 10)) → tile (5,10) at top-left corner

Before: Grid(size=(320,240)) showed 3/4 of content off-screen (center=0,0)
After: Grid(size=(320,240)) shows tile (0,0) at top-left (center=160,120)

Closes #169

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 17:22:26 -05:00
05f28ef7cd Add 14-part tutorial Python files (extracted, tested)
Tutorial scripts extracted from documentation, with fixes:
- Asset filename: kenney_roguelike.png → kenney_tinydungeon.png
- Entity keyword: pos= → grid_pos= (tile coordinates)
- Frame.size property → Frame.resize() method
- Removed sprite_color (deferred to shader support)

All 14 parts pass smoke testing (import + 2-frame run).

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 16:21:09 -05:00
e64c5c147f docs: Fix property extraction and add Scene documentation
Doc generator fixes (tools/generate_dynamic_docs.py):
- Add types.GetSetDescriptorType detection for C++ extension properties
- All 22 classes with properties now have documented Properties sections
- Read-only detection via "read-only" docstring convention

Scene class documentation (src/PySceneObject.h):
- Expanded tp_doc with constructor, properties, lifecycle callbacks
- Documents key advantage: on_key works on ANY scene
- Includes usage examples for basic and subclass patterns

CLAUDE.md additions:
- New section "Adding Documentation for New Python Types"
- Step-by-step guide for tp_doc, PyMethodDef, PyGetSetDef
- Documentation extraction details and troubleshooting

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:48:21 -05:00
b863698f6e test: Add comprehensive Scene object API test
Demonstrates the object-oriented Scene API as alternative to module-level
functions. Key features tested:

- Scene object creation and properties (name, active, children)
- scene.activate() vs mcrfpy.setScene()
- scene.on_key property - can be set on ANY scene, not just current
- Scene visual properties (pos, visible, opacity)
- Subclassing for lifecycle callbacks (on_enter, on_exit, update)

The on_key advantage resolves confusion with keypressScene() which only
works on the currently active scene.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:48:00 -05:00
9f481a2e4a fix: Update test files to use current API patterns
Migrates test suite to current API:
- Frame(x, y, w, h) → Frame(pos=(x, y), size=(w, h))
- Caption("text", x, y) → Caption(pos=(x, y), text="text")
- caption.size → caption.font_size
- Entity(x, y, ...) → Entity((x, y), ...)
- Grid(w, h, ...) → Grid(grid_size=(w, h), ...)
- cell.color → ColorLayer system

Tests now serve as valid API usage examples.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:47:48 -05:00
75 changed files with 15145 additions and 594 deletions

119
CLAUDE.md
View file

@ -625,4 +625,123 @@ After modifying C++ inline documentation with MCRF_* macros:
5. **No drift**: Impossible for docs and code to disagree - they're the same file! 5. **No drift**: Impossible for docs and code to disagree - they're the same file!
The macro system ensures complete, consistent documentation across all Python bindings. The macro system ensures complete, consistent documentation across all Python bindings.
### Adding Documentation for New Python Types
When adding a new Python class/type to the engine, follow these steps to ensure it's properly documented:
#### 1. Class Docstring (tp_doc)
In the `PyTypeObject` definition (usually in the header file), set `tp_doc` with a comprehensive docstring:
```cpp
// In PyMyClass.h
.tp_doc = PyDoc_STR(
"MyClass(arg1: type, arg2: type)\n\n"
"Brief description of what this class does.\n\n"
"Args:\n"
" arg1: Description of first argument.\n"
" arg2: Description of second argument.\n\n"
"Properties:\n"
" prop1 (type, read-only): Description of property.\n"
" prop2 (type): Description of writable property.\n\n"
"Example:\n"
" obj = mcrfpy.MyClass('example', 42)\n"
" print(obj.prop1)\n"
),
```
#### 2. Method Documentation (PyMethodDef)
For each method in the `methods[]` array, use the MCRF_* macros:
```cpp
// In PyMyClass.cpp
PyMethodDef PyMyClass::methods[] = {
{"do_something", (PyCFunction)do_something, METH_VARARGS,
MCRF_METHOD(MyClass, do_something,
MCRF_SIG("(value: int)", "bool"),
MCRF_DESC("Does something with the value."),
MCRF_ARGS_START
MCRF_ARG("value", "The value to process")
MCRF_RETURNS("True if successful, False otherwise")
)},
{NULL} // Sentinel
};
```
#### 3. Property Documentation (PyGetSetDef)
For each property in the `getsetters[]` array, include a docstring:
```cpp
// In PyMyClass.cpp
PyGetSetDef PyMyClass::getsetters[] = {
{"property_name", (getter)get_property, (setter)set_property,
"Property description. Include (type, read-only) if not writable.",
NULL},
{NULL} // Sentinel
};
```
**Important for read-only properties:** Include "read-only" in the docstring so the doc generator detects it:
```cpp
{"name", (getter)get_name, NULL, // NULL setter = read-only
"Object name (str, read-only). Unique identifier.",
NULL},
```
#### 4. Register Type in Module
Ensure the type is properly registered in `McRFPy_API.cpp` and its methods/getsetters are assigned:
```cpp
// Set methods and getsetters before PyType_Ready
mcrfpydef::PyMyClassType.tp_methods = PyMyClass::methods;
mcrfpydef::PyMyClassType.tp_getset = PyMyClass::getsetters;
// Then call PyType_Ready and add to module
```
#### 5. Regenerate Documentation
After adding the new type, regenerate all docs:
```bash
make -j4 # Rebuild with new documentation
cd build
./mcrogueface --headless --exec ../tools/generate_dynamic_docs.py
cp docs/API_REFERENCE_DYNAMIC.md ../docs/
cp docs/api_reference_dynamic.html ../docs/
```
#### 6. Update Type Stubs (Optional)
For IDE support, update `stubs/mcrfpy.pyi` with the new class:
```python
class MyClass:
"""Brief description."""
def __init__(self, arg1: str, arg2: int) -> None: ...
@property
def prop1(self) -> str: ...
def do_something(self, value: int) -> bool: ...
```
### Documentation Extraction Details
The doc generator (`tools/generate_dynamic_docs.py`) uses Python introspection:
- **Classes**: Detected via `inspect.isclass()`, docstring from `cls.__doc__`
- **Methods**: Detected via `callable()` check on class attributes
- **Properties**: Detected via `types.GetSetDescriptorType` (C++ extension) or `property` (Python)
- **Read-only detection**: Checks if "read-only" appears in property docstring
If documentation isn't appearing, verify:
1. The type is exported to the `mcrfpy` module
2. Methods/getsetters arrays are properly assigned before `PyType_Ready()`
3. Docstrings don't contain null bytes or invalid UTF-8
---
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened). - Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).

View file

@ -1,6 +1,6 @@
# McRogueFace API Reference # McRogueFace API Reference
*Generated on 2025-12-28 14:29:42* *Generated on 2025-12-29 14:24:58*
*This documentation was dynamically generated from the compiled module.* *This documentation was dynamically generated from the compiled module.*
@ -289,6 +289,13 @@ Note:
Animation object for animating UI properties Animation object for animating UI properties
**Properties:**
- `duration` *(read-only)*: Animation duration in seconds (float, read-only). Total time for the animation to complete.
- `elapsed` *(read-only)*: Elapsed time in seconds (float, read-only). Time since the animation started.
- `is_complete` *(read-only)*: Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called.
- `is_delta` *(read-only)*: Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.
- `property` *(read-only)*: Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index').
**Methods:** **Methods:**
#### `complete() -> None` #### `complete() -> None`
@ -376,6 +383,28 @@ Attributes:
name (str): Element name name (str): Element name
**Properties:**
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
- `center`: Center position of the arc
- `color`: Arc color
- `end_angle`: Ending angle in degrees
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
- `name`: Name for finding this element.
- `on_click`: Callable executed when arc is clicked.
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
- `pos`: Position as a Vector (same as center).
- `radius`: Arc radius in pixels
- `start_angle`: Starting angle in degrees
- `thickness`: Line thickness
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
- `z_index`: Z-order for rendering (lower values rendered first).
**Methods:** **Methods:**
#### `get_bounds() -> tuple` #### `get_bounds() -> tuple`
@ -447,6 +476,29 @@ Attributes:
name (str): Element name name (str): Element name
w, h (float): Read-only computed size based on text and font w, h (float): Read-only computed size based on text and font
**Properties:**
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
- `fill_color`: Fill color of the text
- `font_size`: Font size (integer) in points
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
- `name`: Name for finding elements
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
- `outline`: Thickness of the border
- `outline_color`: Outline color of the text
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
- `pos`: (x, y) vector
- `text`: The text displayed
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
- `x`: X coordinate of top-left corner
- `y`: Y coordinate of top-left corner
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
**Methods:** **Methods:**
#### `get_bounds() -> tuple` #### `get_bounds() -> tuple`
@ -511,6 +563,27 @@ Attributes:
name (str): Element name name (str): Element name
**Properties:**
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
- `center`: Center position of the circle
- `fill_color`: Fill color of the circle
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
- `name`: Name for finding this element.
- `on_click`: Callable executed when circle is clicked.
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
- `outline`: Outline thickness (0 for no outline)
- `outline_color`: Outline color of the circle
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
- `pos`: Position as a Vector (same as center).
- `radius`: Circle radius in pixels
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
- `z_index`: Z-order for rendering (lower values rendered first).
**Methods:** **Methods:**
#### `get_bounds() -> tuple` #### `get_bounds() -> tuple`
@ -545,6 +618,12 @@ Note:
SFML Color Object SFML Color Object
**Properties:**
- `a`: Alpha component (0-255, where 0=transparent, 255=opaque). Automatically clamped to valid range.
- `b`: Blue component (0-255). Automatically clamped to valid range.
- `g`: Green component (0-255). Automatically clamped to valid range.
- `r`: Red component (0-255). Automatically clamped to valid range.
**Methods:** **Methods:**
#### `from_hex(hex_string: str) -> Color` #### `from_hex(hex_string: str) -> Color`
@ -600,6 +679,11 @@ Methods:
set(x, y, color): Set color at cell position set(x, y, color): Set color at cell position
fill(color): Fill entire layer with color fill(color): Fill entire layer with color
**Properties:**
- `grid_size`: Layer dimensions as (width, height) tuple.
- `visible`: Whether the layer is rendered.
- `z_index`: Layer z-order. Negative values render below entities.
**Methods:** **Methods:**
#### `apply_perspective(entity, visible=None, discovered=None, unknown=None)` #### `apply_perspective(entity, visible=None, discovered=None, unknown=None)`
@ -644,6 +728,12 @@ Call this after the entity moves to update the visibility layer.
Base class for all drawable UI elements Base class for all drawable UI elements
**Properties:**
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
**Methods:** **Methods:**
#### `get_bounds() -> tuple` #### `get_bounds() -> tuple`
@ -703,6 +793,19 @@ Attributes:
opacity (float): Opacity value opacity (float): Opacity value
name (str): Element name name (str): Element name
**Properties:**
- `draw_pos`: Entity position (graphically)
- `grid`: Grid this entity belongs to. Get: Returns the Grid or None. Set: Assign a Grid to move entity, or None to remove from grid.
- `gridstate`: Grid point states for the entity
- `name`: Name for finding elements
- `opacity`: Opacity (0.0 = transparent, 1.0 = opaque)
- `pos`: Entity position (integer grid coordinates)
- `sprite_index`: Sprite index on the texture on the display
- `sprite_number`: Sprite index (DEPRECATED: use sprite_index instead)
- `visible`: Visibility flag
- `x`: Entity x position
- `y`: Entity y position
**Methods:** **Methods:**
#### `at(...)` #### `at(...)`
@ -810,6 +913,12 @@ Remove first occurrence of entity. Raises ValueError if not found.
*Inherits from: IntEnum* *Inherits from: IntEnum*
**Properties:**
- `denominator`: the denominator of a rational number in lowest terms
- `imag`: the imaginary part of a complex number
- `numerator`: the numerator of a rational number in lowest terms
- `real`: the real part of a complex number
**Methods:** **Methods:**
#### `as_integer_ratio(...)` #### `as_integer_ratio(...)`
@ -887,6 +996,10 @@ Return an array of bytes representing an integer.
SFML Font Object SFML Font Object
**Properties:**
- `family` *(read-only)*: Font family name (str, read-only). Retrieved from font metadata.
- `source` *(read-only)*: Source filename path (str, read-only). The path used to load this font.
**Methods:** **Methods:**
### Frame ### Frame
@ -933,6 +1046,32 @@ Attributes:
clip_children (bool): Whether to clip children to frame bounds clip_children (bool): Whether to clip children to frame bounds
cache_subtree (bool): Cache subtree rendering to texture cache_subtree (bool): Cache subtree rendering to texture
**Properties:**
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
- `cache_subtree`: #144: Cache subtree rendering to texture for performance
- `children`: UICollection of objects on top of this one
- `clip_children`: Whether to clip children to frame bounds
- `fill_color`: Fill color of the rectangle
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
- `h`: height of the rectangle
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
- `name`: Name for finding elements
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
- `outline`: Thickness of the border
- `outline_color`: Outline color of the rectangle
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
- `pos`: Position as a Vector
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
- `w`: width of the rectangle
- `x`: X coordinate of top-left corner
- `y`: Y coordinate of top-left corner
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
**Methods:** **Methods:**
#### `get_bounds() -> tuple` #### `get_bounds() -> tuple`
@ -1015,6 +1154,48 @@ Attributes:
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name name (str): Element name
**Properties:**
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
- `center`: Grid coordinate at the center of the Grid's view (pan)
- `center_x`: center of the view X-coordinate
- `center_y`: center of the view Y-coordinate
- `children`: UICollection of UIDrawable children (speech bubbles, effects, overlays)
- `entities`: EntityCollection of entities on this grid
- `fill_color`: Background fill color of the grid
- `fov`: FOV algorithm for this grid (mcrfpy.FOV enum). Used by entity.updateVisibility() and layer methods when fov=None.
- `fov_radius`: Default FOV radius for this grid. Used when radius not specified.
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
- `grid_size`: Grid dimensions (grid_x, grid_y)
- `grid_x`: Grid x dimension
- `grid_y`: Grid y dimension
- `h`: visible widget height
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
- `hovered_cell`: Currently hovered cell as (x, y) tuple, or None if not hovering.
- `layers`: List of grid layers (ColorLayer, TileLayer) sorted by z_index
- `name`: Name for finding elements
- `on_cell_click`: Callback when a grid cell is clicked. Called with (cell_x, cell_y).
- `on_cell_enter`: Callback when mouse enters a grid cell. Called with (cell_x, cell_y).
- `on_cell_exit`: Callback when mouse exits a grid cell. Called with (cell_x, cell_y).
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
- `perspective`: Entity whose perspective to use for FOV rendering (None for omniscient view). Setting an entity automatically enables perspective mode.
- `perspective_enabled`: Whether to use perspective-based FOV rendering. When True with no valid entity, all cells appear undiscovered.
- `pos`: Position of the grid as Vector
- `position`: Position of the grid (x, y)
- `size`: Size of the grid (width, height)
- `texture`: Texture of the grid
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
- `w`: visible widget width
- `x`: top-left corner X-coordinate
- `y`: top-left corner Y-coordinate
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
- `zoom`: zoom factor for displaying the Grid
**Methods:** **Methods:**
#### `add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer` #### `add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer`
@ -1165,12 +1346,22 @@ Note:
UIGridPoint object UIGridPoint object
**Properties:**
- `entities` *(read-only)*: List of entities at this grid cell (read-only)
- `transparent`: Is the GridPoint transparent
- `walkable`: Is the GridPoint walkable
**Methods:** **Methods:**
### GridPointState ### GridPointState
UIGridPointState object UIGridPointState object
**Properties:**
- `discovered`: Has the GridPointState been discovered
- `point`: GridPoint at this position (None if not discovered)
- `visible`: Is the GridPointState visible
**Methods:** **Methods:**
### Line ### Line
@ -1205,6 +1396,26 @@ Attributes:
name (str): Element name name (str): Element name
**Properties:**
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
- `color`: Line color as a Color object.
- `end`: Ending point of the line as a Vector.
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
- `name`: Name for finding this element.
- `on_click`: Callable executed when line is clicked.
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
- `pos`: Position as a Vector (midpoint of line).
- `start`: Starting point of the line as a Vector.
- `thickness`: Line thickness in pixels.
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
- `z_index`: Z-order for rendering (lower values rendered first).
**Methods:** **Methods:**
#### `get_bounds() -> tuple` #### `get_bounds() -> tuple`
@ -1237,7 +1448,56 @@ Note:
### Scene ### Scene
Base class for object-oriented scenes Scene(name: str)
Object-oriented scene management with lifecycle callbacks.
This is the recommended approach for scene management, replacing module-level
functions like createScene(), setScene(), and sceneUI(). Key advantage: you can
set on_key handlers on ANY scene, not just the currently active one.
Args:
name: Unique identifier for this scene. Used for scene transitions.
Properties:
name (str, read-only): Scene's unique identifier.
active (bool, read-only): Whether this scene is currently displayed.
children (UICollection, read-only): UI elements in this scene. Modify to add/remove elements.
on_key (callable): Keyboard handler. Set on ANY scene, regardless of which is active!
pos (Vector): Position offset for all UI elements.
visible (bool): Whether the scene renders.
opacity (float): Scene transparency (0.0-1.0).
Lifecycle Callbacks (override in subclass):
on_enter(): Called when scene becomes active via activate().
on_exit(): Called when scene is deactivated (another scene activates).
on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.
update(dt: float): Called every frame with delta time in seconds.
on_resize(width: int, height: int): Called when window is resized.
Example:
# Basic usage (replacing module functions):
scene = mcrfpy.Scene('main_menu')
scene.children.append(mcrfpy.Caption(text='Welcome', pos=(100, 100)))
scene.on_key = lambda key, action: print(f'Key: {key}')
scene.activate() # Switch to this scene
# Subclassing for lifecycle:
class GameScene(mcrfpy.Scene):
def on_enter(self):
print('Game started!')
def update(self, dt):
self.player.move(dt)
**Properties:**
- `active` *(read-only)*: Whether this scene is currently active (bool, read-only). Only one scene can be active at a time.
- `children` *(read-only)*: UI element collection for this scene (UICollection, read-only). Use to add, remove, or iterate over UI elements. Changes are reflected immediately.
- `name` *(read-only)*: Scene name (str, read-only). Unique identifier for this scene.
- `on_key`: Keyboard event handler (callable or None). Function receives (key: str, action: str) for keyboard events. Set to None to remove the handler.
- `opacity`: Scene opacity (0.0-1.0). Applied to all UI elements during rendering.
- `pos`: Scene position offset (Vector). Applied to all UI elements during rendering.
- `visible`: Scene visibility (bool). If False, scene is not rendered.
**Methods:** **Methods:**
@ -1299,6 +1559,30 @@ Attributes:
name (str): Element name name (str): Element name
w, h (float): Read-only computed size based on texture and scale w, h (float): Read-only computed size based on texture and scale
**Properties:**
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
- `name`: Name for finding elements
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
- `pos`: Position as a Vector
- `scale`: Uniform size factor
- `scale_x`: Horizontal scale factor
- `scale_y`: Vertical scale factor
- `sprite_index`: Which sprite on the texture is shown
- `sprite_number`: Sprite index (DEPRECATED: use sprite_index instead)
- `texture`: Texture object
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
- `x`: X coordinate of top-left corner
- `y`: Y coordinate of top-left corner
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
**Methods:** **Methods:**
#### `get_bounds() -> tuple` #### `get_bounds() -> tuple`
@ -1333,6 +1617,14 @@ Note:
SFML Texture Object SFML Texture Object
**Properties:**
- `sheet_height` *(read-only)*: Number of sprite rows in the texture sheet (int, read-only). Calculated as texture_height / sprite_height.
- `sheet_width` *(read-only)*: Number of sprite columns in the texture sheet (int, read-only). Calculated as texture_width / sprite_width.
- `source` *(read-only)*: Source filename path (str, read-only). The path used to load this texture.
- `sprite_count` *(read-only)*: Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height.
- `sprite_height` *(read-only)*: Height of each sprite in pixels (int, read-only). Specified during texture initialization.
- `sprite_width` *(read-only)*: Width of each sprite in pixels (int, read-only). Specified during texture initialization.
**Methods:** **Methods:**
### TileLayer ### TileLayer
@ -1357,6 +1649,12 @@ Methods:
set(x, y, index): Set tile index at cell position set(x, y, index): Set tile index at cell position
fill(index): Fill entire layer with tile index fill(index): Fill entire layer with tile index
**Properties:**
- `grid_size`: Layer dimensions as (width, height) tuple.
- `texture`: Texture atlas for tile sprites.
- `visible`: Whether the layer is rendered.
- `z_index`: Layer z-order. Negative values render below entities.
**Methods:** **Methods:**
#### `at(x, y) -> int` #### `at(x, y) -> int`
@ -1412,6 +1710,15 @@ Example:
timer.resume() # Resume timer timer.resume() # Resume timer
timer.once = True # Make it one-shot timer.once = True # Make it one-shot
**Properties:**
- `active` *(read-only)*: Whether the timer is active and not paused (bool, read-only). False if cancelled or paused.
- `callback`: The callback function to be called when timer fires (callable). Can be changed while timer is running.
- `interval`: Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running.
- `name` *(read-only)*: Timer name (str, read-only). Unique identifier for this timer.
- `once`: Whether the timer stops after firing once (bool). If False, timer repeats indefinitely.
- `paused` *(read-only)*: Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time.
- `remaining` *(read-only)*: Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused.
**Methods:** **Methods:**
#### `cancel() -> None` #### `cancel() -> None`
@ -1508,6 +1815,11 @@ Iterator for a collection of UI objects
SFML Vector Object SFML Vector Object
**Properties:**
- `int` *(read-only)*: Integer tuple (floor of x and y) for use as dict keys. Read-only.
- `x`: X coordinate of the vector (float)
- `y`: Y coordinate of the vector (float)
**Methods:** **Methods:**
#### `angle() -> float` #### `angle() -> float`
@ -1574,6 +1886,16 @@ Note:
Window singleton for accessing and modifying the game window properties Window singleton for accessing and modifying the game window properties
**Properties:**
- `framerate_limit`: Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate.
- `fullscreen`: Window fullscreen state (bool). Setting this recreates the window.
- `game_resolution`: Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling.
- `resolution`: Window resolution as (width, height) tuple. Setting this recreates the window.
- `scaling_mode`: Viewport scaling mode (str): 'center' (no scaling), 'stretch' (fill window), or 'fit' (maintain aspect ratio).
- `title`: Window title string (str). Displayed in the window title bar.
- `visible`: Window visibility state (bool). Hidden windows still process events.
- `vsync`: Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate.
**Methods:** **Methods:**
#### `center() -> None` #### `center() -> None`

View file

@ -108,7 +108,7 @@
<body> <body>
<div class="container"> <div class="container">
<h1>McRogueFace API Reference</h1> <h1>McRogueFace API Reference</h1>
<p><em>Generated on 2025-12-28 14:29:42</em></p> <p><em>Generated on 2025-12-29 14:23:40</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p> <p><em>This documentation was dynamically generated from the compiled module.</em></p>
<div class="toc"> <div class="toc">
@ -410,6 +410,14 @@ Note:</p>
<div class="method-section"> <div class="method-section">
<h3 id="Animation"><span class="class-name">Animation</span></h3> <h3 id="Animation"><span class="class-name">Animation</span></h3>
<p>Animation object for animating UI properties</p> <p>Animation object for animating UI properties</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>duration</span> (read-only): Animation duration in seconds (float, read-only). Total time for the animation to complete.</li>
<li><span class='property-name'>elapsed</span> (read-only): Elapsed time in seconds (float, read-only). Time since the animation started.</li>
<li><span class='property-name'>is_complete</span> (read-only): Whether animation is complete (bool, read-only). True when elapsed &gt;= duration or complete() was called.</li>
<li><span class='property-name'>is_delta</span> (read-only): Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.</li>
<li><span class='property-name'>property</span> (read-only): Target property name (str, read-only). The property being animated (e.g., &#x27;pos&#x27;, &#x27;opacity&#x27;, &#x27;sprite_index&#x27;).</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -495,6 +503,29 @@ Attributes:
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name name (str): Element name
</p> </p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
<li><span class='property-name'>center</span>: Center position of the arc</li>
<li><span class='property-name'>color</span>: Arc color</li>
<li><span class='property-name'>end_angle</span>: Ending angle in degrees</li>
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
<li><span class='property-name'>name</span>: Name for finding this element.</li>
<li><span class='property-name'>on_click</span>: Callable executed when arc is clicked.</li>
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element&#x27;s bounds.</li>
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element&#x27;s bounds.</li>
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
<li><span class='property-name'>pos</span>: Position as a Vector (same as center).</li>
<li><span class='property-name'>radius</span>: Arc radius in pixels</li>
<li><span class='property-name'>start_angle</span>: Starting angle in degrees</li>
<li><span class='property-name'>thickness</span>: Line thickness</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first).</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -567,6 +598,30 @@ Attributes:
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name name (str): Element name
w, h (float): Read-only computed size based on text and font</p> w, h (float): Read-only computed size based on text and font</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
<li><span class='property-name'>fill_color</span>: Fill color of the text</li>
<li><span class='property-name'>font_size</span>: Font size (integer) in points</li>
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
<li><span class='property-name'>name</span>: Name for finding elements</li>
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element&#x27;s bounds.</li>
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element&#x27;s bounds.</li>
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
<li><span class='property-name'>outline</span>: Thickness of the border</li>
<li><span class='property-name'>outline_color</span>: Outline color of the text</li>
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
<li><span class='property-name'>pos</span>: (x, y) vector</li>
<li><span class='property-name'>text</span>: The text displayed</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
<li><span class='property-name'>x</span>: X coordinate of top-left corner</li>
<li><span class='property-name'>y</span>: Y coordinate of top-left corner</li>
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -632,6 +687,28 @@ Attributes:
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name name (str): Element name
</p> </p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
<li><span class='property-name'>center</span>: Center position of the circle</li>
<li><span class='property-name'>fill_color</span>: Fill color of the circle</li>
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
<li><span class='property-name'>name</span>: Name for finding this element.</li>
<li><span class='property-name'>on_click</span>: Callable executed when circle is clicked.</li>
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element&#x27;s bounds.</li>
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element&#x27;s bounds.</li>
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
<li><span class='property-name'>outline</span>: Outline thickness (0 for no outline)</li>
<li><span class='property-name'>outline_color</span>: Outline color of the circle</li>
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
<li><span class='property-name'>pos</span>: Position as a Vector (same as center).</li>
<li><span class='property-name'>radius</span>: Circle radius in pixels</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first).</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -668,6 +745,13 @@ Note:</p>
<div class="method-section"> <div class="method-section">
<h3 id="Color"><span class="class-name">Color</span></h3> <h3 id="Color"><span class="class-name">Color</span></h3>
<p>SFML Color Object</p> <p>SFML Color Object</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>a</span>: Alpha component (0-255, where 0=transparent, 255=opaque). Automatically clamped to valid range.</li>
<li><span class='property-name'>b</span>: Blue component (0-255). Automatically clamped to valid range.</li>
<li><span class='property-name'>g</span>: Green component (0-255). Automatically clamped to valid range.</li>
<li><span class='property-name'>r</span>: Red component (0-255). Automatically clamped to valid range.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -722,6 +806,12 @@ Methods:
at(x, y): Get color at cell position at(x, y): Get color at cell position
set(x, y, color): Set color at cell position set(x, y, color): Set color at cell position
fill(color): Fill entire layer with color</p> fill(color): Fill entire layer with color</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>grid_size</span>: Layer dimensions as (width, height) tuple.</li>
<li><span class='property-name'>visible</span>: Whether the layer is rendered.</li>
<li><span class='property-name'>z_index</span>: Layer z-order. Negative values render below entities.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -774,6 +864,13 @@ Call this after the entity moves to update the visibility layer.</p>
<div class="method-section"> <div class="method-section">
<h3 id="Drawable"><span class="class-name">Drawable</span></h3> <h3 id="Drawable"><span class="class-name">Drawable</span></h3>
<p>Base class for all drawable UI elements</p> <p>Base class for all drawable UI elements</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -835,6 +932,20 @@ Attributes:
visible (bool): Visibility state visible (bool): Visibility state
opacity (float): Opacity value opacity (float): Opacity value
name (str): Element name</p> name (str): Element name</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>draw_pos</span>: Entity position (graphically)</li>
<li><span class='property-name'>grid</span>: Grid this entity belongs to. Get: Returns the Grid or None. Set: Assign a Grid to move entity, or None to remove from grid.</li>
<li><span class='property-name'>gridstate</span>: Grid point states for the entity</li>
<li><span class='property-name'>name</span>: Name for finding elements</li>
<li><span class='property-name'>opacity</span>: Opacity (0.0 = transparent, 1.0 = opaque)</li>
<li><span class='property-name'>pos</span>: Entity position (integer grid coordinates)</li>
<li><span class='property-name'>sprite_index</span>: Sprite index on the texture on the display</li>
<li><span class='property-name'>sprite_number</span>: Sprite index (DEPRECATED: use sprite_index instead)</li>
<li><span class='property-name'>visible</span>: Visibility flag</li>
<li><span class='property-name'>x</span>: Entity x position</li>
<li><span class='property-name'>y</span>: Entity y position</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -956,6 +1067,13 @@ when the entity moves if it has a grid with perspective set.</p>
<div class="method-section"> <div class="method-section">
<h3 id="FOV"><span class="class-name">FOV</span></h3> <h3 id="FOV"><span class="class-name">FOV</span></h3>
<p><em>Inherits from: IntEnum</em></p> <p><em>Inherits from: IntEnum</em></p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>denominator</span>: the denominator of a rational number in lowest terms</li>
<li><span class='property-name'>imag</span>: the imaginary part of a complex number</li>
<li><span class='property-name'>numerator</span>: the numerator of a rational number in lowest terms</li>
<li><span class='property-name'>real</span>: the real part of a complex number</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -1040,6 +1158,11 @@ Also known as the population count.
<div class="method-section"> <div class="method-section">
<h3 id="Font"><span class="class-name">Font</span></h3> <h3 id="Font"><span class="class-name">Font</span></h3>
<p>SFML Font Object</p> <p>SFML Font Object</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>family</span> (read-only): Font family name (str, read-only). Retrieved from font metadata.</li>
<li><span class='property-name'>source</span> (read-only): Source filename path (str, read-only). The path used to load this font.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
</div> </div>
@ -1085,6 +1208,33 @@ Attributes:
name (str): Element name name (str): Element name
clip_children (bool): Whether to clip children to frame bounds clip_children (bool): Whether to clip children to frame bounds
cache_subtree (bool): Cache subtree rendering to texture</p> cache_subtree (bool): Cache subtree rendering to texture</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
<li><span class='property-name'>cache_subtree</span>: #144: Cache subtree rendering to texture for performance</li>
<li><span class='property-name'>children</span>: UICollection of objects on top of this one</li>
<li><span class='property-name'>clip_children</span>: Whether to clip children to frame bounds</li>
<li><span class='property-name'>fill_color</span>: Fill color of the rectangle</li>
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
<li><span class='property-name'>h</span>: height of the rectangle</li>
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
<li><span class='property-name'>name</span>: Name for finding elements</li>
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element&#x27;s bounds.</li>
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element&#x27;s bounds.</li>
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
<li><span class='property-name'>outline</span>: Thickness of the border</li>
<li><span class='property-name'>outline_color</span>: Outline color of the rectangle</li>
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
<li><span class='property-name'>pos</span>: Position as a Vector</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
<li><span class='property-name'>w</span>: width of the rectangle</li>
<li><span class='property-name'>x</span>: X coordinate of top-left corner</li>
<li><span class='property-name'>y</span>: Y coordinate of top-left corner</li>
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -1168,6 +1318,49 @@ Attributes:
opacity (float): Opacity value opacity (float): Opacity value
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name</p> name (str): Element name</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
<li><span class='property-name'>center</span>: Grid coordinate at the center of the Grid&#x27;s view (pan)</li>
<li><span class='property-name'>center_x</span>: center of the view X-coordinate</li>
<li><span class='property-name'>center_y</span>: center of the view Y-coordinate</li>
<li><span class='property-name'>children</span>: UICollection of UIDrawable children (speech bubbles, effects, overlays)</li>
<li><span class='property-name'>entities</span>: EntityCollection of entities on this grid</li>
<li><span class='property-name'>fill_color</span>: Background fill color of the grid</li>
<li><span class='property-name'>fov</span>: FOV algorithm for this grid (mcrfpy.FOV enum). Used by entity.updateVisibility() and layer methods when fov=None.</li>
<li><span class='property-name'>fov_radius</span>: Default FOV radius for this grid. Used when radius not specified.</li>
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
<li><span class='property-name'>grid_size</span>: Grid dimensions (grid_x, grid_y)</li>
<li><span class='property-name'>grid_x</span>: Grid x dimension</li>
<li><span class='property-name'>grid_y</span>: Grid y dimension</li>
<li><span class='property-name'>h</span>: visible widget height</li>
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
<li><span class='property-name'>hovered_cell</span>: Currently hovered cell as (x, y) tuple, or None if not hovering.</li>
<li><span class='property-name'>layers</span>: List of grid layers (ColorLayer, TileLayer) sorted by z_index</li>
<li><span class='property-name'>name</span>: Name for finding elements</li>
<li><span class='property-name'>on_cell_click</span>: Callback when a grid cell is clicked. Called with (cell_x, cell_y).</li>
<li><span class='property-name'>on_cell_enter</span>: Callback when mouse enters a grid cell. Called with (cell_x, cell_y).</li>
<li><span class='property-name'>on_cell_exit</span>: Callback when mouse exits a grid cell. Called with (cell_x, cell_y).</li>
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element&#x27;s bounds.</li>
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element&#x27;s bounds.</li>
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
<li><span class='property-name'>perspective</span>: Entity whose perspective to use for FOV rendering (None for omniscient view). Setting an entity automatically enables perspective mode.</li>
<li><span class='property-name'>perspective_enabled</span>: Whether to use perspective-based FOV rendering. When True with no valid entity, all cells appear undiscovered.</li>
<li><span class='property-name'>pos</span>: Position of the grid as Vector</li>
<li><span class='property-name'>position</span>: Position of the grid (x, y)</li>
<li><span class='property-name'>size</span>: Size of the grid (width, height)</li>
<li><span class='property-name'>texture</span>: Texture of the grid</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
<li><span class='property-name'>w</span>: visible widget width</li>
<li><span class='property-name'>x</span>: top-left corner X-coordinate</li>
<li><span class='property-name'>y</span>: top-left corner Y-coordinate</li>
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
<li><span class='property-name'>zoom</span>: zoom factor for displaying the Grid</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -1325,12 +1518,24 @@ Note:</p>
<div class="method-section"> <div class="method-section">
<h3 id="GridPoint"><span class="class-name">GridPoint</span></h3> <h3 id="GridPoint"><span class="class-name">GridPoint</span></h3>
<p>UIGridPoint object</p> <p>UIGridPoint object</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>entities</span> (read-only): List of entities at this grid cell (read-only)</li>
<li><span class='property-name'>transparent</span>: Is the GridPoint transparent</li>
<li><span class='property-name'>walkable</span>: Is the GridPoint walkable</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
</div> </div>
<div class="method-section"> <div class="method-section">
<h3 id="GridPointState"><span class="class-name">GridPointState</span></h3> <h3 id="GridPointState"><span class="class-name">GridPointState</span></h3>
<p>UIGridPointState object</p> <p>UIGridPointState object</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>discovered</span>: Has the GridPointState been discovered</li>
<li><span class='property-name'>point</span>: GridPoint at this position (None if not discovered)</li>
<li><span class='property-name'>visible</span>: Is the GridPointState visible</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
</div> </div>
@ -1364,6 +1569,27 @@ Attributes:
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name name (str): Element name
</p> </p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
<li><span class='property-name'>color</span>: Line color as a Color object.</li>
<li><span class='property-name'>end</span>: Ending point of the line as a Vector.</li>
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
<li><span class='property-name'>name</span>: Name for finding this element.</li>
<li><span class='property-name'>on_click</span>: Callable executed when line is clicked.</li>
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element&#x27;s bounds.</li>
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element&#x27;s bounds.</li>
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
<li><span class='property-name'>pos</span>: Position as a Vector (midpoint of line).</li>
<li><span class='property-name'>start</span>: Starting point of the line as a Vector.</li>
<li><span class='property-name'>thickness</span>: Line thickness in pixels.</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first).</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -1399,7 +1625,57 @@ Note:</p>
<div class="method-section"> <div class="method-section">
<h3 id="Scene"><span class="class-name">Scene</span></h3> <h3 id="Scene"><span class="class-name">Scene</span></h3>
<p>Base class for object-oriented scenes</p> <p>Scene(name: str)
Object-oriented scene management with lifecycle callbacks.
This is the recommended approach for scene management, replacing module-level
functions like createScene(), setScene(), and sceneUI(). Key advantage: you can
set on_key handlers on ANY scene, not just the currently active one.
Args:
name: Unique identifier for this scene. Used for scene transitions.
Properties:
name (str, read-only): Scene&#x27;s unique identifier.
active (bool, read-only): Whether this scene is currently displayed.
children (UICollection, read-only): UI elements in this scene. Modify to add/remove elements.
on_key (callable): Keyboard handler. Set on ANY scene, regardless of which is active!
pos (Vector): Position offset for all UI elements.
visible (bool): Whether the scene renders.
opacity (float): Scene transparency (0.0-1.0).
Lifecycle Callbacks (override in subclass):
on_enter(): Called when scene becomes active via activate().
on_exit(): Called when scene is deactivated (another scene activates).
on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.
update(dt: float): Called every frame with delta time in seconds.
on_resize(width: int, height: int): Called when window is resized.
Example:
# Basic usage (replacing module functions):
scene = mcrfpy.Scene(&#x27;main_menu&#x27;)
scene.children.append(mcrfpy.Caption(text=&#x27;Welcome&#x27;, pos=(100, 100)))
scene.on_key = lambda key, action: print(f&#x27;Key: {key}&#x27;)
scene.activate() # Switch to this scene
# Subclassing for lifecycle:
class GameScene(mcrfpy.Scene):
def on_enter(self):
print(&#x27;Game started!&#x27;)
def update(self, dt):
self.player.move(dt)
</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>active</span> (read-only): Whether this scene is currently active (bool, read-only). Only one scene can be active at a time.</li>
<li><span class='property-name'>children</span> (read-only): UI element collection for this scene (UICollection, read-only). Use to add, remove, or iterate over UI elements. Changes are reflected immediately.</li>
<li><span class='property-name'>name</span> (read-only): Scene name (str, read-only). Unique identifier for this scene.</li>
<li><span class='property-name'>on_key</span>: Keyboard event handler (callable or None). Function receives (key: str, action: str) for keyboard events. Set to None to remove the handler.</li>
<li><span class='property-name'>opacity</span>: Scene opacity (0.0-1.0). Applied to all UI elements during rendering.</li>
<li><span class='property-name'>pos</span>: Scene position offset (Vector). Applied to all UI elements during rendering.</li>
<li><span class='property-name'>visible</span>: Scene visibility (bool). If False, scene is not rendered.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -1459,6 +1735,31 @@ Attributes:
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name name (str): Element name
w, h (float): Read-only computed size based on texture and scale</p> w, h (float): Read-only computed size based on texture and scale</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
<li><span class='property-name'>name</span>: Name for finding elements</li>
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element&#x27;s bounds.</li>
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element&#x27;s bounds.</li>
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
<li><span class='property-name'>pos</span>: Position as a Vector</li>
<li><span class='property-name'>scale</span>: Uniform size factor</li>
<li><span class='property-name'>scale_x</span>: Horizontal scale factor</li>
<li><span class='property-name'>scale_y</span>: Vertical scale factor</li>
<li><span class='property-name'>sprite_index</span>: Which sprite on the texture is shown</li>
<li><span class='property-name'>sprite_number</span>: Sprite index (DEPRECATED: use sprite_index instead)</li>
<li><span class='property-name'>texture</span>: Texture object</li>
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
<li><span class='property-name'>x</span>: X coordinate of top-left corner</li>
<li><span class='property-name'>y</span>: Y coordinate of top-left corner</li>
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -1495,6 +1796,15 @@ Note:</p>
<div class="method-section"> <div class="method-section">
<h3 id="Texture"><span class="class-name">Texture</span></h3> <h3 id="Texture"><span class="class-name">Texture</span></h3>
<p>SFML Texture Object</p> <p>SFML Texture Object</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>sheet_height</span> (read-only): Number of sprite rows in the texture sheet (int, read-only). Calculated as texture_height / sprite_height.</li>
<li><span class='property-name'>sheet_width</span> (read-only): Number of sprite columns in the texture sheet (int, read-only). Calculated as texture_width / sprite_width.</li>
<li><span class='property-name'>source</span> (read-only): Source filename path (str, read-only). The path used to load this texture.</li>
<li><span class='property-name'>sprite_count</span> (read-only): Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height.</li>
<li><span class='property-name'>sprite_height</span> (read-only): Height of each sprite in pixels (int, read-only). Specified during texture initialization.</li>
<li><span class='property-name'>sprite_width</span> (read-only): Width of each sprite in pixels (int, read-only). Specified during texture initialization.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
</div> </div>
@ -1519,6 +1829,13 @@ Methods:
at(x, y): Get tile index at cell position at(x, y): Get tile index at cell position
set(x, y, index): Set tile index at cell position set(x, y, index): Set tile index at cell position
fill(index): Fill entire layer with tile index</p> fill(index): Fill entire layer with tile index</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>grid_size</span>: Layer dimensions as (width, height) tuple.</li>
<li><span class='property-name'>texture</span>: Texture atlas for tile sprites.</li>
<li><span class='property-name'>visible</span>: Whether the layer is rendered.</li>
<li><span class='property-name'>z_index</span>: Layer z-order. Negative values render below entities.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -1578,6 +1895,16 @@ Example:
timer.pause() # Pause timer timer.pause() # Pause timer
timer.resume() # Resume timer timer.resume() # Resume timer
timer.once = True # Make it one-shot</p> timer.once = True # Make it one-shot</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>active</span> (read-only): Whether the timer is active and not paused (bool, read-only). False if cancelled or paused.</li>
<li><span class='property-name'>callback</span>: The callback function to be called when timer fires (callable). Can be changed while timer is running.</li>
<li><span class='property-name'>interval</span>: Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running.</li>
<li><span class='property-name'>name</span> (read-only): Timer name (str, read-only). Unique identifier for this timer.</li>
<li><span class='property-name'>once</span>: Whether the timer stops after firing once (bool). If False, timer repeats indefinitely.</li>
<li><span class='property-name'>paused</span> (read-only): Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time.</li>
<li><span class='property-name'>remaining</span> (read-only): Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -1681,6 +2008,12 @@ Use name-based .find() for stable element access.</p>
<div class="method-section"> <div class="method-section">
<h3 id="Vector"><span class="class-name">Vector</span></h3> <h3 id="Vector"><span class="class-name">Vector</span></h3>
<p>SFML Vector Object</p> <p>SFML Vector Object</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>int</span> (read-only): Integer tuple (floor of x and y) for use as dict keys. Read-only.</li>
<li><span class='property-name'>x</span>: X coordinate of the vector (float)</li>
<li><span class='property-name'>y</span>: Y coordinate of the vector (float)</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -1747,6 +2080,17 @@ Note:</p>
<div class="method-section"> <div class="method-section">
<h3 id="Window"><span class="class-name">Window</span></h3> <h3 id="Window"><span class="class-name">Window</span></h3>
<p>Window singleton for accessing and modifying the game window properties</p> <p>Window singleton for accessing and modifying the game window properties</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>framerate_limit</span>: Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate.</li>
<li><span class='property-name'>fullscreen</span>: Window fullscreen state (bool). Setting this recreates the window.</li>
<li><span class='property-name'>game_resolution</span>: Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling.</li>
<li><span class='property-name'>resolution</span>: Window resolution as (width, height) tuple. Setting this recreates the window.</li>
<li><span class='property-name'>scaling_mode</span>: Viewport scaling mode (str): &#x27;center&#x27; (no scaling), &#x27;stretch&#x27; (fill window), or &#x27;fit&#x27; (maintain aspect ratio).</li>
<li><span class='property-name'>title</span>: Window title string (str). Displayed in the window title bar.</li>
<li><span class='property-name'>visible</span>: Window visibility state (bool). Hidden windows still process events.</li>
<li><span class='property-name'>vsync</span>: Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate.</li>
</ul>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">

View file

@ -0,0 +1,30 @@
"""McRogueFace - Part 0: Setting Up McRogueFace
Documentation: https://mcrogueface.github.io/tutorial/part_00_setup
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_00_setup/part_00_setup.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Create a Scene object - this is the preferred approach
scene = mcrfpy.Scene("hello")
# Create a caption to display text
title = mcrfpy.Caption(
pos=(512, 300),
text="Hello, Roguelike!"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 32
# Add the caption to the scene's UI collection
scene.children.append(title)
# Activate the scene to display it
scene.activate()
# Note: There is no run() function!
# The engine is already running - your script is imported by it.

View file

@ -0,0 +1,121 @@
"""McRogueFace - Part 1: The '@' and the Dungeon Grid
Documentation: https://mcrogueface.github.io/tutorial/part_01_grid_movement
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Sprite indices for CP437 tileset
SPRITE_AT = 64 # '@' - player character
SPRITE_FLOOR = 46 # '.' - floor tile
# Grid dimensions (in tiles)
GRID_WIDTH = 20
GRID_HEIGHT = 15
# Create the scene
scene = mcrfpy.Scene("game")
# Load the texture (sprite sheet)
# Parameters: path, sprite_width, sprite_height
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
# The grid displays tiles and contains entities
grid = mcrfpy.Grid(
pos=(100, 80), # Position on screen (pixels)
size=(640, 480), # Display size (pixels)
grid_size=(GRID_WIDTH, GRID_HEIGHT), # Size in tiles
texture=texture
)
# Set the zoom level for better visibility
grid.zoom = 2.0
# Fill the grid with floor tiles
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
# Create the player entity at the center of the grid
player = mcrfpy.Entity(
grid_pos=(GRID_WIDTH // 2, GRID_HEIGHT // 2), # Grid coordinates, not pixels!
texture=texture,
sprite_index=SPRITE_AT
)
# Add the player to the grid
# Option 1: Use the grid parameter in constructor
# player = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=SPRITE_AT, grid=grid)
# Option 2: Append to grid.entities (what we will use)
grid.entities.append(player)
# Add the grid to the scene
scene.children.append(grid)
# Add a title caption
title = mcrfpy.Caption(
pos=(100, 20),
text="Part 1: Grid Movement - Use Arrow Keys or WASD"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 18
scene.children.append(title)
# Add a position display
pos_display = mcrfpy.Caption(
pos=(100, 50),
text=f"Player Position: ({player.x}, {player.y})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input to move the player.
Args:
key: The key that was pressed (e.g., "W", "Up", "Space")
action: Either "start" (key pressed) or "end" (key released)
"""
# Only respond to key press, not release
if action != "start":
return
# Get current player position
px, py = int(player.x), int(player.y)
# Calculate new position based on key
if key == "W" or key == "Up":
py -= 1 # Up decreases Y
elif key == "S" or key == "Down":
py += 1 # Down increases Y
elif key == "A" or key == "Left":
px -= 1 # Left decreases X
elif key == "D" or key == "Right":
px += 1 # Right increases X
elif key == "Escape":
mcrfpy.exit()
return
# Update player position
player.x = px
player.y = py
# Update the position display
pos_display.text = f"Player Position: ({player.x}, {player.y})"
# Set the key handler on the scene
# This is the preferred approach - works on ANY scene, not just the active one
scene.on_key = handle_keys
# Activate the scene
scene.activate()
print("Part 1 loaded! Use WASD or Arrow keys to move.")

View file

@ -0,0 +1,206 @@
"""McRogueFace - Part 2: Walls, Floors, and Collision
Documentation: https://mcrogueface.github.io/tutorial/part_02_tiles_collision
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
# Grid dimensions
GRID_WIDTH = 30
GRID_HEIGHT = 20
# =============================================================================
# Map Creation
# =============================================================================
def create_map(grid: mcrfpy.Grid) -> None:
"""Fill the grid with walls and floors.
Creates a simple room with walls around the edges and floor in the middle.
"""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
# Place walls around the edges
if x == 0 or x == GRID_WIDTH - 1 or y == 0 or y == GRID_HEIGHT - 1:
cell.tilesprite = SPRITE_WALL
cell.walkable = False
else:
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
# Add some interior walls to make it interesting
# Vertical wall
for y in range(5, 15):
cell = grid.at(10, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
# Horizontal wall
for x in range(15, 25):
cell = grid.at(x, 10)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
# Leave gaps for doorways
grid.at(10, 10).tilesprite = SPRITE_FLOOR
grid.at(10, 10).walkable = True
grid.at(20, 10).tilesprite = SPRITE_FLOOR
grid.at(20, 10).walkable = True
# =============================================================================
# Collision Detection
# =============================================================================
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
"""Check if a position is valid for movement.
Args:
grid: The game grid
x: Target X coordinate (in tiles)
y: Target Y coordinate (in tiles)
Returns:
True if the position is walkable, False otherwise
"""
# Check grid bounds first
if x < 0 or x >= GRID_WIDTH:
return False
if y < 0 or y >= GRID_HEIGHT:
return False
# Check if the tile is walkable
cell = grid.at(x, y)
return cell.walkable
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(80, 100),
size=(720, 480),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture
)
grid.zoom = 1.5
# Build the map
create_map(grid)
# Create the player in the center of the left room
player = mcrfpy.Entity(
grid_pos=(5, 10),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(80, 20),
text="Part 2: Walls and Collision"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(80, 55),
text="WASD or Arrow Keys to move | Walls block movement"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
pos_display = mcrfpy.Caption(
pos=(80, 600),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
status_display = mcrfpy.Caption(
pos=(400, 600),
text="Status: Ready"
)
status_display.fill_color = mcrfpy.Color(100, 200, 100)
status_display.font_size = 16
scene.children.append(status_display)
# =============================================================================
# Input Handling
# =============================================================================
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input with collision detection."""
if action != "start":
return
# Get current position
px, py = int(player.x), int(player.y)
# Calculate intended new position
new_x, new_y = px, py
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
elif key == "Escape":
mcrfpy.exit()
return
else:
return # Ignore other keys
# Check collision before moving
if can_move_to(grid, new_x, new_y):
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
status_display.text = "Status: Moved"
status_display.fill_color = mcrfpy.Color(100, 200, 100)
else:
status_display.text = "Status: Blocked!"
status_display.fill_color = mcrfpy.Color(200, 100, 100)
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 2 loaded! Try walking into walls.")

View file

@ -0,0 +1,356 @@
"""McRogueFace - Part 3: Procedural Dungeon Generation
Documentation: https://mcrogueface.github.io/tutorial/part_03_dungeon_generation
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
import random
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
# Grid dimensions
GRID_WIDTH = 50
GRID_HEIGHT = 35
# Room generation parameters
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 8
# =============================================================================
# Room Class
# =============================================================================
class RectangularRoom:
"""A rectangular room with its position and size."""
def __init__(self, x: int, y: int, width: int, height: int):
"""Create a new room.
Args:
x: Left edge X coordinate
y: Top edge Y coordinate
width: Room width in tiles
height: Room height in tiles
"""
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -> tuple[int, int]:
"""Return the center coordinates of the room."""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self) -> tuple[slice, slice]:
"""Return the inner area of the room (excluding walls).
The inner area is one tile smaller on each side to leave room
for walls between adjacent rooms.
"""
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
def intersects(self, other: "RectangularRoom") -> bool:
"""Check if this room overlaps with another room.
Args:
other: Another RectangularRoom to check against
Returns:
True if the rooms overlap, False otherwise
"""
return (
self.x1 <= other.x2 and
self.x2 >= other.x1 and
self.y1 <= other.y2 and
self.y2 >= other.y1
)
# =============================================================================
# Dungeon Generation
# =============================================================================
def fill_with_walls(grid: mcrfpy.Grid) -> None:
"""Fill the entire grid with wall tiles."""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
cell.transparent = False
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
"""Carve out a room by setting its inner tiles to floor.
Args:
grid: The game grid
room: The room to carve
"""
inner_x, inner_y = room.inner
for y in range(inner_y.start, inner_y.stop):
for x in range(inner_x.start, inner_x.stop):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel.
Args:
grid: The game grid
x1: Starting X coordinate
x2: Ending X coordinate
y: Y coordinate of the tunnel
"""
start_x = min(x1, x2)
end_x = max(x1, x2)
for x in range(start_x, end_x + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel.
Args:
grid: The game grid
y1: Starting Y coordinate
y2: Ending Y coordinate
x: X coordinate of the tunnel
"""
start_y = min(y1, y2)
end_y = max(y1, y2)
for y in range(start_y, end_y + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_l_tunnel(
grid: mcrfpy.Grid,
start: tuple[int, int],
end: tuple[int, int]
) -> None:
"""Carve an L-shaped tunnel between two points.
Randomly chooses to go horizontal-then-vertical or vertical-then-horizontal.
Args:
grid: The game grid
start: Starting (x, y) coordinates
end: Ending (x, y) coordinates
"""
x1, y1 = start
x2, y2 = end
# Randomly choose whether to go horizontal or vertical first
if random.random() < 0.5:
# Horizontal first, then vertical
carve_tunnel_horizontal(grid, x1, x2, y1)
carve_tunnel_vertical(grid, y1, y2, x2)
else:
# Vertical first, then horizontal
carve_tunnel_vertical(grid, y1, y2, x1)
carve_tunnel_horizontal(grid, x1, x2, y2)
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
"""Generate a dungeon with rooms and tunnels.
Args:
grid: The game grid to generate the dungeon in
Returns:
The (x, y) coordinates where the player should start
"""
# Start with all walls
fill_with_walls(grid)
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
# Random room dimensions
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
# Random position (leaving 1-tile border)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
# Check for overlap with existing rooms
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue # Skip this room, try another
# No overlap - carve out the room
carve_room(grid, new_room)
# Connect to previous room with a tunnel
if rooms:
# Tunnel from this room's center to the previous room's center
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Return the center of the first room as the player start position
if rooms:
return rooms[0].center
else:
# Fallback if no rooms were generated
return GRID_WIDTH // 2, GRID_HEIGHT // 2
# =============================================================================
# Collision Detection
# =============================================================================
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
"""Check if a position is valid for movement."""
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
return False
return grid.at(x, y).walkable
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(50, 80),
size=(800, 560),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture
)
grid.zoom = 1.0
# Generate the dungeon and get player start position
player_start_x, player_start_y = generate_dungeon(grid)
# Create the player at the starting position
player = mcrfpy.Entity(
grid_pos=(player_start_x, player_start_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(50, 15),
text="Part 3: Procedural Dungeon Generation"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(50, 50),
text="WASD/Arrows: Move | R: Regenerate dungeon | Escape: Quit"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
pos_display = mcrfpy.Caption(
pos=(50, 660),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
room_display = mcrfpy.Caption(
pos=(400, 660),
text="Press R to regenerate the dungeon"
)
room_display.fill_color = mcrfpy.Color(100, 200, 100)
room_display.font_size = 16
scene.children.append(room_display)
# =============================================================================
# Input Handling
# =============================================================================
def regenerate_dungeon() -> None:
"""Generate a new dungeon and reposition the player."""
new_x, new_y = generate_dungeon(grid)
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
room_display.text = "New dungeon generated!"
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input."""
if action != "start":
return
px, py = int(player.x), int(player.y)
new_x, new_y = px, py
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
elif key == "R":
regenerate_dungeon()
return
elif key == "Escape":
mcrfpy.exit()
return
else:
return
if can_move_to(grid, new_x, new_y):
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 3 loaded! Explore the dungeon or press R to regenerate.")

View file

@ -0,0 +1,363 @@
"""McRogueFace - Part 4: Field of View
Documentation: https://mcrogueface.github.io/tutorial/part_04_fov
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_04_fov/part_04_fov.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
import random
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
# Grid dimensions
GRID_WIDTH = 50
GRID_HEIGHT = 35
# Room generation parameters
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 8
# FOV settings
FOV_RADIUS = 8
# Visibility colors (applied as overlays)
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) # Fully transparent - show tile
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) # Dark blue tint - dimmed
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) # Solid black - hidden
# =============================================================================
# Room Class (from Part 3)
# =============================================================================
class RectangularRoom:
"""A rectangular room with its position and size."""
def __init__(self, x: int, y: int, width: int, height: int):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -> tuple[int, int]:
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self) -> tuple[slice, slice]:
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
def intersects(self, other: "RectangularRoom") -> bool:
return (
self.x1 <= other.x2 and
self.x2 >= other.x1 and
self.y1 <= other.y2 and
self.y2 >= other.y1
)
# =============================================================================
# Exploration Tracking
# =============================================================================
# Track which tiles have been discovered (seen at least once)
explored: list[list[bool]] = []
def init_explored() -> None:
"""Initialize the explored array to all False."""
global explored
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
def mark_explored(x: int, y: int) -> None:
"""Mark a tile as explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
explored[y][x] = True
def is_explored(x: int, y: int) -> bool:
"""Check if a tile has been explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
return explored[y][x]
return False
# =============================================================================
# Dungeon Generation (from Part 3, with transparent property)
# =============================================================================
def fill_with_walls(grid: mcrfpy.Grid) -> None:
"""Fill the entire grid with wall tiles."""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
cell.transparent = False # Walls block line of sight
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
"""Carve out a room by setting its inner tiles to floor."""
inner_x, inner_y = room.inner
for y in range(inner_y.start, inner_y.stop):
for x in range(inner_x.start, inner_x.stop):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True # Floors allow line of sight
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_l_tunnel(
grid: mcrfpy.Grid,
start: tuple[int, int],
end: tuple[int, int]
) -> None:
"""Carve an L-shaped tunnel between two points."""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
carve_tunnel_horizontal(grid, x1, x2, y1)
carve_tunnel_vertical(grid, y1, y2, x2)
else:
carve_tunnel_vertical(grid, y1, y2, x1)
carve_tunnel_horizontal(grid, x1, x2, y2)
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
"""Generate a dungeon with rooms and tunnels."""
fill_with_walls(grid)
init_explored() # Reset exploration when generating new dungeon
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
if rooms:
return rooms[0].center
return GRID_WIDTH // 2, GRID_HEIGHT // 2
# =============================================================================
# Field of View
# =============================================================================
def update_fov(grid: mcrfpy.Grid, fov_layer, player_x: int, player_y: int) -> None:
"""Update the field of view visualization.
Args:
grid: The game grid
fov_layer: The ColorLayer for FOV visualization
player_x: Player's X position
player_y: Player's Y position
"""
# Compute FOV from player position
grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
# Update each tile's visibility
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if grid.is_in_fov(x, y):
# Currently visible - mark as explored and show clearly
mark_explored(x, y)
fov_layer.set(x, y, COLOR_VISIBLE)
elif is_explored(x, y):
# Previously seen but not currently visible - show dimmed
fov_layer.set(x, y, COLOR_DISCOVERED)
else:
# Never seen - hide completely
fov_layer.set(x, y, COLOR_UNKNOWN)
# =============================================================================
# Collision Detection
# =============================================================================
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
"""Check if a position is valid for movement."""
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
return False
return grid.at(x, y).walkable
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(50, 80),
size=(800, 560),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture
)
grid.zoom = 1.0
# Generate the dungeon
player_start_x, player_start_y = generate_dungeon(grid)
# Add a color layer for FOV visualization (below entities)
fov_layer = grid.add_layer("color", z_index=-1)
# Initialize the FOV layer to all black (unknown)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Create the player
player = mcrfpy.Entity(
grid_pos=(player_start_x, player_start_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Calculate initial FOV
update_fov(grid, fov_layer, player_start_x, player_start_y)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(50, 15),
text="Part 4: Field of View"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(50, 50),
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
pos_display = mcrfpy.Caption(
pos=(50, 660),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
fov_display = mcrfpy.Caption(
pos=(400, 660),
text=f"FOV Radius: {FOV_RADIUS}"
)
fov_display.fill_color = mcrfpy.Color(100, 200, 100)
fov_display.font_size = 16
scene.children.append(fov_display)
# =============================================================================
# Input Handling
# =============================================================================
def regenerate_dungeon() -> None:
"""Generate a new dungeon and reposition the player."""
new_x, new_y = generate_dungeon(grid)
player.x = new_x
player.y = new_y
# Reset FOV layer to unknown
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Calculate new FOV
update_fov(grid, fov_layer, new_x, new_y)
pos_display.text = f"Position: ({new_x}, {new_y})"
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input."""
if action != "start":
return
px, py = int(player.x), int(player.y)
new_x, new_y = px, py
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
elif key == "R":
regenerate_dungeon()
return
elif key == "Escape":
mcrfpy.exit()
return
else:
return
if can_move_to(grid, new_x, new_y):
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
# Update FOV after movement
update_fov(grid, fov_layer, new_x, new_y)
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 4 loaded! Explore the dungeon - watch the fog of war!")

View file

@ -0,0 +1,685 @@
"""McRogueFace - Part 5: Placing Enemies
Documentation: https://mcrogueface.github.io/tutorial/part_05_enemies
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_05_enemies/part_05_enemies.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
import random
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
# Enemy sprites (lowercase letters in CP437)
SPRITE_GOBLIN = 103 # 'g'
SPRITE_ORC = 111 # 'o'
SPRITE_TROLL = 116 # 't'
# Grid dimensions
GRID_WIDTH = 50
GRID_HEIGHT = 35
# Room generation parameters
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 8
# Enemy spawn parameters
MAX_ENEMIES_PER_ROOM = 3
# FOV settings
FOV_RADIUS = 8
# Visibility colors
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
# =============================================================================
# Enemy Data
# =============================================================================
# Enemy templates - stats for each enemy type
ENEMY_TEMPLATES = {
"goblin": {
"sprite": SPRITE_GOBLIN,
"hp": 6,
"max_hp": 6,
"attack": 3,
"defense": 0,
"color": mcrfpy.Color(100, 200, 100) # Greenish
},
"orc": {
"sprite": SPRITE_ORC,
"hp": 10,
"max_hp": 10,
"attack": 4,
"defense": 1,
"color": mcrfpy.Color(100, 150, 100) # Darker green
},
"troll": {
"sprite": SPRITE_TROLL,
"hp": 16,
"max_hp": 16,
"attack": 6,
"defense": 2,
"color": mcrfpy.Color(50, 150, 50) # Dark green
}
}
# Global storage for entity data
# Maps entity objects to their data dictionaries
entity_data: dict = {}
# Global references
player = None
grid = None
fov_layer = None
# =============================================================================
# Room Class (from Part 3)
# =============================================================================
class RectangularRoom:
"""A rectangular room with its position and size."""
def __init__(self, x: int, y: int, width: int, height: int):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -> tuple[int, int]:
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self) -> tuple[slice, slice]:
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
def intersects(self, other: "RectangularRoom") -> bool:
return (
self.x1 <= other.x2 and
self.x2 >= other.x1 and
self.y1 <= other.y2 and
self.y2 >= other.y1
)
# =============================================================================
# Exploration Tracking (from Part 4)
# =============================================================================
explored: list[list[bool]] = []
def init_explored() -> None:
"""Initialize the explored array to all False."""
global explored
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
def mark_explored(x: int, y: int) -> None:
"""Mark a tile as explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
explored[y][x] = True
def is_explored(x: int, y: int) -> bool:
"""Check if a tile has been explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
return explored[y][x]
return False
# =============================================================================
# Dungeon Generation (from Part 4)
# =============================================================================
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
"""Fill the entire grid with wall tiles."""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
cell.transparent = False
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
"""Carve out a room by setting its inner tiles to floor."""
inner_x, inner_y = room.inner
for y in range(inner_y.start, inner_y.stop):
for x in range(inner_x.start, inner_x.stop):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_l_tunnel(
target_grid: mcrfpy.Grid,
start: tuple[int, int],
end: tuple[int, int]
) -> None:
"""Carve an L-shaped tunnel between two points."""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
carve_tunnel_horizontal(target_grid, x1, x2, y1)
carve_tunnel_vertical(target_grid, y1, y2, x2)
else:
carve_tunnel_vertical(target_grid, y1, y2, x1)
carve_tunnel_horizontal(target_grid, x1, x2, y2)
# =============================================================================
# Enemy Management
# =============================================================================
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, texture: mcrfpy.Texture) -> mcrfpy.Entity:
"""Spawn an enemy at the given position.
Args:
target_grid: The game grid
x: X position in tiles
y: Y position in tiles
enemy_type: Type of enemy ("goblin", "orc", or "troll")
texture: The texture to use for the sprite
Returns:
The created enemy Entity
"""
template = ENEMY_TEMPLATES[enemy_type]
enemy = mcrfpy.Entity(
grid_pos=(x, y),
texture=texture,
sprite_index=template["sprite"]
)
# Start hidden until player sees them
enemy.visible = False
# Add to grid
target_grid.entities.append(enemy)
# Store enemy data
entity_data[enemy] = {
"type": enemy_type,
"name": enemy_type.capitalize(),
"hp": template["hp"],
"max_hp": template["max_hp"],
"attack": template["attack"],
"defense": template["defense"],
"is_player": False
}
return enemy
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, texture: mcrfpy.Texture) -> None:
"""Spawn random enemies in a room.
Args:
target_grid: The game grid
room: The room to spawn enemies in
texture: The texture to use for sprites
"""
# Random number of enemies (0 to MAX_ENEMIES_PER_ROOM)
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
for _ in range(num_enemies):
# Random position within the room's inner area
inner_x, inner_y = room.inner
x = random.randint(inner_x.start, inner_x.stop - 1)
y = random.randint(inner_y.start, inner_y.stop - 1)
# Check if position is already occupied
if get_blocking_entity_at(target_grid, x, y) is not None:
continue # Skip this spawn attempt
# Choose enemy type based on weighted random
roll = random.random()
if roll < 0.6:
enemy_type = "goblin" # 60% chance
elif roll < 0.9:
enemy_type = "orc" # 30% chance
else:
enemy_type = "troll" # 10% chance
spawn_enemy(target_grid, x, y, enemy_type, texture)
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> mcrfpy.Entity | None:
"""Get any entity that blocks movement at the given position.
Args:
target_grid: The game grid
x: X position to check
y: Y position to check
Returns:
The blocking entity, or None if no entity blocks this position
"""
for entity in target_grid.entities:
if int(entity.x) == x and int(entity.y) == y:
return entity
return None
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
"""Remove all enemies from the grid."""
global entity_data
# Get list of enemies to remove (not the player)
enemies_to_remove = []
for entity in target_grid.entities:
if entity in entity_data and not entity_data[entity].get("is_player", False):
enemies_to_remove.append(entity)
# Remove from grid and entity_data
for enemy in enemies_to_remove:
# Find and remove from grid.entities
for i, e in enumerate(target_grid.entities):
if e == enemy:
target_grid.entities.remove(i)
break
# Remove from entity_data
if enemy in entity_data:
del entity_data[enemy]
# =============================================================================
# Entity Visibility
# =============================================================================
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
"""Update visibility of all entities based on FOV.
Entities outside the player's field of view are hidden.
"""
global player
for entity in target_grid.entities:
# Player is always visible
if entity == player:
entity.visible = True
continue
# Other entities are only visible if in FOV
ex, ey = int(entity.x), int(entity.y)
entity.visible = target_grid.is_in_fov(ex, ey)
# =============================================================================
# Field of View (from Part 4)
# =============================================================================
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
"""Update the field of view visualization."""
# Compute FOV from player position
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
# Update each tile's visibility
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if target_grid.is_in_fov(x, y):
mark_explored(x, y)
target_fov_layer.set(x, y, COLOR_VISIBLE)
elif is_explored(x, y):
target_fov_layer.set(x, y, COLOR_DISCOVERED)
else:
target_fov_layer.set(x, y, COLOR_UNKNOWN)
# Update entity visibility
update_entity_visibility(target_grid)
# =============================================================================
# Collision Detection
# =============================================================================
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
"""Check if a position is valid for movement.
A position is valid if:
1. It is within grid bounds
2. The tile is walkable
3. No entity is blocking it
"""
# Check bounds
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
return False
# Check tile walkability
if not target_grid.at(x, y).walkable:
return False
# Check for blocking entities
if get_blocking_entity_at(target_grid, x, y) is not None:
return False
return True
# =============================================================================
# Dungeon Generation with Enemies
# =============================================================================
def generate_dungeon(target_grid: mcrfpy.Grid, texture: mcrfpy.Texture) -> tuple[int, int]:
"""Generate a dungeon with rooms, tunnels, and enemies.
Args:
target_grid: The game grid
texture: The texture for entity sprites
Returns:
The (x, y) coordinates where the player should start
"""
# Clear any existing enemies
clear_enemies(target_grid)
# Fill with walls
fill_with_walls(target_grid)
init_explored()
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(target_grid, new_room)
if rooms:
carve_l_tunnel(target_grid, new_room.center, rooms[-1].center)
# Spawn enemies in all rooms except the first (player starting room)
spawn_enemies_in_room(target_grid, new_room, texture)
rooms.append(new_room)
if rooms:
return rooms[0].center
return GRID_WIDTH // 2, GRID_HEIGHT // 2
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(50, 80),
size=(800, 560),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture
)
grid.zoom = 1.0
# Generate the dungeon (without player first to get starting position)
fill_with_walls(grid)
init_explored()
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Get player starting position
if rooms:
player_start_x, player_start_y = rooms[0].center
else:
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
# Add FOV layer
fov_layer = grid.add_layer("color", z_index=-1)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Create the player
player = mcrfpy.Entity(
grid_pos=(player_start_x, player_start_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Store player data
entity_data[player] = {
"type": "player",
"name": "Player",
"hp": 30,
"max_hp": 30,
"attack": 5,
"defense": 2,
"is_player": True
}
# Now spawn enemies in rooms (except the first one)
for i, room in enumerate(rooms):
if i == 0:
continue # Skip player's starting room
spawn_enemies_in_room(grid, room, texture)
# Calculate initial FOV
update_fov(grid, fov_layer, player_start_x, player_start_y)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(50, 15),
text="Part 5: Placing Enemies"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(50, 50),
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
pos_display = mcrfpy.Caption(
pos=(50, 660),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
status_display = mcrfpy.Caption(
pos=(400, 660),
text="Explore the dungeon..."
)
status_display.fill_color = mcrfpy.Color(100, 200, 100)
status_display.font_size = 16
scene.children.append(status_display)
# =============================================================================
# Input Handling
# =============================================================================
def regenerate_dungeon() -> None:
"""Generate a new dungeon and reposition the player."""
global player, grid, fov_layer, rooms
# Clear enemies
clear_enemies(grid)
# Regenerate dungeon structure
fill_with_walls(grid)
init_explored()
rooms = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Reposition player
if rooms:
new_x, new_y = rooms[0].center
else:
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
player.x = new_x
player.y = new_y
# Spawn new enemies
for i, room in enumerate(rooms):
if i == 0:
continue
spawn_enemies_in_room(grid, room, texture)
# Reset FOV layer
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Update FOV
update_fov(grid, fov_layer, new_x, new_y)
pos_display.text = f"Position: ({new_x}, {new_y})"
status_display.text = "New dungeon generated!"
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input."""
global player, grid, fov_layer
if action != "start":
return
px, py = int(player.x), int(player.y)
new_x, new_y = px, py
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
elif key == "R":
regenerate_dungeon()
return
elif key == "Escape":
mcrfpy.exit()
return
else:
return
# Check for blocking entity (potential combat target)
blocker = get_blocking_entity_at(grid, new_x, new_y)
if blocker is not None and blocker != player:
# For now, just report that we bumped into an enemy
if blocker in entity_data:
enemy_name = entity_data[blocker]["name"]
status_display.text = f"A {enemy_name} blocks your path!"
status_display.fill_color = mcrfpy.Color(200, 150, 100)
return
# Check if we can move
if can_move_to(grid, new_x, new_y):
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
status_display.text = "Exploring..."
status_display.fill_color = mcrfpy.Color(100, 200, 100)
# Update FOV after movement
update_fov(grid, fov_layer, new_x, new_y)
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 5 loaded! Enemies lurk in the dungeon...")

View file

@ -0,0 +1,940 @@
"""McRogueFace - Part 6: Combat System
Documentation: https://mcrogueface.github.io/tutorial/part_06_combat
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_06_combat/part_06_combat.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
import random
from dataclasses import dataclass
from typing import Optional
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
SPRITE_CORPSE = 37 # '%' - remains
# Enemy sprites
SPRITE_GOBLIN = 103 # 'g'
SPRITE_ORC = 111 # 'o'
SPRITE_TROLL = 116 # 't'
# Grid dimensions
GRID_WIDTH = 50
GRID_HEIGHT = 35
# Room generation parameters
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 8
# Enemy spawn parameters
MAX_ENEMIES_PER_ROOM = 3
# FOV settings
FOV_RADIUS = 8
# Visibility colors
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
# Message log settings
MAX_MESSAGES = 5
# =============================================================================
# Fighter Component
# =============================================================================
@dataclass
class Fighter:
"""Combat stats for an entity."""
hp: int
max_hp: int
attack: int
defense: int
name: str
is_player: bool = False
@property
def is_alive(self) -> bool:
"""Check if this fighter is still alive."""
return self.hp > 0
def take_damage(self, amount: int) -> int:
"""Apply damage and return actual damage taken."""
actual_damage = min(self.hp, amount)
self.hp -= actual_damage
return actual_damage
def heal(self, amount: int) -> int:
"""Heal and return actual amount healed."""
actual_heal = min(self.max_hp - self.hp, amount)
self.hp += actual_heal
return actual_heal
# =============================================================================
# Enemy Templates
# =============================================================================
ENEMY_TEMPLATES = {
"goblin": {
"sprite": SPRITE_GOBLIN,
"hp": 6,
"attack": 3,
"defense": 0,
"color": mcrfpy.Color(100, 200, 100)
},
"orc": {
"sprite": SPRITE_ORC,
"hp": 10,
"attack": 4,
"defense": 1,
"color": mcrfpy.Color(100, 150, 100)
},
"troll": {
"sprite": SPRITE_TROLL,
"hp": 16,
"attack": 6,
"defense": 2,
"color": mcrfpy.Color(50, 150, 50)
}
}
# =============================================================================
# Global State
# =============================================================================
# Entity data storage
entity_data: dict[mcrfpy.Entity, Fighter] = {}
# Global references
player: Optional[mcrfpy.Entity] = None
grid: Optional[mcrfpy.Grid] = None
fov_layer = None
texture: Optional[mcrfpy.Texture] = None
# Game state
game_over: bool = False
# Message log
messages: list[tuple[str, mcrfpy.Color]] = []
# =============================================================================
# Room Class
# =============================================================================
class RectangularRoom:
"""A rectangular room with its position and size."""
def __init__(self, x: int, y: int, width: int, height: int):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -> tuple[int, int]:
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self) -> tuple[slice, slice]:
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
def intersects(self, other: "RectangularRoom") -> bool:
return (
self.x1 <= other.x2 and
self.x2 >= other.x1 and
self.y1 <= other.y2 and
self.y2 >= other.y1
)
# =============================================================================
# Exploration Tracking
# =============================================================================
explored: list[list[bool]] = []
def init_explored() -> None:
"""Initialize the explored array to all False."""
global explored
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
def mark_explored(x: int, y: int) -> None:
"""Mark a tile as explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
explored[y][x] = True
def is_explored(x: int, y: int) -> bool:
"""Check if a tile has been explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
return explored[y][x]
return False
# =============================================================================
# Message Log
# =============================================================================
def add_message(text: str, color: mcrfpy.Color = None) -> None:
"""Add a message to the log.
Args:
text: The message text
color: Optional color (defaults to white)
"""
if color is None:
color = mcrfpy.Color(255, 255, 255)
messages.append((text, color))
# Keep only the most recent messages
while len(messages) > MAX_MESSAGES:
messages.pop(0)
# Update the message display
update_message_display()
def update_message_display() -> None:
"""Update the message log UI."""
if message_log_caption is None:
return
# Combine messages into a single string
lines = []
for text, color in messages:
lines.append(text)
message_log_caption.text = "\n".join(lines)
def clear_messages() -> None:
"""Clear all messages."""
global messages
messages = []
update_message_display()
# =============================================================================
# Dungeon Generation
# =============================================================================
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
"""Fill the entire grid with wall tiles."""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
cell.transparent = False
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
"""Carve out a room by setting its inner tiles to floor."""
inner_x, inner_y = room.inner
for y in range(inner_y.start, inner_y.stop):
for x in range(inner_x.start, inner_x.stop):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_l_tunnel(
target_grid: mcrfpy.Grid,
start: tuple[int, int],
end: tuple[int, int]
) -> None:
"""Carve an L-shaped tunnel between two points."""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
carve_tunnel_horizontal(target_grid, x1, x2, y1)
carve_tunnel_vertical(target_grid, y1, y2, x2)
else:
carve_tunnel_vertical(target_grid, y1, y2, x1)
carve_tunnel_horizontal(target_grid, x1, x2, y2)
# =============================================================================
# Entity Management
# =============================================================================
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
"""Spawn an enemy at the given position."""
template = ENEMY_TEMPLATES[enemy_type]
enemy = mcrfpy.Entity(
grid_pos=(x, y),
texture=tex,
sprite_index=template["sprite"]
)
enemy.visible = False
target_grid.entities.append(enemy)
# Create Fighter component for this enemy
entity_data[enemy] = Fighter(
hp=template["hp"],
max_hp=template["hp"],
attack=template["attack"],
defense=template["defense"],
name=enemy_type.capitalize(),
is_player=False
)
return enemy
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None:
"""Spawn random enemies in a room."""
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
for _ in range(num_enemies):
inner_x, inner_y = room.inner
x = random.randint(inner_x.start, inner_x.stop - 1)
y = random.randint(inner_y.start, inner_y.stop - 1)
if get_entity_at(target_grid, x, y) is not None:
continue
roll = random.random()
if roll < 0.6:
enemy_type = "goblin"
elif roll < 0.9:
enemy_type = "orc"
else:
enemy_type = "troll"
spawn_enemy(target_grid, x, y, enemy_type, tex)
def get_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
"""Get any entity at the given position."""
for entity in target_grid.entities:
if int(entity.x) == x and int(entity.y) == y:
# Check if this entity is alive (or is a non-Fighter entity)
if entity in entity_data:
if entity_data[entity].is_alive:
return entity
else:
return entity
return None
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
"""Get any living entity that blocks movement at the given position."""
for entity in target_grid.entities:
if entity == exclude:
continue
if int(entity.x) == x and int(entity.y) == y:
if entity in entity_data and entity_data[entity].is_alive:
return entity
return None
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
"""Remove an entity from the grid and data storage."""
# Find and remove from grid
for i, e in enumerate(target_grid.entities):
if e == entity:
target_grid.entities.remove(i)
break
# Remove from entity data
if entity in entity_data:
del entity_data[entity]
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
"""Remove all enemies from the grid."""
enemies_to_remove = []
for entity in target_grid.entities:
if entity in entity_data and not entity_data[entity].is_player:
enemies_to_remove.append(entity)
for enemy in enemies_to_remove:
remove_entity(target_grid, enemy)
# =============================================================================
# Combat System
# =============================================================================
def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
"""Calculate damage dealt from attacker to defender.
Args:
attacker: The attacking Fighter
defender: The defending Fighter
Returns:
The amount of damage to deal (minimum 0)
"""
damage = max(0, attacker.attack - defender.defense)
return damage
def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
"""Execute an attack from one entity to another.
Args:
attacker_entity: The entity performing the attack
defender_entity: The entity being attacked
"""
global game_over
attacker = entity_data.get(attacker_entity)
defender = entity_data.get(defender_entity)
if attacker is None or defender is None:
return
# Calculate and apply damage
damage = calculate_damage(attacker, defender)
defender.take_damage(damage)
# Generate combat message
if damage > 0:
if attacker.is_player:
add_message(
f"You hit the {defender.name} for {damage} damage!",
mcrfpy.Color(200, 200, 200)
)
else:
add_message(
f"The {attacker.name} hits you for {damage} damage!",
mcrfpy.Color(255, 150, 150)
)
else:
if attacker.is_player:
add_message(
f"You hit the {defender.name} but deal no damage.",
mcrfpy.Color(150, 150, 150)
)
else:
add_message(
f"The {attacker.name} hits you but deals no damage.",
mcrfpy.Color(150, 150, 200)
)
# Check for death
if not defender.is_alive:
handle_death(defender_entity, defender)
def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
"""Handle the death of an entity.
Args:
entity: The entity that died
fighter: The Fighter component of the dead entity
"""
global game_over, grid
if fighter.is_player:
# Player death
add_message("You have died!", mcrfpy.Color(255, 50, 50))
add_message("Press R to restart or Escape to quit.", mcrfpy.Color(200, 200, 200))
game_over = True
# Change player sprite to corpse
entity.sprite_index = SPRITE_CORPSE
else:
# Enemy death
add_message(f"The {fighter.name} dies!", mcrfpy.Color(100, 255, 100))
# Replace with corpse
entity.sprite_index = SPRITE_CORPSE
# Mark as dead (hp is already 0)
# Remove blocking but keep visual corpse
# Actually remove the entity and its data
remove_entity(grid, entity)
# Update HP display
update_hp_display()
# =============================================================================
# Field of View
# =============================================================================
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
"""Update visibility of all entities based on FOV."""
global player
for entity in target_grid.entities:
if entity == player:
entity.visible = True
continue
ex, ey = int(entity.x), int(entity.y)
entity.visible = target_grid.is_in_fov(ex, ey)
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
"""Update the field of view visualization."""
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if target_grid.is_in_fov(x, y):
mark_explored(x, y)
target_fov_layer.set(x, y, COLOR_VISIBLE)
elif is_explored(x, y):
target_fov_layer.set(x, y, COLOR_DISCOVERED)
else:
target_fov_layer.set(x, y, COLOR_UNKNOWN)
update_entity_visibility(target_grid)
# =============================================================================
# Movement and Actions
# =============================================================================
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool:
"""Check if a position is valid for movement."""
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
return False
if not target_grid.at(x, y).walkable:
return False
blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover)
if blocker is not None:
return False
return True
def try_move_or_attack(dx: int, dy: int) -> None:
"""Attempt to move the player or attack if blocked by enemy.
Args:
dx: Change in X position (-1, 0, or 1)
dy: Change in Y position (-1, 0, or 1)
"""
global player, grid, fov_layer, game_over
if game_over:
return
px, py = int(player.x), int(player.y)
target_x = px + dx
target_y = py + dy
# Check bounds
if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT:
return
# Check for blocking entity
blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
if blocker is not None:
# Attack the blocking entity
perform_attack(player, blocker)
# After player attacks, enemies take their turn
enemy_turn()
elif grid.at(target_x, target_y).walkable:
# Move to the empty tile
player.x = target_x
player.y = target_y
pos_display.text = f"Position: ({target_x}, {target_y})"
# Update FOV after movement
update_fov(grid, fov_layer, target_x, target_y)
# Enemies take their turn after player moves
enemy_turn()
# Update HP display
update_hp_display()
# =============================================================================
# Enemy AI
# =============================================================================
def enemy_turn() -> None:
"""Execute enemy actions."""
global player, grid, game_over
if game_over:
return
player_x, player_y = int(player.x), int(player.y)
# Collect enemies that can act
enemies = []
for entity in grid.entities:
if entity == player:
continue
if entity in entity_data and entity_data[entity].is_alive:
enemies.append(entity)
for enemy in enemies:
fighter = entity_data.get(enemy)
if fighter is None or not fighter.is_alive:
continue
ex, ey = int(enemy.x), int(enemy.y)
# Only act if in player's FOV (aware of player)
if not grid.is_in_fov(ex, ey):
continue
# Check if adjacent to player
dx = player_x - ex
dy = player_y - ey
if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
# Adjacent - attack!
perform_attack(enemy, player)
else:
# Not adjacent - try to move toward player
move_toward_player(enemy, ex, ey, player_x, player_y)
def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None:
"""Move an enemy one step toward the player.
Uses simple greedy movement - not true pathfinding.
"""
global grid
# Calculate direction to player
dx = 0
dy = 0
if px < ex:
dx = -1
elif px > ex:
dx = 1
if py < ey:
dy = -1
elif py > ey:
dy = 1
# Try to move in the desired direction
# First try the combined direction
new_x = ex + dx
new_y = ey + dy
if can_move_to(grid, new_x, new_y, enemy):
enemy.x = new_x
enemy.y = new_y
elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy):
# Try horizontal only
enemy.x = ex + dx
elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
# Try vertical only
enemy.y = ey + dy
# If all fail, enemy stays in place
# =============================================================================
# UI Updates
# =============================================================================
def update_hp_display() -> None:
"""Update the HP display in the UI."""
global player
if hp_display is None or player is None:
return
if player in entity_data:
fighter = entity_data[player]
hp_display.text = f"HP: {fighter.hp}/{fighter.max_hp}"
# Color based on health percentage
hp_percent = fighter.hp / fighter.max_hp
if hp_percent > 0.6:
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
elif hp_percent > 0.3:
hp_display.fill_color = mcrfpy.Color(255, 255, 100)
else:
hp_display.fill_color = mcrfpy.Color(255, 100, 100)
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(50, 80),
size=(800, 480),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture
)
grid.zoom = 1.0
# Generate initial dungeon structure
fill_with_walls(grid)
init_explored()
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Get player starting position
if rooms:
player_start_x, player_start_y = rooms[0].center
else:
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
# Add FOV layer
fov_layer = grid.add_layer("color", z_index=-1)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Create the player
player = mcrfpy.Entity(
grid_pos=(player_start_x, player_start_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Create player Fighter component
entity_data[player] = Fighter(
hp=30,
max_hp=30,
attack=5,
defense=2,
name="Player",
is_player=True
)
# Spawn enemies in all rooms except the first
for i, room in enumerate(rooms):
if i == 0:
continue
spawn_enemies_in_room(grid, room, texture)
# Calculate initial FOV
update_fov(grid, fov_layer, player_start_x, player_start_y)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(50, 15),
text="Part 6: Combat System"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(50, 50),
text="WASD/Arrows: Move/Attack | R: Restart | Escape: Quit"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
# Position display
pos_display = mcrfpy.Caption(
pos=(50, 580),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
# HP display
hp_display = mcrfpy.Caption(
pos=(300, 580),
text="HP: 30/30"
)
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
hp_display.font_size = 16
scene.children.append(hp_display)
# Message log (positioned below the grid)
message_log_caption = mcrfpy.Caption(
pos=(50, 610),
text=""
)
message_log_caption.fill_color = mcrfpy.Color(200, 200, 200)
message_log_caption.font_size = 14
scene.children.append(message_log_caption)
# Initial message
add_message("Welcome to the dungeon! Find and defeat the enemies.", mcrfpy.Color(100, 100, 255))
# =============================================================================
# Input Handling
# =============================================================================
def restart_game() -> None:
"""Restart the game with a new dungeon."""
global player, grid, fov_layer, game_over, entity_data, rooms
game_over = False
# Clear all entities and data
entity_data.clear()
# Remove all entities from grid
while len(grid.entities) > 0:
grid.entities.remove(0)
# Regenerate dungeon
fill_with_walls(grid)
init_explored()
clear_messages()
rooms = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Get new player starting position
if rooms:
new_x, new_y = rooms[0].center
else:
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
# Recreate player
player = mcrfpy.Entity(
grid_pos=(new_x, new_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
entity_data[player] = Fighter(
hp=30,
max_hp=30,
attack=5,
defense=2,
name="Player",
is_player=True
)
# Spawn enemies
for i, room in enumerate(rooms):
if i == 0:
continue
spawn_enemies_in_room(grid, room, texture)
# Reset FOV layer
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Update displays
update_fov(grid, fov_layer, new_x, new_y)
pos_display.text = f"Position: ({new_x}, {new_y})"
update_hp_display()
add_message("A new adventure begins!", mcrfpy.Color(100, 100, 255))
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input."""
global game_over
if action != "start":
return
# Handle restart
if key == "R":
restart_game()
return
if key == "Escape":
mcrfpy.exit()
return
# Ignore other input if game is over
if game_over:
return
# Movement and attack
if key == "W" or key == "Up":
try_move_or_attack(0, -1)
elif key == "S" or key == "Down":
try_move_or_attack(0, 1)
elif key == "A" or key == "Left":
try_move_or_attack(-1, 0)
elif key == "D" or key == "Right":
try_move_or_attack(1, 0)
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 6 loaded! Combat is now active. Good luck!")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -53,7 +53,41 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)PySceneClass::__dealloc, .tp_dealloc = (destructor)PySceneClass::__dealloc,
.tp_repr = (reprfunc)PySceneClass::__repr__, .tp_repr = (reprfunc)PySceneClass::__repr__,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // Allow subclassing .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // Allow subclassing
.tp_doc = PyDoc_STR("Base class for object-oriented scenes"), .tp_doc = PyDoc_STR(
"Scene(name: str)\n\n"
"Object-oriented scene management with lifecycle callbacks.\n\n"
"This is the recommended approach for scene management, replacing module-level\n"
"functions like createScene(), setScene(), and sceneUI(). Key advantage: you can\n"
"set on_key handlers on ANY scene, not just the currently active one.\n\n"
"Args:\n"
" name: Unique identifier for this scene. Used for scene transitions.\n\n"
"Properties:\n"
" name (str, read-only): Scene's unique identifier.\n"
" active (bool, read-only): Whether this scene is currently displayed.\n"
" children (UICollection, read-only): UI elements in this scene. Modify to add/remove elements.\n"
" on_key (callable): Keyboard handler. Set on ANY scene, regardless of which is active!\n"
" pos (Vector): Position offset for all UI elements.\n"
" visible (bool): Whether the scene renders.\n"
" opacity (float): Scene transparency (0.0-1.0).\n\n"
"Lifecycle Callbacks (override in subclass):\n"
" on_enter(): Called when scene becomes active via activate().\n"
" on_exit(): Called when scene is deactivated (another scene activates).\n"
" on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.\n"
" update(dt: float): Called every frame with delta time in seconds.\n"
" on_resize(width: int, height: int): Called when window is resized.\n\n"
"Example:\n"
" # Basic usage (replacing module functions):\n"
" scene = mcrfpy.Scene('main_menu')\n"
" scene.children.append(mcrfpy.Caption(text='Welcome', pos=(100, 100)))\n"
" scene.on_key = lambda key, action: print(f'Key: {key}')\n"
" scene.activate() # Switch to this scene\n\n"
" # Subclassing for lifecycle:\n"
" class GameScene(mcrfpy.Scene):\n"
" def on_enter(self):\n"
" print('Game started!')\n"
" def update(self, dt):\n"
" self.player.move(dt)\n"
),
.tp_methods = nullptr, // Set in McRFPy_API.cpp .tp_methods = nullptr, // Set in McRFPy_API.cpp
.tp_getset = nullptr, // Set in McRFPy_API.cpp .tp_getset = nullptr, // Set in McRFPy_API.cpp
.tp_init = (initproc)PySceneClass::__init__, .tp_init = (initproc)PySceneClass::__init__,

View file

@ -6,8 +6,9 @@
#include "Profiler.h" #include "Profiler.h"
#include "PyFOV.h" #include "PyFOV.h"
#include <algorithm> #include <algorithm>
#include <cmath> // #142 - for std::floor #include <cmath> // #142 - for std::floor, std::isnan
#include <cstring> // #150 - for strcmp #include <cstring> // #150 - for strcmp
#include <limits> // #169 - for std::numeric_limits
// UIDrawable methods now in UIBase.h // UIDrawable methods now in UIBase.h
UIGrid::UIGrid() UIGrid::UIGrid()
@ -735,7 +736,9 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
PyObject* fill_color = nullptr; PyObject* fill_color = nullptr;
PyObject* click_handler = nullptr; PyObject* click_handler = nullptr;
PyObject* layers_obj = nullptr; // #150 - layers dict PyObject* layers_obj = nullptr; // #150 - layers dict
float center_x = 0.0f, center_y = 0.0f; // #169 - Use NaN as sentinel to detect if user provided center values
float center_x = std::numeric_limits<float>::quiet_NaN();
float center_y = std::numeric_limits<float>::quiet_NaN();
float zoom = 1.0f; float zoom = 1.0f;
// perspective is now handled via properties, not init args // perspective is now handled via properties, not init args
int visible = 1; int visible = 1;
@ -862,9 +865,19 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
sf::Vector2f(x, y), sf::Vector2f(w, h)); sf::Vector2f(x, y), sf::Vector2f(w, h));
// Set additional properties // Set additional properties
self->data->zoom = zoom; // Set zoom first, needed for default center calculation
// #169 - Calculate default center if not provided by user
// Default: tile (0,0) at top-left of widget
if (std::isnan(center_x)) {
// Center = half widget size (in pixels), so tile 0,0 appears at top-left
center_x = w / (2.0f * zoom);
}
if (std::isnan(center_y)) {
center_y = h / (2.0f * zoom);
}
self->data->center_x = center_x; self->data->center_x = center_x;
self->data->center_y = center_y; self->data->center_y = center_y;
self->data->zoom = zoom;
// perspective is now handled by perspective_entity and perspective_enabled // perspective is now handled by perspective_entity and perspective_enabled
// self->data->perspective = perspective; // self->data->perspective = perspective;
self->data->visible = visible; self->data->visible = visible;
@ -1730,6 +1743,72 @@ PyObject* UIGrid::py_entities_in_radius(PyUIGridObject* self, PyObject* args, Py
return result; return result;
} }
// #169 - center_camera implementations
void UIGrid::center_camera() {
// Center on grid's middle tile
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
center_x = (grid_x / 2.0f) * cell_width;
center_y = (grid_y / 2.0f) * cell_height;
markDirty(); // #144 - View change affects content
}
void UIGrid::center_camera(float tile_x, float tile_y) {
// Position specified tile at top-left of widget
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
// To put tile (tx, ty) at top-left: center = tile_pos + half_viewport
float half_viewport_x = box.getSize().x / zoom / 2.0f;
float half_viewport_y = box.getSize().y / zoom / 2.0f;
center_x = tile_x * cell_width + half_viewport_x;
center_y = tile_y * cell_height + half_viewport_y;
markDirty(); // #144 - View change affects content
}
PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) {
PyObject* pos_arg = nullptr;
// Parse optional positional argument (tuple of tile coordinates)
if (!PyArg_ParseTuple(args, "|O", &pos_arg)) {
return nullptr;
}
if (pos_arg == nullptr || pos_arg == Py_None) {
// No args: center on grid's middle tile
self->data->center_camera();
} else if (PyTuple_Check(pos_arg) && PyTuple_Size(pos_arg) == 2) {
// Tuple provided: center on (tile_x, tile_y)
PyObject* x_obj = PyTuple_GetItem(pos_arg, 0);
PyObject* y_obj = PyTuple_GetItem(pos_arg, 1);
float tile_x, tile_y;
if (PyFloat_Check(x_obj)) {
tile_x = PyFloat_AsDouble(x_obj);
} else if (PyLong_Check(x_obj)) {
tile_x = (float)PyLong_AsLong(x_obj);
} else {
PyErr_SetString(PyExc_TypeError, "tile coordinates must be numeric");
return nullptr;
}
if (PyFloat_Check(y_obj)) {
tile_y = PyFloat_AsDouble(y_obj);
} else if (PyLong_Check(y_obj)) {
tile_y = (float)PyLong_AsLong(y_obj);
} else {
PyErr_SetString(PyExc_TypeError, "tile coordinates must be numeric");
return nullptr;
}
self->data->center_camera(tile_x, tile_y);
} else {
PyErr_SetString(PyExc_TypeError, "center_camera() takes an optional tuple (tile_x, tile_y)");
return nullptr;
}
Py_RETURN_NONE;
}
PyMethodDef UIGrid::methods[] = { PyMethodDef UIGrid::methods[] = {
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
@ -1818,6 +1897,15 @@ PyMethodDef UIGrid::methods[] = {
" radius: Search radius\n\n" " radius: Search radius\n\n"
"Returns:\n" "Returns:\n"
" List of Entity objects within the radius."}, " List of Entity objects within the radius."},
{"center_camera", (PyCFunction)UIGrid::py_center_camera, METH_VARARGS,
"center_camera(pos: tuple = None) -> None\n\n"
"Center the camera on a tile coordinate.\n\n"
"Args:\n"
" pos: Optional (tile_x, tile_y) tuple. If None, centers on grid's middle tile.\n\n"
"Example:\n"
" grid.center_camera() # Center on middle of grid\n"
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
{NULL, NULL, 0, NULL} {NULL, NULL, 0, NULL}
}; };
@ -1929,6 +2017,15 @@ PyMethodDef UIGrid_all_methods[] = {
" radius: Search radius\n\n" " radius: Search radius\n\n"
"Returns:\n" "Returns:\n"
" List of Entity objects within the radius."}, " List of Entity objects within the radius."},
{"center_camera", (PyCFunction)UIGrid::py_center_camera, METH_VARARGS,
"center_camera(pos: tuple = None) -> None\n\n"
"Center the camera on a tile coordinate.\n\n"
"Args:\n"
" pos: Optional (tile_x, tile_y) tuple. If None, centers on grid's middle tile.\n\n"
"Example:\n"
" grid.center_camera() # Center on middle of grid\n"
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
{NULL} // Sentinel {NULL} // Sentinel
}; };

View file

@ -170,6 +170,12 @@ public:
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args); static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args);
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115 static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169
// #169 - Camera positioning
void center_camera(); // Center on grid's middle tile
void center_camera(float tile_x, float tile_y); // Center on specific tile
static PyMethodDef methods[]; static PyMethodDef methods[];
static PyGetSetDef getsetters[]; static PyGetSetDef getsetters[];
static PyObject* get_entities(PyUIGridObject* self, void* closure); static PyObject* get_entities(PyUIGridObject* self, void* closure);

View file

@ -34,12 +34,15 @@ grid = mcrfpy.Grid(
size=(1024, 768) size=(1024, 768)
) )
# Add color layer for floor pattern
color_layer = grid.add_layer("color", z_index=-1)
# Simple floor pattern # Simple floor pattern
for x in range(100): for x in range(100):
for y in range(100): for y in range(100):
cell = grid.at((x, y)) cell = grid.at(x, y)
cell.tilesprite = 0 cell.tilesprite = 0
cell.color = (40, 40, 40, 255) color_layer.set(x, y, mcrfpy.Color(40, 40, 40, 255))
# Create 50 entities with random positions and velocities # Create 50 entities with random positions and velocities
entities = [] entities = []
@ -47,15 +50,15 @@ ENTITY_COUNT = 50
for i in range(ENTITY_COUNT): for i in range(ENTITY_COUNT):
entity = mcrfpy.Entity( entity = mcrfpy.Entity(
grid_pos=(random.randint(0, 99), random.randint(0, 99)), (random.randint(0, 99), random.randint(0, 99)),
sprite_index=random.randint(10, 20) # Use varied sprites sprite_index=random.randint(10, 20), # Use varied sprites
grid=grid
) )
# Give each entity a random velocity # Give each entity a random velocity (stored as Python attributes)
entity.velocity_x = random.uniform(-0.5, 0.5) entity.velocity_x = random.uniform(-0.5, 0.5)
entity.velocity_y = random.uniform(-0.5, 0.5) entity.velocity_y = random.uniform(-0.5, 0.5)
grid.entities.append(entity)
entities.append(entity) entities.append(entity)
ui.append(grid) ui.append(grid)

View file

@ -282,23 +282,23 @@ def setup_grid_stress():
grid.center = (400, 400) # Center view grid.center = (400, 400) # Center view
ui.append(grid) ui.append(grid)
# Fill with alternating colors # Add color layer and fill with alternating colors
color_layer = grid.add_layer("color", z_index=-1)
for y in range(50): for y in range(50):
for x in range(50): for x in range(50):
cell = grid.at(x, y)
if (x + y) % 2 == 0: if (x + y) % 2 == 0:
cell.color = mcrfpy.Color(60, 60, 80) color_layer.set(x, y, mcrfpy.Color(60, 60, 80))
else: else:
cell.color = mcrfpy.Color(40, 40, 60) color_layer.set(x, y, mcrfpy.Color(40, 40, 60))
# Add 50 entities # Add 50 entities
try: try:
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
for i in range(50): for i in range(50):
# Entity takes positional args: (position, texture, sprite_index, grid) # Entity takes tuple position and keyword args
pos = mcrfpy.Vector(random.randint(5, 45), random.randint(5, 45)) pos = (random.randint(5, 45), random.randint(5, 45))
entity = mcrfpy.Entity(pos, texture, random.randint(0, 100), grid) entity = mcrfpy.Entity(pos, texture=texture, sprite_index=random.randint(0, 100), grid=grid)
grid.entities.append(entity) grid.entities.append(entity)
except Exception as e: except Exception as e:
print(f" Note: Could not create entities: {e}") print(f" Note: Could not create entities: {e}")

View file

@ -6,10 +6,14 @@ Uses C++ benchmark logger (start_benchmark/end_benchmark) for accurate timing.
Results written to JSON files for analysis. Results written to JSON files for analysis.
Compares rendering performance between: Compares rendering performance between:
1. Traditional grid.at(x,y).color API (no caching) 1. ColorLayer with per-cell modifications (no caching benefit)
2. New layer system with dirty flag caching 2. ColorLayer with dirty flag caching (static after fill)
3. Various layer configurations 3. Various layer configurations
NOTE: The old grid.at(x,y).color API no longer exists. All color operations
now go through the ColorLayer system. This benchmark compares different
layer usage patterns to measure caching effectiveness.
Usage: Usage:
./mcrogueface --exec tests/benchmarks/layer_performance_test.py ./mcrogueface --exec tests/benchmarks/layer_performance_test.py
# Results in benchmark_*.json files # Results in benchmark_*.json files
@ -94,7 +98,7 @@ def run_next_test():
# ============================================================================ # ============================================================================
def setup_base_layer_static(): def setup_base_layer_static():
"""Traditional grid.at(x,y).color API - no modifications during render.""" """ColorLayer with per-cell set() calls - static after initial fill."""
mcrfpy.createScene("test_base_static") mcrfpy.createScene("test_base_static")
ui = mcrfpy.sceneUI("test_base_static") ui = mcrfpy.sceneUI("test_base_static")
@ -102,17 +106,17 @@ def setup_base_layer_static():
pos=(10, 10), size=(600, 600)) pos=(10, 10), size=(600, 600))
ui.append(grid) ui.append(grid)
# Fill base layer using traditional API # Fill using ColorLayer with per-cell set() calls (baseline)
layer = grid.add_layer("color", z_index=-1)
for y in range(GRID_SIZE): for y in range(GRID_SIZE):
for x in range(GRID_SIZE): for x in range(GRID_SIZE):
cell = grid.at(x, y) layer.set(x, y, mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255))
cell.color = mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255)
mcrfpy.setScene("test_base_static") mcrfpy.setScene("test_base_static")
def setup_base_layer_modified(): def setup_base_layer_modified():
"""Traditional API with single cell modified each frame.""" """ColorLayer with single cell modified each frame - tests dirty flag."""
mcrfpy.createScene("test_base_mod") mcrfpy.createScene("test_base_mod")
ui = mcrfpy.sceneUI("test_base_mod") ui = mcrfpy.sceneUI("test_base_mod")
@ -120,19 +124,16 @@ def setup_base_layer_modified():
pos=(10, 10), size=(600, 600)) pos=(10, 10), size=(600, 600))
ui.append(grid) ui.append(grid)
# Fill base layer # Fill using ColorLayer
for y in range(GRID_SIZE): layer = grid.add_layer("color", z_index=-1)
for x in range(GRID_SIZE): layer.fill(mcrfpy.Color(100, 100, 100, 255))
cell = grid.at(x, y)
cell.color = mcrfpy.Color(100, 100, 100, 255)
# Timer to modify one cell per frame # Timer to modify one cell per frame (triggers dirty flag each frame)
mod_counter = [0] mod_counter = [0]
def modify_cell(runtime): def modify_cell(runtime):
x = mod_counter[0] % GRID_SIZE x = mod_counter[0] % GRID_SIZE
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
cell = grid.at(x, y) layer.set(x, y, mcrfpy.Color(255, 0, 0, 255))
cell.color = mcrfpy.Color(255, 0, 0, 255)
mod_counter[0] += 1 mod_counter[0] += 1
mcrfpy.setScene("test_base_mod") mcrfpy.setScene("test_base_mod")

View file

@ -19,26 +19,30 @@ END_COLOR = mcrfpy.Color(255, 255, 100) # Yellow for end
# Global state # Global state
grid = None grid = None
color_layer = None
mode = "ASTAR" mode = "ASTAR"
start_pos = (5, 10) start_pos = (5, 10)
end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall
def create_map(): def create_map():
"""Create a map with obstacles to show pathfinding differences""" """Create a map with obstacles to show pathfinding differences"""
global grid global grid, color_layer
mcrfpy.createScene("pathfinding_comparison") mcrfpy.createScene("pathfinding_comparison")
# Create grid # Create grid
grid = mcrfpy.Grid(grid_x=30, grid_y=20) grid = mcrfpy.Grid(grid_x=30, grid_y=20)
grid.fill_color = mcrfpy.Color(0, 0, 0) grid.fill_color = mcrfpy.Color(0, 0, 0)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Initialize all as floor # Initialize all as floor
for y in range(20): for y in range(20):
for x in range(30): for x in range(30):
grid.at(x, y).walkable = True grid.at(x, y).walkable = True
grid.at(x, y).color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
# Create obstacles that make A* and Dijkstra differ # Create obstacles that make A* and Dijkstra differ
obstacles = [ obstacles = [
# Vertical wall with gaps # Vertical wall with gaps
@ -50,15 +54,15 @@ def create_map():
[(x, 10) for x in range(20, 25)], [(x, 10) for x in range(20, 25)],
[(25, y) for y in range(5, 15)], [(25, y) for y in range(5, 15)],
] ]
for obstacle_group in obstacles: for obstacle_group in obstacles:
for x, y in obstacle_group: for x, y in obstacle_group:
grid.at(x, y).walkable = False grid.at(x, y).walkable = False
grid.at(x, y).color = WALL_COLOR color_layer.set(x, y, WALL_COLOR)
# Mark start and end # Mark start and end
grid.at(start_pos[0], start_pos[1]).color = START_COLOR color_layer.set(start_pos[0], start_pos[1], START_COLOR)
grid.at(end_pos[0], end_pos[1]).color = END_COLOR color_layer.set(end_pos[0], end_pos[1], END_COLOR)
def clear_paths(): def clear_paths():
"""Clear path highlighting""" """Clear path highlighting"""
@ -66,34 +70,34 @@ def clear_paths():
for x in range(30): for x in range(30):
cell = grid.at(x, y) cell = grid.at(x, y)
if cell.walkable: if cell.walkable:
cell.color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
# Restore start and end colors # Restore start and end colors
grid.at(start_pos[0], start_pos[1]).color = START_COLOR color_layer.set(start_pos[0], start_pos[1], START_COLOR)
grid.at(end_pos[0], end_pos[1]).color = END_COLOR color_layer.set(end_pos[0], end_pos[1], END_COLOR)
def show_astar(): def show_astar():
"""Show A* path""" """Show A* path"""
clear_paths() clear_paths()
# Compute A* path # Compute A* path
path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
# Color the path # Color the path
for i, (x, y) in enumerate(path): for i, (x, y) in enumerate(path):
if (x, y) != start_pos and (x, y) != end_pos: if (x, y) != start_pos and (x, y) != end_pos:
grid.at(x, y).color = ASTAR_COLOR color_layer.set(x, y, ASTAR_COLOR)
status_text.text = f"A* Path: {len(path)} steps (optimized for single target)" status_text.text = f"A* Path: {len(path)} steps (optimized for single target)"
status_text.fill_color = ASTAR_COLOR status_text.fill_color = ASTAR_COLOR
def show_dijkstra(): def show_dijkstra():
"""Show Dijkstra exploration""" """Show Dijkstra exploration"""
clear_paths() clear_paths()
# Compute Dijkstra from start # Compute Dijkstra from start
grid.compute_dijkstra(start_pos[0], start_pos[1]) grid.compute_dijkstra(start_pos[0], start_pos[1])
# Color cells by distance (showing exploration) # Color cells by distance (showing exploration)
max_dist = 40.0 max_dist = 40.0
for y in range(20): for y in range(20):
@ -103,50 +107,50 @@ def show_dijkstra():
if dist is not None and dist < max_dist: if dist is not None and dist < max_dist:
# Color based on distance # Color based on distance
intensity = int(255 * (1 - dist / max_dist)) intensity = int(255 * (1 - dist / max_dist))
grid.at(x, y).color = mcrfpy.Color(0, intensity // 2, intensity) color_layer.set(x, y, mcrfpy.Color(0, intensity // 2, intensity))
# Get the actual path # Get the actual path
path = grid.get_dijkstra_path(end_pos[0], end_pos[1]) path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
# Highlight the actual path more brightly # Highlight the actual path more brightly
for x, y in path: for x, y in path:
if (x, y) != start_pos and (x, y) != end_pos: if (x, y) != start_pos and (x, y) != end_pos:
grid.at(x, y).color = DIJKSTRA_COLOR color_layer.set(x, y, DIJKSTRA_COLOR)
# Restore start and end # Restore start and end
grid.at(start_pos[0], start_pos[1]).color = START_COLOR color_layer.set(start_pos[0], start_pos[1], START_COLOR)
grid.at(end_pos[0], end_pos[1]).color = END_COLOR color_layer.set(end_pos[0], end_pos[1], END_COLOR)
status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)" status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)"
status_text.fill_color = DIJKSTRA_COLOR status_text.fill_color = DIJKSTRA_COLOR
def show_both(): def show_both():
"""Show both paths overlaid""" """Show both paths overlaid"""
clear_paths() clear_paths()
# Get both paths # Get both paths
astar_path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) astar_path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
grid.compute_dijkstra(start_pos[0], start_pos[1]) grid.compute_dijkstra(start_pos[0], start_pos[1])
dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1]) dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
print(astar_path, dijkstra_path) print(astar_path, dijkstra_path)
# Color Dijkstra path first (blue) # Color Dijkstra path first (blue)
for x, y in dijkstra_path: for x, y in dijkstra_path:
if (x, y) != start_pos and (x, y) != end_pos: if (x, y) != start_pos and (x, y) != end_pos:
grid.at(x, y).color = DIJKSTRA_COLOR color_layer.set(x, y, DIJKSTRA_COLOR)
# Then A* path (green) - will overwrite shared cells # Then A* path (green) - will overwrite shared cells
for x, y in astar_path: for x, y in astar_path:
if (x, y) != start_pos and (x, y) != end_pos: if (x, y) != start_pos and (x, y) != end_pos:
grid.at(x, y).color = ASTAR_COLOR color_layer.set(x, y, ASTAR_COLOR)
# Mark differences # Mark differences
different_cells = [] different_cells = []
for cell in dijkstra_path: for cell in dijkstra_path:
if cell not in astar_path: if cell not in astar_path:
different_cells.append(cell) different_cells.append(cell)
status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps" status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps"
if different_cells: if different_cells:
info_text.text = f"Paths differ at {len(different_cells)} cells" info_text.text = f"Paths differ at {len(different_cells)} cells"
@ -202,26 +206,26 @@ grid.size = (600, 400) # 30*20, 20*20
grid.position = (100, 100) grid.position = (100, 100)
# Add title # Add title
title = mcrfpy.Caption("A* vs Dijkstra Pathfinding", 250, 20) title = mcrfpy.Caption(pos=(250, 20), text="A* vs Dijkstra Pathfinding")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Add status # Add status
status_text = mcrfpy.Caption("Press A for A*, D for Dijkstra, B for Both", 100, 60) status_text = mcrfpy.Caption(pos=(100, 60), text="Press A for A*, D for Dijkstra, B for Both")
status_text.fill_color = mcrfpy.Color(200, 200, 200) status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text) ui.append(status_text)
# Add info # Add info
info_text = mcrfpy.Caption("", 100, 520) info_text = mcrfpy.Caption(pos=(100, 520), text="")
info_text.fill_color = mcrfpy.Color(200, 200, 200) info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text) ui.append(info_text)
# Add legend # Add legend
legend1 = mcrfpy.Caption("Red=Start, Yellow=End, Green=A*, Blue=Dijkstra", 100, 540) legend1 = mcrfpy.Caption(pos=(100, 540), text="Red=Start, Yellow=End, Green=A*, Blue=Dijkstra")
legend1.fill_color = mcrfpy.Color(150, 150, 150) legend1.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend1) ui.append(legend1)
legend2 = mcrfpy.Caption("Dark=Walls, Light=Floor", 100, 560) legend2 = mcrfpy.Caption(pos=(100, 560), text="Dark=Walls, Light=Floor")
legend2.fill_color = mcrfpy.Color(150, 150, 150) legend2.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend2) ui.append(legend2)

View file

@ -20,9 +20,8 @@ for y in range(5):
# Create entity # Create entity
print("Creating entity...") print("Creating entity...")
entity = mcrfpy.Entity(2, 2) entity = mcrfpy.Entity((2, 2), grid=grid)
entity.sprite_index = 64 entity.sprite_index = 64
grid.entities.append(entity)
print(f"Entity at ({entity.x}, {entity.y})") print(f"Entity at ({entity.x}, {entity.y})")
# Check gridstate # Check gridstate

View file

@ -20,6 +20,7 @@ NO_PATH_COLOR = mcrfpy.Color(255, 0, 0) # Pure red for unreachable
# Global state # Global state
grid = None grid = None
color_layer = None
entities = [] entities = []
current_combo_index = 0 current_combo_index = 0
all_combinations = [] # All possible pairs all_combinations = [] # All possible pairs
@ -27,14 +28,17 @@ current_path = []
def create_map(): def create_map():
"""Create the map with entities""" """Create the map with entities"""
global grid, entities, all_combinations global grid, color_layer, entities, all_combinations
mcrfpy.createScene("dijkstra_all") mcrfpy.createScene("dijkstra_all")
# Create grid # Create grid
grid = mcrfpy.Grid(grid_x=14, grid_y=10) grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0) grid.fill_color = mcrfpy.Color(0, 0, 0)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Map layout - Entity 1 is intentionally trapped! # Map layout - Entity 1 is intentionally trapped!
map_layout = [ map_layout = [
"..............", # Row 0 "..............", # Row 0
@ -48,29 +52,28 @@ def create_map():
"..W.WWW.......", # Row 8 "..W.WWW.......", # Row 8
"..............", # Row 9 "..............", # Row 9
] ]
# Create the map # Create the map
entity_positions = [] entity_positions = []
for y, row in enumerate(map_layout): for y, row in enumerate(map_layout):
for x, char in enumerate(row): for x, char in enumerate(row):
cell = grid.at(x, y) cell = grid.at(x, y)
if char == 'W': if char == 'W':
cell.walkable = False cell.walkable = False
cell.color = WALL_COLOR color_layer.set(x, y, WALL_COLOR)
else: else:
cell.walkable = True cell.walkable = True
cell.color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
if char == 'E': if char == 'E':
entity_positions.append((x, y)) entity_positions.append((x, y))
# Create entities # Create entities
entities = [] entities = []
for i, (x, y) in enumerate(entity_positions): for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y) entity = mcrfpy.Entity((x, y), grid=grid)
entity.sprite_index = 49 + i # '1', '2', '3' entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity) entities.append(entity)
print("Map Analysis:") print("Map Analysis:")
@ -90,47 +93,47 @@ def create_map():
def clear_path_colors(): def clear_path_colors():
"""Reset all floor tiles to original color""" """Reset all floor tiles to original color"""
global current_path global current_path
for y in range(grid.grid_y): for y in range(grid.grid_y):
for x in range(grid.grid_x): for x in range(grid.grid_x):
cell = grid.at(x, y) cell = grid.at(x, y)
if cell.walkable: if cell.walkable:
cell.color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
current_path = [] current_path = []
def show_combination(index): def show_combination(index):
"""Show a specific path combination (valid or invalid)""" """Show a specific path combination (valid or invalid)"""
global current_combo_index, current_path global current_combo_index, current_path
current_combo_index = index % len(all_combinations) current_combo_index = index % len(all_combinations)
from_idx, to_idx = all_combinations[current_combo_index] from_idx, to_idx = all_combinations[current_combo_index]
# Clear previous path # Clear previous path
clear_path_colors() clear_path_colors()
# Get entities # Get entities
e_from = entities[from_idx] e_from = entities[from_idx]
e_to = entities[to_idx] e_to = entities[to_idx]
# Calculate path # Calculate path
path = e_from.path_to(int(e_to.x), int(e_to.y)) path = e_from.path_to(int(e_to.x), int(e_to.y))
current_path = path if path else [] current_path = path if path else []
# Always color start and end positions # Always color start and end positions
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR color_layer.set(int(e_from.x), int(e_from.y), START_COLOR)
grid.at(int(e_to.x), int(e_to.y)).color = NO_PATH_COLOR if not path else END_COLOR color_layer.set(int(e_to.x), int(e_to.y), NO_PATH_COLOR if not path else END_COLOR)
# Color the path if it exists # Color the path if it exists
if path: if path:
# Color intermediate steps # Color intermediate steps
for i, (x, y) in enumerate(path): for i, (x, y) in enumerate(path):
if i > 0 and i < len(path) - 1: if i > 0 and i < len(path) - 1:
grid.at(x, y).color = PATH_COLOR color_layer.set(x, y, PATH_COLOR)
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = {len(path)} steps" status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = {len(path)} steps"
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green for valid status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green for valid
# Show path steps # Show path steps
path_display = [] path_display = []
for i, (x, y) in enumerate(path[:5]): for i, (x, y) in enumerate(path[:5]):
@ -142,7 +145,7 @@ def show_combination(index):
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = NO PATH!" status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = NO PATH!"
status_text.fill_color = mcrfpy.Color(255, 100, 100) # Red for invalid status_text.fill_color = mcrfpy.Color(255, 100, 100) # Red for invalid
path_text.text = "Path: [] (No valid path exists)" path_text.text = "Path: [] (No valid path exists)"
# Update info # Update info
info_text.text = f"From: Entity {from_idx+1} at ({int(e_from.x)}, {int(e_from.y)}) | To: Entity {to_idx+1} at ({int(e_to.x)}, {int(e_to.y)})" info_text.text = f"From: Entity {from_idx+1} at ({int(e_from.x)}, {int(e_from.y)}) | To: Entity {to_idx+1} at ({int(e_to.x)}, {int(e_to.y)})"
@ -183,37 +186,37 @@ grid.size = (560, 400)
grid.position = (120, 100) grid.position = (120, 100)
# Add title # Add title
title = mcrfpy.Caption("Dijkstra - All Paths (Valid & Invalid)", 200, 20) title = mcrfpy.Caption(pos=(200, 20), text="Dijkstra - All Paths (Valid & Invalid)")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Add status (will change color based on validity) # Add status (will change color based on validity)
status_text = mcrfpy.Caption("Ready", 120, 60) status_text = mcrfpy.Caption(pos=(120, 60), text="Ready")
status_text.fill_color = mcrfpy.Color(255, 255, 100) status_text.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(status_text) ui.append(status_text)
# Add info # Add info
info_text = mcrfpy.Caption("", 120, 80) info_text = mcrfpy.Caption(pos=(120, 80), text="")
info_text.fill_color = mcrfpy.Color(200, 200, 200) info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text) ui.append(info_text)
# Add path display # Add path display
path_text = mcrfpy.Caption("Path: None", 120, 520) path_text = mcrfpy.Caption(pos=(120, 520), text="Path: None")
path_text.fill_color = mcrfpy.Color(200, 200, 200) path_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(path_text) ui.append(path_text)
# Add controls # Add controls
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit", 120, 540) controls = mcrfpy.Caption(pos=(120, 540), text="SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit")
controls.fill_color = mcrfpy.Color(150, 150, 150) controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls) ui.append(controls)
# Add legend # Add legend
legend = mcrfpy.Caption("Red Start→Blue End (valid) | Red Start→Red End (invalid)", 120, 560) legend = mcrfpy.Caption(pos=(120, 560), text="Red Start→Blue End (valid) | Red Start→Red End (invalid)")
legend.fill_color = mcrfpy.Color(150, 150, 150) legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend) ui.append(legend)
# Expected results info # Expected results info
expected = mcrfpy.Caption("Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail", 120, 580) expected = mcrfpy.Caption(pos=(120, 580), text="Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail")
expected.fill_color = mcrfpy.Color(255, 150, 150) expected.fill_color = mcrfpy.Color(255, 150, 150)
ui.append(expected) ui.append(expected)

View file

@ -18,6 +18,7 @@ END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
# Global state # Global state
grid = None grid = None
color_layer = None
entities = [] entities = []
current_path_index = 0 current_path_index = 0
path_combinations = [] path_combinations = []
@ -25,14 +26,17 @@ current_path = []
def create_map(): def create_map():
"""Create the map with entities""" """Create the map with entities"""
global grid, entities global grid, color_layer, entities
mcrfpy.createScene("dijkstra_cycle") mcrfpy.createScene("dijkstra_cycle")
# Create grid # Create grid
grid = mcrfpy.Grid(grid_x=14, grid_y=10) grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0) grid.fill_color = mcrfpy.Color(0, 0, 0)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Map layout # Map layout
map_layout = [ map_layout = [
"..............", # Row 0 "..............", # Row 0
@ -46,29 +50,28 @@ def create_map():
"..W.WWW.......", # Row 8 "..W.WWW.......", # Row 8
"..............", # Row 9 "..............", # Row 9
] ]
# Create the map # Create the map
entity_positions = [] entity_positions = []
for y, row in enumerate(map_layout): for y, row in enumerate(map_layout):
for x, char in enumerate(row): for x, char in enumerate(row):
cell = grid.at(x, y) cell = grid.at(x, y)
if char == 'W': if char == 'W':
cell.walkable = False cell.walkable = False
cell.color = WALL_COLOR color_layer.set(x, y, WALL_COLOR)
else: else:
cell.walkable = True cell.walkable = True
cell.color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
if char == 'E': if char == 'E':
entity_positions.append((x, y)) entity_positions.append((x, y))
# Create entities # Create entities
entities = [] entities = []
for i, (x, y) in enumerate(entity_positions): for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y) entity = mcrfpy.Entity((x, y), grid=grid)
entity.sprite_index = 49 + i # '1', '2', '3' entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity) entities.append(entity)
print("Entities created:") print("Entities created:")
@ -113,48 +116,48 @@ def create_map():
def clear_path_colors(): def clear_path_colors():
"""Reset all floor tiles to original color""" """Reset all floor tiles to original color"""
global current_path global current_path
for y in range(grid.grid_y): for y in range(grid.grid_y):
for x in range(grid.grid_x): for x in range(grid.grid_x):
cell = grid.at(x, y) cell = grid.at(x, y)
if cell.walkable: if cell.walkable:
cell.color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
current_path = [] current_path = []
def show_path(index): def show_path(index):
"""Show a specific path combination""" """Show a specific path combination"""
global current_path_index, current_path global current_path_index, current_path
if not path_combinations: if not path_combinations:
status_text.text = "No valid paths available (Entity 1 is trapped!)" status_text.text = "No valid paths available (Entity 1 is trapped!)"
return return
current_path_index = index % len(path_combinations) current_path_index = index % len(path_combinations)
from_idx, to_idx, path = path_combinations[current_path_index] from_idx, to_idx, path = path_combinations[current_path_index]
# Clear previous path # Clear previous path
clear_path_colors() clear_path_colors()
# Get entities # Get entities
e_from = entities[from_idx] e_from = entities[from_idx]
e_to = entities[to_idx] e_to = entities[to_idx]
# Color the path # Color the path
current_path = path current_path = path
if path: if path:
# Color start and end # Color start and end
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR color_layer.set(int(e_from.x), int(e_from.y), START_COLOR)
grid.at(int(e_to.x), int(e_to.y)).color = END_COLOR color_layer.set(int(e_to.x), int(e_to.y), END_COLOR)
# Color intermediate steps # Color intermediate steps
for i, (x, y) in enumerate(path): for i, (x, y) in enumerate(path):
if i > 0 and i < len(path) - 1: if i > 0 and i < len(path) - 1:
grid.at(x, y).color = PATH_COLOR color_layer.set(x, y, PATH_COLOR)
# Update status # Update status
status_text.text = f"Path {current_path_index + 1}/{len(path_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} ({len(path)} steps)" status_text.text = f"Path {current_path_index + 1}/{len(path_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} ({len(path)} steps)"
# Update path display # Update path display
path_display = [] path_display = []
for i, (x, y) in enumerate(path[:5]): # Show first 5 steps for i, (x, y) in enumerate(path[:5]): # Show first 5 steps
@ -194,27 +197,27 @@ grid.size = (560, 400)
grid.position = (120, 100) grid.position = (120, 100)
# Add title # Add title
title = mcrfpy.Caption("Dijkstra Pathfinding - Cycle Paths", 200, 20) title = mcrfpy.Caption(pos=(200, 20), text="Dijkstra Pathfinding - Cycle Paths")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Add status # Add status
status_text = mcrfpy.Caption("Press SPACE to cycle paths", 120, 60) status_text = mcrfpy.Caption(pos=(120, 60), text="Press SPACE to cycle paths")
status_text.fill_color = mcrfpy.Color(255, 255, 100) status_text.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(status_text) ui.append(status_text)
# Add path display # Add path display
path_text = mcrfpy.Caption("Path: None", 120, 520) path_text = mcrfpy.Caption(pos=(120, 520), text="Path: None")
path_text.fill_color = mcrfpy.Color(200, 200, 200) path_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(path_text) ui.append(path_text)
# Add controls # Add controls
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, R=Refresh, Q=Quit", 120, 540) controls = mcrfpy.Caption(pos=(120, 540), text="SPACE/N=Next, P=Previous, R=Refresh, Q=Quit")
controls.fill_color = mcrfpy.Color(150, 150, 150) controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls) ui.append(controls)
# Add legend # Add legend
legend = mcrfpy.Caption("Red=Start, Blue=End, Green=Path, Dark=Wall", 120, 560) legend = mcrfpy.Caption(pos=(120, 560), text="Red=Start, Blue=End, Green=Path, Dark=Wall")
legend.fill_color = mcrfpy.Color(150, 150, 150) legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend) ui.append(legend)

View file

@ -18,49 +18,52 @@ ENTITY_COLORS = [
# Global state # Global state
grid = None grid = None
color_layer = None
entities = [] entities = []
first_point = None first_point = None
second_point = None second_point = None
def create_simple_map(): def create_simple_map():
"""Create a simple test map""" """Create a simple test map"""
global grid, entities global grid, color_layer, entities
mcrfpy.createScene("dijkstra_debug") mcrfpy.createScene("dijkstra_debug")
# Small grid for easy debugging # Small grid for easy debugging
grid = mcrfpy.Grid(grid_x=10, grid_y=10) grid = mcrfpy.Grid(grid_x=10, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0) grid.fill_color = mcrfpy.Color(0, 0, 0)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
print("Initializing 10x10 grid...") print("Initializing 10x10 grid...")
# Initialize all as floor # Initialize all as floor
for y in range(10): for y in range(10):
for x in range(10): for x in range(10):
grid.at(x, y).walkable = True grid.at(x, y).walkable = True
grid.at(x, y).transparent = True grid.at(x, y).transparent = True
grid.at(x, y).color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
# Add a simple wall # Add a simple wall
print("Adding walls at:") print("Adding walls at:")
walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)] walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)]
for x, y in walls: for x, y in walls:
print(f" Wall at ({x}, {y})") print(f" Wall at ({x}, {y})")
grid.at(x, y).walkable = False grid.at(x, y).walkable = False
grid.at(x, y).color = WALL_COLOR color_layer.set(x, y, WALL_COLOR)
# Create 3 entities # Create 3 entities
entity_positions = [(2, 5), (8, 5), (5, 8)] entity_positions = [(2, 5), (8, 5), (5, 8)]
entities = [] entities = []
print("\nCreating entities at:") print("\nCreating entities at:")
for i, (x, y) in enumerate(entity_positions): for i, (x, y) in enumerate(entity_positions):
print(f" Entity {i+1} at ({x}, {y})") print(f" Entity {i+1} at ({x}, {y})")
entity = mcrfpy.Entity(x, y) entity = mcrfpy.Entity((x, y), grid=grid)
entity.sprite_index = 49 + i # '1', '2', '3' entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity) entities.append(entity)
return grid return grid
def test_path_highlighting(): def test_path_highlighting():
@ -88,12 +91,14 @@ def test_path_highlighting():
print(f" Step {i}: ({x}, {y})") print(f" Step {i}: ({x}, {y})")
# Get current color for debugging # Get current color for debugging
cell = grid.at(x, y) cell = grid.at(x, y)
old_color = (cell.color.r, cell.color.g, cell.color.b) old_c = color_layer.at(x, y)
old_color = (old_c.r, old_c.g, old_c.b)
# Set new color # Set new color
cell.color = PATH_COLOR color_layer.set(x, y, PATH_COLOR)
new_color = (cell.color.r, cell.color.g, cell.color.b) new_c = color_layer.at(x, y)
new_color = (new_c.r, new_c.g, new_c.b)
print(f" Color changed from {old_color} to {new_color}") print(f" Color changed from {old_color} to {new_color}")
print(f" Walkable: {cell.walkable}") print(f" Walkable: {cell.walkable}")
@ -111,8 +116,8 @@ def test_path_highlighting():
# Verify colors were set # Verify colors were set
print("\nVerifying cell colors after highlighting:") print("\nVerifying cell colors after highlighting:")
for x, y in path[:3]: # Check first 3 cells for x, y in path[:3]: # Check first 3 cells
cell = grid.at(x, y) c = color_layer.at(x, y)
color = (cell.color.r, cell.color.g, cell.color.b) color = (c.r, c.g, c.b)
expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b) expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b)
match = color == expected match = color == expected
print(f" Cell ({x}, {y}): color={color}, expected={expected}, match={match}") print(f" Cell ({x}, {y}): color={color}, expected={expected}, match={match}")
@ -143,12 +148,12 @@ grid.position = (50, 50)
grid.size = (400, 400) # 10*40 grid.size = (400, 400) # 10*40
# Add title # Add title
title = mcrfpy.Caption("Dijkstra Debug - Press SPACE to retest, Q to quit", 50, 10) title = mcrfpy.Caption(pos=(50, 10), text="Dijkstra Debug - Press SPACE to retest, Q to quit")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Add debug info # Add debug info
info = mcrfpy.Caption("Check console for debug output", 50, 470) info = mcrfpy.Caption(pos=(50, 470), text="Check console for debug output")
info.fill_color = mcrfpy.Color(200, 200, 200) info.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info) ui.append(info)

View file

@ -29,20 +29,24 @@ ENTITY_COLORS = [
# Global state # Global state
grid = None grid = None
color_layer = None
entities = [] entities = []
first_point = None first_point = None
second_point = None second_point = None
def create_map(): def create_map():
"""Create the interactive map with the layout specified by the user""" """Create the interactive map with the layout specified by the user"""
global grid, entities global grid, color_layer, entities
mcrfpy.createScene("dijkstra_interactive") mcrfpy.createScene("dijkstra_interactive")
# Create grid - 14x10 as specified # Create grid - 14x10 as specified
grid = mcrfpy.Grid(grid_x=14, grid_y=10) grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0) grid.fill_color = mcrfpy.Color(0, 0, 0)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Define the map layout from user's specification # Define the map layout from user's specification
# . = floor, W = wall, E = entity position # . = floor, W = wall, E = entity position
map_layout = [ map_layout = [
@ -57,36 +61,35 @@ def create_map():
"..W.WWW.......", # Row 8 "..W.WWW.......", # Row 8
"..............", # Row 9 "..............", # Row 9
] ]
# Create the map # Create the map
entity_positions = [] entity_positions = []
for y, row in enumerate(map_layout): for y, row in enumerate(map_layout):
for x, char in enumerate(row): for x, char in enumerate(row):
cell = grid.at(x, y) cell = grid.at(x, y)
if char == 'W': if char == 'W':
# Wall # Wall
cell.walkable = False cell.walkable = False
cell.transparent = False cell.transparent = False
cell.color = WALL_COLOR color_layer.set(x, y, WALL_COLOR)
else: else:
# Floor # Floor
cell.walkable = True cell.walkable = True
cell.transparent = True cell.transparent = True
cell.color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
if char == 'E': if char == 'E':
# Entity position # Entity position
entity_positions.append((x, y)) entity_positions.append((x, y))
# Create entities at marked positions # Create entities at marked positions
entities = [] entities = []
for i, (x, y) in enumerate(entity_positions): for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y) entity = mcrfpy.Entity((x, y), grid=grid)
entity.sprite_index = 49 + i # '1', '2', '3' entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity) entities.append(entity)
return grid return grid
def clear_path_highlight(): def clear_path_highlight():
@ -96,37 +99,37 @@ def clear_path_highlight():
for x in range(grid.grid_x): for x in range(grid.grid_x):
cell = grid.at(x, y) cell = grid.at(x, y)
if cell.walkable: if cell.walkable:
cell.color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
def highlight_path(): def highlight_path():
"""Highlight the path between selected entities""" """Highlight the path between selected entities"""
if first_point is None or second_point is None: if first_point is None or second_point is None:
return return
# Clear previous highlighting # Clear previous highlighting
clear_path_highlight() clear_path_highlight()
# Get entities # Get entities
entity1 = entities[first_point] entity1 = entities[first_point]
entity2 = entities[second_point] entity2 = entities[second_point]
# Compute Dijkstra from first entity # Compute Dijkstra from first entity
grid.compute_dijkstra(int(entity1.x), int(entity1.y)) grid.compute_dijkstra(int(entity1.x), int(entity1.y))
# Get path to second entity # Get path to second entity
path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y)) path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y))
if path: if path:
# Highlight the path # Highlight the path
for x, y in path: for x, y in path:
cell = grid.at(x, y) cell = grid.at(x, y)
if cell.walkable: if cell.walkable:
cell.color = PATH_COLOR color_layer.set(x, y, PATH_COLOR)
# Also highlight start and end with entity colors # Also highlight start and end with entity colors
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point] color_layer.set(int(entity1.x), int(entity1.y), ENTITY_COLORS[first_point])
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point] color_layer.set(int(entity2.x), int(entity2.y), ENTITY_COLORS[second_point])
# Update info # Update info
distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y)) distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y))
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units" info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units"
@ -199,34 +202,33 @@ grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 60) grid.position = (120, 60)
# Add title # Add title
title = mcrfpy.Caption("Dijkstra Pathfinding Interactive", 250, 10) title = mcrfpy.Caption(pos=(250, 10), text="Dijkstra Pathfinding Interactive")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Add status text # Add status text
status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480) status_text = mcrfpy.Caption(pos=(120, 480), text="Press 1/2/3 for first entity, A/B/C for second")
status_text.fill_color = mcrfpy.Color(255, 255, 255) status_text.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status_text) ui.append(status_text)
# Add info text # Add info text
info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500) info_text = mcrfpy.Caption(pos=(120, 500), text="Space to clear, Q to quit")
info_text.fill_color = mcrfpy.Color(200, 200, 200) info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text) ui.append(info_text)
# Add legend # Add legend
legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 540) legend1 = mcrfpy.Caption(pos=(120, 540), text="Entities: 1=Red 2=Green 3=Blue")
legend1.fill_color = mcrfpy.Color(150, 150, 150) legend1.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend1) ui.append(legend1)
legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 560) legend2 = mcrfpy.Caption(pos=(120, 560), text="Colors: Dark=Wall Light=Floor Cyan=Path")
legend2.fill_color = mcrfpy.Color(150, 150, 150) legend2.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend2) ui.append(legend2)
# Mark entity positions with colored indicators # Mark entity positions with colored indicators
for i, entity in enumerate(entities): for i, entity in enumerate(entities):
marker = mcrfpy.Caption(str(i+1), marker = mcrfpy.Caption(pos=(120 + int(entity.x) * 40 + 15, 60 + int(entity.y) * 40 + 10),
120 + int(entity.x) * 40 + 15, text=str(i+1))
60 + int(entity.y) * 40 + 10)
marker.fill_color = ENTITY_COLORS[i] marker.fill_color = ENTITY_COLORS[i]
marker.outline = 1 marker.outline = 1
marker.outline_color = mcrfpy.Color(0, 0, 0) marker.outline_color = mcrfpy.Color(0, 0, 0)

View file

@ -32,6 +32,7 @@ ENTITY_COLORS = [
# Global state # Global state
grid = None grid = None
color_layer = None
entities = [] entities = []
first_point = None first_point = None
second_point = None second_point = None
@ -43,14 +44,17 @@ original_positions = [] # Store original entity positions
def create_map(): def create_map():
"""Create the interactive map with the layout specified by the user""" """Create the interactive map with the layout specified by the user"""
global grid, entities, original_positions global grid, color_layer, entities, original_positions
mcrfpy.createScene("dijkstra_enhanced") mcrfpy.createScene("dijkstra_enhanced")
# Create grid - 14x10 as specified # Create grid - 14x10 as specified
grid = mcrfpy.Grid(grid_x=14, grid_y=10) grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0) grid.fill_color = mcrfpy.Color(0, 0, 0)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Define the map layout from user's specification # Define the map layout from user's specification
# . = floor, W = wall, E = entity position # . = floor, W = wall, E = entity position
map_layout = [ map_layout = [
@ -65,87 +69,86 @@ def create_map():
"..W.WWW.......", # Row 8 "..W.WWW.......", # Row 8
"..............", # Row 9 "..............", # Row 9
] ]
# Create the map # Create the map
entity_positions = [] entity_positions = []
for y, row in enumerate(map_layout): for y, row in enumerate(map_layout):
for x, char in enumerate(row): for x, char in enumerate(row):
cell = grid.at(x, y) cell = grid.at(x, y)
if char == 'W': if char == 'W':
# Wall # Wall
cell.walkable = False cell.walkable = False
cell.transparent = False cell.transparent = False
cell.color = WALL_COLOR color_layer.set(x, y, WALL_COLOR)
else: else:
# Floor # Floor
cell.walkable = True cell.walkable = True
cell.transparent = True cell.transparent = True
cell.color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
if char == 'E': if char == 'E':
# Entity position # Entity position
entity_positions.append((x, y)) entity_positions.append((x, y))
# Create entities at marked positions # Create entities at marked positions
entities = [] entities = []
original_positions = [] original_positions = []
for i, (x, y) in enumerate(entity_positions): for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y) entity = mcrfpy.Entity((x, y), grid=grid)
entity.sprite_index = 49 + i # '1', '2', '3' entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity) entities.append(entity)
original_positions.append((x, y)) original_positions.append((x, y))
return grid return grid
def clear_path_highlight(): def clear_path_highlight():
"""Clear any existing path highlighting""" """Clear any existing path highlighting"""
global current_path global current_path
# Reset all floor tiles to original color # Reset all floor tiles to original color
for y in range(grid.grid_y): for y in range(grid.grid_y):
for x in range(grid.grid_x): for x in range(grid.grid_x):
cell = grid.at(x, y) cell = grid.at(x, y)
if cell.walkable: if cell.walkable:
cell.color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
current_path = [] current_path = []
def highlight_path(): def highlight_path():
"""Highlight the path between selected entities using entity.path_to()""" """Highlight the path between selected entities using entity.path_to()"""
global current_path global current_path
if first_point is None or second_point is None: if first_point is None or second_point is None:
return return
# Clear previous highlighting # Clear previous highlighting
clear_path_highlight() clear_path_highlight()
# Get entities # Get entities
entity1 = entities[first_point] entity1 = entities[first_point]
entity2 = entities[second_point] entity2 = entities[second_point]
# Use the new path_to method! # Use the new path_to method!
path = entity1.path_to(int(entity2.x), int(entity2.y)) path = entity1.path_to(int(entity2.x), int(entity2.y))
if path: if path:
current_path = path current_path = path
# Highlight the path # Highlight the path
for i, (x, y) in enumerate(path): for i, (x, y) in enumerate(path):
cell = grid.at(x, y) cell = grid.at(x, y)
if cell.walkable: if cell.walkable:
# Use gradient for path visualization # Use gradient for path visualization
if i < len(path) - 1: if i < len(path) - 1:
cell.color = PATH_COLOR color_layer.set(x, y, PATH_COLOR)
else: else:
cell.color = VISITED_COLOR color_layer.set(x, y, VISITED_COLOR)
# Highlight start and end with entity colors # Highlight start and end with entity colors
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point] color_layer.set(int(entity1.x), int(entity1.y), ENTITY_COLORS[first_point])
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point] color_layer.set(int(entity2.x), int(entity2.y), ENTITY_COLORS[second_point])
# Update info # Update info
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps" info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps"
else: else:
@ -291,39 +294,38 @@ grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 60) grid.position = (120, 60)
# Add title # Add title
title = mcrfpy.Caption("Enhanced Dijkstra Pathfinding", 250, 10) title = mcrfpy.Caption(pos=(250, 10), text="Enhanced Dijkstra Pathfinding")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Add status text # Add status text
status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480) status_text = mcrfpy.Caption(pos=(120, 480), text="Press 1/2/3 for first entity, A/B/C for second")
status_text.fill_color = mcrfpy.Color(255, 255, 255) status_text.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status_text) ui.append(status_text)
# Add info text # Add info text
info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500) info_text = mcrfpy.Caption(pos=(120, 500), text="Space to clear, Q to quit")
info_text.fill_color = mcrfpy.Color(200, 200, 200) info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text) ui.append(info_text)
# Add control text # Add control text
control_text = mcrfpy.Caption("Press M to move, P to pause, R to reset", 120, 520) control_text = mcrfpy.Caption(pos=(120, 520), text="Press M to move, P to pause, R to reset")
control_text.fill_color = mcrfpy.Color(150, 200, 150) control_text.fill_color = mcrfpy.Color(150, 200, 150)
ui.append(control_text) ui.append(control_text)
# Add legend # Add legend
legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 560) legend1 = mcrfpy.Caption(pos=(120, 560), text="Entities: 1=Red 2=Green 3=Blue")
legend1.fill_color = mcrfpy.Color(150, 150, 150) legend1.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend1) ui.append(legend1)
legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 580) legend2 = mcrfpy.Caption(pos=(120, 580), text="Colors: Dark=Wall Light=Floor Cyan=Path")
legend2.fill_color = mcrfpy.Color(150, 150, 150) legend2.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend2) ui.append(legend2)
# Mark entity positions with colored indicators # Mark entity positions with colored indicators
for i, entity in enumerate(entities): for i, entity in enumerate(entities):
marker = mcrfpy.Caption(str(i+1), marker = mcrfpy.Caption(pos=(120 + int(entity.x) * 40 + 15, 60 + int(entity.y) * 40 + 10),
120 + int(entity.x) * 40 + 15, text=str(i+1))
60 + int(entity.y) * 40 + 10)
marker.fill_color = ENTITY_COLORS[i] marker.fill_color = ENTITY_COLORS[i]
marker.outline = 1 marker.outline = 1
marker.outline_color = mcrfpy.Color(0, 0, 0) marker.outline_color = mcrfpy.Color(0, 0, 0)

View file

@ -128,12 +128,12 @@ grid.position = (50, 50)
grid.size = (500, 300) grid.size = (500, 300)
# Add title # Add title
title = mcrfpy.Caption("Dijkstra Pathfinding Test", 200, 10) title = mcrfpy.Caption(pos=(200, 10), text="Dijkstra Pathfinding Test")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Add legend # Add legend
legend = mcrfpy.Caption("Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3", 50, 360) legend = mcrfpy.Caption(pos=(50, 360), text="Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3")
legend.fill_color = mcrfpy.Color(180, 180, 180) legend.fill_color = mcrfpy.Color(180, 180, 180)
ui.append(legend) ui.append(legend)

View file

@ -19,33 +19,36 @@ mcrfpy.createScene("visibility_demo")
grid = mcrfpy.Grid(grid_x=30, grid_y=20) grid = mcrfpy.Grid(grid_x=30, grid_y=20)
grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Initialize grid - all walkable and transparent # Initialize grid - all walkable and transparent
for y in range(20): for y in range(20):
for x in range(30): for x in range(30):
cell = grid.at(x, y) cell = grid.at(x, y)
cell.walkable = True cell.walkable = True
cell.transparent = True cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120) # Floor color color_layer.set(x, y, mcrfpy.Color(100, 100, 120)) # Floor color
# Create walls # Create walls
walls = [ walls = [
# Central cross # Central cross
[(15, y) for y in range(8, 12)], [(15, y) for y in range(8, 12)],
[(x, 10) for x in range(13, 18)], [(x, 10) for x in range(13, 18)],
# Rooms # Rooms
# Top-left room # Top-left room
[(x, 5) for x in range(2, 8)] + [(8, y) for y in range(2, 6)], [(x, 5) for x in range(2, 8)] + [(8, y) for y in range(2, 6)],
[(2, y) for y in range(2, 6)] + [(x, 2) for x in range(2, 8)], [(2, y) for y in range(2, 6)] + [(x, 2) for x in range(2, 8)],
# Top-right room # Top-right room
[(x, 5) for x in range(22, 28)] + [(22, y) for y in range(2, 6)], [(x, 5) for x in range(22, 28)] + [(22, y) for y in range(2, 6)],
[(28, y) for y in range(2, 6)] + [(x, 2) for x in range(22, 28)], [(28, y) for y in range(2, 6)] + [(x, 2) for x in range(22, 28)],
# Bottom-left room # Bottom-left room
[(x, 15) for x in range(2, 8)] + [(8, y) for y in range(15, 18)], [(x, 15) for x in range(2, 8)] + [(8, y) for y in range(15, 18)],
[(2, y) for y in range(15, 18)] + [(x, 18) for x in range(2, 8)], [(2, y) for y in range(15, 18)] + [(x, 18) for x in range(2, 8)],
# Bottom-right room # Bottom-right room
[(x, 15) for x in range(22, 28)] + [(22, y) for y in range(15, 18)], [(x, 15) for x in range(22, 28)] + [(22, y) for y in range(15, 18)],
[(28, y) for y in range(15, 18)] + [(x, 18) for x in range(22, 28)], [(28, y) for y in range(15, 18)] + [(x, 18) for x in range(22, 28)],
@ -57,12 +60,12 @@ for wall_group in walls:
cell = grid.at(x, y) cell = grid.at(x, y)
cell.walkable = False cell.walkable = False
cell.transparent = False cell.transparent = False
cell.color = mcrfpy.Color(40, 20, 20) # Wall color color_layer.set(x, y, mcrfpy.Color(40, 20, 20)) # Wall color
# Create entities # Create entities
player = mcrfpy.Entity(5, 10, grid=grid) player = mcrfpy.Entity((5, 10), grid=grid)
player.sprite_index = 64 # @ player.sprite_index = 64 # @
enemy = mcrfpy.Entity(25, 10, grid=grid) enemy = mcrfpy.Entity((25, 10), grid=grid)
enemy.sprite_index = 69 # E enemy.sprite_index = 69 # E
# Update initial visibility # Update initial visibility
@ -80,24 +83,24 @@ grid.position = (50, 100)
grid.size = (900, 600) # 30*30, 20*30 grid.size = (900, 600) # 30*30, 20*30
# Title # Title
title = mcrfpy.Caption("Interactive Visibility Demo", 350, 20) title = mcrfpy.Caption(pos=(350, 20), text="Interactive Visibility Demo")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Info displays # Info displays
perspective_label = mcrfpy.Caption("Perspective: Omniscient", 50, 50) perspective_label = mcrfpy.Caption(pos=(50, 50), text="Perspective: Omniscient")
perspective_label.fill_color = mcrfpy.Color(200, 200, 200) perspective_label.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(perspective_label) ui.append(perspective_label)
controls = mcrfpy.Caption("WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset", 50, 730) controls = mcrfpy.Caption(pos=(50, 730), text="WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset")
controls.fill_color = mcrfpy.Color(150, 150, 150) controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls) ui.append(controls)
player_info = mcrfpy.Caption("Player: (5, 10)", 700, 50) player_info = mcrfpy.Caption(pos=(700, 50), text="Player: (5, 10)")
player_info.fill_color = mcrfpy.Color(100, 255, 100) player_info.fill_color = mcrfpy.Color(100, 255, 100)
ui.append(player_info) ui.append(player_info)
enemy_info = mcrfpy.Caption("Enemy: (25, 10)", 700, 70) enemy_info = mcrfpy.Caption(pos=(700, 70), text="Enemy: (25, 10)")
enemy_info.fill_color = mcrfpy.Color(255, 100, 100) enemy_info.fill_color = mcrfpy.Color(255, 100, 100)
ui.append(enemy_info) ui.append(enemy_info)

View file

@ -11,6 +11,9 @@ mcrfpy.createScene("vis_test")
print("Creating grid...") print("Creating grid...")
grid = mcrfpy.Grid(grid_x=10, grid_y=10) grid = mcrfpy.Grid(grid_x=10, grid_y=10)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Initialize grid # Initialize grid
print("Initializing grid...") print("Initializing grid...")
for y in range(10): for y in range(10):
@ -18,11 +21,11 @@ for y in range(10):
cell = grid.at(x, y) cell = grid.at(x, y)
cell.walkable = True cell.walkable = True
cell.transparent = True cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120) color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
# Create entity # Create entity
print("Creating entity...") print("Creating entity...")
entity = mcrfpy.Entity(5, 5, grid=grid) entity = mcrfpy.Entity((5, 5), grid=grid)
entity.sprite_index = 64 entity.sprite_index = 64
print("Updating visibility...") print("Updating visibility...")

View file

@ -13,8 +13,8 @@ print("Scene created")
grid = mcrfpy.Grid(grid_x=5, grid_y=5) grid = mcrfpy.Grid(grid_x=5, grid_y=5)
print("Grid created") print("Grid created")
# Create entity without appending # Create entity with grid association
entity = mcrfpy.Entity(2, 2, grid=grid) entity = mcrfpy.Entity((2, 2), grid=grid)
print(f"Entity created at ({entity.x}, {entity.y})") print(f"Entity created at ({entity.x}, {entity.y})")
# Check if gridstate is initialized # Check if gridstate is initialized

View file

@ -8,6 +8,10 @@ while small grids use the original flat storage. Verifies that:
2. Large grids work correctly with chunks 2. Large grids work correctly with chunks
3. Cell access (read/write) works for both modes 3. Cell access (read/write) works for both modes
4. Rendering displays correctly for both modes 4. Rendering displays correctly for both modes
NOTE: This test uses ColorLayer for color operations since cell.color
is no longer supported. The chunk system affects internal storage, which
ColorLayer also uses.
""" """
import mcrfpy import mcrfpy
@ -19,22 +23,21 @@ def test_small_grid():
# Small grid should use flat storage # Small grid should use flat storage
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(10, 10), size=(400, 400)) grid = mcrfpy.Grid(grid_size=(50, 50), pos=(10, 10), size=(400, 400))
color_layer = grid.add_layer("color", z_index=-1)
# Set some cells # Set some cells
for y in range(50): for y in range(50):
for x in range(50): for x in range(50):
cell = grid.at(x, y) cell = grid.at(x, y)
cell.color = mcrfpy.Color((x * 5) % 256, (y * 5) % 256, 128, 255) color_layer.set(x, y, mcrfpy.Color((x * 5) % 256, (y * 5) % 256, 128, 255))
cell.tilesprite = -1 cell.tilesprite = -1
# Verify cells # Verify cells
cell = grid.at(25, 25)
expected_r = (25 * 5) % 256 expected_r = (25 * 5) % 256
expected_g = (25 * 5) % 256 expected_g = (25 * 5) % 256
color = cell.color color = color_layer.at(25, 25)
r, g = color[0], color[1] if color.r != expected_r or color.g != expected_g:
if r != expected_r or g != expected_g: print(f"FAIL: Small grid cell color mismatch. Expected ({expected_r}, {expected_g}), got ({color.r}, {color.g})")
print(f"FAIL: Small grid cell color mismatch. Expected ({expected_r}, {expected_g}), got ({r}, {g})")
return False return False
print(" Small grid: PASS") print(" Small grid: PASS")
@ -46,6 +49,7 @@ def test_large_grid():
# Large grid should use chunk storage (100 > 64) # Large grid should use chunk storage (100 > 64)
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400)) grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400))
color_layer = grid.add_layer("color", z_index=-1)
# Set cells across multiple chunks # Set cells across multiple chunks
# Chunks are 64x64, so a 100x100 grid has 2x2 = 4 chunks # Chunks are 64x64, so a 100x100 grid has 2x2 = 4 chunks
@ -61,15 +65,14 @@ def test_large_grid():
for x, y in test_points: for x, y in test_points:
cell = grid.at(x, y) cell = grid.at(x, y)
cell.color = mcrfpy.Color(x, y, 100, 255) color_layer.set(x, y, mcrfpy.Color(x, y, 100, 255))
cell.tilesprite = -1 cell.tilesprite = -1
# Verify cells # Verify cells
for x, y in test_points: for x, y in test_points:
cell = grid.at(x, y) color = color_layer.at(x, y)
color = cell.color if color.r != x or color.g != y:
if color[0] != x or color[1] != y: print(f"FAIL: Large grid cell ({x},{y}) color mismatch. Expected ({x}, {y}), got ({color.r}, {color.g})")
print(f"FAIL: Large grid cell ({x},{y}) color mismatch. Expected ({x}, {y}), got ({color[0]}, {color[1]})")
return False return False
print(" Large grid cell access: PASS") print(" Large grid cell access: PASS")
@ -81,6 +84,7 @@ def test_very_large_grid():
# 500x500 = 250,000 cells, should use ~64 chunks (8x8) # 500x500 = 250,000 cells, should use ~64 chunks (8x8)
grid = mcrfpy.Grid(grid_size=(500, 500), pos=(10, 10), size=(400, 400)) grid = mcrfpy.Grid(grid_size=(500, 500), pos=(10, 10), size=(400, 400))
color_layer = grid.add_layer("color", z_index=-1)
# Set some cells at various positions # Set some cells at various positions
test_points = [ test_points = [
@ -94,14 +98,12 @@ def test_very_large_grid():
] ]
for x, y in test_points: for x, y in test_points:
cell = grid.at(x, y) color_layer.set(x, y, mcrfpy.Color(x % 256, y % 256, 200, 255))
cell.color = mcrfpy.Color(x % 256, y % 256, 200, 255)
# Verify # Verify
for x, y in test_points: for x, y in test_points:
cell = grid.at(x, y) color = color_layer.at(x, y)
color = cell.color if color.r != (x % 256) or color.g != (y % 256):
if color[0] != (x % 256) or color[1] != (y % 256):
print(f"FAIL: Very large grid cell ({x},{y}) color mismatch") print(f"FAIL: Very large grid cell ({x},{y}) color mismatch")
return False return False
@ -114,20 +116,20 @@ def test_boundary_case():
# 64x64 should use flat storage (not exceeding threshold) # 64x64 should use flat storage (not exceeding threshold)
grid_64 = mcrfpy.Grid(grid_size=(64, 64), pos=(10, 10), size=(400, 400)) grid_64 = mcrfpy.Grid(grid_size=(64, 64), pos=(10, 10), size=(400, 400))
cell = grid_64.at(63, 63) color_layer_64 = grid_64.add_layer("color", z_index=-1)
cell.color = mcrfpy.Color(255, 0, 0, 255) color_layer_64.set(63, 63, mcrfpy.Color(255, 0, 0, 255))
color = grid_64.at(63, 63).color color = color_layer_64.at(63, 63)
if color[0] != 255: if color.r != 255:
print(f"FAIL: 64x64 grid boundary cell not set correctly, got r={color[0]}") print(f"FAIL: 64x64 grid boundary cell not set correctly, got r={color.r}")
return False return False
# 65x65 should use chunk storage (exceeding threshold) # 65x65 should use chunk storage (exceeding threshold)
grid_65 = mcrfpy.Grid(grid_size=(65, 65), pos=(10, 10), size=(400, 400)) grid_65 = mcrfpy.Grid(grid_size=(65, 65), pos=(10, 10), size=(400, 400))
cell = grid_65.at(64, 64) color_layer_65 = grid_65.add_layer("color", z_index=-1)
cell.color = mcrfpy.Color(0, 255, 0, 255) color_layer_65.set(64, 64, mcrfpy.Color(0, 255, 0, 255))
color = grid_65.at(64, 64).color color = color_layer_65.at(64, 64)
if color[1] != 255: if color.g != 255:
print(f"FAIL: 65x65 grid cell not set correctly, got g={color[1]}") print(f"FAIL: 65x65 grid cell not set correctly, got g={color.g}")
return False return False
print(" Boundary cases: PASS") print(" Boundary cases: PASS")
@ -139,19 +141,18 @@ def test_edge_cases():
# Create 100x100 grid # Create 100x100 grid
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400)) grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400))
color_layer = grid.add_layer("color", z_index=-1)
# Test all corners # Test all corners
corners = [(0, 0), (99, 0), (0, 99), (99, 99)] corners = [(0, 0), (99, 0), (0, 99), (99, 99)]
for i, (x, y) in enumerate(corners): for i, (x, y) in enumerate(corners):
cell = grid.at(x, y) color_layer.set(x, y, mcrfpy.Color(i * 60, i * 60, i * 60, 255))
cell.color = mcrfpy.Color(i * 60, i * 60, i * 60, 255)
for i, (x, y) in enumerate(corners): for i, (x, y) in enumerate(corners):
cell = grid.at(x, y)
expected = i * 60 expected = i * 60
color = cell.color color = color_layer.at(x, y)
if color[0] != expected: if color.r != expected:
print(f"FAIL: Corner ({x},{y}) color mismatch, expected {expected}, got {color[0]}") print(f"FAIL: Corner ({x},{y}) color mismatch, expected {expected}, got {color.r}")
return False return False
print(" Edge cases: PASS") print(" Edge cases: PASS")

View file

@ -10,10 +10,10 @@ import sys
# Create a derived Entity class # Create a derived Entity class
class CustomEntity(mcrfpy.Entity): class CustomEntity(mcrfpy.Entity):
def __init__(self, x, y): def __init__(self, pos):
super().__init__(x, y) super().__init__(pos)
self.custom_attribute = "I am custom!" self.custom_attribute = "I am custom!"
def custom_method(self): def custom_method(self):
return "Custom method called" return "Custom method called"
@ -21,11 +21,11 @@ def run_test(runtime):
"""Test that derived entity classes maintain their type in collections""" """Test that derived entity classes maintain their type in collections"""
try: try:
# Create a grid # Create a grid
grid = mcrfpy.Grid(10, 10) grid = mcrfpy.Grid(grid_size=(10, 10))
# Create instances of base and derived entities # Create instances of base and derived entities
base_entity = mcrfpy.Entity(1, 1) base_entity = mcrfpy.Entity((1, 1))
custom_entity = CustomEntity(2, 2) custom_entity = CustomEntity((2, 2))
# Add them to the grid's entity collection # Add them to the grid's entity collection
grid.entities.append(base_entity) grid.entities.append(base_entity)

View file

@ -51,17 +51,17 @@ mcrfpy.setScene("timer_test_scene")
ui = mcrfpy.sceneUI("timer_test_scene") ui = mcrfpy.sceneUI("timer_test_scene")
# Add a bright red frame that should be visible # Add a bright red frame that should be visible
frame = mcrfpy.Frame(100, 100, 400, 300, frame = mcrfpy.Frame(pos=(100, 100), size=(400, 300),
fill_color=mcrfpy.Color(255, 0, 0), # Bright red fill_color=mcrfpy.Color(255, 0, 0), # Bright red
outline_color=mcrfpy.Color(255, 255, 255), # White outline outline_color=mcrfpy.Color(255, 255, 255), # White outline
outline=5.0) outline=5.0)
ui.append(frame) ui.append(frame)
# Add text # Add text
caption = mcrfpy.Caption(mcrfpy.Vector(150, 150), caption = mcrfpy.Caption(pos=(150, 150),
text="TIMER TEST - SHOULD BE VISIBLE", text="TIMER TEST - SHOULD BE VISIBLE",
fill_color=mcrfpy.Color(255, 255, 255)) fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 24 caption.font_size = 24
frame.children.append(caption) frame.children.append(caption)
# Add click handler to demonstrate interaction # Add click handler to demonstrate interaction

View file

@ -1,4 +1,4 @@
import mcrfpy import mcrfpy
e = mcrfpy.Entity(0, 0) e = mcrfpy.Entity((0, 0))
print("Entity attributes:", dir(e)) print("Entity attributes:", dir(e))
print("\nEntity repr:", repr(e)) print("\nEntity repr:", repr(e))

View file

@ -22,7 +22,7 @@ print(f"UI collection type: {type(ui)}")
print(f"Initial UI elements: {len(ui)}") print(f"Initial UI elements: {len(ui)}")
# Add a simple frame # Add a simple frame
frame = mcrfpy.Frame(0, 0, 100, 100, frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100),
fill_color=mcrfpy.Color(255, 255, 255)) fill_color=mcrfpy.Color(255, 255, 255))
ui.append(frame) ui.append(frame)
print(f"After adding frame: {len(ui)} elements") print(f"After adding frame: {len(ui)} elements")

View file

@ -22,14 +22,13 @@ mcrfpy.createScene("grid")
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Title # Title
title = mcrfpy.Caption(400, 30, "Grid Example - Dungeon View") title = mcrfpy.Caption(pos=(400, 30), text="Grid Example - Dungeon View")
title.font = mcrfpy.default_font title.font = mcrfpy.default_font
title.font_size = 24 title.font_size = 24
title.font_color = (255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
# Create main grid (20x15 tiles, each 32x32 pixels) # Create main grid (20x15 tiles, each 32x32 pixels)
grid = mcrfpy.Grid(100, 100, 20, 15, texture, 32, 32) grid = mcrfpy.Grid(pos=(100, 100), grid_size=(20, 15), texture=texture, size=(640, 480))
grid.texture = texture
# Define tile types from Crypt of Sokoban # Define tile types from Crypt of Sokoban
FLOOR = 58 # Stone floor FLOOR = 58 # Stone floor
@ -63,36 +62,21 @@ grid.set_tile(12, 8, BOULDER)
# Create some entities on the grid # Create some entities on the grid
# Player entity # Player entity
player = mcrfpy.Entity(5, 7) player = mcrfpy.Entity((5, 7), texture=texture, sprite_index=84, grid=grid) # Player sprite
player.texture = texture
player.sprite_index = 84 # Player sprite
# Enemy entities # Enemy entities
rat1 = mcrfpy.Entity(12, 5) rat1 = mcrfpy.Entity((12, 5), texture=texture, sprite_index=123, grid=grid) # Rat
rat1.texture = texture
rat1.sprite_index = 123 # Rat
rat2 = mcrfpy.Entity(14, 9) rat2 = mcrfpy.Entity((14, 9), texture=texture, sprite_index=123, grid=grid) # Rat
rat2.texture = texture
rat2.sprite_index = 123 # Rat
cyclops = mcrfpy.Entity(10, 10) cyclops = mcrfpy.Entity((10, 10), texture=texture, sprite_index=109, grid=grid) # Cyclops
cyclops.texture = texture
cyclops.sprite_index = 109 # Cyclops
# Add entities to grid
grid.entities.append(player)
grid.entities.append(rat1)
grid.entities.append(rat2)
grid.entities.append(cyclops)
# Create a smaller grid showing tile palette # Create a smaller grid showing tile palette
palette_label = mcrfpy.Caption(100, 600, "Tile Types:") palette_label = mcrfpy.Caption(pos=(100, 600), text="Tile Types:")
palette_label.font = mcrfpy.default_font palette_label.font = mcrfpy.default_font
palette_label.font_color = (255, 255, 255) palette_label.fill_color = mcrfpy.Color(255, 255, 255)
palette = mcrfpy.Grid(250, 580, 7, 1, texture, 32, 32) palette = mcrfpy.Grid(pos=(250, 580), grid_size=(7, 1), texture=texture, size=(224, 32))
palette.texture = texture
palette.set_tile(0, 0, FLOOR) palette.set_tile(0, 0, FLOOR)
palette.set_tile(1, 0, WALL) palette.set_tile(1, 0, WALL)
palette.set_tile(2, 0, DOOR) palette.set_tile(2, 0, DOOR)
@ -104,17 +88,17 @@ palette.set_tile(6, 0, BOULDER)
# Labels for palette # Labels for palette
labels = ["Floor", "Wall", "Door", "Chest", "Button", "Exit", "Boulder"] labels = ["Floor", "Wall", "Door", "Chest", "Button", "Exit", "Boulder"]
for i, label in enumerate(labels): for i, label in enumerate(labels):
l = mcrfpy.Caption(250 + i * 32, 615, label) l = mcrfpy.Caption(pos=(250 + i * 32, 615), text=label)
l.font = mcrfpy.default_font l.font = mcrfpy.default_font
l.font_size = 10 l.font_size = 10
l.font_color = (255, 255, 255) l.fill_color = mcrfpy.Color(255, 255, 255)
mcrfpy.sceneUI("grid").append(l) mcrfpy.sceneUI("grid").append(l)
# Add info caption # Add info caption
info = mcrfpy.Caption(100, 680, "Grid supports tiles and entities. Entities can move independently of the tile grid.") info = mcrfpy.Caption(pos=(100, 680), text="Grid supports tiles and entities. Entities can move independently of the tile grid.")
info.font = mcrfpy.default_font info.font = mcrfpy.default_font
info.font_size = 14 info.font_size = 14
info.font_color = (200, 200, 200) info.fill_color = mcrfpy.Color(200, 200, 200)
# Add all elements to scene # Add all elements to scene
ui = mcrfpy.sceneUI("grid") ui = mcrfpy.sceneUI("grid")

View file

@ -22,20 +22,20 @@ mcrfpy.createScene("sprites")
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Title # Title
title = mcrfpy.Caption(400, 30, "Sprite Examples") title = mcrfpy.Caption(pos=(400, 30), text="Sprite Examples")
title.font = mcrfpy.default_font title.font = mcrfpy.default_font
title.font_size = 24 title.font_size = 24
title.font_color = (255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
# Create a frame background # Create a frame background
frame = mcrfpy.Frame(50, 80, 700, 500) frame = mcrfpy.Frame(pos=(50, 80), size=(700, 500))
frame.bgcolor = (64, 64, 128) frame.fill_color = mcrfpy.Color(64, 64, 128)
frame.outline = 2 frame.outline = 2
# Player sprite # Player sprite
player_label = mcrfpy.Caption(100, 120, "Player") player_label = mcrfpy.Caption(pos=(100, 120), text="Player")
player_label.font = mcrfpy.default_font player_label.font = mcrfpy.default_font
player_label.font_color = (255, 255, 255) player_label.fill_color = mcrfpy.Color(255, 255, 255)
player = mcrfpy.Sprite(120, 150) player = mcrfpy.Sprite(120, 150)
player.texture = texture player.texture = texture
@ -43,9 +43,9 @@ player.sprite_index = 84 # Player sprite
player.scale = (3.0, 3.0) player.scale = (3.0, 3.0)
# Enemy sprites # Enemy sprites
enemy_label = mcrfpy.Caption(250, 120, "Enemies") enemy_label = mcrfpy.Caption(pos=(250, 120), text="Enemies")
enemy_label.font = mcrfpy.default_font enemy_label.font = mcrfpy.default_font
enemy_label.font_color = (255, 255, 255) enemy_label.fill_color = mcrfpy.Color(255, 255, 255)
rat = mcrfpy.Sprite(250, 150) rat = mcrfpy.Sprite(250, 150)
rat.texture = texture rat.texture = texture
@ -63,9 +63,9 @@ cyclops.sprite_index = 109 # Cyclops
cyclops.scale = (3.0, 3.0) cyclops.scale = (3.0, 3.0)
# Items row # Items row
items_label = mcrfpy.Caption(100, 250, "Items") items_label = mcrfpy.Caption(pos=(100, 250), text="Items")
items_label.font = mcrfpy.default_font items_label.font = mcrfpy.default_font
items_label.font_color = (255, 255, 255) items_label.fill_color = mcrfpy.Color(255, 255, 255)
# Boulder # Boulder
boulder = mcrfpy.Sprite(100, 280) boulder = mcrfpy.Sprite(100, 280)
@ -92,9 +92,9 @@ button.sprite_index = 250 # Button
button.scale = (3.0, 3.0) button.scale = (3.0, 3.0)
# UI elements row # UI elements row
ui_label = mcrfpy.Caption(100, 380, "UI Elements") ui_label = mcrfpy.Caption(pos=(100, 380), text="UI Elements")
ui_label.font = mcrfpy.default_font ui_label.font = mcrfpy.default_font
ui_label.font_color = (255, 255, 255) ui_label.fill_color = mcrfpy.Color(255, 255, 255)
# Hearts # Hearts
heart_full = mcrfpy.Sprite(100, 410) heart_full = mcrfpy.Sprite(100, 410)
@ -119,9 +119,9 @@ armor.sprite_index = 211 # Armor
armor.scale = (3.0, 3.0) armor.scale = (3.0, 3.0)
# Scale demonstration # Scale demonstration
scale_label = mcrfpy.Caption(500, 120, "Scale Demo") scale_label = mcrfpy.Caption(pos=(500, 120), text="Scale Demo")
scale_label.font = mcrfpy.default_font scale_label.font = mcrfpy.default_font
scale_label.font_color = (255, 255, 255) scale_label.fill_color = mcrfpy.Color(255, 255, 255)
# Same sprite at different scales # Same sprite at different scales
for i, scale in enumerate([1.0, 2.0, 3.0, 4.0]): for i, scale in enumerate([1.0, 2.0, 3.0, 4.0]):

View file

@ -17,42 +17,42 @@ def test_transparency_workaround():
# WORKAROUND: Create a full-window opaque frame as the first element # WORKAROUND: Create a full-window opaque frame as the first element
# This acts as an opaque background since the scene clears with transparent # This acts as an opaque background since the scene clears with transparent
print("Creating full-window opaque background...") print("Creating full-window opaque background...")
background = mcrfpy.Frame(0, 0, 1024, 768, background = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
fill_color=mcrfpy.Color(50, 50, 50), # Dark gray fill_color=mcrfpy.Color(50, 50, 50), # Dark gray
outline_color=None, outline_color=None,
outline=0.0) outline=0.0)
ui.append(background) ui.append(background)
print("✓ Added opaque background frame") print("✓ Added opaque background frame")
# Now add normal content on top # Now add normal content on top
print("\nAdding test content...") print("\nAdding test content...")
# Red frame # Red frame
frame1 = mcrfpy.Frame(100, 100, 200, 150, frame1 = mcrfpy.Frame(pos=(100, 100), size=(200, 150),
fill_color=mcrfpy.Color(255, 0, 0), fill_color=mcrfpy.Color(255, 0, 0),
outline_color=mcrfpy.Color(255, 255, 255), outline_color=mcrfpy.Color(255, 255, 255),
outline=3.0) outline=3.0)
ui.append(frame1) ui.append(frame1)
# Green frame # Green frame
frame2 = mcrfpy.Frame(350, 100, 200, 150, frame2 = mcrfpy.Frame(pos=(350, 100), size=(200, 150),
fill_color=mcrfpy.Color(0, 255, 0), fill_color=mcrfpy.Color(0, 255, 0),
outline_color=mcrfpy.Color(0, 0, 0), outline_color=mcrfpy.Color(0, 0, 0),
outline=3.0) outline=3.0)
ui.append(frame2) ui.append(frame2)
# Blue frame # Blue frame
frame3 = mcrfpy.Frame(100, 300, 200, 150, frame3 = mcrfpy.Frame(pos=(100, 300), size=(200, 150),
fill_color=mcrfpy.Color(0, 0, 255), fill_color=mcrfpy.Color(0, 0, 255),
outline_color=mcrfpy.Color(255, 255, 0), outline_color=mcrfpy.Color(255, 255, 0),
outline=3.0) outline=3.0)
ui.append(frame3) ui.append(frame3)
# Add text # Add text
caption = mcrfpy.Caption(mcrfpy.Vector(250, 50), caption = mcrfpy.Caption(pos=(250, 50),
text="OPAQUE BACKGROUND TEST", text="OPAQUE BACKGROUND TEST",
fill_color=mcrfpy.Color(255, 255, 255)) fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 32 caption.font_size = 32
ui.append(caption) ui.append(caption)
# Take screenshot # Take screenshot

View file

@ -31,9 +31,9 @@ def take_screenshot(runtime):
mcrfpy.createScene("test") mcrfpy.createScene("test")
# Add a visible element # Add a visible element
caption = mcrfpy.Caption(100, 100, "Screenshot Test") caption = mcrfpy.Caption(pos=(100, 100), text="Screenshot Test")
caption.font = mcrfpy.default_font caption.font = mcrfpy.default_font
caption.font_color = (255, 255, 255) caption.fill_color = mcrfpy.Color(255, 255, 255)
caption.font_size = 24 caption.font_size = 24
mcrfpy.sceneUI("test").append(caption) mcrfpy.sceneUI("test").append(caption)

View file

@ -30,7 +30,7 @@ mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
# Add visible content - a white frame on default background # Add visible content - a white frame on default background
frame = mcrfpy.Frame(100, 100, 200, 200, frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200),
fill_color=mcrfpy.Color(255, 255, 255)) fill_color=mcrfpy.Color(255, 255, 255))
ui.append(frame) ui.append(frame)

View file

@ -73,6 +73,9 @@ mcrfpy.createScene("chain_test")
grid = mcrfpy.Grid(grid_x=20, grid_y=15) grid = mcrfpy.Grid(grid_x=20, grid_y=15)
grid.fill_color = mcrfpy.Color(20, 20, 30) grid.fill_color = mcrfpy.Color(20, 20, 30)
# Add a color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Simple map # Simple map
for y in range(15): for y in range(15):
for x in range(20): for x in range(20):
@ -80,17 +83,17 @@ for y in range(15):
if x == 0 or x == 19 or y == 0 or y == 14: if x == 0 or x == 19 or y == 0 or y == 14:
cell.walkable = False cell.walkable = False
cell.transparent = False cell.transparent = False
cell.color = mcrfpy.Color(60, 40, 40) color_layer.set(x, y, mcrfpy.Color(60, 40, 40))
else: else:
cell.walkable = True cell.walkable = True
cell.transparent = True cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120) color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
# Create entities # Create entities
player = mcrfpy.Entity(2, 2, grid=grid) player = mcrfpy.Entity((2, 2), grid=grid)
player.sprite_index = 64 # @ player.sprite_index = 64 # @
enemy = mcrfpy.Entity(17, 12, grid=grid) enemy = mcrfpy.Entity((17, 12), grid=grid)
enemy.sprite_index = 69 # E enemy.sprite_index = 69 # E
# UI setup # UI setup
@ -99,15 +102,15 @@ ui.append(grid)
grid.position = (100, 100) grid.position = (100, 100)
grid.size = (600, 450) grid.size = (600, 450)
title = mcrfpy.Caption("Animation Chaining Test", 300, 20) title = mcrfpy.Caption(pos=(300, 20), text="Animation Chaining Test")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
status = mcrfpy.Caption("Press 1: Animate Player | 2: Animate Enemy | 3: Both | Q: Quit", 100, 50) status = mcrfpy.Caption(pos=(100, 50), text="Press 1: Animate Player | 2: Animate Enemy | 3: Both | Q: Quit")
status.fill_color = mcrfpy.Color(200, 200, 200) status.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status) ui.append(status)
info = mcrfpy.Caption("Status: Ready", 100, 70) info = mcrfpy.Caption(pos=(100, 70), text="Status: Ready")
info.fill_color = mcrfpy.Color(100, 255, 100) info.fill_color = mcrfpy.Color(100, 255, 100)
ui.append(info) ui.append(info)

View file

@ -63,14 +63,15 @@ mcrfpy.createScene("anim_debug")
# Simple grid # Simple grid
grid = mcrfpy.Grid(grid_x=15, grid_y=10) grid = mcrfpy.Grid(grid_x=15, grid_y=10)
color_layer = grid.add_layer("color", z_index=-1)
for y in range(10): for y in range(10):
for x in range(15): for x in range(15):
cell = grid.at(x, y) cell = grid.at(x, y)
cell.walkable = True cell.walkable = True
cell.color = mcrfpy.Color(100, 100, 120) color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
# Test entity # Test entity
entity = mcrfpy.Entity(5, 5, grid=grid) entity = mcrfpy.Entity((5, 5), grid=grid)
entity.sprite_index = 64 entity.sprite_index = 64
# UI # UI
@ -79,19 +80,19 @@ ui.append(grid)
grid.position = (100, 150) grid.position = (100, 150)
grid.size = (450, 300) grid.size = (450, 300)
title = mcrfpy.Caption("Animation Debug Tool", 250, 20) title = mcrfpy.Caption(pos=(250, 20), text="Animation Debug Tool")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
status = mcrfpy.Caption("Press keys to test animations", 100, 50) status = mcrfpy.Caption(pos=(100, 50), text="Press keys to test animations")
status.fill_color = mcrfpy.Color(200, 200, 200) status.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status) ui.append(status)
pos_display = mcrfpy.Caption("", 100, 70) pos_display = mcrfpy.Caption(pos=(100, 70), text="")
pos_display.fill_color = mcrfpy.Color(255, 255, 100) pos_display.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(pos_display) ui.append(pos_display)
active_display = mcrfpy.Caption("Active animations: 0", 100, 90) active_display = mcrfpy.Caption(pos=(100, 90), text="Active animations: 0")
active_display.fill_color = mcrfpy.Color(100, 255, 255) active_display.fill_color = mcrfpy.Color(100, 255, 255)
ui.append(active_display) ui.append(active_display)

View file

@ -13,7 +13,7 @@ print("2. Getting UI...")
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
print("3. Creating frame...") print("3. Creating frame...")
frame = mcrfpy.Frame(100, 100, 200, 200) frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
ui.append(frame) ui.append(frame)
print("4. Creating Animation object...") print("4. Creating Animation object...")

View file

@ -30,7 +30,7 @@ def test_1_basic_animation():
"""Test that basic animations still work""" """Test that basic animations still work"""
try: try:
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(100, 100, 100, 100) frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame) ui.append(frame)
anim = mcrfpy.Animation("x", 200.0, 1000, "linear") anim = mcrfpy.Animation("x", 200.0, 1000, "linear")
@ -49,7 +49,7 @@ def test_2_remove_animated_object():
"""Test removing object with active animation""" """Test removing object with active animation"""
try: try:
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(100, 100, 100, 100) frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame) ui.append(frame)
# Start animation # Start animation
@ -73,7 +73,7 @@ def test_3_complete_animation():
"""Test completing animation immediately""" """Test completing animation immediately"""
try: try:
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(100, 100, 100, 100) frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame) ui.append(frame)
# Start animation # Start animation
@ -98,7 +98,7 @@ def test_4_multiple_animations_timer():
nonlocal success nonlocal success
try: try:
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(200, 200, 100, 100) frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100))
ui.append(frame) ui.append(frame)
# Create multiple animations rapidly (this used to crash) # Create multiple animations rapidly (this used to crash)
@ -129,7 +129,7 @@ def test_5_scene_cleanup():
# Add animated objects to first scene # Add animated objects to first scene
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
for i in range(5): for i in range(5):
frame = mcrfpy.Frame(50 * i, 100, 40, 40) frame = mcrfpy.Frame(pos=(50 * i, 100), size=(40, 40))
ui.append(frame) ui.append(frame)
anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce") anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce")
anim.start(frame) anim.start(frame)
@ -148,9 +148,9 @@ def test_6_animation_after_clear():
"""Test animations after clearing UI""" """Test animations after clearing UI"""
try: try:
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
# Create and animate # Create and animate
frame = mcrfpy.Frame(100, 100, 100, 100) frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame) ui.append(frame)
anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic") anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic")
anim.start(frame) anim.start(frame)
@ -207,7 +207,7 @@ mcrfpy.setScene("test")
# Add a background # Add a background
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
bg = mcrfpy.Frame(0, 0, 1024, 768) bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768))
bg.fill_color = mcrfpy.Color(20, 20, 30) bg.fill_color = mcrfpy.Color(20, 20, 30)
ui.append(bg) ui.append(bg)

View file

@ -42,14 +42,14 @@ mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
# Add title and subtitle (to preserve during clearing) # Add title and subtitle (to preserve during clearing)
title = mcrfpy.Caption("Test Title", 400, 20) title = mcrfpy.Caption(pos=(400, 20), text="Test Title")
subtitle = mcrfpy.Caption("Test Subtitle", 400, 50) subtitle = mcrfpy.Caption(pos=(400, 50), text="Test Subtitle")
ui.extend([title, subtitle]) ui.extend([title, subtitle])
# Create initial animated objects # Create initial animated objects
print("Creating initial animated objects...") print("Creating initial animated objects...")
for i in range(10): for i in range(10):
f = mcrfpy.Frame(50 + i*30, 100, 25, 25) f = mcrfpy.Frame(pos=(50 + i*30, 100), size=(25, 25))
f.fill_color = mcrfpy.Color(255, 100, 100) f.fill_color = mcrfpy.Color(255, 100, 100)
ui.append(f) ui.append(f)

View file

@ -17,10 +17,15 @@ import sys
def create_test_grid(): def create_test_grid():
"""Create a test grid with obstacles""" """Create a test grid with obstacles"""
mcrfpy.createScene("dijkstra_test") mcrfpy.createScene("dijkstra_test")
# Create grid # Create grid
grid = mcrfpy.Grid(grid_x=20, grid_y=20) grid = mcrfpy.Grid(grid_x=20, grid_y=20)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Store color_layer on grid for access elsewhere
grid._color_layer = color_layer
# Initialize all cells as walkable # Initialize all cells as walkable
for y in range(grid.grid_y): for y in range(grid.grid_y):
for x in range(grid.grid_x): for x in range(grid.grid_x):
@ -28,8 +33,8 @@ def create_test_grid():
cell.walkable = True cell.walkable = True
cell.transparent = True cell.transparent = True
cell.tilesprite = 46 # . period cell.tilesprite = 46 # . period
cell.color = mcrfpy.Color(50, 50, 50) color_layer.set(x, y, mcrfpy.Color(50, 50, 50))
# Create some walls to make pathfinding interesting # Create some walls to make pathfinding interesting
# Vertical wall # Vertical wall
for y in range(5, 15): for y in range(5, 15):
@ -37,8 +42,8 @@ def create_test_grid():
cell.walkable = False cell.walkable = False
cell.transparent = False cell.transparent = False
cell.tilesprite = 219 # Block cell.tilesprite = 219 # Block
cell.color = mcrfpy.Color(100, 100, 100) color_layer.set(10, y, mcrfpy.Color(100, 100, 100))
# Horizontal wall # Horizontal wall
for x in range(5, 15): for x in range(5, 15):
if x != 10: # Leave a gap if x != 10: # Leave a gap
@ -46,8 +51,8 @@ def create_test_grid():
cell.walkable = False cell.walkable = False
cell.transparent = False cell.transparent = False
cell.tilesprite = 219 cell.tilesprite = 219
cell.color = mcrfpy.Color(100, 100, 100) color_layer.set(x, 10, mcrfpy.Color(100, 100, 100))
return grid return grid
def test_basic_dijkstra(): def test_basic_dijkstra():
@ -133,7 +138,7 @@ def test_multi_target_scenario():
# Mark threat position # Mark threat position
cell = grid.at(tx, ty) cell = grid.at(tx, ty)
cell.tilesprite = 84 # T for threat cell.tilesprite = 84 # T for threat
cell.color = mcrfpy.Color(255, 0, 0) grid._color_layer.set(tx, ty, mcrfpy.Color(255, 0, 0))
# Compute Dijkstra from this threat # Compute Dijkstra from this threat
grid.compute_dijkstra(tx, ty) grid.compute_dijkstra(tx, ty)
@ -176,7 +181,7 @@ def test_multi_target_scenario():
# Mark safe position # Mark safe position
cell = grid.at(best_pos[0], best_pos[1]) cell = grid.at(best_pos[0], best_pos[1])
cell.tilesprite = 83 # S for safe cell.tilesprite = 83 # S for safe
cell.color = mcrfpy.Color(0, 255, 0) grid._color_layer.set(best_pos[0], best_pos[1], mcrfpy.Color(0, 255, 0))
def run_test(runtime): def run_test(runtime):
"""Timer callback to run tests after scene loads""" """Timer callback to run tests after scene loads"""
@ -211,7 +216,7 @@ ui = mcrfpy.sceneUI("dijkstra_test")
ui.append(grid) ui.append(grid)
# Add title # Add title
title = mcrfpy.Caption("Dijkstra Pathfinding Test", 10, 10) title = mcrfpy.Caption(pos=(10, 10), text="Dijkstra Pathfinding Test")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)

View file

@ -17,13 +17,16 @@ mcrfpy.createScene("test_anim")
grid = mcrfpy.Grid(grid_x=15, grid_y=15) grid = mcrfpy.Grid(grid_x=15, grid_y=15)
grid.fill_color = mcrfpy.Color(20, 20, 30) grid.fill_color = mcrfpy.Color(20, 20, 30)
# Add a color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Initialize all cells as walkable floors # Initialize all cells as walkable floors
for y in range(15): for y in range(15):
for x in range(15): for x in range(15):
cell = grid.at(x, y) cell = grid.at(x, y)
cell.walkable = True cell.walkable = True
cell.transparent = True cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120) color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
# Mark the path we'll follow with different color # Mark the path we'll follow with different color
path_cells = [(5,5), (6,5), (7,5), (8,5), (9,5), (10,5), path_cells = [(5,5), (6,5), (7,5), (8,5), (9,5), (10,5),
@ -32,11 +35,10 @@ path_cells = [(5,5), (6,5), (7,5), (8,5), (9,5), (10,5),
(5,9), (5,8), (5,7), (5,6)] (5,9), (5,8), (5,7), (5,6)]
for x, y in path_cells: for x, y in path_cells:
cell = grid.at(x, y) color_layer.set(x, y, mcrfpy.Color(120, 120, 150))
cell.color = mcrfpy.Color(120, 120, 150)
# Create entity at start position # Create entity at start position
entity = mcrfpy.Entity(5, 5, grid=grid) entity = mcrfpy.Entity((5, 5), grid=grid)
entity.sprite_index = 64 # @ entity.sprite_index = 64 # @
# UI setup # UI setup
@ -46,27 +48,27 @@ grid.position = (100, 100)
grid.size = (450, 450) # 15 * 30 pixels per cell grid.size = (450, 450) # 15 * 30 pixels per cell
# Title # Title
title = mcrfpy.Caption("Entity Animation Test - Square Path", 200, 20) title = mcrfpy.Caption(pos=(200, 20), text="Entity Animation Test - Square Path")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Status display # Status display
status = mcrfpy.Caption("Press SPACE to start animation | Q to quit", 100, 50) status = mcrfpy.Caption(pos=(100, 50), text="Press SPACE to start animation | Q to quit")
status.fill_color = mcrfpy.Color(200, 200, 200) status.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status) ui.append(status)
# Position display # Position display
pos_display = mcrfpy.Caption(f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})", 100, 70) pos_display = mcrfpy.Caption(pos=(100, 70), text=f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})")
pos_display.fill_color = mcrfpy.Color(255, 255, 100) pos_display.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(pos_display) ui.append(pos_display)
# Animation info # Animation info
anim_info = mcrfpy.Caption("Animation: Not started", 400, 70) anim_info = mcrfpy.Caption(pos=(400, 70), text="Animation: Not started")
anim_info.fill_color = mcrfpy.Color(100, 255, 255) anim_info.fill_color = mcrfpy.Color(100, 255, 255)
ui.append(anim_info) ui.append(anim_info)
# Debug info # Debug info
debug_info = mcrfpy.Caption("Debug: Waiting...", 100, 570) debug_info = mcrfpy.Caption(pos=(100, 570), text="Debug: Waiting...")
debug_info.fill_color = mcrfpy.Color(150, 150, 150) debug_info.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(debug_info) ui.append(debug_info)

View file

@ -33,16 +33,19 @@ mcrfpy.createScene("fix_demo")
grid = mcrfpy.Grid(grid_x=15, grid_y=10) grid = mcrfpy.Grid(grid_x=15, grid_y=10)
grid.fill_color = mcrfpy.Color(20, 20, 30) grid.fill_color = mcrfpy.Color(20, 20, 30)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Make floor # Make floor
for y in range(10): for y in range(10):
for x in range(15): for x in range(15):
cell = grid.at(x, y) cell = grid.at(x, y)
cell.walkable = True cell.walkable = True
cell.transparent = True cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120) color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
# Create entity # Create entity
entity = mcrfpy.Entity(2, 2, grid=grid) entity = mcrfpy.Entity((2, 2), grid=grid)
entity.sprite_index = 64 # @ entity.sprite_index = 64 # @
# UI # UI
@ -52,19 +55,19 @@ grid.position = (100, 150)
grid.size = (450, 300) grid.size = (450, 300)
# Info displays # Info displays
title = mcrfpy.Caption("Entity Animation Issue Demo", 250, 20) title = mcrfpy.Caption(pos=(250, 20), text="Entity Animation Issue Demo")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
pos_info = mcrfpy.Caption("", 100, 50) pos_info = mcrfpy.Caption(pos=(100, 50), text="")
pos_info.fill_color = mcrfpy.Color(255, 255, 100) pos_info.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(pos_info) ui.append(pos_info)
sprite_info = mcrfpy.Caption("", 100, 70) sprite_info = mcrfpy.Caption(pos=(100, 70), text="")
sprite_info.fill_color = mcrfpy.Color(255, 100, 100) sprite_info.fill_color = mcrfpy.Color(255, 100, 100)
ui.append(sprite_info) ui.append(sprite_info)
status = mcrfpy.Caption("Press SPACE to animate entity", 100, 100) status = mcrfpy.Caption(pos=(100, 100), text="Press SPACE to animate entity")
status.fill_color = mcrfpy.Color(200, 200, 200) status.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status) ui.append(status)

View file

@ -22,8 +22,7 @@ for x, y in walls:
grid.at(x, y).walkable = False grid.at(x, y).walkable = False
# Create entity # Create entity
entity = mcrfpy.Entity(2, 2) entity = mcrfpy.Entity((2, 2), grid=grid)
grid.entities.append(entity)
print(f"Entity at: ({entity.x}, {entity.y})") print(f"Entity at: ({entity.x}, {entity.y})")

View file

@ -9,7 +9,7 @@ print("=" * 50)
# Test 1: Entity without grid # Test 1: Entity without grid
print("Test 1: Entity not in grid") print("Test 1: Entity not in grid")
try: try:
entity = mcrfpy.Entity(5, 5) entity = mcrfpy.Entity((5, 5))
path = entity.path_to(8, 8) path = entity.path_to(8, 8)
print(" ✗ Should have failed for entity not in grid") print(" ✗ Should have failed for entity not in grid")
except ValueError as e: except ValueError as e:
@ -31,8 +31,7 @@ for y in range(5):
for x in range(5): for x in range(5):
grid.at(x, 2).walkable = False grid.at(x, 2).walkable = False
entity = mcrfpy.Entity(1, 1) entity = mcrfpy.Entity((1, 1), grid=grid)
grid.entities.append(entity)
try: try:
path = entity.path_to(1, 4) path = entity.path_to(1, 4)

View file

@ -13,32 +13,28 @@ def test_grid_background():
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
# Create a grid with default background # Create a grid with default background
grid = mcrfpy.Grid(20, 15, grid_size=(20, 15)) grid = mcrfpy.Grid(pos=(50, 50), size=(400, 300), grid_size=(20, 15))
grid.x = 50
grid.y = 50
grid.w = 400
grid.h = 300
ui.append(grid) ui.append(grid)
# Add some tiles to see the background better # Add color layer for some tiles to see the background better
color_layer = grid.add_layer("color", z_index=-1)
for x in range(5, 15): for x in range(5, 15):
for y in range(5, 10): for y in range(5, 10):
point = grid.at(x, y) color_layer.set(x, y, mcrfpy.Color(100, 150, 100))
point.color = mcrfpy.Color(100, 150, 100)
# Add UI to show current background color # Add UI to show current background color
info_frame = mcrfpy.Frame(500, 50, 200, 150, info_frame = mcrfpy.Frame(pos=(500, 50), size=(200, 150),
fill_color=mcrfpy.Color(40, 40, 40), fill_color=mcrfpy.Color(40, 40, 40),
outline_color=mcrfpy.Color(200, 200, 200), outline_color=mcrfpy.Color(200, 200, 200),
outline=2) outline=2)
ui.append(info_frame) ui.append(info_frame)
color_caption = mcrfpy.Caption(510, 60, "Background Color:") color_caption = mcrfpy.Caption(pos=(510, 60), text="Background Color:")
color_caption.font_size = 14 color_caption.font_size = 14
color_caption.fill_color = mcrfpy.Color(255, 255, 255) color_caption.fill_color = mcrfpy.Color(255, 255, 255)
info_frame.children.append(color_caption) info_frame.children.append(color_caption)
color_display = mcrfpy.Caption(510, 80, "") color_display = mcrfpy.Caption(pos=(510, 80), text="")
color_display.font_size = 12 color_display.font_size = 12
color_display.fill_color = mcrfpy.Color(200, 200, 200) color_display.fill_color = mcrfpy.Color(200, 200, 200)
info_frame.children.append(color_display) info_frame.children.append(color_display)

View file

@ -11,8 +11,8 @@ ui = mcrfpy.sceneUI("detect_test")
mcrfpy.setScene("detect_test") mcrfpy.setScene("detect_test")
# Create a frame # Create a frame
frame = mcrfpy.Frame(100, 100, 200, 200) frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
frame.fill_color = (255, 100, 100, 255) frame.fill_color = mcrfpy.Color(255, 100, 100, 255)
ui.append(frame) ui.append(frame)
def test_mode(runtime): def test_mode(runtime):

View file

@ -10,13 +10,13 @@ ui = mcrfpy.sceneUI("headless_test")
mcrfpy.setScene("headless_test") mcrfpy.setScene("headless_test")
# Create a visible indicator # Create a visible indicator
frame = mcrfpy.Frame(200, 200, 400, 200) frame = mcrfpy.Frame(pos=(200, 200), size=(400, 200))
frame.fill_color = (100, 200, 100, 255) frame.fill_color = mcrfpy.Color(100, 200, 100, 255)
ui.append(frame) ui.append(frame)
caption = mcrfpy.Caption((400, 300), "If you see this, windowed mode is working!", mcrfpy.default_font) caption = mcrfpy.Caption(pos=(400, 300), text="If you see this, windowed mode is working!")
caption.size = 24 caption.font_size = 24
caption.fill_color = (255, 255, 255) caption.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(caption) ui.append(caption)
print("Script started. Window should appear unless --headless was specified.") print("Script started. Window should appear unless --headless was specified.")

View file

@ -115,18 +115,18 @@ mcrfpy.setScene("metrics_test")
ui = mcrfpy.sceneUI("metrics_test") ui = mcrfpy.sceneUI("metrics_test")
# Create various UI elements # Create various UI elements
frame1 = mcrfpy.Frame(10, 10, 200, 150) frame1 = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
frame1.fill_color = (100, 100, 100, 128) frame1.fill_color = mcrfpy.Color(100, 100, 100, 128)
ui.append(frame1) ui.append(frame1)
caption1 = mcrfpy.Caption("Test Caption", 50, 50) caption1 = mcrfpy.Caption(pos=(50, 50), text="Test Caption")
ui.append(caption1) ui.append(caption1)
sprite1 = mcrfpy.Sprite(100, 100) sprite1 = mcrfpy.Sprite(pos=(100, 100))
ui.append(sprite1) ui.append(sprite1)
# Invisible element (should not count as visible) # Invisible element (should not count as visible)
frame2 = mcrfpy.Frame(300, 10, 100, 100) frame2 = mcrfpy.Frame(pos=(300, 10), size=(100, 100))
frame2.visible = False frame2.visible = False
ui.append(frame2) ui.append(frame2)

View file

@ -11,17 +11,20 @@ print("=" * 50)
mcrfpy.createScene("test") mcrfpy.createScene("test")
grid = mcrfpy.Grid(grid_x=5, grid_y=5) grid = mcrfpy.Grid(grid_x=5, grid_y=5)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Initialize # Initialize
for y in range(5): for y in range(5):
for x in range(5): for x in range(5):
grid.at(x, y).walkable = True grid.at(x, y).walkable = True
grid.at(x, y).color = mcrfpy.Color(200, 200, 200) # Light gray color_layer.set(x, y, mcrfpy.Color(200, 200, 200)) # Light gray
# Add entities # Add entities
e1 = mcrfpy.Entity(0, 0) e1 = mcrfpy.Entity((0, 0), grid=grid)
e2 = mcrfpy.Entity(4, 4) e2 = mcrfpy.Entity((4, 4), grid=grid)
grid.entities.append(e1) e1.sprite_index = 64
grid.entities.append(e2) e2.sprite_index = 69
print(f"Entity 1 at ({e1.x}, {e1.y})") print(f"Entity 1 at ({e1.x}, {e1.y})")
print(f"Entity 2 at ({e2.x}, {e2.y})") print(f"Entity 2 at ({e2.x}, {e2.y})")
@ -35,24 +38,25 @@ PATH_COLOR = mcrfpy.Color(100, 255, 100) # Green
print(f"\nSetting path cells to green ({PATH_COLOR.r}, {PATH_COLOR.g}, {PATH_COLOR.b})...") print(f"\nSetting path cells to green ({PATH_COLOR.r}, {PATH_COLOR.g}, {PATH_COLOR.b})...")
for x, y in path: for x, y in path:
cell = grid.at(x, y)
# Check before # Check before
before = cell.color[:3] # Get RGB from tuple before_c = color_layer.at(x, y)
before = (before_c.r, before_c.g, before_c.b)
# Set color # Set color
cell.color = PATH_COLOR color_layer.set(x, y, PATH_COLOR)
# Check after # Check after
after = cell.color[:3] # Get RGB from tuple after_c = color_layer.at(x, y)
after = (after_c.r, after_c.g, after_c.b)
print(f" Cell ({x},{y}): {before} -> {after}") print(f" Cell ({x},{y}): {before} -> {after}")
# Verify all path cells # Verify all path cells
print("\nVerifying all cells in grid:") print("\nVerifying all cells in grid:")
for y in range(5): for y in range(5):
for x in range(5): for x in range(5):
cell = grid.at(x, y) c = color_layer.at(x, y)
color = cell.color[:3] # Get RGB from tuple color = (c.r, c.g, c.b)
is_path = (x, y) in path is_path = (x, y) in path
print(f" ({x},{y}): color={color}, in_path={is_path}") print(f" ({x},{y}): color={color}, in_path={is_path}")

View file

@ -21,10 +21,8 @@ for i in range(5):
grid.at(5, i + 2).walkable = False grid.at(5, i + 2).walkable = False
# Create entities # Create entities
e1 = mcrfpy.Entity(2, 5) e1 = mcrfpy.Entity((2, 5), grid=grid)
e2 = mcrfpy.Entity(8, 5) e2 = mcrfpy.Entity((8, 5), grid=grid)
grid.entities.append(e1)
grid.entities.append(e2)
# Test pathfinding between entities # Test pathfinding between entities
print(f"Entity 1 at ({e1.x}, {e1.y})") print(f"Entity 1 at ({e1.x}, {e1.y})")

View file

@ -10,7 +10,7 @@ def test_properties(runtime):
# Test Frame # Test Frame
try: try:
frame = mcrfpy.Frame(10, 10, 100, 100) frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
print(f"Frame visible: {frame.visible}") print(f"Frame visible: {frame.visible}")
frame.visible = False frame.visible = False
print(f"Frame visible after setting to False: {frame.visible}") print(f"Frame visible after setting to False: {frame.visible}")

View file

@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""Test the object-oriented Scene API (alternative to module-level functions).
The Scene object provides an OOP approach to scene management with several advantages:
1. `scene.on_key` can be set on ANY scene, not just the current one
2. `scene.children` provides direct access to UI elements
3. Subclassing enables lifecycle callbacks (on_enter, on_exit, update, etc.)
This is the recommended approach for new code, replacing:
- mcrfpy.createScene(name) -> scene = mcrfpy.Scene(name)
- mcrfpy.setScene(name) -> scene.activate()
- mcrfpy.sceneUI(name) -> scene.children
- mcrfpy.keypressScene(callback) -> scene.on_key = callback
"""
import mcrfpy
import sys
def test_scene_object_basics():
"""Test basic Scene object creation and properties."""
print("=== Test: Scene Object Basics ===")
# Create scene using object-oriented approach
scene = mcrfpy.Scene("oop_test")
# Check name property
assert scene.name == "oop_test", f"Expected 'oop_test', got '{scene.name}'"
print(f" name: {scene.name}")
# Check active property (should be False, not yet activated)
print(f" active: {scene.active}")
# Check children property returns UICollection
children = scene.children
print(f" children type: {type(children).__name__}")
print(f" children count: {len(children)}")
# Add UI elements via children
frame = mcrfpy.Frame(pos=(50, 50), size=(200, 100), fill_color=mcrfpy.Color(100, 100, 200))
scene.children.append(frame)
print(f" children count after append: {len(scene.children)}")
print(" PASS: Basic properties work correctly")
return scene
def test_scene_activation():
"""Test scene activation."""
print("\n=== Test: Scene Activation ===")
scene1 = mcrfpy.Scene("scene_a")
scene2 = mcrfpy.Scene("scene_b")
# Neither active yet
print(f" Before activation - scene1.active: {scene1.active}, scene2.active: {scene2.active}")
# Activate scene1
scene1.activate()
print(f" After scene1.activate() - scene1.active: {scene1.active}, scene2.active: {scene2.active}")
assert scene1.active == True, "scene1 should be active"
assert scene2.active == False, "scene2 should not be active"
# Activate scene2
scene2.activate()
print(f" After scene2.activate() - scene1.active: {scene1.active}, scene2.active: {scene2.active}")
assert scene1.active == False, "scene1 should not be active now"
assert scene2.active == True, "scene2 should be active"
print(" PASS: Scene activation works correctly")
def test_scene_on_key():
"""Test setting on_key callback on scene objects.
This is the KEY ADVANTAGE over module-level keypressScene():
You can set on_key on ANY scene, not just the current one!
"""
print("\n=== Test: Scene on_key Property ===")
scene1 = mcrfpy.Scene("keys_scene1")
scene2 = mcrfpy.Scene("keys_scene2")
# Track which callback was called
callback_log = []
def scene1_keyhandler(key, action):
callback_log.append(("scene1", key, action))
def scene2_keyhandler(key, action):
callback_log.append(("scene2", key, action))
# Set callbacks on BOTH scenes BEFORE activating either
# This is impossible with keypressScene() which only works on current scene!
scene1.on_key = scene1_keyhandler
scene2.on_key = scene2_keyhandler
print(f" scene1.on_key set: {scene1.on_key is not None}")
print(f" scene2.on_key set: {scene2.on_key is not None}")
# Verify callbacks are retrievable
assert callable(scene1.on_key), "scene1.on_key should be callable"
assert callable(scene2.on_key), "scene2.on_key should be callable"
# Test clearing callback
scene1.on_key = None
assert scene1.on_key is None, "scene1.on_key should be None after clearing"
print(" scene1.on_key cleared successfully")
# Re-set it
scene1.on_key = scene1_keyhandler
print(" PASS: on_key property works correctly")
def test_scene_visual_properties():
"""Test scene-level visual properties (pos, visible, opacity)."""
print("\n=== Test: Scene Visual Properties ===")
scene = mcrfpy.Scene("visual_props_test")
# Test pos property
print(f" Initial pos: {scene.pos}")
scene.pos = (100, 50)
print(f" After setting pos=(100, 50): {scene.pos}")
# Test visible property
print(f" Initial visible: {scene.visible}")
scene.visible = False
print(f" After setting visible=False: {scene.visible}")
assert scene.visible == False, "visible should be False"
scene.visible = True
# Test opacity property
print(f" Initial opacity: {scene.opacity}")
scene.opacity = 0.5
print(f" After setting opacity=0.5: {scene.opacity}")
assert 0.49 < scene.opacity < 0.51, f"opacity should be ~0.5, got {scene.opacity}"
print(" PASS: Visual properties work correctly")
def test_scene_subclass():
"""Test subclassing Scene for lifecycle callbacks."""
print("\n=== Test: Scene Subclass with Lifecycle ===")
class GameScene(mcrfpy.Scene):
def __init__(self, name):
super().__init__(name)
self.enter_count = 0
self.exit_count = 0
self.update_count = 0
def on_enter(self):
self.enter_count += 1
print(f" GameScene.on_enter() called (count: {self.enter_count})")
def on_exit(self):
self.exit_count += 1
print(f" GameScene.on_exit() called (count: {self.exit_count})")
def on_keypress(self, key, action):
print(f" GameScene.on_keypress({key}, {action})")
def update(self, dt):
self.update_count += 1
# Note: update is called every frame, so we don't print
game_scene = GameScene("game_scene_test")
other_scene = mcrfpy.Scene("other_scene_test")
# Add some UI to game scene
game_scene.children.append(
mcrfpy.Caption(pos=(100, 100), text="Game Scene", fill_color=mcrfpy.Color(255, 255, 255))
)
print(f" Created GameScene with {len(game_scene.children)} children")
print(f" enter_count before activation: {game_scene.enter_count}")
# Activate - should trigger on_enter
game_scene.activate()
print(f" enter_count after activation: {game_scene.enter_count}")
# Switch away - should trigger on_exit
other_scene.activate()
print(f" exit_count after switching: {game_scene.exit_count}")
print(" PASS: Subclassing works correctly")
def test_comparison_with_module_functions():
"""Demonstrate the difference between old and new approaches."""
print("\n=== Comparison: Module Functions vs Scene Objects ===")
print("\n OLD APPROACH (module-level functions):")
print(" mcrfpy.createScene('my_scene')")
print(" mcrfpy.setScene('my_scene')")
print(" ui = mcrfpy.sceneUI('my_scene')")
print(" ui.append(mcrfpy.Frame(...))")
print(" mcrfpy.keypressScene(handler) # ONLY works on current scene!")
print("\n NEW APPROACH (Scene objects):")
print(" scene = mcrfpy.Scene('my_scene')")
print(" scene.activate()")
print(" scene.children.append(mcrfpy.Frame(...))")
print(" scene.on_key = handler # Works on ANY scene!")
print("\n KEY BENEFITS:")
print(" 1. scene.on_key can be set on non-active scenes")
print(" 2. Subclassing enables on_enter/on_exit/update callbacks")
print(" 3. Object reference makes code more readable")
print(" 4. scene.children is clearer than sceneUI(name)")
print("\n PASS: Documentation complete")
def main():
"""Run all Scene object API tests."""
print("=" * 60)
print("Scene Object API Test Suite")
print("=" * 60)
try:
test_scene_object_basics()
test_scene_activation()
test_scene_on_key()
test_scene_visual_properties()
test_scene_subclass()
test_comparison_with_module_functions()
print("\n" + "=" * 60)
print("ALL TESTS PASSED!")
print("=" * 60)
sys.exit(0)
except Exception as e:
print(f"\nTEST FAILED: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -11,51 +11,51 @@ def create_test_scenes():
# Scene 1: Red background # Scene 1: Red background
mcrfpy.createScene("red_scene") mcrfpy.createScene("red_scene")
ui1 = mcrfpy.sceneUI("red_scene") ui1 = mcrfpy.sceneUI("red_scene")
bg1 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(255, 0, 0, 255)) bg1 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(255, 0, 0, 255))
label1 = mcrfpy.Caption(512, 384, "RED SCENE", font=mcrfpy.Font.font_ui) label1 = mcrfpy.Caption(pos=(512, 384), text="RED SCENE", font=mcrfpy.Font.font_ui)
label1.color = mcrfpy.Color(255, 255, 255, 255) label1.fill_color = mcrfpy.Color(255, 255, 255, 255)
ui1.append(bg1) ui1.append(bg1)
ui1.append(label1) ui1.append(label1)
# Scene 2: Blue background # Scene 2: Blue background
mcrfpy.createScene("blue_scene") mcrfpy.createScene("blue_scene")
ui2 = mcrfpy.sceneUI("blue_scene") ui2 = mcrfpy.sceneUI("blue_scene")
bg2 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(0, 0, 255, 255)) bg2 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(0, 0, 255, 255))
label2 = mcrfpy.Caption(512, 384, "BLUE SCENE", font=mcrfpy.Font.font_ui) label2 = mcrfpy.Caption(pos=(512, 384), text="BLUE SCENE", font=mcrfpy.Font.font_ui)
label2.color = mcrfpy.Color(255, 255, 255, 255) label2.fill_color = mcrfpy.Color(255, 255, 255, 255)
ui2.append(bg2) ui2.append(bg2)
ui2.append(label2) ui2.append(label2)
# Scene 3: Green background # Scene 3: Green background
mcrfpy.createScene("green_scene") mcrfpy.createScene("green_scene")
ui3 = mcrfpy.sceneUI("green_scene") ui3 = mcrfpy.sceneUI("green_scene")
bg3 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(0, 255, 0, 255)) bg3 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(0, 255, 0, 255))
label3 = mcrfpy.Caption(512, 384, "GREEN SCENE", font=mcrfpy.Font.font_ui) label3 = mcrfpy.Caption(pos=(512, 384), text="GREEN SCENE", font=mcrfpy.Font.font_ui)
label3.color = mcrfpy.Color(0, 0, 0, 255) # Black text on green label3.fill_color = mcrfpy.Color(0, 0, 0, 255) # Black text on green
ui3.append(bg3) ui3.append(bg3)
ui3.append(label3) ui3.append(label3)
# Scene 4: Menu scene with buttons # Scene 4: Menu scene with buttons
mcrfpy.createScene("menu_scene") mcrfpy.createScene("menu_scene")
ui4 = mcrfpy.sceneUI("menu_scene") ui4 = mcrfpy.sceneUI("menu_scene")
bg4 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(50, 50, 50, 255)) bg4 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(50, 50, 50, 255))
title = mcrfpy.Caption(512, 100, "SCENE TRANSITION DEMO", font=mcrfpy.Font.font_ui) title = mcrfpy.Caption(pos=(512, 100), text="SCENE TRANSITION DEMO", font=mcrfpy.Font.font_ui)
title.color = mcrfpy.Color(255, 255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255, 255)
ui4.append(bg4) ui4.append(bg4)
ui4.append(title) ui4.append(title)
# Add instruction text # Add instruction text
instructions = mcrfpy.Caption(512, 200, "Press keys 1-6 for different transitions", font=mcrfpy.Font.font_ui) instructions = mcrfpy.Caption(pos=(512, 200), text="Press keys 1-6 for different transitions", font=mcrfpy.Font.font_ui)
instructions.color = mcrfpy.Color(200, 200, 200, 255) instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
ui4.append(instructions) ui4.append(instructions)
controls = mcrfpy.Caption(512, 250, "1: Fade | 2: Slide Left | 3: Slide Right | 4: Slide Up | 5: Slide Down | 6: Instant", font=mcrfpy.Font.font_ui) controls = mcrfpy.Caption(pos=(512, 250), text="1: Fade | 2: Slide Left | 3: Slide Right | 4: Slide Up | 5: Slide Down | 6: Instant", font=mcrfpy.Font.font_ui)
controls.color = mcrfpy.Color(150, 150, 150, 255) controls.fill_color = mcrfpy.Color(150, 150, 150, 255)
ui4.append(controls) ui4.append(controls)
scene_info = mcrfpy.Caption(512, 300, "R: Red Scene | B: Blue Scene | G: Green Scene | M: Menu", font=mcrfpy.Font.font_ui) scene_info = mcrfpy.Caption(pos=(512, 300), text="R: Red Scene | B: Blue Scene | G: Green Scene | M: Menu", font=mcrfpy.Font.font_ui)
scene_info.color = mcrfpy.Color(150, 150, 150, 255) scene_info.fill_color = mcrfpy.Color(150, 150, 150, 255)
ui4.append(scene_info) ui4.append(scene_info)
print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene") print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene")

View file

@ -13,13 +13,13 @@ def test_scene_transitions():
# Scene 1 # Scene 1
mcrfpy.createScene("scene1") mcrfpy.createScene("scene1")
ui1 = mcrfpy.sceneUI("scene1") ui1 = mcrfpy.sceneUI("scene1")
frame1 = mcrfpy.Frame(0, 0, 100, 100, fill_color=mcrfpy.Color(255, 0, 0)) frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100), fill_color=mcrfpy.Color(255, 0, 0))
ui1.append(frame1) ui1.append(frame1)
# Scene 2 # Scene 2
mcrfpy.createScene("scene2") mcrfpy.createScene("scene2")
ui2 = mcrfpy.sceneUI("scene2") ui2 = mcrfpy.sceneUI("scene2")
frame2 = mcrfpy.Frame(0, 0, 100, 100, fill_color=mcrfpy.Color(0, 0, 255)) frame2 = mcrfpy.Frame(pos=(0, 0), size=(100, 100), fill_color=mcrfpy.Color(0, 0, 255))
ui2.append(frame2) ui2.append(frame2)
# Test each transition type # Test each transition type

View file

@ -8,7 +8,7 @@ def simple_test(runtime):
try: try:
# Test basic functionality # Test basic functionality
frame = mcrfpy.Frame(10, 10, 100, 100) frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}") print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}")
bounds = frame.get_bounds() bounds = frame.get_bounds()

View file

@ -18,13 +18,13 @@ def create_demo():
scene = mcrfpy.sceneUI("text_demo") scene = mcrfpy.sceneUI("text_demo")
# Background # Background
bg = mcrfpy.Frame(0, 0, 800, 600) bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
bg.fill_color = (40, 40, 40, 255) bg.fill_color = mcrfpy.Color(40, 40, 40, 255)
scene.append(bg) scene.append(bg)
# Title # Title
title = mcrfpy.Caption("Text Input Widget Demo", 20, 20) title = mcrfpy.Caption(pos=(20, 20), text="Text Input Widget Demo")
title.fill_color = (255, 255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255, 255)
scene.append(title) scene.append(title)
# Focus manager # Focus manager
@ -62,8 +62,8 @@ def create_demo():
inputs.append(comment_input) inputs.append(comment_input)
# Status display # Status display
status = mcrfpy.Caption("Ready for input...", 50, 360) status = mcrfpy.Caption(pos=(50, 360), text="Ready for input...")
status.fill_color = (150, 255, 150, 255) status.fill_color = mcrfpy.Color(150, 255, 150, 255)
scene.append(status) scene.append(status)
# Update handler # Update handler

View file

@ -69,11 +69,11 @@ def main():
mcrfpy.setScene("test") mcrfpy.setScene("test")
# Create multiple captions for testing # Create multiple captions for testing
caption1 = mcrfpy.Caption(50, 50, "Caption 1: Normal", fill_color=(255, 255, 255)) caption1 = mcrfpy.Caption(pos=(50, 50), text="Caption 1: Normal", fill_color=mcrfpy.Color(255, 255, 255))
caption2 = mcrfpy.Caption(50, 100, "Caption 2: Will be invisible", fill_color=(255, 200, 200)) caption2 = mcrfpy.Caption(pos=(50, 100), text="Caption 2: Will be invisible", fill_color=mcrfpy.Color(255, 200, 200))
caption3 = mcrfpy.Caption(50, 150, "Caption 3: 50% opacity", fill_color=(200, 255, 200)) caption3 = mcrfpy.Caption(pos=(50, 150), text="Caption 3: 50% opacity", fill_color=mcrfpy.Color(200, 255, 200))
caption4 = mcrfpy.Caption(50, 200, "Caption 4: 25% opacity", fill_color=(200, 200, 255)) caption4 = mcrfpy.Caption(pos=(50, 200), text="Caption 4: 25% opacity", fill_color=mcrfpy.Color(200, 200, 255))
caption5 = mcrfpy.Caption(50, 250, "Caption 5: 0% opacity", fill_color=(255, 255, 200)) caption5 = mcrfpy.Caption(pos=(50, 250), text="Caption 5: 0% opacity", fill_color=mcrfpy.Color(255, 255, 200))
# Add captions to scene # Add captions to scene
ui = mcrfpy.sceneUI("test") ui = mcrfpy.sceneUI("test")
@ -84,7 +84,7 @@ def main():
ui.append(caption5) ui.append(caption5)
# Also add a frame as background to see transparency better # Also add a frame as background to see transparency better
frame = mcrfpy.Frame(40, 40, 400, 250, fill_color=(50, 50, 50)) frame = mcrfpy.Frame(pos=(40, 40), size=(400, 250), fill_color=mcrfpy.Color(50, 50, 50))
frame.z_index = -1 # Put it behind the captions frame.z_index = -1 # Put it behind the captions
ui.append(frame) ui.append(frame)

View file

@ -18,6 +18,9 @@ mcrfpy.createScene("visibility_test")
grid = mcrfpy.Grid(grid_x=20, grid_y=15) grid = mcrfpy.Grid(grid_x=20, grid_y=15)
grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background
# Add a color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
# Initialize grid - all walkable and transparent # Initialize grid - all walkable and transparent
print("\nInitializing 20x15 grid...") print("\nInitializing 20x15 grid...")
for y in range(15): for y in range(15):
@ -25,7 +28,7 @@ for y in range(15):
cell = grid.at(x, y) cell = grid.at(x, y)
cell.walkable = True cell.walkable = True
cell.transparent = True cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120) # Floor color color_layer.set(x, y, mcrfpy.Color(100, 100, 120)) # Floor color
# Create some walls to block vision # Create some walls to block vision
print("Adding walls...") print("Adding walls...")
@ -47,14 +50,14 @@ for wall_group in walls:
cell = grid.at(x, y) cell = grid.at(x, y)
cell.walkable = False cell.walkable = False
cell.transparent = False cell.transparent = False
cell.color = mcrfpy.Color(40, 20, 20) # Wall color color_layer.set(x, y, mcrfpy.Color(40, 20, 20)) # Wall color
# Create entities # Create entities
print("\nCreating entities...") print("\nCreating entities...")
entities = [ entities = [
mcrfpy.Entity(2, 7), # Left side mcrfpy.Entity((2, 7)), # Left side
mcrfpy.Entity(18, 7), # Right side mcrfpy.Entity((18, 7)), # Right side
mcrfpy.Entity(10, 1), # Top center (above wall) mcrfpy.Entity((10, 1)), # Top center (above wall)
] ]
for i, entity in enumerate(entities): for i, entity in enumerate(entities):
@ -138,17 +141,17 @@ grid.position = (50, 50)
grid.size = (600, 450) # 20*30, 15*30 grid.size = (600, 450) # 20*30, 15*30
# Add title # Add title
title = mcrfpy.Caption("Knowledge Stubs 1 - Visibility Test", 200, 10) title = mcrfpy.Caption(pos=(200, 10), text="Knowledge Stubs 1 - Visibility Test")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Add info # Add info
info = mcrfpy.Caption("Perspective: -1 (omniscient)", 50, 520) info = mcrfpy.Caption(pos=(50, 520), text="Perspective: -1 (omniscient)")
info.fill_color = mcrfpy.Color(200, 200, 200) info.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info) ui.append(info)
# Add legend # Add legend
legend = mcrfpy.Caption("Black=Never seen, Dark gray=Discovered, Normal=Visible", 50, 540) legend = mcrfpy.Caption(pos=(50, 540), text="Black=Never seen, Dark gray=Discovered, Normal=Visible")
legend.fill_color = mcrfpy.Color(150, 150, 150) legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend) ui.append(legend)

View file

@ -4,28 +4,10 @@
import mcrfpy import mcrfpy
import sys import sys
# Colors as tuples (r, g, b, a) # Colors
WALL_COLOR = (60, 30, 30, 255) WALL_COLOR = mcrfpy.Color(60, 30, 30)
FLOOR_COLOR = (200, 200, 220, 255) FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
PATH_COLOR = (100, 255, 100, 255) PATH_COLOR = mcrfpy.Color(100, 255, 100)
def check_render(dt):
"""Timer callback to verify rendering"""
print(f"\nTimer fired after {dt}ms")
# Take screenshot
from mcrfpy import automation
automation.screenshot("visual_path_test.png")
print("Screenshot saved as visual_path_test.png")
# Sample some path cells to verify colors
print("\nSampling path cell colors from grid:")
for x, y in [(1, 1), (2, 2), (3, 3)]:
cell = grid.at(x, y)
color = cell.color
print(f" Cell ({x},{y}): color={color[:3]}")
sys.exit(0)
# Create scene # Create scene
mcrfpy.createScene("visual_test") mcrfpy.createScene("visual_test")
@ -34,20 +16,38 @@ mcrfpy.createScene("visual_test")
grid = mcrfpy.Grid(grid_x=5, grid_y=5) grid = mcrfpy.Grid(grid_x=5, grid_y=5)
grid.fill_color = mcrfpy.Color(0, 0, 0) grid.fill_color = mcrfpy.Color(0, 0, 0)
# Add color layer for cell coloring
color_layer = grid.add_layer("color", z_index=-1)
def check_render(dt):
"""Timer callback to verify rendering"""
print(f"\nTimer fired after {dt}ms")
# Take screenshot
from mcrfpy import automation
automation.screenshot("visual_path_test.png")
print("Screenshot saved as visual_path_test.png")
# Sample some path cells to verify colors
print("\nSampling path cell colors from grid:")
for x, y in [(1, 1), (2, 2), (3, 3)]:
color = color_layer.at(x, y)
print(f" Cell ({x},{y}): color=({color.r}, {color.g}, {color.b})")
sys.exit(0)
# Initialize all cells as floor # Initialize all cells as floor
print("Initializing grid...") print("Initializing grid...")
for y in range(5): for y in range(5):
for x in range(5): for x in range(5):
grid.at(x, y).walkable = True grid.at(x, y).walkable = True
grid.at(x, y).color = FLOOR_COLOR color_layer.set(x, y, FLOOR_COLOR)
# Create entities # Create entities
e1 = mcrfpy.Entity(0, 0) e1 = mcrfpy.Entity((0, 0), grid=grid)
e2 = mcrfpy.Entity(4, 4) e2 = mcrfpy.Entity((4, 4), grid=grid)
e1.sprite_index = 64 # @ e1.sprite_index = 64 # @
e2.sprite_index = 69 # E e2.sprite_index = 69 # E
grid.entities.append(e1)
grid.entities.append(e2)
print(f"Entity 1 at ({e1.x}, {e1.y})") print(f"Entity 1 at ({e1.x}, {e1.y})")
print(f"Entity 2 at ({e2.x}, {e2.y})") print(f"Entity 2 at ({e2.x}, {e2.y})")
@ -60,7 +60,7 @@ print(f"\nPath from E1 to E2: {path}")
if path: if path:
print("\nColoring path cells green...") print("\nColoring path cells green...")
for x, y in path: for x, y in path:
grid.at(x, y).color = PATH_COLOR color_layer.set(x, y, PATH_COLOR)
print(f" Set ({x},{y}) to green") print(f" Set ({x},{y}) to green")
# Set up UI # Set up UI
@ -70,7 +70,7 @@ grid.position = (50, 50)
grid.size = (250, 250) grid.size = (250, 250)
# Add title # Add title
title = mcrfpy.Caption("Path Visualization Test", 50, 10) title = mcrfpy.Caption(pos=(50, 10), text="Path Visualization Test")
title.fill_color = mcrfpy.Color(255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)

View file

@ -16,11 +16,11 @@ def test_issue_38_children():
print("\nTest 1: Passing children argument to Frame constructor") print("\nTest 1: Passing children argument to Frame constructor")
try: try:
# Create some child elements # Create some child elements
child1 = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Child 1") child1 = mcrfpy.Caption(pos=(10, 10), text="Child 1")
child2 = mcrfpy.Sprite(mcrfpy.Vector(10, 30)) child2 = mcrfpy.Sprite(pos=(10, 30))
# Try to create frame with children argument # Try to create frame with children argument
frame = mcrfpy.Frame(10, 10, 200, 150, children=[child1, child2]) frame = mcrfpy.Frame(pos=(10, 10), size=(200, 150), children=[child1, child2])
print("✗ UNEXPECTED: Frame accepted children argument (should fail per issue #38)") print("✗ UNEXPECTED: Frame accepted children argument (should fail per issue #38)")
except TypeError as e: except TypeError as e:
print(f"✓ EXPECTED: Frame constructor rejected children argument: {e}") print(f"✓ EXPECTED: Frame constructor rejected children argument: {e}")
@ -30,12 +30,12 @@ def test_issue_38_children():
# Test 2: Verify children can be added after creation # Test 2: Verify children can be added after creation
print("\nTest 2: Adding children after Frame creation") print("\nTest 2: Adding children after Frame creation")
try: try:
frame = mcrfpy.Frame(10, 10, 200, 150) frame = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
ui.append(frame) ui.append(frame)
# Add children via the children collection # Add children via the children collection
child1 = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Added Child 1") child1 = mcrfpy.Caption(pos=(10, 10), text="Added Child 1")
child2 = mcrfpy.Caption(mcrfpy.Vector(10, 30), text="Added Child 2") child2 = mcrfpy.Caption(pos=(10, 30), text="Added Child 2")
frame.children.append(child1) frame.children.append(child1)
frame.children.append(child2) frame.children.append(child2)
@ -65,33 +65,33 @@ def test_issue_42_click_callback():
return True return True
try: try:
frame1 = mcrfpy.Frame(10, 10, 200, 150) frame1 = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
ui.append(frame1) ui.append(frame1)
frame1.on_click = correct_callback frame1.on_click = correct_callback
print("✓ Click callback with correct signature assigned successfully") print("✓ Click callback with correct signature assigned successfully")
except Exception as e: except Exception as e:
print(f"✗ Failed to assign correct callback: {type(e).__name__}: {e}") print(f"✗ Failed to assign correct callback: {type(e).__name__}: {e}")
# Test 2: Callback with wrong signature (no args) # Test 2: Callback with wrong signature (no args)
print("\nTest 2: Click callback with no arguments") print("\nTest 2: Click callback with no arguments")
def wrong_callback_no_args(): def wrong_callback_no_args():
print(" Wrong callback called") print(" Wrong callback called")
try: try:
frame2 = mcrfpy.Frame(220, 10, 200, 150) frame2 = mcrfpy.Frame(pos=(220, 10), size=(200, 150))
ui.append(frame2) ui.append(frame2)
frame2.on_click = wrong_callback_no_args frame2.on_click = wrong_callback_no_args
print("✓ Click callback with no args assigned (will fail at runtime per issue #42)") print("✓ Click callback with no args assigned (will fail at runtime per issue #42)")
except Exception as e: except Exception as e:
print(f"✗ Failed to assign callback: {type(e).__name__}: {e}") print(f"✗ Failed to assign callback: {type(e).__name__}: {e}")
# Test 3: Callback with wrong signature (too few args) # Test 3: Callback with wrong signature (too few args)
print("\nTest 3: Click callback with too few arguments") print("\nTest 3: Click callback with too few arguments")
def wrong_callback_few_args(x, y): def wrong_callback_few_args(x, y):
print(f" Wrong callback called: x={x}, y={y}") print(f" Wrong callback called: x={x}, y={y}")
try: try:
frame3 = mcrfpy.Frame(10, 170, 200, 150) frame3 = mcrfpy.Frame(pos=(10, 170), size=(200, 150))
ui.append(frame3) ui.append(frame3)
frame3.on_click = wrong_callback_few_args frame3.on_click = wrong_callback_few_args
print("✓ Click callback with 2 args assigned (will fail at runtime per issue #42)") print("✓ Click callback with 2 args assigned (will fail at runtime per issue #42)")

View file

@ -7,24 +7,24 @@ import sys
def test_grid_none_texture(runtime): def test_grid_none_texture(runtime):
"""Test Grid functionality without texture""" """Test Grid functionality without texture"""
print("\n=== Testing Grid with None texture ===") print("\n=== Testing Grid with None texture ===")
# Test 1: Create Grid with None texture # Test 1: Create Grid with None texture
try: try:
grid = mcrfpy.Grid(10, 10, None, mcrfpy.Vector(50, 50), mcrfpy.Vector(400, 400)) grid = mcrfpy.Grid(grid_size=(10, 10), pos=(50, 50), size=(400, 400))
print("✓ Grid created successfully with None texture") print("✓ Grid created successfully with None texture")
except Exception as e: except Exception as e:
print(f"✗ Failed to create Grid with None texture: {e}") print(f"✗ Failed to create Grid with None texture: {e}")
sys.exit(1) sys.exit(1)
# Add to UI # Add to UI
ui = mcrfpy.sceneUI("grid_none_test") ui = mcrfpy.sceneUI("grid_none_test")
ui.append(grid) ui.append(grid)
# Test 2: Verify grid properties # Test 2: Verify grid properties
try: try:
grid_size = grid.grid_size grid_size = grid.grid_size
print(f"✓ Grid size: {grid_size}") print(f"✓ Grid size: {grid_size}")
# Check texture property # Check texture property
texture = grid.texture texture = grid.texture
if texture is None: if texture is None:
@ -33,39 +33,41 @@ def test_grid_none_texture(runtime):
print(f"✗ Grid texture should be None, got: {texture}") print(f"✗ Grid texture should be None, got: {texture}")
except Exception as e: except Exception as e:
print(f"✗ Property access failed: {e}") print(f"✗ Property access failed: {e}")
# Test 3: Access grid points and set colors # Test 3: Access grid points using ColorLayer (new API)
# Note: GridPoint no longer has .color - must use ColorLayer system
try: try:
# Add a color layer to the grid
color_layer = grid.add_layer("color", z_index=-1)
# Create a checkerboard pattern with colors # Create a checkerboard pattern with colors
for x in range(10): for x in range(10):
for y in range(10): for y in range(10):
point = grid.at(x, y)
if (x + y) % 2 == 0: if (x + y) % 2 == 0:
point.color = mcrfpy.Color(255, 0, 0, 255) # Red color_layer.set(x, y, mcrfpy.Color(255, 0, 0, 255)) # Red
else: else:
point.color = mcrfpy.Color(0, 0, 255, 255) # Blue color_layer.set(x, y, mcrfpy.Color(0, 0, 255, 255)) # Blue
print("✓ Successfully set grid point colors") print("✓ Successfully set grid colors via ColorLayer")
except Exception as e: except Exception as e:
print(f"✗ Failed to set grid colors: {e}") print(f"✗ Failed to set grid colors: {e}")
# Test 4: Add entities to the grid # Test 4: Add entities to the grid
try: try:
# Create an entity with its own texture # Create an entity with its own texture
entity_texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) entity_texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
entity = mcrfpy.Entity(mcrfpy.Vector(5, 5), entity_texture, 1, grid) entity = mcrfpy.Entity((5, 5), texture=entity_texture, sprite_index=1, grid=grid)
grid.entities.append(entity) grid.entities.append(entity)
print(f"✓ Added entity to grid, total entities: {len(grid.entities)}") print(f"✓ Added entity to grid, total entities: {len(grid.entities)}")
except Exception as e: except Exception as e:
print(f"✗ Failed to add entity: {e}") print(f"✗ Failed to add entity: {e}")
# Test 5: Test grid interaction properties # Test 5: Test grid interaction properties
try: try:
# Test zoom # Test zoom
grid.zoom = 2.0 grid.zoom = 2.0
print(f"✓ Set zoom to: {grid.zoom}") print(f"✓ Set zoom to: {grid.zoom}")
# Test center # Test center (uses pixel coordinates)
grid.center = mcrfpy.Vector(5, 5) grid.center = (200, 200)
print(f"✓ Set center to: {grid.center}") print(f"✓ Set center to: {grid.center}")
except Exception as e: except Exception as e:
print(f"✗ Grid properties failed: {e}") print(f"✗ Grid properties failed: {e}")
@ -86,7 +88,7 @@ mcrfpy.setScene("grid_none_test")
# Add a background frame so we can see the grid # Add a background frame so we can see the grid
ui = mcrfpy.sceneUI("grid_none_test") ui = mcrfpy.sceneUI("grid_none_test")
background = mcrfpy.Frame(0, 0, 800, 600, background = mcrfpy.Frame(pos=(0, 0), size=(800, 600),
fill_color=mcrfpy.Color(200, 200, 200), fill_color=mcrfpy.Color(200, 200, 200),
outline_color=mcrfpy.Color(0, 0, 0), outline_color=mcrfpy.Color(0, 0, 0),
outline=2.0) outline=2.0)

View file

@ -11,8 +11,8 @@ def test_UICollection():
ui = mcrfpy.sceneUI("collection_test") ui = mcrfpy.sceneUI("collection_test")
# Add various UI elements # Add various UI elements
frame = mcrfpy.Frame(10, 10, 100, 100) frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
caption = mcrfpy.Caption(mcrfpy.Vector(120, 10), text="Test") caption = mcrfpy.Caption(pos=(120, 10), text="Test")
# Skip sprite for now since it requires a texture # Skip sprite for now since it requires a texture
ui.append(frame) ui.append(frame)
@ -74,9 +74,9 @@ def test_UICollection():
# Test type preservation (Issue #76) # Test type preservation (Issue #76)
try: try:
# Add a frame with children to test nested collections # Add a frame with children to test nested collections
parent_frame = mcrfpy.Frame(250, 10, 200, 200, parent_frame = mcrfpy.Frame(pos=(250, 10), size=(200, 200),
fill_color=mcrfpy.Color(200, 200, 200)) fill_color=mcrfpy.Color(200, 200, 200))
child_caption = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Child") child_caption = mcrfpy.Caption(pos=(10, 10), text="Child")
parent_frame.children.append(child_caption) parent_frame.children.append(child_caption)
ui.append(parent_frame) ui.append(parent_frame)

View file

@ -18,50 +18,50 @@ def test_screenshot_validation():
print("Creating UI elements...") print("Creating UI elements...")
# Bright red frame with white outline # Bright red frame with white outline
frame1 = mcrfpy.Frame(50, 50, 300, 200, frame1 = mcrfpy.Frame(pos=(50, 50), size=(300, 200),
fill_color=mcrfpy.Color(255, 0, 0), # Bright red fill_color=mcrfpy.Color(255, 0, 0), # Bright red
outline_color=mcrfpy.Color(255, 255, 255), # White outline_color=mcrfpy.Color(255, 255, 255), # White
outline=5.0) outline=5.0)
ui.append(frame1) ui.append(frame1)
print("Added red frame at (50, 50)") print("Added red frame at (50, 50)")
# Bright green frame # Bright green frame
frame2 = mcrfpy.Frame(400, 50, 300, 200, frame2 = mcrfpy.Frame(pos=(400, 50), size=(300, 200),
fill_color=mcrfpy.Color(0, 255, 0), # Bright green fill_color=mcrfpy.Color(0, 255, 0), # Bright green
outline_color=mcrfpy.Color(0, 0, 0), # Black outline_color=mcrfpy.Color(0, 0, 0), # Black
outline=3.0) outline=3.0)
ui.append(frame2) ui.append(frame2)
print("Added green frame at (400, 50)") print("Added green frame at (400, 50)")
# Blue frame # Blue frame
frame3 = mcrfpy.Frame(50, 300, 300, 200, frame3 = mcrfpy.Frame(pos=(50, 300), size=(300, 200),
fill_color=mcrfpy.Color(0, 0, 255), # Bright blue fill_color=mcrfpy.Color(0, 0, 255), # Bright blue
outline_color=mcrfpy.Color(255, 255, 0), # Yellow outline_color=mcrfpy.Color(255, 255, 0), # Yellow
outline=4.0) outline=4.0)
ui.append(frame3) ui.append(frame3)
print("Added blue frame at (50, 300)") print("Added blue frame at (50, 300)")
# Add text captions # Add text captions
caption1 = mcrfpy.Caption(mcrfpy.Vector(60, 60), caption1 = mcrfpy.Caption(pos=(60, 60),
text="RED FRAME TEST", text="RED FRAME TEST",
fill_color=mcrfpy.Color(255, 255, 255)) fill_color=mcrfpy.Color(255, 255, 255))
caption1.size = 24 caption1.font_size = 24
frame1.children.append(caption1) frame1.children.append(caption1)
caption2 = mcrfpy.Caption(mcrfpy.Vector(410, 60), caption2 = mcrfpy.Caption(pos=(410, 60),
text="GREEN FRAME TEST", text="GREEN FRAME TEST",
fill_color=mcrfpy.Color(0, 0, 0)) fill_color=mcrfpy.Color(0, 0, 0))
caption2.size = 24 caption2.font_size = 24
ui.append(caption2) ui.append(caption2)
caption3 = mcrfpy.Caption(mcrfpy.Vector(60, 310), caption3 = mcrfpy.Caption(pos=(60, 310),
text="BLUE FRAME TEST", text="BLUE FRAME TEST",
fill_color=mcrfpy.Color(255, 255, 0)) fill_color=mcrfpy.Color(255, 255, 0))
caption3.size = 24 caption3.font_size = 24
ui.append(caption3) ui.append(caption3)
# White background frame to ensure non-transparent background # White background frame to ensure non-transparent background
background = mcrfpy.Frame(0, 0, 1024, 768, background = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
fill_color=mcrfpy.Color(200, 200, 200)) # Light gray fill_color=mcrfpy.Color(200, 200, 200)) # Light gray
# Insert at beginning so it's behind everything # Insert at beginning so it's behind everything
ui.remove(len(ui) - 1) # Remove to re-add at start ui.remove(len(ui) - 1) # Remove to re-add at start

View file

@ -11,16 +11,16 @@ mcrfpy.setScene("timer_works")
ui = mcrfpy.sceneUI("timer_works") ui = mcrfpy.sceneUI("timer_works")
# Add visible content # Add visible content
frame = mcrfpy.Frame(100, 100, 300, 200, frame = mcrfpy.Frame(pos=(100, 100), size=(300, 200),
fill_color=mcrfpy.Color(255, 0, 0), fill_color=mcrfpy.Color(255, 0, 0),
outline_color=mcrfpy.Color(255, 255, 255), outline_color=mcrfpy.Color(255, 255, 255),
outline=3.0) outline=3.0)
ui.append(frame) ui.append(frame)
caption = mcrfpy.Caption(mcrfpy.Vector(150, 150), caption = mcrfpy.Caption(pos=(150, 150),
text="TIMER TEST SUCCESS", text="TIMER TEST SUCCESS",
fill_color=mcrfpy.Color(255, 255, 255)) fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 24 caption.font_size = 24
ui.append(caption) ui.append(caption)
# Timer callback with correct signature # Timer callback with correct signature

View file

@ -10,6 +10,7 @@ import inspect
import datetime import datetime
import html import html
import re import re
import types
from pathlib import Path from pathlib import Path
def transform_doc_links(docstring, format='html', base_url=''): def transform_doc_links(docstring, format='html', base_url=''):
@ -214,11 +215,21 @@ def get_all_classes():
"parsed": parse_docstring(method_doc) "parsed": parse_docstring(method_doc)
} }
elif isinstance(attr, property): elif isinstance(attr, property):
# Pure Python property
prop_doc = (attr.fget.__doc__ if attr.fget else "") or "" prop_doc = (attr.fget.__doc__ if attr.fget else "") or ""
class_info["properties"][attr_name] = { class_info["properties"][attr_name] = {
"doc": prop_doc, "doc": prop_doc,
"readonly": attr.fset is None "readonly": attr.fset is None
} }
elif isinstance(attr, (types.GetSetDescriptorType, types.MemberDescriptorType)):
# C++ extension property (PyGetSetDef or PyMemberDef)
prop_doc = attr.__doc__ or ""
# Check if docstring indicates read-only (convention: "read-only" in description)
readonly = "read-only" in prop_doc.lower()
class_info["properties"][attr_name] = {
"doc": prop_doc,
"readonly": readonly
}
except: except:
pass pass