draft tutorial revisions
This commit is contained in:
parent
838da4571d
commit
48359b5a48
70 changed files with 6216 additions and 28 deletions
138
docs/templates/roguelike/constants.py
vendored
Normal file
138
docs/templates/roguelike/constants.py
vendored
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""
|
||||
constants.py - Roguelike Template Constants
|
||||
|
||||
This module defines all the constants used throughout the roguelike template,
|
||||
including sprite indices for CP437 tileset, colors for FOV system, and
|
||||
game configuration values.
|
||||
|
||||
CP437 is the classic IBM PC character set commonly used in traditional roguelikes.
|
||||
The sprite indices correspond to ASCII character codes in a CP437 tileset.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# =============================================================================
|
||||
# SPRITE INDICES (CP437 Character Codes)
|
||||
# =============================================================================
|
||||
# These indices correspond to characters in a CP437-style tileset.
|
||||
# The default McRogueFace tileset uses 16x16 sprites arranged in a grid.
|
||||
|
||||
# Terrain sprites
|
||||
SPRITE_FLOOR = 46 # '.' - Standard floor tile
|
||||
SPRITE_WALL = 35 # '#' - Wall/obstacle tile
|
||||
SPRITE_DOOR_CLOSED = 43 # '+' - Closed door
|
||||
SPRITE_DOOR_OPEN = 47 # '/' - Open door
|
||||
SPRITE_STAIRS_DOWN = 62 # '>' - Stairs going down
|
||||
SPRITE_STAIRS_UP = 60 # '<' - Stairs going up
|
||||
|
||||
# Player sprite
|
||||
SPRITE_PLAYER = 64 # '@' - The classic roguelike player symbol
|
||||
|
||||
# Enemy sprites
|
||||
SPRITE_ORC = 111 # 'o' - Orc enemy
|
||||
SPRITE_TROLL = 84 # 'T' - Troll enemy
|
||||
SPRITE_GOBLIN = 103 # 'g' - Goblin enemy
|
||||
SPRITE_RAT = 114 # 'r' - Giant rat
|
||||
SPRITE_SNAKE = 115 # 's' - Snake
|
||||
SPRITE_ZOMBIE = 90 # 'Z' - Zombie
|
||||
|
||||
# Item sprites
|
||||
SPRITE_POTION = 33 # '!' - Potion
|
||||
SPRITE_SCROLL = 63 # '?' - Scroll
|
||||
SPRITE_GOLD = 36 # '$' - Gold/treasure
|
||||
SPRITE_WEAPON = 41 # ')' - Weapon
|
||||
SPRITE_ARMOR = 91 # '[' - Armor
|
||||
SPRITE_RING = 61 # '=' - Ring
|
||||
|
||||
# =============================================================================
|
||||
# FOV/VISIBILITY COLORS
|
||||
# =============================================================================
|
||||
# These colors are applied as overlays to grid tiles to create the fog of war
|
||||
# effect. The alpha channel determines how much of the original tile shows through.
|
||||
|
||||
# Fully visible - no overlay (alpha = 0 means completely transparent overlay)
|
||||
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
|
||||
|
||||
# Previously explored but not currently visible - dim blue-gray overlay
|
||||
# This creates the "memory" effect where you can see the map layout
|
||||
# but not current enemy positions
|
||||
COLOR_EXPLORED = mcrfpy.Color(50, 50, 80, 180)
|
||||
|
||||
# Never seen - completely black (alpha = 255 means fully opaque)
|
||||
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
|
||||
|
||||
# =============================================================================
|
||||
# TILE COLORS
|
||||
# =============================================================================
|
||||
# Base colors for different tile types (applied to the tile's color property)
|
||||
|
||||
COLOR_FLOOR = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
||||
COLOR_WALL = mcrfpy.Color(100, 100, 100) # Lighter gray walls
|
||||
COLOR_FLOOR_LIT = mcrfpy.Color(100, 90, 70) # Warm lit floor
|
||||
COLOR_WALL_LIT = mcrfpy.Color(130, 110, 80) # Warm lit walls
|
||||
|
||||
# =============================================================================
|
||||
# ENTITY COLORS
|
||||
# =============================================================================
|
||||
# Colors applied to entity sprites
|
||||
|
||||
COLOR_PLAYER = mcrfpy.Color(255, 255, 255) # White player
|
||||
COLOR_ORC = mcrfpy.Color(63, 127, 63) # Green orc
|
||||
COLOR_TROLL = mcrfpy.Color(0, 127, 0) # Darker green troll
|
||||
COLOR_GOBLIN = mcrfpy.Color(127, 127, 0) # Yellow-green goblin
|
||||
|
||||
# =============================================================================
|
||||
# GAME CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Map dimensions (in tiles)
|
||||
MAP_WIDTH = 80
|
||||
MAP_HEIGHT = 45
|
||||
|
||||
# Room generation parameters
|
||||
ROOM_MIN_SIZE = 6 # Minimum room dimension
|
||||
ROOM_MAX_SIZE = 12 # Maximum room dimension
|
||||
MAX_ROOMS = 30 # Maximum number of rooms to generate
|
||||
|
||||
# FOV settings
|
||||
FOV_RADIUS = 8 # How far the player can see
|
||||
|
||||
# Display settings
|
||||
GRID_PIXEL_WIDTH = 1024 # Grid display width in pixels
|
||||
GRID_PIXEL_HEIGHT = 768 # Grid display height in pixels
|
||||
|
||||
# Sprite size (should match your tileset)
|
||||
SPRITE_WIDTH = 16
|
||||
SPRITE_HEIGHT = 16
|
||||
|
||||
# =============================================================================
|
||||
# ENEMY DEFINITIONS
|
||||
# =============================================================================
|
||||
# Dictionary of enemy types with their properties for easy spawning
|
||||
|
||||
ENEMY_TYPES = {
|
||||
"orc": {
|
||||
"sprite": SPRITE_ORC,
|
||||
"color": COLOR_ORC,
|
||||
"name": "Orc",
|
||||
"hp": 10,
|
||||
"power": 3,
|
||||
"defense": 0,
|
||||
},
|
||||
"troll": {
|
||||
"sprite": SPRITE_TROLL,
|
||||
"color": COLOR_TROLL,
|
||||
"name": "Troll",
|
||||
"hp": 16,
|
||||
"power": 4,
|
||||
"defense": 1,
|
||||
},
|
||||
"goblin": {
|
||||
"sprite": SPRITE_GOBLIN,
|
||||
"color": COLOR_GOBLIN,
|
||||
"name": "Goblin",
|
||||
"hp": 6,
|
||||
"power": 2,
|
||||
"defense": 0,
|
||||
},
|
||||
}
|
||||
340
docs/templates/roguelike/dungeon.py
vendored
Normal file
340
docs/templates/roguelike/dungeon.py
vendored
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
"""
|
||||
dungeon.py - Procedural Dungeon Generation
|
||||
|
||||
This module provides classic roguelike dungeon generation using the
|
||||
"rooms and corridors" algorithm:
|
||||
|
||||
1. Generate random non-overlapping rectangular rooms
|
||||
2. Connect rooms with L-shaped corridors
|
||||
3. Mark tiles as walkable/transparent based on terrain type
|
||||
|
||||
The algorithm is simple but effective, producing dungeons similar to
|
||||
the original Rogue game.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import random
|
||||
from typing import Iterator, Tuple, List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import mcrfpy
|
||||
|
||||
from constants import (
|
||||
MAP_WIDTH, MAP_HEIGHT,
|
||||
ROOM_MIN_SIZE, ROOM_MAX_SIZE, MAX_ROOMS,
|
||||
SPRITE_FLOOR, SPRITE_WALL,
|
||||
COLOR_FLOOR, COLOR_WALL,
|
||||
)
|
||||
|
||||
|
||||
class RectangularRoom:
|
||||
"""
|
||||
A rectangular room in the dungeon.
|
||||
|
||||
This class represents a single room and provides utilities for
|
||||
working with room geometry. Rooms are defined by their top-left
|
||||
corner (x1, y1) and bottom-right corner (x2, y2).
|
||||
|
||||
Attributes:
|
||||
x1, y1: Top-left corner coordinates
|
||||
x2, y2: Bottom-right corner coordinates
|
||||
"""
|
||||
|
||||
def __init__(self, x: int, y: int, width: int, height: int) -> None:
|
||||
"""
|
||||
Create a new rectangular room.
|
||||
|
||||
Args:
|
||||
x: X coordinate of the top-left corner
|
||||
y: Y coordinate of the top-left corner
|
||||
width: Width of the room in tiles
|
||||
height: Height of the room 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.
|
||||
|
||||
This is useful for connecting rooms with corridors and
|
||||
for placing the player in the starting room.
|
||||
|
||||
Returns:
|
||||
Tuple of (center_x, center_y)
|
||||
"""
|
||||
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 as a pair of slices.
|
||||
|
||||
The inner area excludes the walls (1 tile border), giving
|
||||
the floor area where entities can be placed.
|
||||
|
||||
Returns:
|
||||
Tuple of (x_slice, y_slice) for array indexing
|
||||
"""
|
||||
# Add 1 to exclude the walls on all sides
|
||||
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.
|
||||
|
||||
Used during generation to ensure rooms don't overlap.
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def inner_tiles(self) -> Iterator[Tuple[int, int]]:
|
||||
"""
|
||||
Iterate over all floor tile coordinates in the room.
|
||||
|
||||
Yields coordinates for the interior of the room (excluding walls).
|
||||
|
||||
Yields:
|
||||
Tuples of (x, y) coordinates
|
||||
"""
|
||||
for x in range(self.x1 + 1, self.x2):
|
||||
for y in range(self.y1 + 1, self.y2):
|
||||
yield x, y
|
||||
|
||||
|
||||
def tunnel_between(
|
||||
start: Tuple[int, int],
|
||||
end: Tuple[int, int]
|
||||
) -> Iterator[Tuple[int, int]]:
|
||||
"""
|
||||
Generate an L-shaped tunnel between two points.
|
||||
|
||||
The tunnel goes horizontally first, then vertically (or vice versa,
|
||||
chosen randomly). This creates the classic roguelike corridor style.
|
||||
|
||||
Args:
|
||||
start: Starting (x, y) coordinates
|
||||
end: Ending (x, y) coordinates
|
||||
|
||||
Yields:
|
||||
Tuples of (x, y) coordinates for each tile in the tunnel
|
||||
"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
# Randomly choose whether to go horizontal-first or vertical-first
|
||||
if random.random() < 0.5:
|
||||
# Horizontal first, then vertical
|
||||
corner_x, corner_y = x2, y1
|
||||
else:
|
||||
# Vertical first, then horizontal
|
||||
corner_x, corner_y = x1, y2
|
||||
|
||||
# Generate the horizontal segment
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
|
||||
# Generate the vertical segment
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
|
||||
# Generate to the endpoint (if needed)
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
|
||||
def generate_dungeon(
|
||||
max_rooms: int = MAX_ROOMS,
|
||||
room_min_size: int = ROOM_MIN_SIZE,
|
||||
room_max_size: int = ROOM_MAX_SIZE,
|
||||
map_width: int = MAP_WIDTH,
|
||||
map_height: int = MAP_HEIGHT,
|
||||
) -> List[RectangularRoom]:
|
||||
"""
|
||||
Generate a dungeon using the rooms-and-corridors algorithm.
|
||||
|
||||
This function creates a list of non-overlapping rooms. The actual
|
||||
tile data should be applied to a Grid using populate_grid().
|
||||
|
||||
Algorithm:
|
||||
1. Try to place MAX_ROOMS rooms randomly
|
||||
2. Reject rooms that overlap existing rooms
|
||||
3. Connect each new room to the previous room with a corridor
|
||||
|
||||
Args:
|
||||
max_rooms: Maximum number of rooms to generate
|
||||
room_min_size: Minimum room dimension
|
||||
room_max_size: Maximum room dimension
|
||||
map_width: Width of the dungeon in tiles
|
||||
map_height: Height of the dungeon in tiles
|
||||
|
||||
Returns:
|
||||
List of RectangularRoom objects representing the dungeon layout
|
||||
"""
|
||||
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 (ensuring room fits within map bounds)
|
||||
x = random.randint(0, map_width - room_width - 1)
|
||||
y = random.randint(0, map_height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
# Check if this room overlaps with any existing room
|
||||
if any(new_room.intersects(other) for other in rooms):
|
||||
continue # Skip this room, try again
|
||||
|
||||
# Room is valid, add it
|
||||
rooms.append(new_room)
|
||||
|
||||
return rooms
|
||||
|
||||
|
||||
def populate_grid(grid: mcrfpy.Grid, rooms: List[RectangularRoom]) -> None:
|
||||
"""
|
||||
Apply dungeon layout to a McRogueFace Grid.
|
||||
|
||||
This function:
|
||||
1. Fills the entire grid with walls
|
||||
2. Carves out floor tiles for each room
|
||||
3. Carves corridors connecting adjacent rooms
|
||||
4. Sets walkable/transparent flags appropriately
|
||||
|
||||
Args:
|
||||
grid: The McRogueFace Grid to populate
|
||||
rooms: List of RectangularRoom objects from generate_dungeon()
|
||||
"""
|
||||
grid_width, grid_height = grid.grid_size
|
||||
|
||||
# Step 1: Fill entire map with walls
|
||||
for x in range(grid_width):
|
||||
for y in range(grid_height):
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = SPRITE_WALL
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
point.color = COLOR_WALL
|
||||
|
||||
# Step 2: Carve out rooms
|
||||
for room in rooms:
|
||||
for x, y in room.inner_tiles():
|
||||
# Bounds check (room might extend past grid)
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = SPRITE_FLOOR
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
point.color = COLOR_FLOOR
|
||||
|
||||
# Step 3: Carve corridors between adjacent rooms
|
||||
for i in range(1, len(rooms)):
|
||||
# Connect each room to the previous room
|
||||
start = rooms[i - 1].center
|
||||
end = rooms[i].center
|
||||
|
||||
for x, y in tunnel_between(start, end):
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = SPRITE_FLOOR
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
point.color = COLOR_FLOOR
|
||||
|
||||
|
||||
def get_random_floor_position(
|
||||
grid: mcrfpy.Grid,
|
||||
rooms: List[RectangularRoom],
|
||||
exclude_first_room: bool = False
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Get a random walkable floor position for entity placement.
|
||||
|
||||
This is useful for placing enemies, items, or other entities
|
||||
in valid floor locations.
|
||||
|
||||
Args:
|
||||
grid: The populated Grid to search
|
||||
rooms: List of rooms (used for faster random selection)
|
||||
exclude_first_room: If True, won't return positions from the
|
||||
first room (where the player usually starts)
|
||||
|
||||
Returns:
|
||||
Tuple of (x, y) coordinates of a walkable floor tile
|
||||
"""
|
||||
available_rooms = rooms[1:] if exclude_first_room and len(rooms) > 1 else rooms
|
||||
|
||||
if not available_rooms:
|
||||
# Fallback: find any walkable tile
|
||||
grid_width, grid_height = grid.grid_size
|
||||
walkable_tiles = []
|
||||
for x in range(grid_width):
|
||||
for y in range(grid_height):
|
||||
if grid.at(x, y).walkable:
|
||||
walkable_tiles.append((x, y))
|
||||
return random.choice(walkable_tiles) if walkable_tiles else (1, 1)
|
||||
|
||||
# Pick a random room and a random position within it
|
||||
room = random.choice(available_rooms)
|
||||
floor_tiles = list(room.inner_tiles())
|
||||
return random.choice(floor_tiles)
|
||||
|
||||
|
||||
def get_spawn_positions(
|
||||
rooms: List[RectangularRoom],
|
||||
count: int,
|
||||
exclude_first_room: bool = True
|
||||
) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Get multiple spawn positions for enemies.
|
||||
|
||||
Distributes enemies across different rooms for better gameplay.
|
||||
|
||||
Args:
|
||||
rooms: List of rooms from dungeon generation
|
||||
count: Number of positions to generate
|
||||
exclude_first_room: If True, won't spawn in the player's starting room
|
||||
|
||||
Returns:
|
||||
List of (x, y) coordinate tuples
|
||||
"""
|
||||
available_rooms = rooms[1:] if exclude_first_room and len(rooms) > 1 else rooms
|
||||
|
||||
if not available_rooms:
|
||||
return []
|
||||
|
||||
positions = []
|
||||
for i in range(count):
|
||||
# Cycle through rooms to distribute enemies
|
||||
room = available_rooms[i % len(available_rooms)]
|
||||
floor_tiles = list(room.inner_tiles())
|
||||
|
||||
# Try to avoid placing on the same tile
|
||||
available_tiles = [t for t in floor_tiles if t not in positions]
|
||||
if available_tiles:
|
||||
positions.append(random.choice(available_tiles))
|
||||
elif floor_tiles:
|
||||
positions.append(random.choice(floor_tiles))
|
||||
|
||||
return positions
|
||||
364
docs/templates/roguelike/entities.py
vendored
Normal file
364
docs/templates/roguelike/entities.py
vendored
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
"""
|
||||
entities.py - Entity Management for Roguelike Template
|
||||
|
||||
This module provides entity creation and management utilities for the
|
||||
roguelike template. Entities in McRogueFace are game objects that exist
|
||||
on a Grid, such as the player, enemies, items, and NPCs.
|
||||
|
||||
The module includes:
|
||||
- Entity factory functions for creating common entity types
|
||||
- Helper functions for entity management
|
||||
- Simple data containers for entity stats (for future expansion)
|
||||
|
||||
Note: McRogueFace entities are simple position + sprite objects. For
|
||||
complex game logic like AI, combat, and inventory, you'll want to wrap
|
||||
them in Python classes that reference the underlying Entity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Tuple, Optional, List, Dict, Any, TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import mcrfpy
|
||||
|
||||
from constants import (
|
||||
SPRITE_PLAYER, SPRITE_ORC, SPRITE_TROLL, SPRITE_GOBLIN,
|
||||
COLOR_PLAYER, COLOR_ORC, COLOR_TROLL, COLOR_GOBLIN,
|
||||
ENEMY_TYPES,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityStats:
|
||||
"""
|
||||
Optional stats container for game entities.
|
||||
|
||||
This dataclass can be used to track stats for entities that need them.
|
||||
Attach it to your entity wrapper class for combat, leveling, etc.
|
||||
|
||||
Attributes:
|
||||
hp: Current hit points
|
||||
max_hp: Maximum hit points
|
||||
power: Attack power
|
||||
defense: Damage reduction
|
||||
name: Display name for the entity
|
||||
"""
|
||||
hp: int = 10
|
||||
max_hp: int = 10
|
||||
power: int = 3
|
||||
defense: int = 0
|
||||
name: str = "Unknown"
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if the entity is still alive."""
|
||||
return self.hp > 0
|
||||
|
||||
def take_damage(self, amount: int) -> int:
|
||||
"""
|
||||
Apply damage, accounting for defense.
|
||||
|
||||
Args:
|
||||
amount: Raw damage amount
|
||||
|
||||
Returns:
|
||||
Actual damage dealt after defense
|
||||
"""
|
||||
actual_damage = max(0, amount - self.defense)
|
||||
self.hp = max(0, self.hp - actual_damage)
|
||||
return actual_damage
|
||||
|
||||
def heal(self, amount: int) -> int:
|
||||
"""
|
||||
Heal the entity.
|
||||
|
||||
Args:
|
||||
amount: Amount to heal
|
||||
|
||||
Returns:
|
||||
Actual amount healed (may be less if near max HP)
|
||||
"""
|
||||
old_hp = self.hp
|
||||
self.hp = min(self.max_hp, self.hp + amount)
|
||||
return self.hp - old_hp
|
||||
|
||||
|
||||
def create_player(
|
||||
grid: mcrfpy.Grid,
|
||||
texture: mcrfpy.Texture,
|
||||
x: int,
|
||||
y: int
|
||||
) -> mcrfpy.Entity:
|
||||
"""
|
||||
Create and place the player entity on the grid.
|
||||
|
||||
The player uses the classic '@' symbol (sprite index 64 in CP437).
|
||||
|
||||
Args:
|
||||
grid: The Grid to place the player on
|
||||
texture: The texture/tileset to use
|
||||
x: Starting X position
|
||||
y: Starting Y position
|
||||
|
||||
Returns:
|
||||
The created player Entity
|
||||
"""
|
||||
import mcrfpy
|
||||
|
||||
player = mcrfpy.Entity(
|
||||
pos=(x, y),
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_PLAYER
|
||||
)
|
||||
grid.entities.append(player)
|
||||
|
||||
return player
|
||||
|
||||
|
||||
def create_enemy(
|
||||
grid: mcrfpy.Grid,
|
||||
texture: mcrfpy.Texture,
|
||||
x: int,
|
||||
y: int,
|
||||
enemy_type: str = "orc"
|
||||
) -> Tuple[mcrfpy.Entity, EntityStats]:
|
||||
"""
|
||||
Create an enemy entity with associated stats.
|
||||
|
||||
Enemy types are defined in constants.py. Currently available:
|
||||
- "orc": Standard enemy, balanced stats
|
||||
- "troll": Tough enemy, high HP and power
|
||||
- "goblin": Weak enemy, low stats
|
||||
|
||||
Args:
|
||||
grid: The Grid to place the enemy on
|
||||
texture: The texture/tileset to use
|
||||
x: X position
|
||||
y: Y position
|
||||
enemy_type: Key from ENEMY_TYPES dict
|
||||
|
||||
Returns:
|
||||
Tuple of (Entity, EntityStats) for the created enemy
|
||||
"""
|
||||
import mcrfpy
|
||||
|
||||
# Get enemy definition, default to orc if not found
|
||||
enemy_def = ENEMY_TYPES.get(enemy_type, ENEMY_TYPES["orc"])
|
||||
|
||||
entity = mcrfpy.Entity(
|
||||
pos=(x, y),
|
||||
texture=texture,
|
||||
sprite_index=enemy_def["sprite"]
|
||||
)
|
||||
grid.entities.append(entity)
|
||||
|
||||
stats = EntityStats(
|
||||
hp=enemy_def["hp"],
|
||||
max_hp=enemy_def["hp"],
|
||||
power=enemy_def["power"],
|
||||
defense=enemy_def["defense"],
|
||||
name=enemy_def["name"]
|
||||
)
|
||||
|
||||
return entity, stats
|
||||
|
||||
|
||||
def create_enemies_in_rooms(
|
||||
grid: mcrfpy.Grid,
|
||||
texture: mcrfpy.Texture,
|
||||
rooms: list,
|
||||
enemies_per_room: int = 2,
|
||||
skip_first_room: bool = True
|
||||
) -> List[Tuple[mcrfpy.Entity, EntityStats]]:
|
||||
"""
|
||||
Populate dungeon rooms with enemies.
|
||||
|
||||
This helper function places random enemies throughout the dungeon,
|
||||
typically skipping the first room (where the player starts).
|
||||
|
||||
Args:
|
||||
grid: The Grid to populate
|
||||
texture: The texture/tileset to use
|
||||
rooms: List of RectangularRoom objects from dungeon generation
|
||||
enemies_per_room: Maximum enemies to spawn per room
|
||||
skip_first_room: If True, don't spawn enemies in the first room
|
||||
|
||||
Returns:
|
||||
List of (Entity, EntityStats) tuples for all created enemies
|
||||
"""
|
||||
import random
|
||||
|
||||
enemies = []
|
||||
enemy_type_keys = list(ENEMY_TYPES.keys())
|
||||
|
||||
# Iterate through rooms, optionally skipping the first
|
||||
rooms_to_populate = rooms[1:] if skip_first_room else rooms
|
||||
|
||||
for room in rooms_to_populate:
|
||||
# Random number of enemies (0 to enemies_per_room)
|
||||
num_enemies = random.randint(0, enemies_per_room)
|
||||
|
||||
# Get available floor tiles in this room
|
||||
floor_tiles = list(room.inner_tiles())
|
||||
|
||||
for _ in range(num_enemies):
|
||||
if not floor_tiles:
|
||||
break
|
||||
|
||||
# Pick a random position and remove it from available
|
||||
pos = random.choice(floor_tiles)
|
||||
floor_tiles.remove(pos)
|
||||
|
||||
# Pick a random enemy type (weighted toward weaker enemies)
|
||||
if random.random() < 0.8:
|
||||
enemy_type = "orc" # 80% orcs
|
||||
else:
|
||||
enemy_type = "troll" # 20% trolls
|
||||
|
||||
x, y = pos
|
||||
entity, stats = create_enemy(grid, texture, x, y, enemy_type)
|
||||
enemies.append((entity, stats))
|
||||
|
||||
return enemies
|
||||
|
||||
|
||||
def get_blocking_entity_at(
|
||||
entities: List[mcrfpy.Entity],
|
||||
x: int,
|
||||
y: int
|
||||
) -> Optional[mcrfpy.Entity]:
|
||||
"""
|
||||
Check if there's a blocking entity at the given position.
|
||||
|
||||
Useful for collision detection - checks if an entity exists at
|
||||
the target position before moving there.
|
||||
|
||||
Args:
|
||||
entities: List of entities to check
|
||||
x: X coordinate to check
|
||||
y: Y coordinate to check
|
||||
|
||||
Returns:
|
||||
The entity at that position, or None if empty
|
||||
"""
|
||||
for entity in entities:
|
||||
if entity.pos[0] == x and entity.pos[1] == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
|
||||
def move_entity(
|
||||
entity: mcrfpy.Entity,
|
||||
grid: mcrfpy.Grid,
|
||||
dx: int,
|
||||
dy: int,
|
||||
entities: List[mcrfpy.Entity] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Attempt to move an entity by a delta.
|
||||
|
||||
Checks for:
|
||||
- Grid bounds
|
||||
- Walkable terrain
|
||||
- Other blocking entities (if entities list provided)
|
||||
|
||||
Args:
|
||||
entity: The entity to move
|
||||
grid: The grid for terrain collision
|
||||
dx: Delta X (-1, 0, or 1 typically)
|
||||
dy: Delta Y (-1, 0, or 1 typically)
|
||||
entities: Optional list of entities to check for collision
|
||||
|
||||
Returns:
|
||||
True if movement succeeded, False otherwise
|
||||
"""
|
||||
dest_x = entity.pos[0] + dx
|
||||
dest_y = entity.pos[1] + dy
|
||||
|
||||
# Check grid bounds
|
||||
grid_width, grid_height = grid.grid_size
|
||||
if not (0 <= dest_x < grid_width and 0 <= dest_y < grid_height):
|
||||
return False
|
||||
|
||||
# Check if tile is walkable
|
||||
if not grid.at(dest_x, dest_y).walkable:
|
||||
return False
|
||||
|
||||
# Check for blocking entities
|
||||
if entities and get_blocking_entity_at(entities, dest_x, dest_y):
|
||||
return False
|
||||
|
||||
# Move is valid
|
||||
entity.pos = (dest_x, dest_y)
|
||||
return True
|
||||
|
||||
|
||||
def distance_between(
|
||||
entity1: mcrfpy.Entity,
|
||||
entity2: mcrfpy.Entity
|
||||
) -> float:
|
||||
"""
|
||||
Calculate the Chebyshev distance between two entities.
|
||||
|
||||
Chebyshev distance (also called chessboard distance) counts
|
||||
diagonal moves as 1, which is standard for roguelikes.
|
||||
|
||||
Args:
|
||||
entity1: First entity
|
||||
entity2: Second entity
|
||||
|
||||
Returns:
|
||||
Distance in tiles (diagonal = 1)
|
||||
"""
|
||||
dx = abs(entity1.pos[0] - entity2.pos[0])
|
||||
dy = abs(entity1.pos[1] - entity2.pos[1])
|
||||
return max(dx, dy)
|
||||
|
||||
|
||||
def entities_in_radius(
|
||||
center: mcrfpy.Entity,
|
||||
entities: List[mcrfpy.Entity],
|
||||
radius: float
|
||||
) -> List[mcrfpy.Entity]:
|
||||
"""
|
||||
Find all entities within a given radius of a center entity.
|
||||
|
||||
Uses Chebyshev distance for roguelike-style radius.
|
||||
|
||||
Args:
|
||||
center: The entity to search around
|
||||
entities: List of entities to check
|
||||
radius: Maximum distance in tiles
|
||||
|
||||
Returns:
|
||||
List of entities within the radius (excluding center)
|
||||
"""
|
||||
nearby = []
|
||||
for entity in entities:
|
||||
if entity is not center:
|
||||
if distance_between(center, entity) <= radius:
|
||||
nearby.append(entity)
|
||||
return nearby
|
||||
|
||||
|
||||
def remove_entity(
|
||||
entity: mcrfpy.Entity,
|
||||
grid: mcrfpy.Grid
|
||||
) -> bool:
|
||||
"""
|
||||
Remove an entity from a grid.
|
||||
|
||||
Args:
|
||||
entity: The entity to remove
|
||||
grid: The grid containing the entity
|
||||
|
||||
Returns:
|
||||
True if removal succeeded, False otherwise
|
||||
"""
|
||||
try:
|
||||
idx = entity.index()
|
||||
grid.entities.remove(idx)
|
||||
return True
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
290
docs/templates/roguelike/game.py
vendored
Normal file
290
docs/templates/roguelike/game.py
vendored
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
"""
|
||||
game.py - Roguelike Template Main Entry Point
|
||||
|
||||
A minimal but complete roguelike starter using McRogueFace.
|
||||
|
||||
This template demonstrates:
|
||||
- Scene and grid setup
|
||||
- Procedural dungeon generation
|
||||
- Player entity with keyboard movement
|
||||
- Enemy entities (static, no AI)
|
||||
- Field of view using TCOD via Entity.update_visibility()
|
||||
- FOV visualization with grid color overlays
|
||||
|
||||
Run with: ./mcrogueface
|
||||
|
||||
Controls:
|
||||
- Arrow keys / WASD: Move player
|
||||
- Escape: Quit game
|
||||
|
||||
The template is designed to be extended. Good next steps:
|
||||
- Add enemy AI (chase player, pathfinding)
|
||||
- Implement combat system
|
||||
- Add items and inventory
|
||||
- Add multiple dungeon levels
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from typing import List, Tuple
|
||||
|
||||
# Import our template modules
|
||||
from constants import (
|
||||
MAP_WIDTH, MAP_HEIGHT,
|
||||
SPRITE_WIDTH, SPRITE_HEIGHT,
|
||||
FOV_RADIUS,
|
||||
COLOR_VISIBLE, COLOR_EXPLORED, COLOR_UNKNOWN,
|
||||
SPRITE_PLAYER,
|
||||
)
|
||||
from dungeon import generate_dungeon, populate_grid, RectangularRoom
|
||||
from entities import (
|
||||
create_player,
|
||||
create_enemies_in_rooms,
|
||||
move_entity,
|
||||
EntityStats,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GAME STATE
|
||||
# =============================================================================
|
||||
# Global game state - in a larger game, you'd use a proper state management
|
||||
# system, but for a template this keeps things simple and visible.
|
||||
|
||||
class GameState:
|
||||
"""Container for all game state."""
|
||||
|
||||
def __init__(self):
|
||||
# Core game objects (set during initialization)
|
||||
self.grid: mcrfpy.Grid = None
|
||||
self.player: mcrfpy.Entity = None
|
||||
self.rooms: List[RectangularRoom] = []
|
||||
self.enemies: List[Tuple[mcrfpy.Entity, EntityStats]] = []
|
||||
|
||||
# Texture reference
|
||||
self.texture: mcrfpy.Texture = None
|
||||
|
||||
|
||||
# Global game state instance
|
||||
game = GameState()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FOV (FIELD OF VIEW) SYSTEM
|
||||
# =============================================================================
|
||||
|
||||
def update_fov() -> None:
|
||||
"""
|
||||
Update the field of view based on player position.
|
||||
|
||||
This function:
|
||||
1. Calls update_visibility() on the player entity to compute FOV using TCOD
|
||||
2. Applies color overlays to tiles based on visibility state
|
||||
|
||||
The FOV creates the classic roguelike effect where:
|
||||
- Visible tiles are fully bright (no overlay)
|
||||
- Previously seen tiles are dimmed (remembered layout)
|
||||
- Never-seen tiles are completely dark
|
||||
|
||||
TCOD handles the actual FOV computation based on the grid's
|
||||
walkable and transparent flags set during dungeon generation.
|
||||
"""
|
||||
if not game.player or not game.grid:
|
||||
return
|
||||
|
||||
# Tell McRogueFace/TCOD to recompute visibility from player position
|
||||
game.player.update_visibility()
|
||||
|
||||
grid_width, grid_height = game.grid.grid_size
|
||||
|
||||
# Apply visibility colors to each tile
|
||||
for x in range(grid_width):
|
||||
for y in range(grid_height):
|
||||
point = game.grid.at(x, y)
|
||||
|
||||
# Get the player's visibility state for this tile
|
||||
state = game.player.at(x, y)
|
||||
|
||||
if state.visible:
|
||||
# Currently visible - no overlay (full brightness)
|
||||
point.color_overlay = COLOR_VISIBLE
|
||||
elif state.discovered:
|
||||
# Previously seen - dimmed overlay (memory)
|
||||
point.color_overlay = COLOR_EXPLORED
|
||||
else:
|
||||
# Never seen - completely dark
|
||||
point.color_overlay = COLOR_UNKNOWN
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# INPUT HANDLING
|
||||
# =============================================================================
|
||||
|
||||
def handle_keys(key: str, state: str) -> None:
|
||||
"""
|
||||
Handle keyboard input for player movement and game controls.
|
||||
|
||||
This is the main input handler registered with McRogueFace.
|
||||
It processes key events and updates game state accordingly.
|
||||
|
||||
Args:
|
||||
key: The key that was pressed (e.g., "W", "Up", "Escape")
|
||||
state: Either "start" (key pressed) or "end" (key released)
|
||||
"""
|
||||
# Only process key press events, not releases
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
# Movement deltas: (dx, dy)
|
||||
movement = {
|
||||
# Arrow keys
|
||||
"Up": (0, -1),
|
||||
"Down": (0, 1),
|
||||
"Left": (-1, 0),
|
||||
"Right": (1, 0),
|
||||
# WASD keys
|
||||
"W": (0, -1),
|
||||
"S": (0, 1),
|
||||
"A": (-1, 0),
|
||||
"D": (1, 0),
|
||||
# Numpad (for diagonal movement if desired)
|
||||
"Numpad8": (0, -1),
|
||||
"Numpad2": (0, 1),
|
||||
"Numpad4": (-1, 0),
|
||||
"Numpad6": (1, 0),
|
||||
"Numpad7": (-1, -1),
|
||||
"Numpad9": (1, -1),
|
||||
"Numpad1": (-1, 1),
|
||||
"Numpad3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
|
||||
# Get list of all entity objects for collision checking
|
||||
all_entities = [e for e, _ in game.enemies]
|
||||
|
||||
# Attempt to move the player
|
||||
if move_entity(game.player, game.grid, dx, dy, all_entities):
|
||||
# Movement succeeded - update FOV
|
||||
update_fov()
|
||||
|
||||
# Center camera on player
|
||||
px, py = game.player.pos
|
||||
game.grid.center = (px, py)
|
||||
|
||||
elif key == "Escape":
|
||||
# Quit the game
|
||||
mcrfpy.exit()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GAME INITIALIZATION
|
||||
# =============================================================================
|
||||
|
||||
def initialize_game() -> None:
|
||||
"""
|
||||
Set up the game world.
|
||||
|
||||
This function:
|
||||
1. Creates the scene and loads resources
|
||||
2. Generates the dungeon layout
|
||||
3. Creates and places all entities
|
||||
4. Initializes the FOV system
|
||||
5. Sets up input handling
|
||||
"""
|
||||
# Create the game scene
|
||||
mcrfpy.createScene("game")
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Load the tileset texture
|
||||
# The default McRogueFace texture works great for roguelikes
|
||||
game.texture = mcrfpy.Texture(
|
||||
"assets/kenney_tinydungeon.png",
|
||||
SPRITE_WIDTH,
|
||||
SPRITE_HEIGHT
|
||||
)
|
||||
|
||||
# Create the grid (tile-based game world)
|
||||
# Using keyword arguments for clarity - this is the preferred style
|
||||
game.grid = mcrfpy.Grid(
|
||||
pos=(0, 0), # Screen position in pixels
|
||||
size=(1024, 768), # Display size in pixels
|
||||
grid_size=(MAP_WIDTH, MAP_HEIGHT), # Map size in tiles
|
||||
texture=game.texture
|
||||
)
|
||||
ui.append(game.grid)
|
||||
|
||||
# Generate dungeon layout
|
||||
game.rooms = generate_dungeon()
|
||||
|
||||
# Apply dungeon to grid (sets tiles, walkable flags, etc.)
|
||||
populate_grid(game.grid, game.rooms)
|
||||
|
||||
# Place player in the center of the first room
|
||||
if game.rooms:
|
||||
start_x, start_y = game.rooms[0].center
|
||||
else:
|
||||
# Fallback if no rooms generated
|
||||
start_x, start_y = MAP_WIDTH // 2, MAP_HEIGHT // 2
|
||||
|
||||
game.player = create_player(
|
||||
grid=game.grid,
|
||||
texture=game.texture,
|
||||
x=start_x,
|
||||
y=start_y
|
||||
)
|
||||
|
||||
# Center camera on player
|
||||
game.grid.center = (start_x, start_y)
|
||||
|
||||
# Spawn enemies in other rooms
|
||||
game.enemies = create_enemies_in_rooms(
|
||||
grid=game.grid,
|
||||
texture=game.texture,
|
||||
rooms=game.rooms,
|
||||
enemies_per_room=2,
|
||||
skip_first_room=True
|
||||
)
|
||||
|
||||
# Initial FOV calculation
|
||||
update_fov()
|
||||
|
||||
# Register input handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Switch to game scene
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN ENTRY POINT
|
||||
# =============================================================================
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Main entry point for the roguelike template.
|
||||
|
||||
This function is called when the script starts. It initializes
|
||||
the game and McRogueFace handles the game loop automatically.
|
||||
"""
|
||||
initialize_game()
|
||||
|
||||
# Display welcome message
|
||||
print("=" * 50)
|
||||
print(" ROGUELIKE TEMPLATE")
|
||||
print("=" * 50)
|
||||
print("Controls:")
|
||||
print(" Arrow keys / WASD - Move")
|
||||
print(" Escape - Quit")
|
||||
print()
|
||||
print(f"Dungeon generated with {len(game.rooms)} rooms")
|
||||
print(f"Enemies spawned: {len(game.enemies)}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# Run the game
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
else:
|
||||
# McRogueFace runs game.py directly, not as __main__
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue