diff --git a/CLAUDE.md b/CLAUDE.md index b9e553c..5f82320 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 2. **Always Check Gitea First** - Before starting work: Check open issues for related tasks or blockers + - Before implementing: Read relevant wiki pages per the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) consultation table - When using `/roadmap` command: Query Gitea for up-to-date issue status - When researching a feature: Search Gitea wiki and issues before grepping codebase - When encountering a bug: Check if an issue already exists @@ -29,9 +30,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 4. **Document as You Go** - When work on one issue interacts with another system: Add notes to related issues - - When discovering undocumented behavior: Create task to document it - - When documentation misleads you: Create task to correct or expand it - - When implementing a feature: Update the Gitea wiki if appropriate + - When discovering undocumented behavior: Note it for wiki update + - When documentation misleads you: Note it for wiki correction + - After committing code changes: Update relevant wiki pages (with user permission) + - Follow the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) for wiki update procedures 5. **Cross-Reference Everything** - Commit messages should reference issue numbers (e.g., "Fixes #104", "Addresses #125") diff --git a/docs/api_reference_dynamic.html b/docs/api_reference_dynamic.html index 6e93bc2..14e58f6 100644 --- a/docs/api_reference_dynamic.html +++ b/docs/api_reference_dynamic.html @@ -108,7 +108,7 @@

McRogueFace API Reference

-

Generated on 2025-10-30 21:14:43

+

Generated on 2025-11-29 10:12:05

This documentation was dynamically generated from the compiled module.

@@ -118,8 +118,11 @@
  • Classes
  • +
    +

    end_benchmark() -> str

    +

    Stop benchmark capture and write data to JSON file. + +Note:

    +

    Returns: str: The filename of the written benchmark data

    +

    Raises: RuntimeError: If no benchmark is currently running Returns the auto-generated filename (e.g., 'benchmark_12345_20250528_143022.json')

    +
    +

    exit() -> None

    Cleanly shut down the game engine and exit the application. @@ -263,6 +277,19 @@ Note:

    Returns: None Only one music track can play at a time. Loading new music stops the current track.

    +
    +

    log_benchmark(message: str) -> None

    +

    Add a log message to the current benchmark frame. + +Note:

    +

    Arguments:

    + +

    Returns: None

    +

    Raises: RuntimeError: If no benchmark is currently running Messages appear in the 'logs' array of each frame in the output JSON.

    +
    +

    playSound(buffer_id: int) -> None

    Play a sound effect using a previously loaded buffer.

    @@ -285,6 +312,18 @@ Note:

    Raises: KeyError: If the specified scene doesn't exist

    +
    +

    setDevConsole(enabled: bool) -> None

    +

    Enable or disable the developer console overlay. + +Note:

    +

    Arguments:

    + +

    Returns: None When disabled, the grave/tilde key will not open the console. Use this to ship games without debug features.

    +
    +

    setMusicVolume(volume: int) -> None

    Set the global music volume.

    @@ -344,6 +383,15 @@ Note:

    Returns: None If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.

    +
    +

    start_benchmark() -> None

    +

    Start capturing benchmark data to a file. + +Note:

    +

    Returns: None

    +

    Raises: RuntimeError: If a benchmark is already running Benchmark filename is auto-generated from PID and timestamp. Use end_benchmark() to stop and get filename.

    +
    +

    Classes

    @@ -398,6 +446,73 @@ Note:

    +
    +

    Arc

    +

    Inherits from: Drawable

    +

    Arc(center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs) + +An arc UI element for drawing curved line segments. + +Args: + center (tuple, optional): Center position as (x, y). Default: (0, 0) + radius (float, optional): Arc radius in pixels. Default: 0 + start_angle (float, optional): Starting angle in degrees. Default: 0 + end_angle (float, optional): Ending angle in degrees. Default: 90 + color (Color, optional): Arc color. Default: White + thickness (float, optional): Line thickness. Default: 1.0 + +Keyword Args: + click (callable): Click handler. Default: None + visible (bool): Visibility state. Default: True + opacity (float): Opacity (0.0-1.0). Default: 1.0 + z_index (int): Rendering order. Default: 0 + name (str): Element name for finding. Default: None + +Attributes: + center (Vector): Center position + radius (float): Arc radius + start_angle (float): Starting angle in degrees + end_angle (float): Ending angle in degrees + color (Color): Arc color + thickness (float): Line thickness + visible (bool): Visibility state + opacity (float): Opacity value + z_index (int): Rendering order + name (str): Element name +

    +

    Methods:

    + +
    +
    get_bounds() -> tuple
    +

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

    +

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

    +
    + +
    +
    move(dx: float, dy: float) -> None
    +

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

    +
    +
    dx: Horizontal offset in pixels
    +
    dy: Vertical offset in pixels
    +
    +
    + +
    +
    resize(width: float, height: float) -> None
    +

    Resize the element to new dimensions. + +Note:

    +
    +
    width: New width in pixels
    +
    height: New height in pixels
    +
    +
    +
    +

    Caption

    Inherits from: Drawable

    @@ -462,6 +577,71 @@ Note:

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

    Resize the element to new dimensions. +Note:

    +
    +
    width: New width in pixels
    +
    height: New height in pixels
    +
    +
    + + +
    +

    Circle

    +

    Inherits from: Drawable

    +

    Circle(radius=0, center=None, fill_color=None, outline_color=None, outline=0, **kwargs) + +A circle UI element for drawing filled or outlined circles. + +Args: + radius (float, optional): Circle radius in pixels. Default: 0 + center (tuple, optional): Center position as (x, y). Default: (0, 0) + fill_color (Color, optional): Fill color. Default: White + outline_color (Color, optional): Outline color. Default: Transparent + outline (float, optional): Outline thickness. Default: 0 (no outline) + +Keyword Args: + click (callable): Click handler. Default: None + visible (bool): Visibility state. Default: True + opacity (float): Opacity (0.0-1.0). Default: 1.0 + z_index (int): Rendering order. Default: 0 + name (str): Element name for finding. Default: None + +Attributes: + radius (float): Circle radius + center (Vector): Center position + fill_color (Color): Fill color + outline_color (Color): Outline color + outline (float): Outline thickness + visible (bool): Visibility state + opacity (float): Opacity value + z_index (int): Rendering order + name (str): Element name +

    +

    Methods:

    + +
    +
    get_bounds() -> tuple
    +

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

    +

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

    +
    + +
    +
    move(dx: float, dy: float) -> None
    +

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

    +
    +
    dx: Horizontal offset in pixels
    +
    dy: Vertical offset in pixels
    +
    +
    + +
    +
    resize(width: float, height: float) -> None
    +

    Resize the element to new dimensions. + Note:

    width: New width in pixels
    @@ -508,6 +688,43 @@ Note:

    +
    +

    ColorLayer

    +

    ColorLayer(z_index=-1, grid_size=None) + +A grid layer that stores RGBA colors per cell. + +Args: + z_index (int): Render order. Negative = below entities. Default: -1 + grid_size (tuple): Dimensions as (width, height). Default: parent grid size + +Attributes: + z_index (int): Layer z-order relative to entities + visible (bool): Whether layer is rendered + grid_size (tuple): Layer dimensions (read-only) + +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

    +

    Methods:

    + +
    +
    at(x, y) -> Color
    +

    Get the color at cell position (x, y).

    +
    + +
    +
    fill(color)
    +

    Fill the entire layer with the specified color.

    +
    + +
    +
    set(x, y, color)
    +

    Set the color at cell position (x, y).

    +
    +
    +

    Drawable

    Base class for all drawable UI elements

    @@ -643,23 +860,44 @@ when the entity moves if it has a grid with perspective set.

    Methods:

    -
    append(...)
    +
    append(entity)
    +

    Add an entity to the end of the collection.

    -
    count(...)
    +
    count(entity) -> int
    +

    Count occurrences of entity in the collection.

    -
    extend(...)
    +
    extend(iterable)
    +

    Add all entities from an iterable to the collection.

    -
    index(...)
    +
    find(name) -> entity or list
    +

    Find entities by name.

    +

    Returns: Single entity if exact match, list if wildcard, None if not found.

    -
    remove(...)
    +
    index(entity) -> int
    +

    Return index of first occurrence of entity. Raises ValueError if not found.

    +
    + +
    +
    insert(index, entity)
    +

    Insert entity at index. Like list.insert(), indices past the end append.

    +
    + +
    +
    pop([index]) -> entity
    +

    Remove and return entity at index (default: last entity).

    +
    + +
    +
    remove(entity)
    +

    Remove first occurrence of entity. Raises ValueError if not found.

    @@ -695,6 +933,7 @@ Keyword Args: w (float): Width override. Default: 0 h (float): Height override. Default: 0 clip_children (bool): Whether to clip children to frame bounds. Default: False + cache_subtree (bool): Cache rendering to texture for performance. Default: False Attributes: x, y (float): Position in pixels @@ -708,7 +947,8 @@ Attributes: opacity (float): Opacity value z_index (int): Rendering order name (str): Element name - clip_children (bool): Whether to clip children to frame bounds

    + clip_children (bool): Whether to clip children to frame bounds + cache_subtree (bool): Cache subtree rendering to texture

    Methods:

    @@ -794,6 +1034,17 @@ Attributes: name (str): Element name

    Methods:

    +
    +
    add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer
    +

    Add a new layer to the grid.

    +
    +
    type: Layer type ('color' or 'tile')
    +
    z_index: Render order. Negative = below entities, >= 0 = above entities. Default: -1
    +
    texture: Texture for tile layers. Required for 'tile' type.
    +
    +

    Returns: The created ColorLayer or TileLayer object.

    +
    +
    at(...)
    @@ -822,8 +1073,8 @@ Attributes:
    -
    compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]
    -

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

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

    Compute field of view from a position.

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

    Returns: List of tuples (x, y, visible, discovered) for all visible cells: - x, y: Grid coordinates - visible: True (all returned cells are visible) - discovered: True (FOV implies discovery) Also updates the internal FOV state for use with is_in_fov().

    @@ -885,6 +1135,15 @@ Note:

    Returns: True if the cell is visible, False otherwise Must call compute_fov() first to calculate visibility.

    +
    +
    layer(z_index: int) -> ColorLayer | TileLayer | None
    +

    Get a layer by its z_index.

    +
    +
    z_index: The z_index of the layer to find.
    +
    +

    Returns: The layer with the specified z_index, or None if not found.

    +
    +
    move(dx: float, dy: float) -> None

    Move the element by a relative offset. @@ -896,6 +1155,14 @@ Note:

    +
    +
    remove_layer(layer: ColorLayer | TileLayer) -> None
    +

    Remove a layer from the grid.

    +
    +
    layer: The layer to remove.
    +
    +
    +
    resize(width: float, height: float) -> None

    Resize the element to new dimensions. @@ -920,6 +1187,69 @@ Note:

    Methods:

    +
    +

    Line

    +

    Inherits from: Drawable

    +

    Line(start=None, end=None, thickness=1.0, color=None, **kwargs) + +A line UI element for drawing straight lines between two points. + +Args: + start (tuple, optional): Starting point as (x, y). Default: (0, 0) + end (tuple, optional): Ending point as (x, y). Default: (0, 0) + thickness (float, optional): Line thickness in pixels. Default: 1.0 + color (Color, optional): Line color. Default: White + +Keyword Args: + click (callable): Click handler. Default: None + visible (bool): Visibility state. Default: True + opacity (float): Opacity (0.0-1.0). Default: 1.0 + z_index (int): Rendering order. Default: 0 + name (str): Element name for finding. Default: None + +Attributes: + start (Vector): Starting point + end (Vector): Ending point + thickness (float): Line thickness + color (Color): Line color + visible (bool): Visibility state + opacity (float): Opacity value + z_index (int): Rendering order + name (str): Element name +

    +

    Methods:

    + +
    +
    get_bounds() -> tuple
    +

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

    +

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

    +
    + +
    +
    move(dx: float, dy: float) -> None
    +

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

    +
    +
    dx: Horizontal offset in pixels
    +
    dy: Vertical offset in pixels
    +
    +
    + +
    +
    resize(width: float, height: float) -> None
    +

    Resize the element to new dimensions. + +Note:

    +
    +
    width: New width in pixels
    +
    height: New height in pixels
    +
    +
    +
    +

    Scene

    Base class for object-oriented scenes

    @@ -1029,6 +1359,45 @@ Note:

    Methods:

    +
    +

    TileLayer

    +

    TileLayer(z_index=-1, texture=None, grid_size=None) + +A grid layer that stores sprite indices per cell. + +Args: + z_index (int): Render order. Negative = below entities. Default: -1 + texture (Texture): Sprite atlas for tile rendering. Default: None + grid_size (tuple): Dimensions as (width, height). Default: parent grid size + +Attributes: + z_index (int): Layer z-order relative to entities + visible (bool): Whether layer is rendered + texture (Texture): Tile sprite atlas + grid_size (tuple): Layer dimensions (read-only) + +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

    +

    Methods:

    + +
    +
    at(x, y) -> int
    +

    Get the tile index at cell position (x, y). Returns -1 if no tile.

    +
    + +
    +
    fill(index)
    +

    Fill the entire layer with the specified tile index.

    +
    + +
    +
    set(x, y, index)
    +

    Set the tile index at cell position (x, y). Use -1 for no tile.

    +
    +
    +

    Timer

    Timer(name, callback, interval, once=False) @@ -1106,23 +1475,50 @@ Note:

    Methods:

    -
    append(...)
    +
    append(element)
    +

    Add an element to the end of the collection.

    -
    count(...)
    +
    count(element) -> int
    +

    Count occurrences of element in the collection.

    -
    extend(...)
    +
    extend(iterable)
    +

    Add all elements from an iterable to the collection.

    -
    index(...)
    +
    find(name, recursive=False) -> element or list
    +

    Find elements by name.

    +

    Returns: Single element if exact match, list if wildcard, None if not found.

    -
    remove(...)
    +
    index(element) -> int
    +

    Return index of first occurrence of element. Raises ValueError if not found.

    +
    + +
    +
    insert(index, element)
    +

    Insert element at index. Like list.insert(), indices past the end append. + +Note: If using z_index for sorting, insertion order may not persist after +the next render. Use name-based .find() for stable element access.

    +
    + +
    +
    pop([index]) -> element
    +

    Remove and return element at index (default: last element). + +Note: If using z_index for sorting, indices may shift after render. +Use name-based .find() for stable element access.

    +
    + +
    +
    remove(element)
    +

    Remove first occurrence of element. Raises ValueError if not found.

    @@ -1173,6 +1569,14 @@ Note:

    Returns: float: Dot product of the two vectors

    +
    +
    floor() -> Vector
    +

    Return a new vector with floored (integer) coordinates. + +Note:

    +

    Returns: Vector: New Vector with floor(x) and floor(y) Useful for grid-based positioning. For a hashable tuple, use the .int property instead.

    +
    +
    magnitude() -> float

    Calculate the length/magnitude of this vector.

    diff --git a/docs/mcrfpy.3 b/docs/mcrfpy.3 index 5c46cba..8e4953d 100644 --- a/docs/mcrfpy.3 +++ b/docs/mcrfpy.3 @@ -14,11 +14,11 @@ . ftr VB CB . ftr VBI CBI .\} -.TH "MCRFPY" "3" "2025-10-30" "McRogueFace dev" "" +.TH "MCRFPY" "3" "2025-11-29" "McRogueFace dev" "" .hy .SH McRogueFace API Reference .PP -\f[I]Generated on 2025-10-30 20:49:34\f[R] +\f[I]Generated on 2025-11-29 10:12:05\f[R] .PP \f[I]This documentation was dynamically generated from the compiled module.\f[R] @@ -31,10 +31,16 @@ Classes .IP \[bu] 2 Animation .IP \[bu] 2 +Arc +.IP \[bu] 2 Caption .IP \[bu] 2 +Circle +.IP \[bu] 2 Color .IP \[bu] 2 +ColorLayer +.IP \[bu] 2 Drawable .IP \[bu] 2 Entity @@ -51,12 +57,16 @@ GridPoint .IP \[bu] 2 GridPointState .IP \[bu] 2 +Line +.IP \[bu] 2 Scene .IP \[bu] 2 Sprite .IP \[bu] 2 Texture .IP \[bu] 2 +TileLayer +.IP \[bu] 2 Timer .IP \[bu] 2 UICollection @@ -110,6 +120,17 @@ Note: .PP \f[B]Returns:\f[R] None No error is raised if the timer doesn\[cq]t exist. +.SS \f[V]end_benchmark() -> str\f[R] +.PP +Stop benchmark capture and write data to JSON file. +.PP +Note: +.PP +\f[B]Returns:\f[R] str: The filename of the written benchmark data +.PP +\f[B]Raises:\f[R] RuntimeError: If no benchmark is currently running +Returns the auto-generated filename (e.g., +`benchmark_12345_20250528_143022.json') .SS \f[V]exit() -> None\f[R] .PP Cleanly shut down the game engine and exit the application. @@ -180,6 +201,19 @@ OGG, FLAC) .PP \f[B]Returns:\f[R] None Only one music track can play at a time. Loading new music stops the current track. +.SS \f[V]log_benchmark(message: str) -> None\f[R] +.PP +Add a log message to the current benchmark frame. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]message\f[R]: Text to associate with the +current frame +.PP +\f[B]Returns:\f[R] None +.PP +\f[B]Raises:\f[R] RuntimeError: If no benchmark is currently running +Messages appear in the `logs' array of each frame in the output JSON. .SS \f[V]playSound(buffer_id: int) -> None\f[R] .PP Play a sound effect using a previously loaded buffer. @@ -201,6 +235,18 @@ If None, uses current scene in the scene .PP \f[B]Raises:\f[R] KeyError: If the specified scene doesn\[cq]t exist +.SS \f[V]setDevConsole(enabled: bool) -> None\f[R] +.PP +Enable or disable the developer console overlay. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]enabled\f[R]: True to enable the console +(default), False to disable +.PP +\f[B]Returns:\f[R] None When disabled, the grave/tilde key will not open +the console. +Use this to ship games without debug features. .SS \f[V]setMusicVolume(volume: int) -> None\f[R] .PP Set the global music volume. @@ -255,6 +301,17 @@ Note: \f[B]Returns:\f[R] None If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument. +.SS \f[V]start_benchmark() -> None\f[R] +.PP +Start capturing benchmark data to a file. +.PP +Note: +.PP +\f[B]Returns:\f[R] None +.PP +\f[B]Raises:\f[R] RuntimeError: If a benchmark is already running +Benchmark filename is auto-generated from PID and timestamp. +Use end_benchmark() to stop and get filename. .SS Classes .SS Animation .PP @@ -316,6 +373,62 @@ update in seconds \f[B]Returns:\f[R] bool: True if animation is still running, False if complete Typically called by AnimationManager automatically. Manual calls only needed for custom animation control. +.SS Arc +.PP +\f[I]Inherits from: Drawable\f[R] +.PP +Arc(center=None, radius=0, start_angle=0, end_angle=90, color=None, +thickness=1, **kwargs) +.PP +An arc UI element for drawing curved line segments. +.PP +Args: center (tuple, optional): Center position as (x, y). +Default: (0, 0) radius (float, optional): Arc radius in pixels. +Default: 0 start_angle (float, optional): Starting angle in degrees. +Default: 0 end_angle (float, optional): Ending angle in degrees. +Default: 90 color (Color, optional): Arc color. +Default: White thickness (float, optional): Line thickness. +Default: 1.0 +.PP +Keyword Args: click (callable): Click handler. +Default: None visible (bool): Visibility state. +Default: True opacity (float): Opacity (0.0-1.0). +Default: 1.0 z_index (int): Rendering order. +Default: 0 name (str): Element name for finding. +Default: None +.PP +Attributes: center (Vector): Center position radius (float): Arc radius +start_angle (float): Starting angle in degrees end_angle (float): Ending +angle in degrees color (Color): Arc color thickness (float): Line +thickness visible (bool): Visibility state opacity (float): Opacity +value z_index (int): Rendering order name (str): Element name +.PP +\f[B]Methods:\f[R] +.SS \f[V]get_bounds() -> tuple\f[R] +.PP +Get the bounding rectangle of this drawable element. +.PP +Note: +.PP +\f[B]Returns:\f[R] tuple: (x, y, width, height) representing the +element\[cq]s bounds The bounds are in screen coordinates and account +for current position and size. +.SS \f[V]move(dx: float, dy: float) -> None\f[R] +.PP +Move the element by a relative offset. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]dx\f[R]: Horizontal offset in pixels - +\f[V]dy\f[R]: Vertical offset in pixels +.SS \f[V]resize(width: float, height: float) -> None\f[R] +.PP +Resize the element to new dimensions. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]width\f[R]: New width in pixels - +\f[V]height\f[R]: New height in pixels .SS Caption .PP \f[I]Inherits from: Drawable\f[R] @@ -378,6 +491,61 @@ Note: .PP \f[B]Arguments:\f[R] - \f[V]width\f[R]: New width in pixels - \f[V]height\f[R]: New height in pixels +.SS Circle +.PP +\f[I]Inherits from: Drawable\f[R] +.PP +Circle(radius=0, center=None, fill_color=None, outline_color=None, +outline=0, **kwargs) +.PP +A circle UI element for drawing filled or outlined circles. +.PP +Args: radius (float, optional): Circle radius in pixels. +Default: 0 center (tuple, optional): Center position as (x, y). +Default: (0, 0) fill_color (Color, optional): Fill color. +Default: White outline_color (Color, optional): Outline color. +Default: Transparent outline (float, optional): Outline thickness. +Default: 0 (no outline) +.PP +Keyword Args: click (callable): Click handler. +Default: None visible (bool): Visibility state. +Default: True opacity (float): Opacity (0.0-1.0). +Default: 1.0 z_index (int): Rendering order. +Default: 0 name (str): Element name for finding. +Default: None +.PP +Attributes: radius (float): Circle radius center (Vector): Center +position fill_color (Color): Fill color outline_color (Color): Outline +color outline (float): Outline thickness visible (bool): Visibility +state opacity (float): Opacity value z_index (int): Rendering order name +(str): Element name +.PP +\f[B]Methods:\f[R] +.SS \f[V]get_bounds() -> tuple\f[R] +.PP +Get the bounding rectangle of this drawable element. +.PP +Note: +.PP +\f[B]Returns:\f[R] tuple: (x, y, width, height) representing the +element\[cq]s bounds The bounds are in screen coordinates and account +for current position and size. +.SS \f[V]move(dx: float, dy: float) -> None\f[R] +.PP +Move the element by a relative offset. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]dx\f[R]: Horizontal offset in pixels - +\f[V]dy\f[R]: Vertical offset in pixels +.SS \f[V]resize(width: float, height: float) -> None\f[R] +.PP +Resize the element to new dimensions. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]width\f[R]: New width in pixels - +\f[V]height\f[R]: New height in pixels .SS Color .PP SFML Color Object @@ -419,6 +587,34 @@ Note: \f[B]Returns:\f[R] str: Hex string in format `#RRGGBB' or `#RRGGBBAA' (if alpha < 255) Alpha component is only included if not fully opaque (< 255) +.SS ColorLayer +.PP +ColorLayer(z_index=-1, grid_size=None) +.PP +A grid layer that stores RGBA colors per cell. +.PP +Args: z_index (int): Render order. +Negative = below entities. +Default: -1 grid_size (tuple): Dimensions as (width, height). +Default: parent grid size +.PP +Attributes: z_index (int): Layer z-order relative to entities visible +(bool): Whether layer is rendered grid_size (tuple): Layer dimensions +(read-only) +.PP +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 +.PP +\f[B]Methods:\f[R] +.SS \f[V]at(x, y) -> Color\f[R] +.PP +Get the color at cell position (x, y). +.SS \f[V]fill(color)\f[R] +.PP +Fill the entire layer with the specified color. +.SS \f[V]set(x, y, color)\f[R] +.PP +Set the color at cell position (x, y). .SS Drawable .PP Base class for all drawable UI elements @@ -531,11 +727,36 @@ perspective set. Iterable, indexable collection of Entities .PP \f[B]Methods:\f[R] -.SS \f[V]append(...)\f[R] -.SS \f[V]count(...)\f[R] -.SS \f[V]extend(...)\f[R] -.SS \f[V]index(...)\f[R] -.SS \f[V]remove(...)\f[R] +.SS \f[V]append(entity)\f[R] +.PP +Add an entity to the end of the collection. +.SS \f[V]count(entity) -> int\f[R] +.PP +Count occurrences of entity in the collection. +.SS \f[V]extend(iterable)\f[R] +.PP +Add all entities from an iterable to the collection. +.SS \f[V]find(name) -> entity or list\f[R] +.PP +Find entities by name. +.PP +\f[B]Returns:\f[R] Single entity if exact match, list if wildcard, None +if not found. +.SS \f[V]index(entity) -> int\f[R] +.PP +Return index of first occurrence of entity. +Raises ValueError if not found. +.SS \f[V]insert(index, entity)\f[R] +.PP +Insert entity at index. +Like list.insert(), indices past the end append. +.SS \f[V]pop([index]) -> entity\f[R] +.PP +Remove and return entity at index (default: last entity). +.SS \f[V]remove(entity)\f[R] +.PP +Remove first occurrence of entity. +Raises ValueError if not found. .SS Font .PP SFML Font Object @@ -568,6 +789,8 @@ Default: 0 w (float): Width override. Default: 0 h (float): Height override. Default: 0 clip_children (bool): Whether to clip children to frame bounds. +Default: False cache_subtree (bool): Cache rendering to texture for +performance. Default: False .PP Attributes: x, y (float): Position in pixels w, h (float): Size in @@ -577,7 +800,7 @@ thickness click (callable): Click event handler children (list): Collection of child drawable elements visible (bool): Visibility state opacity (float): Opacity value z_index (int): Rendering order name (str): Element name clip_children (bool): Whether to clip children to -frame bounds +frame bounds cache_subtree (bool): Cache subtree rendering to texture .PP \f[B]Methods:\f[R] .SS \f[V]get_bounds() -> tuple\f[R] @@ -653,6 +876,17 @@ visible (bool): Visibility state opacity (float): Opacity value z_index (int): Rendering order name (str): Element name .PP \f[B]Methods:\f[R] +.SS \f[V]add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer\f[R] +.PP +Add a new layer to the grid. +.PP +\f[B]Arguments:\f[R] - \f[V]type\f[R]: Layer type (`color' or `tile') - +\f[V]z_index\f[R]: Render order. +Negative = below entities, >= 0 = above entities. +Default: -1 - \f[V]texture\f[R]: Texture for tile layers. +Required for `tile' type. +.PP +\f[B]Returns:\f[R] The created ColorLayer or TileLayer object. .SS \f[V]at(...)\f[R] .SS \f[V]compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\f[R] .PP @@ -673,20 +907,15 @@ Compute Dijkstra map from root position. \f[B]Arguments:\f[R] - \f[V]root_x\f[R]: X coordinate of the root/target - \f[V]root_y\f[R]: Y coordinate of the root/target - \f[V]diagonal_cost\f[R]: Cost of diagonal movement (default: 1.41) -.SS \f[V]compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]\f[R] +.SS \f[V]compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\f[R] .PP -Compute field of view from a position and return visible cells. +Compute field of view from a position. .PP \f[B]Arguments:\f[R] - \f[V]x\f[R]: X coordinate of the viewer - \f[V]y\f[R]: Y coordinate of the viewer - \f[V]radius\f[R]: Maximum view distance (0 = unlimited) - \f[V]light_walls\f[R]: Whether walls are lit when visible - \f[V]algorithm\f[R]: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8) -.PP -\f[B]Returns:\f[R] List of tuples (x, y, visible, discovered) for all -visible cells: - x, y: Grid coordinates - visible: True (all returned -cells are visible) - discovered: True (FOV implies discovery) Also -updates the internal FOV state for use with is_in_fov(). .SS \f[V]find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\f[R] .PP Find A* path between two points. @@ -736,6 +965,15 @@ Y coordinate to check .PP \f[B]Returns:\f[R] True if the cell is visible, False otherwise Must call compute_fov() first to calculate visibility. +.SS \f[V]layer(z_index: int) -> ColorLayer | TileLayer | None\f[R] +.PP +Get a layer by its z_index. +.PP +\f[B]Arguments:\f[R] - \f[V]z_index\f[R]: The z_index of the layer to +find. +.PP +\f[B]Returns:\f[R] The layer with the specified z_index, or None if not +found. .SS \f[V]move(dx: float, dy: float) -> None\f[R] .PP Move the element by a relative offset. @@ -744,6 +982,11 @@ Note: .PP \f[B]Arguments:\f[R] - \f[V]dx\f[R]: Horizontal offset in pixels - \f[V]dy\f[R]: Vertical offset in pixels +.SS \f[V]remove_layer(layer: ColorLayer | TileLayer) -> None\f[R] +.PP +Remove a layer from the grid. +.PP +\f[B]Arguments:\f[R] - \f[V]layer\f[R]: The layer to remove. .SS \f[V]resize(width: float, height: float) -> None\f[R] .PP Resize the element to new dimensions. @@ -762,6 +1005,58 @@ UIGridPoint object UIGridPointState object .PP \f[B]Methods:\f[R] +.SS Line +.PP +\f[I]Inherits from: Drawable\f[R] +.PP +Line(start=None, end=None, thickness=1.0, color=None, **kwargs) +.PP +A line UI element for drawing straight lines between two points. +.PP +Args: start (tuple, optional): Starting point as (x, y). +Default: (0, 0) end (tuple, optional): Ending point as (x, y). +Default: (0, 0) thickness (float, optional): Line thickness in pixels. +Default: 1.0 color (Color, optional): Line color. +Default: White +.PP +Keyword Args: click (callable): Click handler. +Default: None visible (bool): Visibility state. +Default: True opacity (float): Opacity (0.0-1.0). +Default: 1.0 z_index (int): Rendering order. +Default: 0 name (str): Element name for finding. +Default: None +.PP +Attributes: start (Vector): Starting point end (Vector): Ending point +thickness (float): Line thickness color (Color): Line color visible +(bool): Visibility state opacity (float): Opacity value z_index (int): +Rendering order name (str): Element name +.PP +\f[B]Methods:\f[R] +.SS \f[V]get_bounds() -> tuple\f[R] +.PP +Get the bounding rectangle of this drawable element. +.PP +Note: +.PP +\f[B]Returns:\f[R] tuple: (x, y, width, height) representing the +element\[cq]s bounds The bounds are in screen coordinates and account +for current position and size. +.SS \f[V]move(dx: float, dy: float) -> None\f[R] +.PP +Move the element by a relative offset. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]dx\f[R]: Horizontal offset in pixels - +\f[V]dy\f[R]: Vertical offset in pixels +.SS \f[V]resize(width: float, height: float) -> None\f[R] +.PP +Resize the element to new dimensions. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]width\f[R]: New width in pixels - +\f[V]height\f[R]: New height in pixels .SS Scene .PP Base class for object-oriented scenes @@ -864,6 +1159,38 @@ Note: SFML Texture Object .PP \f[B]Methods:\f[R] +.SS TileLayer +.PP +TileLayer(z_index=-1, texture=None, grid_size=None) +.PP +A grid layer that stores sprite indices per cell. +.PP +Args: z_index (int): Render order. +Negative = below entities. +Default: -1 texture (Texture): Sprite atlas for tile rendering. +Default: None grid_size (tuple): Dimensions as (width, height). +Default: parent grid size +.PP +Attributes: z_index (int): Layer z-order relative to entities visible +(bool): Whether layer is rendered texture (Texture): Tile sprite atlas +grid_size (tuple): Layer dimensions (read-only) +.PP +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 +.PP +\f[B]Methods:\f[R] +.SS \f[V]at(x, y) -> int\f[R] +.PP +Get the tile index at cell position (x, y). +Returns -1 if no tile. +.SS \f[V]fill(index)\f[R] +.PP +Fill the entire layer with the specified tile index. +.SS \f[V]set(x, y, index)\f[R] +.PP +Set the tile index at cell position (x, y). +Use -1 for no tile. .SS Timer .PP Timer(name, callback, interval, once=False) @@ -937,11 +1264,43 @@ Timer will fire after the remaining time elapses. Iterable, indexable collection of UI objects .PP \f[B]Methods:\f[R] -.SS \f[V]append(...)\f[R] -.SS \f[V]count(...)\f[R] -.SS \f[V]extend(...)\f[R] -.SS \f[V]index(...)\f[R] -.SS \f[V]remove(...)\f[R] +.SS \f[V]append(element)\f[R] +.PP +Add an element to the end of the collection. +.SS \f[V]count(element) -> int\f[R] +.PP +Count occurrences of element in the collection. +.SS \f[V]extend(iterable)\f[R] +.PP +Add all elements from an iterable to the collection. +.SS \f[V]find(name, recursive=False) -> element or list\f[R] +.PP +Find elements by name. +.PP +\f[B]Returns:\f[R] Single element if exact match, list if wildcard, None +if not found. +.SS \f[V]index(element) -> int\f[R] +.PP +Return index of first occurrence of element. +Raises ValueError if not found. +.SS \f[V]insert(index, element)\f[R] +.PP +Insert element at index. +Like list.insert(), indices past the end append. +.PP +Note: If using z_index for sorting, insertion order may not persist +after the next render. +Use name-based .find() for stable element access. +.SS \f[V]pop([index]) -> element\f[R] +.PP +Remove and return element at index (default: last element). +.PP +Note: If using z_index for sorting, indices may shift after render. +Use name-based .find() for stable element access. +.SS \f[V]remove(element)\f[R] +.PP +Remove first occurrence of element. +Raises ValueError if not found. .SS UICollectionIter .PP Iterator for a collection of UI objects @@ -981,6 +1340,15 @@ Calculate the dot product with another vector. \f[B]Arguments:\f[R] - \f[V]other\f[R]: The other vector .PP \f[B]Returns:\f[R] float: Dot product of the two vectors +.SS \f[V]floor() -> Vector\f[R] +.PP +Return a new vector with floored (integer) coordinates. +.PP +Note: +.PP +\f[B]Returns:\f[R] Vector: New Vector with floor(x) and floor(y) Useful +for grid-based positioning. +For a hashable tuple, use the .int property instead. .SS \f[V]magnitude() -> float\f[R] .PP Calculate the length/magnitude of this vector. diff --git a/src/PyScene.cpp b/src/PyScene.cpp index 3288cd4..57806bd 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -61,7 +61,7 @@ void PyScene::do_mouse_input(std::string button, std::string type) void PyScene::doAction(std::string name, std::string type) { - if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) { + if (name.compare("left") == 0 || name.compare("right") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) { do_mouse_input(name, type); } else if ACTIONONCE("debug_menu") { diff --git a/stubs/mcrfpy.pyi b/stubs/mcrfpy.pyi index 919794b..b6654fa 100644 --- a/stubs/mcrfpy.pyi +++ b/stubs/mcrfpy.pyi @@ -96,98 +96,159 @@ class Drawable: ... class Frame(Drawable): - """Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None) - + """Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, on_click=None, children=None) + A rectangular frame UI element that can contain other drawable elements. """ - + @overload def __init__(self) -> None: ... @overload def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0, fill_color: Optional[Color] = None, outline_color: Optional[Color] = None, - outline: float = 0, click: Optional[Callable] = None, + outline: float = 0, on_click: Optional[Callable] = None, children: Optional[List[UIElement]] = None) -> None: ... - + w: float h: float fill_color: Color outline_color: Color outline: float - click: Optional[Callable[[float, float, int], None]] + on_click: Optional[Callable[[float, float, int], None]] + on_enter: Optional[Callable[[], None]] + on_exit: Optional[Callable[[], None]] + on_move: Optional[Callable[[float, float], None]] children: 'UICollection' clip_children: bool class Caption(Drawable): - """Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None) - + """Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, on_click=None) + A text display UI element with customizable font and styling. """ - + @overload def __init__(self) -> None: ... @overload def __init__(self, text: str = '', x: float = 0, y: float = 0, font: Optional[Font] = None, fill_color: Optional[Color] = None, outline_color: Optional[Color] = None, outline: float = 0, - click: Optional[Callable] = None) -> None: ... - + on_click: Optional[Callable] = None) -> None: ... + text: str font: Font fill_color: Color outline_color: Color outline: float - click: Optional[Callable[[float, float, int], None]] + on_click: Optional[Callable[[float, float, int], None]] + on_enter: Optional[Callable[[], None]] + on_exit: Optional[Callable[[], None]] + on_move: Optional[Callable[[float, float], None]] w: float # Read-only, computed from text h: float # Read-only, computed from text class Sprite(Drawable): - """Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None) - + """Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, on_click=None) + A sprite UI element that displays a texture or portion of a texture atlas. """ - + @overload def __init__(self) -> None: ... @overload def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None, sprite_index: int = 0, scale: float = 1.0, - click: Optional[Callable] = None) -> None: ... - + on_click: Optional[Callable] = None) -> None: ... + texture: Texture sprite_index: int scale: float - click: Optional[Callable[[float, float, int], None]] + on_click: Optional[Callable[[float, float, int], None]] + on_enter: Optional[Callable[[], None]] + on_exit: Optional[Callable[[], None]] + on_move: Optional[Callable[[float, float], None]] w: float # Read-only, computed from texture h: float # Read-only, computed from texture class Grid(Drawable): - """Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None) - + """Grid(pos=None, size=None, grid_size=(20, 20), texture=None, on_click=None, layers=None) + A grid-based tilemap UI element for rendering tile-based levels and game worlds. + Supports multiple rendering layers (ColorLayer, TileLayer) and entity management. """ - + @overload def __init__(self) -> None: ... @overload - def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20), - texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16, - scale: float = 1.0, click: Optional[Callable] = None) -> None: ... - + def __init__(self, pos: Optional[Tuple[float, float]] = None, + size: Optional[Tuple[float, float]] = None, + grid_size: Tuple[int, int] = (20, 20), + texture: Optional[Texture] = None, + on_click: Optional[Callable] = None, + layers: Optional[Dict[str, str]] = None) -> None: ... + grid_size: Tuple[int, int] - tile_width: int - tile_height: int + grid_x: int # Read-only grid width + grid_y: int # Read-only grid height texture: Texture - scale: float - points: List[List['GridPoint']] + zoom: float + center: Tuple[float, float] + center_x: float + center_y: float + fill_color: Color entities: 'EntityCollection' - background_color: Color - click: Optional[Callable[[int, int, int], None]] - + children: 'UICollection' + layers: List[Union['ColorLayer', 'TileLayer']] + hovered_cell: Optional[Tuple[int, int]] + + # Mouse event handlers + on_click: Optional[Callable[[float, float, int], None]] + on_enter: Optional[Callable[[], None]] + on_exit: Optional[Callable[[], None]] + on_move: Optional[Callable[[float, float], None]] + + # Grid cell event handlers + on_cell_click: Optional[Callable[[int, int], None]] + on_cell_enter: Optional[Callable[[int, int], None]] + on_cell_exit: Optional[Callable[[int, int], None]] + def at(self, x: int, y: int) -> 'GridPoint': """Get grid point at tile coordinates.""" ... + def add_layer(self, type: str, z_index: int = -1, + texture: Optional[Texture] = None) -> Union['ColorLayer', 'TileLayer']: + """Add a rendering layer. type='color' or 'tile'. z_index<0 = below entities.""" + ... + + def remove_layer(self, layer: Union['ColorLayer', 'TileLayer']) -> None: + """Remove a layer from the grid.""" + ... + + def compute_fov(self, x: int, y: int, radius: int) -> None: + """Compute field of view from a position.""" + ... + + def is_in_fov(self, x: int, y: int) -> bool: + """Check if a cell is visible in the current FOV.""" + ... + + def compute_dijkstra(self, sources: List[Tuple[int, int]], max_cost: float = -1) -> None: + """Compute Dijkstra distance map from source points.""" + ... + + def get_dijkstra_distance(self, x: int, y: int) -> float: + """Get distance to nearest source for a cell.""" + ... + + def get_dijkstra_path(self, x: int, y: int) -> List[Tuple[int, int]]: + """Get path from cell to nearest source.""" + ... + + def find_path(self, x1: int, y1: int, x2: int, y2: int) -> List[Tuple[int, int]]: + """Find A* path between two cells.""" + ... + class GridPoint: """Grid point representing a single tile.""" @@ -197,10 +258,49 @@ class GridPoint: class GridPointState: """State information for a grid point.""" - + texture_index: int color: Color +class ColorLayer: + """Grid layer that renders solid colors per cell.""" + + z_index: int + visible: bool + grid_size: Tuple[int, int] + + def fill(self, color: Color) -> None: + """Fill all cells with a color.""" + ... + + def set(self, x: int, y: int, color: Color) -> None: + """Set color at a specific cell.""" + ... + + def at(self, x: int, y: int) -> Color: + """Get color at a specific cell.""" + ... + +class TileLayer: + """Grid layer that renders texture tiles per cell.""" + + z_index: int + visible: bool + texture: Texture + grid_size: Tuple[int, int] + + def fill(self, sprite_index: int) -> None: + """Fill all cells with a sprite index.""" + ... + + def set(self, x: int, y: int, sprite_index: int) -> None: + """Set tile sprite at a specific cell.""" + ... + + def at(self, x: int, y: int) -> int: + """Get tile sprite index at a specific cell.""" + ... + class Entity(Drawable): """Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='') diff --git a/tests/demo/screens/grid_demo.py b/tests/demo/screens/grid_demo.py index 1797885..434c139 100644 --- a/tests/demo/screens/grid_demo.py +++ b/tests/demo/screens/grid_demo.py @@ -8,29 +8,33 @@ class GridDemo(DemoScreen): def setup(self): self.add_title("Grid System") - self.add_description("Tile-based rendering with camera, zoom, and children support") + self.add_description("Multi-layer rendering with camera, zoom, and children support") - # Create a grid - grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 120), size=(400, 280)) + # Create a grid with no default layers + grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 120), size=(400, 280), layers={}) grid.fill_color = mcrfpy.Color(20, 20, 40) + + # Add a color layer for the checkerboard pattern (z_index=-1 = below entities) + color_layer = grid.add_layer("color", z_index=-1) + # Center camera on middle of grid (in pixel coordinates: cells * cell_size / 2) # For 15x10 grid with 16x16 cells: center = (15*16/2, 10*16/2) = (120, 80) grid.center = (120, 80) self.ui.append(grid) - # Set some tile colors to create a pattern + # Set tile colors via the color layer to create a pattern for x in range(15): for y in range(10): point = grid.at(x, y) # Checkerboard pattern if (x + y) % 2 == 0: - point.color = mcrfpy.Color(40, 40, 60) + color_layer.set(x, y, mcrfpy.Color(40, 40, 60)) else: - point.color = mcrfpy.Color(30, 30, 50) + color_layer.set(x, y, mcrfpy.Color(30, 30, 50)) # Border if x == 0 or x == 14 or y == 0 or y == 9: - point.color = mcrfpy.Color(80, 60, 40) + color_layer.set(x, y, mcrfpy.Color(80, 60, 40)) point.walkable = False # Add some children to the grid @@ -53,13 +57,12 @@ class GridDemo(DemoScreen): props = [ "grid_size: (15, 10)", - "zoom: 1.0", + "layers: [ColorLayer]", "center: (120, 80)", - "fill_color: dark blue", "", "Features:", + "- Multi-layer rendering", "- Camera pan/zoom", - "- Tile colors", "- Children collection", "- FOV/pathfinding", ] @@ -69,8 +72,9 @@ class GridDemo(DemoScreen): info.children.append(cap) # Code example - code = """# Grid with children -grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240)) -grid.at(5, 5).color = mcrfpy.Color(255, 0, 0) # Red tile + code = """# Grid with layers +grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240), layers={}) +layer = grid.add_layer("color", z_index=-1) # Below entities +layer.set(5, 5, mcrfpy.Color(255, 0, 0)) # Red tile grid.children.append(mcrfpy.Caption("Label", pos=(80, 48)))""" self.add_code_example(code, y=420) diff --git a/tests/demo/screenshots/demo_03_grid_system.png b/tests/demo/screenshots/demo_03_grid_system.png index 1488001..3f24695 100644 Binary files a/tests/demo/screenshots/demo_03_grid_system.png and b/tests/demo/screenshots/demo_03_grid_system.png differ diff --git a/tests/demo/screenshots/demo_04_animation_system.png b/tests/demo/screenshots/demo_04_animation_system.png index 08bb883..123d05c 100644 Binary files a/tests/demo/screenshots/demo_04_animation_system.png and b/tests/demo/screenshots/demo_04_animation_system.png differ diff --git a/tests/vllm_demo/0_basic_vllm_demo.py b/tests/vllm_demo/0_basic_vllm_demo.py new file mode 100644 index 0000000..bf37fa4 --- /dev/null +++ b/tests/vllm_demo/0_basic_vllm_demo.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +""" +VLLM Integration Demo for McRogueFace +===================================== + +Demonstrates using a local Vision-Language Model (Gemma 3) with +McRogueFace headless rendering to create an AI-driven agent. + +Requirements: +- Local VLLM running at http://192.168.1.100:8100 +- McRogueFace built with headless mode support + +This is a research-grade demo for issue #156. +""" + +import mcrfpy +from mcrfpy import automation +import sys +import requests +import base64 +import os +import random + +# VLLM configuration +VLLM_URL = "http://192.168.1.100:8100/v1/chat/completions" +SCREENSHOT_PATH = "/tmp/vllm_demo_screenshot.png" + +# Sprite constants from Crypt of Sokoban tileset +FLOOR_COMMON = 0 # 95% of floors +FLOOR_SPECKLE1 = 12 # 4% of floors +FLOOR_SPECKLE2 = 24 # 1% of floors +WALL_TILE = 40 # Wall sprite +PLAYER_SPRITE = 84 # Player character +RAT_SPRITE = 123 # Enemy/rat creature + +def file_to_base64(file_path): + """Convert any image file to base64 string.""" + with open(file_path, 'rb') as f: + return base64.b64encode(f.read()).decode('utf-8') + +def llm_chat_completion(messages: list): + """Chat completion endpoint of local LLM""" + try: + response = requests.post(VLLM_URL, json={'messages': messages}, timeout=60) + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + +def message_with_image(text, image_path): + """Create a message with an embedded image for vision models.""" + image_data = file_to_base64(image_path) + return { + "role": "user", + "content": [ + {"type": "text", "text": text}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64," + image_data}} + ] + } + +def get_floor_tile(): + """Return a floor tile sprite with realistic distribution.""" + roll = random.random() + if roll < 0.95: + return FLOOR_COMMON + elif roll < 0.99: + return FLOOR_SPECKLE1 + else: + return FLOOR_SPECKLE2 + +def setup_scene(): + """Create a dungeon scene with player agent and NPC rat.""" + print("Setting up scene...") + + # Create and set scene + mcrfpy.createScene("vllm_demo") + mcrfpy.setScene("vllm_demo") + ui = mcrfpy.sceneUI("vllm_demo") + + # Load the game texture (16x16 tiles from Crypt of Sokoban) + texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) + + # Create grid: 1014px wide at position (5,5) + # Using 20x15 grid for a reasonable dungeon size + grid = mcrfpy.Grid( + grid_size=(20, 15), + texture=texture, + pos=(5, 5), + size=(1014, 700) + ) + grid.fill_color = mcrfpy.Color(20, 20, 30) + + # Set zoom factor to 2.0 for better visibility + grid.zoom = 2.0 + + ui.append(grid) + + # Set up floor tiles and walls with proper sprite distribution + for x in range(20): + for y in range(15): + point = grid.at(x, y) + # Create walls around the edges + if x == 0 or x == 19 or y == 0 or y == 14: + point.tilesprite = WALL_TILE + point.walkable = False + point.transparent = False # Walls block FOV + else: + # Floor inside with varied sprites + point.tilesprite = get_floor_tile() + point.walkable = True + point.transparent = True # Floors don't block FOV + + # Add some interior walls for interest - a room divider + for y in range(5, 10): + point = grid.at(10, y) + point.tilesprite = WALL_TILE + point.walkable = False + point.transparent = False + # Door opening + door = grid.at(10, 7) + door.tilesprite = get_floor_tile() + door.walkable = True + door.transparent = True + + # Create a ColorLayer for fog of war (z_index=10 to render on top) + fov_layer = grid.add_layer('color', z_index=10) + fov_layer.fill(mcrfpy.Color(0, 0, 0, 255)) # Start all black (unknown) + + # Create the player entity ("The Agent") + player = mcrfpy.Entity(grid_pos=(5, 7), texture=texture, sprite_index=PLAYER_SPRITE) + grid.entities.append(player) + + # Create an NPC rat entity (closer so it's visible in FOV) + rat = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=RAT_SPRITE) + grid.entities.append(rat) + + # Bind the fog layer to player's perspective + # visible = transparent, discovered = dim, unknown = black + fov_layer.apply_perspective( + entity=player, + visible=mcrfpy.Color(0, 0, 0, 0), # Transparent when visible + discovered=mcrfpy.Color(40, 40, 60, 180), # Dark overlay when discovered but not visible + unknown=mcrfpy.Color(0, 0, 0, 255) # Black when never seen + ) + + # Update visibility from player's position + player.update_visibility() + + # Center the camera on the agent entity + px, py = int(player.pos[0]), int(player.pos[1]) + grid.center = (px * 16 + 8, py * 16 + 8) + + return grid, player, rat + +def check_entity_visible(grid, entity): + """Check if an entity is within the current FOV.""" + ex, ey = int(entity.pos[0]), int(entity.pos[1]) + return grid.is_in_fov(ex, ey) + +def build_grounded_prompt(grid, player, rat): + """Build a text prompt with visually grounded information.""" + observations = [] + + # Check what the agent can see + if check_entity_visible(grid, rat): + observations.append("You see a rat to the east.") + + # Could add more observations here: + # - walls blocking path + # - items on ground + # - doors/exits + + if not observations: + observations.append("The area appears clear.") + + return " ".join(observations) + +def run_demo(): + """Main demo function.""" + print("=" * 60) + print("VLLM Integration Demo (Research Mode)") + print("=" * 60) + print() + + # Setup the scene + grid, player, rat = setup_scene() + + # Advance simulation to ensure scene is ready + mcrfpy.step(0.016) + + # Take screenshot + print(f"Taking screenshot: {SCREENSHOT_PATH}") + result = automation.screenshot(SCREENSHOT_PATH) + if not result: + print("ERROR: Failed to take screenshot") + return False + + file_size = os.path.getsize(SCREENSHOT_PATH) + print(f"Screenshot saved: {file_size} bytes") + print() + + # Build grounded observations + grounded_text = build_grounded_prompt(grid, player, rat) + print(f"Grounded observations: {grounded_text}") + print() + + # Query 1: Ask VLLM to describe what it sees + print("-" * 40) + print("Query 1: Describe what you see") + print("-" * 40) + + system_prompt = """You are an AI agent in a roguelike dungeon game. You can see the game world through screenshots. +The view shows a top-down grid-based dungeon with tiles, walls, and creatures. +Your character is the humanoid figure. The dark areas are outside your field of vision. +Other creatures may be enemies or NPCs. Describe what you observe concisely.""" + + user_prompt = f"""Look at this game screenshot. {grounded_text} + +Describe what you see in the dungeon from your character's perspective. +Be specific about: +- Your position in the room +- Any creatures you can see +- The layout of walls and passages +- Areas obscured by fog of war (darkness)""" + + messages = [ + {"role": "system", "content": system_prompt}, + message_with_image(user_prompt, SCREENSHOT_PATH) + ] + + resp = llm_chat_completion(messages) + + if "error" in resp: + print(f"VLLM Error: {resp['error']}") + print("\nNote: The VLLM server may not be running or accessible.") + print("Screenshot is saved for manual inspection.") + description = "I can see a dungeon scene." + else: + description = resp.get('choices', [{}])[0].get('message', {}).get('content', 'No response') + print(f"\nVLLM Response:\n{description}") + print() + + # Query 2: Ask what action the agent would like to take + print("-" * 40) + print("Query 2: What would you like to do?") + print("-" * 40) + + messages.append({"role": "assistant", "content": description}) + messages.append({ + "role": "user", + "content": f"""Based on what you see, what action would you like to take? + +Available actions: +- GO NORTH / SOUTH / EAST / WEST - move in that direction +- WAIT - stay in place and observe +- LOOK - examine your surroundings more carefully + +{grounded_text} + +State your reasoning briefly, then declare your action clearly (e.g., "Action: GO EAST").""" + }) + + resp = llm_chat_completion(messages) + + if "error" in resp: + print(f"VLLM Error: {resp['error']}") + else: + action = resp.get('choices', [{}])[0].get('message', {}).get('content', 'No response') + print(f"\nVLLM Response:\n{action}") + print() + + print("=" * 60) + print("Demo Complete") + print("=" * 60) + print(f"\nScreenshot preserved at: {SCREENSHOT_PATH}") + print("Grid settings: zoom=2.0, FOV radius=8, perspective rendering enabled") + + return True + +# Main execution +if __name__ == "__main__": + try: + success = run_demo() + if success: + print("\nPASS") + sys.exit(0) + else: + print("\nFAIL") + sys.exit(1) + except Exception as e: + print(f"\nError: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/vllm_demo/1_multi_agent_demo.py b/tests/vllm_demo/1_multi_agent_demo.py new file mode 100644 index 0000000..50e06fb --- /dev/null +++ b/tests/vllm_demo/1_multi_agent_demo.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 +""" +Multi-Agent VLLM Demo for McRogueFace +===================================== + +Demonstrates cycling through multiple agent perspectives, +each with their own FOV and grounded observations. + +Three agents: +- Wizard (left side) - can see the rat but not the other agents +- Blacksmith (right side) - can see the knight, rat, and the wall +- Knight (right side) - can see the blacksmith, rat, and the wall + +Each agent gets their own screenshot and VLLM query. +""" + +import mcrfpy +from mcrfpy import automation +import sys +import requests +import base64 +import os +import random + +# VLLM configuration +VLLM_URL = "http://192.168.1.100:8100/v1/chat/completions" +SCREENSHOT_DIR = "/tmp/vllm_multi_agent" + +# Sprite constants +FLOOR_COMMON = 0 +FLOOR_SPECKLE1 = 12 +FLOOR_SPECKLE2 = 24 +WALL_TILE = 40 + +# Agent sprites +WIZARD_SPRITE = 84 +BLACKSMITH_SPRITE = 86 +KNIGHT_SPRITE = 96 +RAT_SPRITE = 123 + + +def file_to_base64(file_path): + """Convert any image file to base64 string.""" + with open(file_path, 'rb') as f: + return base64.b64encode(f.read()).decode('utf-8') + + +def llm_chat_completion(messages: list): + """Chat completion endpoint of local LLM""" + try: + response = requests.post(VLLM_URL, json={'messages': messages}, timeout=60) + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + +def message_with_image(text, image_path): + """Create a message with an embedded image for vision models.""" + image_data = file_to_base64(image_path) + return { + "role": "user", + "content": [ + {"type": "text", "text": text}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64," + image_data}} + ] + } + + +def get_floor_tile(): + """Return a floor tile sprite with realistic distribution.""" + roll = random.random() + if roll < 0.95: + return FLOOR_COMMON + elif roll < 0.99: + return FLOOR_SPECKLE1 + else: + return FLOOR_SPECKLE2 + + +class Agent: + """Wrapper for an agent entity with metadata.""" + def __init__(self, name, entity, description): + self.name = name + self.entity = entity + self.description = description # e.g., "a wizard", "a blacksmith" + + @property + def pos(self): + return (int(self.entity.pos[0]), int(self.entity.pos[1])) + + +def setup_scene(): + """Create a dungeon scene with multiple agents.""" + print("Setting up multi-agent scene...") + + # Create and set scene + mcrfpy.createScene("multi_agent_demo") + mcrfpy.setScene("multi_agent_demo") + ui = mcrfpy.sceneUI("multi_agent_demo") + + # Load the game texture + texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) + + # Create grid + grid = mcrfpy.Grid( + grid_size=(25, 15), + texture=texture, + pos=(5, 5), + size=(1014, 700) + ) + grid.fill_color = mcrfpy.Color(20, 20, 30) + grid.zoom = 2.0 + ui.append(grid) + + # Set up floor tiles and walls + for x in range(25): + for y in range(15): + point = grid.at(x, y) + if x == 0 or x == 24 or y == 0 or y == 14: + point.tilesprite = WALL_TILE + point.walkable = False + point.transparent = False + else: + point.tilesprite = get_floor_tile() + point.walkable = True + point.transparent = True + + # Add a wall divider in the middle (blocks wizard's view of right side) + for y in range(3, 12): + point = grid.at(10, y) + point.tilesprite = WALL_TILE + point.walkable = False + point.transparent = False + + # Door opening in the wall + door = grid.at(10, 7) + door.tilesprite = get_floor_tile() + door.walkable = True + door.transparent = True + + # Create FOV layer for fog of war + fov_layer = grid.add_layer('color', z_index=10) + fov_layer.fill(mcrfpy.Color(0, 0, 0, 255)) + + # Create agents + agents = [] + + # Wizard on the left side + wizard_entity = mcrfpy.Entity(grid_pos=(4, 7), texture=texture, sprite_index=WIZARD_SPRITE) + grid.entities.append(wizard_entity) + agents.append(Agent("Wizard", wizard_entity, "a wizard")) + + # Blacksmith on the right side (upper) + blacksmith_entity = mcrfpy.Entity(grid_pos=(18, 5), texture=texture, sprite_index=BLACKSMITH_SPRITE) + grid.entities.append(blacksmith_entity) + agents.append(Agent("Blacksmith", blacksmith_entity, "a blacksmith")) + + # Knight on the right side (lower) + knight_entity = mcrfpy.Entity(grid_pos=(18, 10), texture=texture, sprite_index=KNIGHT_SPRITE) + grid.entities.append(knight_entity) + agents.append(Agent("Knight", knight_entity, "a knight")) + + # Rat in the middle-right area (visible to blacksmith and knight, maybe wizard through door) + rat_entity = mcrfpy.Entity(grid_pos=(14, 7), texture=texture, sprite_index=RAT_SPRITE) + grid.entities.append(rat_entity) + + return grid, fov_layer, agents, rat_entity + + +def switch_perspective(grid, fov_layer, agent): + """Switch the grid view to an agent's perspective.""" + # Reset fog layer to all unknown (black) before switching + # This prevents discovered tiles from one agent carrying over to another + fov_layer.fill(mcrfpy.Color(0, 0, 0, 255)) + + # Apply this agent's perspective + fov_layer.apply_perspective( + entity=agent.entity, + visible=mcrfpy.Color(0, 0, 0, 0), + discovered=mcrfpy.Color(40, 40, 60, 180), + unknown=mcrfpy.Color(0, 0, 0, 255) + ) + + # Update visibility from agent's position + agent.entity.update_visibility() + + # Center camera on this agent + px, py = agent.pos + grid.center = (px * 16 + 8, py * 16 + 8) + + +def get_visible_entities(grid, observer, all_agents, rat): + """Get list of entities visible to the observer.""" + visible = [] + ox, oy = observer.pos + + # Check rat visibility + rx, ry = int(rat.pos[0]), int(rat.pos[1]) + if grid.is_in_fov(rx, ry): + # Determine direction + direction = get_direction(ox, oy, rx, ry) + visible.append(f"a rat to the {direction}") + + # Check other agents + for agent in all_agents: + if agent.name == observer.name: + continue + ax, ay = agent.pos + if grid.is_in_fov(ax, ay): + direction = get_direction(ox, oy, ax, ay) + visible.append(f"{agent.description} to the {direction}") + + return visible + + +def get_direction(from_x, from_y, to_x, to_y): + """Get cardinal direction from one point to another.""" + dx = to_x - from_x + dy = to_y - from_y + + # Primary direction + if abs(dx) > abs(dy): + return "east" if dx > 0 else "west" + elif abs(dy) > abs(dx): + return "south" if dy > 0 else "north" + else: + # Diagonal - pick one + ns = "south" if dy > 0 else "north" + ew = "east" if dx > 0 else "west" + return f"{ns}{ew}" + + +def build_grounded_prompt(visible_entities): + """Build grounded text from visible entities.""" + if not visible_entities: + return "The area appears clear." + + if len(visible_entities) == 1: + return f"You see {visible_entities[0]}." + else: + items = ", ".join(visible_entities[:-1]) + f" and {visible_entities[-1]}" + return f"You see {items}." + + +def query_agent(agent, screenshot_path, grounded_text): + """Query VLLM for a single agent's perspective.""" + system_prompt = f"""You are {agent.description} in a roguelike dungeon game. You can see the game world through screenshots. +The view shows a top-down grid-based dungeon. Your character is centered in the view. +The dark areas are outside your field of vision. Other figures may be allies, enemies, or NPCs. +Describe what you observe concisely and decide on an action.""" + + user_prompt = f"""Look at this game screenshot from your perspective as {agent.description}. {grounded_text} + +Describe what you see briefly, then choose an action: +- GO NORTH / SOUTH / EAST / WEST +- WAIT +- LOOK + +State your reasoning in 1-2 sentences, then declare: "Action: " """ + + messages = [ + {"role": "system", "content": system_prompt}, + message_with_image(user_prompt, screenshot_path) + ] + + resp = llm_chat_completion(messages) + + if "error" in resp: + return f"VLLM Error: {resp['error']}" + else: + return resp.get('choices', [{}])[0].get('message', {}).get('content', 'No response') + + +def run_demo(): + """Main demo function.""" + print("=" * 70) + print("Multi-Agent VLLM Demo") + print("=" * 70) + print() + + # Create screenshot directory + os.makedirs(SCREENSHOT_DIR, exist_ok=True) + + # Setup scene + grid, fov_layer, agents, rat = setup_scene() + + # Cycle through each agent's perspective + for i, agent in enumerate(agents): + print(f"\n{'='*70}") + print(f"Agent {i+1}/3: {agent.name} ({agent.description})") + print(f"Position: {agent.pos}") + print("=" * 70) + + # Switch to this agent's perspective + switch_perspective(grid, fov_layer, agent) + + # Advance simulation + mcrfpy.step(0.016) + + # Take screenshot + screenshot_path = os.path.join(SCREENSHOT_DIR, f"{i}_{agent.name.lower()}_view.png") + result = automation.screenshot(screenshot_path) + if not result: + print(f"ERROR: Failed to take screenshot for {agent.name}") + continue + + file_size = os.path.getsize(screenshot_path) + print(f"Screenshot: {screenshot_path} ({file_size} bytes)") + + # Get visible entities for this agent + visible = get_visible_entities(grid, agent, agents, rat) + grounded_text = build_grounded_prompt(visible) + print(f"Grounded observations: {grounded_text}") + + # Query VLLM + print(f"\nQuerying VLLM for {agent.name}...") + print("-" * 50) + response = query_agent(agent, screenshot_path, grounded_text) + print(f"\n{agent.name}'s Response:\n{response}") + print() + + print("\n" + "=" * 70) + print("Multi-Agent Demo Complete") + print("=" * 70) + print(f"\nScreenshots saved to: {SCREENSHOT_DIR}/") + for i, agent in enumerate(agents): + print(f" - {i}_{agent.name.lower()}_view.png") + + return True + + +# Main execution +if __name__ == "__main__": + try: + success = run_demo() + if success: + print("\nPASS") + sys.exit(0) + else: + print("\nFAIL") + sys.exit(1) + except Exception as e: + print(f"\nError: {e}") + import traceback + traceback.print_exc() + sys.exit(1)