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!
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).

View file

@ -1,6 +1,6 @@
# 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.*
@ -289,6 +289,13 @@ Note:
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:**
#### `complete() -> None`
@ -376,6 +383,28 @@ Attributes:
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:**
#### `get_bounds() -> tuple`
@ -447,6 +476,29 @@ Attributes:
name (str): Element name
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:**
#### `get_bounds() -> tuple`
@ -511,6 +563,27 @@ Attributes:
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:**
#### `get_bounds() -> tuple`
@ -545,6 +618,12 @@ Note:
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:**
#### `from_hex(hex_string: str) -> Color`
@ -600,6 +679,11 @@ Methods:
set(x, y, color): Set color at cell position
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:**
#### `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
**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:**
#### `get_bounds() -> tuple`
@ -703,6 +793,19 @@ Attributes:
opacity (float): Opacity value
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:**
#### `at(...)`
@ -810,6 +913,12 @@ Remove first occurrence of entity. Raises ValueError if not found.
*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:**
#### `as_integer_ratio(...)`
@ -887,6 +996,10 @@ Return an array of bytes representing an integer.
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:**
### Frame
@ -933,6 +1046,32 @@ Attributes:
clip_children (bool): Whether to clip children to frame bounds
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:**
#### `get_bounds() -> tuple`
@ -1015,6 +1154,48 @@ Attributes:
z_index (int): Rendering order
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:**
#### `add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer`
@ -1165,12 +1346,22 @@ Note:
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:**
### GridPointState
UIGridPointState object
**Properties:**
- `discovered`: Has the GridPointState been discovered
- `point`: GridPoint at this position (None if not discovered)
- `visible`: Is the GridPointState visible
**Methods:**
### Line
@ -1205,6 +1396,26 @@ Attributes:
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:**
#### `get_bounds() -> tuple`
@ -1237,7 +1448,56 @@ Note:
### 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:**
@ -1299,6 +1559,30 @@ Attributes:
name (str): Element name
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:**
#### `get_bounds() -> tuple`
@ -1333,6 +1617,14 @@ Note:
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:**
### TileLayer
@ -1357,6 +1649,12 @@ Methods:
set(x, y, index): Set tile index at cell position
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:**
#### `at(x, y) -> int`
@ -1412,6 +1710,15 @@ Example:
timer.resume() # Resume timer
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:**
#### `cancel() -> None`
@ -1508,6 +1815,11 @@ Iterator for a collection of UI objects
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:**
#### `angle() -> float`
@ -1574,6 +1886,16 @@ Note:
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:**
#### `center() -> None`

View file

@ -108,7 +108,7 @@
<body>
<div class="container">
<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>
<div class="toc">
@ -410,6 +410,14 @@ Note:</p>
<div class="method-section">
<h3 id="Animation"><span class="class-name">Animation</span></h3>
<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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -495,6 +503,29 @@ Attributes:
z_index (int): Rendering order
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>: 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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -567,6 +598,30 @@ Attributes:
z_index (int): Rendering order
name (str): Element name
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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -632,6 +687,28 @@ Attributes:
z_index (int): Rendering order
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>: 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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -668,6 +745,13 @@ Note:</p>
<div class="method-section">
<h3 id="Color"><span class="class-name">Color</span></h3>
<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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -722,6 +806,12 @@ Methods:
at(x, y): Get color at cell position
set(x, y, color): Set color at cell position
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>
<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">
<h3 id="Drawable"><span class="class-name">Drawable</span></h3>
<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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -835,6 +932,20 @@ Attributes:
visible (bool): Visibility state
opacity (float): Opacity value
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>
<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">
<h3 id="FOV"><span class="class-name">FOV</span></h3>
<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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -1040,6 +1158,11 @@ Also known as the population count.
<div class="method-section">
<h3 id="Font"><span class="class-name">Font</span></h3>
<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>
</div>
@ -1085,6 +1208,33 @@ Attributes:
name (str): Element name
clip_children (bool): Whether to clip children to frame bounds
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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -1168,6 +1318,49 @@ Attributes:
opacity (float): Opacity value
z_index (int): Rendering order
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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -1325,12 +1518,24 @@ Note:</p>
<div class="method-section">
<h3 id="GridPoint"><span class="class-name">GridPoint</span></h3>
<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>
</div>
<div class="method-section">
<h3 id="GridPointState"><span class="class-name">GridPointState</span></h3>
<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>
</div>
@ -1364,6 +1569,27 @@ Attributes:
z_index (int): Rendering order
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'>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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -1399,7 +1625,57 @@ Note:</p>
<div class="method-section">
<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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -1459,6 +1735,31 @@ Attributes:
z_index (int): Rendering order
name (str): Element name
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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -1495,6 +1796,15 @@ Note:</p>
<div class="method-section">
<h3 id="Texture"><span class="class-name">Texture</span></h3>
<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>
</div>
@ -1519,6 +1829,13 @@ Methods:
at(x, y): Get tile index at cell position
set(x, y, index): Set tile index at cell position
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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -1578,6 +1895,16 @@ Example:
timer.pause() # Pause timer
timer.resume() # Resume timer
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>
<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">
<h3 id="Vector"><span class="class-name">Vector</span></h3>
<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>
<div style="margin-left: 20px; margin-bottom: 15px;">
@ -1747,6 +2080,17 @@ Note:</p>
<div class="method-section">
<h3 id="Window"><span class="class-name">Window</span></h3>
<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>
<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_repr = (reprfunc)PySceneClass::__repr__,
.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_getset = nullptr, // Set in McRFPy_API.cpp
.tp_init = (initproc)PySceneClass::__init__,

View file

@ -6,8 +6,9 @@
#include "Profiler.h"
#include "PyFOV.h"
#include <algorithm>
#include <cmath> // #142 - for std::floor
#include <cmath> // #142 - for std::floor, std::isnan
#include <cstring> // #150 - for strcmp
#include <limits> // #169 - for std::numeric_limits
// UIDrawable methods now in UIBase.h
UIGrid::UIGrid()
@ -735,7 +736,9 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
PyObject* fill_color = nullptr;
PyObject* click_handler = nullptr;
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;
// perspective is now handled via properties, not init args
int visible = 1;
@ -862,9 +865,19 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
sf::Vector2f(x, y), sf::Vector2f(w, h));
// 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_y = center_y;
self->data->zoom = zoom;
// perspective is now handled by perspective_entity and perspective_enabled
// self->data->perspective = perspective;
self->data->visible = visible;
@ -1730,6 +1743,72 @@ PyObject* UIGrid::py_entities_in_radius(PyUIGridObject* self, PyObject* args, Py
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[] = {
{"at", (PyCFunction)UIGrid::py_at, 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"
"Returns:\n"
" 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}
};
@ -1929,6 +2017,15 @@ PyMethodDef UIGrid_all_methods[] = {
" radius: Search radius\n\n"
"Returns:\n"
" 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
};

View file

@ -170,6 +170,12 @@ public:
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_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 PyGetSetDef getsetters[];
static PyObject* get_entities(PyUIGridObject* self, void* closure);

View file

@ -34,12 +34,15 @@ grid = mcrfpy.Grid(
size=(1024, 768)
)
# Add color layer for floor pattern
color_layer = grid.add_layer("color", z_index=-1)
# Simple floor pattern
for x in range(100):
for y in range(100):
cell = grid.at((x, y))
cell = grid.at(x, y)
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
entities = []
@ -47,15 +50,15 @@ ENTITY_COUNT = 50
for i in range(ENTITY_COUNT):
entity = mcrfpy.Entity(
grid_pos=(random.randint(0, 99), random.randint(0, 99)),
sprite_index=random.randint(10, 20) # Use varied sprites
(random.randint(0, 99), random.randint(0, 99)),
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_y = random.uniform(-0.5, 0.5)
grid.entities.append(entity)
entities.append(entity)
ui.append(grid)

View file

@ -282,23 +282,23 @@ def setup_grid_stress():
grid.center = (400, 400) # Center view
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 x in range(50):
cell = grid.at(x, y)
if (x + y) % 2 == 0:
cell.color = mcrfpy.Color(60, 60, 80)
color_layer.set(x, y, mcrfpy.Color(60, 60, 80))
else:
cell.color = mcrfpy.Color(40, 40, 60)
color_layer.set(x, y, mcrfpy.Color(40, 40, 60))
# Add 50 entities
try:
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
for i in range(50):
# Entity takes positional args: (position, texture, sprite_index, grid)
pos = mcrfpy.Vector(random.randint(5, 45), random.randint(5, 45))
entity = mcrfpy.Entity(pos, texture, random.randint(0, 100), grid)
# Entity takes tuple position and keyword args
pos = (random.randint(5, 45), random.randint(5, 45))
entity = mcrfpy.Entity(pos, texture=texture, sprite_index=random.randint(0, 100), grid=grid)
grid.entities.append(entity)
except Exception as 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.
Compares rendering performance between:
1. Traditional grid.at(x,y).color API (no caching)
2. New layer system with dirty flag caching
1. ColorLayer with per-cell modifications (no caching benefit)
2. ColorLayer with dirty flag caching (static after fill)
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:
./mcrogueface --exec tests/benchmarks/layer_performance_test.py
# Results in benchmark_*.json files
@ -94,7 +98,7 @@ def run_next_test():
# ============================================================================
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")
ui = mcrfpy.sceneUI("test_base_static")
@ -102,17 +106,17 @@ def setup_base_layer_static():
pos=(10, 10), size=(600, 600))
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 x in range(GRID_SIZE):
cell = grid.at(x, y)
cell.color = mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255)
layer.set(x, y, mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255))
mcrfpy.setScene("test_base_static")
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")
ui = mcrfpy.sceneUI("test_base_mod")
@ -120,19 +124,16 @@ def setup_base_layer_modified():
pos=(10, 10), size=(600, 600))
ui.append(grid)
# Fill base layer
for y in range(GRID_SIZE):
for x in range(GRID_SIZE):
cell = grid.at(x, y)
cell.color = mcrfpy.Color(100, 100, 100, 255)
# Fill using ColorLayer
layer = grid.add_layer("color", z_index=-1)
layer.fill(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]
def modify_cell(runtime):
x = mod_counter[0] % GRID_SIZE
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
cell = grid.at(x, y)
cell.color = mcrfpy.Color(255, 0, 0, 255)
layer.set(x, y, mcrfpy.Color(255, 0, 0, 255))
mod_counter[0] += 1
mcrfpy.setScene("test_base_mod")

View file

@ -19,13 +19,14 @@ END_COLOR = mcrfpy.Color(255, 255, 100) # Yellow for end
# Global state
grid = None
color_layer = None
mode = "ASTAR"
start_pos = (5, 10)
end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall
def create_map():
"""Create a map with obstacles to show pathfinding differences"""
global grid
global grid, color_layer
mcrfpy.createScene("pathfinding_comparison")
@ -33,11 +34,14 @@ def create_map():
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
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
for y in range(20):
for x in range(30):
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
obstacles = [
@ -54,11 +58,11 @@ def create_map():
for obstacle_group in obstacles:
for x, y in obstacle_group:
grid.at(x, y).walkable = False
grid.at(x, y).color = WALL_COLOR
color_layer.set(x, y, WALL_COLOR)
# Mark start and end
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
color_layer.set(start_pos[0], start_pos[1], START_COLOR)
color_layer.set(end_pos[0], end_pos[1], END_COLOR)
def clear_paths():
"""Clear path highlighting"""
@ -66,11 +70,11 @@ def clear_paths():
for x in range(30):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
# Restore start and end colors
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
color_layer.set(start_pos[0], start_pos[1], START_COLOR)
color_layer.set(end_pos[0], end_pos[1], END_COLOR)
def show_astar():
"""Show A* path"""
@ -82,7 +86,7 @@ def show_astar():
# Color the path
for i, (x, y) in enumerate(path):
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.fill_color = ASTAR_COLOR
@ -103,7 +107,7 @@ def show_dijkstra():
if dist is not None and dist < max_dist:
# Color based on distance
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
path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
@ -111,11 +115,11 @@ def show_dijkstra():
# Highlight the actual path more brightly
for x, y in path:
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
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
color_layer.set(start_pos[0], start_pos[1], START_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.fill_color = DIJKSTRA_COLOR
@ -134,12 +138,12 @@ def show_both():
# Color Dijkstra path first (blue)
for x, y in dijkstra_path:
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
for x, y in astar_path:
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
different_cells = []
@ -202,26 +206,26 @@ grid.size = (600, 400) # 30*20, 20*20
grid.position = (100, 100)
# 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)
ui.append(title)
# 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)
ui.append(status_text)
# 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)
ui.append(info_text)
# 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)
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)
ui.append(legend2)

View file

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

View file

@ -20,6 +20,7 @@ NO_PATH_COLOR = mcrfpy.Color(255, 0, 0) # Pure red for unreachable
# Global state
grid = None
color_layer = None
entities = []
current_combo_index = 0
all_combinations = [] # All possible pairs
@ -27,7 +28,7 @@ current_path = []
def create_map():
"""Create the map with entities"""
global grid, entities, all_combinations
global grid, color_layer, entities, all_combinations
mcrfpy.createScene("dijkstra_all")
@ -35,6 +36,9 @@ def create_map():
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
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 = [
"..............", # Row 0
@ -57,10 +61,10 @@ def create_map():
if char == 'W':
cell.walkable = False
cell.color = WALL_COLOR
color_layer.set(x, y, WALL_COLOR)
else:
cell.walkable = True
cell.color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
if char == 'E':
entity_positions.append((x, y))
@ -68,9 +72,8 @@ def create_map():
# Create entities
entities = []
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'
grid.entities.append(entity)
entities.append(entity)
print("Map Analysis:")
@ -95,7 +98,7 @@ def clear_path_colors():
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
current_path = []
@ -118,15 +121,15 @@ def show_combination(index):
current_path = path if path else []
# Always color start and end positions
grid.at(int(e_from.x), int(e_from.y)).color = 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_from.x), int(e_from.y), START_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
if path:
# Color intermediate steps
for i, (x, y) in enumerate(path):
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.fill_color = mcrfpy.Color(100, 255, 100) # Green for valid
@ -183,37 +186,37 @@ grid.size = (560, 400)
grid.position = (120, 100)
# 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)
ui.append(title)
# 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)
ui.append(status_text)
# 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)
ui.append(info_text)
# 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)
ui.append(path_text)
# 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)
ui.append(controls)
# 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)
ui.append(legend)
# 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)
ui.append(expected)

View file

@ -18,6 +18,7 @@ END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
# Global state
grid = None
color_layer = None
entities = []
current_path_index = 0
path_combinations = []
@ -25,7 +26,7 @@ current_path = []
def create_map():
"""Create the map with entities"""
global grid, entities
global grid, color_layer, entities
mcrfpy.createScene("dijkstra_cycle")
@ -33,6 +34,9 @@ def create_map():
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
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 = [
"..............", # Row 0
@ -55,10 +59,10 @@ def create_map():
if char == 'W':
cell.walkable = False
cell.color = WALL_COLOR
color_layer.set(x, y, WALL_COLOR)
else:
cell.walkable = True
cell.color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
if char == 'E':
entity_positions.append((x, y))
@ -66,9 +70,8 @@ def create_map():
# Create entities
entities = []
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'
grid.entities.append(entity)
entities.append(entity)
print("Entities created:")
@ -118,7 +121,7 @@ def clear_path_colors():
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
current_path = []
@ -144,13 +147,13 @@ def show_path(index):
current_path = path
if path:
# Color start and end
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR
grid.at(int(e_to.x), int(e_to.y)).color = END_COLOR
color_layer.set(int(e_from.x), int(e_from.y), START_COLOR)
color_layer.set(int(e_to.x), int(e_to.y), END_COLOR)
# Color intermediate steps
for i, (x, y) in enumerate(path):
if i > 0 and i < len(path) - 1:
grid.at(x, y).color = PATH_COLOR
color_layer.set(x, y, PATH_COLOR)
# 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)"
@ -194,27 +197,27 @@ grid.size = (560, 400)
grid.position = (120, 100)
# 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)
ui.append(title)
# 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)
ui.append(status_text)
# 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)
ui.append(path_text)
# 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)
ui.append(controls)
# 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)
ui.append(legend)

View file

@ -18,13 +18,14 @@ ENTITY_COLORS = [
# Global state
grid = None
color_layer = None
entities = []
first_point = None
second_point = None
def create_simple_map():
"""Create a simple test map"""
global grid, entities
global grid, color_layer, entities
mcrfpy.createScene("dijkstra_debug")
@ -32,6 +33,9 @@ def create_simple_map():
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
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...")
# Initialize all as floor
@ -39,7 +43,7 @@ def create_simple_map():
for x in range(10):
grid.at(x, y).walkable = 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
print("Adding walls at:")
@ -47,7 +51,7 @@ def create_simple_map():
for x, y in walls:
print(f" Wall at ({x}, {y})")
grid.at(x, y).walkable = False
grid.at(x, y).color = WALL_COLOR
color_layer.set(x, y, WALL_COLOR)
# Create 3 entities
entity_positions = [(2, 5), (8, 5), (5, 8)]
@ -56,9 +60,8 @@ def create_simple_map():
print("\nCreating entities at:")
for i, (x, y) in enumerate(entity_positions):
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'
grid.entities.append(entity)
entities.append(entity)
return grid
@ -88,11 +91,13 @@ def test_path_highlighting():
print(f" Step {i}: ({x}, {y})")
# Get current color for debugging
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
cell.color = PATH_COLOR
new_color = (cell.color.r, cell.color.g, cell.color.b)
color_layer.set(x, y, PATH_COLOR)
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" Walkable: {cell.walkable}")
@ -111,8 +116,8 @@ def test_path_highlighting():
# Verify colors were set
print("\nVerifying cell colors after highlighting:")
for x, y in path[:3]: # Check first 3 cells
cell = grid.at(x, y)
color = (cell.color.r, cell.color.g, cell.color.b)
c = color_layer.at(x, y)
color = (c.r, c.g, c.b)
expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b)
match = color == expected
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
# 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)
ui.append(title)
# 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)
ui.append(info)

View file

@ -29,13 +29,14 @@ ENTITY_COLORS = [
# Global state
grid = None
color_layer = None
entities = []
first_point = None
second_point = None
def create_map():
"""Create the interactive map with the layout specified by the user"""
global grid, entities
global grid, color_layer, entities
mcrfpy.createScene("dijkstra_interactive")
@ -43,6 +44,9 @@ def create_map():
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
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
# . = floor, W = wall, E = entity position
map_layout = [
@ -68,12 +72,12 @@ def create_map():
# Wall
cell.walkable = False
cell.transparent = False
cell.color = WALL_COLOR
color_layer.set(x, y, WALL_COLOR)
else:
# Floor
cell.walkable = True
cell.transparent = True
cell.color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
if char == 'E':
# Entity position
@ -82,9 +86,8 @@ def create_map():
# Create entities at marked positions
entities = []
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'
grid.entities.append(entity)
entities.append(entity)
return grid
@ -96,7 +99,7 @@ def clear_path_highlight():
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
def highlight_path():
"""Highlight the path between selected entities"""
@ -121,11 +124,11 @@ def highlight_path():
for x, y in path:
cell = grid.at(x, y)
if cell.walkable:
cell.color = PATH_COLOR
color_layer.set(x, y, PATH_COLOR)
# Also highlight start and end with entity colors
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point]
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point]
color_layer.set(int(entity1.x), int(entity1.y), ENTITY_COLORS[first_point])
color_layer.set(int(entity2.x), int(entity2.y), ENTITY_COLORS[second_point])
# Update info
distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y))
@ -199,34 +202,33 @@ grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 60)
# 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)
ui.append(title)
# 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)
ui.append(status_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)
ui.append(info_text)
# 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)
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)
ui.append(legend2)
# Mark entity positions with colored indicators
for i, entity in enumerate(entities):
marker = mcrfpy.Caption(str(i+1),
120 + int(entity.x) * 40 + 15,
60 + int(entity.y) * 40 + 10)
marker = mcrfpy.Caption(pos=(120 + int(entity.x) * 40 + 15, 60 + int(entity.y) * 40 + 10),
text=str(i+1))
marker.fill_color = ENTITY_COLORS[i]
marker.outline = 1
marker.outline_color = mcrfpy.Color(0, 0, 0)

View file

@ -32,6 +32,7 @@ ENTITY_COLORS = [
# Global state
grid = None
color_layer = None
entities = []
first_point = None
second_point = None
@ -43,7 +44,7 @@ original_positions = [] # Store original entity positions
def create_map():
"""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")
@ -51,6 +52,9 @@ def create_map():
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
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
# . = floor, W = wall, E = entity position
map_layout = [
@ -76,12 +80,12 @@ def create_map():
# Wall
cell.walkable = False
cell.transparent = False
cell.color = WALL_COLOR
color_layer.set(x, y, WALL_COLOR)
else:
# Floor
cell.walkable = True
cell.transparent = True
cell.color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
if char == 'E':
# Entity position
@ -91,9 +95,8 @@ def create_map():
entities = []
original_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'
grid.entities.append(entity)
entities.append(entity)
original_positions.append((x, y))
@ -108,7 +111,7 @@ def clear_path_highlight():
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
current_path = []
@ -138,13 +141,13 @@ def highlight_path():
if cell.walkable:
# Use gradient for path visualization
if i < len(path) - 1:
cell.color = PATH_COLOR
color_layer.set(x, y, PATH_COLOR)
else:
cell.color = VISITED_COLOR
color_layer.set(x, y, VISITED_COLOR)
# Highlight start and end with entity colors
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point]
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point]
color_layer.set(int(entity1.x), int(entity1.y), ENTITY_COLORS[first_point])
color_layer.set(int(entity2.x), int(entity2.y), ENTITY_COLORS[second_point])
# Update info
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps"
@ -291,39 +294,38 @@ grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 60)
# 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)
ui.append(title)
# 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)
ui.append(status_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)
ui.append(info_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)
ui.append(control_text)
# 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)
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)
ui.append(legend2)
# Mark entity positions with colored indicators
for i, entity in enumerate(entities):
marker = mcrfpy.Caption(str(i+1),
120 + int(entity.x) * 40 + 15,
60 + int(entity.y) * 40 + 10)
marker = mcrfpy.Caption(pos=(120 + int(entity.x) * 40 + 15, 60 + int(entity.y) * 40 + 10),
text=str(i+1))
marker.fill_color = ENTITY_COLORS[i]
marker.outline = 1
marker.outline_color = mcrfpy.Color(0, 0, 0)

View file

@ -128,12 +128,12 @@ grid.position = (50, 50)
grid.size = (500, 300)
# 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)
ui.append(title)
# 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)
ui.append(legend)

View file

@ -19,13 +19,16 @@ mcrfpy.createScene("visibility_demo")
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
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
for y in range(20):
for x in range(30):
cell = grid.at(x, y)
cell.walkable = 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
walls = [
@ -57,12 +60,12 @@ for wall_group in walls:
cell = grid.at(x, y)
cell.walkable = 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
player = mcrfpy.Entity(5, 10, grid=grid)
player = mcrfpy.Entity((5, 10), grid=grid)
player.sprite_index = 64 # @
enemy = mcrfpy.Entity(25, 10, grid=grid)
enemy = mcrfpy.Entity((25, 10), grid=grid)
enemy.sprite_index = 69 # E
# Update initial visibility
@ -80,24 +83,24 @@ grid.position = (50, 100)
grid.size = (900, 600) # 30*30, 20*30
# 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)
ui.append(title)
# 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)
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)
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)
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)
ui.append(enemy_info)

View file

@ -11,6 +11,9 @@ mcrfpy.createScene("vis_test")
print("Creating grid...")
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
print("Initializing grid...")
for y in range(10):
@ -18,11 +21,11 @@ for y in range(10):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120)
color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
# Create entity
print("Creating entity...")
entity = mcrfpy.Entity(5, 5, grid=grid)
entity = mcrfpy.Entity((5, 5), grid=grid)
entity.sprite_index = 64
print("Updating visibility...")

View file

@ -13,8 +13,8 @@ print("Scene created")
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
print("Grid created")
# Create entity without appending
entity = mcrfpy.Entity(2, 2, grid=grid)
# Create entity with grid association
entity = mcrfpy.Entity((2, 2), grid=grid)
print(f"Entity created at ({entity.x}, {entity.y})")
# 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
3. Cell access (read/write) works 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
@ -19,22 +23,21 @@ def test_small_grid():
# Small grid should use flat storage
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
for y in range(50):
for x in range(50):
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
# Verify cells
cell = grid.at(25, 25)
expected_r = (25 * 5) % 256
expected_g = (25 * 5) % 256
color = cell.color
r, g = color[0], color[1]
if r != expected_r or g != expected_g:
print(f"FAIL: Small grid cell color mismatch. Expected ({expected_r}, {expected_g}), got ({r}, {g})")
color = color_layer.at(25, 25)
if color.r != expected_r or color.g != expected_g:
print(f"FAIL: Small grid cell color mismatch. Expected ({expected_r}, {expected_g}), got ({color.r}, {color.g})")
return False
print(" Small grid: PASS")
@ -46,6 +49,7 @@ def test_large_grid():
# Large grid should use chunk storage (100 > 64)
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
# 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:
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
# Verify cells
for x, y in test_points:
cell = grid.at(x, y)
color = cell.color
if color[0] != x or color[1] != y:
print(f"FAIL: Large grid cell ({x},{y}) color mismatch. Expected ({x}, {y}), got ({color[0]}, {color[1]})")
color = color_layer.at(x, y)
if color.r != x or color.g != y:
print(f"FAIL: Large grid cell ({x},{y}) color mismatch. Expected ({x}, {y}), got ({color.r}, {color.g})")
return False
print(" Large grid cell access: PASS")
@ -81,6 +84,7 @@ def test_very_large_grid():
# 500x500 = 250,000 cells, should use ~64 chunks (8x8)
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
test_points = [
@ -94,14 +98,12 @@ def test_very_large_grid():
]
for x, y in test_points:
cell = grid.at(x, y)
cell.color = mcrfpy.Color(x % 256, y % 256, 200, 255)
color_layer.set(x, y, mcrfpy.Color(x % 256, y % 256, 200, 255))
# Verify
for x, y in test_points:
cell = grid.at(x, y)
color = cell.color
if color[0] != (x % 256) or color[1] != (y % 256):
color = color_layer.at(x, y)
if color.r != (x % 256) or color.g != (y % 256):
print(f"FAIL: Very large grid cell ({x},{y}) color mismatch")
return False
@ -114,20 +116,20 @@ def test_boundary_case():
# 64x64 should use flat storage (not exceeding threshold)
grid_64 = mcrfpy.Grid(grid_size=(64, 64), pos=(10, 10), size=(400, 400))
cell = grid_64.at(63, 63)
cell.color = mcrfpy.Color(255, 0, 0, 255)
color = grid_64.at(63, 63).color
if color[0] != 255:
print(f"FAIL: 64x64 grid boundary cell not set correctly, got r={color[0]}")
color_layer_64 = grid_64.add_layer("color", z_index=-1)
color_layer_64.set(63, 63, mcrfpy.Color(255, 0, 0, 255))
color = color_layer_64.at(63, 63)
if color.r != 255:
print(f"FAIL: 64x64 grid boundary cell not set correctly, got r={color.r}")
return False
# 65x65 should use chunk storage (exceeding threshold)
grid_65 = mcrfpy.Grid(grid_size=(65, 65), pos=(10, 10), size=(400, 400))
cell = grid_65.at(64, 64)
cell.color = mcrfpy.Color(0, 255, 0, 255)
color = grid_65.at(64, 64).color
if color[1] != 255:
print(f"FAIL: 65x65 grid cell not set correctly, got g={color[1]}")
color_layer_65 = grid_65.add_layer("color", z_index=-1)
color_layer_65.set(64, 64, mcrfpy.Color(0, 255, 0, 255))
color = color_layer_65.at(64, 64)
if color.g != 255:
print(f"FAIL: 65x65 grid cell not set correctly, got g={color.g}")
return False
print(" Boundary cases: PASS")
@ -139,19 +141,18 @@ def test_edge_cases():
# Create 100x100 grid
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
corners = [(0, 0), (99, 0), (0, 99), (99, 99)]
for i, (x, y) in enumerate(corners):
cell = grid.at(x, y)
cell.color = mcrfpy.Color(i * 60, i * 60, i * 60, 255)
color_layer.set(x, y, mcrfpy.Color(i * 60, i * 60, i * 60, 255))
for i, (x, y) in enumerate(corners):
cell = grid.at(x, y)
expected = i * 60
color = cell.color
if color[0] != expected:
print(f"FAIL: Corner ({x},{y}) color mismatch, expected {expected}, got {color[0]}")
color = color_layer.at(x, y)
if color.r != expected:
print(f"FAIL: Corner ({x},{y}) color mismatch, expected {expected}, got {color.r}")
return False
print(" Edge cases: PASS")

View file

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

View file

@ -51,17 +51,17 @@ mcrfpy.setScene("timer_test_scene")
ui = mcrfpy.sceneUI("timer_test_scene")
# 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
outline_color=mcrfpy.Color(255, 255, 255), # White outline
outline=5.0)
ui.append(frame)
# Add text
caption = mcrfpy.Caption(mcrfpy.Vector(150, 150),
caption = mcrfpy.Caption(pos=(150, 150),
text="TIMER TEST - SHOULD BE VISIBLE",
fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 24
caption.font_size = 24
frame.children.append(caption)
# Add click handler to demonstrate interaction

View file

@ -1,4 +1,4 @@
import mcrfpy
e = mcrfpy.Entity(0, 0)
e = mcrfpy.Entity((0, 0))
print("Entity attributes:", dir(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)}")
# 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))
ui.append(frame)
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)
# 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_size = 24
title.font_color = (255, 255, 255)
title.fill_color = mcrfpy.Color(255, 255, 255)
# Create main grid (20x15 tiles, each 32x32 pixels)
grid = mcrfpy.Grid(100, 100, 20, 15, texture, 32, 32)
grid.texture = texture
grid = mcrfpy.Grid(pos=(100, 100), grid_size=(20, 15), texture=texture, size=(640, 480))
# Define tile types from Crypt of Sokoban
FLOOR = 58 # Stone floor
@ -63,36 +62,21 @@ grid.set_tile(12, 8, BOULDER)
# Create some entities on the grid
# Player entity
player = mcrfpy.Entity(5, 7)
player.texture = texture
player.sprite_index = 84 # Player sprite
player = mcrfpy.Entity((5, 7), texture=texture, sprite_index=84, grid=grid) # Player sprite
# Enemy entities
rat1 = mcrfpy.Entity(12, 5)
rat1.texture = texture
rat1.sprite_index = 123 # Rat
rat1 = mcrfpy.Entity((12, 5), texture=texture, sprite_index=123, grid=grid) # Rat
rat2 = mcrfpy.Entity(14, 9)
rat2.texture = texture
rat2.sprite_index = 123 # Rat
rat2 = mcrfpy.Entity((14, 9), texture=texture, sprite_index=123, grid=grid) # Rat
cyclops = mcrfpy.Entity(10, 10)
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)
cyclops = mcrfpy.Entity((10, 10), texture=texture, sprite_index=109, grid=grid) # Cyclops
# 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_color = (255, 255, 255)
palette_label.fill_color = mcrfpy.Color(255, 255, 255)
palette = mcrfpy.Grid(250, 580, 7, 1, texture, 32, 32)
palette.texture = texture
palette = mcrfpy.Grid(pos=(250, 580), grid_size=(7, 1), texture=texture, size=(224, 32))
palette.set_tile(0, 0, FLOOR)
palette.set_tile(1, 0, WALL)
palette.set_tile(2, 0, DOOR)
@ -104,17 +88,17 @@ palette.set_tile(6, 0, BOULDER)
# Labels for palette
labels = ["Floor", "Wall", "Door", "Chest", "Button", "Exit", "Boulder"]
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_size = 10
l.font_color = (255, 255, 255)
l.fill_color = mcrfpy.Color(255, 255, 255)
mcrfpy.sceneUI("grid").append(l)
# 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_size = 14
info.font_color = (200, 200, 200)
info.fill_color = mcrfpy.Color(200, 200, 200)
# Add all elements to scene
ui = mcrfpy.sceneUI("grid")

View file

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

View file

@ -17,7 +17,7 @@ def test_transparency_workaround():
# WORKAROUND: Create a full-window opaque frame as the first element
# This acts as an opaque background since the scene clears with transparent
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
outline_color=None,
outline=0.0)
@ -28,31 +28,31 @@ def test_transparency_workaround():
print("\nAdding test content...")
# 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),
outline_color=mcrfpy.Color(255, 255, 255),
outline=3.0)
ui.append(frame1)
# 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),
outline_color=mcrfpy.Color(0, 0, 0),
outline=3.0)
ui.append(frame2)
# 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),
outline_color=mcrfpy.Color(255, 255, 0),
outline=3.0)
ui.append(frame3)
# Add text
caption = mcrfpy.Caption(mcrfpy.Vector(250, 50),
caption = mcrfpy.Caption(pos=(250, 50),
text="OPAQUE BACKGROUND TEST",
fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 32
caption.font_size = 32
ui.append(caption)
# Take screenshot

View file

@ -31,9 +31,9 @@ def take_screenshot(runtime):
mcrfpy.createScene("test")
# 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_color = (255, 255, 255)
caption.fill_color = mcrfpy.Color(255, 255, 255)
caption.font_size = 24
mcrfpy.sceneUI("test").append(caption)

View file

@ -30,7 +30,7 @@ mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# 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))
ui.append(frame)

View file

@ -73,6 +73,9 @@ mcrfpy.createScene("chain_test")
grid = mcrfpy.Grid(grid_x=20, grid_y=15)
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
for y in range(15):
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:
cell.walkable = False
cell.transparent = False
cell.color = mcrfpy.Color(60, 40, 40)
color_layer.set(x, y, mcrfpy.Color(60, 40, 40))
else:
cell.walkable = True
cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120)
color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
# Create entities
player = mcrfpy.Entity(2, 2, grid=grid)
player = mcrfpy.Entity((2, 2), grid=grid)
player.sprite_index = 64 # @
enemy = mcrfpy.Entity(17, 12, grid=grid)
enemy = mcrfpy.Entity((17, 12), grid=grid)
enemy.sprite_index = 69 # E
# UI setup
@ -99,15 +102,15 @@ ui.append(grid)
grid.position = (100, 100)
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)
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)
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)
ui.append(info)

View file

@ -63,14 +63,15 @@ mcrfpy.createScene("anim_debug")
# Simple grid
grid = mcrfpy.Grid(grid_x=15, grid_y=10)
color_layer = grid.add_layer("color", z_index=-1)
for y in range(10):
for x in range(15):
cell = grid.at(x, y)
cell.walkable = True
cell.color = mcrfpy.Color(100, 100, 120)
color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
# Test entity
entity = mcrfpy.Entity(5, 5, grid=grid)
entity = mcrfpy.Entity((5, 5), grid=grid)
entity.sprite_index = 64
# UI
@ -79,19 +80,19 @@ ui.append(grid)
grid.position = (100, 150)
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)
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)
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)
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)
ui.append(active_display)

View file

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

View file

@ -30,7 +30,7 @@ def test_1_basic_animation():
"""Test that basic animations still work"""
try:
ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(100, 100, 100, 100)
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("x", 200.0, 1000, "linear")
@ -49,7 +49,7 @@ def test_2_remove_animated_object():
"""Test removing object with active animation"""
try:
ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(100, 100, 100, 100)
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
# Start animation
@ -73,7 +73,7 @@ def test_3_complete_animation():
"""Test completing animation immediately"""
try:
ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(100, 100, 100, 100)
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
# Start animation
@ -98,7 +98,7 @@ def test_4_multiple_animations_timer():
nonlocal success
try:
ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(200, 200, 100, 100)
frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100))
ui.append(frame)
# Create multiple animations rapidly (this used to crash)
@ -129,7 +129,7 @@ def test_5_scene_cleanup():
# Add animated objects to first scene
ui = mcrfpy.sceneUI("test")
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)
anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce")
anim.start(frame)
@ -150,7 +150,7 @@ def test_6_animation_after_clear():
ui = mcrfpy.sceneUI("test")
# Create and animate
frame = mcrfpy.Frame(100, 100, 100, 100)
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic")
anim.start(frame)
@ -207,7 +207,7 @@ mcrfpy.setScene("test")
# Add a background
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)
ui.append(bg)

View file

@ -42,14 +42,14 @@ mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# Add title and subtitle (to preserve during clearing)
title = mcrfpy.Caption("Test Title", 400, 20)
subtitle = mcrfpy.Caption("Test Subtitle", 400, 50)
title = mcrfpy.Caption(pos=(400, 20), text="Test Title")
subtitle = mcrfpy.Caption(pos=(400, 50), text="Test Subtitle")
ui.extend([title, subtitle])
# Create initial animated objects
print("Creating initial animated objects...")
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)
ui.append(f)

View file

@ -21,6 +21,11 @@ def create_test_grid():
# Create grid
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
for y in range(grid.grid_y):
for x in range(grid.grid_x):
@ -28,7 +33,7 @@ def create_test_grid():
cell.walkable = True
cell.transparent = True
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
# Vertical wall
@ -37,7 +42,7 @@ def create_test_grid():
cell.walkable = False
cell.transparent = False
cell.tilesprite = 219 # Block
cell.color = mcrfpy.Color(100, 100, 100)
color_layer.set(10, y, mcrfpy.Color(100, 100, 100))
# Horizontal wall
for x in range(5, 15):
@ -46,7 +51,7 @@ def create_test_grid():
cell.walkable = False
cell.transparent = False
cell.tilesprite = 219
cell.color = mcrfpy.Color(100, 100, 100)
color_layer.set(x, 10, mcrfpy.Color(100, 100, 100))
return grid
@ -133,7 +138,7 @@ def test_multi_target_scenario():
# Mark threat position
cell = grid.at(tx, ty)
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
grid.compute_dijkstra(tx, ty)
@ -176,7 +181,7 @@ def test_multi_target_scenario():
# Mark safe position
cell = grid.at(best_pos[0], best_pos[1])
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):
"""Timer callback to run tests after scene loads"""
@ -211,7 +216,7 @@ ui = mcrfpy.sceneUI("dijkstra_test")
ui.append(grid)
# 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)
ui.append(title)

View file

@ -17,13 +17,16 @@ mcrfpy.createScene("test_anim")
grid = mcrfpy.Grid(grid_x=15, grid_y=15)
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
for y in range(15):
for x in range(15):
cell = grid.at(x, y)
cell.walkable = 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
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)]
for x, y in path_cells:
cell = grid.at(x, y)
cell.color = mcrfpy.Color(120, 120, 150)
color_layer.set(x, y, mcrfpy.Color(120, 120, 150))
# Create entity at start position
entity = mcrfpy.Entity(5, 5, grid=grid)
entity = mcrfpy.Entity((5, 5), grid=grid)
entity.sprite_index = 64 # @
# UI setup
@ -46,27 +48,27 @@ grid.position = (100, 100)
grid.size = (450, 450) # 15 * 30 pixels per cell
# 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)
ui.append(title)
# 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)
ui.append(status)
# 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)
ui.append(pos_display)
# 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)
ui.append(anim_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)
ui.append(debug_info)

View file

@ -33,16 +33,19 @@ mcrfpy.createScene("fix_demo")
grid = mcrfpy.Grid(grid_x=15, grid_y=10)
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
for y in range(10):
for x in range(15):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120)
color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
# Create entity
entity = mcrfpy.Entity(2, 2, grid=grid)
entity = mcrfpy.Entity((2, 2), grid=grid)
entity.sprite_index = 64 # @
# UI
@ -52,19 +55,19 @@ grid.position = (100, 150)
grid.size = (450, 300)
# 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)
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)
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)
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)
ui.append(status)

View file

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

View file

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

View file

@ -13,32 +13,28 @@ def test_grid_background():
ui = mcrfpy.sceneUI("test")
# Create a grid with default background
grid = mcrfpy.Grid(20, 15, grid_size=(20, 15))
grid.x = 50
grid.y = 50
grid.w = 400
grid.h = 300
grid = mcrfpy.Grid(pos=(50, 50), size=(400, 300), grid_size=(20, 15))
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 y in range(5, 10):
point = grid.at(x, y)
point.color = mcrfpy.Color(100, 150, 100)
color_layer.set(x, y, mcrfpy.Color(100, 150, 100))
# 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),
outline_color=mcrfpy.Color(200, 200, 200),
outline=2)
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.fill_color = mcrfpy.Color(255, 255, 255)
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.fill_color = mcrfpy.Color(200, 200, 200)
info_frame.children.append(color_display)

View file

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

View file

@ -10,13 +10,13 @@ ui = mcrfpy.sceneUI("headless_test")
mcrfpy.setScene("headless_test")
# Create a visible indicator
frame = mcrfpy.Frame(200, 200, 400, 200)
frame.fill_color = (100, 200, 100, 255)
frame = mcrfpy.Frame(pos=(200, 200), size=(400, 200))
frame.fill_color = mcrfpy.Color(100, 200, 100, 255)
ui.append(frame)
caption = mcrfpy.Caption((400, 300), "If you see this, windowed mode is working!", mcrfpy.default_font)
caption.size = 24
caption.fill_color = (255, 255, 255)
caption = mcrfpy.Caption(pos=(400, 300), text="If you see this, windowed mode is working!")
caption.font_size = 24
caption.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(caption)
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")
# Create various UI elements
frame1 = mcrfpy.Frame(10, 10, 200, 150)
frame1.fill_color = (100, 100, 100, 128)
frame1 = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
frame1.fill_color = mcrfpy.Color(100, 100, 100, 128)
ui.append(frame1)
caption1 = mcrfpy.Caption("Test Caption", 50, 50)
caption1 = mcrfpy.Caption(pos=(50, 50), text="Test Caption")
ui.append(caption1)
sprite1 = mcrfpy.Sprite(100, 100)
sprite1 = mcrfpy.Sprite(pos=(100, 100))
ui.append(sprite1)
# 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
ui.append(frame2)

View file

@ -11,17 +11,20 @@ print("=" * 50)
mcrfpy.createScene("test")
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
for y in range(5):
for x in range(5):
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
e1 = mcrfpy.Entity(0, 0)
e2 = mcrfpy.Entity(4, 4)
grid.entities.append(e1)
grid.entities.append(e2)
e1 = mcrfpy.Entity((0, 0), grid=grid)
e2 = mcrfpy.Entity((4, 4), grid=grid)
e1.sprite_index = 64
e2.sprite_index = 69
print(f"Entity 1 at ({e1.x}, {e1.y})")
print(f"Entity 2 at ({e2.x}, {e2.y})")
@ -35,15 +38,16 @@ PATH_COLOR = mcrfpy.Color(100, 255, 100) # Green
print(f"\nSetting path cells to green ({PATH_COLOR.r}, {PATH_COLOR.g}, {PATH_COLOR.b})...")
for x, y in path:
cell = grid.at(x, y)
# 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
cell.color = PATH_COLOR
color_layer.set(x, y, PATH_COLOR)
# 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}")
@ -51,8 +55,8 @@ for x, y in path:
print("\nVerifying all cells in grid:")
for y in range(5):
for x in range(5):
cell = grid.at(x, y)
color = cell.color[:3] # Get RGB from tuple
c = color_layer.at(x, y)
color = (c.r, c.g, c.b)
is_path = (x, y) in 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
# Create entities
e1 = mcrfpy.Entity(2, 5)
e2 = mcrfpy.Entity(8, 5)
grid.entities.append(e1)
grid.entities.append(e2)
e1 = mcrfpy.Entity((2, 5), grid=grid)
e2 = mcrfpy.Entity((8, 5), grid=grid)
# Test pathfinding between entities
print(f"Entity 1 at ({e1.x}, {e1.y})")

View file

@ -10,7 +10,7 @@ def test_properties(runtime):
# Test Frame
try:
frame = mcrfpy.Frame(10, 10, 100, 100)
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
print(f"Frame visible: {frame.visible}")
frame.visible = False
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
mcrfpy.createScene("red_scene")
ui1 = mcrfpy.sceneUI("red_scene")
bg1 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(255, 0, 0, 255))
label1 = mcrfpy.Caption(512, 384, "RED SCENE", font=mcrfpy.Font.font_ui)
label1.color = mcrfpy.Color(255, 255, 255, 255)
bg1 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(255, 0, 0, 255))
label1 = mcrfpy.Caption(pos=(512, 384), text="RED SCENE", font=mcrfpy.Font.font_ui)
label1.fill_color = mcrfpy.Color(255, 255, 255, 255)
ui1.append(bg1)
ui1.append(label1)
# Scene 2: Blue background
mcrfpy.createScene("blue_scene")
ui2 = mcrfpy.sceneUI("blue_scene")
bg2 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(0, 0, 255, 255))
label2 = mcrfpy.Caption(512, 384, "BLUE SCENE", font=mcrfpy.Font.font_ui)
label2.color = mcrfpy.Color(255, 255, 255, 255)
bg2 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(0, 0, 255, 255))
label2 = mcrfpy.Caption(pos=(512, 384), text="BLUE SCENE", font=mcrfpy.Font.font_ui)
label2.fill_color = mcrfpy.Color(255, 255, 255, 255)
ui2.append(bg2)
ui2.append(label2)
# Scene 3: Green background
mcrfpy.createScene("green_scene")
ui3 = mcrfpy.sceneUI("green_scene")
bg3 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(0, 255, 0, 255))
label3 = mcrfpy.Caption(512, 384, "GREEN SCENE", font=mcrfpy.Font.font_ui)
label3.color = mcrfpy.Color(0, 0, 0, 255) # Black text on green
bg3 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(0, 255, 0, 255))
label3 = mcrfpy.Caption(pos=(512, 384), text="GREEN SCENE", font=mcrfpy.Font.font_ui)
label3.fill_color = mcrfpy.Color(0, 0, 0, 255) # Black text on green
ui3.append(bg3)
ui3.append(label3)
# Scene 4: Menu scene with buttons
mcrfpy.createScene("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.color = mcrfpy.Color(255, 255, 255, 255)
title = mcrfpy.Caption(pos=(512, 100), text="SCENE TRANSITION DEMO", font=mcrfpy.Font.font_ui)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
ui4.append(bg4)
ui4.append(title)
# Add instruction text
instructions = mcrfpy.Caption(512, 200, "Press keys 1-6 for different transitions", font=mcrfpy.Font.font_ui)
instructions.color = mcrfpy.Color(200, 200, 200, 255)
instructions = mcrfpy.Caption(pos=(512, 200), text="Press keys 1-6 for different transitions", font=mcrfpy.Font.font_ui)
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
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.color = mcrfpy.Color(150, 150, 150, 255)
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.fill_color = mcrfpy.Color(150, 150, 150, 255)
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.color = mcrfpy.Color(150, 150, 150, 255)
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.fill_color = mcrfpy.Color(150, 150, 150, 255)
ui4.append(scene_info)
print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene")

View file

@ -13,13 +13,13 @@ def test_scene_transitions():
# Scene 1
mcrfpy.createScene("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)
# Scene 2
mcrfpy.createScene("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)
# Test each transition type

View file

@ -8,7 +8,7 @@ def simple_test(runtime):
try:
# 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}")
bounds = frame.get_bounds()

View file

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

View file

@ -69,11 +69,11 @@ def main():
mcrfpy.setScene("test")
# Create multiple captions for testing
caption1 = mcrfpy.Caption(50, 50, "Caption 1: Normal", fill_color=(255, 255, 255))
caption2 = mcrfpy.Caption(50, 100, "Caption 2: Will be invisible", fill_color=(255, 200, 200))
caption3 = mcrfpy.Caption(50, 150, "Caption 3: 50% opacity", fill_color=(200, 255, 200))
caption4 = mcrfpy.Caption(50, 200, "Caption 4: 25% opacity", fill_color=(200, 200, 255))
caption5 = mcrfpy.Caption(50, 250, "Caption 5: 0% opacity", fill_color=(255, 255, 200))
caption1 = mcrfpy.Caption(pos=(50, 50), text="Caption 1: Normal", fill_color=mcrfpy.Color(255, 255, 255))
caption2 = mcrfpy.Caption(pos=(50, 100), text="Caption 2: Will be invisible", fill_color=mcrfpy.Color(255, 200, 200))
caption3 = mcrfpy.Caption(pos=(50, 150), text="Caption 3: 50% opacity", fill_color=mcrfpy.Color(200, 255, 200))
caption4 = mcrfpy.Caption(pos=(50, 200), text="Caption 4: 25% opacity", fill_color=mcrfpy.Color(200, 200, 255))
caption5 = mcrfpy.Caption(pos=(50, 250), text="Caption 5: 0% opacity", fill_color=mcrfpy.Color(255, 255, 200))
# Add captions to scene
ui = mcrfpy.sceneUI("test")
@ -84,7 +84,7 @@ def main():
ui.append(caption5)
# 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
ui.append(frame)

View file

@ -18,6 +18,9 @@ mcrfpy.createScene("visibility_test")
grid = mcrfpy.Grid(grid_x=20, grid_y=15)
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
print("\nInitializing 20x15 grid...")
for y in range(15):
@ -25,7 +28,7 @@ for y in range(15):
cell = grid.at(x, y)
cell.walkable = 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
print("Adding walls...")
@ -47,14 +50,14 @@ for wall_group in walls:
cell = grid.at(x, y)
cell.walkable = 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
print("\nCreating entities...")
entities = [
mcrfpy.Entity(2, 7), # Left side
mcrfpy.Entity(18, 7), # Right side
mcrfpy.Entity(10, 1), # Top center (above wall)
mcrfpy.Entity((2, 7)), # Left side
mcrfpy.Entity((18, 7)), # Right side
mcrfpy.Entity((10, 1)), # Top center (above wall)
]
for i, entity in enumerate(entities):
@ -138,17 +141,17 @@ grid.position = (50, 50)
grid.size = (600, 450) # 20*30, 15*30
# 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)
ui.append(title)
# 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)
ui.append(info)
# 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)
ui.append(legend)

View file

@ -4,10 +4,20 @@
import mcrfpy
import sys
# Colors as tuples (r, g, b, a)
WALL_COLOR = (60, 30, 30, 255)
FLOOR_COLOR = (200, 200, 220, 255)
PATH_COLOR = (100, 255, 100, 255)
# Colors
WALL_COLOR = mcrfpy.Color(60, 30, 30)
FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
PATH_COLOR = mcrfpy.Color(100, 255, 100)
# Create scene
mcrfpy.createScene("visual_test")
# Create grid
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
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"""
@ -21,33 +31,23 @@ def check_render(dt):
# 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]}")
color = color_layer.at(x, y)
print(f" Cell ({x},{y}): color=({color.r}, {color.g}, {color.b})")
sys.exit(0)
# Create scene
mcrfpy.createScene("visual_test")
# Create grid
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Initialize all cells as floor
print("Initializing grid...")
for y in range(5):
for x in range(5):
grid.at(x, y).walkable = True
grid.at(x, y).color = FLOOR_COLOR
color_layer.set(x, y, FLOOR_COLOR)
# Create entities
e1 = mcrfpy.Entity(0, 0)
e2 = mcrfpy.Entity(4, 4)
e1 = mcrfpy.Entity((0, 0), grid=grid)
e2 = mcrfpy.Entity((4, 4), grid=grid)
e1.sprite_index = 64 # @
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 2 at ({e2.x}, {e2.y})")
@ -60,7 +60,7 @@ print(f"\nPath from E1 to E2: {path}")
if path:
print("\nColoring path cells green...")
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")
# Set up UI
@ -70,7 +70,7 @@ grid.position = (50, 50)
grid.size = (250, 250)
# 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)
ui.append(title)

View file

@ -16,11 +16,11 @@ def test_issue_38_children():
print("\nTest 1: Passing children argument to Frame constructor")
try:
# Create some child elements
child1 = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Child 1")
child2 = mcrfpy.Sprite(mcrfpy.Vector(10, 30))
child1 = mcrfpy.Caption(pos=(10, 10), text="Child 1")
child2 = mcrfpy.Sprite(pos=(10, 30))
# 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)")
except TypeError as 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
print("\nTest 2: Adding children after Frame creation")
try:
frame = mcrfpy.Frame(10, 10, 200, 150)
frame = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
ui.append(frame)
# Add children via the children collection
child1 = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Added Child 1")
child2 = mcrfpy.Caption(mcrfpy.Vector(10, 30), text="Added Child 2")
child1 = mcrfpy.Caption(pos=(10, 10), text="Added Child 1")
child2 = mcrfpy.Caption(pos=(10, 30), text="Added Child 2")
frame.children.append(child1)
frame.children.append(child2)
@ -65,7 +65,7 @@ def test_issue_42_click_callback():
return True
try:
frame1 = mcrfpy.Frame(10, 10, 200, 150)
frame1 = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
ui.append(frame1)
frame1.on_click = correct_callback
print("✓ Click callback with correct signature assigned successfully")
@ -78,7 +78,7 @@ def test_issue_42_click_callback():
print(" Wrong callback called")
try:
frame2 = mcrfpy.Frame(220, 10, 200, 150)
frame2 = mcrfpy.Frame(pos=(220, 10), size=(200, 150))
ui.append(frame2)
frame2.on_click = wrong_callback_no_args
print("✓ Click callback with no args assigned (will fail at runtime per issue #42)")
@ -91,7 +91,7 @@ def test_issue_42_click_callback():
print(f" Wrong callback called: x={x}, y={y}")
try:
frame3 = mcrfpy.Frame(10, 170, 200, 150)
frame3 = mcrfpy.Frame(pos=(10, 170), size=(200, 150))
ui.append(frame3)
frame3.on_click = wrong_callback_few_args
print("✓ Click callback with 2 args assigned (will fail at runtime per issue #42)")

View file

@ -10,7 +10,7 @@ def test_grid_none_texture(runtime):
# Test 1: Create Grid with None texture
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")
except Exception as e:
print(f"✗ Failed to create Grid with None texture: {e}")
@ -34,17 +34,19 @@ def test_grid_none_texture(runtime):
except Exception as 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:
# Add a color layer to the grid
color_layer = grid.add_layer("color", z_index=-1)
# Create a checkerboard pattern with colors
for x in range(10):
for y in range(10):
point = grid.at(x, y)
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:
point.color = mcrfpy.Color(0, 0, 255, 255) # Blue
print("✓ Successfully set grid point colors")
color_layer.set(x, y, mcrfpy.Color(0, 0, 255, 255)) # Blue
print("✓ Successfully set grid colors via ColorLayer")
except Exception as e:
print(f"✗ Failed to set grid colors: {e}")
@ -52,7 +54,7 @@ def test_grid_none_texture(runtime):
try:
# Create an entity with its own texture
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)
print(f"✓ Added entity to grid, total entities: {len(grid.entities)}")
except Exception as e:
@ -64,8 +66,8 @@ def test_grid_none_texture(runtime):
grid.zoom = 2.0
print(f"✓ Set zoom to: {grid.zoom}")
# Test center
grid.center = mcrfpy.Vector(5, 5)
# Test center (uses pixel coordinates)
grid.center = (200, 200)
print(f"✓ Set center to: {grid.center}")
except Exception as 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
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),
outline_color=mcrfpy.Color(0, 0, 0),
outline=2.0)

View file

@ -11,8 +11,8 @@ def test_UICollection():
ui = mcrfpy.sceneUI("collection_test")
# Add various UI elements
frame = mcrfpy.Frame(10, 10, 100, 100)
caption = mcrfpy.Caption(mcrfpy.Vector(120, 10), text="Test")
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
caption = mcrfpy.Caption(pos=(120, 10), text="Test")
# Skip sprite for now since it requires a texture
ui.append(frame)
@ -74,9 +74,9 @@ def test_UICollection():
# Test type preservation (Issue #76)
try:
# 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))
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)
ui.append(parent_frame)

View file

@ -18,7 +18,7 @@ def test_screenshot_validation():
print("Creating UI elements...")
# 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
outline_color=mcrfpy.Color(255, 255, 255), # White
outline=5.0)
@ -26,7 +26,7 @@ def test_screenshot_validation():
print("Added red frame at (50, 50)")
# 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
outline_color=mcrfpy.Color(0, 0, 0), # Black
outline=3.0)
@ -34,7 +34,7 @@ def test_screenshot_validation():
print("Added green frame at (400, 50)")
# 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
outline_color=mcrfpy.Color(255, 255, 0), # Yellow
outline=4.0)
@ -42,26 +42,26 @@ def test_screenshot_validation():
print("Added blue frame at (50, 300)")
# Add text captions
caption1 = mcrfpy.Caption(mcrfpy.Vector(60, 60),
caption1 = mcrfpy.Caption(pos=(60, 60),
text="RED FRAME TEST",
fill_color=mcrfpy.Color(255, 255, 255))
caption1.size = 24
caption1.font_size = 24
frame1.children.append(caption1)
caption2 = mcrfpy.Caption(mcrfpy.Vector(410, 60),
caption2 = mcrfpy.Caption(pos=(410, 60),
text="GREEN FRAME TEST",
fill_color=mcrfpy.Color(0, 0, 0))
caption2.size = 24
caption2.font_size = 24
ui.append(caption2)
caption3 = mcrfpy.Caption(mcrfpy.Vector(60, 310),
caption3 = mcrfpy.Caption(pos=(60, 310),
text="BLUE FRAME TEST",
fill_color=mcrfpy.Color(255, 255, 0))
caption3.size = 24
caption3.font_size = 24
ui.append(caption3)
# 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
# Insert at beginning so it's behind everything
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")
# 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),
outline_color=mcrfpy.Color(255, 255, 255),
outline=3.0)
ui.append(frame)
caption = mcrfpy.Caption(mcrfpy.Vector(150, 150),
caption = mcrfpy.Caption(pos=(150, 150),
text="TIMER TEST SUCCESS",
fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 24
caption.font_size = 24
ui.append(caption)
# Timer callback with correct signature

View file

@ -10,6 +10,7 @@ import inspect
import datetime
import html
import re
import types
from pathlib import Path
def transform_doc_links(docstring, format='html', base_url=''):
@ -214,11 +215,21 @@ def get_all_classes():
"parsed": parse_docstring(method_doc)
}
elif isinstance(attr, property):
# Pure Python property
prop_doc = (attr.fget.__doc__ if attr.fget else "") or ""
class_info["properties"][attr_name] = {
"doc": prop_doc,
"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:
pass