Add 14-part tutorial Python files (extracted, tested)
Tutorial scripts extracted from documentation, with fixes: - Asset filename: kenney_roguelike.png → kenney_tinydungeon.png - Entity keyword: pos= → grid_pos= (tile coordinates) - Frame.size property → Frame.resize() method - Removed sprite_color (deferred to shader support) All 14 parts pass smoke testing (import + 2-frame run). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e64c5c147f
commit
05f28ef7cd
14 changed files with 13355 additions and 0 deletions
30
docs/tutorials/part_00_setup/part_00_setup.py
Normal file
30
docs/tutorials/part_00_setup/part_00_setup.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""McRogueFace - Part 0: Setting Up McRogueFace
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_00_setup
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_00_setup/part_00_setup.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Create a Scene object - this is the preferred approach
|
||||||
|
scene = mcrfpy.Scene("hello")
|
||||||
|
|
||||||
|
# Create a caption to display text
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(512, 300),
|
||||||
|
text="Hello, Roguelike!"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 32
|
||||||
|
|
||||||
|
# Add the caption to the scene's UI collection
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
# Activate the scene to display it
|
||||||
|
scene.activate()
|
||||||
|
|
||||||
|
# Note: There is no run() function!
|
||||||
|
# The engine is already running - your script is imported by it.
|
||||||
121
docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
Normal file
121
docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
"""McRogueFace - Part 1: The '@' and the Dungeon Grid
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_01_grid_movement
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_AT = 64 # '@' - player character
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor tile
|
||||||
|
|
||||||
|
# Grid dimensions (in tiles)
|
||||||
|
GRID_WIDTH = 20
|
||||||
|
GRID_HEIGHT = 15
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load the texture (sprite sheet)
|
||||||
|
# Parameters: path, sprite_width, sprite_height
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
# The grid displays tiles and contains entities
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(100, 80), # Position on screen (pixels)
|
||||||
|
size=(640, 480), # Display size (pixels)
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT), # Size in tiles
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set the zoom level for better visibility
|
||||||
|
grid.zoom = 2.0
|
||||||
|
|
||||||
|
# Fill the grid with floor tiles
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
|
||||||
|
# Create the player entity at the center of the grid
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(GRID_WIDTH // 2, GRID_HEIGHT // 2), # Grid coordinates, not pixels!
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_AT
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the player to the grid
|
||||||
|
# Option 1: Use the grid parameter in constructor
|
||||||
|
# player = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=SPRITE_AT, grid=grid)
|
||||||
|
|
||||||
|
# Option 2: Append to grid.entities (what we will use)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Add the grid to the scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# Add a title caption
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(100, 20),
|
||||||
|
text="Part 1: Grid Movement - Use Arrow Keys or WASD"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 18
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
# Add a position display
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(100, 50),
|
||||||
|
text=f"Player Position: ({player.x}, {player.y})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input to move the player.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key that was pressed (e.g., "W", "Up", "Space")
|
||||||
|
action: Either "start" (key pressed) or "end" (key released)
|
||||||
|
"""
|
||||||
|
# Only respond to key press, not release
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current player position
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
|
||||||
|
# Calculate new position based on key
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
py -= 1 # Up decreases Y
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
py += 1 # Down increases Y
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
px -= 1 # Left decreases X
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
px += 1 # Right increases X
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update player position
|
||||||
|
player.x = px
|
||||||
|
player.y = py
|
||||||
|
|
||||||
|
# Update the position display
|
||||||
|
pos_display.text = f"Player Position: ({player.x}, {player.y})"
|
||||||
|
|
||||||
|
# Set the key handler on the scene
|
||||||
|
# This is the preferred approach - works on ANY scene, not just the active one
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# Activate the scene
|
||||||
|
scene.activate()
|
||||||
|
|
||||||
|
print("Part 1 loaded! Use WASD or Arrow keys to move.")
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
"""McRogueFace - Part 2: Walls, Floors, and Collision
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_02_tiles_collision
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 30
|
||||||
|
GRID_HEIGHT = 20
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Map Creation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def create_map(grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the grid with walls and floors.
|
||||||
|
|
||||||
|
Creates a simple room with walls around the edges and floor in the middle.
|
||||||
|
"""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
|
||||||
|
# Place walls around the edges
|
||||||
|
if x == 0 or x == GRID_WIDTH - 1 or y == 0 or y == GRID_HEIGHT - 1:
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
else:
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
|
||||||
|
# Add some interior walls to make it interesting
|
||||||
|
# Vertical wall
|
||||||
|
for y in range(5, 15):
|
||||||
|
cell = grid.at(10, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
|
||||||
|
# Horizontal wall
|
||||||
|
for x in range(15, 25):
|
||||||
|
cell = grid.at(x, 10)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
|
||||||
|
# Leave gaps for doorways
|
||||||
|
grid.at(10, 10).tilesprite = SPRITE_FLOOR
|
||||||
|
grid.at(10, 10).walkable = True
|
||||||
|
grid.at(20, 10).tilesprite = SPRITE_FLOOR
|
||||||
|
grid.at(20, 10).walkable = True
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Collision Detection
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||||
|
"""Check if a position is valid for movement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
x: Target X coordinate (in tiles)
|
||||||
|
y: Target Y coordinate (in tiles)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the position is walkable, False otherwise
|
||||||
|
"""
|
||||||
|
# Check grid bounds first
|
||||||
|
if x < 0 or x >= GRID_WIDTH:
|
||||||
|
return False
|
||||||
|
if y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if the tile is walkable
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
return cell.walkable
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(80, 100),
|
||||||
|
size=(720, 480),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.5
|
||||||
|
|
||||||
|
# Build the map
|
||||||
|
create_map(grid)
|
||||||
|
|
||||||
|
# Create the player in the center of the left room
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(5, 10),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(80, 20),
|
||||||
|
text="Part 2: Walls and Collision"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(80, 55),
|
||||||
|
text="WASD or Arrow Keys to move | Walls block movement"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(80, 600),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
status_display = mcrfpy.Caption(
|
||||||
|
pos=(400, 600),
|
||||||
|
text="Status: Ready"
|
||||||
|
)
|
||||||
|
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
status_display.font_size = 16
|
||||||
|
scene.children.append(status_display)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input with collision detection."""
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current position
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
|
||||||
|
# Calculate intended new position
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return # Ignore other keys
|
||||||
|
|
||||||
|
# Check collision before moving
|
||||||
|
if can_move_to(grid, new_x, new_y):
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
status_display.text = "Status: Moved"
|
||||||
|
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
else:
|
||||||
|
status_display.text = "Status: Blocked!"
|
||||||
|
status_display.fill_color = mcrfpy.Color(200, 100, 100)
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 2 loaded! Try walking into walls.")
|
||||||
|
|
@ -0,0 +1,356 @@
|
||||||
|
"""McRogueFace - Part 3: Procedural Dungeon Generation
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_03_dungeon_generation
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 35
|
||||||
|
|
||||||
|
# Room generation parameters
|
||||||
|
ROOM_MIN_SIZE = 6
|
||||||
|
ROOM_MAX_SIZE = 12
|
||||||
|
MAX_ROOMS = 8
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Room Class
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size."""
|
||||||
|
|
||||||
|
def __init__(self, x: int, y: int, width: int, height: int):
|
||||||
|
"""Create a new room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: Left edge X coordinate
|
||||||
|
y: Top edge Y coordinate
|
||||||
|
width: Room width in tiles
|
||||||
|
height: Room height in tiles
|
||||||
|
"""
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self) -> tuple[int, int]:
|
||||||
|
"""Return the center coordinates of the room."""
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self) -> tuple[slice, slice]:
|
||||||
|
"""Return the inner area of the room (excluding walls).
|
||||||
|
|
||||||
|
The inner area is one tile smaller on each side to leave room
|
||||||
|
for walls between adjacent rooms.
|
||||||
|
"""
|
||||||
|
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||||
|
|
||||||
|
def intersects(self, other: "RectangularRoom") -> bool:
|
||||||
|
"""Check if this room overlaps with another room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: Another RectangularRoom to check against
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the rooms overlap, False otherwise
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2 and
|
||||||
|
self.x2 >= other.x1 and
|
||||||
|
self.y1 <= other.y2 and
|
||||||
|
self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def fill_with_walls(grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the entire grid with wall tiles."""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
|
||||||
|
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||||
|
"""Carve out a room by setting its inner tiles to floor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
room: The room to carve
|
||||||
|
"""
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
for y in range(inner_y.start, inner_y.stop):
|
||||||
|
for x in range(inner_x.start, inner_x.stop):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||||
|
"""Carve a horizontal tunnel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
x1: Starting X coordinate
|
||||||
|
x2: Ending X coordinate
|
||||||
|
y: Y coordinate of the tunnel
|
||||||
|
"""
|
||||||
|
start_x = min(x1, x2)
|
||||||
|
end_x = max(x1, x2)
|
||||||
|
for x in range(start_x, end_x + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||||
|
"""Carve a vertical tunnel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
y1: Starting Y coordinate
|
||||||
|
y2: Ending Y coordinate
|
||||||
|
x: X coordinate of the tunnel
|
||||||
|
"""
|
||||||
|
start_y = min(y1, y2)
|
||||||
|
end_y = max(y1, y2)
|
||||||
|
for y in range(start_y, end_y + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_l_tunnel(
|
||||||
|
grid: mcrfpy.Grid,
|
||||||
|
start: tuple[int, int],
|
||||||
|
end: tuple[int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Carve an L-shaped tunnel between two points.
|
||||||
|
|
||||||
|
Randomly chooses to go horizontal-then-vertical or vertical-then-horizontal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
start: Starting (x, y) coordinates
|
||||||
|
end: Ending (x, y) coordinates
|
||||||
|
"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
# Randomly choose whether to go horizontal or vertical first
|
||||||
|
if random.random() < 0.5:
|
||||||
|
# Horizontal first, then vertical
|
||||||
|
carve_tunnel_horizontal(grid, x1, x2, y1)
|
||||||
|
carve_tunnel_vertical(grid, y1, y2, x2)
|
||||||
|
else:
|
||||||
|
# Vertical first, then horizontal
|
||||||
|
carve_tunnel_vertical(grid, y1, y2, x1)
|
||||||
|
carve_tunnel_horizontal(grid, x1, x2, y2)
|
||||||
|
|
||||||
|
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
|
||||||
|
"""Generate a dungeon with rooms and tunnels.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid to generate the dungeon in
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The (x, y) coordinates where the player should start
|
||||||
|
"""
|
||||||
|
# Start with all walls
|
||||||
|
fill_with_walls(grid)
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
# Random room dimensions
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
|
||||||
|
# Random position (leaving 1-tile border)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
# Check for overlap with existing rooms
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue # Skip this room, try another
|
||||||
|
|
||||||
|
# No overlap - carve out the room
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
# Connect to previous room with a tunnel
|
||||||
|
if rooms:
|
||||||
|
# Tunnel from this room's center to the previous room's center
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Return the center of the first room as the player start position
|
||||||
|
if rooms:
|
||||||
|
return rooms[0].center
|
||||||
|
else:
|
||||||
|
# Fallback if no rooms were generated
|
||||||
|
return GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Collision Detection
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||||
|
"""Check if a position is valid for movement."""
|
||||||
|
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
return grid.at(x, y).walkable
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(800, 560),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.0
|
||||||
|
|
||||||
|
# Generate the dungeon and get player start position
|
||||||
|
player_start_x, player_start_y = generate_dungeon(grid)
|
||||||
|
|
||||||
|
# Create the player at the starting position
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(player_start_x, player_start_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(50, 15),
|
||||||
|
text="Part 3: Procedural Dungeon Generation"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(50, 50),
|
||||||
|
text="WASD/Arrows: Move | R: Regenerate dungeon | Escape: Quit"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(50, 660),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
room_display = mcrfpy.Caption(
|
||||||
|
pos=(400, 660),
|
||||||
|
text="Press R to regenerate the dungeon"
|
||||||
|
)
|
||||||
|
room_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
room_display.font_size = 16
|
||||||
|
scene.children.append(room_display)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def regenerate_dungeon() -> None:
|
||||||
|
"""Generate a new dungeon and reposition the player."""
|
||||||
|
new_x, new_y = generate_dungeon(grid)
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
room_display.text = "New dungeon generated!"
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input."""
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
elif key == "R":
|
||||||
|
regenerate_dungeon()
|
||||||
|
return
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if can_move_to(grid, new_x, new_y):
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 3 loaded! Explore the dungeon or press R to regenerate.")
|
||||||
363
docs/tutorials/part_04_fov/part_04_fov.py
Normal file
363
docs/tutorials/part_04_fov/part_04_fov.py
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
"""McRogueFace - Part 4: Field of View
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_04_fov
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_04_fov/part_04_fov.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 35
|
||||||
|
|
||||||
|
# Room generation parameters
|
||||||
|
ROOM_MIN_SIZE = 6
|
||||||
|
ROOM_MAX_SIZE = 12
|
||||||
|
MAX_ROOMS = 8
|
||||||
|
|
||||||
|
# FOV settings
|
||||||
|
FOV_RADIUS = 8
|
||||||
|
|
||||||
|
# Visibility colors (applied as overlays)
|
||||||
|
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) # Fully transparent - show tile
|
||||||
|
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) # Dark blue tint - dimmed
|
||||||
|
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) # Solid black - hidden
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Room Class (from Part 3)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size."""
|
||||||
|
|
||||||
|
def __init__(self, x: int, y: int, width: int, height: int):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self) -> tuple[int, int]:
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self) -> tuple[slice, slice]:
|
||||||
|
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||||
|
|
||||||
|
def intersects(self, other: "RectangularRoom") -> bool:
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2 and
|
||||||
|
self.x2 >= other.x1 and
|
||||||
|
self.y1 <= other.y2 and
|
||||||
|
self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Exploration Tracking
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Track which tiles have been discovered (seen at least once)
|
||||||
|
explored: list[list[bool]] = []
|
||||||
|
|
||||||
|
def init_explored() -> None:
|
||||||
|
"""Initialize the explored array to all False."""
|
||||||
|
global explored
|
||||||
|
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||||
|
|
||||||
|
def mark_explored(x: int, y: int) -> None:
|
||||||
|
"""Mark a tile as explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
explored[y][x] = True
|
||||||
|
|
||||||
|
def is_explored(x: int, y: int) -> bool:
|
||||||
|
"""Check if a tile has been explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
return explored[y][x]
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation (from Part 3, with transparent property)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def fill_with_walls(grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the entire grid with wall tiles."""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False # Walls block line of sight
|
||||||
|
|
||||||
|
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||||
|
"""Carve out a room by setting its inner tiles to floor."""
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
for y in range(inner_y.start, inner_y.stop):
|
||||||
|
for x in range(inner_x.start, inner_x.stop):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True # Floors allow line of sight
|
||||||
|
|
||||||
|
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||||
|
"""Carve a horizontal tunnel."""
|
||||||
|
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||||
|
"""Carve a vertical tunnel."""
|
||||||
|
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_l_tunnel(
|
||||||
|
grid: mcrfpy.Grid,
|
||||||
|
start: tuple[int, int],
|
||||||
|
end: tuple[int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Carve an L-shaped tunnel between two points."""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
carve_tunnel_horizontal(grid, x1, x2, y1)
|
||||||
|
carve_tunnel_vertical(grid, y1, y2, x2)
|
||||||
|
else:
|
||||||
|
carve_tunnel_vertical(grid, y1, y2, x1)
|
||||||
|
carve_tunnel_horizontal(grid, x1, x2, y2)
|
||||||
|
|
||||||
|
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
|
||||||
|
"""Generate a dungeon with rooms and tunnels."""
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored() # Reset exploration when generating new dungeon
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
return rooms[0].center
|
||||||
|
return GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Field of View
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_fov(grid: mcrfpy.Grid, fov_layer, player_x: int, player_y: int) -> None:
|
||||||
|
"""Update the field of view visualization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
fov_layer: The ColorLayer for FOV visualization
|
||||||
|
player_x: Player's X position
|
||||||
|
player_y: Player's Y position
|
||||||
|
"""
|
||||||
|
# Compute FOV from player position
|
||||||
|
grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||||
|
|
||||||
|
# Update each tile's visibility
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
if grid.is_in_fov(x, y):
|
||||||
|
# Currently visible - mark as explored and show clearly
|
||||||
|
mark_explored(x, y)
|
||||||
|
fov_layer.set(x, y, COLOR_VISIBLE)
|
||||||
|
elif is_explored(x, y):
|
||||||
|
# Previously seen but not currently visible - show dimmed
|
||||||
|
fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||||
|
else:
|
||||||
|
# Never seen - hide completely
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Collision Detection
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||||
|
"""Check if a position is valid for movement."""
|
||||||
|
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
return grid.at(x, y).walkable
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(800, 560),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.0
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
player_start_x, player_start_y = generate_dungeon(grid)
|
||||||
|
|
||||||
|
# Add a color layer for FOV visualization (below entities)
|
||||||
|
fov_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
|
# Initialize the FOV layer to all black (unknown)
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Create the player
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(player_start_x, player_start_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Calculate initial FOV
|
||||||
|
update_fov(grid, fov_layer, player_start_x, player_start_y)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(50, 15),
|
||||||
|
text="Part 4: Field of View"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(50, 50),
|
||||||
|
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(50, 660),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
fov_display = mcrfpy.Caption(
|
||||||
|
pos=(400, 660),
|
||||||
|
text=f"FOV Radius: {FOV_RADIUS}"
|
||||||
|
)
|
||||||
|
fov_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
fov_display.font_size = 16
|
||||||
|
scene.children.append(fov_display)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def regenerate_dungeon() -> None:
|
||||||
|
"""Generate a new dungeon and reposition the player."""
|
||||||
|
new_x, new_y = generate_dungeon(grid)
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
|
||||||
|
# Reset FOV layer to unknown
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Calculate new FOV
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input."""
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
elif key == "R":
|
||||||
|
regenerate_dungeon()
|
||||||
|
return
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if can_move_to(grid, new_x, new_y):
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
|
||||||
|
# Update FOV after movement
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 4 loaded! Explore the dungeon - watch the fog of war!")
|
||||||
685
docs/tutorials/part_05_enemies/part_05_enemies.py
Normal file
685
docs/tutorials/part_05_enemies/part_05_enemies.py
Normal file
|
|
@ -0,0 +1,685 @@
|
||||||
|
"""McRogueFace - Part 5: Placing Enemies
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_05_enemies
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_05_enemies/part_05_enemies.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
|
||||||
|
# Enemy sprites (lowercase letters in CP437)
|
||||||
|
SPRITE_GOBLIN = 103 # 'g'
|
||||||
|
SPRITE_ORC = 111 # 'o'
|
||||||
|
SPRITE_TROLL = 116 # 't'
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 35
|
||||||
|
|
||||||
|
# Room generation parameters
|
||||||
|
ROOM_MIN_SIZE = 6
|
||||||
|
ROOM_MAX_SIZE = 12
|
||||||
|
MAX_ROOMS = 8
|
||||||
|
|
||||||
|
# Enemy spawn parameters
|
||||||
|
MAX_ENEMIES_PER_ROOM = 3
|
||||||
|
|
||||||
|
# FOV settings
|
||||||
|
FOV_RADIUS = 8
|
||||||
|
|
||||||
|
# Visibility colors
|
||||||
|
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
|
||||||
|
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
|
||||||
|
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Enemy Data
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Enemy templates - stats for each enemy type
|
||||||
|
ENEMY_TEMPLATES = {
|
||||||
|
"goblin": {
|
||||||
|
"sprite": SPRITE_GOBLIN,
|
||||||
|
"hp": 6,
|
||||||
|
"max_hp": 6,
|
||||||
|
"attack": 3,
|
||||||
|
"defense": 0,
|
||||||
|
"color": mcrfpy.Color(100, 200, 100) # Greenish
|
||||||
|
},
|
||||||
|
"orc": {
|
||||||
|
"sprite": SPRITE_ORC,
|
||||||
|
"hp": 10,
|
||||||
|
"max_hp": 10,
|
||||||
|
"attack": 4,
|
||||||
|
"defense": 1,
|
||||||
|
"color": mcrfpy.Color(100, 150, 100) # Darker green
|
||||||
|
},
|
||||||
|
"troll": {
|
||||||
|
"sprite": SPRITE_TROLL,
|
||||||
|
"hp": 16,
|
||||||
|
"max_hp": 16,
|
||||||
|
"attack": 6,
|
||||||
|
"defense": 2,
|
||||||
|
"color": mcrfpy.Color(50, 150, 50) # Dark green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global storage for entity data
|
||||||
|
# Maps entity objects to their data dictionaries
|
||||||
|
entity_data: dict = {}
|
||||||
|
|
||||||
|
# Global references
|
||||||
|
player = None
|
||||||
|
grid = None
|
||||||
|
fov_layer = None
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Room Class (from Part 3)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size."""
|
||||||
|
|
||||||
|
def __init__(self, x: int, y: int, width: int, height: int):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self) -> tuple[int, int]:
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self) -> tuple[slice, slice]:
|
||||||
|
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||||
|
|
||||||
|
def intersects(self, other: "RectangularRoom") -> bool:
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2 and
|
||||||
|
self.x2 >= other.x1 and
|
||||||
|
self.y1 <= other.y2 and
|
||||||
|
self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Exploration Tracking (from Part 4)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
explored: list[list[bool]] = []
|
||||||
|
|
||||||
|
def init_explored() -> None:
|
||||||
|
"""Initialize the explored array to all False."""
|
||||||
|
global explored
|
||||||
|
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||||
|
|
||||||
|
def mark_explored(x: int, y: int) -> None:
|
||||||
|
"""Mark a tile as explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
explored[y][x] = True
|
||||||
|
|
||||||
|
def is_explored(x: int, y: int) -> bool:
|
||||||
|
"""Check if a tile has been explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
return explored[y][x]
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation (from Part 4)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the entire grid with wall tiles."""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
|
||||||
|
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||||
|
"""Carve out a room by setting its inner tiles to floor."""
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
for y in range(inner_y.start, inner_y.stop):
|
||||||
|
for x in range(inner_x.start, inner_x.stop):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||||
|
"""Carve a horizontal tunnel."""
|
||||||
|
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||||
|
"""Carve a vertical tunnel."""
|
||||||
|
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_l_tunnel(
|
||||||
|
target_grid: mcrfpy.Grid,
|
||||||
|
start: tuple[int, int],
|
||||||
|
end: tuple[int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Carve an L-shaped tunnel between two points."""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
carve_tunnel_horizontal(target_grid, x1, x2, y1)
|
||||||
|
carve_tunnel_vertical(target_grid, y1, y2, x2)
|
||||||
|
else:
|
||||||
|
carve_tunnel_vertical(target_grid, y1, y2, x1)
|
||||||
|
carve_tunnel_horizontal(target_grid, x1, x2, y2)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Enemy Management
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, texture: mcrfpy.Texture) -> mcrfpy.Entity:
|
||||||
|
"""Spawn an enemy at the given position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_grid: The game grid
|
||||||
|
x: X position in tiles
|
||||||
|
y: Y position in tiles
|
||||||
|
enemy_type: Type of enemy ("goblin", "orc", or "troll")
|
||||||
|
texture: The texture to use for the sprite
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created enemy Entity
|
||||||
|
"""
|
||||||
|
template = ENEMY_TEMPLATES[enemy_type]
|
||||||
|
|
||||||
|
enemy = mcrfpy.Entity(
|
||||||
|
grid_pos=(x, y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=template["sprite"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Start hidden until player sees them
|
||||||
|
enemy.visible = False
|
||||||
|
|
||||||
|
# Add to grid
|
||||||
|
target_grid.entities.append(enemy)
|
||||||
|
|
||||||
|
# Store enemy data
|
||||||
|
entity_data[enemy] = {
|
||||||
|
"type": enemy_type,
|
||||||
|
"name": enemy_type.capitalize(),
|
||||||
|
"hp": template["hp"],
|
||||||
|
"max_hp": template["max_hp"],
|
||||||
|
"attack": template["attack"],
|
||||||
|
"defense": template["defense"],
|
||||||
|
"is_player": False
|
||||||
|
}
|
||||||
|
|
||||||
|
return enemy
|
||||||
|
|
||||||
|
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, texture: mcrfpy.Texture) -> None:
|
||||||
|
"""Spawn random enemies in a room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_grid: The game grid
|
||||||
|
room: The room to spawn enemies in
|
||||||
|
texture: The texture to use for sprites
|
||||||
|
"""
|
||||||
|
# Random number of enemies (0 to MAX_ENEMIES_PER_ROOM)
|
||||||
|
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
|
||||||
|
|
||||||
|
for _ in range(num_enemies):
|
||||||
|
# Random position within the room's inner area
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
x = random.randint(inner_x.start, inner_x.stop - 1)
|
||||||
|
y = random.randint(inner_y.start, inner_y.stop - 1)
|
||||||
|
|
||||||
|
# Check if position is already occupied
|
||||||
|
if get_blocking_entity_at(target_grid, x, y) is not None:
|
||||||
|
continue # Skip this spawn attempt
|
||||||
|
|
||||||
|
# Choose enemy type based on weighted random
|
||||||
|
roll = random.random()
|
||||||
|
if roll < 0.6:
|
||||||
|
enemy_type = "goblin" # 60% chance
|
||||||
|
elif roll < 0.9:
|
||||||
|
enemy_type = "orc" # 30% chance
|
||||||
|
else:
|
||||||
|
enemy_type = "troll" # 10% chance
|
||||||
|
|
||||||
|
spawn_enemy(target_grid, x, y, enemy_type, texture)
|
||||||
|
|
||||||
|
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> mcrfpy.Entity | None:
|
||||||
|
"""Get any entity that blocks movement at the given position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_grid: The game grid
|
||||||
|
x: X position to check
|
||||||
|
y: Y position to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The blocking entity, or None if no entity blocks this position
|
||||||
|
"""
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if int(entity.x) == x and int(entity.y) == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Remove all enemies from the grid."""
|
||||||
|
global entity_data
|
||||||
|
|
||||||
|
# Get list of enemies to remove (not the player)
|
||||||
|
enemies_to_remove = []
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if entity in entity_data and not entity_data[entity].get("is_player", False):
|
||||||
|
enemies_to_remove.append(entity)
|
||||||
|
|
||||||
|
# Remove from grid and entity_data
|
||||||
|
for enemy in enemies_to_remove:
|
||||||
|
# Find and remove from grid.entities
|
||||||
|
for i, e in enumerate(target_grid.entities):
|
||||||
|
if e == enemy:
|
||||||
|
target_grid.entities.remove(i)
|
||||||
|
break
|
||||||
|
# Remove from entity_data
|
||||||
|
if enemy in entity_data:
|
||||||
|
del entity_data[enemy]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Entity Visibility
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Update visibility of all entities based on FOV.
|
||||||
|
|
||||||
|
Entities outside the player's field of view are hidden.
|
||||||
|
"""
|
||||||
|
global player
|
||||||
|
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
# Player is always visible
|
||||||
|
if entity == player:
|
||||||
|
entity.visible = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Other entities are only visible if in FOV
|
||||||
|
ex, ey = int(entity.x), int(entity.y)
|
||||||
|
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Field of View (from Part 4)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||||
|
"""Update the field of view visualization."""
|
||||||
|
# Compute FOV from player position
|
||||||
|
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||||
|
|
||||||
|
# Update each tile's visibility
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
if target_grid.is_in_fov(x, y):
|
||||||
|
mark_explored(x, y)
|
||||||
|
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||||
|
elif is_explored(x, y):
|
||||||
|
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||||
|
else:
|
||||||
|
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Update entity visibility
|
||||||
|
update_entity_visibility(target_grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Collision Detection
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||||
|
"""Check if a position is valid for movement.
|
||||||
|
|
||||||
|
A position is valid if:
|
||||||
|
1. It is within grid bounds
|
||||||
|
2. The tile is walkable
|
||||||
|
3. No entity is blocking it
|
||||||
|
"""
|
||||||
|
# Check bounds
|
||||||
|
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check tile walkability
|
||||||
|
if not target_grid.at(x, y).walkable:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for blocking entities
|
||||||
|
if get_blocking_entity_at(target_grid, x, y) is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation with Enemies
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def generate_dungeon(target_grid: mcrfpy.Grid, texture: mcrfpy.Texture) -> tuple[int, int]:
|
||||||
|
"""Generate a dungeon with rooms, tunnels, and enemies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_grid: The game grid
|
||||||
|
texture: The texture for entity sprites
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The (x, y) coordinates where the player should start
|
||||||
|
"""
|
||||||
|
# Clear any existing enemies
|
||||||
|
clear_enemies(target_grid)
|
||||||
|
|
||||||
|
# Fill with walls
|
||||||
|
fill_with_walls(target_grid)
|
||||||
|
init_explored()
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(target_grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(target_grid, new_room.center, rooms[-1].center)
|
||||||
|
# Spawn enemies in all rooms except the first (player starting room)
|
||||||
|
spawn_enemies_in_room(target_grid, new_room, texture)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
return rooms[0].center
|
||||||
|
return GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(800, 560),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.0
|
||||||
|
|
||||||
|
# Generate the dungeon (without player first to get starting position)
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored()
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Get player starting position
|
||||||
|
if rooms:
|
||||||
|
player_start_x, player_start_y = rooms[0].center
|
||||||
|
else:
|
||||||
|
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# Add FOV layer
|
||||||
|
fov_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Create the player
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(player_start_x, player_start_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Store player data
|
||||||
|
entity_data[player] = {
|
||||||
|
"type": "player",
|
||||||
|
"name": "Player",
|
||||||
|
"hp": 30,
|
||||||
|
"max_hp": 30,
|
||||||
|
"attack": 5,
|
||||||
|
"defense": 2,
|
||||||
|
"is_player": True
|
||||||
|
}
|
||||||
|
|
||||||
|
# Now spawn enemies in rooms (except the first one)
|
||||||
|
for i, room in enumerate(rooms):
|
||||||
|
if i == 0:
|
||||||
|
continue # Skip player's starting room
|
||||||
|
spawn_enemies_in_room(grid, room, texture)
|
||||||
|
|
||||||
|
# Calculate initial FOV
|
||||||
|
update_fov(grid, fov_layer, player_start_x, player_start_y)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(50, 15),
|
||||||
|
text="Part 5: Placing Enemies"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(50, 50),
|
||||||
|
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(50, 660),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
status_display = mcrfpy.Caption(
|
||||||
|
pos=(400, 660),
|
||||||
|
text="Explore the dungeon..."
|
||||||
|
)
|
||||||
|
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
status_display.font_size = 16
|
||||||
|
scene.children.append(status_display)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def regenerate_dungeon() -> None:
|
||||||
|
"""Generate a new dungeon and reposition the player."""
|
||||||
|
global player, grid, fov_layer, rooms
|
||||||
|
|
||||||
|
# Clear enemies
|
||||||
|
clear_enemies(grid)
|
||||||
|
|
||||||
|
# Regenerate dungeon structure
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored()
|
||||||
|
|
||||||
|
rooms = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Reposition player
|
||||||
|
if rooms:
|
||||||
|
new_x, new_y = rooms[0].center
|
||||||
|
else:
|
||||||
|
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
|
||||||
|
# Spawn new enemies
|
||||||
|
for i, room in enumerate(rooms):
|
||||||
|
if i == 0:
|
||||||
|
continue
|
||||||
|
spawn_enemies_in_room(grid, room, texture)
|
||||||
|
|
||||||
|
# Reset FOV layer
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Update FOV
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
status_display.text = "New dungeon generated!"
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input."""
|
||||||
|
global player, grid, fov_layer
|
||||||
|
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
elif key == "R":
|
||||||
|
regenerate_dungeon()
|
||||||
|
return
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for blocking entity (potential combat target)
|
||||||
|
blocker = get_blocking_entity_at(grid, new_x, new_y)
|
||||||
|
if blocker is not None and blocker != player:
|
||||||
|
# For now, just report that we bumped into an enemy
|
||||||
|
if blocker in entity_data:
|
||||||
|
enemy_name = entity_data[blocker]["name"]
|
||||||
|
status_display.text = f"A {enemy_name} blocks your path!"
|
||||||
|
status_display.fill_color = mcrfpy.Color(200, 150, 100)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if we can move
|
||||||
|
if can_move_to(grid, new_x, new_y):
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
status_display.text = "Exploring..."
|
||||||
|
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
|
||||||
|
# Update FOV after movement
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 5 loaded! Enemies lurk in the dungeon...")
|
||||||
940
docs/tutorials/part_06_combat/part_06_combat.py
Normal file
940
docs/tutorials/part_06_combat/part_06_combat.py
Normal file
|
|
@ -0,0 +1,940 @@
|
||||||
|
"""McRogueFace - Part 6: Combat System
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_06_combat
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_06_combat/part_06_combat.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
SPRITE_CORPSE = 37 # '%' - remains
|
||||||
|
|
||||||
|
# Enemy sprites
|
||||||
|
SPRITE_GOBLIN = 103 # 'g'
|
||||||
|
SPRITE_ORC = 111 # 'o'
|
||||||
|
SPRITE_TROLL = 116 # 't'
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 35
|
||||||
|
|
||||||
|
# Room generation parameters
|
||||||
|
ROOM_MIN_SIZE = 6
|
||||||
|
ROOM_MAX_SIZE = 12
|
||||||
|
MAX_ROOMS = 8
|
||||||
|
|
||||||
|
# Enemy spawn parameters
|
||||||
|
MAX_ENEMIES_PER_ROOM = 3
|
||||||
|
|
||||||
|
# FOV settings
|
||||||
|
FOV_RADIUS = 8
|
||||||
|
|
||||||
|
# Visibility colors
|
||||||
|
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
|
||||||
|
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
|
||||||
|
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
|
||||||
|
|
||||||
|
# Message log settings
|
||||||
|
MAX_MESSAGES = 5
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Fighter Component
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Fighter:
|
||||||
|
"""Combat stats for an entity."""
|
||||||
|
hp: int
|
||||||
|
max_hp: int
|
||||||
|
attack: int
|
||||||
|
defense: int
|
||||||
|
name: str
|
||||||
|
is_player: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_alive(self) -> bool:
|
||||||
|
"""Check if this fighter is still alive."""
|
||||||
|
return self.hp > 0
|
||||||
|
|
||||||
|
def take_damage(self, amount: int) -> int:
|
||||||
|
"""Apply damage and return actual damage taken."""
|
||||||
|
actual_damage = min(self.hp, amount)
|
||||||
|
self.hp -= actual_damage
|
||||||
|
return actual_damage
|
||||||
|
|
||||||
|
def heal(self, amount: int) -> int:
|
||||||
|
"""Heal and return actual amount healed."""
|
||||||
|
actual_heal = min(self.max_hp - self.hp, amount)
|
||||||
|
self.hp += actual_heal
|
||||||
|
return actual_heal
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Enemy Templates
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
ENEMY_TEMPLATES = {
|
||||||
|
"goblin": {
|
||||||
|
"sprite": SPRITE_GOBLIN,
|
||||||
|
"hp": 6,
|
||||||
|
"attack": 3,
|
||||||
|
"defense": 0,
|
||||||
|
"color": mcrfpy.Color(100, 200, 100)
|
||||||
|
},
|
||||||
|
"orc": {
|
||||||
|
"sprite": SPRITE_ORC,
|
||||||
|
"hp": 10,
|
||||||
|
"attack": 4,
|
||||||
|
"defense": 1,
|
||||||
|
"color": mcrfpy.Color(100, 150, 100)
|
||||||
|
},
|
||||||
|
"troll": {
|
||||||
|
"sprite": SPRITE_TROLL,
|
||||||
|
"hp": 16,
|
||||||
|
"attack": 6,
|
||||||
|
"defense": 2,
|
||||||
|
"color": mcrfpy.Color(50, 150, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Global State
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Entity data storage
|
||||||
|
entity_data: dict[mcrfpy.Entity, Fighter] = {}
|
||||||
|
|
||||||
|
# Global references
|
||||||
|
player: Optional[mcrfpy.Entity] = None
|
||||||
|
grid: Optional[mcrfpy.Grid] = None
|
||||||
|
fov_layer = None
|
||||||
|
texture: Optional[mcrfpy.Texture] = None
|
||||||
|
|
||||||
|
# Game state
|
||||||
|
game_over: bool = False
|
||||||
|
|
||||||
|
# Message log
|
||||||
|
messages: list[tuple[str, mcrfpy.Color]] = []
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Room Class
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size."""
|
||||||
|
|
||||||
|
def __init__(self, x: int, y: int, width: int, height: int):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self) -> tuple[int, int]:
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self) -> tuple[slice, slice]:
|
||||||
|
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||||
|
|
||||||
|
def intersects(self, other: "RectangularRoom") -> bool:
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2 and
|
||||||
|
self.x2 >= other.x1 and
|
||||||
|
self.y1 <= other.y2 and
|
||||||
|
self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Exploration Tracking
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
explored: list[list[bool]] = []
|
||||||
|
|
||||||
|
def init_explored() -> None:
|
||||||
|
"""Initialize the explored array to all False."""
|
||||||
|
global explored
|
||||||
|
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||||
|
|
||||||
|
def mark_explored(x: int, y: int) -> None:
|
||||||
|
"""Mark a tile as explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
explored[y][x] = True
|
||||||
|
|
||||||
|
def is_explored(x: int, y: int) -> bool:
|
||||||
|
"""Check if a tile has been explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
return explored[y][x]
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Message Log
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def add_message(text: str, color: mcrfpy.Color = None) -> None:
|
||||||
|
"""Add a message to the log.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The message text
|
||||||
|
color: Optional color (defaults to white)
|
||||||
|
"""
|
||||||
|
if color is None:
|
||||||
|
color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
|
messages.append((text, color))
|
||||||
|
|
||||||
|
# Keep only the most recent messages
|
||||||
|
while len(messages) > MAX_MESSAGES:
|
||||||
|
messages.pop(0)
|
||||||
|
|
||||||
|
# Update the message display
|
||||||
|
update_message_display()
|
||||||
|
|
||||||
|
def update_message_display() -> None:
|
||||||
|
"""Update the message log UI."""
|
||||||
|
if message_log_caption is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Combine messages into a single string
|
||||||
|
lines = []
|
||||||
|
for text, color in messages:
|
||||||
|
lines.append(text)
|
||||||
|
|
||||||
|
message_log_caption.text = "\n".join(lines)
|
||||||
|
|
||||||
|
def clear_messages() -> None:
|
||||||
|
"""Clear all messages."""
|
||||||
|
global messages
|
||||||
|
messages = []
|
||||||
|
update_message_display()
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the entire grid with wall tiles."""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
|
||||||
|
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||||
|
"""Carve out a room by setting its inner tiles to floor."""
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
for y in range(inner_y.start, inner_y.stop):
|
||||||
|
for x in range(inner_x.start, inner_x.stop):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||||
|
"""Carve a horizontal tunnel."""
|
||||||
|
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||||
|
"""Carve a vertical tunnel."""
|
||||||
|
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_l_tunnel(
|
||||||
|
target_grid: mcrfpy.Grid,
|
||||||
|
start: tuple[int, int],
|
||||||
|
end: tuple[int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Carve an L-shaped tunnel between two points."""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
carve_tunnel_horizontal(target_grid, x1, x2, y1)
|
||||||
|
carve_tunnel_vertical(target_grid, y1, y2, x2)
|
||||||
|
else:
|
||||||
|
carve_tunnel_vertical(target_grid, y1, y2, x1)
|
||||||
|
carve_tunnel_horizontal(target_grid, x1, x2, y2)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Entity Management
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
|
||||||
|
"""Spawn an enemy at the given position."""
|
||||||
|
template = ENEMY_TEMPLATES[enemy_type]
|
||||||
|
|
||||||
|
enemy = mcrfpy.Entity(
|
||||||
|
grid_pos=(x, y),
|
||||||
|
texture=tex,
|
||||||
|
sprite_index=template["sprite"]
|
||||||
|
)
|
||||||
|
|
||||||
|
enemy.visible = False
|
||||||
|
|
||||||
|
target_grid.entities.append(enemy)
|
||||||
|
|
||||||
|
# Create Fighter component for this enemy
|
||||||
|
entity_data[enemy] = Fighter(
|
||||||
|
hp=template["hp"],
|
||||||
|
max_hp=template["hp"],
|
||||||
|
attack=template["attack"],
|
||||||
|
defense=template["defense"],
|
||||||
|
name=enemy_type.capitalize(),
|
||||||
|
is_player=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return enemy
|
||||||
|
|
||||||
|
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None:
|
||||||
|
"""Spawn random enemies in a room."""
|
||||||
|
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
|
||||||
|
|
||||||
|
for _ in range(num_enemies):
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
x = random.randint(inner_x.start, inner_x.stop - 1)
|
||||||
|
y = random.randint(inner_y.start, inner_y.stop - 1)
|
||||||
|
|
||||||
|
if get_entity_at(target_grid, x, y) is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
roll = random.random()
|
||||||
|
if roll < 0.6:
|
||||||
|
enemy_type = "goblin"
|
||||||
|
elif roll < 0.9:
|
||||||
|
enemy_type = "orc"
|
||||||
|
else:
|
||||||
|
enemy_type = "troll"
|
||||||
|
|
||||||
|
spawn_enemy(target_grid, x, y, enemy_type, tex)
|
||||||
|
|
||||||
|
def get_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
|
||||||
|
"""Get any entity at the given position."""
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if int(entity.x) == x and int(entity.y) == y:
|
||||||
|
# Check if this entity is alive (or is a non-Fighter entity)
|
||||||
|
if entity in entity_data:
|
||||||
|
if entity_data[entity].is_alive:
|
||||||
|
return entity
|
||||||
|
else:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
|
||||||
|
"""Get any living entity that blocks movement at the given position."""
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if entity == exclude:
|
||||||
|
continue
|
||||||
|
if int(entity.x) == x and int(entity.y) == y:
|
||||||
|
if entity in entity_data and entity_data[entity].is_alive:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||||
|
"""Remove an entity from the grid and data storage."""
|
||||||
|
# Find and remove from grid
|
||||||
|
for i, e in enumerate(target_grid.entities):
|
||||||
|
if e == entity:
|
||||||
|
target_grid.entities.remove(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Remove from entity data
|
||||||
|
if entity in entity_data:
|
||||||
|
del entity_data[entity]
|
||||||
|
|
||||||
|
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Remove all enemies from the grid."""
|
||||||
|
enemies_to_remove = []
|
||||||
|
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if entity in entity_data and not entity_data[entity].is_player:
|
||||||
|
enemies_to_remove.append(entity)
|
||||||
|
|
||||||
|
for enemy in enemies_to_remove:
|
||||||
|
remove_entity(target_grid, enemy)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Combat System
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
|
||||||
|
"""Calculate damage dealt from attacker to defender.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attacker: The attacking Fighter
|
||||||
|
defender: The defending Fighter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The amount of damage to deal (minimum 0)
|
||||||
|
"""
|
||||||
|
damage = max(0, attacker.attack - defender.defense)
|
||||||
|
return damage
|
||||||
|
|
||||||
|
def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
|
||||||
|
"""Execute an attack from one entity to another.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attacker_entity: The entity performing the attack
|
||||||
|
defender_entity: The entity being attacked
|
||||||
|
"""
|
||||||
|
global game_over
|
||||||
|
|
||||||
|
attacker = entity_data.get(attacker_entity)
|
||||||
|
defender = entity_data.get(defender_entity)
|
||||||
|
|
||||||
|
if attacker is None or defender is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate and apply damage
|
||||||
|
damage = calculate_damage(attacker, defender)
|
||||||
|
defender.take_damage(damage)
|
||||||
|
|
||||||
|
# Generate combat message
|
||||||
|
if damage > 0:
|
||||||
|
if attacker.is_player:
|
||||||
|
add_message(
|
||||||
|
f"You hit the {defender.name} for {damage} damage!",
|
||||||
|
mcrfpy.Color(200, 200, 200)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
add_message(
|
||||||
|
f"The {attacker.name} hits you for {damage} damage!",
|
||||||
|
mcrfpy.Color(255, 150, 150)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if attacker.is_player:
|
||||||
|
add_message(
|
||||||
|
f"You hit the {defender.name} but deal no damage.",
|
||||||
|
mcrfpy.Color(150, 150, 150)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
add_message(
|
||||||
|
f"The {attacker.name} hits you but deals no damage.",
|
||||||
|
mcrfpy.Color(150, 150, 200)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for death
|
||||||
|
if not defender.is_alive:
|
||||||
|
handle_death(defender_entity, defender)
|
||||||
|
|
||||||
|
def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
|
||||||
|
"""Handle the death of an entity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity: The entity that died
|
||||||
|
fighter: The Fighter component of the dead entity
|
||||||
|
"""
|
||||||
|
global game_over, grid
|
||||||
|
|
||||||
|
if fighter.is_player:
|
||||||
|
# Player death
|
||||||
|
add_message("You have died!", mcrfpy.Color(255, 50, 50))
|
||||||
|
add_message("Press R to restart or Escape to quit.", mcrfpy.Color(200, 200, 200))
|
||||||
|
game_over = True
|
||||||
|
|
||||||
|
# Change player sprite to corpse
|
||||||
|
entity.sprite_index = SPRITE_CORPSE
|
||||||
|
else:
|
||||||
|
# Enemy death
|
||||||
|
add_message(f"The {fighter.name} dies!", mcrfpy.Color(100, 255, 100))
|
||||||
|
|
||||||
|
# Replace with corpse
|
||||||
|
entity.sprite_index = SPRITE_CORPSE
|
||||||
|
|
||||||
|
# Mark as dead (hp is already 0)
|
||||||
|
# Remove blocking but keep visual corpse
|
||||||
|
# Actually remove the entity and its data
|
||||||
|
remove_entity(grid, entity)
|
||||||
|
|
||||||
|
# Update HP display
|
||||||
|
update_hp_display()
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Field of View
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Update visibility of all entities based on FOV."""
|
||||||
|
global player
|
||||||
|
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if entity == player:
|
||||||
|
entity.visible = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
ex, ey = int(entity.x), int(entity.y)
|
||||||
|
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||||
|
|
||||||
|
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||||
|
"""Update the field of view visualization."""
|
||||||
|
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||||
|
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
if target_grid.is_in_fov(x, y):
|
||||||
|
mark_explored(x, y)
|
||||||
|
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||||
|
elif is_explored(x, y):
|
||||||
|
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||||
|
else:
|
||||||
|
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
update_entity_visibility(target_grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Movement and Actions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool:
|
||||||
|
"""Check if a position is valid for movement."""
|
||||||
|
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not target_grid.at(x, y).walkable:
|
||||||
|
return False
|
||||||
|
|
||||||
|
blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover)
|
||||||
|
if blocker is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def try_move_or_attack(dx: int, dy: int) -> None:
|
||||||
|
"""Attempt to move the player or attack if blocked by enemy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dx: Change in X position (-1, 0, or 1)
|
||||||
|
dy: Change in Y position (-1, 0, or 1)
|
||||||
|
"""
|
||||||
|
global player, grid, fov_layer, game_over
|
||||||
|
|
||||||
|
if game_over:
|
||||||
|
return
|
||||||
|
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
target_x = px + dx
|
||||||
|
target_y = py + dy
|
||||||
|
|
||||||
|
# Check bounds
|
||||||
|
if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for blocking entity
|
||||||
|
blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
|
||||||
|
|
||||||
|
if blocker is not None:
|
||||||
|
# Attack the blocking entity
|
||||||
|
perform_attack(player, blocker)
|
||||||
|
# After player attacks, enemies take their turn
|
||||||
|
enemy_turn()
|
||||||
|
elif grid.at(target_x, target_y).walkable:
|
||||||
|
# Move to the empty tile
|
||||||
|
player.x = target_x
|
||||||
|
player.y = target_y
|
||||||
|
pos_display.text = f"Position: ({target_x}, {target_y})"
|
||||||
|
|
||||||
|
# Update FOV after movement
|
||||||
|
update_fov(grid, fov_layer, target_x, target_y)
|
||||||
|
|
||||||
|
# Enemies take their turn after player moves
|
||||||
|
enemy_turn()
|
||||||
|
|
||||||
|
# Update HP display
|
||||||
|
update_hp_display()
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Enemy AI
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def enemy_turn() -> None:
|
||||||
|
"""Execute enemy actions."""
|
||||||
|
global player, grid, game_over
|
||||||
|
|
||||||
|
if game_over:
|
||||||
|
return
|
||||||
|
|
||||||
|
player_x, player_y = int(player.x), int(player.y)
|
||||||
|
|
||||||
|
# Collect enemies that can act
|
||||||
|
enemies = []
|
||||||
|
for entity in grid.entities:
|
||||||
|
if entity == player:
|
||||||
|
continue
|
||||||
|
if entity in entity_data and entity_data[entity].is_alive:
|
||||||
|
enemies.append(entity)
|
||||||
|
|
||||||
|
for enemy in enemies:
|
||||||
|
fighter = entity_data.get(enemy)
|
||||||
|
if fighter is None or not fighter.is_alive:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ex, ey = int(enemy.x), int(enemy.y)
|
||||||
|
|
||||||
|
# Only act if in player's FOV (aware of player)
|
||||||
|
if not grid.is_in_fov(ex, ey):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if adjacent to player
|
||||||
|
dx = player_x - ex
|
||||||
|
dy = player_y - ey
|
||||||
|
|
||||||
|
if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
|
||||||
|
# Adjacent - attack!
|
||||||
|
perform_attack(enemy, player)
|
||||||
|
else:
|
||||||
|
# Not adjacent - try to move toward player
|
||||||
|
move_toward_player(enemy, ex, ey, player_x, player_y)
|
||||||
|
|
||||||
|
def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None:
|
||||||
|
"""Move an enemy one step toward the player.
|
||||||
|
|
||||||
|
Uses simple greedy movement - not true pathfinding.
|
||||||
|
"""
|
||||||
|
global grid
|
||||||
|
|
||||||
|
# Calculate direction to player
|
||||||
|
dx = 0
|
||||||
|
dy = 0
|
||||||
|
|
||||||
|
if px < ex:
|
||||||
|
dx = -1
|
||||||
|
elif px > ex:
|
||||||
|
dx = 1
|
||||||
|
|
||||||
|
if py < ey:
|
||||||
|
dy = -1
|
||||||
|
elif py > ey:
|
||||||
|
dy = 1
|
||||||
|
|
||||||
|
# Try to move in the desired direction
|
||||||
|
# First try the combined direction
|
||||||
|
new_x = ex + dx
|
||||||
|
new_y = ey + dy
|
||||||
|
|
||||||
|
if can_move_to(grid, new_x, new_y, enemy):
|
||||||
|
enemy.x = new_x
|
||||||
|
enemy.y = new_y
|
||||||
|
elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy):
|
||||||
|
# Try horizontal only
|
||||||
|
enemy.x = ex + dx
|
||||||
|
elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
|
||||||
|
# Try vertical only
|
||||||
|
enemy.y = ey + dy
|
||||||
|
# If all fail, enemy stays in place
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Updates
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_hp_display() -> None:
|
||||||
|
"""Update the HP display in the UI."""
|
||||||
|
global player
|
||||||
|
|
||||||
|
if hp_display is None or player is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if player in entity_data:
|
||||||
|
fighter = entity_data[player]
|
||||||
|
hp_display.text = f"HP: {fighter.hp}/{fighter.max_hp}"
|
||||||
|
|
||||||
|
# Color based on health percentage
|
||||||
|
hp_percent = fighter.hp / fighter.max_hp
|
||||||
|
if hp_percent > 0.6:
|
||||||
|
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
|
||||||
|
elif hp_percent > 0.3:
|
||||||
|
hp_display.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
else:
|
||||||
|
hp_display.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(800, 480),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.0
|
||||||
|
|
||||||
|
# Generate initial dungeon structure
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored()
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Get player starting position
|
||||||
|
if rooms:
|
||||||
|
player_start_x, player_start_y = rooms[0].center
|
||||||
|
else:
|
||||||
|
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# Add FOV layer
|
||||||
|
fov_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Create the player
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(player_start_x, player_start_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Create player Fighter component
|
||||||
|
entity_data[player] = Fighter(
|
||||||
|
hp=30,
|
||||||
|
max_hp=30,
|
||||||
|
attack=5,
|
||||||
|
defense=2,
|
||||||
|
name="Player",
|
||||||
|
is_player=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Spawn enemies in all rooms except the first
|
||||||
|
for i, room in enumerate(rooms):
|
||||||
|
if i == 0:
|
||||||
|
continue
|
||||||
|
spawn_enemies_in_room(grid, room, texture)
|
||||||
|
|
||||||
|
# Calculate initial FOV
|
||||||
|
update_fov(grid, fov_layer, player_start_x, player_start_y)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(50, 15),
|
||||||
|
text="Part 6: Combat System"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(50, 50),
|
||||||
|
text="WASD/Arrows: Move/Attack | R: Restart | Escape: Quit"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
# Position display
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(50, 580),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
# HP display
|
||||||
|
hp_display = mcrfpy.Caption(
|
||||||
|
pos=(300, 580),
|
||||||
|
text="HP: 30/30"
|
||||||
|
)
|
||||||
|
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
|
||||||
|
hp_display.font_size = 16
|
||||||
|
scene.children.append(hp_display)
|
||||||
|
|
||||||
|
# Message log (positioned below the grid)
|
||||||
|
message_log_caption = mcrfpy.Caption(
|
||||||
|
pos=(50, 610),
|
||||||
|
text=""
|
||||||
|
)
|
||||||
|
message_log_caption.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
message_log_caption.font_size = 14
|
||||||
|
scene.children.append(message_log_caption)
|
||||||
|
|
||||||
|
# Initial message
|
||||||
|
add_message("Welcome to the dungeon! Find and defeat the enemies.", mcrfpy.Color(100, 100, 255))
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def restart_game() -> None:
|
||||||
|
"""Restart the game with a new dungeon."""
|
||||||
|
global player, grid, fov_layer, game_over, entity_data, rooms
|
||||||
|
|
||||||
|
game_over = False
|
||||||
|
|
||||||
|
# Clear all entities and data
|
||||||
|
entity_data.clear()
|
||||||
|
|
||||||
|
# Remove all entities from grid
|
||||||
|
while len(grid.entities) > 0:
|
||||||
|
grid.entities.remove(0)
|
||||||
|
|
||||||
|
# Regenerate dungeon
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored()
|
||||||
|
clear_messages()
|
||||||
|
|
||||||
|
rooms = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Get new player starting position
|
||||||
|
if rooms:
|
||||||
|
new_x, new_y = rooms[0].center
|
||||||
|
else:
|
||||||
|
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# Recreate player
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(new_x, new_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
entity_data[player] = Fighter(
|
||||||
|
hp=30,
|
||||||
|
max_hp=30,
|
||||||
|
attack=5,
|
||||||
|
defense=2,
|
||||||
|
name="Player",
|
||||||
|
is_player=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Spawn enemies
|
||||||
|
for i, room in enumerate(rooms):
|
||||||
|
if i == 0:
|
||||||
|
continue
|
||||||
|
spawn_enemies_in_room(grid, room, texture)
|
||||||
|
|
||||||
|
# Reset FOV layer
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Update displays
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
update_hp_display()
|
||||||
|
|
||||||
|
add_message("A new adventure begins!", mcrfpy.Color(100, 100, 255))
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input."""
|
||||||
|
global game_over
|
||||||
|
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle restart
|
||||||
|
if key == "R":
|
||||||
|
restart_game()
|
||||||
|
return
|
||||||
|
|
||||||
|
if key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ignore other input if game is over
|
||||||
|
if game_over:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Movement and attack
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
try_move_or_attack(0, -1)
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
try_move_or_attack(0, 1)
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
try_move_or_attack(-1, 0)
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
try_move_or_attack(1, 0)
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 6 loaded! Combat is now active. Good luck!")
|
||||||
1035
docs/tutorials/part_07_ui/part_07_ui.py
Normal file
1035
docs/tutorials/part_07_ui/part_07_ui.py
Normal file
File diff suppressed because it is too large
Load diff
1275
docs/tutorials/part_08_items/part_08_items.py
Normal file
1275
docs/tutorials/part_08_items/part_08_items.py
Normal file
File diff suppressed because it is too large
Load diff
1396
docs/tutorials/part_09_ranged/part_09_ranged.py
Normal file
1396
docs/tutorials/part_09_ranged/part_09_ranged.py
Normal file
File diff suppressed because it is too large
Load diff
1565
docs/tutorials/part_10_save_load/part_10_save_load.py
Normal file
1565
docs/tutorials/part_10_save_load/part_10_save_load.py
Normal file
File diff suppressed because it is too large
Load diff
1735
docs/tutorials/part_11_levels/part_11_levels.py
Normal file
1735
docs/tutorials/part_11_levels/part_11_levels.py
Normal file
File diff suppressed because it is too large
Load diff
1850
docs/tutorials/part_12_experience/part_12_experience.py
Normal file
1850
docs/tutorials/part_12_experience/part_12_experience.py
Normal file
File diff suppressed because it is too large
Load diff
1798
docs/tutorials/part_13_equipment/part_13_equipment.py
Normal file
1798
docs/tutorials/part_13_equipment/part_13_equipment.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue