diff --git a/docs/tutorials/part_00_setup/part_00_setup.py b/docs/tutorials/part_00_setup/part_00_setup.py
deleted file mode 100644
index f90eed9..0000000
--- a/docs/tutorials/part_00_setup/part_00_setup.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""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.
\ No newline at end of file
diff --git a/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py b/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
deleted file mode 100644
index 53c236e..0000000
--- a/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
+++ /dev/null
@@ -1,121 +0,0 @@
-"""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.")
\ No newline at end of file
diff --git a/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py b/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py
deleted file mode 100644
index 66feaa4..0000000
--- a/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py
+++ /dev/null
@@ -1,206 +0,0 @@
-"""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.")
\ No newline at end of file
diff --git a/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py b/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py
deleted file mode 100644
index 632ad2f..0000000
--- a/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py
+++ /dev/null
@@ -1,356 +0,0 @@
-"""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.")
\ No newline at end of file
diff --git a/docs/tutorials/part_04_fov/part_04_fov.py b/docs/tutorials/part_04_fov/part_04_fov.py
deleted file mode 100644
index 97d9187..0000000
--- a/docs/tutorials/part_04_fov/part_04_fov.py
+++ /dev/null
@@ -1,363 +0,0 @@
-"""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!")
\ No newline at end of file
diff --git a/docs/tutorials/part_05_enemies/part_05_enemies.py b/docs/tutorials/part_05_enemies/part_05_enemies.py
deleted file mode 100644
index 9abfc42..0000000
--- a/docs/tutorials/part_05_enemies/part_05_enemies.py
+++ /dev/null
@@ -1,685 +0,0 @@
-"""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...")
\ No newline at end of file
diff --git a/docs/tutorials/part_06_combat/part_06_combat.py b/docs/tutorials/part_06_combat/part_06_combat.py
deleted file mode 100644
index 59d6ab2..0000000
--- a/docs/tutorials/part_06_combat/part_06_combat.py
+++ /dev/null
@@ -1,940 +0,0 @@
-"""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!")
\ No newline at end of file
diff --git a/docs/tutorials/part_07_ui/part_07_ui.py b/docs/tutorials/part_07_ui/part_07_ui.py
deleted file mode 100644
index 459adee..0000000
--- a/docs/tutorials/part_07_ui/part_07_ui.py
+++ /dev/null
@@ -1,1035 +0,0 @@
-"""McRogueFace - Part 7: User Interface
-
-Documentation: https://mcrogueface.github.io/tutorial/part_07_ui
-Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_07_ui/part_07_ui.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 = 30
-
-# 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 colors
-COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200)
-COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150)
-COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50)
-COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100)
-COLOR_HEAL = mcrfpy.Color(100, 255, 100)
-COLOR_INFO = mcrfpy.Color(100, 100, 255)
-COLOR_WARNING = mcrfpy.Color(255, 200, 50)
-
-# UI Layout constants
-UI_TOP_HEIGHT = 60
-UI_BOTTOM_HEIGHT = 150
-GAME_AREA_Y = UI_TOP_HEIGHT
-GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT
-
-# =============================================================================
-# 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:
- return self.hp > 0
-
- def take_damage(self, amount: int) -> int:
- actual_damage = min(self.hp, amount)
- self.hp -= actual_damage
- return actual_damage
-
- def heal(self, amount: int) -> int:
- 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)
- }
-}
-
-# =============================================================================
-# Message Log System
-# =============================================================================
-
-class MessageLog:
- """A message log that displays recent game messages with colors."""
-
- def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6):
- """Create a new message log.
-
- Args:
- x: X position of the log
- y: Y position of the log
- width: Width of the log area
- height: Height of the log area
- max_messages: Maximum number of messages to display
- """
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_messages = max_messages
- self.messages: list[tuple[str, mcrfpy.Color]] = []
- self.captions: list[mcrfpy.Caption] = []
-
- # Create the background frame
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- # Create caption for each message line
- line_height = 20
- for i in range(max_messages):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 5 + i * line_height),
- text=""
- )
- caption.font_size = 14
- caption.fill_color = mcrfpy.Color(200, 200, 200)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- """Add the message log UI elements to a scene."""
- scene.children.append(self.frame)
- for caption in self.captions:
- scene.children.append(caption)
-
- def add(self, 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(200, 200, 200)
-
- self.messages.append((text, color))
-
- # Keep only the most recent messages
- while len(self.messages) > self.max_messages:
- self.messages.pop(0)
-
- self._refresh()
-
- def _refresh(self) -> None:
- """Update the caption displays with current messages."""
- for i, caption in enumerate(self.captions):
- if i < len(self.messages):
- text, color = self.messages[i]
- caption.text = text
- caption.fill_color = color
- else:
- caption.text = ""
-
- def clear(self) -> None:
- """Clear all messages."""
- self.messages.clear()
- self._refresh()
-
-# =============================================================================
-# Health Bar System
-# =============================================================================
-
-class HealthBar:
- """A visual health bar using nested frames."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- """Create a new health bar.
-
- Args:
- x: X position
- y: Y position
- width: Total width of the health bar
- height: Height of the health bar
- """
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_hp = 30
- self.current_hp = 30
-
- # Background frame (dark red - shows when damaged)
- self.bg_frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0)
- self.bg_frame.outline = 2
- self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150)
-
- # Foreground frame (the actual health - shrinks as HP decreases)
- self.fg_frame = mcrfpy.Frame(
- pos=(x + 2, y + 2),
- size=(width - 4, height - 4)
- )
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- self.fg_frame.outline = 0
-
- # HP text label
- self.label = mcrfpy.Caption(
- pos=(x + 5, y + 2),
- text=f"HP: {self.current_hp}/{self.max_hp}"
- )
- self.label.font_size = 16
- self.label.fill_color = mcrfpy.Color(255, 255, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- """Add the health bar UI elements to a scene."""
- scene.children.append(self.bg_frame)
- scene.children.append(self.fg_frame)
- scene.children.append(self.label)
-
- def update(self, current_hp: int, max_hp: int) -> None:
- """Update the health bar display.
-
- Args:
- current_hp: Current HP value
- max_hp: Maximum HP value
- """
- self.current_hp = current_hp
- self.max_hp = max_hp
-
- # Calculate fill percentage
- percent = max(0, current_hp / max_hp) if max_hp > 0 else 0
-
- # Update the foreground width
- inner_width = self.width - 4
- self.fg_frame.resize(int(inner_width * percent), self.height - 4)
-
- # Update the label
- self.label.text = f"HP: {current_hp}/{max_hp}"
-
- # Update color based on health percentage
- if percent > 0.6:
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0) # Green
- elif percent > 0.3:
- self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0) # Yellow
- else:
- self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0) # Red
-
-# =============================================================================
-# Stats Panel
-# =============================================================================
-
-class StatsPanel:
- """A panel displaying player stats and dungeon info."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- """Create a new stats panel.
-
- Args:
- x: X position
- y: Y position
- width: Panel width
- height: Panel height
- """
- self.x = x
- self.y = y
- self.width = width
- self.height = height
-
- # Background frame
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- # Dungeon level caption
- self.level_caption = mcrfpy.Caption(
- pos=(x + 10, y + 10),
- text="Dungeon Level: 1"
- )
- self.level_caption.font_size = 16
- self.level_caption.fill_color = mcrfpy.Color(200, 200, 255)
-
- # Attack stat caption
- self.attack_caption = mcrfpy.Caption(
- pos=(x + 10, y + 35),
- text="Attack: 5"
- )
- self.attack_caption.font_size = 14
- self.attack_caption.fill_color = mcrfpy.Color(255, 200, 150)
-
- # Defense stat caption
- self.defense_caption = mcrfpy.Caption(
- pos=(x + 120, y + 35),
- text="Defense: 2"
- )
- self.defense_caption.font_size = 14
- self.defense_caption.fill_color = mcrfpy.Color(150, 200, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- """Add the stats panel UI elements to a scene."""
- scene.children.append(self.frame)
- scene.children.append(self.level_caption)
- scene.children.append(self.attack_caption)
- scene.children.append(self.defense_caption)
-
- def update(self, dungeon_level: int, attack: int, defense: int) -> None:
- """Update the stats panel display.
-
- Args:
- dungeon_level: Current dungeon level
- attack: Player attack stat
- defense: Player defense stat
- """
- self.level_caption.text = f"Dungeon Level: {dungeon_level}"
- self.attack_caption.text = f"Attack: {attack}"
- self.defense_caption.text = f"Defense: {defense}"
-
-# =============================================================================
-# Global State
-# =============================================================================
-
-entity_data: dict[mcrfpy.Entity, Fighter] = {}
-player: Optional[mcrfpy.Entity] = None
-grid: Optional[mcrfpy.Grid] = None
-fov_layer = None
-texture: Optional[mcrfpy.Texture] = None
-game_over: bool = False
-dungeon_level: int = 1
-
-# UI components
-message_log: Optional[MessageLog] = None
-health_bar: Optional[HealthBar] = None
-stats_panel: Optional[StatsPanel] = None
-
-# =============================================================================
-# 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:
- global explored
- explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
-
-def mark_explored(x: int, y: int) -> None:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- explored[y][x] = True
-
-def is_explored(x: int, y: int) -> bool:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- return explored[y][x]
- return False
-
-# =============================================================================
-# Dungeon Generation
-# =============================================================================
-
-def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
- 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:
- 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:
- 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:
- 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:
- 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:
- 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)
-
- 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:
- 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]:
- for entity in target_grid.entities:
- if int(entity.x) == x and int(entity.y) == y:
- 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]:
- 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:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in entity_data:
- del entity_data[entity]
-
-def clear_enemies(target_grid: mcrfpy.Grid) -> None:
- 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:
- return max(0, attacker.attack - defender.defense)
-
-def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
- global game_over
-
- attacker = entity_data.get(attacker_entity)
- defender = entity_data.get(defender_entity)
-
- if attacker is None or defender is None:
- return
-
- damage = calculate_damage(attacker, defender)
- defender.take_damage(damage)
-
- if damage > 0:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} for {damage} damage!",
- COLOR_PLAYER_ATTACK
- )
- else:
- message_log.add(
- f"The {attacker.name} hits you for {damage} damage!",
- COLOR_ENEMY_ATTACK
- )
- else:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} but deal no damage.",
- mcrfpy.Color(150, 150, 150)
- )
- else:
- message_log.add(
- f"The {attacker.name} hits but deals no damage.",
- mcrfpy.Color(150, 150, 200)
- )
-
- if not defender.is_alive:
- handle_death(defender_entity, defender)
-
- update_ui()
-
-def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
- global game_over, grid
-
- if fighter.is_player:
- message_log.add("You have died!", COLOR_PLAYER_DEATH)
- message_log.add("Press R to restart or Escape to quit.", COLOR_INFO)
- game_over = True
- entity.sprite_index = SPRITE_CORPSE
- else:
- message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH)
- remove_entity(grid, entity)
-
-# =============================================================================
-# Field of View
-# =============================================================================
-
-def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
- 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:
- 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:
- 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:
- 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
-
- if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT:
- return
-
- blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
-
- if blocker is not None:
- perform_attack(player, blocker)
- enemy_turn()
- elif grid.at(target_x, target_y).walkable:
- player.x = target_x
- player.y = target_y
- update_fov(grid, fov_layer, target_x, target_y)
- enemy_turn()
-
- update_ui()
-
-# =============================================================================
-# Enemy AI
-# =============================================================================
-
-def enemy_turn() -> None:
- global player, grid, game_over
-
- if game_over:
- return
-
- player_x, player_y = int(player.x), int(player.y)
-
- 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)
-
- if not grid.is_in_fov(ex, ey):
- continue
-
- dx = player_x - ex
- dy = player_y - ey
-
- if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
- perform_attack(enemy, player)
- else:
- 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:
- global grid
-
- dx = 0
- dy = 0
-
- if px < ex:
- dx = -1
- elif px > ex:
- dx = 1
-
- if py < ey:
- dy = -1
- elif py > ey:
- dy = 1
-
- 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):
- enemy.x = ex + dx
- elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
- enemy.y = ey + dy
-
-# =============================================================================
-# UI Updates
-# =============================================================================
-
-def update_ui() -> None:
- """Update all UI components."""
- global player, health_bar, stats_panel, dungeon_level
-
- if player in entity_data:
- fighter = entity_data[player]
- health_bar.update(fighter.hp, fighter.max_hp)
- stats_panel.update(dungeon_level, fighter.attack, fighter.defense)
-
-# =============================================================================
-# Game Setup
-# =============================================================================
-
-# Create the scene
-scene = mcrfpy.Scene("game")
-
-# Load texture
-texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
-
-# Create the grid (positioned to leave room for UI)
-grid = mcrfpy.Grid(
- pos=(20, GAME_AREA_Y),
- size=(800, GAME_AREA_HEIGHT - 20),
- 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)
-
-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)
-
-# Calculate initial FOV
-update_fov(grid, fov_layer, player_start_x, player_start_y)
-
-# Add grid to scene
-scene.children.append(grid)
-
-# =============================================================================
-# Create UI Elements
-# =============================================================================
-
-# Title bar
-title = mcrfpy.Caption(
- pos=(20, 10),
- text="Part 7: User Interface"
-)
-title.fill_color = mcrfpy.Color(255, 255, 255)
-title.font_size = 24
-scene.children.append(title)
-
-# Instructions
-instructions = mcrfpy.Caption(
- pos=(300, 15),
- text="WASD/Arrows: Move/Attack | R: Restart | Escape: Quit"
-)
-instructions.fill_color = mcrfpy.Color(180, 180, 180)
-instructions.font_size = 14
-scene.children.append(instructions)
-
-# Health Bar (top right area)
-health_bar = HealthBar(
- x=700,
- y=10,
- width=300,
- height=30
-)
-health_bar.add_to_scene(scene)
-
-# Stats Panel (below health bar)
-stats_panel = StatsPanel(
- x=830,
- y=GAME_AREA_Y,
- width=180,
- height=80
-)
-stats_panel.add_to_scene(scene)
-
-# Message Log (bottom of screen)
-message_log = MessageLog(
- x=20,
- y=768 - UI_BOTTOM_HEIGHT + 10,
- width=800,
- height=UI_BOTTOM_HEIGHT - 20,
- max_messages=6
-)
-message_log.add_to_scene(scene)
-
-# Initial messages
-message_log.add("Welcome to the dungeon!", COLOR_INFO)
-message_log.add("Find and defeat all enemies to progress.", COLOR_INFO)
-
-# Initialize UI
-update_ui()
-
-# =============================================================================
-# Input Handling
-# =============================================================================
-
-def restart_game() -> None:
- """Restart the game with a new dungeon."""
- global player, grid, fov_layer, game_over, entity_data, rooms, dungeon_level
-
- game_over = False
- dungeon_level = 1
-
- entity_data.clear()
-
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- fill_with_walls(grid)
- init_explored()
- message_log.clear()
-
- 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)
-
- if rooms:
- new_x, new_y = rooms[0].center
- else:
- new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
-
- 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
- )
-
- for i, room in enumerate(rooms):
- if i == 0:
- continue
- spawn_enemies_in_room(grid, room, texture)
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, new_x, new_y)
-
- message_log.add("A new adventure begins!", COLOR_INFO)
-
- update_ui()
-
-def handle_keys(key: str, action: str) -> None:
- global game_over
-
- if action != "start":
- return
-
- if key == "R":
- restart_game()
- return
-
- if key == "Escape":
- mcrfpy.exit()
- return
-
- if game_over:
- return
-
- 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 7 loaded! Notice the improved UI with health bar and message log.")
\ No newline at end of file
diff --git a/docs/tutorials/part_08_items/part_08_items.py b/docs/tutorials/part_08_items/part_08_items.py
deleted file mode 100644
index e8f271e..0000000
--- a/docs/tutorials/part_08_items/part_08_items.py
+++ /dev/null
@@ -1,1275 +0,0 @@
-"""McRogueFace - Part 8: Items and Inventory
-
-Documentation: https://mcrogueface.github.io/tutorial/part_08_items
-Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_08_items/part_08_items.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, field
-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
-SPRITE_POTION = 173 # Potion sprite (or use '!' = 33)
-
-# Enemy sprites
-SPRITE_GOBLIN = 103 # 'g'
-SPRITE_ORC = 111 # 'o'
-SPRITE_TROLL = 116 # 't'
-
-# Grid dimensions
-GRID_WIDTH = 50
-GRID_HEIGHT = 30
-
-# Room generation parameters
-ROOM_MIN_SIZE = 6
-ROOM_MAX_SIZE = 12
-MAX_ROOMS = 8
-
-# Enemy spawn parameters
-MAX_ENEMIES_PER_ROOM = 3
-
-# Item spawn parameters
-MAX_ITEMS_PER_ROOM = 2
-
-# 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 colors
-COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200)
-COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150)
-COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50)
-COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100)
-COLOR_HEAL = mcrfpy.Color(100, 255, 100)
-COLOR_PICKUP = mcrfpy.Color(100, 200, 255)
-COLOR_INFO = mcrfpy.Color(100, 100, 255)
-COLOR_WARNING = mcrfpy.Color(255, 200, 50)
-COLOR_INVALID = mcrfpy.Color(255, 100, 100)
-
-# UI Layout constants
-UI_TOP_HEIGHT = 60
-UI_BOTTOM_HEIGHT = 150
-GAME_AREA_Y = UI_TOP_HEIGHT
-GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT
-
-# =============================================================================
-# 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:
- return self.hp > 0
-
- def take_damage(self, amount: int) -> int:
- actual_damage = min(self.hp, amount)
- self.hp -= actual_damage
- return actual_damage
-
- def heal(self, amount: int) -> int:
- actual_heal = min(self.max_hp - self.hp, amount)
- self.hp += actual_heal
- return actual_heal
-
-# =============================================================================
-# Item Component
-# =============================================================================
-
-@dataclass
-class Item:
- """Data for an item that can be picked up and used."""
- name: str
- item_type: str
- heal_amount: int = 0
-
- def describe(self) -> str:
- """Return a description of what this item does."""
- if self.item_type == "health_potion":
- return f"Restores {self.heal_amount} HP"
- return "Unknown item"
-
-# =============================================================================
-# Inventory System
-# =============================================================================
-
-@dataclass
-class Inventory:
- """Container for items the player is carrying."""
- capacity: int = 10
- items: list = field(default_factory=list)
-
- def add(self, item: Item) -> bool:
- """Add an item to the inventory.
-
- Args:
- item: The item to add
-
- Returns:
- True if item was added, False if inventory is full
- """
- if len(self.items) >= self.capacity:
- return False
- self.items.append(item)
- return True
-
- def remove(self, index: int) -> Optional[Item]:
- """Remove and return an item by index.
-
- Args:
- index: The index of the item to remove
-
- Returns:
- The removed item, or None if index is invalid
- """
- if 0 <= index < len(self.items):
- return self.items.pop(index)
- return None
-
- def get(self, index: int) -> Optional[Item]:
- """Get an item by index without removing it."""
- if 0 <= index < len(self.items):
- return self.items[index]
- return None
-
- def is_full(self) -> bool:
- """Check if the inventory is full."""
- return len(self.items) >= self.capacity
-
- def count(self) -> int:
- """Return the number of items in the inventory."""
- return len(self.items)
-
-# =============================================================================
-# Item Templates
-# =============================================================================
-
-ITEM_TEMPLATES = {
- "health_potion": {
- "name": "Health Potion",
- "sprite": SPRITE_POTION,
-
- "item_type": "health_potion",
- "heal_amount": 10
- }
-}
-
-# =============================================================================
-# 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)
- }
-}
-
-# =============================================================================
-# Message Log System
-# =============================================================================
-
-class MessageLog:
- """A message log that displays recent game messages with colors."""
-
- def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_messages = max_messages
- self.messages: list[tuple[str, mcrfpy.Color]] = []
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- line_height = 20
- for i in range(max_messages):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 5 + i * line_height),
- text=""
- )
- caption.font_size = 14
- caption.fill_color = mcrfpy.Color(200, 200, 200)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- for caption in self.captions:
- scene.children.append(caption)
-
- def add(self, text: str, color: mcrfpy.Color = None) -> None:
- if color is None:
- color = mcrfpy.Color(200, 200, 200)
-
- self.messages.append((text, color))
-
- while len(self.messages) > self.max_messages:
- self.messages.pop(0)
-
- self._refresh()
-
- def _refresh(self) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(self.messages):
- text, color = self.messages[i]
- caption.text = text
- caption.fill_color = color
- else:
- caption.text = ""
-
- def clear(self) -> None:
- self.messages.clear()
- self._refresh()
-
-# =============================================================================
-# Health Bar System
-# =============================================================================
-
-class HealthBar:
- """A visual health bar using nested frames."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_hp = 30
- self.current_hp = 30
-
- self.bg_frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0)
- self.bg_frame.outline = 2
- self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150)
-
- self.fg_frame = mcrfpy.Frame(
- pos=(x + 2, y + 2),
- size=(width - 4, height - 4)
- )
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- self.fg_frame.outline = 0
-
- self.label = mcrfpy.Caption(
- pos=(x + 5, y + 2),
- text=f"HP: {self.current_hp}/{self.max_hp}"
- )
- self.label.font_size = 16
- self.label.fill_color = mcrfpy.Color(255, 255, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.bg_frame)
- scene.children.append(self.fg_frame)
- scene.children.append(self.label)
-
- def update(self, current_hp: int, max_hp: int) -> None:
- self.current_hp = current_hp
- self.max_hp = max_hp
-
- percent = max(0, current_hp / max_hp) if max_hp > 0 else 0
-
- inner_width = self.width - 4
- self.fg_frame.resize(int(inner_width * percent), self.height - 4)
-
- self.label.text = f"HP: {current_hp}/{max_hp}"
-
- if percent > 0.6:
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- elif percent > 0.3:
- self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0)
- else:
- self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0)
-
-# =============================================================================
-# Inventory Panel
-# =============================================================================
-
-class InventoryPanel:
- """A panel displaying the player's inventory."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- self.title = mcrfpy.Caption(
- pos=(x + 10, y + 5),
- text="Inventory (G:pickup, U:use)"
- )
- self.title.font_size = 14
- self.title.fill_color = mcrfpy.Color(200, 200, 255)
-
- # Create slots for inventory items
- for i in range(5): # Show up to 5 items
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 25 + i * 18),
- text=""
- )
- caption.font_size = 13
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- scene.children.append(self.title)
- for caption in self.captions:
- scene.children.append(caption)
-
- def update(self, inventory: Inventory) -> None:
- """Update the inventory display."""
- for i, caption in enumerate(self.captions):
- if i < len(inventory.items):
- item = inventory.items[i]
- caption.text = f"{i+1}. {item.name}"
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- else:
- caption.text = f"{i+1}. ---"
- caption.fill_color = mcrfpy.Color(80, 80, 80)
-
-# =============================================================================
-# Global State
-# =============================================================================
-
-entity_data: dict[mcrfpy.Entity, Fighter] = {}
-item_data: dict[mcrfpy.Entity, Item] = {}
-
-player: Optional[mcrfpy.Entity] = None
-player_inventory: Optional[Inventory] = None
-grid: Optional[mcrfpy.Grid] = None
-fov_layer = None
-texture: Optional[mcrfpy.Texture] = None
-game_over: bool = False
-dungeon_level: int = 1
-
-# UI components
-message_log: Optional[MessageLog] = None
-health_bar: Optional[HealthBar] = None
-inventory_panel: Optional[InventoryPanel] = None
-
-# =============================================================================
-# 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:
- global explored
- explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
-
-def mark_explored(x: int, y: int) -> None:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- explored[y][x] = True
-
-def is_explored(x: int, y: int) -> bool:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- return explored[y][x]
- return False
-
-# =============================================================================
-# Dungeon Generation
-# =============================================================================
-
-def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
- 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:
- 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:
- 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:
- 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:
- 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:
- 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)
-
- 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:
- 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 is_position_occupied(target_grid, x, y):
- 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)
-
-# =============================================================================
-# Item Management
-# =============================================================================
-
-def spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
- """Spawn an item at the given position.
-
- Args:
- target_grid: The game grid
- x: X position in tiles
- y: Y position in tiles
- item_type: Type of item to spawn
- tex: The texture to use
-
- Returns:
- The created item entity
- """
- template = ITEM_TEMPLATES[item_type]
-
- item_entity = mcrfpy.Entity(
- grid_pos=(x, y),
- texture=tex,
- sprite_index=template["sprite"]
- )
-
- item_entity.visible = False
-
- target_grid.entities.append(item_entity)
-
- # Create Item data
- item_data[item_entity] = Item(
- name=template["name"],
- item_type=template["item_type"],
- heal_amount=template.get("heal_amount", 0)
- )
-
- return item_entity
-
-def spawn_items_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None:
- """Spawn random items in a room.
-
- Args:
- target_grid: The game grid
- room: The room to spawn items in
- tex: The texture to use
- """
- num_items = random.randint(0, MAX_ITEMS_PER_ROOM)
-
- for _ in range(num_items):
- 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 blocked by entity or item
- if is_position_occupied(target_grid, x, y):
- continue
-
- # For now, only spawn health potions
- spawn_item(target_grid, x, y, "health_potion", tex)
-
-def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
- """Get an item entity at the given position.
-
- Args:
- target_grid: The game grid
- x: X position to check
- y: Y position to check
-
- Returns:
- The item entity, or None if no item at position
- """
- for entity in target_grid.entities:
- if entity in item_data:
- if int(entity.x) == x and int(entity.y) == y:
- return entity
- return None
-
-def pickup_item() -> bool:
- """Try to pick up an item at the player's position.
-
- Returns:
- True if an item was picked up, False otherwise
- """
- global player, player_inventory, grid
-
- px, py = int(player.x), int(player.y)
- item_entity = get_item_at(grid, px, py)
-
- if item_entity is None:
- message_log.add("There is nothing to pick up here.", COLOR_INVALID)
- return False
-
- if player_inventory.is_full():
- message_log.add("Your inventory is full!", COLOR_WARNING)
- return False
-
- # Get the item data
- item = item_data.get(item_entity)
- if item is None:
- return False
-
- # Add to inventory
- player_inventory.add(item)
-
- # Remove from ground
- remove_item_entity(grid, item_entity)
-
- message_log.add(f"You pick up the {item.name}.", COLOR_PICKUP)
-
- update_ui()
- return True
-
-def use_item(index: int) -> bool:
- """Use an item from the inventory.
-
- Args:
- index: The inventory index of the item to use
-
- Returns:
- True if an item was used, False otherwise
- """
- global player, player_inventory
-
- item = player_inventory.get(index)
- if item is None:
- message_log.add("Invalid item selection.", COLOR_INVALID)
- return False
-
- # Handle different item types
- if item.item_type == "health_potion":
- fighter = entity_data.get(player)
- if fighter is None:
- return False
-
- if fighter.hp >= fighter.max_hp:
- message_log.add("You are already at full health!", COLOR_WARNING)
- return False
-
- # Apply healing
- actual_heal = fighter.heal(item.heal_amount)
-
- # Remove the item from inventory
- player_inventory.remove(index)
-
- message_log.add(f"You drink the {item.name} and recover {actual_heal} HP!", COLOR_HEAL)
-
- update_ui()
- return True
-
- message_log.add(f"You cannot use the {item.name}.", COLOR_INVALID)
- return False
-
-def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
- """Remove an item entity from the grid and item_data."""
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
-
- if entity in item_data:
- del item_data[entity]
-
-# =============================================================================
-# Position Checking
-# =============================================================================
-
-def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
- """Check if a position has any entity (enemy, item, or player).
-
- Args:
- target_grid: The game grid
- x: X position to check
- y: Y position to check
-
- Returns:
- True if position is occupied, False otherwise
- """
- for entity in target_grid.entities:
- if int(entity.x) == x and int(entity.y) == y:
- return True
- return False
-
-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:
- # Only fighters block movement
- 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:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in entity_data:
- del entity_data[entity]
-
-def clear_all_entities(target_grid: mcrfpy.Grid) -> None:
- """Remove all entities (enemies and items) except the player."""
- global entity_data, item_data
-
- entities_to_remove = []
- for entity in target_grid.entities:
- if entity in entity_data and not entity_data[entity].is_player:
- entities_to_remove.append(entity)
- elif entity in item_data:
- entities_to_remove.append(entity)
-
- for entity in entities_to_remove:
- if entity in entity_data:
- del entity_data[entity]
- if entity in item_data:
- del item_data[entity]
-
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
-
-# =============================================================================
-# Combat System
-# =============================================================================
-
-def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
- return max(0, attacker.attack - defender.defense)
-
-def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
- global game_over
-
- attacker = entity_data.get(attacker_entity)
- defender = entity_data.get(defender_entity)
-
- if attacker is None or defender is None:
- return
-
- damage = calculate_damage(attacker, defender)
- defender.take_damage(damage)
-
- if damage > 0:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} for {damage} damage!",
- COLOR_PLAYER_ATTACK
- )
- else:
- message_log.add(
- f"The {attacker.name} hits you for {damage} damage!",
- COLOR_ENEMY_ATTACK
- )
- else:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} but deal no damage.",
- mcrfpy.Color(150, 150, 150)
- )
- else:
- message_log.add(
- f"The {attacker.name} hits but deals no damage.",
- mcrfpy.Color(150, 150, 200)
- )
-
- if not defender.is_alive:
- handle_death(defender_entity, defender)
-
- update_ui()
-
-def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
- global game_over, grid
-
- if fighter.is_player:
- message_log.add("You have died!", COLOR_PLAYER_DEATH)
- message_log.add("Press R to restart or Escape to quit.", COLOR_INFO)
- game_over = True
- entity.sprite_index = SPRITE_CORPSE
- else:
- message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH)
- remove_entity(grid, entity)
-
-# =============================================================================
-# Field of View
-# =============================================================================
-
-def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
- 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:
- 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:
- 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:
- 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
-
- if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT:
- return
-
- blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
-
- if blocker is not None:
- perform_attack(player, blocker)
- enemy_turn()
- elif grid.at(target_x, target_y).walkable:
- player.x = target_x
- player.y = target_y
- update_fov(grid, fov_layer, target_x, target_y)
- enemy_turn()
-
- update_ui()
-
-# =============================================================================
-# Enemy AI
-# =============================================================================
-
-def enemy_turn() -> None:
- global player, grid, game_over
-
- if game_over:
- return
-
- player_x, player_y = int(player.x), int(player.y)
-
- 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)
-
- if not grid.is_in_fov(ex, ey):
- continue
-
- dx = player_x - ex
- dy = player_y - ey
-
- if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
- perform_attack(enemy, player)
- else:
- 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:
- global grid
-
- dx = 0
- dy = 0
-
- if px < ex:
- dx = -1
- elif px > ex:
- dx = 1
-
- if py < ey:
- dy = -1
- elif py > ey:
- dy = 1
-
- 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):
- enemy.x = ex + dx
- elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
- enemy.y = ey + dy
-
-# =============================================================================
-# UI Updates
-# =============================================================================
-
-def update_ui() -> None:
- """Update all UI components."""
- global player, health_bar, inventory_panel, player_inventory
-
- if player in entity_data:
- fighter = entity_data[player]
- health_bar.update(fighter.hp, fighter.max_hp)
-
- if player_inventory:
- inventory_panel.update(player_inventory)
-
-# =============================================================================
-# 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=(20, GAME_AREA_Y),
- size=(700, GAME_AREA_HEIGHT - 20),
- grid_size=(GRID_WIDTH, GRID_HEIGHT),
- texture=texture
-)
-grid.zoom = 1.0
-
-# Generate initial dungeon
-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)
-
-entity_data[player] = Fighter(
- hp=30,
- max_hp=30,
- attack=5,
- defense=2,
- name="Player",
- is_player=True
-)
-
-# Create player inventory
-player_inventory = Inventory(capacity=10)
-
-# Spawn enemies and items
-for i, room in enumerate(rooms):
- if i == 0:
- continue # Skip player's starting room
- spawn_enemies_in_room(grid, room, texture)
- spawn_items_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)
-
-# =============================================================================
-# Create UI Elements
-# =============================================================================
-
-# Title bar
-title = mcrfpy.Caption(
- pos=(20, 10),
- text="Part 8: Items and Inventory"
-)
-title.fill_color = mcrfpy.Color(255, 255, 255)
-title.font_size = 24
-scene.children.append(title)
-
-# Instructions
-instructions = mcrfpy.Caption(
- pos=(300, 15),
- text="WASD: Move | G: Pickup | 1-5: Use item | R: Restart"
-)
-instructions.fill_color = mcrfpy.Color(180, 180, 180)
-instructions.font_size = 14
-scene.children.append(instructions)
-
-# Health Bar
-health_bar = HealthBar(
- x=730,
- y=10,
- width=280,
- height=30
-)
-health_bar.add_to_scene(scene)
-
-# Inventory Panel
-inventory_panel = InventoryPanel(
- x=730,
- y=GAME_AREA_Y,
- width=280,
- height=150
-)
-inventory_panel.add_to_scene(scene)
-
-# Message Log
-message_log = MessageLog(
- x=20,
- y=768 - UI_BOTTOM_HEIGHT + 10,
- width=990,
- height=UI_BOTTOM_HEIGHT - 20,
- max_messages=6
-)
-message_log.add_to_scene(scene)
-
-# Initial messages
-message_log.add("Welcome to the dungeon!", COLOR_INFO)
-message_log.add("Find potions to heal. Press G to pick up items.", COLOR_INFO)
-
-# Initialize UI
-update_ui()
-
-# =============================================================================
-# Input Handling
-# =============================================================================
-
-def restart_game() -> None:
- """Restart the game with a new dungeon."""
- global player, grid, fov_layer, game_over, entity_data, item_data, rooms
- global player_inventory
-
- game_over = False
-
- entity_data.clear()
- item_data.clear()
-
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- fill_with_walls(grid)
- init_explored()
- message_log.clear()
-
- 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)
-
- if rooms:
- new_x, new_y = rooms[0].center
- else:
- new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
-
- 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
- )
-
- # Reset inventory
- player_inventory = Inventory(capacity=10)
-
- for i, room in enumerate(rooms):
- if i == 0:
- continue
- spawn_enemies_in_room(grid, room, texture)
- spawn_items_in_room(grid, room, texture)
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, new_x, new_y)
-
- message_log.add("A new adventure begins!", COLOR_INFO)
-
- update_ui()
-
-def handle_keys(key: str, action: str) -> None:
- global game_over
-
- if action != "start":
- return
-
- if key == "R":
- restart_game()
- return
-
- if key == "Escape":
- mcrfpy.exit()
- return
-
- if game_over:
- return
-
- # Movement
- 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)
- # Pickup
- elif key == "G" or key == ",":
- pickup_item()
- # Use items by number key
- elif key in ["1", "2", "3", "4", "5"]:
- index = int(key) - 1
- if use_item(index):
- enemy_turn() # Using an item takes a turn
- update_ui()
-
-scene.on_key = handle_keys
-
-# =============================================================================
-# Start the Game
-# =============================================================================
-
-scene.activate()
-print("Part 8 loaded! Pick up health potions with G, use with 1-5.")
\ No newline at end of file
diff --git a/docs/tutorials/part_09_ranged/part_09_ranged.py b/docs/tutorials/part_09_ranged/part_09_ranged.py
deleted file mode 100644
index f855a75..0000000
--- a/docs/tutorials/part_09_ranged/part_09_ranged.py
+++ /dev/null
@@ -1,1396 +0,0 @@
-"""McRogueFace - Part 9: Ranged Combat and Targeting
-
-Documentation: https://mcrogueface.github.io/tutorial/part_09_ranged
-Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_09_ranged/part_09_ranged.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, field
-from typing import Optional
-from enum import Enum
-
-# =============================================================================
-# Constants
-# =============================================================================
-
-# Sprite indices for CP437 tileset
-SPRITE_WALL = 35 # '#' - wall
-SPRITE_FLOOR = 46 # '.' - floor
-SPRITE_PLAYER = 64 # '@' - player
-SPRITE_CORPSE = 37 # '%' - remains
-SPRITE_POTION = 173 # Potion sprite
-SPRITE_CURSOR = 88 # 'X' - targeting cursor
-
-# Enemy sprites
-SPRITE_GOBLIN = 103 # 'g'
-SPRITE_ORC = 111 # 'o'
-SPRITE_TROLL = 116 # 't'
-
-# Grid dimensions
-GRID_WIDTH = 50
-GRID_HEIGHT = 30
-
-# Room generation parameters
-ROOM_MIN_SIZE = 6
-ROOM_MAX_SIZE = 12
-MAX_ROOMS = 8
-
-# Enemy spawn parameters
-MAX_ENEMIES_PER_ROOM = 3
-
-# Item spawn parameters
-MAX_ITEMS_PER_ROOM = 2
-
-# FOV and targeting settings
-FOV_RADIUS = 8
-RANGED_ATTACK_RANGE = 6
-RANGED_ATTACK_DAMAGE = 4
-
-# 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 colors
-COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200)
-COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150)
-COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50)
-COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100)
-COLOR_HEAL = mcrfpy.Color(100, 255, 100)
-COLOR_PICKUP = mcrfpy.Color(100, 200, 255)
-COLOR_INFO = mcrfpy.Color(100, 100, 255)
-COLOR_WARNING = mcrfpy.Color(255, 200, 50)
-COLOR_INVALID = mcrfpy.Color(255, 100, 100)
-COLOR_RANGED = mcrfpy.Color(255, 255, 100)
-
-# UI Layout constants
-UI_TOP_HEIGHT = 60
-UI_BOTTOM_HEIGHT = 150
-GAME_AREA_Y = UI_TOP_HEIGHT
-GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT
-
-# =============================================================================
-# Game Modes
-# =============================================================================
-
-class GameMode(Enum):
- """The current game input mode."""
- NORMAL = "normal"
- TARGETING = "targeting"
-
-# =============================================================================
-# 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:
- return self.hp > 0
-
- def take_damage(self, amount: int) -> int:
- actual_damage = min(self.hp, amount)
- self.hp -= actual_damage
- return actual_damage
-
- def heal(self, amount: int) -> int:
- actual_heal = min(self.max_hp - self.hp, amount)
- self.hp += actual_heal
- return actual_heal
-
-# =============================================================================
-# Item Component
-# =============================================================================
-
-@dataclass
-class Item:
- """Data for an item that can be picked up and used."""
- name: str
- item_type: str
- heal_amount: int = 0
-
- def describe(self) -> str:
- if self.item_type == "health_potion":
- return f"Restores {self.heal_amount} HP"
- return "Unknown item"
-
-# =============================================================================
-# Inventory System
-# =============================================================================
-
-@dataclass
-class Inventory:
- """Container for items the player is carrying."""
- capacity: int = 10
- items: list = field(default_factory=list)
-
- def add(self, item: Item) -> bool:
- if len(self.items) >= self.capacity:
- return False
- self.items.append(item)
- return True
-
- def remove(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items.pop(index)
- return None
-
- def get(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items[index]
- return None
-
- def is_full(self) -> bool:
- return len(self.items) >= self.capacity
-
- def count(self) -> int:
- return len(self.items)
-
-# =============================================================================
-# Templates
-# =============================================================================
-
-ITEM_TEMPLATES = {
- "health_potion": {
- "name": "Health Potion",
- "sprite": SPRITE_POTION,
-
- "item_type": "health_potion",
- "heal_amount": 10
- }
-}
-
-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)
- }
-}
-
-# =============================================================================
-# Message Log System
-# =============================================================================
-
-class MessageLog:
- """A message log that displays recent game messages with colors."""
-
- def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_messages = max_messages
- self.messages: list[tuple[str, mcrfpy.Color]] = []
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- line_height = 20
- for i in range(max_messages):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 5 + i * line_height),
- text=""
- )
- caption.font_size = 14
- caption.fill_color = mcrfpy.Color(200, 200, 200)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- for caption in self.captions:
- scene.children.append(caption)
-
- def add(self, text: str, color: mcrfpy.Color = None) -> None:
- if color is None:
- color = mcrfpy.Color(200, 200, 200)
-
- self.messages.append((text, color))
-
- while len(self.messages) > self.max_messages:
- self.messages.pop(0)
-
- self._refresh()
-
- def _refresh(self) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(self.messages):
- text, color = self.messages[i]
- caption.text = text
- caption.fill_color = color
- else:
- caption.text = ""
-
- def clear(self) -> None:
- self.messages.clear()
- self._refresh()
-
-# =============================================================================
-# Health Bar System
-# =============================================================================
-
-class HealthBar:
- """A visual health bar using nested frames."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_hp = 30
- self.current_hp = 30
-
- self.bg_frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0)
- self.bg_frame.outline = 2
- self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150)
-
- self.fg_frame = mcrfpy.Frame(
- pos=(x + 2, y + 2),
- size=(width - 4, height - 4)
- )
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- self.fg_frame.outline = 0
-
- self.label = mcrfpy.Caption(
- pos=(x + 5, y + 2),
- text=f"HP: {self.current_hp}/{self.max_hp}"
- )
- self.label.font_size = 16
- self.label.fill_color = mcrfpy.Color(255, 255, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.bg_frame)
- scene.children.append(self.fg_frame)
- scene.children.append(self.label)
-
- def update(self, current_hp: int, max_hp: int) -> None:
- self.current_hp = current_hp
- self.max_hp = max_hp
-
- percent = max(0, current_hp / max_hp) if max_hp > 0 else 0
-
- inner_width = self.width - 4
- self.fg_frame.resize(int(inner_width * percent), self.height - 4)
-
- self.label.text = f"HP: {current_hp}/{max_hp}"
-
- if percent > 0.6:
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- elif percent > 0.3:
- self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0)
- else:
- self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0)
-
-# =============================================================================
-# Inventory Panel
-# =============================================================================
-
-class InventoryPanel:
- """A panel displaying the player's inventory."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- self.title = mcrfpy.Caption(
- pos=(x + 10, y + 5),
- text="Inventory (G:pickup, 1-5:use)"
- )
- self.title.font_size = 14
- self.title.fill_color = mcrfpy.Color(200, 200, 255)
-
- for i in range(5):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 25 + i * 18),
- text=""
- )
- caption.font_size = 13
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- scene.children.append(self.title)
- for caption in self.captions:
- scene.children.append(caption)
-
- def update(self, inventory: Inventory) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(inventory.items):
- item = inventory.items[i]
- caption.text = f"{i+1}. {item.name}"
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- else:
- caption.text = f"{i+1}. ---"
- caption.fill_color = mcrfpy.Color(80, 80, 80)
-
-# =============================================================================
-# Mode Display
-# =============================================================================
-
-class ModeDisplay:
- """Displays the current game mode."""
-
- def __init__(self, x: int, y: int):
- self.caption = mcrfpy.Caption(
- pos=(x, y),
- text="[NORMAL MODE]"
- )
- self.caption.font_size = 16
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.caption)
-
- def update(self, mode: GameMode) -> None:
- if mode == GameMode.NORMAL:
- self.caption.text = "[NORMAL MODE] - F: Ranged attack"
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
- elif mode == GameMode.TARGETING:
- self.caption.text = "[TARGETING] - Arrows: Move, Enter: Fire, Esc: Cancel"
- self.caption.fill_color = mcrfpy.Color(255, 255, 100)
-
-# =============================================================================
-# Global State
-# =============================================================================
-
-entity_data: dict[mcrfpy.Entity, Fighter] = {}
-item_data: dict[mcrfpy.Entity, Item] = {}
-
-player: Optional[mcrfpy.Entity] = None
-player_inventory: Optional[Inventory] = None
-grid: Optional[mcrfpy.Grid] = None
-fov_layer = None
-texture: Optional[mcrfpy.Texture] = None
-game_over: bool = False
-dungeon_level: int = 1
-
-# Game mode state
-game_mode: GameMode = GameMode.NORMAL
-target_cursor: Optional[mcrfpy.Entity] = None
-target_x: int = 0
-target_y: int = 0
-
-# UI components
-message_log: Optional[MessageLog] = None
-health_bar: Optional[HealthBar] = None
-inventory_panel: Optional[InventoryPanel] = None
-mode_display: Optional[ModeDisplay] = None
-
-# =============================================================================
-# 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:
- global explored
- explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
-
-def mark_explored(x: int, y: int) -> None:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- explored[y][x] = True
-
-def is_explored(x: int, y: int) -> bool:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- return explored[y][x]
- return False
-
-# =============================================================================
-# Dungeon Generation
-# =============================================================================
-
-def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
- 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:
- 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:
- 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:
- 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:
- 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:
- 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)
-
- 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:
- 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 is_position_occupied(target_grid, x, y):
- 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 spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
- template = ITEM_TEMPLATES[item_type]
-
- item_entity = mcrfpy.Entity(
- grid_pos=(x, y),
- texture=tex,
- sprite_index=template["sprite"]
- )
-
- item_entity.visible = False
-
- target_grid.entities.append(item_entity)
-
- item_data[item_entity] = Item(
- name=template["name"],
- item_type=template["item_type"],
- heal_amount=template.get("heal_amount", 0)
- )
-
- return item_entity
-
-def spawn_items_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None:
- num_items = random.randint(0, MAX_ITEMS_PER_ROOM)
-
- for _ in range(num_items):
- 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 is_position_occupied(target_grid, x, y):
- continue
-
- spawn_item(target_grid, x, y, "health_potion", tex)
-
-def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
- for entity in target_grid.entities:
- if int(entity.x) == x and int(entity.y) == y:
- return True
- return False
-
-def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
- for entity in target_grid.entities:
- if entity in item_data:
- if int(entity.x) == x and int(entity.y) == y:
- return entity
- return None
-
-def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
- 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:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in entity_data:
- del entity_data[entity]
-
-def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in item_data:
- del item_data[entity]
-
-def clear_all_entities(target_grid: mcrfpy.Grid) -> None:
- global entity_data, item_data
-
- entities_to_remove = []
- for entity in target_grid.entities:
- if entity in entity_data and not entity_data[entity].is_player:
- entities_to_remove.append(entity)
- elif entity in item_data:
- entities_to_remove.append(entity)
-
- for entity in entities_to_remove:
- if entity in entity_data:
- del entity_data[entity]
- if entity in item_data:
- del item_data[entity]
-
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
-
-# =============================================================================
-# Targeting System
-# =============================================================================
-
-def enter_targeting_mode() -> None:
- """Enter targeting mode for ranged attack."""
- global game_mode, target_cursor, target_x, target_y, player, grid, texture
-
- # Start at player position
- target_x = int(player.x)
- target_y = int(player.y)
-
- # Create the targeting cursor
- target_cursor = mcrfpy.Entity(
- grid_pos=(target_x, target_y),
- texture=texture,
- sprite_index=SPRITE_CURSOR
- )
- grid.entities.append(target_cursor)
-
- game_mode = GameMode.TARGETING
-
- message_log.add("Targeting mode: Use arrows to aim, Enter to fire, Esc to cancel.", COLOR_INFO)
- mode_display.update(game_mode)
-
-def exit_targeting_mode() -> None:
- """Exit targeting mode without firing."""
- global game_mode, target_cursor, grid
-
- if target_cursor is not None:
- # Remove cursor from grid
- for i, e in enumerate(grid.entities):
- if e == target_cursor:
- grid.entities.remove(i)
- break
- target_cursor = None
-
- game_mode = GameMode.NORMAL
- mode_display.update(game_mode)
-
-def move_cursor(dx: int, dy: int) -> None:
- """Move the targeting cursor."""
- global target_x, target_y, target_cursor, grid, player
-
- new_x = target_x + dx
- new_y = target_y + dy
-
- # Check bounds
- if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
- return
-
- # Check if position is in FOV (can only target visible tiles)
- if not grid.is_in_fov(new_x, new_y):
- message_log.add("You cannot see that location.", COLOR_INVALID)
- return
-
- # Check range
- player_x, player_y = int(player.x), int(player.y)
- distance = abs(new_x - player_x) + abs(new_y - player_y)
- if distance > RANGED_ATTACK_RANGE:
- message_log.add(f"Target is out of range (max {RANGED_ATTACK_RANGE}).", COLOR_WARNING)
- return
-
- # Move cursor
- target_x = new_x
- target_y = new_y
- target_cursor.x = target_x
- target_cursor.y = target_y
-
- # Show info about target location
- enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
- if enemy and enemy in entity_data:
- fighter = entity_data[enemy]
- message_log.add(f"Target: {fighter.name} (HP: {fighter.hp}/{fighter.max_hp})", COLOR_RANGED)
-
-def confirm_target() -> None:
- """Confirm the target and execute ranged attack."""
- global game_mode, target_x, target_y, player, grid
-
- # Check if targeting self
- if target_x == int(player.x) and target_y == int(player.y):
- message_log.add("You cannot target yourself!", COLOR_INVALID)
- return
-
- # Check for enemy at target
- target_enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
-
- if target_enemy is None or target_enemy not in entity_data:
- message_log.add("No valid target at that location.", COLOR_INVALID)
- return
-
- # Perform ranged attack
- perform_ranged_attack(target_enemy)
-
- # Exit targeting mode
- exit_targeting_mode()
-
- # Enemies take their turn
- enemy_turn()
-
- update_ui()
-
-def perform_ranged_attack(target_entity: mcrfpy.Entity) -> None:
- """Execute a ranged attack on the target.
-
- Args:
- target_entity: The entity to attack
- """
- global player, game_over
-
- defender = entity_data.get(target_entity)
- attacker = entity_data.get(player)
-
- if defender is None or attacker is None:
- return
-
- # Ranged attacks deal fixed damage (ignores defense partially)
- damage = max(1, RANGED_ATTACK_DAMAGE - defender.defense // 2)
-
- defender.take_damage(damage)
-
- message_log.add(
- f"Your ranged attack hits the {defender.name} for {damage} damage!",
- COLOR_RANGED
- )
-
- # Check for death
- if not defender.is_alive:
- handle_death(target_entity, defender)
-
-# =============================================================================
-# Combat System
-# =============================================================================
-
-def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
- return max(0, attacker.attack - defender.defense)
-
-def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
- global game_over
-
- attacker = entity_data.get(attacker_entity)
- defender = entity_data.get(defender_entity)
-
- if attacker is None or defender is None:
- return
-
- damage = calculate_damage(attacker, defender)
- defender.take_damage(damage)
-
- if damage > 0:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} for {damage} damage!",
- COLOR_PLAYER_ATTACK
- )
- else:
- message_log.add(
- f"The {attacker.name} hits you for {damage} damage!",
- COLOR_ENEMY_ATTACK
- )
- else:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} but deal no damage.",
- mcrfpy.Color(150, 150, 150)
- )
- else:
- message_log.add(
- f"The {attacker.name} hits but deals no damage.",
- mcrfpy.Color(150, 150, 200)
- )
-
- if not defender.is_alive:
- handle_death(defender_entity, defender)
-
- update_ui()
-
-def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
- global game_over, grid
-
- if fighter.is_player:
- message_log.add("You have died!", COLOR_PLAYER_DEATH)
- message_log.add("Press R to restart or Escape to quit.", COLOR_INFO)
- game_over = True
- entity.sprite_index = SPRITE_CORPSE
- else:
- message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH)
- remove_entity(grid, entity)
-
-# =============================================================================
-# Item Actions
-# =============================================================================
-
-def pickup_item() -> bool:
- global player, player_inventory, grid
-
- px, py = int(player.x), int(player.y)
- item_entity = get_item_at(grid, px, py)
-
- if item_entity is None:
- message_log.add("There is nothing to pick up here.", COLOR_INVALID)
- return False
-
- if player_inventory.is_full():
- message_log.add("Your inventory is full!", COLOR_WARNING)
- return False
-
- item = item_data.get(item_entity)
- if item is None:
- return False
-
- player_inventory.add(item)
- remove_item_entity(grid, item_entity)
-
- message_log.add(f"You pick up the {item.name}.", COLOR_PICKUP)
-
- update_ui()
- return True
-
-def use_item(index: int) -> bool:
- global player, player_inventory
-
- item = player_inventory.get(index)
- if item is None:
- message_log.add("Invalid item selection.", COLOR_INVALID)
- return False
-
- if item.item_type == "health_potion":
- fighter = entity_data.get(player)
- if fighter is None:
- return False
-
- if fighter.hp >= fighter.max_hp:
- message_log.add("You are already at full health!", COLOR_WARNING)
- return False
-
- actual_heal = fighter.heal(item.heal_amount)
- player_inventory.remove(index)
-
- message_log.add(f"You drink the {item.name} and recover {actual_heal} HP!", COLOR_HEAL)
-
- update_ui()
- return True
-
- message_log.add(f"You cannot use the {item.name}.", COLOR_INVALID)
- return False
-
-# =============================================================================
-# Field of View
-# =============================================================================
-
-def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
- global player, target_cursor
-
- for entity in target_grid.entities:
- if entity == player:
- entity.visible = True
- continue
-
- # Targeting cursor is always visible during targeting mode
- if entity == target_cursor:
- 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:
- 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:
- 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:
- 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
-
- if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT:
- return
-
- blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
-
- if blocker is not None:
- perform_attack(player, blocker)
- enemy_turn()
- elif grid.at(target_x, target_y).walkable:
- player.x = target_x
- player.y = target_y
- update_fov(grid, fov_layer, target_x, target_y)
- enemy_turn()
-
- update_ui()
-
-# =============================================================================
-# Enemy AI
-# =============================================================================
-
-def enemy_turn() -> None:
- global player, grid, game_over
-
- if game_over:
- return
-
- player_x, player_y = int(player.x), int(player.y)
-
- 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)
-
- if not grid.is_in_fov(ex, ey):
- continue
-
- dx = player_x - ex
- dy = player_y - ey
-
- if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
- perform_attack(enemy, player)
- else:
- 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:
- global grid
-
- dx = 0
- dy = 0
-
- if px < ex:
- dx = -1
- elif px > ex:
- dx = 1
-
- if py < ey:
- dy = -1
- elif py > ey:
- dy = 1
-
- 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):
- enemy.x = ex + dx
- elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
- enemy.y = ey + dy
-
-# =============================================================================
-# UI Updates
-# =============================================================================
-
-def update_ui() -> None:
- global player, health_bar, inventory_panel, player_inventory
-
- if player in entity_data:
- fighter = entity_data[player]
- health_bar.update(fighter.hp, fighter.max_hp)
-
- if player_inventory:
- inventory_panel.update(player_inventory)
-
-# =============================================================================
-# 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=(20, GAME_AREA_Y),
- size=(700, GAME_AREA_HEIGHT - 20),
- grid_size=(GRID_WIDTH, GRID_HEIGHT),
- texture=texture
-)
-grid.zoom = 1.0
-
-# Generate initial dungeon
-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)
-
-entity_data[player] = Fighter(
- hp=30,
- max_hp=30,
- attack=5,
- defense=2,
- name="Player",
- is_player=True
-)
-
-# Create player inventory
-player_inventory = Inventory(capacity=10)
-
-# Spawn enemies and items
-for i, room in enumerate(rooms):
- if i == 0:
- continue
- spawn_enemies_in_room(grid, room, texture)
- spawn_items_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)
-
-# =============================================================================
-# Create UI Elements
-# =============================================================================
-
-# Title bar
-title = mcrfpy.Caption(
- pos=(20, 10),
- text="Part 9: Ranged Combat"
-)
-title.fill_color = mcrfpy.Color(255, 255, 255)
-title.font_size = 24
-scene.children.append(title)
-
-# Instructions
-instructions = mcrfpy.Caption(
- pos=(280, 15),
- text="WASD: Move | F: Ranged attack | G: Pickup | 1-5: Use | R: Restart"
-)
-instructions.fill_color = mcrfpy.Color(180, 180, 180)
-instructions.font_size = 14
-scene.children.append(instructions)
-
-# Health Bar
-health_bar = HealthBar(
- x=730,
- y=10,
- width=280,
- height=30
-)
-health_bar.add_to_scene(scene)
-
-# Mode Display
-mode_display = ModeDisplay(x=20, y=40)
-mode_display.add_to_scene(scene)
-
-# Inventory Panel
-inventory_panel = InventoryPanel(
- x=730,
- y=GAME_AREA_Y,
- width=280,
- height=150
-)
-inventory_panel.add_to_scene(scene)
-
-# Message Log
-message_log = MessageLog(
- x=20,
- y=768 - UI_BOTTOM_HEIGHT + 10,
- width=990,
- height=UI_BOTTOM_HEIGHT - 20,
- max_messages=6
-)
-message_log.add_to_scene(scene)
-
-# Initial messages
-message_log.add("Welcome to the dungeon!", COLOR_INFO)
-message_log.add("Press F to enter targeting mode for ranged attacks.", COLOR_INFO)
-
-# Initialize UI
-update_ui()
-
-# =============================================================================
-# Input Handling
-# =============================================================================
-
-def restart_game() -> None:
- global player, grid, fov_layer, game_over, entity_data, item_data, rooms
- global player_inventory, game_mode, target_cursor
-
- # Exit targeting mode if active
- if game_mode == GameMode.TARGETING:
- exit_targeting_mode()
-
- game_over = False
- game_mode = GameMode.NORMAL
-
- entity_data.clear()
- item_data.clear()
-
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- fill_with_walls(grid)
- init_explored()
- message_log.clear()
-
- 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)
-
- if rooms:
- new_x, new_y = rooms[0].center
- else:
- new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
-
- 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
- )
-
- player_inventory = Inventory(capacity=10)
-
- for i, room in enumerate(rooms):
- if i == 0:
- continue
- spawn_enemies_in_room(grid, room, texture)
- spawn_items_in_room(grid, room, texture)
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, new_x, new_y)
-
- message_log.add("A new adventure begins!", COLOR_INFO)
-
- mode_display.update(game_mode)
- update_ui()
-
-def handle_keys(key: str, action: str) -> None:
- global game_over, game_mode
-
- if action != "start":
- return
-
- # Always allow restart and quit
- if key == "R":
- restart_game()
- return
-
- if key == "Escape":
- if game_mode == GameMode.TARGETING:
- exit_targeting_mode()
- message_log.add("Targeting cancelled.", COLOR_INFO)
- return
- else:
- mcrfpy.exit()
- return
-
- if game_over:
- return
-
- # Handle input based on game mode
- if game_mode == GameMode.TARGETING:
- handle_targeting_input(key)
- else:
- handle_normal_input(key)
-
-def handle_normal_input(key: str) -> None:
- """Handle input in normal game mode."""
- # Movement
- 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)
- # Ranged attack (enter targeting mode)
- elif key == "F":
- enter_targeting_mode()
- # Pickup
- elif key == "G" or key == ",":
- pickup_item()
- # Use items
- elif key in ["1", "2", "3", "4", "5"]:
- index = int(key) - 1
- if use_item(index):
- enemy_turn()
- update_ui()
-
-def handle_targeting_input(key: str) -> None:
- """Handle input in targeting mode."""
- if key == "Up" or key == "W":
- move_cursor(0, -1)
- elif key == "Down" or key == "S":
- move_cursor(0, 1)
- elif key == "Left" or key == "A":
- move_cursor(-1, 0)
- elif key == "Right" or key == "D":
- move_cursor(1, 0)
- elif key == "Return" or key == "Space":
- confirm_target()
-
-scene.on_key = handle_keys
-
-# =============================================================================
-# Start the Game
-# =============================================================================
-
-scene.activate()
-print("Part 9 loaded! Press F to enter targeting mode for ranged attacks.")
\ No newline at end of file
diff --git a/docs/tutorials/part_10_save_load/part_10_save_load.py b/docs/tutorials/part_10_save_load/part_10_save_load.py
deleted file mode 100644
index a0c6380..0000000
--- a/docs/tutorials/part_10_save_load/part_10_save_load.py
+++ /dev/null
@@ -1,1565 +0,0 @@
-"""McRogueFace - Part 10: Saving and Loading
-
-Documentation: https://mcrogueface.github.io/tutorial/part_10_save_load
-Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_10_save_load/part_10_save_load.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
-import json
-import os
-from dataclasses import dataclass, field
-from typing import Optional
-from enum import Enum
-
-# =============================================================================
-# Constants
-# =============================================================================
-
-# Sprite indices for CP437 tileset
-SPRITE_WALL = 35 # '#' - wall
-SPRITE_FLOOR = 46 # '.' - floor
-SPRITE_PLAYER = 64 # '@' - player
-SPRITE_CORPSE = 37 # '%' - remains
-SPRITE_POTION = 173 # Potion sprite
-SPRITE_CURSOR = 88 # 'X' - targeting cursor
-
-# Enemy sprites
-SPRITE_GOBLIN = 103 # 'g'
-SPRITE_ORC = 111 # 'o'
-SPRITE_TROLL = 116 # 't'
-
-# Grid dimensions
-GRID_WIDTH = 50
-GRID_HEIGHT = 30
-
-# Room generation parameters
-ROOM_MIN_SIZE = 6
-ROOM_MAX_SIZE = 12
-MAX_ROOMS = 8
-
-# Enemy spawn parameters
-MAX_ENEMIES_PER_ROOM = 3
-
-# Item spawn parameters
-MAX_ITEMS_PER_ROOM = 2
-
-# FOV and targeting settings
-FOV_RADIUS = 8
-RANGED_ATTACK_RANGE = 6
-RANGED_ATTACK_DAMAGE = 4
-
-# Save file location
-SAVE_FILE = "savegame.json"
-
-# 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 colors
-COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200)
-COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150)
-COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50)
-COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100)
-COLOR_HEAL = mcrfpy.Color(100, 255, 100)
-COLOR_PICKUP = mcrfpy.Color(100, 200, 255)
-COLOR_INFO = mcrfpy.Color(100, 100, 255)
-COLOR_WARNING = mcrfpy.Color(255, 200, 50)
-COLOR_INVALID = mcrfpy.Color(255, 100, 100)
-COLOR_RANGED = mcrfpy.Color(255, 255, 100)
-COLOR_SAVE = mcrfpy.Color(100, 255, 200)
-
-# UI Layout constants
-UI_TOP_HEIGHT = 60
-UI_BOTTOM_HEIGHT = 150
-GAME_AREA_Y = UI_TOP_HEIGHT
-GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT
-
-# =============================================================================
-# Game Modes
-# =============================================================================
-
-class GameMode(Enum):
- NORMAL = "normal"
- TARGETING = "targeting"
-
-# =============================================================================
-# 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:
- return self.hp > 0
-
- def take_damage(self, amount: int) -> int:
- actual_damage = min(self.hp, amount)
- self.hp -= actual_damage
- return actual_damage
-
- def heal(self, amount: int) -> int:
- actual_heal = min(self.max_hp - self.hp, amount)
- self.hp += actual_heal
- return actual_heal
-
- def to_dict(self) -> dict:
- """Serialize fighter data to dictionary."""
- return {
- "hp": self.hp,
- "max_hp": self.max_hp,
- "attack": self.attack,
- "defense": self.defense,
- "name": self.name,
- "is_player": self.is_player
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Fighter":
- """Deserialize fighter data from dictionary."""
- return cls(
- hp=data["hp"],
- max_hp=data["max_hp"],
- attack=data["attack"],
- defense=data["defense"],
- name=data["name"],
- is_player=data.get("is_player", False)
- )
-
-# =============================================================================
-# Item Component
-# =============================================================================
-
-@dataclass
-class Item:
- """Data for an item that can be picked up and used."""
- name: str
- item_type: str
- heal_amount: int = 0
-
- def to_dict(self) -> dict:
- """Serialize item data to dictionary."""
- return {
- "name": self.name,
- "item_type": self.item_type,
- "heal_amount": self.heal_amount
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Item":
- """Deserialize item data from dictionary."""
- return cls(
- name=data["name"],
- item_type=data["item_type"],
- heal_amount=data.get("heal_amount", 0)
- )
-
-# =============================================================================
-# Inventory System
-# =============================================================================
-
-@dataclass
-class Inventory:
- """Container for items the player is carrying."""
- capacity: int = 10
- items: list = field(default_factory=list)
-
- def add(self, item: Item) -> bool:
- if len(self.items) >= self.capacity:
- return False
- self.items.append(item)
- return True
-
- def remove(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items.pop(index)
- return None
-
- def get(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items[index]
- return None
-
- def is_full(self) -> bool:
- return len(self.items) >= self.capacity
-
- def count(self) -> int:
- return len(self.items)
-
- def to_dict(self) -> dict:
- """Serialize inventory to dictionary."""
- return {
- "capacity": self.capacity,
- "items": [item.to_dict() for item in self.items]
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Inventory":
- """Deserialize inventory from dictionary."""
- inv = cls(capacity=data.get("capacity", 10))
- inv.items = [Item.from_dict(item_data) for item_data in data.get("items", [])]
- return inv
-
-# =============================================================================
-# Templates
-# =============================================================================
-
-ITEM_TEMPLATES = {
- "health_potion": {
- "name": "Health Potion",
- "sprite": SPRITE_POTION,
-
- "item_type": "health_potion",
- "heal_amount": 10
- }
-}
-
-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)
- }
-}
-
-# =============================================================================
-# Message Log System
-# =============================================================================
-
-class MessageLog:
- """A message log that displays recent game messages with colors."""
-
- def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_messages = max_messages
- self.messages: list[tuple[str, mcrfpy.Color]] = []
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- line_height = 20
- for i in range(max_messages):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 5 + i * line_height),
- text=""
- )
- caption.font_size = 14
- caption.fill_color = mcrfpy.Color(200, 200, 200)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- for caption in self.captions:
- scene.children.append(caption)
-
- def add(self, text: str, color: mcrfpy.Color = None) -> None:
- if color is None:
- color = mcrfpy.Color(200, 200, 200)
-
- self.messages.append((text, color))
-
- while len(self.messages) > self.max_messages:
- self.messages.pop(0)
-
- self._refresh()
-
- def _refresh(self) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(self.messages):
- text, color = self.messages[i]
- caption.text = text
- caption.fill_color = color
- else:
- caption.text = ""
-
- def clear(self) -> None:
- self.messages.clear()
- self._refresh()
-
-# =============================================================================
-# Health Bar System
-# =============================================================================
-
-class HealthBar:
- """A visual health bar using nested frames."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_hp = 30
- self.current_hp = 30
-
- self.bg_frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0)
- self.bg_frame.outline = 2
- self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150)
-
- self.fg_frame = mcrfpy.Frame(
- pos=(x + 2, y + 2),
- size=(width - 4, height - 4)
- )
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- self.fg_frame.outline = 0
-
- self.label = mcrfpy.Caption(
- pos=(x + 5, y + 2),
- text=f"HP: {self.current_hp}/{self.max_hp}"
- )
- self.label.font_size = 16
- self.label.fill_color = mcrfpy.Color(255, 255, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.bg_frame)
- scene.children.append(self.fg_frame)
- scene.children.append(self.label)
-
- def update(self, current_hp: int, max_hp: int) -> None:
- self.current_hp = current_hp
- self.max_hp = max_hp
-
- percent = max(0, current_hp / max_hp) if max_hp > 0 else 0
-
- inner_width = self.width - 4
- self.fg_frame.resize(int(inner_width * percent), self.height - 4)
-
- self.label.text = f"HP: {current_hp}/{max_hp}"
-
- if percent > 0.6:
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- elif percent > 0.3:
- self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0)
- else:
- self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0)
-
-# =============================================================================
-# Inventory Panel
-# =============================================================================
-
-class InventoryPanel:
- """A panel displaying the player's inventory."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- self.title = mcrfpy.Caption(
- pos=(x + 10, y + 5),
- text="Inventory (G:pickup, 1-5:use)"
- )
- self.title.font_size = 14
- self.title.fill_color = mcrfpy.Color(200, 200, 255)
-
- for i in range(5):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 25 + i * 18),
- text=""
- )
- caption.font_size = 13
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- scene.children.append(self.title)
- for caption in self.captions:
- scene.children.append(caption)
-
- def update(self, inventory: Inventory) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(inventory.items):
- item = inventory.items[i]
- caption.text = f"{i+1}. {item.name}"
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- else:
- caption.text = f"{i+1}. ---"
- caption.fill_color = mcrfpy.Color(80, 80, 80)
-
-# =============================================================================
-# Mode Display
-# =============================================================================
-
-class ModeDisplay:
- """Displays the current game mode."""
-
- def __init__(self, x: int, y: int):
- self.caption = mcrfpy.Caption(
- pos=(x, y),
- text="[NORMAL MODE]"
- )
- self.caption.font_size = 16
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.caption)
-
- def update(self, mode: GameMode) -> None:
- if mode == GameMode.NORMAL:
- self.caption.text = "[NORMAL] F:Ranged | S:Save"
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
- elif mode == GameMode.TARGETING:
- self.caption.text = "[TARGETING] Arrows:Move, Enter:Fire, Esc:Cancel"
- self.caption.fill_color = mcrfpy.Color(255, 255, 100)
-
-# =============================================================================
-# Global State
-# =============================================================================
-
-entity_data: dict[mcrfpy.Entity, Fighter] = {}
-item_data: dict[mcrfpy.Entity, Item] = {}
-
-player: Optional[mcrfpy.Entity] = None
-player_inventory: Optional[Inventory] = None
-grid: Optional[mcrfpy.Grid] = None
-fov_layer = None
-texture: Optional[mcrfpy.Texture] = None
-game_over: bool = False
-dungeon_level: int = 1
-
-# Game mode state
-game_mode: GameMode = GameMode.NORMAL
-target_cursor: Optional[mcrfpy.Entity] = None
-target_x: int = 0
-target_y: int = 0
-
-# UI components
-message_log: Optional[MessageLog] = None
-health_bar: Optional[HealthBar] = None
-inventory_panel: Optional[InventoryPanel] = None
-mode_display: Optional[ModeDisplay] = None
-
-# =============================================================================
-# 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:
- global explored
- explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
-
-def mark_explored(x: int, y: int) -> None:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- explored[y][x] = True
-
-def is_explored(x: int, y: int) -> bool:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- return explored[y][x]
- return False
-
-# =============================================================================
-# Save/Load System
-# =============================================================================
-
-def save_game() -> bool:
- """Save the current game state to a JSON file.
-
- Returns:
- True if save succeeded, False otherwise
- """
- global player, player_inventory, grid, explored, dungeon_level
-
- try:
- # Collect tile data
- tiles = []
- for y in range(GRID_HEIGHT):
- row = []
- for x in range(GRID_WIDTH):
- cell = grid.at(x, y)
- row.append({
- "tilesprite": cell.tilesprite,
- "walkable": cell.walkable,
- "transparent": cell.transparent
- })
- tiles.append(row)
-
- # Collect enemy data
- enemies = []
- for entity in grid.entities:
- if entity == player:
- continue
- if entity in entity_data:
- fighter = entity_data[entity]
- enemies.append({
- "x": int(entity.x),
- "y": int(entity.y),
- "type": fighter.name.lower(),
- "fighter": fighter.to_dict()
- })
-
- # Collect ground item data
- items_on_ground = []
- for entity in grid.entities:
- if entity in item_data:
- item = item_data[entity]
- items_on_ground.append({
- "x": int(entity.x),
- "y": int(entity.y),
- "item": item.to_dict()
- })
-
- # Build save data structure
- save_data = {
- "version": 1, # For future compatibility
- "dungeon_level": dungeon_level,
- "player": {
- "x": int(player.x),
- "y": int(player.y),
- "fighter": entity_data[player].to_dict(),
- "inventory": player_inventory.to_dict()
- },
- "tiles": tiles,
- "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)],
- "enemies": enemies,
- "items": items_on_ground
- }
-
- # Write to file
- with open(SAVE_FILE, "w") as f:
- json.dump(save_data, f, indent=2)
-
- message_log.add("Game saved successfully!", COLOR_SAVE)
- return True
-
- except Exception as e:
- message_log.add(f"Failed to save: {str(e)}", COLOR_INVALID)
- print(f"Save error: {e}")
- return False
-
-def load_game() -> bool:
- """Load a saved game from JSON file.
-
- Returns:
- True if load succeeded, False otherwise
- """
- global player, player_inventory, grid, explored, dungeon_level
- global entity_data, item_data, fov_layer, game_over
-
- if not os.path.exists(SAVE_FILE):
- return False
-
- try:
- with open(SAVE_FILE, "r") as f:
- save_data = json.load(f)
-
- # Clear current game state
- entity_data.clear()
- item_data.clear()
-
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- # Restore dungeon level
- dungeon_level = save_data.get("dungeon_level", 1)
-
- # Restore tiles
- tiles = save_data["tiles"]
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- cell = grid.at(x, y)
- tile_data = tiles[y][x]
- cell.tilesprite = tile_data["tilesprite"]
- cell.walkable = tile_data["walkable"]
- cell.transparent = tile_data["transparent"]
-
- # Restore explored state
- global explored
- explored_data = save_data["explored"]
- explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)]
-
- # Restore player
- player_data = save_data["player"]
- player = mcrfpy.Entity(
- grid_pos=(player_data["x"], player_data["y"]),
- texture=texture,
- sprite_index=SPRITE_PLAYER
- )
- grid.entities.append(player)
-
- entity_data[player] = Fighter.from_dict(player_data["fighter"])
- player_inventory = Inventory.from_dict(player_data["inventory"])
-
- # Restore enemies
- for enemy_data in save_data.get("enemies", []):
- enemy_type = enemy_data["type"]
- template = ENEMY_TEMPLATES.get(enemy_type, ENEMY_TEMPLATES["goblin"])
-
- enemy = mcrfpy.Entity(
- grid_pos=(enemy_data["x"], enemy_data["y"]),
- texture=texture,
- sprite_index=template["sprite"]
- )
- enemy.visible = False
-
- grid.entities.append(enemy)
- entity_data[enemy] = Fighter.from_dict(enemy_data["fighter"])
-
- # Restore ground items
- for item_entry in save_data.get("items", []):
- template = ITEM_TEMPLATES.get(
- item_entry["item"]["item_type"],
- ITEM_TEMPLATES["health_potion"]
- )
-
- item_entity = mcrfpy.Entity(
- grid_pos=(item_entry["x"], item_entry["y"]),
- texture=texture,
- sprite_index=template["sprite"]
- )
- item_entity.visible = False
-
- grid.entities.append(item_entity)
- item_data[item_entity] = Item.from_dict(item_entry["item"])
-
- # Reset FOV layer
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- # Compute initial FOV
- update_fov(grid, fov_layer, int(player.x), int(player.y))
-
- game_over = False
-
- message_log.add("Game loaded successfully!", COLOR_SAVE)
- update_ui()
- return True
-
- except Exception as e:
- message_log.add(f"Failed to load: {str(e)}", COLOR_INVALID)
- print(f"Load error: {e}")
- return False
-
-def delete_save() -> bool:
- """Delete the save file.
-
- Returns:
- True if deletion succeeded or file did not exist
- """
- try:
- if os.path.exists(SAVE_FILE):
- os.remove(SAVE_FILE)
- return True
- except Exception as e:
- print(f"Delete save error: {e}")
- return False
-
-def has_save_file() -> bool:
- """Check if a save file exists."""
- return os.path.exists(SAVE_FILE)
-
-# =============================================================================
-# Dungeon Generation
-# =============================================================================
-
-def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
- 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:
- 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:
- 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:
- 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:
- 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:
- 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)
-
- 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:
- 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 is_position_occupied(target_grid, x, y):
- 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 spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
- template = ITEM_TEMPLATES[item_type]
-
- item_entity = mcrfpy.Entity(
- grid_pos=(x, y),
- texture=tex,
- sprite_index=template["sprite"]
- )
-
- item_entity.visible = False
-
- target_grid.entities.append(item_entity)
-
- item_data[item_entity] = Item(
- name=template["name"],
- item_type=template["item_type"],
- heal_amount=template.get("heal_amount", 0)
- )
-
- return item_entity
-
-def spawn_items_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None:
- num_items = random.randint(0, MAX_ITEMS_PER_ROOM)
-
- for _ in range(num_items):
- 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 is_position_occupied(target_grid, x, y):
- continue
-
- spawn_item(target_grid, x, y, "health_potion", tex)
-
-def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
- for entity in target_grid.entities:
- if int(entity.x) == x and int(entity.y) == y:
- return True
- return False
-
-def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
- for entity in target_grid.entities:
- if entity in item_data:
- if int(entity.x) == x and int(entity.y) == y:
- return entity
- return None
-
-def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
- 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:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in entity_data:
- del entity_data[entity]
-
-def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in item_data:
- del item_data[entity]
-
-# =============================================================================
-# Targeting System
-# =============================================================================
-
-def enter_targeting_mode() -> None:
- global game_mode, target_cursor, target_x, target_y, player, grid, texture
-
- target_x = int(player.x)
- target_y = int(player.y)
-
- target_cursor = mcrfpy.Entity(
- grid_pos=(target_x, target_y),
- texture=texture,
- sprite_index=SPRITE_CURSOR
- )
- grid.entities.append(target_cursor)
-
- game_mode = GameMode.TARGETING
-
- message_log.add("Targeting mode: Arrows to aim, Enter to fire, Esc to cancel.", COLOR_INFO)
- mode_display.update(game_mode)
-
-def exit_targeting_mode() -> None:
- global game_mode, target_cursor, grid
-
- if target_cursor is not None:
- for i, e in enumerate(grid.entities):
- if e == target_cursor:
- grid.entities.remove(i)
- break
- target_cursor = None
-
- game_mode = GameMode.NORMAL
- mode_display.update(game_mode)
-
-def move_cursor(dx: int, dy: int) -> None:
- global target_x, target_y, target_cursor, grid, player
-
- new_x = target_x + dx
- new_y = target_y + dy
-
- if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
- return
-
- if not grid.is_in_fov(new_x, new_y):
- message_log.add("You cannot see that location.", COLOR_INVALID)
- return
-
- player_x, player_y = int(player.x), int(player.y)
- distance = abs(new_x - player_x) + abs(new_y - player_y)
- if distance > RANGED_ATTACK_RANGE:
- message_log.add(f"Target is out of range (max {RANGED_ATTACK_RANGE}).", COLOR_WARNING)
- return
-
- target_x = new_x
- target_y = new_y
- target_cursor.x = target_x
- target_cursor.y = target_y
-
- enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
- if enemy and enemy in entity_data:
- fighter = entity_data[enemy]
- message_log.add(f"Target: {fighter.name} (HP: {fighter.hp}/{fighter.max_hp})", COLOR_RANGED)
-
-def confirm_target() -> None:
- global game_mode, target_x, target_y, player, grid
-
- if target_x == int(player.x) and target_y == int(player.y):
- message_log.add("You cannot target yourself!", COLOR_INVALID)
- return
-
- target_enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
-
- if target_enemy is None or target_enemy not in entity_data:
- message_log.add("No valid target at that location.", COLOR_INVALID)
- return
-
- perform_ranged_attack(target_enemy)
- exit_targeting_mode()
- enemy_turn()
- update_ui()
-
-def perform_ranged_attack(target_entity: mcrfpy.Entity) -> None:
- global player, game_over
-
- defender = entity_data.get(target_entity)
- attacker = entity_data.get(player)
-
- if defender is None or attacker is None:
- return
-
- damage = max(1, RANGED_ATTACK_DAMAGE - defender.defense // 2)
-
- defender.take_damage(damage)
-
- message_log.add(
- f"Your ranged attack hits the {defender.name} for {damage} damage!",
- COLOR_RANGED
- )
-
- if not defender.is_alive:
- handle_death(target_entity, defender)
-
-# =============================================================================
-# Combat System
-# =============================================================================
-
-def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
- return max(0, attacker.attack - defender.defense)
-
-def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
- global game_over
-
- attacker = entity_data.get(attacker_entity)
- defender = entity_data.get(defender_entity)
-
- if attacker is None or defender is None:
- return
-
- damage = calculate_damage(attacker, defender)
- defender.take_damage(damage)
-
- if damage > 0:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} for {damage} damage!",
- COLOR_PLAYER_ATTACK
- )
- else:
- message_log.add(
- f"The {attacker.name} hits you for {damage} damage!",
- COLOR_ENEMY_ATTACK
- )
- else:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} but deal no damage.",
- mcrfpy.Color(150, 150, 150)
- )
- else:
- message_log.add(
- f"The {attacker.name} hits but deals no damage.",
- mcrfpy.Color(150, 150, 200)
- )
-
- if not defender.is_alive:
- handle_death(defender_entity, defender)
-
- update_ui()
-
-def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
- global game_over, grid
-
- if fighter.is_player:
- message_log.add("You have died!", COLOR_PLAYER_DEATH)
- message_log.add("Press R to restart or Escape to quit.", COLOR_INFO)
- game_over = True
- entity.sprite_index = SPRITE_CORPSE
- # Delete save on death (permadeath!)
- delete_save()
- else:
- message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH)
- remove_entity(grid, entity)
-
-# =============================================================================
-# Item Actions
-# =============================================================================
-
-def pickup_item() -> bool:
- global player, player_inventory, grid
-
- px, py = int(player.x), int(player.y)
- item_entity = get_item_at(grid, px, py)
-
- if item_entity is None:
- message_log.add("There is nothing to pick up here.", COLOR_INVALID)
- return False
-
- if player_inventory.is_full():
- message_log.add("Your inventory is full!", COLOR_WARNING)
- return False
-
- item = item_data.get(item_entity)
- if item is None:
- return False
-
- player_inventory.add(item)
- remove_item_entity(grid, item_entity)
-
- message_log.add(f"You pick up the {item.name}.", COLOR_PICKUP)
-
- update_ui()
- return True
-
-def use_item(index: int) -> bool:
- global player, player_inventory
-
- item = player_inventory.get(index)
- if item is None:
- message_log.add("Invalid item selection.", COLOR_INVALID)
- return False
-
- if item.item_type == "health_potion":
- fighter = entity_data.get(player)
- if fighter is None:
- return False
-
- if fighter.hp >= fighter.max_hp:
- message_log.add("You are already at full health!", COLOR_WARNING)
- return False
-
- actual_heal = fighter.heal(item.heal_amount)
- player_inventory.remove(index)
-
- message_log.add(f"You drink the {item.name} and recover {actual_heal} HP!", COLOR_HEAL)
-
- update_ui()
- return True
-
- message_log.add(f"You cannot use the {item.name}.", COLOR_INVALID)
- return False
-
-# =============================================================================
-# Field of View
-# =============================================================================
-
-def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
- global player, target_cursor
-
- for entity in target_grid.entities:
- if entity == player:
- entity.visible = True
- continue
-
- if entity == target_cursor:
- 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:
- 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:
- 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:
- global player, grid, fov_layer, game_over
-
- if game_over:
- return
-
- px, py = int(player.x), int(player.y)
- new_target_x = px + dx
- new_target_y = py + dy
-
- if new_target_x < 0 or new_target_x >= GRID_WIDTH or new_target_y < 0 or new_target_y >= GRID_HEIGHT:
- return
-
- blocker = get_blocking_entity_at(grid, new_target_x, new_target_y, exclude=player)
-
- if blocker is not None:
- perform_attack(player, blocker)
- enemy_turn()
- elif grid.at(new_target_x, new_target_y).walkable:
- player.x = new_target_x
- player.y = new_target_y
- update_fov(grid, fov_layer, new_target_x, new_target_y)
- enemy_turn()
-
- update_ui()
-
-# =============================================================================
-# Enemy AI
-# =============================================================================
-
-def enemy_turn() -> None:
- global player, grid, game_over
-
- if game_over:
- return
-
- player_x, player_y = int(player.x), int(player.y)
-
- 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)
-
- if not grid.is_in_fov(ex, ey):
- continue
-
- dx = player_x - ex
- dy = player_y - ey
-
- if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
- perform_attack(enemy, player)
- else:
- 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:
- global grid
-
- dx = 0
- dy = 0
-
- if px < ex:
- dx = -1
- elif px > ex:
- dx = 1
-
- if py < ey:
- dy = -1
- elif py > ey:
- dy = 1
-
- 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):
- enemy.x = ex + dx
- elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
- enemy.y = ey + dy
-
-# =============================================================================
-# UI Updates
-# =============================================================================
-
-def update_ui() -> None:
- global player, health_bar, inventory_panel, player_inventory
-
- if player in entity_data:
- fighter = entity_data[player]
- health_bar.update(fighter.hp, fighter.max_hp)
-
- if player_inventory:
- inventory_panel.update(player_inventory)
-
-# =============================================================================
-# New Game Generation
-# =============================================================================
-
-def generate_new_game() -> None:
- """Generate a fresh dungeon with new player."""
- global player, player_inventory, grid, fov_layer, game_over
- global entity_data, item_data, dungeon_level, game_mode
-
- # Reset state
- game_over = False
- game_mode = GameMode.NORMAL
- dungeon_level = 1
-
- entity_data.clear()
- item_data.clear()
-
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- fill_with_walls(grid)
- init_explored()
- message_log.clear()
-
- 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:
- new_x, new_y = rooms[0].center
- else:
- new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
-
- 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
- )
-
- player_inventory = Inventory(capacity=10)
-
- for i, room in enumerate(rooms):
- if i == 0:
- continue
- spawn_enemies_in_room(grid, room, texture)
- spawn_items_in_room(grid, room, texture)
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, new_x, new_y)
-
- mode_display.update(game_mode)
- update_ui()
-
-# =============================================================================
-# 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=(20, GAME_AREA_Y),
- size=(700, GAME_AREA_HEIGHT - 20),
- grid_size=(GRID_WIDTH, GRID_HEIGHT),
- texture=texture
-)
-grid.zoom = 1.0
-
-# 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)
-
-# Add grid to scene
-scene.children.append(grid)
-
-# =============================================================================
-# Create UI Elements
-# =============================================================================
-
-# Title bar
-title = mcrfpy.Caption(
- pos=(20, 10),
- text="Part 10: Save/Load"
-)
-title.fill_color = mcrfpy.Color(255, 255, 255)
-title.font_size = 24
-scene.children.append(title)
-
-# Instructions
-instructions = mcrfpy.Caption(
- pos=(250, 15),
- text="WASD:Move | F:Ranged | G:Pickup | Ctrl+S:Save | R:Restart"
-)
-instructions.fill_color = mcrfpy.Color(180, 180, 180)
-instructions.font_size = 14
-scene.children.append(instructions)
-
-# Health Bar
-health_bar = HealthBar(
- x=730,
- y=10,
- width=280,
- height=30
-)
-health_bar.add_to_scene(scene)
-
-# Mode Display
-mode_display = ModeDisplay(x=20, y=40)
-mode_display.add_to_scene(scene)
-
-# Inventory Panel
-inventory_panel = InventoryPanel(
- x=730,
- y=GAME_AREA_Y,
- width=280,
- height=150
-)
-inventory_panel.add_to_scene(scene)
-
-# Message Log
-message_log = MessageLog(
- x=20,
- y=768 - UI_BOTTOM_HEIGHT + 10,
- width=990,
- height=UI_BOTTOM_HEIGHT - 20,
- max_messages=6
-)
-message_log.add_to_scene(scene)
-
-# =============================================================================
-# Initialize Game (Load or New)
-# =============================================================================
-
-# Initialize explored array
-init_explored()
-
-# Try to load existing save, otherwise generate new game
-if has_save_file():
- message_log.add("Found saved game. Loading...", COLOR_INFO)
- if not load_game():
- message_log.add("Failed to load. Starting new game.", COLOR_WARNING)
- generate_new_game()
- message_log.add("Welcome to the dungeon!", COLOR_INFO)
-else:
- generate_new_game()
- message_log.add("Welcome to the dungeon!", COLOR_INFO)
- message_log.add("Press Ctrl+S to save your progress.", COLOR_INFO)
-
-# =============================================================================
-# Input Handling
-# =============================================================================
-
-def handle_keys(key: str, action: str) -> None:
- global game_over, game_mode
-
- if action != "start":
- return
-
- # Always allow restart
- if key == "R":
- delete_save()
- generate_new_game()
- message_log.add("A new adventure begins!", COLOR_INFO)
- return
-
- if key == "Escape":
- if game_mode == GameMode.TARGETING:
- exit_targeting_mode()
- message_log.add("Targeting cancelled.", COLOR_INFO)
- return
- else:
- # Save on quit
- if not game_over:
- save_game()
- mcrfpy.exit()
- return
-
- # Save game (Ctrl+S or just S when not moving)
- if key == "S" and game_mode == GameMode.NORMAL:
- # Check if this is meant to be a save (could add modifier check)
- # For simplicity, we will use a dedicated save key
- pass
-
- # Dedicated save with period key
- if key == "Period" and game_mode == GameMode.NORMAL and not game_over:
- save_game()
- return
-
- if game_over:
- return
-
- # Handle input based on game mode
- if game_mode == GameMode.TARGETING:
- handle_targeting_input(key)
- else:
- handle_normal_input(key)
-
-def handle_normal_input(key: str) -> None:
- # Movement
- 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)
- # Ranged attack
- elif key == "F":
- enter_targeting_mode()
- # Pickup
- elif key == "G" or key == ",":
- pickup_item()
- # Use items
- elif key in ["1", "2", "3", "4", "5"]:
- index = int(key) - 1
- if use_item(index):
- enemy_turn()
- update_ui()
-
-def handle_targeting_input(key: str) -> None:
- if key == "Up" or key == "W":
- move_cursor(0, -1)
- elif key == "Down" or key == "S":
- move_cursor(0, 1)
- elif key == "Left" or key == "A":
- move_cursor(-1, 0)
- elif key == "Right" or key == "D":
- move_cursor(1, 0)
- elif key == "Return" or key == "Space":
- confirm_target()
-
-scene.on_key = handle_keys
-
-# =============================================================================
-# Start the Game
-# =============================================================================
-
-scene.activate()
-print("Part 10 loaded! Press Period (.) to save, Escape saves and quits.")
\ No newline at end of file
diff --git a/docs/tutorials/part_11_levels/part_11_levels.py b/docs/tutorials/part_11_levels/part_11_levels.py
deleted file mode 100644
index ee31c04..0000000
--- a/docs/tutorials/part_11_levels/part_11_levels.py
+++ /dev/null
@@ -1,1735 +0,0 @@
-"""McRogueFace - Part 11: Multiple Dungeon Levels
-
-Documentation: https://mcrogueface.github.io/tutorial/part_11_levels
-Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_11_levels/part_11_levels.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
-import json
-import os
-from dataclasses import dataclass, field
-from typing import Optional
-from enum import Enum
-
-# =============================================================================
-# Constants
-# =============================================================================
-
-# Sprite indices for CP437 tileset
-SPRITE_WALL = 35 # '#' - wall
-SPRITE_FLOOR = 46 # '.' - floor
-SPRITE_PLAYER = 64 # '@' - player
-SPRITE_CORPSE = 37 # '%' - remains
-SPRITE_POTION = 173 # Potion sprite
-SPRITE_CURSOR = 88 # 'X' - targeting cursor
-SPRITE_STAIRS_DOWN = 62 # '>' - stairs down
-
-# Enemy sprites
-SPRITE_GOBLIN = 103 # 'g'
-SPRITE_ORC = 111 # 'o'
-SPRITE_TROLL = 116 # 't'
-
-# Grid dimensions
-GRID_WIDTH = 50
-GRID_HEIGHT = 30
-
-# Room generation parameters
-ROOM_MIN_SIZE = 6
-ROOM_MAX_SIZE = 12
-MAX_ROOMS = 8
-
-# FOV and targeting settings
-FOV_RADIUS = 8
-RANGED_ATTACK_RANGE = 6
-RANGED_ATTACK_DAMAGE = 4
-
-# Save file location
-SAVE_FILE = "savegame.json"
-
-# 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 colors
-COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200)
-COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150)
-COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50)
-COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100)
-COLOR_HEAL = mcrfpy.Color(100, 255, 100)
-COLOR_PICKUP = mcrfpy.Color(100, 200, 255)
-COLOR_INFO = mcrfpy.Color(100, 100, 255)
-COLOR_WARNING = mcrfpy.Color(255, 200, 50)
-COLOR_INVALID = mcrfpy.Color(255, 100, 100)
-COLOR_RANGED = mcrfpy.Color(255, 255, 100)
-COLOR_SAVE = mcrfpy.Color(100, 255, 200)
-COLOR_DESCEND = mcrfpy.Color(200, 200, 255)
-
-# UI Layout constants
-UI_TOP_HEIGHT = 60
-UI_BOTTOM_HEIGHT = 150
-GAME_AREA_Y = UI_TOP_HEIGHT
-GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT
-
-# =============================================================================
-# Game Modes
-# =============================================================================
-
-class GameMode(Enum):
- NORMAL = "normal"
- TARGETING = "targeting"
-
-# =============================================================================
-# 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:
- return self.hp > 0
-
- def take_damage(self, amount: int) -> int:
- actual_damage = min(self.hp, amount)
- self.hp -= actual_damage
- return actual_damage
-
- def heal(self, amount: int) -> int:
- actual_heal = min(self.max_hp - self.hp, amount)
- self.hp += actual_heal
- return actual_heal
-
- def to_dict(self) -> dict:
- return {
- "hp": self.hp,
- "max_hp": self.max_hp,
- "attack": self.attack,
- "defense": self.defense,
- "name": self.name,
- "is_player": self.is_player
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Fighter":
- return cls(
- hp=data["hp"],
- max_hp=data["max_hp"],
- attack=data["attack"],
- defense=data["defense"],
- name=data["name"],
- is_player=data.get("is_player", False)
- )
-
-# =============================================================================
-# Item Component
-# =============================================================================
-
-@dataclass
-class Item:
- """Data for an item that can be picked up and used."""
- name: str
- item_type: str
- heal_amount: int = 0
-
- def to_dict(self) -> dict:
- return {
- "name": self.name,
- "item_type": self.item_type,
- "heal_amount": self.heal_amount
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Item":
- return cls(
- name=data["name"],
- item_type=data["item_type"],
- heal_amount=data.get("heal_amount", 0)
- )
-
-# =============================================================================
-# Inventory System
-# =============================================================================
-
-@dataclass
-class Inventory:
- """Container for items the player is carrying."""
- capacity: int = 10
- items: list = field(default_factory=list)
-
- def add(self, item: Item) -> bool:
- if len(self.items) >= self.capacity:
- return False
- self.items.append(item)
- return True
-
- def remove(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items.pop(index)
- return None
-
- def get(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items[index]
- return None
-
- def is_full(self) -> bool:
- return len(self.items) >= self.capacity
-
- def count(self) -> int:
- return len(self.items)
-
- def to_dict(self) -> dict:
- return {
- "capacity": self.capacity,
- "items": [item.to_dict() for item in self.items]
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Inventory":
- inv = cls(capacity=data.get("capacity", 10))
- inv.items = [Item.from_dict(item_data) for item_data in data.get("items", [])]
- return inv
-
-# =============================================================================
-# Templates
-# =============================================================================
-
-ITEM_TEMPLATES = {
- "health_potion": {
- "name": "Health Potion",
- "sprite": SPRITE_POTION,
-
- "item_type": "health_potion",
- "heal_amount": 10
- },
- "greater_health_potion": {
- "name": "Greater Health Potion",
- "sprite": SPRITE_POTION,
-
- "item_type": "health_potion",
- "heal_amount": 20
- }
-}
-
-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)
- }
-}
-
-# =============================================================================
-# Difficulty Scaling
-# =============================================================================
-
-def get_max_enemies_per_room(level: int) -> int:
- """Get maximum enemies per room based on dungeon level."""
- return min(2 + level, 6)
-
-def get_max_items_per_room(level: int) -> int:
- """Get maximum items per room based on dungeon level."""
- return min(1 + level // 2, 3)
-
-def get_enemy_weights(level: int) -> list[tuple[str, float]]:
- """Get enemy spawn weights based on dungeon level.
-
- Returns list of (enemy_type, cumulative_weight) tuples.
- """
- if level <= 2:
- # Levels 1-2: Mostly goblins
- return [("goblin", 0.8), ("orc", 0.95), ("troll", 1.0)]
- elif level <= 4:
- # Levels 3-4: More orcs
- return [("goblin", 0.5), ("orc", 0.85), ("troll", 1.0)]
- else:
- # Level 5+: Dangerous mix
- return [("goblin", 0.3), ("orc", 0.6), ("troll", 1.0)]
-
-def get_item_weights(level: int) -> list[tuple[str, float]]:
- """Get item spawn weights based on dungeon level."""
- if level <= 2:
- return [("health_potion", 1.0)]
- else:
- return [("health_potion", 0.7), ("greater_health_potion", 1.0)]
-
-# =============================================================================
-# Message Log System
-# =============================================================================
-
-class MessageLog:
- """A message log that displays recent game messages with colors."""
-
- def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_messages = max_messages
- self.messages: list[tuple[str, mcrfpy.Color]] = []
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- line_height = 20
- for i in range(max_messages):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 5 + i * line_height),
- text=""
- )
- caption.font_size = 14
- caption.fill_color = mcrfpy.Color(200, 200, 200)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- for caption in self.captions:
- scene.children.append(caption)
-
- def add(self, text: str, color: mcrfpy.Color = None) -> None:
- if color is None:
- color = mcrfpy.Color(200, 200, 200)
-
- self.messages.append((text, color))
-
- while len(self.messages) > self.max_messages:
- self.messages.pop(0)
-
- self._refresh()
-
- def _refresh(self) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(self.messages):
- text, color = self.messages[i]
- caption.text = text
- caption.fill_color = color
- else:
- caption.text = ""
-
- def clear(self) -> None:
- self.messages.clear()
- self._refresh()
-
-# =============================================================================
-# Health Bar System
-# =============================================================================
-
-class HealthBar:
- """A visual health bar using nested frames."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_hp = 30
- self.current_hp = 30
-
- self.bg_frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0)
- self.bg_frame.outline = 2
- self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150)
-
- self.fg_frame = mcrfpy.Frame(
- pos=(x + 2, y + 2),
- size=(width - 4, height - 4)
- )
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- self.fg_frame.outline = 0
-
- self.label = mcrfpy.Caption(
- pos=(x + 5, y + 2),
- text=f"HP: {self.current_hp}/{self.max_hp}"
- )
- self.label.font_size = 16
- self.label.fill_color = mcrfpy.Color(255, 255, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.bg_frame)
- scene.children.append(self.fg_frame)
- scene.children.append(self.label)
-
- def update(self, current_hp: int, max_hp: int) -> None:
- self.current_hp = current_hp
- self.max_hp = max_hp
-
- percent = max(0, current_hp / max_hp) if max_hp > 0 else 0
-
- inner_width = self.width - 4
- self.fg_frame.resize(int(inner_width * percent), self.height - 4)
-
- self.label.text = f"HP: {current_hp}/{max_hp}"
-
- if percent > 0.6:
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- elif percent > 0.3:
- self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0)
- else:
- self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0)
-
-# =============================================================================
-# Inventory Panel
-# =============================================================================
-
-class InventoryPanel:
- """A panel displaying the player's inventory."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- self.title = mcrfpy.Caption(
- pos=(x + 10, y + 5),
- text="Inventory (G:pickup, 1-5:use)"
- )
- self.title.font_size = 14
- self.title.fill_color = mcrfpy.Color(200, 200, 255)
-
- for i in range(5):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 25 + i * 18),
- text=""
- )
- caption.font_size = 13
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- scene.children.append(self.title)
- for caption in self.captions:
- scene.children.append(caption)
-
- def update(self, inventory: Inventory) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(inventory.items):
- item = inventory.items[i]
- caption.text = f"{i+1}. {item.name}"
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- else:
- caption.text = f"{i+1}. ---"
- caption.fill_color = mcrfpy.Color(80, 80, 80)
-
-# =============================================================================
-# Level Display
-# =============================================================================
-
-class LevelDisplay:
- """Displays current dungeon level."""
-
- def __init__(self, x: int, y: int):
- self.caption = mcrfpy.Caption(
- pos=(x, y),
- text="Level: 1"
- )
- self.caption.font_size = 18
- self.caption.fill_color = mcrfpy.Color(200, 200, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.caption)
-
- def update(self, level: int) -> None:
- self.caption.text = f"Dungeon Level: {level}"
-
-# =============================================================================
-# Mode Display
-# =============================================================================
-
-class ModeDisplay:
- """Displays the current game mode."""
-
- def __init__(self, x: int, y: int):
- self.caption = mcrfpy.Caption(
- pos=(x, y),
- text="[NORMAL MODE]"
- )
- self.caption.font_size = 16
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.caption)
-
- def update(self, mode: GameMode) -> None:
- if mode == GameMode.NORMAL:
- self.caption.text = "[NORMAL] F:Ranged | >:Descend"
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
- elif mode == GameMode.TARGETING:
- self.caption.text = "[TARGETING] Arrows:Move, Enter:Fire, Esc:Cancel"
- self.caption.fill_color = mcrfpy.Color(255, 255, 100)
-
-# =============================================================================
-# Global State
-# =============================================================================
-
-entity_data: dict[mcrfpy.Entity, Fighter] = {}
-item_data: dict[mcrfpy.Entity, Item] = {}
-
-player: Optional[mcrfpy.Entity] = None
-player_inventory: Optional[Inventory] = None
-grid: Optional[mcrfpy.Grid] = None
-fov_layer = None
-texture: Optional[mcrfpy.Texture] = None
-game_over: bool = False
-dungeon_level: int = 1
-
-# Stairs position
-stairs_position: tuple[int, int] = (0, 0)
-
-# Game mode state
-game_mode: GameMode = GameMode.NORMAL
-target_cursor: Optional[mcrfpy.Entity] = None
-target_x: int = 0
-target_y: int = 0
-
-# UI components
-message_log: Optional[MessageLog] = None
-health_bar: Optional[HealthBar] = None
-inventory_panel: Optional[InventoryPanel] = None
-mode_display: Optional[ModeDisplay] = None
-level_display: Optional[LevelDisplay] = None
-
-# =============================================================================
-# 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:
- global explored
- explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
-
-def mark_explored(x: int, y: int) -> None:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- explored[y][x] = True
-
-def is_explored(x: int, y: int) -> bool:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- return explored[y][x]
- return False
-
-# =============================================================================
-# Dungeon Generation
-# =============================================================================
-
-def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
- 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:
- 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:
- 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:
- 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:
- 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)
-
-def place_stairs(target_grid: mcrfpy.Grid, x: int, y: int) -> None:
- """Place stairs down at the given position."""
- global stairs_position
-
- cell = target_grid.at(x, y)
- cell.tilesprite = SPRITE_STAIRS_DOWN
- cell.walkable = True
- cell.transparent = True
-
- stairs_position = (x, y)
-
-def generate_dungeon(target_grid: mcrfpy.Grid, level: int) -> tuple[int, int]:
- """Generate a new dungeon level.
-
- Args:
- target_grid: The grid to generate into
- level: Current dungeon level (affects difficulty)
-
- Returns:
- Player starting position (x, y)
- """
- global stairs_position
-
- fill_with_walls(target_grid)
-
- 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)
-
- rooms.append(new_room)
-
- # Place stairs in the last room
- if rooms:
- stairs_x, stairs_y = rooms[-1].center
- place_stairs(target_grid, stairs_x, stairs_y)
-
- # Return starting position (first room center)
- if rooms:
- return rooms[0].center
- else:
- return GRID_WIDTH // 2, GRID_HEIGHT // 2
-
-# =============================================================================
-# Entity Management
-# =============================================================================
-
-def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
- 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)
-
- 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, level: int) -> None:
- """Spawn enemies in a room with level-scaled difficulty."""
- max_enemies = get_max_enemies_per_room(level)
- num_enemies = random.randint(0, max_enemies)
- enemy_weights = get_enemy_weights(level)
-
- 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 is_position_occupied(target_grid, x, y):
- continue
-
- # Select enemy type based on weights
- roll = random.random()
- enemy_type = "goblin"
- for etype, threshold in enemy_weights:
- if roll < threshold:
- enemy_type = etype
- break
-
- spawn_enemy(target_grid, x, y, enemy_type, tex)
-
-def spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
- template = ITEM_TEMPLATES[item_type]
-
- item_entity = mcrfpy.Entity(
- grid_pos=(x, y),
- texture=tex,
- sprite_index=template["sprite"]
- )
-
- item_entity.visible = False
-
- target_grid.entities.append(item_entity)
-
- item_data[item_entity] = Item(
- name=template["name"],
- item_type=template["item_type"],
- heal_amount=template.get("heal_amount", 0)
- )
-
- return item_entity
-
-def spawn_items_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture, level: int) -> None:
- """Spawn items in a room with level-scaled variety."""
- max_items = get_max_items_per_room(level)
- num_items = random.randint(0, max_items)
- item_weights = get_item_weights(level)
-
- for _ in range(num_items):
- 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 is_position_occupied(target_grid, x, y):
- continue
-
- # Select item type based on weights
- roll = random.random()
- item_type = "health_potion"
- for itype, threshold in item_weights:
- if roll < threshold:
- item_type = itype
- break
-
- spawn_item(target_grid, x, y, item_type, tex)
-
-def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
- for entity in target_grid.entities:
- if int(entity.x) == x and int(entity.y) == y:
- return True
- return False
-
-def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
- for entity in target_grid.entities:
- if entity in item_data:
- if int(entity.x) == x and int(entity.y) == y:
- return entity
- return None
-
-def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
- 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:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in entity_data:
- del entity_data[entity]
-
-def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in item_data:
- del item_data[entity]
-
-def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None:
- """Remove all entities except the player."""
- global entity_data, item_data
-
- entities_to_remove = []
- for entity in target_grid.entities:
- if entity in entity_data and entity_data[entity].is_player:
- continue
- entities_to_remove.append(entity)
-
- for entity in entities_to_remove:
- if entity in entity_data:
- del entity_data[entity]
- if entity in item_data:
- del item_data[entity]
-
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
-
-# =============================================================================
-# Level Transition
-# =============================================================================
-
-def descend_stairs() -> bool:
- """Attempt to descend stairs at player's position.
-
- Returns:
- True if descended, False if no stairs here
- """
- global player, dungeon_level, grid, fov_layer, stairs_position
-
- px, py = int(player.x), int(player.y)
-
- # Check if player is on stairs
- if (px, py) != stairs_position:
- message_log.add("There are no stairs here.", COLOR_INVALID)
- return False
-
- # Descend to next level
- dungeon_level += 1
-
- # Clear current level's entities (except player)
- clear_entities_except_player(grid)
-
- # Generate new dungeon
- init_explored()
- player_start = generate_dungeon(grid, dungeon_level)
-
- # Move player to starting position
- player.x = player_start[0]
- player.y = player_start[1]
-
- # Spawn enemies and items based on level
- # We need to get the rooms - regenerate them
- # For simplicity, spawn in all floor tiles
- spawn_entities_for_level(grid, texture, dungeon_level)
-
- # Reset FOV
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, player_start[0], player_start[1])
-
- message_log.add(f"You descend to level {dungeon_level}...", COLOR_DESCEND)
- level_display.update(dungeon_level)
- update_ui()
-
- return True
-
-def spawn_entities_for_level(target_grid: mcrfpy.Grid, tex: mcrfpy.Texture, level: int) -> None:
- """Spawn enemies and items throughout the dungeon."""
- # Find all floor tiles and group them into rough areas
- floor_tiles = []
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- cell = target_grid.at(x, y)
- if cell.walkable and cell.tilesprite == SPRITE_FLOOR:
- floor_tiles.append((x, y))
-
- # Spawn enemies
- max_enemies = get_max_enemies_per_room(level) * 3 # Approximate total
- enemy_weights = get_enemy_weights(level)
-
- for _ in range(max_enemies):
- if not floor_tiles:
- break
-
- x, y = random.choice(floor_tiles)
-
- # Don't spawn on player or stairs
- if (x, y) == (int(player.x), int(player.y)):
- continue
- if (x, y) == stairs_position:
- continue
- if is_position_occupied(target_grid, x, y):
- continue
-
- roll = random.random()
- enemy_type = "goblin"
- for etype, threshold in enemy_weights:
- if roll < threshold:
- enemy_type = etype
- break
-
- spawn_enemy(target_grid, x, y, enemy_type, tex)
-
- # Spawn items
- max_items = get_max_items_per_room(level) * 2
- item_weights = get_item_weights(level)
-
- for _ in range(max_items):
- if not floor_tiles:
- break
-
- x, y = random.choice(floor_tiles)
-
- if (x, y) == (int(player.x), int(player.y)):
- continue
- if (x, y) == stairs_position:
- continue
- if is_position_occupied(target_grid, x, y):
- continue
-
- roll = random.random()
- item_type = "health_potion"
- for itype, threshold in item_weights:
- if roll < threshold:
- item_type = itype
- break
-
- spawn_item(target_grid, x, y, item_type, tex)
-
-# =============================================================================
-# Save/Load System
-# =============================================================================
-
-def save_game() -> bool:
- """Save the current game state to a JSON file."""
- global player, player_inventory, grid, explored, dungeon_level, stairs_position
-
- try:
- tiles = []
- for y in range(GRID_HEIGHT):
- row = []
- for x in range(GRID_WIDTH):
- cell = grid.at(x, y)
- row.append({
- "tilesprite": cell.tilesprite,
- "walkable": cell.walkable,
- "transparent": cell.transparent
- })
- tiles.append(row)
-
- enemies = []
- for entity in grid.entities:
- if entity == player:
- continue
- if entity in entity_data:
- fighter = entity_data[entity]
- enemies.append({
- "x": int(entity.x),
- "y": int(entity.y),
- "type": fighter.name.lower(),
- "fighter": fighter.to_dict()
- })
-
- items_on_ground = []
- for entity in grid.entities:
- if entity in item_data:
- item = item_data[entity]
- items_on_ground.append({
- "x": int(entity.x),
- "y": int(entity.y),
- "item": item.to_dict()
- })
-
- save_data = {
- "version": 2,
- "dungeon_level": dungeon_level,
- "stairs_position": list(stairs_position),
- "player": {
- "x": int(player.x),
- "y": int(player.y),
- "fighter": entity_data[player].to_dict(),
- "inventory": player_inventory.to_dict()
- },
- "tiles": tiles,
- "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)],
- "enemies": enemies,
- "items": items_on_ground
- }
-
- with open(SAVE_FILE, "w") as f:
- json.dump(save_data, f, indent=2)
-
- message_log.add("Game saved successfully!", COLOR_SAVE)
- return True
-
- except Exception as e:
- message_log.add(f"Failed to save: {str(e)}", COLOR_INVALID)
- return False
-
-def load_game() -> bool:
- """Load a saved game from JSON file."""
- global player, player_inventory, grid, explored, dungeon_level
- global entity_data, item_data, fov_layer, game_over, stairs_position
-
- if not os.path.exists(SAVE_FILE):
- return False
-
- try:
- with open(SAVE_FILE, "r") as f:
- save_data = json.load(f)
-
- entity_data.clear()
- item_data.clear()
-
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- dungeon_level = save_data.get("dungeon_level", 1)
- stairs_position = tuple(save_data.get("stairs_position", [0, 0]))
-
- tiles = save_data["tiles"]
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- cell = grid.at(x, y)
- tile_data = tiles[y][x]
- cell.tilesprite = tile_data["tilesprite"]
- cell.walkable = tile_data["walkable"]
- cell.transparent = tile_data["transparent"]
-
- global explored
- explored_data = save_data["explored"]
- explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)]
-
- player_data = save_data["player"]
- player = mcrfpy.Entity(
- grid_pos=(player_data["x"], player_data["y"]),
- texture=texture,
- sprite_index=SPRITE_PLAYER
- )
- grid.entities.append(player)
-
- entity_data[player] = Fighter.from_dict(player_data["fighter"])
- player_inventory = Inventory.from_dict(player_data["inventory"])
-
- for enemy_data in save_data.get("enemies", []):
- enemy_type = enemy_data["type"]
- template = ENEMY_TEMPLATES.get(enemy_type, ENEMY_TEMPLATES["goblin"])
-
- enemy = mcrfpy.Entity(
- grid_pos=(enemy_data["x"], enemy_data["y"]),
- texture=texture,
- sprite_index=template["sprite"]
- )
- enemy.visible = False
-
- grid.entities.append(enemy)
- entity_data[enemy] = Fighter.from_dict(enemy_data["fighter"])
-
- for item_entry in save_data.get("items", []):
- item_type = item_entry["item"]["item_type"]
- template = ITEM_TEMPLATES.get(item_type, ITEM_TEMPLATES["health_potion"])
-
- item_entity = mcrfpy.Entity(
- grid_pos=(item_entry["x"], item_entry["y"]),
- texture=texture,
- sprite_index=template["sprite"]
- )
- item_entity.visible = False
-
- grid.entities.append(item_entity)
- item_data[item_entity] = Item.from_dict(item_entry["item"])
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, int(player.x), int(player.y))
-
- game_over = False
-
- message_log.add("Game loaded successfully!", COLOR_SAVE)
- level_display.update(dungeon_level)
- update_ui()
- return True
-
- except Exception as e:
- message_log.add(f"Failed to load: {str(e)}", COLOR_INVALID)
- return False
-
-def delete_save() -> bool:
- try:
- if os.path.exists(SAVE_FILE):
- os.remove(SAVE_FILE)
- return True
- except Exception:
- return False
-
-def has_save_file() -> bool:
- return os.path.exists(SAVE_FILE)
-
-# =============================================================================
-# Targeting System
-# =============================================================================
-
-def enter_targeting_mode() -> None:
- global game_mode, target_cursor, target_x, target_y, player, grid, texture
-
- target_x = int(player.x)
- target_y = int(player.y)
-
- target_cursor = mcrfpy.Entity(
- grid_pos=(target_x, target_y),
- texture=texture,
- sprite_index=SPRITE_CURSOR
- )
- grid.entities.append(target_cursor)
-
- game_mode = GameMode.TARGETING
-
- message_log.add("Targeting mode: Arrows to aim, Enter to fire, Esc to cancel.", COLOR_INFO)
- mode_display.update(game_mode)
-
-def exit_targeting_mode() -> None:
- global game_mode, target_cursor, grid
-
- if target_cursor is not None:
- for i, e in enumerate(grid.entities):
- if e == target_cursor:
- grid.entities.remove(i)
- break
- target_cursor = None
-
- game_mode = GameMode.NORMAL
- mode_display.update(game_mode)
-
-def move_cursor(dx: int, dy: int) -> None:
- global target_x, target_y, target_cursor, grid, player
-
- new_x = target_x + dx
- new_y = target_y + dy
-
- if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
- return
-
- if not grid.is_in_fov(new_x, new_y):
- message_log.add("You cannot see that location.", COLOR_INVALID)
- return
-
- player_x, player_y = int(player.x), int(player.y)
- distance = abs(new_x - player_x) + abs(new_y - player_y)
- if distance > RANGED_ATTACK_RANGE:
- message_log.add(f"Target is out of range (max {RANGED_ATTACK_RANGE}).", COLOR_WARNING)
- return
-
- target_x = new_x
- target_y = new_y
- target_cursor.x = target_x
- target_cursor.y = target_y
-
- enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
- if enemy and enemy in entity_data:
- fighter = entity_data[enemy]
- message_log.add(f"Target: {fighter.name} (HP: {fighter.hp}/{fighter.max_hp})", COLOR_RANGED)
-
-def confirm_target() -> None:
- global game_mode, target_x, target_y, player, grid
-
- if target_x == int(player.x) and target_y == int(player.y):
- message_log.add("You cannot target yourself!", COLOR_INVALID)
- return
-
- target_enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
-
- if target_enemy is None or target_enemy not in entity_data:
- message_log.add("No valid target at that location.", COLOR_INVALID)
- return
-
- perform_ranged_attack(target_enemy)
- exit_targeting_mode()
- enemy_turn()
- update_ui()
-
-def perform_ranged_attack(target_entity: mcrfpy.Entity) -> None:
- global player, game_over
-
- defender = entity_data.get(target_entity)
- attacker = entity_data.get(player)
-
- if defender is None or attacker is None:
- return
-
- damage = max(1, RANGED_ATTACK_DAMAGE - defender.defense // 2)
-
- defender.take_damage(damage)
-
- message_log.add(
- f"Your ranged attack hits the {defender.name} for {damage} damage!",
- COLOR_RANGED
- )
-
- if not defender.is_alive:
- handle_death(target_entity, defender)
-
-# =============================================================================
-# Combat System
-# =============================================================================
-
-def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
- return max(0, attacker.attack - defender.defense)
-
-def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
- global game_over
-
- attacker = entity_data.get(attacker_entity)
- defender = entity_data.get(defender_entity)
-
- if attacker is None or defender is None:
- return
-
- damage = calculate_damage(attacker, defender)
- defender.take_damage(damage)
-
- if damage > 0:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} for {damage} damage!",
- COLOR_PLAYER_ATTACK
- )
- else:
- message_log.add(
- f"The {attacker.name} hits you for {damage} damage!",
- COLOR_ENEMY_ATTACK
- )
- else:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} but deal no damage.",
- mcrfpy.Color(150, 150, 150)
- )
- else:
- message_log.add(
- f"The {attacker.name} hits but deals no damage.",
- mcrfpy.Color(150, 150, 200)
- )
-
- if not defender.is_alive:
- handle_death(defender_entity, defender)
-
- update_ui()
-
-def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
- global game_over, grid
-
- if fighter.is_player:
- message_log.add("You have died!", COLOR_PLAYER_DEATH)
- message_log.add("Press R to restart or Escape to quit.", COLOR_INFO)
- game_over = True
- entity.sprite_index = SPRITE_CORPSE
- delete_save()
- else:
- message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH)
- remove_entity(grid, entity)
-
-# =============================================================================
-# Item Actions
-# =============================================================================
-
-def pickup_item() -> bool:
- global player, player_inventory, grid
-
- px, py = int(player.x), int(player.y)
- item_entity = get_item_at(grid, px, py)
-
- if item_entity is None:
- message_log.add("There is nothing to pick up here.", COLOR_INVALID)
- return False
-
- if player_inventory.is_full():
- message_log.add("Your inventory is full!", COLOR_WARNING)
- return False
-
- item = item_data.get(item_entity)
- if item is None:
- return False
-
- player_inventory.add(item)
- remove_item_entity(grid, item_entity)
-
- message_log.add(f"You pick up the {item.name}.", COLOR_PICKUP)
-
- update_ui()
- return True
-
-def use_item(index: int) -> bool:
- global player, player_inventory
-
- item = player_inventory.get(index)
- if item is None:
- message_log.add("Invalid item selection.", COLOR_INVALID)
- return False
-
- if item.item_type == "health_potion":
- fighter = entity_data.get(player)
- if fighter is None:
- return False
-
- if fighter.hp >= fighter.max_hp:
- message_log.add("You are already at full health!", COLOR_WARNING)
- return False
-
- actual_heal = fighter.heal(item.heal_amount)
- player_inventory.remove(index)
-
- message_log.add(f"You drink the {item.name} and recover {actual_heal} HP!", COLOR_HEAL)
-
- update_ui()
- return True
-
- message_log.add(f"You cannot use the {item.name}.", COLOR_INVALID)
- return False
-
-# =============================================================================
-# Field of View
-# =============================================================================
-
-def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
- global player, target_cursor
-
- for entity in target_grid.entities:
- if entity == player:
- entity.visible = True
- continue
-
- if entity == target_cursor:
- 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:
- 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:
- 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:
- global player, grid, fov_layer, game_over
-
- if game_over:
- return
-
- px, py = int(player.x), int(player.y)
- new_target_x = px + dx
- new_target_y = py + dy
-
- if new_target_x < 0 or new_target_x >= GRID_WIDTH or new_target_y < 0 or new_target_y >= GRID_HEIGHT:
- return
-
- blocker = get_blocking_entity_at(grid, new_target_x, new_target_y, exclude=player)
-
- if blocker is not None:
- perform_attack(player, blocker)
- enemy_turn()
- elif grid.at(new_target_x, new_target_y).walkable:
- player.x = new_target_x
- player.y = new_target_y
- update_fov(grid, fov_layer, new_target_x, new_target_y)
- enemy_turn()
-
- update_ui()
-
-# =============================================================================
-# Enemy AI
-# =============================================================================
-
-def enemy_turn() -> None:
- global player, grid, game_over
-
- if game_over:
- return
-
- player_x, player_y = int(player.x), int(player.y)
-
- 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)
-
- if not grid.is_in_fov(ex, ey):
- continue
-
- dx = player_x - ex
- dy = player_y - ey
-
- if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
- perform_attack(enemy, player)
- else:
- 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:
- global grid
-
- dx = 0
- dy = 0
-
- if px < ex:
- dx = -1
- elif px > ex:
- dx = 1
-
- if py < ey:
- dy = -1
- elif py > ey:
- dy = 1
-
- 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):
- enemy.x = ex + dx
- elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
- enemy.y = ey + dy
-
-# =============================================================================
-# UI Updates
-# =============================================================================
-
-def update_ui() -> None:
- global player, health_bar, inventory_panel, player_inventory
-
- if player in entity_data:
- fighter = entity_data[player]
- health_bar.update(fighter.hp, fighter.max_hp)
-
- if player_inventory:
- inventory_panel.update(player_inventory)
-
-# =============================================================================
-# New Game Generation
-# =============================================================================
-
-def generate_new_game() -> None:
- """Generate a fresh dungeon with new player."""
- global player, player_inventory, grid, fov_layer, game_over
- global entity_data, item_data, dungeon_level, game_mode
-
- game_over = False
- game_mode = GameMode.NORMAL
- dungeon_level = 1
-
- entity_data.clear()
- item_data.clear()
-
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- init_explored()
- message_log.clear()
-
- player_start = generate_dungeon(grid, dungeon_level)
-
- player = mcrfpy.Entity(
- grid_pos=(player_start[0], player_start[1]),
- 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
- )
-
- player_inventory = Inventory(capacity=10)
-
- spawn_entities_for_level(grid, texture, dungeon_level)
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, player_start[0], player_start[1])
-
- mode_display.update(game_mode)
- level_display.update(dungeon_level)
- update_ui()
-
-# =============================================================================
-# Game Setup
-# =============================================================================
-
-scene = mcrfpy.Scene("game")
-texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
-
-grid = mcrfpy.Grid(
- pos=(20, GAME_AREA_Y),
- size=(700, GAME_AREA_HEIGHT - 20),
- grid_size=(GRID_WIDTH, GRID_HEIGHT),
- texture=texture
-)
-grid.zoom = 1.0
-
-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)
-
-scene.children.append(grid)
-
-# =============================================================================
-# Create UI Elements
-# =============================================================================
-
-title = mcrfpy.Caption(
- pos=(20, 10),
- text="Part 11: Multiple Levels"
-)
-title.fill_color = mcrfpy.Color(255, 255, 255)
-title.font_size = 24
-scene.children.append(title)
-
-instructions = mcrfpy.Caption(
- pos=(260, 15),
- text="WASD:Move | >:Descend | F:Ranged | G:Pickup | R:Restart"
-)
-instructions.fill_color = mcrfpy.Color(180, 180, 180)
-instructions.font_size = 14
-scene.children.append(instructions)
-
-health_bar = HealthBar(x=730, y=10, width=280, height=30)
-health_bar.add_to_scene(scene)
-
-level_display = LevelDisplay(x=730, y=45)
-level_display.add_to_scene(scene)
-
-mode_display = ModeDisplay(x=20, y=40)
-mode_display.add_to_scene(scene)
-
-inventory_panel = InventoryPanel(x=730, y=GAME_AREA_Y, width=280, height=150)
-inventory_panel.add_to_scene(scene)
-
-message_log = MessageLog(
- x=20,
- y=768 - UI_BOTTOM_HEIGHT + 10,
- width=990,
- height=UI_BOTTOM_HEIGHT - 20,
- max_messages=6
-)
-message_log.add_to_scene(scene)
-
-# =============================================================================
-# Initialize Game
-# =============================================================================
-
-init_explored()
-
-if has_save_file():
- message_log.add("Found saved game. Loading...", COLOR_INFO)
- if not load_game():
- message_log.add("Failed to load. Starting new game.", COLOR_WARNING)
- generate_new_game()
- message_log.add("Welcome to the dungeon!", COLOR_INFO)
-else:
- generate_new_game()
- message_log.add("Welcome to the dungeon!", COLOR_INFO)
- message_log.add("Find the stairs (>) to descend deeper.", COLOR_INFO)
-
-# =============================================================================
-# Input Handling
-# =============================================================================
-
-def handle_keys(key: str, action: str) -> None:
- global game_over, game_mode
-
- if action != "start":
- return
-
- if key == "R":
- delete_save()
- generate_new_game()
- message_log.add("A new adventure begins!", COLOR_INFO)
- return
-
- if key == "Escape":
- if game_mode == GameMode.TARGETING:
- exit_targeting_mode()
- message_log.add("Targeting cancelled.", COLOR_INFO)
- return
- else:
- if not game_over:
- save_game()
- mcrfpy.exit()
- return
-
- if key == "Period" and game_mode == GameMode.NORMAL and not game_over:
- save_game()
- return
-
- if game_over:
- return
-
- if game_mode == GameMode.TARGETING:
- handle_targeting_input(key)
- else:
- handle_normal_input(key)
-
-def handle_normal_input(key: str) -> None:
- 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)
- elif key == "F":
- enter_targeting_mode()
- elif key == "G" or key == ",":
- pickup_item()
- elif key == "Period" and mcrfpy.keypressed("LShift"):
- # Shift+. (>) to descend stairs
- descend_stairs()
- elif key in ["1", "2", "3", "4", "5"]:
- index = int(key) - 1
- if use_item(index):
- enemy_turn()
- update_ui()
-
-def handle_targeting_input(key: str) -> None:
- if key == "Up" or key == "W":
- move_cursor(0, -1)
- elif key == "Down" or key == "S":
- move_cursor(0, 1)
- elif key == "Left" or key == "A":
- move_cursor(-1, 0)
- elif key == "Right" or key == "D":
- move_cursor(1, 0)
- elif key == "Return" or key == "Space":
- confirm_target()
-
-scene.on_key = handle_keys
-
-# =============================================================================
-# Start the Game
-# =============================================================================
-
-scene.activate()
-print("Part 11 loaded! Find stairs (>) to descend to deeper levels.")
\ No newline at end of file
diff --git a/docs/tutorials/part_12_experience/part_12_experience.py b/docs/tutorials/part_12_experience/part_12_experience.py
deleted file mode 100644
index ec59009..0000000
--- a/docs/tutorials/part_12_experience/part_12_experience.py
+++ /dev/null
@@ -1,1850 +0,0 @@
-"""McRogueFace - Part 12: Experience and Leveling
-
-Documentation: https://mcrogueface.github.io/tutorial/part_12_experience
-Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_12_experience/part_12_experience.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
-import json
-import os
-from dataclasses import dataclass, field
-from typing import Optional
-from enum import Enum
-
-# =============================================================================
-# Constants
-# =============================================================================
-
-# Sprite indices for CP437 tileset
-SPRITE_WALL = 35 # '#' - wall
-SPRITE_FLOOR = 46 # '.' - floor
-SPRITE_PLAYER = 64 # '@' - player
-SPRITE_CORPSE = 37 # '%' - remains
-SPRITE_POTION = 173 # Potion sprite
-SPRITE_CURSOR = 88 # 'X' - targeting cursor
-SPRITE_STAIRS_DOWN = 62 # '>' - stairs down
-
-# Enemy sprites
-SPRITE_GOBLIN = 103 # 'g'
-SPRITE_ORC = 111 # 'o'
-SPRITE_TROLL = 116 # 't'
-
-# Grid dimensions
-GRID_WIDTH = 50
-GRID_HEIGHT = 30
-
-# Room generation parameters
-ROOM_MIN_SIZE = 6
-ROOM_MAX_SIZE = 12
-MAX_ROOMS = 8
-
-# FOV and targeting settings
-FOV_RADIUS = 8
-RANGED_ATTACK_RANGE = 6
-RANGED_ATTACK_DAMAGE = 4
-
-# Save file location
-SAVE_FILE = "savegame.json"
-
-# XP values for enemies
-ENEMY_XP_VALUES = {
- "goblin": 35,
- "orc": 50,
- "troll": 100
-}
-
-# 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 colors
-COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200)
-COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150)
-COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50)
-COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100)
-COLOR_HEAL = mcrfpy.Color(100, 255, 100)
-COLOR_PICKUP = mcrfpy.Color(100, 200, 255)
-COLOR_INFO = mcrfpy.Color(100, 100, 255)
-COLOR_WARNING = mcrfpy.Color(255, 200, 50)
-COLOR_INVALID = mcrfpy.Color(255, 100, 100)
-COLOR_RANGED = mcrfpy.Color(255, 255, 100)
-COLOR_SAVE = mcrfpy.Color(100, 255, 200)
-COLOR_DESCEND = mcrfpy.Color(200, 200, 255)
-COLOR_LEVEL_UP = mcrfpy.Color(255, 255, 100)
-COLOR_XP = mcrfpy.Color(200, 200, 100)
-
-# UI Layout constants
-UI_TOP_HEIGHT = 60
-UI_BOTTOM_HEIGHT = 150
-GAME_AREA_Y = UI_TOP_HEIGHT
-GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT
-
-# =============================================================================
-# Game Modes
-# =============================================================================
-
-class GameMode(Enum):
- NORMAL = "normal"
- TARGETING = "targeting"
-
-# =============================================================================
-# Fighter Component with XP
-# =============================================================================
-
-@dataclass
-class Fighter:
- """Combat stats for an entity with experience system."""
- hp: int
- max_hp: int
- attack: int
- defense: int
- name: str
- is_player: bool = False
- xp: int = 0
- level: int = 1
-
- @property
- def is_alive(self) -> bool:
- return self.hp > 0
-
- @property
- def xp_to_next_level(self) -> int:
- """Calculate XP needed to reach the next level."""
- return self.level * 100
-
- @property
- def xp_progress(self) -> float:
- """Get XP progress as a percentage (0.0 to 1.0)."""
- return self.xp / self.xp_to_next_level if self.xp_to_next_level > 0 else 0
-
- def take_damage(self, amount: int) -> int:
- actual_damage = min(self.hp, amount)
- self.hp -= actual_damage
- return actual_damage
-
- def heal(self, amount: int) -> int:
- actual_heal = min(self.max_hp - self.hp, amount)
- self.hp += actual_heal
- return actual_heal
-
- def gain_xp(self, amount: int) -> bool:
- """Add XP and check for level up.
-
- Args:
- amount: XP to add
-
- Returns:
- True if leveled up, False otherwise
- """
- self.xp += amount
-
- if self.xp >= self.xp_to_next_level:
- self.level_up()
- return True
-
- return False
-
- def level_up(self) -> None:
- """Level up the character, increasing stats."""
- # Subtract XP cost (excess carries over)
- self.xp -= self.xp_to_next_level
-
- self.level += 1
-
- # Stat increases
- hp_increase = 5
- attack_increase = 1
- defense_increase = 1 if self.level % 3 == 0 else 0 # Every 3rd level
-
- self.max_hp += hp_increase
- self.hp = self.max_hp # Full heal on level up!
- self.attack += attack_increase
- self.defense += defense_increase
-
- def to_dict(self) -> dict:
- return {
- "hp": self.hp,
- "max_hp": self.max_hp,
- "attack": self.attack,
- "defense": self.defense,
- "name": self.name,
- "is_player": self.is_player,
- "xp": self.xp,
- "level": self.level
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Fighter":
- return cls(
- hp=data["hp"],
- max_hp=data["max_hp"],
- attack=data["attack"],
- defense=data["defense"],
- name=data["name"],
- is_player=data.get("is_player", False),
- xp=data.get("xp", 0),
- level=data.get("level", 1)
- )
-
-# =============================================================================
-# Item Component
-# =============================================================================
-
-@dataclass
-class Item:
- """Data for an item that can be picked up and used."""
- name: str
- item_type: str
- heal_amount: int = 0
-
- def to_dict(self) -> dict:
- return {
- "name": self.name,
- "item_type": self.item_type,
- "heal_amount": self.heal_amount
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Item":
- return cls(
- name=data["name"],
- item_type=data["item_type"],
- heal_amount=data.get("heal_amount", 0)
- )
-
-# =============================================================================
-# Inventory System
-# =============================================================================
-
-@dataclass
-class Inventory:
- """Container for items the player is carrying."""
- capacity: int = 10
- items: list = field(default_factory=list)
-
- def add(self, item: Item) -> bool:
- if len(self.items) >= self.capacity:
- return False
- self.items.append(item)
- return True
-
- def remove(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items.pop(index)
- return None
-
- def get(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items[index]
- return None
-
- def is_full(self) -> bool:
- return len(self.items) >= self.capacity
-
- def count(self) -> int:
- return len(self.items)
-
- def to_dict(self) -> dict:
- return {
- "capacity": self.capacity,
- "items": [item.to_dict() for item in self.items]
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Inventory":
- inv = cls(capacity=data.get("capacity", 10))
- inv.items = [Item.from_dict(item_data) for item_data in data.get("items", [])]
- return inv
-
-# =============================================================================
-# Templates
-# =============================================================================
-
-ITEM_TEMPLATES = {
- "health_potion": {
- "name": "Health Potion",
- "sprite": SPRITE_POTION,
-
- "item_type": "health_potion",
- "heal_amount": 10
- },
- "greater_health_potion": {
- "name": "Greater Health Potion",
- "sprite": SPRITE_POTION,
-
- "item_type": "health_potion",
- "heal_amount": 20
- }
-}
-
-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)
- }
-}
-
-# =============================================================================
-# Difficulty Scaling
-# =============================================================================
-
-def get_max_enemies_per_room(level: int) -> int:
- return min(2 + level, 6)
-
-def get_max_items_per_room(level: int) -> int:
- return min(1 + level // 2, 3)
-
-def get_enemy_weights(level: int) -> list[tuple[str, float]]:
- if level <= 2:
- return [("goblin", 0.8), ("orc", 0.95), ("troll", 1.0)]
- elif level <= 4:
- return [("goblin", 0.5), ("orc", 0.85), ("troll", 1.0)]
- else:
- return [("goblin", 0.3), ("orc", 0.6), ("troll", 1.0)]
-
-def get_item_weights(level: int) -> list[tuple[str, float]]:
- if level <= 2:
- return [("health_potion", 1.0)]
- else:
- return [("health_potion", 0.7), ("greater_health_potion", 1.0)]
-
-# =============================================================================
-# Message Log System
-# =============================================================================
-
-class MessageLog:
- """A message log that displays recent game messages with colors."""
-
- def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_messages = max_messages
- self.messages: list[tuple[str, mcrfpy.Color]] = []
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- line_height = 20
- for i in range(max_messages):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 5 + i * line_height),
- text=""
- )
- caption.font_size = 14
- caption.fill_color = mcrfpy.Color(200, 200, 200)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- for caption in self.captions:
- scene.children.append(caption)
-
- def add(self, text: str, color: mcrfpy.Color = None) -> None:
- if color is None:
- color = mcrfpy.Color(200, 200, 200)
-
- self.messages.append((text, color))
-
- while len(self.messages) > self.max_messages:
- self.messages.pop(0)
-
- self._refresh()
-
- def _refresh(self) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(self.messages):
- text, color = self.messages[i]
- caption.text = text
- caption.fill_color = color
- else:
- caption.text = ""
-
- def clear(self) -> None:
- self.messages.clear()
- self._refresh()
-
-# =============================================================================
-# Health Bar System
-# =============================================================================
-
-class HealthBar:
- """A visual health bar using nested frames."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_hp = 30
- self.current_hp = 30
-
- self.bg_frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0)
- self.bg_frame.outline = 2
- self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150)
-
- self.fg_frame = mcrfpy.Frame(
- pos=(x + 2, y + 2),
- size=(width - 4, height - 4)
- )
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- self.fg_frame.outline = 0
-
- self.label = mcrfpy.Caption(
- pos=(x + 5, y + 2),
- text=f"HP: {self.current_hp}/{self.max_hp}"
- )
- self.label.font_size = 16
- self.label.fill_color = mcrfpy.Color(255, 255, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.bg_frame)
- scene.children.append(self.fg_frame)
- scene.children.append(self.label)
-
- def update(self, current_hp: int, max_hp: int) -> None:
- self.current_hp = current_hp
- self.max_hp = max_hp
-
- percent = max(0, current_hp / max_hp) if max_hp > 0 else 0
-
- inner_width = self.width - 4
- self.fg_frame.resize(int(inner_width * percent), self.height - 4)
-
- self.label.text = f"HP: {current_hp}/{max_hp}"
-
- if percent > 0.6:
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- elif percent > 0.3:
- self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0)
- else:
- self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0)
-
-# =============================================================================
-# XP Bar System
-# =============================================================================
-
-class XPBar:
- """A visual XP bar showing progress to next level."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
-
- self.bg_frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.bg_frame.fill_color = mcrfpy.Color(40, 40, 80)
- self.bg_frame.outline = 2
- self.bg_frame.outline_color = mcrfpy.Color(100, 100, 150)
-
- self.fg_frame = mcrfpy.Frame(
- pos=(x + 2, y + 2),
- size=(0, height - 4)
- )
- self.fg_frame.fill_color = mcrfpy.Color(200, 200, 50)
- self.fg_frame.outline = 0
-
- self.label = mcrfpy.Caption(
- pos=(x + 5, y + 1),
- text="Level 1 | XP: 0/100"
- )
- self.label.font_size = 14
- self.label.fill_color = mcrfpy.Color(255, 255, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.bg_frame)
- scene.children.append(self.fg_frame)
- scene.children.append(self.label)
-
- def update(self, level: int, xp: int, xp_to_next: int) -> None:
- """Update the XP bar display.
-
- Args:
- level: Current player level
- xp: Current XP
- xp_to_next: XP needed to reach next level
- """
- percent = xp / xp_to_next if xp_to_next > 0 else 0
- percent = min(1.0, max(0.0, percent))
-
- inner_width = self.width - 4
- self.fg_frame.resize(int(inner_width * percent), self.height - 4)
-
- self.label.text = f"Level {level} | XP: {xp}/{xp_to_next}"
-
-# =============================================================================
-# Inventory Panel
-# =============================================================================
-
-class InventoryPanel:
- """A panel displaying the player's inventory."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- self.title = mcrfpy.Caption(
- pos=(x + 10, y + 5),
- text="Inventory (G:pickup, 1-5:use)"
- )
- self.title.font_size = 14
- self.title.fill_color = mcrfpy.Color(200, 200, 255)
-
- for i in range(5):
- caption = mcrfpy.Caption(
- pos=(x + 10, y + 25 + i * 18),
- text=""
- )
- caption.font_size = 13
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- scene.children.append(self.title)
- for caption in self.captions:
- scene.children.append(caption)
-
- def update(self, inventory: Inventory) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(inventory.items):
- item = inventory.items[i]
- caption.text = f"{i+1}. {item.name}"
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- else:
- caption.text = f"{i+1}. ---"
- caption.fill_color = mcrfpy.Color(80, 80, 80)
-
-# =============================================================================
-# Stats Panel
-# =============================================================================
-
-class StatsPanel:
- """A panel displaying player stats."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
-
- self.frame = mcrfpy.Frame(
- pos=(x, y),
- size=(width, height)
- )
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- self.title = mcrfpy.Caption(
- pos=(x + 10, y + 5),
- text="Stats"
- )
- self.title.font_size = 14
- self.title.fill_color = mcrfpy.Color(200, 200, 255)
-
- self.attack_label = mcrfpy.Caption(
- pos=(x + 10, y + 25),
- text="Attack: 5"
- )
- self.attack_label.font_size = 13
- self.attack_label.fill_color = mcrfpy.Color(255, 150, 150)
-
- self.defense_label = mcrfpy.Caption(
- pos=(x + 10, y + 43),
- text="Defense: 2"
- )
- self.defense_label.font_size = 13
- self.defense_label.fill_color = mcrfpy.Color(150, 150, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- scene.children.append(self.title)
- scene.children.append(self.attack_label)
- scene.children.append(self.defense_label)
-
- def update(self, attack: int, defense: int) -> None:
- self.attack_label.text = f"Attack: {attack}"
- self.defense_label.text = f"Defense: {defense}"
-
-# =============================================================================
-# Level Display
-# =============================================================================
-
-class LevelDisplay:
- """Displays current dungeon level."""
-
- def __init__(self, x: int, y: int):
- self.caption = mcrfpy.Caption(
- pos=(x, y),
- text="Dungeon Level: 1"
- )
- self.caption.font_size = 16
- self.caption.fill_color = mcrfpy.Color(200, 200, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.caption)
-
- def update(self, level: int) -> None:
- self.caption.text = f"Dungeon: {level}"
-
-# =============================================================================
-# Mode Display
-# =============================================================================
-
-class ModeDisplay:
- """Displays the current game mode."""
-
- def __init__(self, x: int, y: int):
- self.caption = mcrfpy.Caption(
- pos=(x, y),
- text="[NORMAL MODE]"
- )
- self.caption.font_size = 16
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.caption)
-
- def update(self, mode: GameMode) -> None:
- if mode == GameMode.NORMAL:
- self.caption.text = "[NORMAL] F:Ranged | >:Descend"
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
- elif mode == GameMode.TARGETING:
- self.caption.text = "[TARGETING] Arrows:Move, Enter:Fire, Esc:Cancel"
- self.caption.fill_color = mcrfpy.Color(255, 255, 100)
-
-# =============================================================================
-# Global State
-# =============================================================================
-
-entity_data: dict[mcrfpy.Entity, Fighter] = {}
-item_data: dict[mcrfpy.Entity, Item] = {}
-
-player: Optional[mcrfpy.Entity] = None
-player_inventory: Optional[Inventory] = None
-grid: Optional[mcrfpy.Grid] = None
-fov_layer = None
-texture: Optional[mcrfpy.Texture] = None
-game_over: bool = False
-dungeon_level: int = 1
-stairs_position: tuple[int, int] = (0, 0)
-
-game_mode: GameMode = GameMode.NORMAL
-target_cursor: Optional[mcrfpy.Entity] = None
-target_x: int = 0
-target_y: int = 0
-
-# UI components
-message_log: Optional[MessageLog] = None
-health_bar: Optional[HealthBar] = None
-xp_bar: Optional[XPBar] = None
-inventory_panel: Optional[InventoryPanel] = None
-stats_panel: Optional[StatsPanel] = None
-mode_display: Optional[ModeDisplay] = None
-level_display: Optional[LevelDisplay] = None
-
-# =============================================================================
-# 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:
- global explored
- explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
-
-def mark_explored(x: int, y: int) -> None:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- explored[y][x] = True
-
-def is_explored(x: int, y: int) -> bool:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- return explored[y][x]
- return False
-
-# =============================================================================
-# Dungeon Generation
-# =============================================================================
-
-def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
- 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:
- 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:
- 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:
- 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:
- 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)
-
-def place_stairs(target_grid: mcrfpy.Grid, x: int, y: int) -> None:
- global stairs_position
- cell = target_grid.at(x, y)
- cell.tilesprite = SPRITE_STAIRS_DOWN
- cell.walkable = True
- cell.transparent = True
- stairs_position = (x, y)
-
-def generate_dungeon(target_grid: mcrfpy.Grid, level: int) -> tuple[int, int]:
- global stairs_position
- fill_with_walls(target_grid)
-
- 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)
-
- rooms.append(new_room)
-
- if rooms:
- stairs_x, stairs_y = rooms[-1].center
- place_stairs(target_grid, stairs_x, stairs_y)
-
- if rooms:
- return rooms[0].center
- else:
- return GRID_WIDTH // 2, GRID_HEIGHT // 2
-
-# =============================================================================
-# Entity Management
-# =============================================================================
-
-def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
- 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)
-
- 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_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
- template = ITEM_TEMPLATES[item_type]
-
- item_entity = mcrfpy.Entity(
- grid_pos=(x, y),
- texture=tex,
- sprite_index=template["sprite"]
- )
-
- item_entity.visible = False
-
- target_grid.entities.append(item_entity)
-
- item_data[item_entity] = Item(
- name=template["name"],
- item_type=template["item_type"],
- heal_amount=template.get("heal_amount", 0)
- )
-
- return item_entity
-
-def spawn_entities_for_level(target_grid: mcrfpy.Grid, tex: mcrfpy.Texture, level: int) -> None:
- floor_tiles = []
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- cell = target_grid.at(x, y)
- if cell.walkable and cell.tilesprite == SPRITE_FLOOR:
- floor_tiles.append((x, y))
-
- max_enemies = get_max_enemies_per_room(level) * 3
- enemy_weights = get_enemy_weights(level)
-
- for _ in range(max_enemies):
- if not floor_tiles:
- break
-
- x, y = random.choice(floor_tiles)
-
- if (x, y) == (int(player.x), int(player.y)):
- continue
- if (x, y) == stairs_position:
- continue
- if is_position_occupied(target_grid, x, y):
- continue
-
- roll = random.random()
- enemy_type = "goblin"
- for etype, threshold in enemy_weights:
- if roll < threshold:
- enemy_type = etype
- break
-
- spawn_enemy(target_grid, x, y, enemy_type, tex)
-
- max_items = get_max_items_per_room(level) * 2
- item_weights = get_item_weights(level)
-
- for _ in range(max_items):
- if not floor_tiles:
- break
-
- x, y = random.choice(floor_tiles)
-
- if (x, y) == (int(player.x), int(player.y)):
- continue
- if (x, y) == stairs_position:
- continue
- if is_position_occupied(target_grid, x, y):
- continue
-
- roll = random.random()
- item_type = "health_potion"
- for itype, threshold in item_weights:
- if roll < threshold:
- item_type = itype
- break
-
- spawn_item(target_grid, x, y, item_type, tex)
-
-def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
- for entity in target_grid.entities:
- if int(entity.x) == x and int(entity.y) == y:
- return True
- return False
-
-def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
- for entity in target_grid.entities:
- if entity in item_data:
- if int(entity.x) == x and int(entity.y) == y:
- return entity
- return None
-
-def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
- 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:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in entity_data:
- del entity_data[entity]
-
-def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in item_data:
- del item_data[entity]
-
-def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None:
- global entity_data, item_data
-
- entities_to_remove = []
- for entity in target_grid.entities:
- if entity in entity_data and entity_data[entity].is_player:
- continue
- entities_to_remove.append(entity)
-
- for entity in entities_to_remove:
- if entity in entity_data:
- del entity_data[entity]
- if entity in item_data:
- del item_data[entity]
-
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
-
-# =============================================================================
-# XP and Level Up
-# =============================================================================
-
-def award_xp(enemy_name: str) -> None:
- """Award XP for killing an enemy.
-
- Args:
- enemy_name: Name of the defeated enemy
- """
- global player
-
- fighter = entity_data.get(player)
- if fighter is None:
- return
-
- enemy_type = enemy_name.lower()
- xp_amount = ENEMY_XP_VALUES.get(enemy_type, 35)
-
- leveled_up = fighter.gain_xp(xp_amount)
-
- if leveled_up:
- message_log.add(
- f"You gained {xp_amount} XP and reached level {fighter.level}!",
- COLOR_LEVEL_UP
- )
- message_log.add(
- f"HP +5, Attack +1{', Defense +1' if fighter.level % 3 == 0 else ''}!",
- COLOR_LEVEL_UP
- )
- else:
- message_log.add(f"You gain {xp_amount} XP.", COLOR_XP)
-
- update_ui()
-
-# =============================================================================
-# Level Transition
-# =============================================================================
-
-def descend_stairs() -> bool:
- global player, dungeon_level, grid, fov_layer, stairs_position
-
- px, py = int(player.x), int(player.y)
-
- if (px, py) != stairs_position:
- message_log.add("There are no stairs here.", COLOR_INVALID)
- return False
-
- dungeon_level += 1
-
- clear_entities_except_player(grid)
-
- init_explored()
- player_start = generate_dungeon(grid, dungeon_level)
-
- player.x = player_start[0]
- player.y = player_start[1]
-
- spawn_entities_for_level(grid, texture, dungeon_level)
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, player_start[0], player_start[1])
-
- message_log.add(f"You descend to level {dungeon_level}...", COLOR_DESCEND)
- level_display.update(dungeon_level)
- update_ui()
-
- return True
-
-# =============================================================================
-# Save/Load System
-# =============================================================================
-
-def save_game() -> bool:
- global player, player_inventory, grid, explored, dungeon_level, stairs_position
-
- try:
- tiles = []
- for y in range(GRID_HEIGHT):
- row = []
- for x in range(GRID_WIDTH):
- cell = grid.at(x, y)
- row.append({
- "tilesprite": cell.tilesprite,
- "walkable": cell.walkable,
- "transparent": cell.transparent
- })
- tiles.append(row)
-
- enemies = []
- for entity in grid.entities:
- if entity == player:
- continue
- if entity in entity_data:
- fighter = entity_data[entity]
- enemies.append({
- "x": int(entity.x),
- "y": int(entity.y),
- "type": fighter.name.lower(),
- "fighter": fighter.to_dict()
- })
-
- items_on_ground = []
- for entity in grid.entities:
- if entity in item_data:
- item = item_data[entity]
- items_on_ground.append({
- "x": int(entity.x),
- "y": int(entity.y),
- "item": item.to_dict()
- })
-
- save_data = {
- "version": 3,
- "dungeon_level": dungeon_level,
- "stairs_position": list(stairs_position),
- "player": {
- "x": int(player.x),
- "y": int(player.y),
- "fighter": entity_data[player].to_dict(),
- "inventory": player_inventory.to_dict()
- },
- "tiles": tiles,
- "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)],
- "enemies": enemies,
- "items": items_on_ground
- }
-
- with open(SAVE_FILE, "w") as f:
- json.dump(save_data, f, indent=2)
-
- message_log.add("Game saved successfully!", COLOR_SAVE)
- return True
-
- except Exception as e:
- message_log.add(f"Failed to save: {str(e)}", COLOR_INVALID)
- return False
-
-def load_game() -> bool:
- global player, player_inventory, grid, explored, dungeon_level
- global entity_data, item_data, fov_layer, game_over, stairs_position
-
- if not os.path.exists(SAVE_FILE):
- return False
-
- try:
- with open(SAVE_FILE, "r") as f:
- save_data = json.load(f)
-
- entity_data.clear()
- item_data.clear()
-
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- dungeon_level = save_data.get("dungeon_level", 1)
- stairs_position = tuple(save_data.get("stairs_position", [0, 0]))
-
- tiles = save_data["tiles"]
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- cell = grid.at(x, y)
- tile_data = tiles[y][x]
- cell.tilesprite = tile_data["tilesprite"]
- cell.walkable = tile_data["walkable"]
- cell.transparent = tile_data["transparent"]
-
- global explored
- explored_data = save_data["explored"]
- explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)]
-
- player_data = save_data["player"]
- player = mcrfpy.Entity(
- grid_pos=(player_data["x"], player_data["y"]),
- texture=texture,
- sprite_index=SPRITE_PLAYER
- )
- grid.entities.append(player)
-
- entity_data[player] = Fighter.from_dict(player_data["fighter"])
- player_inventory = Inventory.from_dict(player_data["inventory"])
-
- for enemy_data in save_data.get("enemies", []):
- enemy_type = enemy_data["type"]
- template = ENEMY_TEMPLATES.get(enemy_type, ENEMY_TEMPLATES["goblin"])
-
- enemy = mcrfpy.Entity(
- grid_pos=(enemy_data["x"], enemy_data["y"]),
- texture=texture,
- sprite_index=template["sprite"]
- )
- enemy.visible = False
-
- grid.entities.append(enemy)
- entity_data[enemy] = Fighter.from_dict(enemy_data["fighter"])
-
- for item_entry in save_data.get("items", []):
- item_type = item_entry["item"]["item_type"]
- template = ITEM_TEMPLATES.get(item_type, ITEM_TEMPLATES["health_potion"])
-
- item_entity = mcrfpy.Entity(
- grid_pos=(item_entry["x"], item_entry["y"]),
- texture=texture,
- sprite_index=template["sprite"]
- )
- item_entity.visible = False
-
- grid.entities.append(item_entity)
- item_data[item_entity] = Item.from_dict(item_entry["item"])
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, int(player.x), int(player.y))
-
- game_over = False
-
- message_log.add("Game loaded successfully!", COLOR_SAVE)
- level_display.update(dungeon_level)
- update_ui()
- return True
-
- except Exception as e:
- message_log.add(f"Failed to load: {str(e)}", COLOR_INVALID)
- return False
-
-def delete_save() -> bool:
- try:
- if os.path.exists(SAVE_FILE):
- os.remove(SAVE_FILE)
- return True
- except Exception:
- return False
-
-def has_save_file() -> bool:
- return os.path.exists(SAVE_FILE)
-
-# =============================================================================
-# Targeting System
-# =============================================================================
-
-def enter_targeting_mode() -> None:
- global game_mode, target_cursor, target_x, target_y, player, grid, texture
-
- target_x = int(player.x)
- target_y = int(player.y)
-
- target_cursor = mcrfpy.Entity(
- grid_pos=(target_x, target_y),
- texture=texture,
- sprite_index=SPRITE_CURSOR
- )
- grid.entities.append(target_cursor)
-
- game_mode = GameMode.TARGETING
-
- message_log.add("Targeting mode: Arrows to aim, Enter to fire, Esc to cancel.", COLOR_INFO)
- mode_display.update(game_mode)
-
-def exit_targeting_mode() -> None:
- global game_mode, target_cursor, grid
-
- if target_cursor is not None:
- for i, e in enumerate(grid.entities):
- if e == target_cursor:
- grid.entities.remove(i)
- break
- target_cursor = None
-
- game_mode = GameMode.NORMAL
- mode_display.update(game_mode)
-
-def move_cursor(dx: int, dy: int) -> None:
- global target_x, target_y, target_cursor, grid, player
-
- new_x = target_x + dx
- new_y = target_y + dy
-
- if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
- return
-
- if not grid.is_in_fov(new_x, new_y):
- message_log.add("You cannot see that location.", COLOR_INVALID)
- return
-
- player_x, player_y = int(player.x), int(player.y)
- distance = abs(new_x - player_x) + abs(new_y - player_y)
- if distance > RANGED_ATTACK_RANGE:
- message_log.add(f"Target is out of range (max {RANGED_ATTACK_RANGE}).", COLOR_WARNING)
- return
-
- target_x = new_x
- target_y = new_y
- target_cursor.x = target_x
- target_cursor.y = target_y
-
- enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
- if enemy and enemy in entity_data:
- fighter = entity_data[enemy]
- message_log.add(f"Target: {fighter.name} (HP: {fighter.hp}/{fighter.max_hp})", COLOR_RANGED)
-
-def confirm_target() -> None:
- global game_mode, target_x, target_y, player, grid
-
- if target_x == int(player.x) and target_y == int(player.y):
- message_log.add("You cannot target yourself!", COLOR_INVALID)
- return
-
- target_enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
-
- if target_enemy is None or target_enemy not in entity_data:
- message_log.add("No valid target at that location.", COLOR_INVALID)
- return
-
- perform_ranged_attack(target_enemy)
- exit_targeting_mode()
- enemy_turn()
- update_ui()
-
-def perform_ranged_attack(target_entity: mcrfpy.Entity) -> None:
- global player, game_over
-
- defender = entity_data.get(target_entity)
- attacker = entity_data.get(player)
-
- if defender is None or attacker is None:
- return
-
- damage = max(1, RANGED_ATTACK_DAMAGE - defender.defense // 2)
-
- defender.take_damage(damage)
-
- message_log.add(
- f"Your ranged attack hits the {defender.name} for {damage} damage!",
- COLOR_RANGED
- )
-
- if not defender.is_alive:
- handle_death(target_entity, defender)
-
-# =============================================================================
-# Combat System
-# =============================================================================
-
-def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
- return max(0, attacker.attack - defender.defense)
-
-def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
- global game_over
-
- attacker = entity_data.get(attacker_entity)
- defender = entity_data.get(defender_entity)
-
- if attacker is None or defender is None:
- return
-
- damage = calculate_damage(attacker, defender)
- defender.take_damage(damage)
-
- if damage > 0:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} for {damage} damage!",
- COLOR_PLAYER_ATTACK
- )
- else:
- message_log.add(
- f"The {attacker.name} hits you for {damage} damage!",
- COLOR_ENEMY_ATTACK
- )
- else:
- if attacker.is_player:
- message_log.add(
- f"You hit the {defender.name} but deal no damage.",
- mcrfpy.Color(150, 150, 150)
- )
- else:
- message_log.add(
- f"The {attacker.name} hits but deals no damage.",
- mcrfpy.Color(150, 150, 200)
- )
-
- if not defender.is_alive:
- handle_death(defender_entity, defender)
-
- update_ui()
-
-def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
- global game_over, grid
-
- if fighter.is_player:
- message_log.add("You have died!", COLOR_PLAYER_DEATH)
- message_log.add("Press R to restart or Escape to quit.", COLOR_INFO)
- game_over = True
- entity.sprite_index = SPRITE_CORPSE
- delete_save()
- else:
- message_log.add(f"The {fighter.name} dies!", COLOR_ENEMY_DEATH)
- # Award XP before removing the entity
- award_xp(fighter.name)
- remove_entity(grid, entity)
-
-# =============================================================================
-# Item Actions
-# =============================================================================
-
-def pickup_item() -> bool:
- global player, player_inventory, grid
-
- px, py = int(player.x), int(player.y)
- item_entity = get_item_at(grid, px, py)
-
- if item_entity is None:
- message_log.add("There is nothing to pick up here.", COLOR_INVALID)
- return False
-
- if player_inventory.is_full():
- message_log.add("Your inventory is full!", COLOR_WARNING)
- return False
-
- item = item_data.get(item_entity)
- if item is None:
- return False
-
- player_inventory.add(item)
- remove_item_entity(grid, item_entity)
-
- message_log.add(f"You pick up the {item.name}.", COLOR_PICKUP)
-
- update_ui()
- return True
-
-def use_item(index: int) -> bool:
- global player, player_inventory
-
- item = player_inventory.get(index)
- if item is None:
- message_log.add("Invalid item selection.", COLOR_INVALID)
- return False
-
- if item.item_type == "health_potion":
- fighter = entity_data.get(player)
- if fighter is None:
- return False
-
- if fighter.hp >= fighter.max_hp:
- message_log.add("You are already at full health!", COLOR_WARNING)
- return False
-
- actual_heal = fighter.heal(item.heal_amount)
- player_inventory.remove(index)
-
- message_log.add(f"You drink the {item.name} and recover {actual_heal} HP!", COLOR_HEAL)
-
- update_ui()
- return True
-
- message_log.add(f"You cannot use the {item.name}.", COLOR_INVALID)
- return False
-
-# =============================================================================
-# Field of View
-# =============================================================================
-
-def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
- global player, target_cursor
-
- for entity in target_grid.entities:
- if entity == player:
- entity.visible = True
- continue
-
- if entity == target_cursor:
- 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:
- 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:
- 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:
- global player, grid, fov_layer, game_over
-
- if game_over:
- return
-
- px, py = int(player.x), int(player.y)
- new_target_x = px + dx
- new_target_y = py + dy
-
- if new_target_x < 0 or new_target_x >= GRID_WIDTH or new_target_y < 0 or new_target_y >= GRID_HEIGHT:
- return
-
- blocker = get_blocking_entity_at(grid, new_target_x, new_target_y, exclude=player)
-
- if blocker is not None:
- perform_attack(player, blocker)
- enemy_turn()
- elif grid.at(new_target_x, new_target_y).walkable:
- player.x = new_target_x
- player.y = new_target_y
- update_fov(grid, fov_layer, new_target_x, new_target_y)
- enemy_turn()
-
- update_ui()
-
-# =============================================================================
-# Enemy AI
-# =============================================================================
-
-def enemy_turn() -> None:
- global player, grid, game_over
-
- if game_over:
- return
-
- player_x, player_y = int(player.x), int(player.y)
-
- 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)
-
- if not grid.is_in_fov(ex, ey):
- continue
-
- dx = player_x - ex
- dy = player_y - ey
-
- if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
- perform_attack(enemy, player)
- else:
- 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:
- global grid
-
- dx = 0
- dy = 0
-
- if px < ex:
- dx = -1
- elif px > ex:
- dx = 1
-
- if py < ey:
- dy = -1
- elif py > ey:
- dy = 1
-
- 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):
- enemy.x = ex + dx
- elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
- enemy.y = ey + dy
-
-# =============================================================================
-# UI Updates
-# =============================================================================
-
-def update_ui() -> None:
- global player, health_bar, xp_bar, inventory_panel, stats_panel, player_inventory
-
- if player in entity_data:
- fighter = entity_data[player]
- health_bar.update(fighter.hp, fighter.max_hp)
- xp_bar.update(fighter.level, fighter.xp, fighter.xp_to_next_level)
- stats_panel.update(fighter.attack, fighter.defense)
-
- if player_inventory:
- inventory_panel.update(player_inventory)
-
-# =============================================================================
-# New Game Generation
-# =============================================================================
-
-def generate_new_game() -> None:
- global player, player_inventory, grid, fov_layer, game_over
- global entity_data, item_data, dungeon_level, game_mode
-
- game_over = False
- game_mode = GameMode.NORMAL
- dungeon_level = 1
-
- entity_data.clear()
- item_data.clear()
-
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- init_explored()
- message_log.clear()
-
- player_start = generate_dungeon(grid, dungeon_level)
-
- player = mcrfpy.Entity(
- grid_pos=(player_start[0], player_start[1]),
- 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,
- xp=0,
- level=1
- )
-
- player_inventory = Inventory(capacity=10)
-
- spawn_entities_for_level(grid, texture, dungeon_level)
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
-
- update_fov(grid, fov_layer, player_start[0], player_start[1])
-
- mode_display.update(game_mode)
- level_display.update(dungeon_level)
- update_ui()
-
-# =============================================================================
-# Game Setup
-# =============================================================================
-
-scene = mcrfpy.Scene("game")
-texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
-
-grid = mcrfpy.Grid(
- pos=(20, GAME_AREA_Y),
- size=(700, GAME_AREA_HEIGHT - 20),
- grid_size=(GRID_WIDTH, GRID_HEIGHT),
- texture=texture
-)
-grid.zoom = 1.0
-
-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)
-
-scene.children.append(grid)
-
-# =============================================================================
-# Create UI Elements
-# =============================================================================
-
-title = mcrfpy.Caption(
- pos=(20, 10),
- text="Part 12: Experience System"
-)
-title.fill_color = mcrfpy.Color(255, 255, 255)
-title.font_size = 24
-scene.children.append(title)
-
-instructions = mcrfpy.Caption(
- pos=(300, 15),
- text="WASD:Move | >:Descend | F:Ranged | G:Pickup"
-)
-instructions.fill_color = mcrfpy.Color(180, 180, 180)
-instructions.font_size = 14
-scene.children.append(instructions)
-
-health_bar = HealthBar(x=730, y=10, width=280, height=25)
-health_bar.add_to_scene(scene)
-
-xp_bar = XPBar(x=730, y=40, width=280, height=20)
-xp_bar.add_to_scene(scene)
-
-level_display = LevelDisplay(x=860, y=65)
-level_display.add_to_scene(scene)
-
-mode_display = ModeDisplay(x=20, y=40)
-mode_display.add_to_scene(scene)
-
-stats_panel = StatsPanel(x=730, y=GAME_AREA_Y, width=140, height=70)
-stats_panel.add_to_scene(scene)
-
-inventory_panel = InventoryPanel(x=730, y=GAME_AREA_Y + 75, width=280, height=120)
-inventory_panel.add_to_scene(scene)
-
-message_log = MessageLog(
- x=20,
- y=768 - UI_BOTTOM_HEIGHT + 10,
- width=990,
- height=UI_BOTTOM_HEIGHT - 20,
- max_messages=6
-)
-message_log.add_to_scene(scene)
-
-# =============================================================================
-# Initialize Game
-# =============================================================================
-
-init_explored()
-
-if has_save_file():
- message_log.add("Found saved game. Loading...", COLOR_INFO)
- if not load_game():
- message_log.add("Failed to load. Starting new game.", COLOR_WARNING)
- generate_new_game()
- message_log.add("Welcome to the dungeon!", COLOR_INFO)
-else:
- generate_new_game()
- message_log.add("Welcome to the dungeon!", COLOR_INFO)
- message_log.add("Kill enemies to gain XP and level up!", COLOR_INFO)
-
-# =============================================================================
-# Input Handling
-# =============================================================================
-
-def handle_keys(key: str, action: str) -> None:
- global game_over, game_mode
-
- if action != "start":
- return
-
- if key == "R":
- delete_save()
- generate_new_game()
- message_log.add("A new adventure begins!", COLOR_INFO)
- return
-
- if key == "Escape":
- if game_mode == GameMode.TARGETING:
- exit_targeting_mode()
- message_log.add("Targeting cancelled.", COLOR_INFO)
- return
- else:
- if not game_over:
- save_game()
- mcrfpy.exit()
- return
-
- if key == "Period" and game_mode == GameMode.NORMAL and not game_over:
- # Check for shift to descend
- descend_stairs()
- return
-
- if game_over:
- return
-
- if game_mode == GameMode.TARGETING:
- handle_targeting_input(key)
- else:
- handle_normal_input(key)
-
-def handle_normal_input(key: str) -> None:
- 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)
- elif key == "F":
- enter_targeting_mode()
- elif key == "G" or key == ",":
- pickup_item()
- elif key in ["1", "2", "3", "4", "5"]:
- index = int(key) - 1
- if use_item(index):
- enemy_turn()
- update_ui()
-
-def handle_targeting_input(key: str) -> None:
- if key == "Up" or key == "W":
- move_cursor(0, -1)
- elif key == "Down" or key == "S":
- move_cursor(0, 1)
- elif key == "Left" or key == "A":
- move_cursor(-1, 0)
- elif key == "Right" or key == "D":
- move_cursor(1, 0)
- elif key == "Return" or key == "Space":
- confirm_target()
-
-scene.on_key = handle_keys
-
-# =============================================================================
-# Start the Game
-# =============================================================================
-
-scene.activate()
-print("Part 12 loaded! Kill enemies to gain XP and level up!")
\ No newline at end of file
diff --git a/docs/tutorials/part_13_equipment/part_13_equipment.py b/docs/tutorials/part_13_equipment/part_13_equipment.py
deleted file mode 100644
index 725f20b..0000000
--- a/docs/tutorials/part_13_equipment/part_13_equipment.py
+++ /dev/null
@@ -1,1798 +0,0 @@
-"""McRogueFace - Part 13: Equipment System
-
-Documentation: https://mcrogueface.github.io/tutorial/part_13_equipment
-Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_13_equipment/part_13_equipment.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
-import json
-import os
-from dataclasses import dataclass, field
-from typing import Optional
-from enum import Enum
-
-# =============================================================================
-# Constants
-# =============================================================================
-
-# Sprite indices for CP437 tileset
-SPRITE_WALL = 35 # '#' - wall
-SPRITE_FLOOR = 46 # '.' - floor
-SPRITE_PLAYER = 64 # '@' - player
-SPRITE_CORPSE = 37 # '%' - remains
-SPRITE_POTION = 173 # Potion sprite
-SPRITE_CURSOR = 88 # 'X' - targeting cursor
-SPRITE_STAIRS_DOWN = 62 # '>' - stairs down
-SPRITE_SWORD = 47 # '/' - weapon
-SPRITE_ARMOR = 91 # '[' - armor
-
-# Enemy sprites
-SPRITE_GOBLIN = 103 # 'g'
-SPRITE_ORC = 111 # 'o'
-SPRITE_TROLL = 116 # 't'
-
-# Grid dimensions
-GRID_WIDTH = 50
-GRID_HEIGHT = 30
-
-# Room generation parameters
-ROOM_MIN_SIZE = 6
-ROOM_MAX_SIZE = 12
-MAX_ROOMS = 8
-
-# FOV and targeting settings
-FOV_RADIUS = 8
-RANGED_ATTACK_RANGE = 6
-RANGED_ATTACK_DAMAGE = 4
-
-# Save file location
-SAVE_FILE = "savegame.json"
-
-# XP values for enemies
-ENEMY_XP_VALUES = {
- "goblin": 35,
- "orc": 50,
- "troll": 100
-}
-
-# 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 colors
-COLOR_PLAYER_ATTACK = mcrfpy.Color(200, 200, 200)
-COLOR_ENEMY_ATTACK = mcrfpy.Color(255, 150, 150)
-COLOR_PLAYER_DEATH = mcrfpy.Color(255, 50, 50)
-COLOR_ENEMY_DEATH = mcrfpy.Color(100, 255, 100)
-COLOR_HEAL = mcrfpy.Color(100, 255, 100)
-COLOR_PICKUP = mcrfpy.Color(100, 200, 255)
-COLOR_INFO = mcrfpy.Color(100, 100, 255)
-COLOR_WARNING = mcrfpy.Color(255, 200, 50)
-COLOR_INVALID = mcrfpy.Color(255, 100, 100)
-COLOR_RANGED = mcrfpy.Color(255, 255, 100)
-COLOR_SAVE = mcrfpy.Color(100, 255, 200)
-COLOR_DESCEND = mcrfpy.Color(200, 200, 255)
-COLOR_LEVEL_UP = mcrfpy.Color(255, 255, 100)
-COLOR_XP = mcrfpy.Color(200, 200, 100)
-COLOR_EQUIP = mcrfpy.Color(150, 200, 255)
-
-# UI Layout constants
-UI_TOP_HEIGHT = 60
-UI_BOTTOM_HEIGHT = 150
-GAME_AREA_Y = UI_TOP_HEIGHT
-GAME_AREA_HEIGHT = 768 - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT
-
-# =============================================================================
-# Game Modes
-# =============================================================================
-
-class GameMode(Enum):
- NORMAL = "normal"
- TARGETING = "targeting"
-
-# =============================================================================
-# Equipment Component
-# =============================================================================
-
-@dataclass
-class Equipment:
- """An equippable item that provides stat bonuses."""
- name: str
- slot: str # "weapon" or "armor"
- attack_bonus: int = 0
- defense_bonus: int = 0
-
- def to_dict(self) -> dict:
- return {
- "name": self.name,
- "slot": self.slot,
- "attack_bonus": self.attack_bonus,
- "defense_bonus": self.defense_bonus
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Equipment":
- return cls(
- name=data["name"],
- slot=data["slot"],
- attack_bonus=data.get("attack_bonus", 0),
- defense_bonus=data.get("defense_bonus", 0)
- )
-
-# =============================================================================
-# Fighter Component with Equipment
-# =============================================================================
-
-@dataclass
-class Fighter:
- """Combat stats for an entity with experience and equipment."""
- hp: int
- max_hp: int
- base_attack: int
- base_defense: int
- name: str
- is_player: bool = False
- xp: int = 0
- level: int = 1
- weapon: Optional[Equipment] = None
- armor: Optional[Equipment] = None
-
- @property
- def attack(self) -> int:
- """Total attack including equipment bonus."""
- bonus = self.weapon.attack_bonus if self.weapon else 0
- return self.base_attack + bonus
-
- @property
- def defense(self) -> int:
- """Total defense including equipment bonus."""
- bonus = self.armor.defense_bonus if self.armor else 0
- return self.base_defense + bonus
-
- @property
- def is_alive(self) -> bool:
- return self.hp > 0
-
- @property
- def xp_to_next_level(self) -> int:
- return self.level * 100
-
- @property
- def xp_progress(self) -> float:
- return self.xp / self.xp_to_next_level if self.xp_to_next_level > 0 else 0
-
- def take_damage(self, amount: int) -> int:
- actual_damage = min(self.hp, amount)
- self.hp -= actual_damage
- return actual_damage
-
- def heal(self, amount: int) -> int:
- actual_heal = min(self.max_hp - self.hp, amount)
- self.hp += actual_heal
- return actual_heal
-
- def gain_xp(self, amount: int) -> bool:
- self.xp += amount
- if self.xp >= self.xp_to_next_level:
- self.level_up()
- return True
- return False
-
- def level_up(self) -> None:
- self.xp -= self.xp_to_next_level
- self.level += 1
- hp_increase = 5
- attack_increase = 1
- defense_increase = 1 if self.level % 3 == 0 else 0
- self.max_hp += hp_increase
- self.hp = self.max_hp
- self.base_attack += attack_increase
- self.base_defense += defense_increase
-
- def equip(self, equipment: Equipment) -> Optional[Equipment]:
- """Equip an item, returning any previously equipped item.
-
- Args:
- equipment: The equipment to equip
-
- Returns:
- Previously equipped item in that slot, or None
- """
- old_equipment = None
-
- if equipment.slot == "weapon":
- old_equipment = self.weapon
- self.weapon = equipment
- elif equipment.slot == "armor":
- old_equipment = self.armor
- self.armor = equipment
-
- return old_equipment
-
- def unequip(self, slot: str) -> Optional[Equipment]:
- """Unequip an item from a slot.
-
- Args:
- slot: "weapon" or "armor"
-
- Returns:
- The unequipped item, or None if slot was empty
- """
- if slot == "weapon":
- item = self.weapon
- self.weapon = None
- return item
- elif slot == "armor":
- item = self.armor
- self.armor = None
- return item
- return None
-
- def to_dict(self) -> dict:
- return {
- "hp": self.hp,
- "max_hp": self.max_hp,
- "base_attack": self.base_attack,
- "base_defense": self.base_defense,
- "name": self.name,
- "is_player": self.is_player,
- "xp": self.xp,
- "level": self.level,
- "weapon": self.weapon.to_dict() if self.weapon else None,
- "armor": self.armor.to_dict() if self.armor else None
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Fighter":
- fighter = cls(
- hp=data["hp"],
- max_hp=data["max_hp"],
- base_attack=data.get("base_attack", data.get("attack", 5)),
- base_defense=data.get("base_defense", data.get("defense", 2)),
- name=data["name"],
- is_player=data.get("is_player", False),
- xp=data.get("xp", 0),
- level=data.get("level", 1)
- )
- if data.get("weapon"):
- fighter.weapon = Equipment.from_dict(data["weapon"])
- if data.get("armor"):
- fighter.armor = Equipment.from_dict(data["armor"])
- return fighter
-
-# =============================================================================
-# Item Component
-# =============================================================================
-
-@dataclass
-class Item:
- """Data for an item that can be picked up and used."""
- name: str
- item_type: str
- heal_amount: int = 0
- equipment: Optional[Equipment] = None
-
- def to_dict(self) -> dict:
- result = {
- "name": self.name,
- "item_type": self.item_type,
- "heal_amount": self.heal_amount
- }
- if self.equipment:
- result["equipment"] = self.equipment.to_dict()
- return result
-
- @classmethod
- def from_dict(cls, data: dict) -> "Item":
- item = cls(
- name=data["name"],
- item_type=data["item_type"],
- heal_amount=data.get("heal_amount", 0)
- )
- if data.get("equipment"):
- item.equipment = Equipment.from_dict(data["equipment"])
- return item
-
-# =============================================================================
-# Inventory System
-# =============================================================================
-
-@dataclass
-class Inventory:
- """Container for items the player is carrying."""
- capacity: int = 10
- items: list = field(default_factory=list)
-
- def add(self, item: Item) -> bool:
- if len(self.items) >= self.capacity:
- return False
- self.items.append(item)
- return True
-
- def remove(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items.pop(index)
- return None
-
- def get(self, index: int) -> Optional[Item]:
- if 0 <= index < len(self.items):
- return self.items[index]
- return None
-
- def is_full(self) -> bool:
- return len(self.items) >= self.capacity
-
- def count(self) -> int:
- return len(self.items)
-
- def to_dict(self) -> dict:
- return {
- "capacity": self.capacity,
- "items": [item.to_dict() for item in self.items]
- }
-
- @classmethod
- def from_dict(cls, data: dict) -> "Inventory":
- inv = cls(capacity=data.get("capacity", 10))
- inv.items = [Item.from_dict(item_data) for item_data in data.get("items", [])]
- return inv
-
-# =============================================================================
-# Templates
-# =============================================================================
-
-ITEM_TEMPLATES = {
- "health_potion": {
- "name": "Health Potion",
- "sprite": SPRITE_POTION,
-
- "item_type": "health_potion",
- "heal_amount": 10
- },
- "greater_health_potion": {
- "name": "Greater Health Potion",
- "sprite": SPRITE_POTION,
-
- "item_type": "health_potion",
- "heal_amount": 20
- },
- # Weapons
- "dagger": {
- "name": "Dagger",
- "sprite": SPRITE_SWORD,
-
- "item_type": "equipment",
- "slot": "weapon",
- "attack_bonus": 2,
- "defense_bonus": 0
- },
- "sword": {
- "name": "Sword",
- "sprite": SPRITE_SWORD,
-
- "item_type": "equipment",
- "slot": "weapon",
- "attack_bonus": 4,
- "defense_bonus": 0
- },
- "great_axe": {
- "name": "Great Axe",
- "sprite": SPRITE_SWORD,
-
- "item_type": "equipment",
- "slot": "weapon",
- "attack_bonus": 6,
- "defense_bonus": -1
- },
- # Armor
- "leather_armor": {
- "name": "Leather Armor",
- "sprite": SPRITE_ARMOR,
-
- "item_type": "equipment",
- "slot": "armor",
- "attack_bonus": 0,
- "defense_bonus": 2
- },
- "chain_mail": {
- "name": "Chain Mail",
- "sprite": SPRITE_ARMOR,
-
- "item_type": "equipment",
- "slot": "armor",
- "attack_bonus": 0,
- "defense_bonus": 4
- },
- "plate_armor": {
- "name": "Plate Armor",
- "sprite": SPRITE_ARMOR,
-
- "item_type": "equipment",
- "slot": "armor",
- "attack_bonus": -1,
- "defense_bonus": 6
- }
-}
-
-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)
- }
-}
-
-# =============================================================================
-# Difficulty Scaling with Equipment
-# =============================================================================
-
-def get_max_enemies_per_room(level: int) -> int:
- return min(2 + level, 6)
-
-def get_max_items_per_room(level: int) -> int:
- return min(1 + level // 2, 4)
-
-def get_enemy_weights(level: int) -> list[tuple[str, float]]:
- if level <= 2:
- return [("goblin", 0.8), ("orc", 0.95), ("troll", 1.0)]
- elif level <= 4:
- return [("goblin", 0.5), ("orc", 0.85), ("troll", 1.0)]
- else:
- return [("goblin", 0.3), ("orc", 0.6), ("troll", 1.0)]
-
-def get_item_weights(level: int) -> list[tuple[str, float]]:
- """Get item spawn weights based on dungeon level."""
- if level <= 1:
- return [
- ("health_potion", 0.7),
- ("dagger", 0.85),
- ("leather_armor", 1.0)
- ]
- elif level <= 3:
- return [
- ("health_potion", 0.4),
- ("greater_health_potion", 0.55),
- ("dagger", 0.65),
- ("sword", 0.75),
- ("leather_armor", 0.85),
- ("chain_mail", 1.0)
- ]
- else:
- return [
- ("health_potion", 0.2),
- ("greater_health_potion", 0.4),
- ("sword", 0.55),
- ("great_axe", 0.7),
- ("chain_mail", 0.85),
- ("plate_armor", 1.0)
- ]
-
-# =============================================================================
-# Message Log System
-# =============================================================================
-
-class MessageLog:
- def __init__(self, x: int, y: int, width: int, height: int, max_messages: int = 6):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
- self.max_messages = max_messages
- self.messages: list[tuple[str, mcrfpy.Color]] = []
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(pos=(x, y), size=(width, height))
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- line_height = 20
- for i in range(max_messages):
- caption = mcrfpy.Caption(pos=(x + 10, y + 5 + i * line_height), text="")
- caption.font_size = 14
- caption.fill_color = mcrfpy.Color(200, 200, 200)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- for caption in self.captions:
- scene.children.append(caption)
-
- def add(self, text: str, color: mcrfpy.Color = None) -> None:
- if color is None:
- color = mcrfpy.Color(200, 200, 200)
- self.messages.append((text, color))
- while len(self.messages) > self.max_messages:
- self.messages.pop(0)
- self._refresh()
-
- def _refresh(self) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(self.messages):
- text, color = self.messages[i]
- caption.text = text
- caption.fill_color = color
- else:
- caption.text = ""
-
- def clear(self) -> None:
- self.messages.clear()
- self._refresh()
-
-# =============================================================================
-# Health Bar System
-# =============================================================================
-
-class HealthBar:
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x, self.y = x, y
- self.width, self.height = width, height
-
- self.bg_frame = mcrfpy.Frame(pos=(x, y), size=(width, height))
- self.bg_frame.fill_color = mcrfpy.Color(80, 0, 0)
- self.bg_frame.outline = 2
- self.bg_frame.outline_color = mcrfpy.Color(150, 150, 150)
-
- self.fg_frame = mcrfpy.Frame(pos=(x + 2, y + 2), size=(width - 4, height - 4))
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- self.fg_frame.outline = 0
-
- self.label = mcrfpy.Caption(pos=(x + 5, y + 2), text="HP: 30/30")
- self.label.font_size = 16
- self.label.fill_color = mcrfpy.Color(255, 255, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.bg_frame)
- scene.children.append(self.fg_frame)
- scene.children.append(self.label)
-
- def update(self, current_hp: int, max_hp: int) -> None:
- percent = max(0, current_hp / max_hp) if max_hp > 0 else 0
- self.fg_frame.resize(int((self.width - 4) * percent), self.height - 4)
- self.label.text = f"HP: {current_hp}/{max_hp}"
- if percent > 0.6:
- self.fg_frame.fill_color = mcrfpy.Color(0, 180, 0)
- elif percent > 0.3:
- self.fg_frame.fill_color = mcrfpy.Color(180, 180, 0)
- else:
- self.fg_frame.fill_color = mcrfpy.Color(180, 0, 0)
-
-# =============================================================================
-# XP Bar System
-# =============================================================================
-
-class XPBar:
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x, self.y = x, y
- self.width, self.height = width, height
-
- self.bg_frame = mcrfpy.Frame(pos=(x, y), size=(width, height))
- self.bg_frame.fill_color = mcrfpy.Color(40, 40, 80)
- self.bg_frame.outline = 2
- self.bg_frame.outline_color = mcrfpy.Color(100, 100, 150)
-
- self.fg_frame = mcrfpy.Frame(pos=(x + 2, y + 2), size=(0, height - 4))
- self.fg_frame.fill_color = mcrfpy.Color(200, 200, 50)
- self.fg_frame.outline = 0
-
- self.label = mcrfpy.Caption(pos=(x + 5, y + 1), text="Level 1 | XP: 0/100")
- self.label.font_size = 14
- self.label.fill_color = mcrfpy.Color(255, 255, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.bg_frame)
- scene.children.append(self.fg_frame)
- scene.children.append(self.label)
-
- def update(self, level: int, xp: int, xp_to_next: int) -> None:
- percent = min(1.0, max(0.0, xp / xp_to_next if xp_to_next > 0 else 0))
- self.fg_frame.resize(int((self.width - 4) * percent), self.height - 4)
- self.label.text = f"Level {level} | XP: {xp}/{xp_to_next}"
-
-# =============================================================================
-# Equipment Panel
-# =============================================================================
-
-class EquipmentPanel:
- """Panel showing equipped items."""
-
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x, self.y = x, y
- self.width, self.height = width, height
-
- self.frame = mcrfpy.Frame(pos=(x, y), size=(width, height))
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- self.title = mcrfpy.Caption(pos=(x + 10, y + 5), text="Equipment")
- self.title.font_size = 14
- self.title.fill_color = mcrfpy.Color(200, 200, 255)
-
- self.weapon_label = mcrfpy.Caption(pos=(x + 10, y + 25), text="Weapon: None")
- self.weapon_label.font_size = 13
- self.weapon_label.fill_color = mcrfpy.Color(255, 150, 150)
-
- self.armor_label = mcrfpy.Caption(pos=(x + 10, y + 43), text="Armor: None")
- self.armor_label.font_size = 13
- self.armor_label.fill_color = mcrfpy.Color(150, 150, 255)
-
- self.stats_label = mcrfpy.Caption(pos=(x + 10, y + 61), text="ATK: 5 | DEF: 2")
- self.stats_label.font_size = 12
- self.stats_label.fill_color = mcrfpy.Color(200, 200, 200)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- scene.children.append(self.title)
- scene.children.append(self.weapon_label)
- scene.children.append(self.armor_label)
- scene.children.append(self.stats_label)
-
- def update(self, fighter: "Fighter") -> None:
- if fighter.weapon:
- weapon_text = f"Weapon: {fighter.weapon.name} (+{fighter.weapon.attack_bonus} ATK)"
- else:
- weapon_text = "Weapon: None"
- self.weapon_label.text = weapon_text
-
- if fighter.armor:
- armor_text = f"Armor: {fighter.armor.name} (+{fighter.armor.defense_bonus} DEF)"
- else:
- armor_text = "Armor: None"
- self.armor_label.text = armor_text
-
- self.stats_label.text = f"Total ATK: {fighter.attack} | DEF: {fighter.defense}"
-
-# =============================================================================
-# Inventory Panel
-# =============================================================================
-
-class InventoryPanel:
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x, self.y = x, y
- self.width, self.height = width, height
- self.captions: list[mcrfpy.Caption] = []
-
- self.frame = mcrfpy.Frame(pos=(x, y), size=(width, height))
- self.frame.fill_color = mcrfpy.Color(20, 20, 30, 200)
- self.frame.outline = 2
- self.frame.outline_color = mcrfpy.Color(80, 80, 100)
-
- self.title = mcrfpy.Caption(pos=(x + 10, y + 5), text="Inventory (G:get E:equip)")
- self.title.font_size = 14
- self.title.fill_color = mcrfpy.Color(200, 200, 255)
-
- for i in range(5):
- caption = mcrfpy.Caption(pos=(x + 10, y + 25 + i * 18), text="")
- caption.font_size = 13
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- self.captions.append(caption)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.frame)
- scene.children.append(self.title)
- for caption in self.captions:
- scene.children.append(caption)
-
- def update(self, inventory: Inventory) -> None:
- for i, caption in enumerate(self.captions):
- if i < len(inventory.items):
- item = inventory.items[i]
- # Show item type indicator
- if item.item_type == "equipment" and item.equipment:
- if item.equipment.slot == "weapon":
- caption.text = f"{i+1}. {item.name} [W]"
- caption.fill_color = mcrfpy.Color(255, 150, 150)
- else:
- caption.text = f"{i+1}. {item.name} [A]"
- caption.fill_color = mcrfpy.Color(150, 150, 255)
- else:
- caption.text = f"{i+1}. {item.name}"
- caption.fill_color = mcrfpy.Color(180, 180, 180)
- else:
- caption.text = f"{i+1}. ---"
- caption.fill_color = mcrfpy.Color(80, 80, 80)
-
-# =============================================================================
-# Level Display
-# =============================================================================
-
-class LevelDisplay:
- def __init__(self, x: int, y: int):
- self.caption = mcrfpy.Caption(pos=(x, y), text="Dungeon: 1")
- self.caption.font_size = 16
- self.caption.fill_color = mcrfpy.Color(200, 200, 255)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.caption)
-
- def update(self, level: int) -> None:
- self.caption.text = f"Dungeon: {level}"
-
-# =============================================================================
-# Mode Display
-# =============================================================================
-
-class ModeDisplay:
- def __init__(self, x: int, y: int):
- self.caption = mcrfpy.Caption(pos=(x, y), text="[NORMAL MODE]")
- self.caption.font_size = 16
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
-
- def add_to_scene(self, scene: mcrfpy.Scene) -> None:
- scene.children.append(self.caption)
-
- def update(self, mode: GameMode) -> None:
- if mode == GameMode.NORMAL:
- self.caption.text = "[NORMAL] F:Ranged | >:Descend | E:Equip"
- self.caption.fill_color = mcrfpy.Color(100, 255, 100)
- elif mode == GameMode.TARGETING:
- self.caption.text = "[TARGETING] Arrows:Move, Enter:Fire, Esc:Cancel"
- self.caption.fill_color = mcrfpy.Color(255, 255, 100)
-
-# =============================================================================
-# Global State
-# =============================================================================
-
-entity_data: dict[mcrfpy.Entity, Fighter] = {}
-item_data: dict[mcrfpy.Entity, Item] = {}
-
-player: Optional[mcrfpy.Entity] = None
-player_inventory: Optional[Inventory] = None
-grid: Optional[mcrfpy.Grid] = None
-fov_layer = None
-texture: Optional[mcrfpy.Texture] = None
-game_over: bool = False
-dungeon_level: int = 1
-stairs_position: tuple[int, int] = (0, 0)
-
-game_mode: GameMode = GameMode.NORMAL
-target_cursor: Optional[mcrfpy.Entity] = None
-target_x: int = 0
-target_y: int = 0
-
-# UI components
-message_log: Optional[MessageLog] = None
-health_bar: Optional[HealthBar] = None
-xp_bar: Optional[XPBar] = None
-inventory_panel: Optional[InventoryPanel] = None
-equipment_panel: Optional[EquipmentPanel] = None
-mode_display: Optional[ModeDisplay] = None
-level_display: Optional[LevelDisplay] = None
-
-# =============================================================================
-# Room Class
-# =============================================================================
-
-class RectangularRoom:
- def __init__(self, x: int, y: int, width: int, height: int):
- self.x1, self.y1 = x, y
- self.x2, self.y2 = x + width, y + height
-
- @property
- def center(self) -> tuple[int, int]:
- return (self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2
-
- @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:
- global explored
- explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
-
-def mark_explored(x: int, y: int) -> None:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- explored[y][x] = True
-
-def is_explored(x: int, y: int) -> bool:
- if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
- return explored[y][x]
- return False
-
-# =============================================================================
-# Dungeon Generation (abbreviated for space)
-# =============================================================================
-
-def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
- 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:
- 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:
- 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:
- 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:
- 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)
-
-def place_stairs(target_grid: mcrfpy.Grid, x: int, y: int) -> None:
- global stairs_position
- cell = target_grid.at(x, y)
- cell.tilesprite = SPRITE_STAIRS_DOWN
- cell.walkable = True
- cell.transparent = True
- stairs_position = (x, y)
-
-def generate_dungeon(target_grid: mcrfpy.Grid, level: int) -> tuple[int, int]:
- fill_with_walls(target_grid)
- 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)
-
- if any(new_room.intersects(other) for other in rooms):
- continue
-
- carve_room(target_grid, new_room)
- if rooms:
- carve_l_tunnel(target_grid, new_room.center, rooms[-1].center)
- rooms.append(new_room)
-
- if rooms:
- place_stairs(target_grid, *rooms[-1].center)
- return rooms[0].center
- return GRID_WIDTH // 2, GRID_HEIGHT // 2
-
-# =============================================================================
-# Entity Management
-# =============================================================================
-
-def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
- 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)
- entity_data[enemy] = Fighter(
- hp=template["hp"], max_hp=template["hp"],
- base_attack=template["attack"], base_defense=template["defense"],
- name=enemy_type.capitalize(), is_player=False
- )
- return enemy
-
-def spawn_item(target_grid: mcrfpy.Grid, x: int, y: int, item_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
- template = ITEM_TEMPLATES[item_type]
- item_entity = mcrfpy.Entity(grid_pos=(x, y), texture=tex, sprite_index=template["sprite"])
- item_entity.visible = False
- target_grid.entities.append(item_entity)
-
- # Create the item
- item = Item(
- name=template["name"],
- item_type=template["item_type"],
- heal_amount=template.get("heal_amount", 0)
- )
-
- # If it is equipment, create the equipment data
- if template["item_type"] == "equipment":
- item.equipment = Equipment(
- name=template["name"],
- slot=template["slot"],
- attack_bonus=template.get("attack_bonus", 0),
- defense_bonus=template.get("defense_bonus", 0)
- )
-
- item_data[item_entity] = item
- return item_entity
-
-def spawn_entities_for_level(target_grid: mcrfpy.Grid, tex: mcrfpy.Texture, level: int) -> None:
- floor_tiles = []
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- cell = target_grid.at(x, y)
- if cell.walkable and cell.tilesprite == SPRITE_FLOOR:
- floor_tiles.append((x, y))
-
- # Spawn enemies
- max_enemies = get_max_enemies_per_room(level) * 3
- enemy_weights = get_enemy_weights(level)
-
- for _ in range(max_enemies):
- if not floor_tiles:
- break
- x, y = random.choice(floor_tiles)
- if (x, y) == (int(player.x), int(player.y)) or (x, y) == stairs_position:
- continue
- if is_position_occupied(target_grid, x, y):
- continue
-
- roll = random.random()
- enemy_type = "goblin"
- for etype, threshold in enemy_weights:
- if roll < threshold:
- enemy_type = etype
- break
- spawn_enemy(target_grid, x, y, enemy_type, tex)
-
- # Spawn items
- max_items = get_max_items_per_room(level) * 2
- item_weights = get_item_weights(level)
-
- for _ in range(max_items):
- if not floor_tiles:
- break
- x, y = random.choice(floor_tiles)
- if (x, y) == (int(player.x), int(player.y)) or (x, y) == stairs_position:
- continue
- if is_position_occupied(target_grid, x, y):
- continue
-
- roll = random.random()
- item_type = "health_potion"
- for itype, threshold in item_weights:
- if roll < threshold:
- item_type = itype
- break
- spawn_item(target_grid, x, y, item_type, tex)
-
-def is_position_occupied(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
- for entity in target_grid.entities:
- if int(entity.x) == x and int(entity.y) == y:
- return True
- return False
-
-def get_item_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
- for entity in target_grid.entities:
- if entity in item_data and int(entity.x) == x and int(entity.y) == y:
- return entity
- return None
-
-def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
- 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:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in entity_data:
- del entity_data[entity]
-
-def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
- if entity in item_data:
- del item_data[entity]
-
-def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None:
- entities_to_remove = []
- for entity in target_grid.entities:
- if entity in entity_data and entity_data[entity].is_player:
- continue
- entities_to_remove.append(entity)
-
- for entity in entities_to_remove:
- if entity in entity_data:
- del entity_data[entity]
- if entity in item_data:
- del item_data[entity]
- for i, e in enumerate(target_grid.entities):
- if e == entity:
- target_grid.entities.remove(i)
- break
-
-# =============================================================================
-# Equipment Actions
-# =============================================================================
-
-def equip_item(index: int) -> bool:
- """Equip an item from inventory.
-
- Args:
- index: Inventory index of item to equip
-
- Returns:
- True if item was equipped, False otherwise
- """
- global player, player_inventory
-
- item = player_inventory.get(index)
- if item is None:
- message_log.add("Invalid item selection.", COLOR_INVALID)
- return False
-
- if item.item_type != "equipment" or item.equipment is None:
- message_log.add(f"The {item.name} cannot be equipped.", COLOR_INVALID)
- return False
-
- fighter = entity_data.get(player)
- if fighter is None:
- return False
-
- # Remove from inventory
- player_inventory.remove(index)
-
- # Equip and get old equipment
- old_equipment = fighter.equip(item.equipment)
-
- message_log.add(f"You equip the {item.name}.", COLOR_EQUIP)
-
- # Add old equipment back to inventory
- if old_equipment:
- old_item = Item(
- name=old_equipment.name,
- item_type="equipment",
- equipment=old_equipment
- )
- if player_inventory.add(old_item):
- message_log.add(f"You unequip the {old_equipment.name}.", COLOR_INFO)
- else:
- # Inventory full - drop on ground
- drop_equipment(old_equipment)
- message_log.add(f"Inventory full! {old_equipment.name} dropped.", COLOR_WARNING)
-
- update_ui()
- return True
-
-def drop_equipment(equipment: Equipment) -> None:
- """Drop equipment on the ground at player position."""
- global player, grid, texture
-
- px, py = int(player.x), int(player.y)
-
- # Find template for this equipment
- template = None
- for key, tmpl in ITEM_TEMPLATES.items():
- if tmpl["name"] == equipment.name:
- template = tmpl
- break
-
- if template is None:
- # Use default appearance
- template = {
- "sprite": SPRITE_SWORD if equipment.slot == "weapon" else SPRITE_ARMOR,
- "color": mcrfpy.Color(200, 200, 200)
- }
-
- item_entity = mcrfpy.Entity(
- grid_pos=(px, py),
- texture=texture,
- sprite_index=template["sprite"]
- )
- item_entity.visible = True
-
- grid.entities.append(item_entity)
-
- item_data[item_entity] = Item(
- name=equipment.name,
- item_type="equipment",
- equipment=equipment
- )
-
-# =============================================================================
-# XP and Level Up
-# =============================================================================
-
-def award_xp(enemy_name: str) -> None:
- global player
- fighter = entity_data.get(player)
- if fighter is None:
- return
-
- xp_amount = ENEMY_XP_VALUES.get(enemy_name.lower(), 35)
- leveled_up = fighter.gain_xp(xp_amount)
-
- if leveled_up:
- message_log.add(
- f"You gained {xp_amount} XP and reached level {fighter.level}!",
- COLOR_LEVEL_UP
- )
- message_log.add(
- f"HP +5, Attack +1{', Defense +1' if fighter.level % 3 == 0 else ''}!",
- COLOR_LEVEL_UP
- )
- else:
- message_log.add(f"You gain {xp_amount} XP.", COLOR_XP)
- update_ui()
-
-# =============================================================================
-# Level Transition
-# =============================================================================
-
-def descend_stairs() -> bool:
- global player, dungeon_level, grid, fov_layer, stairs_position
-
- px, py = int(player.x), int(player.y)
- if (px, py) != stairs_position:
- message_log.add("There are no stairs here.", COLOR_INVALID)
- return False
-
- dungeon_level += 1
- clear_entities_except_player(grid)
- init_explored()
- player_start = generate_dungeon(grid, dungeon_level)
- player.x, player.y = player_start
- spawn_entities_for_level(grid, texture, dungeon_level)
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
- update_fov(grid, fov_layer, player_start[0], player_start[1])
-
- message_log.add(f"You descend to level {dungeon_level}...", COLOR_DESCEND)
- level_display.update(dungeon_level)
- update_ui()
- return True
-
-# =============================================================================
-# Save/Load System
-# =============================================================================
-
-def save_game() -> bool:
- global player, player_inventory, grid, explored, dungeon_level, stairs_position
-
- try:
- tiles = []
- for y in range(GRID_HEIGHT):
- row = []
- for x in range(GRID_WIDTH):
- cell = grid.at(x, y)
- row.append({
- "tilesprite": cell.tilesprite,
- "walkable": cell.walkable,
- "transparent": cell.transparent
- })
- tiles.append(row)
-
- enemies = []
- for entity in grid.entities:
- if entity == player:
- continue
- if entity in entity_data:
- enemies.append({
- "x": int(entity.x), "y": int(entity.y),
- "type": entity_data[entity].name.lower(),
- "fighter": entity_data[entity].to_dict()
- })
-
- items_on_ground = []
- for entity in grid.entities:
- if entity in item_data:
- items_on_ground.append({
- "x": int(entity.x), "y": int(entity.y),
- "item": item_data[entity].to_dict()
- })
-
- save_data = {
- "version": 4,
- "dungeon_level": dungeon_level,
- "stairs_position": list(stairs_position),
- "player": {
- "x": int(player.x), "y": int(player.y),
- "fighter": entity_data[player].to_dict(),
- "inventory": player_inventory.to_dict()
- },
- "tiles": tiles,
- "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)],
- "enemies": enemies,
- "items": items_on_ground
- }
-
- with open(SAVE_FILE, "w") as f:
- json.dump(save_data, f, indent=2)
-
- message_log.add("Game saved!", COLOR_SAVE)
- return True
- except Exception as e:
- message_log.add(f"Save failed: {e}", COLOR_INVALID)
- return False
-
-def load_game() -> bool:
- global player, player_inventory, grid, explored, dungeon_level
- global entity_data, item_data, fov_layer, game_over, stairs_position
-
- if not os.path.exists(SAVE_FILE):
- return False
-
- try:
- with open(SAVE_FILE, "r") as f:
- save_data = json.load(f)
-
- entity_data.clear()
- item_data.clear()
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- dungeon_level = save_data.get("dungeon_level", 1)
- stairs_position = tuple(save_data.get("stairs_position", [0, 0]))
-
- tiles = save_data["tiles"]
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- cell = grid.at(x, y)
- tile_data = tiles[y][x]
- cell.tilesprite = tile_data["tilesprite"]
- cell.walkable = tile_data["walkable"]
- cell.transparent = tile_data["transparent"]
-
- global explored
- explored = save_data["explored"]
-
- player_data = save_data["player"]
- player = mcrfpy.Entity(
- grid_pos=(player_data["x"], player_data["y"]),
- texture=texture, sprite_index=SPRITE_PLAYER
- )
- grid.entities.append(player)
- entity_data[player] = Fighter.from_dict(player_data["fighter"])
- player_inventory = Inventory.from_dict(player_data["inventory"])
-
- for enemy_data in save_data.get("enemies", []):
- template = ENEMY_TEMPLATES.get(enemy_data["type"], ENEMY_TEMPLATES["goblin"])
- enemy = mcrfpy.Entity(
- grid_pos=(enemy_data["x"], enemy_data["y"]),
- texture=texture, sprite_index=template["sprite"]
- )
- enemy.visible = False
- grid.entities.append(enemy)
- entity_data[enemy] = Fighter.from_dict(enemy_data["fighter"])
-
- for item_entry in save_data.get("items", []):
- item = Item.from_dict(item_entry["item"])
- # Find template for sprite
- template = None
- for key, tmpl in ITEM_TEMPLATES.items():
- if tmpl["name"] == item.name:
- template = tmpl
- break
- if template is None:
- template = ITEM_TEMPLATES["health_potion"]
-
- item_entity = mcrfpy.Entity(
- grid_pos=(item_entry["x"], item_entry["y"]),
- texture=texture, sprite_index=template["sprite"]
- )
- item_entity.visible = False
- grid.entities.append(item_entity)
- item_data[item_entity] = item
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
- update_fov(grid, fov_layer, int(player.x), int(player.y))
-
- game_over = False
- message_log.add("Game loaded!", COLOR_SAVE)
- level_display.update(dungeon_level)
- update_ui()
- return True
- except Exception as e:
- message_log.add(f"Load failed: {e}", COLOR_INVALID)
- return False
-
-def delete_save() -> bool:
- try:
- if os.path.exists(SAVE_FILE):
- os.remove(SAVE_FILE)
- return True
- except:
- return False
-
-def has_save_file() -> bool:
- return os.path.exists(SAVE_FILE)
-
-# =============================================================================
-# Targeting System
-# =============================================================================
-
-def enter_targeting_mode() -> None:
- global game_mode, target_cursor, target_x, target_y
- target_x, target_y = int(player.x), int(player.y)
- target_cursor = mcrfpy.Entity(grid_pos=(target_x, target_y), texture=texture, sprite_index=SPRITE_CURSOR)
- grid.entities.append(target_cursor)
- game_mode = GameMode.TARGETING
- message_log.add("Targeting: Arrows to aim, Enter to fire, Esc to cancel.", COLOR_INFO)
- mode_display.update(game_mode)
-
-def exit_targeting_mode() -> None:
- global game_mode, target_cursor
- if target_cursor:
- for i, e in enumerate(grid.entities):
- if e == target_cursor:
- grid.entities.remove(i)
- break
- target_cursor = None
- game_mode = GameMode.NORMAL
- mode_display.update(game_mode)
-
-def move_cursor(dx: int, dy: int) -> None:
- global target_x, target_y
- new_x, new_y = target_x + dx, target_y + dy
- if not (0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT):
- return
- if not grid.is_in_fov(new_x, new_y):
- message_log.add("Cannot see that location.", COLOR_INVALID)
- return
- distance = abs(new_x - int(player.x)) + abs(new_y - int(player.y))
- if distance > RANGED_ATTACK_RANGE:
- message_log.add(f"Out of range (max {RANGED_ATTACK_RANGE}).", COLOR_WARNING)
- return
- target_x, target_y = new_x, new_y
- target_cursor.x, target_cursor.y = target_x, target_y
- enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
- if enemy and enemy in entity_data:
- f = entity_data[enemy]
- message_log.add(f"Target: {f.name} (HP: {f.hp}/{f.max_hp})", COLOR_RANGED)
-
-def confirm_target() -> None:
- if target_x == int(player.x) and target_y == int(player.y):
- message_log.add("Cannot target yourself!", COLOR_INVALID)
- return
- target_enemy = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
- if not target_enemy or target_enemy not in entity_data:
- message_log.add("No valid target.", COLOR_INVALID)
- return
- perform_ranged_attack(target_enemy)
- exit_targeting_mode()
- enemy_turn()
- update_ui()
-
-def perform_ranged_attack(target_entity: mcrfpy.Entity) -> None:
- defender = entity_data.get(target_entity)
- attacker = entity_data.get(player)
- if not defender or not attacker:
- return
- damage = max(1, RANGED_ATTACK_DAMAGE - defender.defense // 2)
- defender.take_damage(damage)
- message_log.add(f"Ranged attack hits {defender.name} for {damage}!", COLOR_RANGED)
- if not defender.is_alive:
- handle_death(target_entity, defender)
-
-# =============================================================================
-# Combat System
-# =============================================================================
-
-def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
- return max(0, attacker.attack - defender.defense)
-
-def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
- attacker = entity_data.get(attacker_entity)
- defender = entity_data.get(defender_entity)
- if not attacker or not defender:
- return
-
- damage = calculate_damage(attacker, defender)
- defender.take_damage(damage)
-
- if damage > 0:
- if attacker.is_player:
- message_log.add(f"You hit {defender.name} for {damage}!", COLOR_PLAYER_ATTACK)
- else:
- message_log.add(f"{attacker.name} hits you for {damage}!", COLOR_ENEMY_ATTACK)
- else:
- if attacker.is_player:
- message_log.add(f"You hit {defender.name} but deal no damage.", mcrfpy.Color(150, 150, 150))
- else:
- message_log.add(f"{attacker.name} hits but deals no damage.", mcrfpy.Color(150, 150, 200))
-
- if not defender.is_alive:
- handle_death(defender_entity, defender)
- update_ui()
-
-def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
- global game_over
- if fighter.is_player:
- message_log.add("You have died!", COLOR_PLAYER_DEATH)
- message_log.add("Press R to restart.", COLOR_INFO)
- game_over = True
- entity.sprite_index = SPRITE_CORPSE
- delete_save()
- else:
- message_log.add(f"{fighter.name} dies!", COLOR_ENEMY_DEATH)
- award_xp(fighter.name)
- remove_entity(grid, entity)
-
-# =============================================================================
-# Item Actions
-# =============================================================================
-
-def pickup_item() -> bool:
- px, py = int(player.x), int(player.y)
- item_entity = get_item_at(grid, px, py)
- if not item_entity:
- message_log.add("Nothing to pick up.", COLOR_INVALID)
- return False
- if player_inventory.is_full():
- message_log.add("Inventory full!", COLOR_WARNING)
- return False
- item = item_data.get(item_entity)
- if not item:
- return False
- player_inventory.add(item)
- remove_item_entity(grid, item_entity)
- message_log.add(f"Picked up {item.name}.", COLOR_PICKUP)
- update_ui()
- return True
-
-def use_item(index: int) -> bool:
- item = player_inventory.get(index)
- if not item:
- message_log.add("Invalid selection.", COLOR_INVALID)
- return False
-
- # Handle equipment
- if item.item_type == "equipment":
- return equip_item(index)
-
- # Handle consumables
- if item.item_type == "health_potion":
- fighter = entity_data.get(player)
- if not fighter:
- return False
- if fighter.hp >= fighter.max_hp:
- message_log.add("Already at full health!", COLOR_WARNING)
- return False
- actual_heal = fighter.heal(item.heal_amount)
- player_inventory.remove(index)
- message_log.add(f"Healed {actual_heal} HP!", COLOR_HEAL)
- update_ui()
- return True
-
- message_log.add(f"Cannot use {item.name}.", COLOR_INVALID)
- return False
-
-# =============================================================================
-# Field of View
-# =============================================================================
-
-def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
- for entity in target_grid.entities:
- if entity == player or entity == target_cursor:
- entity.visible = True
- else:
- entity.visible = target_grid.is_in_fov(int(entity.x), int(entity.y))
-
-def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
- 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
-# =============================================================================
-
-def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool:
- if not (0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT):
- return False
- if not target_grid.at(x, y).walkable:
- return False
- return get_blocking_entity_at(target_grid, x, y, exclude=mover) is None
-
-def try_move_or_attack(dx: int, dy: int) -> None:
- global game_over
- if game_over:
- return
- px, py = int(player.x), int(player.y)
- new_x, new_y = px + dx, py + dy
- if not (0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT):
- return
-
- blocker = get_blocking_entity_at(grid, new_x, new_y, exclude=player)
- if blocker:
- perform_attack(player, blocker)
- enemy_turn()
- elif grid.at(new_x, new_y).walkable:
- player.x, player.y = new_x, new_y
- update_fov(grid, fov_layer, new_x, new_y)
- enemy_turn()
- update_ui()
-
-# =============================================================================
-# Enemy AI
-# =============================================================================
-
-def enemy_turn() -> None:
- global game_over
- if game_over:
- return
- px, py = int(player.x), int(player.y)
-
- for entity in list(grid.entities):
- if entity == player or entity not in entity_data:
- continue
- fighter = entity_data[entity]
- if not fighter.is_alive:
- continue
- ex, ey = int(entity.x), int(entity.y)
- if not grid.is_in_fov(ex, ey):
- continue
-
- dx, dy = px - ex, py - ey
- if abs(dx) <= 1 and abs(dy) <= 1 and (dx or dy):
- perform_attack(entity, player)
- else:
- move_toward_player(entity, ex, ey, px, py)
-
-def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None:
- dx = 1 if px > ex else (-1 if px < ex else 0)
- dy = 1 if py > ey else (-1 if py < ey else 0)
-
- if can_move_to(grid, ex + dx, ey + dy, enemy):
- enemy.x, enemy.y = ex + dx, ey + dy
- elif dx and can_move_to(grid, ex + dx, ey, enemy):
- enemy.x = ex + dx
- elif dy and can_move_to(grid, ex, ey + dy, enemy):
- enemy.y = ey + dy
-
-# =============================================================================
-# UI Updates
-# =============================================================================
-
-def update_ui() -> None:
- if player in entity_data:
- fighter = entity_data[player]
- health_bar.update(fighter.hp, fighter.max_hp)
- xp_bar.update(fighter.level, fighter.xp, fighter.xp_to_next_level)
- equipment_panel.update(fighter)
- if player_inventory:
- inventory_panel.update(player_inventory)
-
-# =============================================================================
-# New Game
-# =============================================================================
-
-def generate_new_game() -> None:
- global player, player_inventory, game_over, dungeon_level, game_mode
-
- game_over = False
- game_mode = GameMode.NORMAL
- dungeon_level = 1
-
- entity_data.clear()
- item_data.clear()
- while len(grid.entities) > 0:
- grid.entities.remove(0)
-
- init_explored()
- message_log.clear()
-
- player_start = generate_dungeon(grid, dungeon_level)
-
- player = mcrfpy.Entity(grid_pos=player_start, texture=texture, sprite_index=SPRITE_PLAYER)
- grid.entities.append(player)
-
- entity_data[player] = Fighter(
- hp=30, max_hp=30, base_attack=5, base_defense=2,
- name="Player", is_player=True, xp=0, level=1
- )
-
- player_inventory = Inventory(capacity=10)
- spawn_entities_for_level(grid, texture, dungeon_level)
-
- for y in range(GRID_HEIGHT):
- for x in range(GRID_WIDTH):
- fov_layer.set(x, y, COLOR_UNKNOWN)
- update_fov(grid, fov_layer, player_start[0], player_start[1])
-
- mode_display.update(game_mode)
- level_display.update(dungeon_level)
- update_ui()
-
-# =============================================================================
-# Game Setup
-# =============================================================================
-
-scene = mcrfpy.Scene("game")
-texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
-
-grid = mcrfpy.Grid(
- pos=(20, GAME_AREA_Y),
- size=(700, GAME_AREA_HEIGHT - 20),
- grid_size=(GRID_WIDTH, GRID_HEIGHT),
- texture=texture
-)
-grid.zoom = 1.0
-
-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)
-
-scene.children.append(grid)
-
-# UI Elements
-title = mcrfpy.Caption(pos=(20, 10), text="Part 13: Equipment System")
-title.fill_color = mcrfpy.Color(255, 255, 255)
-title.font_size = 24
-scene.children.append(title)
-
-instructions = mcrfpy.Caption(pos=(300, 15), text="WASD:Move | E:Equip | G:Get | >:Descend")
-instructions.fill_color = mcrfpy.Color(180, 180, 180)
-instructions.font_size = 14
-scene.children.append(instructions)
-
-health_bar = HealthBar(x=730, y=10, width=280, height=25)
-health_bar.add_to_scene(scene)
-
-xp_bar = XPBar(x=730, y=40, width=280, height=20)
-xp_bar.add_to_scene(scene)
-
-level_display = LevelDisplay(x=860, y=65)
-level_display.add_to_scene(scene)
-
-mode_display = ModeDisplay(x=20, y=40)
-mode_display.add_to_scene(scene)
-
-equipment_panel = EquipmentPanel(x=730, y=GAME_AREA_Y, width=280, height=85)
-equipment_panel.add_to_scene(scene)
-
-inventory_panel = InventoryPanel(x=730, y=GAME_AREA_Y + 90, width=280, height=120)
-inventory_panel.add_to_scene(scene)
-
-message_log = MessageLog(x=20, y=768 - UI_BOTTOM_HEIGHT + 10, width=990, height=UI_BOTTOM_HEIGHT - 20, max_messages=6)
-message_log.add_to_scene(scene)
-
-# Initialize
-init_explored()
-
-if has_save_file():
- message_log.add("Loading saved game...", COLOR_INFO)
- if not load_game():
- generate_new_game()
- message_log.add("Welcome to the dungeon!", COLOR_INFO)
-else:
- generate_new_game()
- message_log.add("Welcome to the dungeon!", COLOR_INFO)
- message_log.add("Find equipment to grow stronger!", COLOR_INFO)
-
-# =============================================================================
-# Input Handling
-# =============================================================================
-
-def handle_keys(key: str, action: str) -> None:
- global game_over, game_mode
-
- if action != "start":
- return
-
- if key == "R":
- delete_save()
- generate_new_game()
- message_log.add("New adventure begins!", COLOR_INFO)
- return
-
- if key == "Escape":
- if game_mode == GameMode.TARGETING:
- exit_targeting_mode()
- message_log.add("Targeting cancelled.", COLOR_INFO)
- else:
- if not game_over:
- save_game()
- mcrfpy.exit()
- return
-
- if game_over:
- return
-
- if game_mode == GameMode.TARGETING:
- handle_targeting_input(key)
- else:
- handle_normal_input(key)
-
-def handle_normal_input(key: str) -> None:
- if key in ("W", "Up"):
- try_move_or_attack(0, -1)
- elif key in ("S", "Down"):
- try_move_or_attack(0, 1)
- elif key in ("A", "Left"):
- try_move_or_attack(-1, 0)
- elif key in ("D", "Right"):
- try_move_or_attack(1, 0)
- elif key == "F":
- enter_targeting_mode()
- elif key in ("G", ","):
- pickup_item()
- elif key == "Period":
- descend_stairs()
- elif key == "E":
- message_log.add("Press 1-5 to equip an item from inventory.", COLOR_INFO)
- elif key in "12345":
- index = int(key) - 1
- if use_item(index):
- enemy_turn()
- update_ui()
-
-def handle_targeting_input(key: str) -> None:
- if key in ("Up", "W"):
- move_cursor(0, -1)
- elif key in ("Down", "S"):
- move_cursor(0, 1)
- elif key in ("Left", "A"):
- move_cursor(-1, 0)
- elif key in ("Right", "D"):
- move_cursor(1, 0)
- elif key in ("Return", "Space"):
- confirm_target()
-
-scene.on_key = handle_keys
-
-# =============================================================================
-# Start the Game
-# =============================================================================
-
-scene.activate()
-print("Part 13: Equipment System - Tutorial Complete!")
-print("Find weapons and armor to become stronger!")
\ No newline at end of file
diff --git a/src/PySceneObject.h b/src/PySceneObject.h
index 22ef8ab..bc12ea3 100644
--- a/src/PySceneObject.h
+++ b/src/PySceneObject.h
@@ -53,41 +53,7 @@ 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(
- "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_doc = PyDoc_STR("Base class for object-oriented scenes"),
.tp_methods = nullptr, // Set in McRFPy_API.cpp
.tp_getset = nullptr, // Set in McRFPy_API.cpp
.tp_init = (initproc)PySceneClass::__init__,
diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp
index 7b59b12..e56ed5d 100644
--- a/src/UIGrid.cpp
+++ b/src/UIGrid.cpp
@@ -6,9 +6,8 @@
#include "Profiler.h"
#include "PyFOV.h"
#include
-#include // #142 - for std::floor, std::isnan
+#include // #142 - for std::floor
#include // #150 - for strcmp
-#include // #169 - for std::numeric_limits
// UIDrawable methods now in UIBase.h
UIGrid::UIGrid()
@@ -736,9 +735,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
PyObject* fill_color = nullptr;
PyObject* click_handler = nullptr;
PyObject* layers_obj = nullptr; // #150 - layers dict
- // #169 - Use NaN as sentinel to detect if user provided center values
- float center_x = std::numeric_limits::quiet_NaN();
- float center_y = std::numeric_limits::quiet_NaN();
+ float center_x = 0.0f, center_y = 0.0f;
float zoom = 1.0f;
// perspective is now handled via properties, not init args
int visible = 1;
@@ -865,19 +862,9 @@ 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;
@@ -1743,72 +1730,6 @@ 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,
@@ -1897,15 +1818,6 @@ 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}
};
@@ -2017,15 +1929,6 @@ 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
};
diff --git a/src/UIGrid.h b/src/UIGrid.h
index 3751466..2af03b7 100644
--- a/src/UIGrid.h
+++ b/src/UIGrid.h
@@ -170,12 +170,6 @@ 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);
diff --git a/tests/benchmarks/benchmark_moving_entities.py b/tests/benchmarks/benchmark_moving_entities.py
index 23d79d6..6c0fb76 100644
--- a/tests/benchmarks/benchmark_moving_entities.py
+++ b/tests/benchmarks/benchmark_moving_entities.py
@@ -34,15 +34,12 @@ 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
- color_layer.set(x, y, mcrfpy.Color(40, 40, 40, 255))
+ cell.color = (40, 40, 40, 255)
# Create 50 entities with random positions and velocities
entities = []
@@ -50,15 +47,15 @@ ENTITY_COUNT = 50
for i in range(ENTITY_COUNT):
entity = mcrfpy.Entity(
- (random.randint(0, 99), random.randint(0, 99)),
- sprite_index=random.randint(10, 20), # Use varied sprites
- grid=grid
+ grid_pos=(random.randint(0, 99), random.randint(0, 99)),
+ sprite_index=random.randint(10, 20) # Use varied sprites
)
- # Give each entity a random velocity (stored as Python attributes)
+ # Give each entity a random velocity
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)
diff --git a/tests/benchmarks/benchmark_suite.py b/tests/benchmarks/benchmark_suite.py
index 72c24c7..18806d4 100644
--- a/tests/benchmarks/benchmark_suite.py
+++ b/tests/benchmarks/benchmark_suite.py
@@ -282,23 +282,23 @@ def setup_grid_stress():
grid.center = (400, 400) # Center view
ui.append(grid)
- # Add color layer and fill with alternating colors
- color_layer = grid.add_layer("color", z_index=-1)
+ # Fill with alternating colors
for y in range(50):
for x in range(50):
+ cell = grid.at(x, y)
if (x + y) % 2 == 0:
- color_layer.set(x, y, mcrfpy.Color(60, 60, 80))
+ cell.color = mcrfpy.Color(60, 60, 80)
else:
- color_layer.set(x, y, mcrfpy.Color(40, 40, 60))
+ cell.color = 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 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)
+ # 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)
grid.entities.append(entity)
except Exception as e:
print(f" Note: Could not create entities: {e}")
diff --git a/tests/benchmarks/layer_performance_test.py b/tests/benchmarks/layer_performance_test.py
index 6649a6b..a81b441 100644
--- a/tests/benchmarks/layer_performance_test.py
+++ b/tests/benchmarks/layer_performance_test.py
@@ -6,14 +6,10 @@ Uses C++ benchmark logger (start_benchmark/end_benchmark) for accurate timing.
Results written to JSON files for analysis.
Compares rendering performance between:
-1. ColorLayer with per-cell modifications (no caching benefit)
-2. ColorLayer with dirty flag caching (static after fill)
+1. Traditional grid.at(x,y).color API (no caching)
+2. New layer system with dirty flag caching
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
@@ -98,7 +94,7 @@ def run_next_test():
# ============================================================================
def setup_base_layer_static():
- """ColorLayer with per-cell set() calls - static after initial fill."""
+ """Traditional grid.at(x,y).color API - no modifications during render."""
mcrfpy.createScene("test_base_static")
ui = mcrfpy.sceneUI("test_base_static")
@@ -106,17 +102,17 @@ def setup_base_layer_static():
pos=(10, 10), size=(600, 600))
ui.append(grid)
- # Fill using ColorLayer with per-cell set() calls (baseline)
- layer = grid.add_layer("color", z_index=-1)
+ # Fill base layer using traditional API
for y in range(GRID_SIZE):
for x in range(GRID_SIZE):
- layer.set(x, y, mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255))
+ cell = grid.at(x, y)
+ cell.color = mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255)
mcrfpy.setScene("test_base_static")
def setup_base_layer_modified():
- """ColorLayer with single cell modified each frame - tests dirty flag."""
+ """Traditional API with single cell modified each frame."""
mcrfpy.createScene("test_base_mod")
ui = mcrfpy.sceneUI("test_base_mod")
@@ -124,16 +120,19 @@ def setup_base_layer_modified():
pos=(10, 10), size=(600, 600))
ui.append(grid)
- # Fill using ColorLayer
- layer = grid.add_layer("color", z_index=-1)
- layer.fill(mcrfpy.Color(100, 100, 100, 255))
+ # 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)
- # Timer to modify one cell per frame (triggers dirty flag each frame)
+ # Timer to modify one cell per frame
mod_counter = [0]
def modify_cell(runtime):
x = mod_counter[0] % GRID_SIZE
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
- layer.set(x, y, mcrfpy.Color(255, 0, 0, 255))
+ cell = grid.at(x, y)
+ cell.color = mcrfpy.Color(255, 0, 0, 255)
mod_counter[0] += 1
mcrfpy.setScene("test_base_mod")
diff --git a/tests/integration/astar_vs_dijkstra.py b/tests/integration/astar_vs_dijkstra.py
index be75ea2..5b93c99 100644
--- a/tests/integration/astar_vs_dijkstra.py
+++ b/tests/integration/astar_vs_dijkstra.py
@@ -19,30 +19,26 @@ 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, color_layer
-
+ global grid
+
mcrfpy.createScene("pathfinding_comparison")
-
+
# Create grid
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
- color_layer.set(x, y, FLOOR_COLOR)
-
+ grid.at(x, y).color = FLOOR_COLOR
+
# Create obstacles that make A* and Dijkstra differ
obstacles = [
# Vertical wall with gaps
@@ -54,15 +50,15 @@ def create_map():
[(x, 10) for x in range(20, 25)],
[(25, y) for y in range(5, 15)],
]
-
+
for obstacle_group in obstacles:
for x, y in obstacle_group:
grid.at(x, y).walkable = False
- color_layer.set(x, y, WALL_COLOR)
-
+ grid.at(x, y).color = WALL_COLOR
+
# Mark start and end
- color_layer.set(start_pos[0], start_pos[1], START_COLOR)
- color_layer.set(end_pos[0], end_pos[1], END_COLOR)
+ grid.at(start_pos[0], start_pos[1]).color = START_COLOR
+ grid.at(end_pos[0], end_pos[1]).color = END_COLOR
def clear_paths():
"""Clear path highlighting"""
@@ -70,34 +66,34 @@ def clear_paths():
for x in range(30):
cell = grid.at(x, y)
if cell.walkable:
- color_layer.set(x, y, FLOOR_COLOR)
-
+ cell.color = FLOOR_COLOR
+
# Restore start and end colors
- color_layer.set(start_pos[0], start_pos[1], START_COLOR)
- color_layer.set(end_pos[0], end_pos[1], END_COLOR)
+ grid.at(start_pos[0], start_pos[1]).color = START_COLOR
+ grid.at(end_pos[0], end_pos[1]).color = END_COLOR
def show_astar():
"""Show A* path"""
clear_paths()
-
+
# Compute A* path
path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
-
+
# Color the path
for i, (x, y) in enumerate(path):
if (x, y) != start_pos and (x, y) != end_pos:
- color_layer.set(x, y, ASTAR_COLOR)
-
+ grid.at(x, y).color = ASTAR_COLOR
+
status_text.text = f"A* Path: {len(path)} steps (optimized for single target)"
status_text.fill_color = ASTAR_COLOR
def show_dijkstra():
"""Show Dijkstra exploration"""
clear_paths()
-
+
# Compute Dijkstra from start
grid.compute_dijkstra(start_pos[0], start_pos[1])
-
+
# Color cells by distance (showing exploration)
max_dist = 40.0
for y in range(20):
@@ -107,50 +103,50 @@ def show_dijkstra():
if dist is not None and dist < max_dist:
# Color based on distance
intensity = int(255 * (1 - dist / max_dist))
- color_layer.set(x, y, mcrfpy.Color(0, intensity // 2, intensity))
-
+ grid.at(x, y).color = mcrfpy.Color(0, intensity // 2, intensity)
+
# Get the actual path
path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
-
+
# Highlight the actual path more brightly
for x, y in path:
if (x, y) != start_pos and (x, y) != end_pos:
- color_layer.set(x, y, DIJKSTRA_COLOR)
-
+ grid.at(x, y).color = DIJKSTRA_COLOR
+
# Restore start and end
- color_layer.set(start_pos[0], start_pos[1], START_COLOR)
- color_layer.set(end_pos[0], end_pos[1], END_COLOR)
-
+ grid.at(start_pos[0], start_pos[1]).color = START_COLOR
+ grid.at(end_pos[0], end_pos[1]).color = END_COLOR
+
status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)"
status_text.fill_color = DIJKSTRA_COLOR
def show_both():
"""Show both paths overlaid"""
clear_paths()
-
+
# Get both paths
astar_path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
grid.compute_dijkstra(start_pos[0], start_pos[1])
dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
-
+
print(astar_path, dijkstra_path)
# Color Dijkstra path first (blue)
for x, y in dijkstra_path:
if (x, y) != start_pos and (x, y) != end_pos:
- color_layer.set(x, y, DIJKSTRA_COLOR)
-
+ grid.at(x, y).color = 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:
- color_layer.set(x, y, ASTAR_COLOR)
-
+ grid.at(x, y).color = ASTAR_COLOR
+
# Mark differences
different_cells = []
for cell in dijkstra_path:
if cell not in astar_path:
different_cells.append(cell)
-
+
status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps"
if different_cells:
info_text.text = f"Paths differ at {len(different_cells)} cells"
@@ -206,26 +202,26 @@ grid.size = (600, 400) # 30*20, 20*20
grid.position = (100, 100)
# Add title
-title = mcrfpy.Caption(pos=(250, 20), text="A* vs Dijkstra Pathfinding")
+title = mcrfpy.Caption("A* vs Dijkstra Pathfinding", 250, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status
-status_text = mcrfpy.Caption(pos=(100, 60), text="Press A for A*, D for Dijkstra, B for Both")
+status_text = mcrfpy.Caption("Press A for A*, D for Dijkstra, B for Both", 100, 60)
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
# Add info
-info_text = mcrfpy.Caption(pos=(100, 520), text="")
+info_text = mcrfpy.Caption("", 100, 520)
info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text)
# Add legend
-legend1 = mcrfpy.Caption(pos=(100, 540), text="Red=Start, Yellow=End, Green=A*, Blue=Dijkstra")
+legend1 = mcrfpy.Caption("Red=Start, Yellow=End, Green=A*, Blue=Dijkstra", 100, 540)
legend1.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend1)
-legend2 = mcrfpy.Caption(pos=(100, 560), text="Dark=Walls, Light=Floor")
+legend2 = mcrfpy.Caption("Dark=Walls, Light=Floor", 100, 560)
legend2.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend2)
diff --git a/tests/integration/debug_visibility.py b/tests/integration/debug_visibility.py
index 89a4ab7..da0bd60 100644
--- a/tests/integration/debug_visibility.py
+++ b/tests/integration/debug_visibility.py
@@ -20,8 +20,9 @@ for y in range(5):
# Create entity
print("Creating entity...")
-entity = mcrfpy.Entity((2, 2), grid=grid)
+entity = mcrfpy.Entity(2, 2)
entity.sprite_index = 64
+grid.entities.append(entity)
print(f"Entity at ({entity.x}, {entity.y})")
# Check gridstate
diff --git a/tests/integration/dijkstra_all_paths.py b/tests/integration/dijkstra_all_paths.py
index 79ce919..e205f08 100644
--- a/tests/integration/dijkstra_all_paths.py
+++ b/tests/integration/dijkstra_all_paths.py
@@ -20,7 +20,6 @@ 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
@@ -28,17 +27,14 @@ current_path = []
def create_map():
"""Create the map with entities"""
- global grid, color_layer, entities, all_combinations
-
+ global grid, entities, all_combinations
+
mcrfpy.createScene("dijkstra_all")
-
+
# Create grid
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
@@ -52,28 +48,29 @@ def create_map():
"..W.WWW.......", # Row 8
"..............", # Row 9
]
-
+
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
-
+
if char == 'W':
cell.walkable = False
- color_layer.set(x, y, WALL_COLOR)
+ cell.color = WALL_COLOR
else:
cell.walkable = True
- color_layer.set(x, y, FLOOR_COLOR)
-
+ cell.color = FLOOR_COLOR
+
if char == 'E':
entity_positions.append((x, y))
-
+
# Create entities
entities = []
for i, (x, y) in enumerate(entity_positions):
- entity = mcrfpy.Entity((x, y), grid=grid)
+ entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
+ grid.entities.append(entity)
entities.append(entity)
print("Map Analysis:")
@@ -93,47 +90,47 @@ def create_map():
def clear_path_colors():
"""Reset all floor tiles to original color"""
global current_path
-
+
for y in range(grid.grid_y):
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
- color_layer.set(x, y, FLOOR_COLOR)
-
+ cell.color = FLOOR_COLOR
+
current_path = []
def show_combination(index):
"""Show a specific path combination (valid or invalid)"""
global current_combo_index, current_path
-
+
current_combo_index = index % len(all_combinations)
from_idx, to_idx = all_combinations[current_combo_index]
-
+
# Clear previous path
clear_path_colors()
-
+
# Get entities
e_from = entities[from_idx]
e_to = entities[to_idx]
-
+
# Calculate path
path = e_from.path_to(int(e_to.x), int(e_to.y))
current_path = path if path else []
-
+
# Always color start and end positions
- 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)
-
+ 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 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:
- color_layer.set(x, y, PATH_COLOR)
-
+ grid.at(x, y).color = 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
-
+
# Show path steps
path_display = []
for i, (x, y) in enumerate(path[:5]):
@@ -145,7 +142,7 @@ def show_combination(index):
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = NO PATH!"
status_text.fill_color = mcrfpy.Color(255, 100, 100) # Red for invalid
path_text.text = "Path: [] (No valid path exists)"
-
+
# Update info
info_text.text = f"From: Entity {from_idx+1} at ({int(e_from.x)}, {int(e_from.y)}) | To: Entity {to_idx+1} at ({int(e_to.x)}, {int(e_to.y)})"
@@ -186,37 +183,37 @@ grid.size = (560, 400)
grid.position = (120, 100)
# Add title
-title = mcrfpy.Caption(pos=(200, 20), text="Dijkstra - All Paths (Valid & Invalid)")
+title = mcrfpy.Caption("Dijkstra - All Paths (Valid & Invalid)", 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status (will change color based on validity)
-status_text = mcrfpy.Caption(pos=(120, 60), text="Ready")
+status_text = mcrfpy.Caption("Ready", 120, 60)
status_text.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(status_text)
# Add info
-info_text = mcrfpy.Caption(pos=(120, 80), text="")
+info_text = mcrfpy.Caption("", 120, 80)
info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text)
# Add path display
-path_text = mcrfpy.Caption(pos=(120, 520), text="Path: None")
+path_text = mcrfpy.Caption("Path: None", 120, 520)
path_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(path_text)
# Add controls
-controls = mcrfpy.Caption(pos=(120, 540), text="SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit")
+controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit", 120, 540)
controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls)
# Add legend
-legend = mcrfpy.Caption(pos=(120, 560), text="Red Start→Blue End (valid) | Red Start→Red End (invalid)")
+legend = mcrfpy.Caption("Red Start→Blue End (valid) | Red Start→Red End (invalid)", 120, 560)
legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend)
# Expected results info
-expected = mcrfpy.Caption(pos=(120, 580), text="Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail")
+expected = mcrfpy.Caption("Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail", 120, 580)
expected.fill_color = mcrfpy.Color(255, 150, 150)
ui.append(expected)
diff --git a/tests/integration/dijkstra_cycle_paths.py b/tests/integration/dijkstra_cycle_paths.py
index 2f71862..201219c 100644
--- a/tests/integration/dijkstra_cycle_paths.py
+++ b/tests/integration/dijkstra_cycle_paths.py
@@ -18,7 +18,6 @@ END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
# Global state
grid = None
-color_layer = None
entities = []
current_path_index = 0
path_combinations = []
@@ -26,17 +25,14 @@ current_path = []
def create_map():
"""Create the map with entities"""
- global grid, color_layer, entities
-
+ global grid, entities
+
mcrfpy.createScene("dijkstra_cycle")
-
+
# Create grid
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
@@ -50,28 +46,29 @@ def create_map():
"..W.WWW.......", # Row 8
"..............", # Row 9
]
-
+
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
-
+
if char == 'W':
cell.walkable = False
- color_layer.set(x, y, WALL_COLOR)
+ cell.color = WALL_COLOR
else:
cell.walkable = True
- color_layer.set(x, y, FLOOR_COLOR)
-
+ cell.color = FLOOR_COLOR
+
if char == 'E':
entity_positions.append((x, y))
-
+
# Create entities
entities = []
for i, (x, y) in enumerate(entity_positions):
- entity = mcrfpy.Entity((x, y), grid=grid)
+ entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
+ grid.entities.append(entity)
entities.append(entity)
print("Entities created:")
@@ -116,48 +113,48 @@ def create_map():
def clear_path_colors():
"""Reset all floor tiles to original color"""
global current_path
-
+
for y in range(grid.grid_y):
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
- color_layer.set(x, y, FLOOR_COLOR)
-
+ cell.color = FLOOR_COLOR
+
current_path = []
def show_path(index):
"""Show a specific path combination"""
global current_path_index, current_path
-
+
if not path_combinations:
status_text.text = "No valid paths available (Entity 1 is trapped!)"
return
-
+
current_path_index = index % len(path_combinations)
from_idx, to_idx, path = path_combinations[current_path_index]
-
+
# Clear previous path
clear_path_colors()
-
+
# Get entities
e_from = entities[from_idx]
e_to = entities[to_idx]
-
+
# Color the path
current_path = path
if path:
# Color start and end
- 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)
-
+ 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 intermediate steps
for i, (x, y) in enumerate(path):
if i > 0 and i < len(path) - 1:
- color_layer.set(x, y, PATH_COLOR)
-
+ grid.at(x, y).color = 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)"
-
+
# Update path display
path_display = []
for i, (x, y) in enumerate(path[:5]): # Show first 5 steps
@@ -197,27 +194,27 @@ grid.size = (560, 400)
grid.position = (120, 100)
# Add title
-title = mcrfpy.Caption(pos=(200, 20), text="Dijkstra Pathfinding - Cycle Paths")
+title = mcrfpy.Caption("Dijkstra Pathfinding - Cycle Paths", 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status
-status_text = mcrfpy.Caption(pos=(120, 60), text="Press SPACE to cycle paths")
+status_text = mcrfpy.Caption("Press SPACE to cycle paths", 120, 60)
status_text.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(status_text)
# Add path display
-path_text = mcrfpy.Caption(pos=(120, 520), text="Path: None")
+path_text = mcrfpy.Caption("Path: None", 120, 520)
path_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(path_text)
# Add controls
-controls = mcrfpy.Caption(pos=(120, 540), text="SPACE/N=Next, P=Previous, R=Refresh, Q=Quit")
+controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, R=Refresh, Q=Quit", 120, 540)
controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls)
# Add legend
-legend = mcrfpy.Caption(pos=(120, 560), text="Red=Start, Blue=End, Green=Path, Dark=Wall")
+legend = mcrfpy.Caption("Red=Start, Blue=End, Green=Path, Dark=Wall", 120, 560)
legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend)
diff --git a/tests/integration/dijkstra_debug.py b/tests/integration/dijkstra_debug.py
index 6538fae..fd182b8 100644
--- a/tests/integration/dijkstra_debug.py
+++ b/tests/integration/dijkstra_debug.py
@@ -18,52 +18,49 @@ 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, color_layer, entities
-
+ global grid, entities
+
mcrfpy.createScene("dijkstra_debug")
-
+
# Small grid for easy debugging
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
for y in range(10):
for x in range(10):
grid.at(x, y).walkable = True
grid.at(x, y).transparent = True
- color_layer.set(x, y, FLOOR_COLOR)
-
+ grid.at(x, y).color = FLOOR_COLOR
+
# Add a simple wall
print("Adding walls at:")
walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)]
for x, y in walls:
print(f" Wall at ({x}, {y})")
grid.at(x, y).walkable = False
- color_layer.set(x, y, WALL_COLOR)
-
+ grid.at(x, y).color = WALL_COLOR
+
# Create 3 entities
entity_positions = [(2, 5), (8, 5), (5, 8)]
entities = []
-
+
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), grid=grid)
+ entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
+ grid.entities.append(entity)
entities.append(entity)
-
+
return grid
def test_path_highlighting():
@@ -91,14 +88,12 @@ def test_path_highlighting():
print(f" Step {i}: ({x}, {y})")
# Get current color for debugging
cell = grid.at(x, y)
- old_c = color_layer.at(x, y)
- old_color = (old_c.r, old_c.g, old_c.b)
-
+ old_color = (cell.color.r, cell.color.g, cell.color.b)
+
# Set new color
- 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)
-
+ cell.color = PATH_COLOR
+ new_color = (cell.color.r, cell.color.g, cell.color.b)
+
print(f" Color changed from {old_color} to {new_color}")
print(f" Walkable: {cell.walkable}")
@@ -116,8 +111,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
- c = color_layer.at(x, y)
- color = (c.r, c.g, c.b)
+ cell = grid.at(x, y)
+ color = (cell.color.r, cell.color.g, cell.color.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}")
@@ -148,12 +143,12 @@ grid.position = (50, 50)
grid.size = (400, 400) # 10*40
# Add title
-title = mcrfpy.Caption(pos=(50, 10), text="Dijkstra Debug - Press SPACE to retest, Q to quit")
+title = mcrfpy.Caption("Dijkstra Debug - Press SPACE to retest, Q to quit", 50, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add debug info
-info = mcrfpy.Caption(pos=(50, 470), text="Check console for debug output")
+info = mcrfpy.Caption("Check console for debug output", 50, 470)
info.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info)
diff --git a/tests/integration/dijkstra_interactive.py b/tests/integration/dijkstra_interactive.py
index c9deeae..fdf2176 100644
--- a/tests/integration/dijkstra_interactive.py
+++ b/tests/integration/dijkstra_interactive.py
@@ -29,24 +29,20 @@ 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, color_layer, entities
-
+ global grid, entities
+
mcrfpy.createScene("dijkstra_interactive")
-
+
# Create grid - 14x10 as specified
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 = [
@@ -61,35 +57,36 @@ def create_map():
"..W.WWW.......", # Row 8
"..............", # Row 9
]
-
+
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
-
+
if char == 'W':
# Wall
cell.walkable = False
cell.transparent = False
- color_layer.set(x, y, WALL_COLOR)
+ cell.color = WALL_COLOR
else:
# Floor
cell.walkable = True
cell.transparent = True
- color_layer.set(x, y, FLOOR_COLOR)
-
+ cell.color = FLOOR_COLOR
+
if char == 'E':
# Entity position
entity_positions.append((x, y))
-
+
# Create entities at marked positions
entities = []
for i, (x, y) in enumerate(entity_positions):
- entity = mcrfpy.Entity((x, y), grid=grid)
+ entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
+ grid.entities.append(entity)
entities.append(entity)
-
+
return grid
def clear_path_highlight():
@@ -99,37 +96,37 @@ def clear_path_highlight():
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
- color_layer.set(x, y, FLOOR_COLOR)
+ cell.color = FLOOR_COLOR
def highlight_path():
"""Highlight the path between selected entities"""
if first_point is None or second_point is None:
return
-
+
# Clear previous highlighting
clear_path_highlight()
-
+
# Get entities
entity1 = entities[first_point]
entity2 = entities[second_point]
-
+
# Compute Dijkstra from first entity
grid.compute_dijkstra(int(entity1.x), int(entity1.y))
-
+
# Get path to second entity
path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y))
-
+
if path:
# Highlight the path
for x, y in path:
cell = grid.at(x, y)
if cell.walkable:
- color_layer.set(x, y, PATH_COLOR)
-
+ cell.color = PATH_COLOR
+
# Also highlight start and end with entity colors
- 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])
-
+ 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]
+
# Update info
distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y))
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units"
@@ -202,33 +199,34 @@ grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 60)
# Add title
-title = mcrfpy.Caption(pos=(250, 10), text="Dijkstra Pathfinding Interactive")
+title = mcrfpy.Caption("Dijkstra Pathfinding Interactive", 250, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status text
-status_text = mcrfpy.Caption(pos=(120, 480), text="Press 1/2/3 for first entity, A/B/C for second")
+status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480)
status_text.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status_text)
# Add info text
-info_text = mcrfpy.Caption(pos=(120, 500), text="Space to clear, Q to quit")
+info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500)
info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text)
# Add legend
-legend1 = mcrfpy.Caption(pos=(120, 540), text="Entities: 1=Red 2=Green 3=Blue")
+legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 540)
legend1.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend1)
-legend2 = mcrfpy.Caption(pos=(120, 560), text="Colors: Dark=Wall Light=Floor Cyan=Path")
+legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 560)
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(pos=(120 + int(entity.x) * 40 + 15, 60 + int(entity.y) * 40 + 10),
- text=str(i+1))
+ marker = mcrfpy.Caption(str(i+1),
+ 120 + int(entity.x) * 40 + 15,
+ 60 + int(entity.y) * 40 + 10)
marker.fill_color = ENTITY_COLORS[i]
marker.outline = 1
marker.outline_color = mcrfpy.Color(0, 0, 0)
diff --git a/tests/integration/dijkstra_interactive_enhanced.py b/tests/integration/dijkstra_interactive_enhanced.py
index 35c8655..34da805 100644
--- a/tests/integration/dijkstra_interactive_enhanced.py
+++ b/tests/integration/dijkstra_interactive_enhanced.py
@@ -32,7 +32,6 @@ ENTITY_COLORS = [
# Global state
grid = None
-color_layer = None
entities = []
first_point = None
second_point = None
@@ -44,17 +43,14 @@ original_positions = [] # Store original entity positions
def create_map():
"""Create the interactive map with the layout specified by the user"""
- global grid, color_layer, entities, original_positions
-
+ global grid, entities, original_positions
+
mcrfpy.createScene("dijkstra_enhanced")
-
+
# Create grid - 14x10 as specified
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 = [
@@ -69,86 +65,87 @@ def create_map():
"..W.WWW.......", # Row 8
"..............", # Row 9
]
-
+
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
-
+
if char == 'W':
# Wall
cell.walkable = False
cell.transparent = False
- color_layer.set(x, y, WALL_COLOR)
+ cell.color = WALL_COLOR
else:
# Floor
cell.walkable = True
cell.transparent = True
- color_layer.set(x, y, FLOOR_COLOR)
-
+ cell.color = FLOOR_COLOR
+
if char == 'E':
# Entity position
entity_positions.append((x, y))
-
+
# Create entities at marked positions
entities = []
original_positions = []
for i, (x, y) in enumerate(entity_positions):
- entity = mcrfpy.Entity((x, y), grid=grid)
+ entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
+ grid.entities.append(entity)
entities.append(entity)
original_positions.append((x, y))
-
+
return grid
def clear_path_highlight():
"""Clear any existing path highlighting"""
global current_path
-
+
# Reset all floor tiles to original color
for y in range(grid.grid_y):
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
- color_layer.set(x, y, FLOOR_COLOR)
-
+ cell.color = FLOOR_COLOR
+
current_path = []
def highlight_path():
"""Highlight the path between selected entities using entity.path_to()"""
global current_path
-
+
if first_point is None or second_point is None:
return
-
+
# Clear previous highlighting
clear_path_highlight()
-
+
# Get entities
entity1 = entities[first_point]
entity2 = entities[second_point]
-
+
# Use the new path_to method!
path = entity1.path_to(int(entity2.x), int(entity2.y))
-
+
if path:
current_path = path
-
+
# Highlight the path
for i, (x, y) in enumerate(path):
cell = grid.at(x, y)
if cell.walkable:
# Use gradient for path visualization
if i < len(path) - 1:
- color_layer.set(x, y, PATH_COLOR)
+ cell.color = PATH_COLOR
else:
- color_layer.set(x, y, VISITED_COLOR)
-
+ cell.color = VISITED_COLOR
+
# Highlight start and end with entity colors
- 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])
-
+ 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]
+
# Update info
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps"
else:
@@ -294,38 +291,39 @@ grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 60)
# Add title
-title = mcrfpy.Caption(pos=(250, 10), text="Enhanced Dijkstra Pathfinding")
+title = mcrfpy.Caption("Enhanced Dijkstra Pathfinding", 250, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status text
-status_text = mcrfpy.Caption(pos=(120, 480), text="Press 1/2/3 for first entity, A/B/C for second")
+status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480)
status_text.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status_text)
# Add info text
-info_text = mcrfpy.Caption(pos=(120, 500), text="Space to clear, Q to quit")
+info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500)
info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text)
# Add control text
-control_text = mcrfpy.Caption(pos=(120, 520), text="Press M to move, P to pause, R to reset")
+control_text = mcrfpy.Caption("Press M to move, P to pause, R to reset", 120, 520)
control_text.fill_color = mcrfpy.Color(150, 200, 150)
ui.append(control_text)
# Add legend
-legend1 = mcrfpy.Caption(pos=(120, 560), text="Entities: 1=Red 2=Green 3=Blue")
+legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 560)
legend1.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend1)
-legend2 = mcrfpy.Caption(pos=(120, 580), text="Colors: Dark=Wall Light=Floor Cyan=Path")
+legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 580)
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(pos=(120 + int(entity.x) * 40 + 15, 60 + int(entity.y) * 40 + 10),
- text=str(i+1))
+ marker = mcrfpy.Caption(str(i+1),
+ 120 + int(entity.x) * 40 + 15,
+ 60 + int(entity.y) * 40 + 10)
marker.fill_color = ENTITY_COLORS[i]
marker.outline = 1
marker.outline_color = mcrfpy.Color(0, 0, 0)
diff --git a/tests/integration/dijkstra_test.py b/tests/integration/dijkstra_test.py
index 928a56e..9f99eeb 100644
--- a/tests/integration/dijkstra_test.py
+++ b/tests/integration/dijkstra_test.py
@@ -128,12 +128,12 @@ grid.position = (50, 50)
grid.size = (500, 300)
# Add title
-title = mcrfpy.Caption(pos=(200, 10), text="Dijkstra Pathfinding Test")
+title = mcrfpy.Caption("Dijkstra Pathfinding Test", 200, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add legend
-legend = mcrfpy.Caption(pos=(50, 360), text="Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3")
+legend = mcrfpy.Caption("Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3", 50, 360)
legend.fill_color = mcrfpy.Color(180, 180, 180)
ui.append(legend)
diff --git a/tests/integration/interactive_visibility.py b/tests/integration/interactive_visibility.py
index dcb386d..3d7aef8 100644
--- a/tests/integration/interactive_visibility.py
+++ b/tests/integration/interactive_visibility.py
@@ -19,36 +19,33 @@ 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
- color_layer.set(x, y, mcrfpy.Color(100, 100, 120)) # Floor color
+ cell.color = mcrfpy.Color(100, 100, 120) # Floor color
# Create walls
walls = [
# Central cross
[(15, y) for y in range(8, 12)],
[(x, 10) for x in range(13, 18)],
-
+
# Rooms
# Top-left room
[(x, 5) for x in range(2, 8)] + [(8, y) for y in range(2, 6)],
[(2, y) for y in range(2, 6)] + [(x, 2) for x in range(2, 8)],
-
- # Top-right room
+
+ # Top-right room
[(x, 5) for x in range(22, 28)] + [(22, y) for y in range(2, 6)],
[(28, y) for y in range(2, 6)] + [(x, 2) for x in range(22, 28)],
-
+
# Bottom-left room
[(x, 15) for x in range(2, 8)] + [(8, y) for y in range(15, 18)],
[(2, y) for y in range(15, 18)] + [(x, 18) for x in range(2, 8)],
-
+
# Bottom-right room
[(x, 15) for x in range(22, 28)] + [(22, y) for y in range(15, 18)],
[(28, y) for y in range(15, 18)] + [(x, 18) for x in range(22, 28)],
@@ -60,12 +57,12 @@ for wall_group in walls:
cell = grid.at(x, y)
cell.walkable = False
cell.transparent = False
- color_layer.set(x, y, mcrfpy.Color(40, 20, 20)) # Wall color
+ cell.color = 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
@@ -83,24 +80,24 @@ grid.position = (50, 100)
grid.size = (900, 600) # 30*30, 20*30
# Title
-title = mcrfpy.Caption(pos=(350, 20), text="Interactive Visibility Demo")
+title = mcrfpy.Caption("Interactive Visibility Demo", 350, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Info displays
-perspective_label = mcrfpy.Caption(pos=(50, 50), text="Perspective: Omniscient")
+perspective_label = mcrfpy.Caption("Perspective: Omniscient", 50, 50)
perspective_label.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(perspective_label)
-controls = mcrfpy.Caption(pos=(50, 730), text="WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset")
+controls = mcrfpy.Caption("WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset", 50, 730)
controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls)
-player_info = mcrfpy.Caption(pos=(700, 50), text="Player: (5, 10)")
+player_info = mcrfpy.Caption("Player: (5, 10)", 700, 50)
player_info.fill_color = mcrfpy.Color(100, 255, 100)
ui.append(player_info)
-enemy_info = mcrfpy.Caption(pos=(700, 70), text="Enemy: (25, 10)")
+enemy_info = mcrfpy.Caption("Enemy: (25, 10)", 700, 70)
enemy_info.fill_color = mcrfpy.Color(255, 100, 100)
ui.append(enemy_info)
diff --git a/tests/integration/simple_interactive_visibility.py b/tests/integration/simple_interactive_visibility.py
index 6243ecb..fd95d5a 100644
--- a/tests/integration/simple_interactive_visibility.py
+++ b/tests/integration/simple_interactive_visibility.py
@@ -11,9 +11,6 @@ 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):
@@ -21,11 +18,11 @@ for y in range(10):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
- color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
+ cell.color = 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...")
diff --git a/tests/integration/simple_visibility_test.py b/tests/integration/simple_visibility_test.py
index 1e00b73..5c20758 100644
--- a/tests/integration/simple_visibility_test.py
+++ b/tests/integration/simple_visibility_test.py
@@ -13,8 +13,8 @@ print("Scene created")
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
print("Grid created")
-# Create entity with grid association
-entity = mcrfpy.Entity((2, 2), grid=grid)
+# Create entity without appending
+entity = mcrfpy.Entity(2, 2, grid=grid)
print(f"Entity created at ({entity.x}, {entity.y})")
# Check if gridstate is initialized
diff --git a/tests/regression/issue_123_chunk_system_test.py b/tests/regression/issue_123_chunk_system_test.py
index f8be052..0437494 100644
--- a/tests/regression/issue_123_chunk_system_test.py
+++ b/tests/regression/issue_123_chunk_system_test.py
@@ -8,10 +8,6 @@ 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
@@ -23,21 +19,22 @@ 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)
- color_layer.set(x, y, mcrfpy.Color((x * 5) % 256, (y * 5) % 256, 128, 255))
+ cell.color = 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 = 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})")
+ 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})")
return False
print(" Small grid: PASS")
@@ -49,7 +46,6 @@ 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
@@ -65,14 +61,15 @@ def test_large_grid():
for x, y in test_points:
cell = grid.at(x, y)
- color_layer.set(x, y, mcrfpy.Color(x, y, 100, 255))
+ cell.color = mcrfpy.Color(x, y, 100, 255)
cell.tilesprite = -1
# Verify cells
for x, y in test_points:
- 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})")
+ 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]})")
return False
print(" Large grid cell access: PASS")
@@ -84,7 +81,6 @@ 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 = [
@@ -98,12 +94,14 @@ def test_very_large_grid():
]
for x, y in test_points:
- color_layer.set(x, y, mcrfpy.Color(x % 256, y % 256, 200, 255))
+ cell = grid.at(x, y)
+ cell.color = mcrfpy.Color(x % 256, y % 256, 200, 255)
# Verify
for x, y in test_points:
- color = color_layer.at(x, y)
- if color.r != (x % 256) or color.g != (y % 256):
+ cell = grid.at(x, y)
+ color = cell.color
+ if color[0] != (x % 256) or color[1] != (y % 256):
print(f"FAIL: Very large grid cell ({x},{y}) color mismatch")
return False
@@ -116,20 +114,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))
- 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}")
+ 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]}")
return False
# 65x65 should use chunk storage (exceeding threshold)
grid_65 = mcrfpy.Grid(grid_size=(65, 65), pos=(10, 10), size=(400, 400))
- 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}")
+ 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]}")
return False
print(" Boundary cases: PASS")
@@ -141,18 +139,19 @@ 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):
- color_layer.set(x, y, mcrfpy.Color(i * 60, i * 60, i * 60, 255))
+ cell = grid.at(x, y)
+ cell.color = 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 = color_layer.at(x, y)
- if color.r != expected:
- print(f"FAIL: Corner ({x},{y}) color mismatch, expected {expected}, got {color.r}")
+ color = cell.color
+ if color[0] != expected:
+ print(f"FAIL: Corner ({x},{y}) color mismatch, expected {expected}, got {color[0]}")
return False
print(" Edge cases: PASS")
diff --git a/tests/regression/issue_76_test.py b/tests/regression/issue_76_test.py
index ecd985d..96dd723 100644
--- a/tests/regression/issue_76_test.py
+++ b/tests/regression/issue_76_test.py
@@ -10,10 +10,10 @@ import sys
# Create a derived Entity class
class CustomEntity(mcrfpy.Entity):
- def __init__(self, pos):
- super().__init__(pos)
+ def __init__(self, x, y):
+ super().__init__(x, y)
self.custom_attribute = "I am custom!"
-
+
def custom_method(self):
return "Custom method called"
@@ -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(grid_size=(10, 10))
-
+ grid = mcrfpy.Grid(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)
diff --git a/tests/unit/WORKING_automation_test_example.py b/tests/unit/WORKING_automation_test_example.py
index 91daaef..bb22673 100644
--- a/tests/unit/WORKING_automation_test_example.py
+++ b/tests/unit/WORKING_automation_test_example.py
@@ -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(pos=(100, 100), size=(400, 300),
+frame = mcrfpy.Frame(100, 100, 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(pos=(150, 150),
+caption = mcrfpy.Caption(mcrfpy.Vector(150, 150),
text="TIMER TEST - SHOULD BE VISIBLE",
fill_color=mcrfpy.Color(255, 255, 255))
-caption.font_size = 24
+caption.size = 24
frame.children.append(caption)
# Add click handler to demonstrate interaction
diff --git a/tests/unit/check_entity_attrs.py b/tests/unit/check_entity_attrs.py
index 564ea62..d0a44b8 100644
--- a/tests/unit/check_entity_attrs.py
+++ b/tests/unit/check_entity_attrs.py
@@ -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))
\ No newline at end of file
diff --git a/tests/unit/debug_render_test.py b/tests/unit/debug_render_test.py
index 1442f09..d7c7f6c 100644
--- a/tests/unit/debug_render_test.py
+++ b/tests/unit/debug_render_test.py
@@ -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(pos=(0, 0), size=(100, 100),
+frame = mcrfpy.Frame(0, 0, 100, 100,
fill_color=mcrfpy.Color(255, 255, 255))
ui.append(frame)
print(f"After adding frame: {len(ui)} elements")
diff --git a/tests/unit/generate_grid_screenshot.py b/tests/unit/generate_grid_screenshot.py
index 8c4500d..706b704 100644
--- a/tests/unit/generate_grid_screenshot.py
+++ b/tests/unit/generate_grid_screenshot.py
@@ -22,13 +22,14 @@ mcrfpy.createScene("grid")
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Title
-title = mcrfpy.Caption(pos=(400, 30), text="Grid Example - Dungeon View")
+title = mcrfpy.Caption(400, 30, "Grid Example - Dungeon View")
title.font = mcrfpy.default_font
title.font_size = 24
-title.fill_color = mcrfpy.Color(255, 255, 255)
+title.font_color = (255, 255, 255)
# Create main grid (20x15 tiles, each 32x32 pixels)
-grid = mcrfpy.Grid(pos=(100, 100), grid_size=(20, 15), texture=texture, size=(640, 480))
+grid = mcrfpy.Grid(100, 100, 20, 15, texture, 32, 32)
+grid.texture = texture
# Define tile types from Crypt of Sokoban
FLOOR = 58 # Stone floor
@@ -62,21 +63,36 @@ grid.set_tile(12, 8, BOULDER)
# Create some entities on the grid
# Player entity
-player = mcrfpy.Entity((5, 7), texture=texture, sprite_index=84, grid=grid) # Player sprite
+player = mcrfpy.Entity(5, 7)
+player.texture = texture
+player.sprite_index = 84 # Player sprite
# Enemy entities
-rat1 = mcrfpy.Entity((12, 5), texture=texture, sprite_index=123, grid=grid) # Rat
+rat1 = mcrfpy.Entity(12, 5)
+rat1.texture = texture
+rat1.sprite_index = 123 # Rat
-rat2 = mcrfpy.Entity((14, 9), texture=texture, sprite_index=123, grid=grid) # Rat
+rat2 = mcrfpy.Entity(14, 9)
+rat2.texture = texture
+rat2.sprite_index = 123 # Rat
-cyclops = mcrfpy.Entity((10, 10), texture=texture, sprite_index=109, grid=grid) # Cyclops
+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)
# Create a smaller grid showing tile palette
-palette_label = mcrfpy.Caption(pos=(100, 600), text="Tile Types:")
+palette_label = mcrfpy.Caption(100, 600, "Tile Types:")
palette_label.font = mcrfpy.default_font
-palette_label.fill_color = mcrfpy.Color(255, 255, 255)
+palette_label.font_color = (255, 255, 255)
-palette = mcrfpy.Grid(pos=(250, 580), grid_size=(7, 1), texture=texture, size=(224, 32))
+palette = mcrfpy.Grid(250, 580, 7, 1, texture, 32, 32)
+palette.texture = texture
palette.set_tile(0, 0, FLOOR)
palette.set_tile(1, 0, WALL)
palette.set_tile(2, 0, DOOR)
@@ -88,17 +104,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(pos=(250 + i * 32, 615), text=label)
+ l = mcrfpy.Caption(250 + i * 32, 615, label)
l.font = mcrfpy.default_font
l.font_size = 10
- l.fill_color = mcrfpy.Color(255, 255, 255)
+ l.font_color = (255, 255, 255)
mcrfpy.sceneUI("grid").append(l)
# Add info caption
-info = mcrfpy.Caption(pos=(100, 680), text="Grid supports tiles and entities. Entities can move independently of the tile grid.")
+info = mcrfpy.Caption(100, 680, "Grid supports tiles and entities. Entities can move independently of the tile grid.")
info.font = mcrfpy.default_font
info.font_size = 14
-info.fill_color = mcrfpy.Color(200, 200, 200)
+info.font_color = (200, 200, 200)
# Add all elements to scene
ui = mcrfpy.sceneUI("grid")
diff --git a/tests/unit/generate_sprite_screenshot.py b/tests/unit/generate_sprite_screenshot.py
index ff6114c..3a314bb 100644
--- a/tests/unit/generate_sprite_screenshot.py
+++ b/tests/unit/generate_sprite_screenshot.py
@@ -22,20 +22,20 @@ mcrfpy.createScene("sprites")
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Title
-title = mcrfpy.Caption(pos=(400, 30), text="Sprite Examples")
+title = mcrfpy.Caption(400, 30, "Sprite Examples")
title.font = mcrfpy.default_font
title.font_size = 24
-title.fill_color = mcrfpy.Color(255, 255, 255)
+title.font_color = (255, 255, 255)
# Create a frame background
-frame = mcrfpy.Frame(pos=(50, 80), size=(700, 500))
-frame.fill_color = mcrfpy.Color(64, 64, 128)
+frame = mcrfpy.Frame(50, 80, 700, 500)
+frame.bgcolor = (64, 64, 128)
frame.outline = 2
# Player sprite
-player_label = mcrfpy.Caption(pos=(100, 120), text="Player")
+player_label = mcrfpy.Caption(100, 120, "Player")
player_label.font = mcrfpy.default_font
-player_label.fill_color = mcrfpy.Color(255, 255, 255)
+player_label.font_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(pos=(250, 120), text="Enemies")
+enemy_label = mcrfpy.Caption(250, 120, "Enemies")
enemy_label.font = mcrfpy.default_font
-enemy_label.fill_color = mcrfpy.Color(255, 255, 255)
+enemy_label.font_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(pos=(100, 250), text="Items")
+items_label = mcrfpy.Caption(100, 250, "Items")
items_label.font = mcrfpy.default_font
-items_label.fill_color = mcrfpy.Color(255, 255, 255)
+items_label.font_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(pos=(100, 380), text="UI Elements")
+ui_label = mcrfpy.Caption(100, 380, "UI Elements")
ui_label.font = mcrfpy.default_font
-ui_label.fill_color = mcrfpy.Color(255, 255, 255)
+ui_label.font_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(pos=(500, 120), text="Scale Demo")
+scale_label = mcrfpy.Caption(500, 120, "Scale Demo")
scale_label.font = mcrfpy.default_font
-scale_label.fill_color = mcrfpy.Color(255, 255, 255)
+scale_label.font_color = (255, 255, 255)
# Same sprite at different scales
for i, scale in enumerate([1.0, 2.0, 3.0, 4.0]):
diff --git a/tests/unit/screenshot_transparency_fix_test.py b/tests/unit/screenshot_transparency_fix_test.py
index 5d5e333..7da8878 100644
--- a/tests/unit/screenshot_transparency_fix_test.py
+++ b/tests/unit/screenshot_transparency_fix_test.py
@@ -17,42 +17,42 @@ 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(pos=(0, 0), size=(1024, 768),
+ background = mcrfpy.Frame(0, 0, 1024, 768,
fill_color=mcrfpy.Color(50, 50, 50), # Dark gray
outline_color=None,
outline=0.0)
ui.append(background)
print("✓ Added opaque background frame")
-
+
# Now add normal content on top
print("\nAdding test content...")
-
+
# Red frame
- frame1 = mcrfpy.Frame(pos=(100, 100), size=(200, 150),
+ frame1 = mcrfpy.Frame(100, 100, 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(pos=(350, 100), size=(200, 150),
+ frame2 = mcrfpy.Frame(350, 100, 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(pos=(100, 300), size=(200, 150),
+ frame3 = mcrfpy.Frame(100, 300, 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(pos=(250, 50),
+ caption = mcrfpy.Caption(mcrfpy.Vector(250, 50),
text="OPAQUE BACKGROUND TEST",
fill_color=mcrfpy.Color(255, 255, 255))
- caption.font_size = 32
+ caption.size = 32
ui.append(caption)
# Take screenshot
diff --git a/tests/unit/simple_screenshot_test.py b/tests/unit/simple_screenshot_test.py
index 3117a81..42815a4 100644
--- a/tests/unit/simple_screenshot_test.py
+++ b/tests/unit/simple_screenshot_test.py
@@ -31,9 +31,9 @@ def take_screenshot(runtime):
mcrfpy.createScene("test")
# Add a visible element
-caption = mcrfpy.Caption(pos=(100, 100), text="Screenshot Test")
+caption = mcrfpy.Caption(100, 100, "Screenshot Test")
caption.font = mcrfpy.default_font
-caption.fill_color = mcrfpy.Color(255, 255, 255)
+caption.font_color = (255, 255, 255)
caption.font_size = 24
mcrfpy.sceneUI("test").append(caption)
diff --git a/tests/unit/simple_timer_screenshot_test.py b/tests/unit/simple_timer_screenshot_test.py
index d4aa001..5a5c9ac 100644
--- a/tests/unit/simple_timer_screenshot_test.py
+++ b/tests/unit/simple_timer_screenshot_test.py
@@ -30,7 +30,7 @@ mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# Add visible content - a white frame on default background
-frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200),
+frame = mcrfpy.Frame(100, 100, 200, 200,
fill_color=mcrfpy.Color(255, 255, 255))
ui.append(frame)
diff --git a/tests/unit/test_animation_chaining.py b/tests/unit/test_animation_chaining.py
index 7b3700a..b8402fd 100644
--- a/tests/unit/test_animation_chaining.py
+++ b/tests/unit/test_animation_chaining.py
@@ -73,9 +73,6 @@ 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):
@@ -83,17 +80,17 @@ for y in range(15):
if x == 0 or x == 19 or y == 0 or y == 14:
cell.walkable = False
cell.transparent = False
- color_layer.set(x, y, mcrfpy.Color(60, 40, 40))
+ cell.color = mcrfpy.Color(60, 40, 40)
else:
cell.walkable = True
cell.transparent = True
- color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
+ cell.color = 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
@@ -102,15 +99,15 @@ ui.append(grid)
grid.position = (100, 100)
grid.size = (600, 450)
-title = mcrfpy.Caption(pos=(300, 20), text="Animation Chaining Test")
+title = mcrfpy.Caption("Animation Chaining Test", 300, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
-status = mcrfpy.Caption(pos=(100, 50), text="Press 1: Animate Player | 2: Animate Enemy | 3: Both | Q: Quit")
+status = mcrfpy.Caption("Press 1: Animate Player | 2: Animate Enemy | 3: Both | Q: Quit", 100, 50)
status.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status)
-info = mcrfpy.Caption(pos=(100, 70), text="Status: Ready")
+info = mcrfpy.Caption("Status: Ready", 100, 70)
info.fill_color = mcrfpy.Color(100, 255, 100)
ui.append(info)
diff --git a/tests/unit/test_animation_debug.py b/tests/unit/test_animation_debug.py
index 16c21a7..0b7ab7c 100644
--- a/tests/unit/test_animation_debug.py
+++ b/tests/unit/test_animation_debug.py
@@ -63,15 +63,14 @@ 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
- color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
+ cell.color = 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
@@ -80,19 +79,19 @@ ui.append(grid)
grid.position = (100, 150)
grid.size = (450, 300)
-title = mcrfpy.Caption(pos=(250, 20), text="Animation Debug Tool")
+title = mcrfpy.Caption("Animation Debug Tool", 250, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
-status = mcrfpy.Caption(pos=(100, 50), text="Press keys to test animations")
+status = mcrfpy.Caption("Press keys to test animations", 100, 50)
status.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status)
-pos_display = mcrfpy.Caption(pos=(100, 70), text="")
+pos_display = mcrfpy.Caption("", 100, 70)
pos_display.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(pos_display)
-active_display = mcrfpy.Caption(pos=(100, 90), text="Active animations: 0")
+active_display = mcrfpy.Caption("Active animations: 0", 100, 90)
active_display.fill_color = mcrfpy.Color(100, 255, 255)
ui.append(active_display)
diff --git a/tests/unit/test_animation_immediate.py b/tests/unit/test_animation_immediate.py
index e78c63c..d24f713 100644
--- a/tests/unit/test_animation_immediate.py
+++ b/tests/unit/test_animation_immediate.py
@@ -13,7 +13,7 @@ print("2. Getting UI...")
ui = mcrfpy.sceneUI("test")
print("3. Creating frame...")
-frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
+frame = mcrfpy.Frame(100, 100, 200, 200)
ui.append(frame)
print("4. Creating Animation object...")
diff --git a/tests/unit/test_animation_raii.py b/tests/unit/test_animation_raii.py
index 53de59b..86ce225 100644
--- a/tests/unit/test_animation_raii.py
+++ b/tests/unit/test_animation_raii.py
@@ -30,7 +30,7 @@ def test_1_basic_animation():
"""Test that basic animations still work"""
try:
ui = mcrfpy.sceneUI("test")
- frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
+ frame = mcrfpy.Frame(100, 100, 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(pos=(100, 100), size=(100, 100))
+ frame = mcrfpy.Frame(100, 100, 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(pos=(100, 100), size=(100, 100))
+ frame = mcrfpy.Frame(100, 100, 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(pos=(200, 200), size=(100, 100))
+ frame = mcrfpy.Frame(200, 200, 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(pos=(50 * i, 100), size=(40, 40))
+ frame = mcrfpy.Frame(50 * i, 100, 40, 40)
ui.append(frame)
anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce")
anim.start(frame)
@@ -148,9 +148,9 @@ def test_6_animation_after_clear():
"""Test animations after clearing UI"""
try:
ui = mcrfpy.sceneUI("test")
-
+
# Create and animate
- frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
+ frame = mcrfpy.Frame(100, 100, 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(pos=(0, 0), size=(1024, 768))
+bg = mcrfpy.Frame(0, 0, 1024, 768)
bg.fill_color = mcrfpy.Color(20, 20, 30)
ui.append(bg)
diff --git a/tests/unit/test_animation_removal.py b/tests/unit/test_animation_removal.py
index 3aac09d..a626d91 100644
--- a/tests/unit/test_animation_removal.py
+++ b/tests/unit/test_animation_removal.py
@@ -42,14 +42,14 @@ mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# Add title and subtitle (to preserve during clearing)
-title = mcrfpy.Caption(pos=(400, 20), text="Test Title")
-subtitle = mcrfpy.Caption(pos=(400, 50), text="Test Subtitle")
+title = mcrfpy.Caption("Test Title", 400, 20)
+subtitle = mcrfpy.Caption("Test Subtitle", 400, 50)
ui.extend([title, subtitle])
# Create initial animated objects
print("Creating initial animated objects...")
for i in range(10):
- f = mcrfpy.Frame(pos=(50 + i*30, 100), size=(25, 25))
+ f = mcrfpy.Frame(50 + i*30, 100, 25, 25)
f.fill_color = mcrfpy.Color(255, 100, 100)
ui.append(f)
diff --git a/tests/unit/test_dijkstra_pathfinding.py b/tests/unit/test_dijkstra_pathfinding.py
index a28b103..65ee1e6 100644
--- a/tests/unit/test_dijkstra_pathfinding.py
+++ b/tests/unit/test_dijkstra_pathfinding.py
@@ -17,15 +17,10 @@ import sys
def create_test_grid():
"""Create a test grid with obstacles"""
mcrfpy.createScene("dijkstra_test")
-
+
# 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):
@@ -33,8 +28,8 @@ def create_test_grid():
cell.walkable = True
cell.transparent = True
cell.tilesprite = 46 # . period
- color_layer.set(x, y, mcrfpy.Color(50, 50, 50))
-
+ cell.color = mcrfpy.Color(50, 50, 50)
+
# Create some walls to make pathfinding interesting
# Vertical wall
for y in range(5, 15):
@@ -42,8 +37,8 @@ def create_test_grid():
cell.walkable = False
cell.transparent = False
cell.tilesprite = 219 # Block
- color_layer.set(10, y, mcrfpy.Color(100, 100, 100))
-
+ cell.color = mcrfpy.Color(100, 100, 100)
+
# Horizontal wall
for x in range(5, 15):
if x != 10: # Leave a gap
@@ -51,8 +46,8 @@ def create_test_grid():
cell.walkable = False
cell.transparent = False
cell.tilesprite = 219
- color_layer.set(x, 10, mcrfpy.Color(100, 100, 100))
-
+ cell.color = mcrfpy.Color(100, 100, 100)
+
return grid
def test_basic_dijkstra():
@@ -138,7 +133,7 @@ def test_multi_target_scenario():
# Mark threat position
cell = grid.at(tx, ty)
cell.tilesprite = 84 # T for threat
- grid._color_layer.set(tx, ty, mcrfpy.Color(255, 0, 0))
+ cell.color = mcrfpy.Color(255, 0, 0)
# Compute Dijkstra from this threat
grid.compute_dijkstra(tx, ty)
@@ -181,7 +176,7 @@ def test_multi_target_scenario():
# Mark safe position
cell = grid.at(best_pos[0], best_pos[1])
cell.tilesprite = 83 # S for safe
- grid._color_layer.set(best_pos[0], best_pos[1], mcrfpy.Color(0, 255, 0))
+ cell.color = mcrfpy.Color(0, 255, 0)
def run_test(runtime):
"""Timer callback to run tests after scene loads"""
@@ -216,7 +211,7 @@ ui = mcrfpy.sceneUI("dijkstra_test")
ui.append(grid)
# Add title
-title = mcrfpy.Caption(pos=(10, 10), text="Dijkstra Pathfinding Test")
+title = mcrfpy.Caption("Dijkstra Pathfinding Test", 10, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
diff --git a/tests/unit/test_entity_animation.py b/tests/unit/test_entity_animation.py
index 2cf539e..342f340 100644
--- a/tests/unit/test_entity_animation.py
+++ b/tests/unit/test_entity_animation.py
@@ -17,16 +17,13 @@ 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
- color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
+ cell.color = 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),
@@ -35,10 +32,11 @@ 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:
- color_layer.set(x, y, mcrfpy.Color(120, 120, 150))
+ cell = grid.at(x, y)
+ cell.color = 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
@@ -48,27 +46,27 @@ grid.position = (100, 100)
grid.size = (450, 450) # 15 * 30 pixels per cell
# Title
-title = mcrfpy.Caption(pos=(200, 20), text="Entity Animation Test - Square Path")
+title = mcrfpy.Caption("Entity Animation Test - Square Path", 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Status display
-status = mcrfpy.Caption(pos=(100, 50), text="Press SPACE to start animation | Q to quit")
+status = mcrfpy.Caption("Press SPACE to start animation | Q to quit", 100, 50)
status.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status)
# Position display
-pos_display = mcrfpy.Caption(pos=(100, 70), text=f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})")
+pos_display = mcrfpy.Caption(f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})", 100, 70)
pos_display.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(pos_display)
# Animation info
-anim_info = mcrfpy.Caption(pos=(400, 70), text="Animation: Not started")
+anim_info = mcrfpy.Caption("Animation: Not started", 400, 70)
anim_info.fill_color = mcrfpy.Color(100, 255, 255)
ui.append(anim_info)
# Debug info
-debug_info = mcrfpy.Caption(pos=(100, 570), text="Debug: Waiting...")
+debug_info = mcrfpy.Caption("Debug: Waiting...", 100, 570)
debug_info.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(debug_info)
diff --git a/tests/unit/test_entity_fix.py b/tests/unit/test_entity_fix.py
index eef131b..90a660d 100644
--- a/tests/unit/test_entity_fix.py
+++ b/tests/unit/test_entity_fix.py
@@ -33,19 +33,16 @@ 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
- color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
+ cell.color = 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
@@ -55,19 +52,19 @@ grid.position = (100, 150)
grid.size = (450, 300)
# Info displays
-title = mcrfpy.Caption(pos=(250, 20), text="Entity Animation Issue Demo")
+title = mcrfpy.Caption("Entity Animation Issue Demo", 250, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
-pos_info = mcrfpy.Caption(pos=(100, 50), text="")
+pos_info = mcrfpy.Caption("", 100, 50)
pos_info.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(pos_info)
-sprite_info = mcrfpy.Caption(pos=(100, 70), text="")
+sprite_info = mcrfpy.Caption("", 100, 70)
sprite_info.fill_color = mcrfpy.Color(255, 100, 100)
ui.append(sprite_info)
-status = mcrfpy.Caption(pos=(100, 100), text="Press SPACE to animate entity")
+status = mcrfpy.Caption("Press SPACE to animate entity", 100, 100)
status.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status)
diff --git a/tests/unit/test_entity_path_to.py b/tests/unit/test_entity_path_to.py
index caeb4c1..eab54d4 100644
--- a/tests/unit/test_entity_path_to.py
+++ b/tests/unit/test_entity_path_to.py
@@ -22,7 +22,8 @@ for x, y in walls:
grid.at(x, y).walkable = False
# Create entity
-entity = mcrfpy.Entity((2, 2), grid=grid)
+entity = mcrfpy.Entity(2, 2)
+grid.entities.append(entity)
print(f"Entity at: ({entity.x}, {entity.y})")
diff --git a/tests/unit/test_entity_path_to_edge_cases.py b/tests/unit/test_entity_path_to_edge_cases.py
index ef67d8f..f255aca 100644
--- a/tests/unit/test_entity_path_to_edge_cases.py
+++ b/tests/unit/test_entity_path_to_edge_cases.py
@@ -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,7 +31,8 @@ for y in range(5):
for x in range(5):
grid.at(x, 2).walkable = False
-entity = mcrfpy.Entity((1, 1), grid=grid)
+entity = mcrfpy.Entity(1, 1)
+grid.entities.append(entity)
try:
path = entity.path_to(1, 4)
diff --git a/tests/unit/test_grid_background.py b/tests/unit/test_grid_background.py
index b74daf4..c79cf8e 100644
--- a/tests/unit/test_grid_background.py
+++ b/tests/unit/test_grid_background.py
@@ -13,28 +13,32 @@ def test_grid_background():
ui = mcrfpy.sceneUI("test")
# Create a grid with default background
- grid = mcrfpy.Grid(pos=(50, 50), size=(400, 300), grid_size=(20, 15))
+ grid = mcrfpy.Grid(20, 15, grid_size=(20, 15))
+ grid.x = 50
+ grid.y = 50
+ grid.w = 400
+ grid.h = 300
ui.append(grid)
-
- # Add color layer for some tiles to see the background better
- color_layer = grid.add_layer("color", z_index=-1)
+
+ # Add some tiles to see the background better
for x in range(5, 15):
for y in range(5, 10):
- color_layer.set(x, y, mcrfpy.Color(100, 150, 100))
+ point = grid.at(x, y)
+ point.color = mcrfpy.Color(100, 150, 100)
# Add UI to show current background color
- info_frame = mcrfpy.Frame(pos=(500, 50), size=(200, 150),
+ info_frame = mcrfpy.Frame(500, 50, 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(pos=(510, 60), text="Background Color:")
+
+ color_caption = mcrfpy.Caption(510, 60, "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(pos=(510, 80), text="")
+
+ color_display = mcrfpy.Caption(510, 80, "")
color_display.font_size = 12
color_display.fill_color = mcrfpy.Color(200, 200, 200)
info_frame.children.append(color_display)
diff --git a/tests/unit/test_headless_detection.py b/tests/unit/test_headless_detection.py
index babe65d..bfc284e 100644
--- a/tests/unit/test_headless_detection.py
+++ b/tests/unit/test_headless_detection.py
@@ -11,8 +11,8 @@ ui = mcrfpy.sceneUI("detect_test")
mcrfpy.setScene("detect_test")
# Create a frame
-frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
-frame.fill_color = mcrfpy.Color(255, 100, 100, 255)
+frame = mcrfpy.Frame(100, 100, 200, 200)
+frame.fill_color = (255, 100, 100, 255)
ui.append(frame)
def test_mode(runtime):
diff --git a/tests/unit/test_headless_modes.py b/tests/unit/test_headless_modes.py
index 3e36658..124e9f9 100644
--- a/tests/unit/test_headless_modes.py
+++ b/tests/unit/test_headless_modes.py
@@ -10,13 +10,13 @@ ui = mcrfpy.sceneUI("headless_test")
mcrfpy.setScene("headless_test")
# Create a visible indicator
-frame = mcrfpy.Frame(pos=(200, 200), size=(400, 200))
-frame.fill_color = mcrfpy.Color(100, 200, 100, 255)
+frame = mcrfpy.Frame(200, 200, 400, 200)
+frame.fill_color = (100, 200, 100, 255)
ui.append(frame)
-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)
+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)
ui.append(caption)
print("Script started. Window should appear unless --headless was specified.")
diff --git a/tests/unit/test_metrics.py b/tests/unit/test_metrics.py
index 885e2c5..e760b2b 100644
--- a/tests/unit/test_metrics.py
+++ b/tests/unit/test_metrics.py
@@ -115,18 +115,18 @@ mcrfpy.setScene("metrics_test")
ui = mcrfpy.sceneUI("metrics_test")
# Create various UI elements
-frame1 = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
-frame1.fill_color = mcrfpy.Color(100, 100, 100, 128)
+frame1 = mcrfpy.Frame(10, 10, 200, 150)
+frame1.fill_color = (100, 100, 100, 128)
ui.append(frame1)
-caption1 = mcrfpy.Caption(pos=(50, 50), text="Test Caption")
+caption1 = mcrfpy.Caption("Test Caption", 50, 50)
ui.append(caption1)
-sprite1 = mcrfpy.Sprite(pos=(100, 100))
+sprite1 = mcrfpy.Sprite(100, 100)
ui.append(sprite1)
# Invisible element (should not count as visible)
-frame2 = mcrfpy.Frame(pos=(300, 10), size=(100, 100))
+frame2 = mcrfpy.Frame(300, 10, 100, 100)
frame2.visible = False
ui.append(frame2)
diff --git a/tests/unit/test_path_colors.py b/tests/unit/test_path_colors.py
index 1bcd9cd..779ff9e 100644
--- a/tests/unit/test_path_colors.py
+++ b/tests/unit/test_path_colors.py
@@ -11,20 +11,17 @@ 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
- color_layer.set(x, y, mcrfpy.Color(200, 200, 200)) # Light gray
+ grid.at(x, y).color = mcrfpy.Color(200, 200, 200) # Light gray
# Add entities
-e1 = mcrfpy.Entity((0, 0), grid=grid)
-e2 = mcrfpy.Entity((4, 4), grid=grid)
-e1.sprite_index = 64
-e2.sprite_index = 69
+e1 = mcrfpy.Entity(0, 0)
+e2 = mcrfpy.Entity(4, 4)
+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})")
@@ -38,25 +35,24 @@ 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_c = color_layer.at(x, y)
- before = (before_c.r, before_c.g, before_c.b)
-
+ before = cell.color[:3] # Get RGB from tuple
+
# Set color
- color_layer.set(x, y, PATH_COLOR)
-
+ cell.color = PATH_COLOR
+
# Check after
- after_c = color_layer.at(x, y)
- after = (after_c.r, after_c.g, after_c.b)
-
+ after = cell.color[:3] # Get RGB from tuple
+
print(f" Cell ({x},{y}): {before} -> {after}")
# Verify all path cells
print("\nVerifying all cells in grid:")
for y in range(5):
for x in range(5):
- c = color_layer.at(x, y)
- color = (c.r, c.g, c.b)
+ cell = grid.at(x, y)
+ color = cell.color[:3] # Get RGB from tuple
is_path = (x, y) in path
print(f" ({x},{y}): color={color}, in_path={is_path}")
diff --git a/tests/unit/test_pathfinding_integration.py b/tests/unit/test_pathfinding_integration.py
index a27f6a5..8f779f6 100644
--- a/tests/unit/test_pathfinding_integration.py
+++ b/tests/unit/test_pathfinding_integration.py
@@ -21,8 +21,10 @@ for i in range(5):
grid.at(5, i + 2).walkable = False
# Create entities
-e1 = mcrfpy.Entity((2, 5), grid=grid)
-e2 = mcrfpy.Entity((8, 5), grid=grid)
+e1 = mcrfpy.Entity(2, 5)
+e2 = mcrfpy.Entity(8, 5)
+grid.entities.append(e1)
+grid.entities.append(e2)
# Test pathfinding between entities
print(f"Entity 1 at ({e1.x}, {e1.y})")
diff --git a/tests/unit/test_properties_quick.py b/tests/unit/test_properties_quick.py
index e16774a..31822c2 100644
--- a/tests/unit/test_properties_quick.py
+++ b/tests/unit/test_properties_quick.py
@@ -10,7 +10,7 @@ def test_properties(runtime):
# Test Frame
try:
- frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
+ frame = mcrfpy.Frame(10, 10, 100, 100)
print(f"Frame visible: {frame.visible}")
frame.visible = False
print(f"Frame visible after setting to False: {frame.visible}")
diff --git a/tests/unit/test_scene_object_api.py b/tests/unit/test_scene_object_api.py
deleted file mode 100644
index f52c1d4..0000000
--- a/tests/unit/test_scene_object_api.py
+++ /dev/null
@@ -1,235 +0,0 @@
-#!/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()
diff --git a/tests/unit/test_scene_transitions.py b/tests/unit/test_scene_transitions.py
index ea541b6..603db6a 100644
--- a/tests/unit/test_scene_transitions.py
+++ b/tests/unit/test_scene_transitions.py
@@ -11,51 +11,51 @@ def create_test_scenes():
# Scene 1: Red background
mcrfpy.createScene("red_scene")
ui1 = mcrfpy.sceneUI("red_scene")
- 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)
+ 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)
ui1.append(bg1)
ui1.append(label1)
-
+
# Scene 2: Blue background
mcrfpy.createScene("blue_scene")
ui2 = mcrfpy.sceneUI("blue_scene")
- 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)
+ 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)
ui2.append(bg2)
ui2.append(label2)
-
+
# Scene 3: Green background
mcrfpy.createScene("green_scene")
ui3 = mcrfpy.sceneUI("green_scene")
- 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
+ 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
ui3.append(bg3)
ui3.append(label3)
-
+
# Scene 4: Menu scene with buttons
mcrfpy.createScene("menu_scene")
ui4 = mcrfpy.sceneUI("menu_scene")
- bg4 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(50, 50, 50, 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)
+ bg4 = mcrfpy.Frame(0, 0, 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)
ui4.append(bg4)
ui4.append(title)
-
+
# Add instruction text
- 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)
+ 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)
ui4.append(instructions)
-
- 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)
+
+ 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)
ui4.append(controls)
-
- 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)
+
+ 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)
ui4.append(scene_info)
print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene")
diff --git a/tests/unit/test_scene_transitions_headless.py b/tests/unit/test_scene_transitions_headless.py
index 1e9b571..3dd791a 100644
--- a/tests/unit/test_scene_transitions_headless.py
+++ b/tests/unit/test_scene_transitions_headless.py
@@ -13,13 +13,13 @@ def test_scene_transitions():
# Scene 1
mcrfpy.createScene("scene1")
ui1 = mcrfpy.sceneUI("scene1")
- frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100), fill_color=mcrfpy.Color(255, 0, 0))
+ frame1 = mcrfpy.Frame(0, 0, 100, 100, fill_color=mcrfpy.Color(255, 0, 0))
ui1.append(frame1)
-
- # Scene 2
+
+ # Scene 2
mcrfpy.createScene("scene2")
ui2 = mcrfpy.sceneUI("scene2")
- frame2 = mcrfpy.Frame(pos=(0, 0), size=(100, 100), fill_color=mcrfpy.Color(0, 0, 255))
+ frame2 = mcrfpy.Frame(0, 0, 100, 100, fill_color=mcrfpy.Color(0, 0, 255))
ui2.append(frame2)
# Test each transition type
diff --git a/tests/unit/test_simple_drawable.py b/tests/unit/test_simple_drawable.py
index 8a03baf..a42fdcb 100644
--- a/tests/unit/test_simple_drawable.py
+++ b/tests/unit/test_simple_drawable.py
@@ -8,7 +8,7 @@ def simple_test(runtime):
try:
# Test basic functionality
- frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
+ frame = mcrfpy.Frame(10, 10, 100, 100)
print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}")
bounds = frame.get_bounds()
diff --git a/tests/unit/test_text_input.py b/tests/unit/test_text_input.py
index bc39a7f..69464df 100644
--- a/tests/unit/test_text_input.py
+++ b/tests/unit/test_text_input.py
@@ -18,13 +18,13 @@ def create_demo():
scene = mcrfpy.sceneUI("text_demo")
# Background
- bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
- bg.fill_color = mcrfpy.Color(40, 40, 40, 255)
+ bg = mcrfpy.Frame(0, 0, 800, 600)
+ bg.fill_color = (40, 40, 40, 255)
scene.append(bg)
-
+
# Title
- title = mcrfpy.Caption(pos=(20, 20), text="Text Input Widget Demo")
- title.fill_color = mcrfpy.Color(255, 255, 255, 255)
+ title = mcrfpy.Caption("Text Input Widget Demo", 20, 20)
+ title.fill_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(pos=(50, 360), text="Ready for input...")
- status.fill_color = mcrfpy.Color(150, 255, 150, 255)
+ status = mcrfpy.Caption("Ready for input...", 50, 360)
+ status.fill_color = (150, 255, 150, 255)
scene.append(status)
# Update handler
diff --git a/tests/unit/test_uicaption_visual.py b/tests/unit/test_uicaption_visual.py
index a4e3fc5..7d578f2 100644
--- a/tests/unit/test_uicaption_visual.py
+++ b/tests/unit/test_uicaption_visual.py
@@ -69,11 +69,11 @@ def main():
mcrfpy.setScene("test")
# Create multiple captions for testing
- 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))
+ 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))
# 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(pos=(40, 40), size=(400, 250), fill_color=mcrfpy.Color(50, 50, 50))
+ frame = mcrfpy.Frame(40, 40, 400, 250, fill_color=(50, 50, 50))
frame.z_index = -1 # Put it behind the captions
ui.append(frame)
diff --git a/tests/unit/test_visibility.py b/tests/unit/test_visibility.py
index b866078..23ea9fc 100644
--- a/tests/unit/test_visibility.py
+++ b/tests/unit/test_visibility.py
@@ -18,9 +18,6 @@ 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):
@@ -28,7 +25,7 @@ for y in range(15):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
- color_layer.set(x, y, mcrfpy.Color(100, 100, 120)) # Floor color
+ cell.color = mcrfpy.Color(100, 100, 120) # Floor color
# Create some walls to block vision
print("Adding walls...")
@@ -50,14 +47,14 @@ for wall_group in walls:
cell = grid.at(x, y)
cell.walkable = False
cell.transparent = False
- color_layer.set(x, y, mcrfpy.Color(40, 20, 20)) # Wall color
+ cell.color = 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):
@@ -141,17 +138,17 @@ grid.position = (50, 50)
grid.size = (600, 450) # 20*30, 15*30
# Add title
-title = mcrfpy.Caption(pos=(200, 10), text="Knowledge Stubs 1 - Visibility Test")
+title = mcrfpy.Caption("Knowledge Stubs 1 - Visibility Test", 200, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add info
-info = mcrfpy.Caption(pos=(50, 520), text="Perspective: -1 (omniscient)")
+info = mcrfpy.Caption("Perspective: -1 (omniscient)", 50, 520)
info.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info)
# Add legend
-legend = mcrfpy.Caption(pos=(50, 540), text="Black=Never seen, Dark gray=Discovered, Normal=Visible")
+legend = mcrfpy.Caption("Black=Never seen, Dark gray=Discovered, Normal=Visible", 50, 540)
legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend)
diff --git a/tests/unit/test_visual_path.py b/tests/unit/test_visual_path.py
index 11a8c71..31b385f 100644
--- a/tests/unit/test_visual_path.py
+++ b/tests/unit/test_visual_path.py
@@ -4,10 +4,28 @@
import mcrfpy
import sys
-# Colors
-WALL_COLOR = mcrfpy.Color(60, 30, 30)
-FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
-PATH_COLOR = mcrfpy.Color(100, 255, 100)
+# 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)
+
+def check_render(dt):
+ """Timer callback to verify rendering"""
+ print(f"\nTimer fired after {dt}ms")
+
+ # Take screenshot
+ from mcrfpy import automation
+ automation.screenshot("visual_path_test.png")
+ print("Screenshot saved as visual_path_test.png")
+
+ # Sample some path cells to verify colors
+ print("\nSampling path cell colors from grid:")
+ for x, y in [(1, 1), (2, 2), (3, 3)]:
+ cell = grid.at(x, y)
+ color = cell.color
+ print(f" Cell ({x},{y}): color={color[:3]}")
+
+ sys.exit(0)
# Create scene
mcrfpy.createScene("visual_test")
@@ -16,38 +34,20 @@ mcrfpy.createScene("visual_test")
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"""
- print(f"\nTimer fired after {dt}ms")
-
- # Take screenshot
- from mcrfpy import automation
- automation.screenshot("visual_path_test.png")
- print("Screenshot saved as visual_path_test.png")
-
- # Sample some path cells to verify colors
- print("\nSampling path cell colors from grid:")
- for x, y in [(1, 1), (2, 2), (3, 3)]:
- color = color_layer.at(x, y)
- print(f" Cell ({x},{y}): color=({color.r}, {color.g}, {color.b})")
-
- sys.exit(0)
-
# Initialize all cells as floor
print("Initializing grid...")
for y in range(5):
for x in range(5):
grid.at(x, y).walkable = True
- color_layer.set(x, y, FLOOR_COLOR)
+ grid.at(x, y).color = FLOOR_COLOR
# Create entities
-e1 = mcrfpy.Entity((0, 0), grid=grid)
-e2 = mcrfpy.Entity((4, 4), grid=grid)
+e1 = mcrfpy.Entity(0, 0)
+e2 = mcrfpy.Entity(4, 4)
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:
- color_layer.set(x, y, PATH_COLOR)
+ grid.at(x, y).color = 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(pos=(50, 10), text="Path Visualization Test")
+title = mcrfpy.Caption("Path Visualization Test", 50, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
diff --git a/tests/unit/ui_Frame_test_detailed.py b/tests/unit/ui_Frame_test_detailed.py
index 938a5a4..3058d70 100644
--- a/tests/unit/ui_Frame_test_detailed.py
+++ b/tests/unit/ui_Frame_test_detailed.py
@@ -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(pos=(10, 10), text="Child 1")
- child2 = mcrfpy.Sprite(pos=(10, 30))
-
+ child1 = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Child 1")
+ child2 = mcrfpy.Sprite(mcrfpy.Vector(10, 30))
+
# Try to create frame with children argument
- frame = mcrfpy.Frame(pos=(10, 10), size=(200, 150), children=[child1, child2])
+ frame = mcrfpy.Frame(10, 10, 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(pos=(10, 10), size=(200, 150))
+ frame = mcrfpy.Frame(10, 10, 200, 150)
ui.append(frame)
-
+
# Add children via the children collection
- child1 = mcrfpy.Caption(pos=(10, 10), text="Added Child 1")
- child2 = mcrfpy.Caption(pos=(10, 30), text="Added Child 2")
+ child1 = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Added Child 1")
+ child2 = mcrfpy.Caption(mcrfpy.Vector(10, 30), text="Added Child 2")
frame.children.append(child1)
frame.children.append(child2)
@@ -65,33 +65,33 @@ def test_issue_42_click_callback():
return True
try:
- frame1 = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
+ frame1 = mcrfpy.Frame(10, 10, 200, 150)
ui.append(frame1)
frame1.on_click = correct_callback
print("✓ Click callback with correct signature assigned successfully")
except Exception as e:
print(f"✗ Failed to assign correct callback: {type(e).__name__}: {e}")
-
+
# Test 2: Callback with wrong signature (no args)
print("\nTest 2: Click callback with no arguments")
def wrong_callback_no_args():
print(" Wrong callback called")
-
+
try:
- frame2 = mcrfpy.Frame(pos=(220, 10), size=(200, 150))
+ frame2 = mcrfpy.Frame(220, 10, 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)")
except Exception as e:
print(f"✗ Failed to assign callback: {type(e).__name__}: {e}")
-
+
# Test 3: Callback with wrong signature (too few args)
print("\nTest 3: Click callback with too few arguments")
def wrong_callback_few_args(x, y):
print(f" Wrong callback called: x={x}, y={y}")
-
+
try:
- frame3 = mcrfpy.Frame(pos=(10, 170), size=(200, 150))
+ frame3 = mcrfpy.Frame(10, 170, 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)")
diff --git a/tests/unit/ui_Grid_none_texture_test.py b/tests/unit/ui_Grid_none_texture_test.py
index a283cee..38150ef 100644
--- a/tests/unit/ui_Grid_none_texture_test.py
+++ b/tests/unit/ui_Grid_none_texture_test.py
@@ -7,24 +7,24 @@ import sys
def test_grid_none_texture(runtime):
"""Test Grid functionality without texture"""
print("\n=== Testing Grid with None texture ===")
-
+
# Test 1: Create Grid with None texture
try:
- grid = mcrfpy.Grid(grid_size=(10, 10), pos=(50, 50), size=(400, 400))
+ grid = mcrfpy.Grid(10, 10, None, mcrfpy.Vector(50, 50), mcrfpy.Vector(400, 400))
print("✓ Grid created successfully with None texture")
except Exception as e:
print(f"✗ Failed to create Grid with None texture: {e}")
sys.exit(1)
-
+
# Add to UI
ui = mcrfpy.sceneUI("grid_none_test")
ui.append(grid)
-
+
# Test 2: Verify grid properties
try:
grid_size = grid.grid_size
print(f"✓ Grid size: {grid_size}")
-
+
# Check texture property
texture = grid.texture
if texture is None:
@@ -33,41 +33,39 @@ def test_grid_none_texture(runtime):
print(f"✗ Grid texture should be None, got: {texture}")
except Exception as e:
print(f"✗ Property access failed: {e}")
-
- # Test 3: Access grid points using ColorLayer (new API)
- # Note: GridPoint no longer has .color - must use ColorLayer system
+
+ # Test 3: Access grid points and set colors
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:
- color_layer.set(x, y, mcrfpy.Color(255, 0, 0, 255)) # Red
+ point.color = mcrfpy.Color(255, 0, 0, 255) # Red
else:
- color_layer.set(x, y, mcrfpy.Color(0, 0, 255, 255)) # Blue
- print("✓ Successfully set grid colors via ColorLayer")
+ point.color = mcrfpy.Color(0, 0, 255, 255) # Blue
+ print("✓ Successfully set grid point colors")
except Exception as e:
print(f"✗ Failed to set grid colors: {e}")
-
+
# Test 4: Add entities to the grid
try:
# Create an entity with its own texture
entity_texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
- entity = mcrfpy.Entity((5, 5), texture=entity_texture, sprite_index=1, grid=grid)
+ entity = mcrfpy.Entity(mcrfpy.Vector(5, 5), entity_texture, 1, grid)
grid.entities.append(entity)
print(f"✓ Added entity to grid, total entities: {len(grid.entities)}")
except Exception as e:
print(f"✗ Failed to add entity: {e}")
-
+
# Test 5: Test grid interaction properties
try:
# Test zoom
grid.zoom = 2.0
print(f"✓ Set zoom to: {grid.zoom}")
-
- # Test center (uses pixel coordinates)
- grid.center = (200, 200)
+
+ # Test center
+ grid.center = mcrfpy.Vector(5, 5)
print(f"✓ Set center to: {grid.center}")
except Exception as e:
print(f"✗ Grid properties failed: {e}")
@@ -88,7 +86,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(pos=(0, 0), size=(800, 600),
+background = mcrfpy.Frame(0, 0, 800, 600,
fill_color=mcrfpy.Color(200, 200, 200),
outline_color=mcrfpy.Color(0, 0, 0),
outline=2.0)
diff --git a/tests/unit/ui_UICollection_issue69_test.py b/tests/unit/ui_UICollection_issue69_test.py
index 44af8d2..3299bcd 100644
--- a/tests/unit/ui_UICollection_issue69_test.py
+++ b/tests/unit/ui_UICollection_issue69_test.py
@@ -11,8 +11,8 @@ def test_UICollection():
ui = mcrfpy.sceneUI("collection_test")
# Add various UI elements
- frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
- caption = mcrfpy.Caption(pos=(120, 10), text="Test")
+ frame = mcrfpy.Frame(10, 10, 100, 100)
+ caption = mcrfpy.Caption(mcrfpy.Vector(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(pos=(250, 10), size=(200, 200),
+ parent_frame = mcrfpy.Frame(250, 10, 200, 200,
fill_color=mcrfpy.Color(200, 200, 200))
- child_caption = mcrfpy.Caption(pos=(10, 10), text="Child")
+ child_caption = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Child")
parent_frame.children.append(child_caption)
ui.append(parent_frame)
diff --git a/tests/unit/validate_screenshot_test.py b/tests/unit/validate_screenshot_test.py
index 7e1a068..e949eda 100644
--- a/tests/unit/validate_screenshot_test.py
+++ b/tests/unit/validate_screenshot_test.py
@@ -18,50 +18,50 @@ def test_screenshot_validation():
print("Creating UI elements...")
# Bright red frame with white outline
- frame1 = mcrfpy.Frame(pos=(50, 50), size=(300, 200),
+ frame1 = mcrfpy.Frame(50, 50, 300, 200,
fill_color=mcrfpy.Color(255, 0, 0), # Bright red
outline_color=mcrfpy.Color(255, 255, 255), # White
outline=5.0)
ui.append(frame1)
print("Added red frame at (50, 50)")
-
+
# Bright green frame
- frame2 = mcrfpy.Frame(pos=(400, 50), size=(300, 200),
+ frame2 = mcrfpy.Frame(400, 50, 300, 200,
fill_color=mcrfpy.Color(0, 255, 0), # Bright green
outline_color=mcrfpy.Color(0, 0, 0), # Black
outline=3.0)
ui.append(frame2)
print("Added green frame at (400, 50)")
-
+
# Blue frame
- frame3 = mcrfpy.Frame(pos=(50, 300), size=(300, 200),
+ frame3 = mcrfpy.Frame(50, 300, 300, 200,
fill_color=mcrfpy.Color(0, 0, 255), # Bright blue
outline_color=mcrfpy.Color(255, 255, 0), # Yellow
outline=4.0)
ui.append(frame3)
print("Added blue frame at (50, 300)")
-
+
# Add text captions
- caption1 = mcrfpy.Caption(pos=(60, 60),
+ caption1 = mcrfpy.Caption(mcrfpy.Vector(60, 60),
text="RED FRAME TEST",
fill_color=mcrfpy.Color(255, 255, 255))
- caption1.font_size = 24
+ caption1.size = 24
frame1.children.append(caption1)
-
- caption2 = mcrfpy.Caption(pos=(410, 60),
+
+ caption2 = mcrfpy.Caption(mcrfpy.Vector(410, 60),
text="GREEN FRAME TEST",
fill_color=mcrfpy.Color(0, 0, 0))
- caption2.font_size = 24
+ caption2.size = 24
ui.append(caption2)
-
- caption3 = mcrfpy.Caption(pos=(60, 310),
+
+ caption3 = mcrfpy.Caption(mcrfpy.Vector(60, 310),
text="BLUE FRAME TEST",
fill_color=mcrfpy.Color(255, 255, 0))
- caption3.font_size = 24
+ caption3.size = 24
ui.append(caption3)
-
+
# White background frame to ensure non-transparent background
- background = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
+ background = mcrfpy.Frame(0, 0, 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
diff --git a/tests/unit/working_timer_test.py b/tests/unit/working_timer_test.py
index a9d96c5..4435014 100644
--- a/tests/unit/working_timer_test.py
+++ b/tests/unit/working_timer_test.py
@@ -11,16 +11,16 @@ mcrfpy.setScene("timer_works")
ui = mcrfpy.sceneUI("timer_works")
# Add visible content
-frame = mcrfpy.Frame(pos=(100, 100), size=(300, 200),
+frame = mcrfpy.Frame(100, 100, 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(pos=(150, 150),
+caption = mcrfpy.Caption(mcrfpy.Vector(150, 150),
text="TIMER TEST SUCCESS",
fill_color=mcrfpy.Color(255, 255, 255))
-caption.font_size = 24
+caption.size = 24
ui.append(caption)
# Timer callback with correct signature
diff --git a/tools/generate_dynamic_docs.py b/tools/generate_dynamic_docs.py
index e70cdc4..c6e4d4d 100644
--- a/tools/generate_dynamic_docs.py
+++ b/tools/generate_dynamic_docs.py
@@ -10,7 +10,6 @@ import inspect
import datetime
import html
import re
-import types
from pathlib import Path
def transform_doc_links(docstring, format='html', base_url=''):
@@ -215,21 +214,11 @@ 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