draft tutorial revisions

This commit is contained in:
John McCardle 2026-01-03 11:01:10 -05:00
commit 48359b5a48
70 changed files with 6216 additions and 28 deletions

138
docs/templates/roguelike/constants.py vendored Normal file
View 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
View 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
View 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
View 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()