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

289
docs/templates/complete/ai.py vendored Normal file
View file

@ -0,0 +1,289 @@
"""
ai.py - Enemy AI System for McRogueFace Roguelike
Simple AI behaviors for enemies: chase player when visible, wander otherwise.
Uses A* pathfinding via entity.path_to() for movement.
"""
from typing import List, Tuple, Optional, TYPE_CHECKING
import random
from entities import Enemy, Player, Actor
from combat import melee_attack, CombatResult
if TYPE_CHECKING:
from dungeon import Dungeon
class AIBehavior:
"""Base class for AI behaviors."""
def take_turn(self, enemy: Enemy, player: Player, dungeon: 'Dungeon',
enemies: List[Enemy]) -> Optional[CombatResult]:
"""
Execute one turn of AI behavior.
Args:
enemy: The enemy taking a turn
player: The player to potentially chase/attack
dungeon: The dungeon map
enemies: List of all enemies (for collision avoidance)
Returns:
CombatResult if combat occurred, None otherwise
"""
raise NotImplementedError
class BasicChaseAI(AIBehavior):
"""
Simple chase AI: If player is visible, move toward them.
If adjacent, attack. Otherwise, stand still or wander.
"""
def __init__(self, sight_range: int = 8):
"""
Args:
sight_range: How far the enemy can see
"""
self.sight_range = sight_range
def can_see_player(self, enemy: Enemy, player: Player,
dungeon: 'Dungeon') -> bool:
"""Check if enemy can see the player."""
# Simple distance check combined with line of sight
distance = enemy.distance_to(player)
if distance > self.sight_range:
return False
# Check line of sight using Bresenham's line
return self._has_line_of_sight(enemy.x, enemy.y, player.x, player.y, dungeon)
def _has_line_of_sight(self, x1: int, y1: int, x2: int, y2: int,
dungeon: 'Dungeon') -> bool:
"""
Check if there's a clear line of sight between two points.
Uses Bresenham's line algorithm.
"""
dx = abs(x2 - x1)
dy = abs(y2 - y1)
x, y = x1, y1
sx = 1 if x1 < x2 else -1
sy = 1 if y1 < y2 else -1
if dx > dy:
err = dx / 2
while x != x2:
if not dungeon.is_transparent(x, y):
return False
err -= dy
if err < 0:
y += sy
err += dx
x += sx
else:
err = dy / 2
while y != y2:
if not dungeon.is_transparent(x, y):
return False
err -= dx
if err < 0:
x += sx
err += dy
y += sy
return True
def get_path_to_player(self, enemy: Enemy, player: Player) -> List[Tuple[int, int]]:
"""
Get a path from enemy to player using A* pathfinding.
Uses the entity's built-in path_to method.
"""
try:
path = enemy.entity.path_to(player.x, player.y)
# Convert path to list of tuples
return [(int(p[0]), int(p[1])) for p in path] if path else []
except (AttributeError, TypeError):
# Fallback: simple direction-based movement
return []
def is_position_blocked(self, x: int, y: int, dungeon: 'Dungeon',
enemies: List[Enemy], player: Player) -> bool:
"""Check if a position is blocked by terrain or another actor."""
# Check terrain
if not dungeon.is_walkable(x, y):
return True
# Check player position
if player.x == x and player.y == y:
return True
# Check other enemies
for other in enemies:
if other.is_alive and other.x == x and other.y == y:
return True
return False
def move_toward(self, enemy: Enemy, target_x: int, target_y: int,
dungeon: 'Dungeon', enemies: List[Enemy],
player: Player) -> bool:
"""
Move one step toward the target position.
Returns True if movement occurred, False otherwise.
"""
# Try pathfinding first
path = self.get_path_to_player(enemy, player)
if path and len(path) > 1:
# First element is current position, second is next step
next_x, next_y = path[1]
else:
# Fallback: move in the general direction
dx = 0
dy = 0
if target_x < enemy.x:
dx = -1
elif target_x > enemy.x:
dx = 1
if target_y < enemy.y:
dy = -1
elif target_y > enemy.y:
dy = 1
next_x = enemy.x + dx
next_y = enemy.y + dy
# Check if the position is blocked
if not self.is_position_blocked(next_x, next_y, dungeon, enemies, player):
enemy.move_to(next_x, next_y)
return True
# Try moving in just one axis
if next_x != enemy.x:
if not self.is_position_blocked(next_x, enemy.y, dungeon, enemies, player):
enemy.move_to(next_x, enemy.y)
return True
if next_y != enemy.y:
if not self.is_position_blocked(enemy.x, next_y, dungeon, enemies, player):
enemy.move_to(enemy.x, next_y)
return True
return False
def take_turn(self, enemy: Enemy, player: Player, dungeon: 'Dungeon',
enemies: List[Enemy]) -> Optional[CombatResult]:
"""Execute the enemy's turn."""
if not enemy.is_alive:
return None
# Check if adjacent to player (can attack)
if enemy.distance_to(player) == 1:
return melee_attack(enemy, player)
# Check if can see player
if self.can_see_player(enemy, player, dungeon):
# Move toward player
self.move_toward(enemy, player.x, player.y, dungeon, enemies, player)
return None
class WanderingAI(BasicChaseAI):
"""
AI that wanders randomly when it can't see the player.
More active than BasicChaseAI.
"""
def __init__(self, sight_range: int = 8, wander_chance: float = 0.3):
"""
Args:
sight_range: How far the enemy can see
wander_chance: Probability of wandering each turn (0.0 to 1.0)
"""
super().__init__(sight_range)
self.wander_chance = wander_chance
def wander(self, enemy: Enemy, dungeon: 'Dungeon',
enemies: List[Enemy], player: Player) -> bool:
"""
Move in a random direction.
Returns True if movement occurred.
"""
# Random direction
directions = [
(-1, 0), (1, 0), (0, -1), (0, 1), # Cardinal
(-1, -1), (1, -1), (-1, 1), (1, 1) # Diagonal
]
random.shuffle(directions)
for dx, dy in directions:
new_x = enemy.x + dx
new_y = enemy.y + dy
if not self.is_position_blocked(new_x, new_y, dungeon, enemies, player):
enemy.move_to(new_x, new_y)
return True
return False
def take_turn(self, enemy: Enemy, player: Player, dungeon: 'Dungeon',
enemies: List[Enemy]) -> Optional[CombatResult]:
"""Execute the enemy's turn with wandering behavior."""
if not enemy.is_alive:
return None
# Check if adjacent to player (can attack)
if enemy.distance_to(player) == 1:
return melee_attack(enemy, player)
# Check if can see player
if self.can_see_player(enemy, player, dungeon):
# Chase player
self.move_toward(enemy, player.x, player.y, dungeon, enemies, player)
else:
# Wander randomly
if random.random() < self.wander_chance:
self.wander(enemy, dungeon, enemies, player)
return None
# Default AI instance
default_ai = WanderingAI(sight_range=8, wander_chance=0.3)
def process_enemy_turns(enemies: List[Enemy], player: Player,
dungeon: 'Dungeon',
ai: AIBehavior = None) -> List[CombatResult]:
"""
Process turns for all enemies.
Args:
enemies: List of all enemies
player: The player
dungeon: The dungeon map
ai: AI behavior to use (defaults to WanderingAI)
Returns:
List of combat results from this round of enemy actions
"""
if ai is None:
ai = default_ai
results = []
for enemy in enemies:
if enemy.is_alive:
result = ai.take_turn(enemy, player, dungeon, enemies)
if result:
results.append(result)
return results

187
docs/templates/complete/combat.py vendored Normal file
View file

@ -0,0 +1,187 @@
"""
combat.py - Combat System for McRogueFace Roguelike
Handles attack resolution, damage calculation, and combat outcomes.
"""
from dataclasses import dataclass
from typing import Tuple, Optional
import random
from entities import Actor, Player, Enemy
from constants import (
MSG_PLAYER_ATTACK, MSG_PLAYER_KILL, MSG_PLAYER_MISS,
MSG_ENEMY_ATTACK, MSG_ENEMY_MISS
)
@dataclass
class CombatResult:
"""
Result of a combat action.
Attributes:
attacker: The attacking actor
defender: The defending actor
damage: Damage dealt (after defense)
killed: Whether the defender was killed
message: Human-readable result message
message_color: Color tuple for the message
"""
attacker: Actor
defender: Actor
damage: int
killed: bool
message: str
message_color: Tuple[int, int, int, int]
def calculate_damage(attack: int, defense: int, variance: float = 0.2) -> int:
"""
Calculate damage with some randomness.
Args:
attack: Attacker's attack power
defense: Defender's defense value
variance: Random variance as percentage (0.2 = +/-20%)
Returns:
Final damage amount (minimum 0)
"""
# Base damage is attack vs defense
base_damage = attack - defense
# Add some variance
if base_damage > 0:
variance_amount = int(base_damage * variance)
damage = base_damage + random.randint(-variance_amount, variance_amount)
else:
# Small chance to do 1 damage even with high defense
damage = 1 if random.random() < 0.1 else 0
return max(0, damage)
def attack(attacker: Actor, defender: Actor) -> CombatResult:
"""
Perform an attack from one actor to another.
Args:
attacker: The actor making the attack
defender: The actor being attacked
Returns:
CombatResult with outcome details
"""
# Calculate damage
damage = calculate_damage(
attacker.fighter.attack,
defender.fighter.defense
)
# Apply damage
actual_damage = defender.fighter.take_damage(damage + defender.fighter.defense)
# Note: take_damage applies defense internally, so we add it back
# Actually, we calculated damage already reduced by defense, so just apply it:
defender.fighter.hp = max(0, defender.fighter.hp - damage + actual_damage)
# Simplified: just use take_damage properly
# Reset and do it right:
# Apply raw damage (defense already calculated)
defender.fighter.hp = max(0, defender.fighter.hp - damage)
killed = not defender.is_alive
# Generate message based on attacker/defender types
if isinstance(attacker, Player):
if killed:
message = MSG_PLAYER_KILL % defender.name
color = (255, 255, 100, 255) # Yellow for kills
elif damage > 0:
message = MSG_PLAYER_ATTACK % (defender.name, damage)
color = (255, 255, 255, 255) # White for hits
else:
message = MSG_PLAYER_MISS % defender.name
color = (150, 150, 150, 255) # Gray for misses
else:
if damage > 0:
message = MSG_ENEMY_ATTACK % (attacker.name, damage)
color = (255, 100, 100, 255) # Red for enemy hits
else:
message = MSG_ENEMY_MISS % attacker.name
color = (150, 150, 150, 255) # Gray for misses
return CombatResult(
attacker=attacker,
defender=defender,
damage=damage,
killed=killed,
message=message,
message_color=color
)
def melee_attack(attacker: Actor, defender: Actor) -> CombatResult:
"""
Perform a melee attack (bump attack).
This is the standard roguelike bump-to-attack.
Args:
attacker: The actor making the attack
defender: The actor being attacked
Returns:
CombatResult with outcome details
"""
return attack(attacker, defender)
def try_attack(attacker: Actor, target_x: int, target_y: int,
enemies: list, player: Optional[Player] = None) -> Optional[CombatResult]:
"""
Attempt to attack whatever is at the target position.
Args:
attacker: The actor making the attack
target_x: X coordinate to attack
target_y: Y coordinate to attack
enemies: List of Enemy actors
player: The player (if attacker is an enemy)
Returns:
CombatResult if something was attacked, None otherwise
"""
# Check if player is attacking
if isinstance(attacker, Player):
# Look for enemy at position
for enemy in enemies:
if enemy.is_alive and enemy.x == target_x and enemy.y == target_y:
return melee_attack(attacker, enemy)
else:
# Enemy attacking - check if player is at position
if player and player.x == target_x and player.y == target_y:
return melee_attack(attacker, player)
return None
def process_kill(attacker: Actor, defender: Actor) -> int:
"""
Process the aftermath of killing an enemy.
Args:
attacker: The actor that made the kill
defender: The actor that was killed
Returns:
XP gained (if attacker is player and defender is enemy)
"""
xp_gained = 0
if isinstance(attacker, Player) and isinstance(defender, Enemy):
xp_gained = defender.xp_reward
attacker.gain_xp(xp_gained)
# Remove the dead actor from the grid
defender.remove()
return xp_gained

210
docs/templates/complete/constants.py vendored Normal file
View file

@ -0,0 +1,210 @@
"""
constants.py - Game Constants for McRogueFace Complete Roguelike Template
All configuration values in one place for easy tweaking.
"""
# =============================================================================
# WINDOW AND DISPLAY
# =============================================================================
SCREEN_WIDTH = 1024
SCREEN_HEIGHT = 768
# Grid display area (where the dungeon is rendered)
GRID_X = 0
GRID_Y = 0
GRID_WIDTH = 800
GRID_HEIGHT = 600
# Tile dimensions (must match your texture)
TILE_WIDTH = 16
TILE_HEIGHT = 16
# =============================================================================
# DUNGEON GENERATION
# =============================================================================
# Size of the dungeon in tiles
DUNGEON_WIDTH = 80
DUNGEON_HEIGHT = 45
# Room size constraints
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 15
# Enemy spawning per room
MAX_ENEMIES_PER_ROOM = 3
MIN_ENEMIES_PER_ROOM = 0
# =============================================================================
# SPRITE INDICES (for kenney_tinydungeon.png - 16x16 tiles)
# Adjust these if using a different tileset
# =============================================================================
# Terrain
SPRITE_FLOOR = 48 # Dungeon floor
SPRITE_WALL = 33 # Wall tile
SPRITE_STAIRS_DOWN = 50 # Stairs going down
SPRITE_DOOR = 49 # Door tile
# Player sprites
SPRITE_PLAYER = 84 # Player character (knight)
# Enemy sprites
SPRITE_GOBLIN = 111 # Goblin enemy
SPRITE_ORC = 112 # Orc enemy
SPRITE_TROLL = 116 # Troll enemy
# Items (for future expansion)
SPRITE_POTION = 89 # Health potion
SPRITE_CHEST = 91 # Treasure chest
# =============================================================================
# COLORS (R, G, B, A)
# =============================================================================
# Map colors
COLOR_DARK_WALL = (50, 50, 100, 255)
COLOR_DARK_FLOOR = (30, 30, 50, 255)
COLOR_LIGHT_WALL = (100, 100, 150, 255)
COLOR_LIGHT_FLOOR = (80, 80, 100, 255)
# FOV overlay colors
COLOR_FOG = (0, 0, 0, 200) # Unexplored areas
COLOR_REMEMBERED = (0, 0, 0, 128) # Seen but not visible
COLOR_VISIBLE = (0, 0, 0, 0) # Currently visible (transparent)
# UI Colors
COLOR_UI_BG = (20, 20, 30, 230)
COLOR_UI_BORDER = (80, 80, 120, 255)
COLOR_TEXT = (255, 255, 255, 255)
COLOR_TEXT_HIGHLIGHT = (255, 255, 100, 255)
# Health bar colors
COLOR_HP_BAR_BG = (80, 0, 0, 255)
COLOR_HP_BAR_FILL = (0, 180, 0, 255)
COLOR_HP_BAR_WARNING = (180, 180, 0, 255)
COLOR_HP_BAR_CRITICAL = (180, 0, 0, 255)
# Message log colors
COLOR_MSG_DEFAULT = (255, 255, 255, 255)
COLOR_MSG_DAMAGE = (255, 100, 100, 255)
COLOR_MSG_HEAL = (100, 255, 100, 255)
COLOR_MSG_INFO = (100, 100, 255, 255)
COLOR_MSG_IMPORTANT = (255, 255, 100, 255)
# =============================================================================
# PLAYER STATS
# =============================================================================
PLAYER_START_HP = 30
PLAYER_START_ATTACK = 5
PLAYER_START_DEFENSE = 2
# =============================================================================
# ENEMY STATS
# Each enemy type: (hp, attack, defense, xp_reward, name)
# =============================================================================
ENEMY_STATS = {
'goblin': {
'hp': 10,
'attack': 3,
'defense': 0,
'xp': 35,
'sprite': SPRITE_GOBLIN,
'name': 'Goblin'
},
'orc': {
'hp': 16,
'attack': 4,
'defense': 1,
'xp': 50,
'sprite': SPRITE_ORC,
'name': 'Orc'
},
'troll': {
'hp': 24,
'attack': 6,
'defense': 2,
'xp': 100,
'sprite': SPRITE_TROLL,
'name': 'Troll'
}
}
# Enemy spawn weights per dungeon level
# Format: {level: [(enemy_type, weight), ...]}
# Higher weight = more likely to spawn
ENEMY_SPAWN_WEIGHTS = {
1: [('goblin', 100)],
2: [('goblin', 80), ('orc', 20)],
3: [('goblin', 60), ('orc', 40)],
4: [('goblin', 40), ('orc', 50), ('troll', 10)],
5: [('goblin', 20), ('orc', 50), ('troll', 30)],
}
# Default weights for levels beyond those defined
DEFAULT_SPAWN_WEIGHTS = [('goblin', 10), ('orc', 50), ('troll', 40)]
# =============================================================================
# FOV (Field of View) SETTINGS
# =============================================================================
FOV_RADIUS = 8 # How far the player can see
FOV_LIGHT_WALLS = True # Whether walls at FOV edge are visible
# =============================================================================
# INPUT KEYS
# Key names as returned by McRogueFace keypressScene
# =============================================================================
KEY_UP = ['Up', 'W', 'Numpad8']
KEY_DOWN = ['Down', 'S', 'Numpad2']
KEY_LEFT = ['Left', 'A', 'Numpad4']
KEY_RIGHT = ['Right', 'D', 'Numpad6']
# Diagonal movement (numpad)
KEY_UP_LEFT = ['Numpad7']
KEY_UP_RIGHT = ['Numpad9']
KEY_DOWN_LEFT = ['Numpad1']
KEY_DOWN_RIGHT = ['Numpad3']
# Actions
KEY_WAIT = ['Period', 'Numpad5'] # Skip turn
KEY_DESCEND = ['Greater', 'Space'] # Go down stairs (> key or space)
# =============================================================================
# GAME MESSAGES
# =============================================================================
MSG_WELCOME = "Welcome to the dungeon! Find the stairs to descend deeper."
MSG_DESCEND = "You descend the stairs to level %d..."
MSG_PLAYER_ATTACK = "You attack the %s for %d damage!"
MSG_PLAYER_KILL = "You have slain the %s!"
MSG_PLAYER_MISS = "You attack the %s but do no damage."
MSG_ENEMY_ATTACK = "The %s attacks you for %d damage!"
MSG_ENEMY_MISS = "The %s attacks you but does no damage."
MSG_BLOCKED = "You can't move there!"
MSG_STAIRS = "You see stairs leading down here. Press > or Space to descend."
MSG_DEATH = "You have died! Press R to restart."
MSG_NO_STAIRS = "There are no stairs here."
# =============================================================================
# UI LAYOUT
# =============================================================================
# Health bar
HP_BAR_X = 10
HP_BAR_Y = 620
HP_BAR_WIDTH = 200
HP_BAR_HEIGHT = 24
# Message log
MSG_LOG_X = 10
MSG_LOG_Y = 660
MSG_LOG_WIDTH = 780
MSG_LOG_HEIGHT = 100
MSG_LOG_MAX_LINES = 5
# Dungeon level display
LEVEL_DISPLAY_X = 700
LEVEL_DISPLAY_Y = 620
# =============================================================================
# ASSET PATHS
# =============================================================================
TEXTURE_PATH = "assets/kenney_tinydungeon.png"
FONT_PATH = "assets/JetbrainsMono.ttf"

298
docs/templates/complete/dungeon.py vendored Normal file
View file

@ -0,0 +1,298 @@
"""
dungeon.py - Procedural Dungeon Generation for McRogueFace
Generates a roguelike dungeon with rooms connected by corridors.
Includes stairs placement for multi-level progression.
"""
import random
from dataclasses import dataclass
from typing import List, Tuple, Optional
from constants import (
DUNGEON_WIDTH, DUNGEON_HEIGHT,
ROOM_MIN_SIZE, ROOM_MAX_SIZE, MAX_ROOMS,
SPRITE_FLOOR, SPRITE_WALL, SPRITE_STAIRS_DOWN,
MAX_ENEMIES_PER_ROOM, MIN_ENEMIES_PER_ROOM,
ENEMY_SPAWN_WEIGHTS, DEFAULT_SPAWN_WEIGHTS
)
@dataclass
class Rect:
"""A rectangle representing a room in the dungeon."""
x: int
y: int
width: int
height: int
@property
def x2(self) -> int:
return self.x + self.width
@property
def y2(self) -> int:
return self.y + self.height
@property
def center(self) -> Tuple[int, int]:
"""Return the center coordinates of this room."""
center_x = (self.x + self.x2) // 2
center_y = (self.y + self.y2) // 2
return center_x, center_y
def intersects(self, other: 'Rect') -> bool:
"""Check if this room overlaps with another (with 1 tile buffer)."""
return (self.x <= other.x2 + 1 and self.x2 + 1 >= other.x and
self.y <= other.y2 + 1 and self.y2 + 1 >= other.y)
def inner(self) -> Tuple[int, int, int, int]:
"""Return the inner area of the room (excluding walls)."""
return self.x + 1, self.y + 1, self.width - 2, self.height - 2
class Tile:
"""Represents a single tile in the dungeon."""
def __init__(self, walkable: bool = False, transparent: bool = False,
sprite: int = SPRITE_WALL):
self.walkable = walkable
self.transparent = transparent
self.sprite = sprite
self.explored = False
self.visible = False
class Dungeon:
"""
The dungeon map with rooms, corridors, and tile data.
Attributes:
width: Width of the dungeon in tiles
height: Height of the dungeon in tiles
level: Current dungeon depth
tiles: 2D array of Tile objects
rooms: List of rooms (Rect objects)
player_start: Starting position for the player
stairs_pos: Position of the stairs down
"""
def __init__(self, width: int = DUNGEON_WIDTH, height: int = DUNGEON_HEIGHT,
level: int = 1):
self.width = width
self.height = height
self.level = level
self.tiles: List[List[Tile]] = []
self.rooms: List[Rect] = []
self.player_start: Tuple[int, int] = (0, 0)
self.stairs_pos: Tuple[int, int] = (0, 0)
# Initialize all tiles as walls
self._init_tiles()
def _init_tiles(self) -> None:
"""Fill the dungeon with wall tiles."""
self.tiles = [
[Tile(walkable=False, transparent=False, sprite=SPRITE_WALL)
for _ in range(self.height)]
for _ in range(self.width)
]
def in_bounds(self, x: int, y: int) -> bool:
"""Check if coordinates are within dungeon bounds."""
return 0 <= x < self.width and 0 <= y < self.height
def is_walkable(self, x: int, y: int) -> bool:
"""Check if a tile can be walked on."""
if not self.in_bounds(x, y):
return False
return self.tiles[x][y].walkable
def is_transparent(self, x: int, y: int) -> bool:
"""Check if a tile allows light to pass through."""
if not self.in_bounds(x, y):
return False
return self.tiles[x][y].transparent
def get_tile(self, x: int, y: int) -> Optional[Tile]:
"""Get the tile at the given position."""
if not self.in_bounds(x, y):
return None
return self.tiles[x][y]
def set_tile(self, x: int, y: int, walkable: bool, transparent: bool,
sprite: int) -> None:
"""Set properties of a tile."""
if self.in_bounds(x, y):
tile = self.tiles[x][y]
tile.walkable = walkable
tile.transparent = transparent
tile.sprite = sprite
def carve_room(self, room: Rect) -> None:
"""Carve out a room in the dungeon (make tiles walkable)."""
inner_x, inner_y, inner_w, inner_h = room.inner()
for x in range(inner_x, inner_x + inner_w):
for y in range(inner_y, inner_y + inner_h):
self.set_tile(x, y, walkable=True, transparent=True,
sprite=SPRITE_FLOOR)
def carve_tunnel_h(self, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite=SPRITE_FLOOR)
def carve_tunnel_v(self, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite=SPRITE_FLOOR)
def connect_rooms(self, room1: Rect, room2: Rect) -> None:
"""Connect two rooms with an L-shaped corridor."""
x1, y1 = room1.center
x2, y2 = room2.center
# Randomly choose to go horizontal then vertical, or vice versa
if random.random() < 0.5:
self.carve_tunnel_h(x1, x2, y1)
self.carve_tunnel_v(y1, y2, x2)
else:
self.carve_tunnel_v(y1, y2, x1)
self.carve_tunnel_h(x1, x2, y2)
def place_stairs(self) -> None:
"""Place stairs in the last room."""
if self.rooms:
# Stairs go in the center of the last room
self.stairs_pos = self.rooms[-1].center
x, y = self.stairs_pos
self.set_tile(x, y, walkable=True, transparent=True,
sprite=SPRITE_STAIRS_DOWN)
def generate(self) -> None:
"""Generate the dungeon using BSP-style room placement."""
self._init_tiles()
self.rooms.clear()
for _ in range(MAX_ROOMS):
# Random room dimensions
w = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
h = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
# Random position (ensure room fits in dungeon)
x = random.randint(1, self.width - w - 1)
y = random.randint(1, self.height - h - 1)
new_room = Rect(x, y, w, h)
# Check for intersections with existing rooms
if any(new_room.intersects(other) for other in self.rooms):
continue
# Room is valid - carve it out
self.carve_room(new_room)
if self.rooms:
# Connect to previous room
self.connect_rooms(self.rooms[-1], new_room)
else:
# First room - player starts here
self.player_start = new_room.center
self.rooms.append(new_room)
# Place stairs in the last room
self.place_stairs()
def get_spawn_positions(self) -> List[Tuple[int, int]]:
"""
Get valid spawn positions for enemies.
Returns positions from all rooms except the first (player start).
"""
positions = []
for room in self.rooms[1:]: # Skip first room (player start)
inner_x, inner_y, inner_w, inner_h = room.inner()
for x in range(inner_x, inner_x + inner_w):
for y in range(inner_y, inner_y + inner_h):
# Don't spawn on stairs
if (x, y) != self.stairs_pos:
positions.append((x, y))
return positions
def get_enemy_spawns(self) -> List[Tuple[str, int, int]]:
"""
Determine which enemies to spawn and where.
Returns list of (enemy_type, x, y) tuples.
"""
spawns = []
# Get spawn weights for this level
weights = ENEMY_SPAWN_WEIGHTS.get(self.level, DEFAULT_SPAWN_WEIGHTS)
# Create weighted list for random selection
enemy_types = []
for enemy_type, weight in weights:
enemy_types.extend([enemy_type] * weight)
# Spawn enemies in each room (except the first)
for room in self.rooms[1:]:
num_enemies = random.randint(MIN_ENEMIES_PER_ROOM, MAX_ENEMIES_PER_ROOM)
# Scale up enemies slightly with dungeon level
num_enemies = min(num_enemies + (self.level - 1) // 2, MAX_ENEMIES_PER_ROOM + 2)
inner_x, inner_y, inner_w, inner_h = room.inner()
used_positions = set()
for _ in range(num_enemies):
# Find an unused position
attempts = 0
while attempts < 20:
x = random.randint(inner_x, inner_x + inner_w - 1)
y = random.randint(inner_y, inner_y + inner_h - 1)
if (x, y) not in used_positions and (x, y) != self.stairs_pos:
enemy_type = random.choice(enemy_types)
spawns.append((enemy_type, x, y))
used_positions.add((x, y))
break
attempts += 1
return spawns
def apply_to_grid(self, grid) -> None:
"""
Apply the dungeon data to a McRogueFace Grid object.
Args:
grid: A mcrfpy.Grid object to update
"""
for x in range(self.width):
for y in range(self.height):
tile = self.tiles[x][y]
point = grid.at(x, y)
point.tilesprite = tile.sprite
point.walkable = tile.walkable
point.transparent = tile.transparent
def generate_dungeon(level: int = 1) -> Dungeon:
"""
Convenience function to generate a new dungeon.
Args:
level: The dungeon depth (affects enemy spawns)
Returns:
A fully generated Dungeon object
"""
dungeon = Dungeon(level=level)
dungeon.generate()
return dungeon

319
docs/templates/complete/entities.py vendored Normal file
View file

@ -0,0 +1,319 @@
"""
entities.py - Player and Enemy Entity Definitions
Defines the game actors with stats, rendering, and basic behaviors.
Uses composition with McRogueFace Entity objects for rendering.
"""
from dataclasses import dataclass, field
from typing import Optional, List, Tuple, TYPE_CHECKING
import mcrfpy
from constants import (
PLAYER_START_HP, PLAYER_START_ATTACK, PLAYER_START_DEFENSE,
SPRITE_PLAYER, ENEMY_STATS, FOV_RADIUS
)
if TYPE_CHECKING:
from dungeon import Dungeon
@dataclass
class Fighter:
"""
Combat statistics component for entities that can fight.
Attributes:
hp: Current hit points
max_hp: Maximum hit points
attack: Attack power
defense: Damage reduction
"""
hp: int
max_hp: int
attack: int
defense: int
@property
def is_alive(self) -> bool:
"""Check if this fighter is still alive."""
return self.hp > 0
@property
def hp_percent(self) -> float:
"""Return HP as a percentage (0.0 to 1.0)."""
if self.max_hp <= 0:
return 0.0
return self.hp / self.max_hp
def heal(self, amount: int) -> int:
"""
Heal by the given amount, up to max_hp.
Returns:
The actual amount healed.
"""
old_hp = self.hp
self.hp = min(self.hp + amount, self.max_hp)
return self.hp - old_hp
def take_damage(self, amount: int) -> int:
"""
Take damage, reduced by defense.
Args:
amount: Raw damage before defense calculation
Returns:
The actual damage taken after defense.
"""
# Defense reduces damage, minimum 0
actual_damage = max(0, amount - self.defense)
self.hp = max(0, self.hp - actual_damage)
return actual_damage
class Actor:
"""
Base class for all game actors (player and enemies).
Wraps a McRogueFace Entity and adds game logic.
"""
def __init__(self, x: int, y: int, sprite: int, name: str,
texture: mcrfpy.Texture, grid: mcrfpy.Grid,
fighter: Fighter):
"""
Create a new actor.
Args:
x: Starting X position
y: Starting Y position
sprite: Sprite index for rendering
name: Display name of this actor
texture: Texture for the entity sprite
grid: Grid to add the entity to
fighter: Combat statistics
"""
self.name = name
self.fighter = fighter
self.grid = grid
self._x = x
self._y = y
# Create the McRogueFace entity
self.entity = mcrfpy.Entity((x, y), texture, sprite)
grid.entities.append(self.entity)
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int) -> None:
self._x = value
self.entity.pos = (value, self._y)
@property
def y(self) -> int:
return self._y
@y.setter
def y(self, value: int) -> None:
self._y = value
self.entity.pos = (self._x, value)
@property
def pos(self) -> Tuple[int, int]:
return (self._x, self._y)
@pos.setter
def pos(self, value: Tuple[int, int]) -> None:
self._x, self._y = value
self.entity.pos = value
@property
def is_alive(self) -> bool:
return self.fighter.is_alive
def move(self, dx: int, dy: int) -> None:
"""Move by the given delta."""
self.x += dx
self.y += dy
def move_to(self, x: int, y: int) -> None:
"""Move to an absolute position."""
self.pos = (x, y)
def distance_to(self, other: 'Actor') -> int:
"""Calculate Manhattan distance to another actor."""
return abs(self.x - other.x) + abs(self.y - other.y)
def remove(self) -> None:
"""Remove this actor's entity from the grid."""
try:
idx = self.entity.index()
self.grid.entities.remove(idx)
except (ValueError, RuntimeError):
pass # Already removed
class Player(Actor):
"""
The player character with additional player-specific functionality.
"""
def __init__(self, x: int, y: int, texture: mcrfpy.Texture,
grid: mcrfpy.Grid):
fighter = Fighter(
hp=PLAYER_START_HP,
max_hp=PLAYER_START_HP,
attack=PLAYER_START_ATTACK,
defense=PLAYER_START_DEFENSE
)
super().__init__(
x=x, y=y,
sprite=SPRITE_PLAYER,
name="Player",
texture=texture,
grid=grid,
fighter=fighter
)
self.xp = 0
self.level = 1
self.dungeon_level = 1
def gain_xp(self, amount: int) -> bool:
"""
Gain experience points.
Args:
amount: XP to gain
Returns:
True if the player leveled up
"""
self.xp += amount
xp_to_level = self.xp_for_next_level
if self.xp >= xp_to_level:
self.level_up()
return True
return False
@property
def xp_for_next_level(self) -> int:
"""XP required for the next level."""
return self.level * 100
def level_up(self) -> None:
"""Level up the player, improving stats."""
self.level += 1
# Improve stats
hp_increase = 5
attack_increase = 1
defense_increase = 1 if self.level % 3 == 0 else 0
self.fighter.max_hp += hp_increase
self.fighter.hp += hp_increase # Heal the increase amount
self.fighter.attack += attack_increase
self.fighter.defense += defense_increase
def update_fov(self, dungeon: 'Dungeon') -> None:
"""
Update field of view based on player position.
Uses entity.update_visibility() for TCOD FOV calculation.
"""
# Update the entity's visibility data
self.entity.update_visibility()
# Apply FOV to dungeon tiles
for x in range(dungeon.width):
for y in range(dungeon.height):
state = self.entity.at(x, y)
tile = dungeon.get_tile(x, y)
if tile:
tile.visible = state.visible
if state.visible:
tile.explored = True
class Enemy(Actor):
"""
An enemy actor with AI behavior.
"""
def __init__(self, x: int, y: int, enemy_type: str,
texture: mcrfpy.Texture, grid: mcrfpy.Grid):
"""
Create a new enemy.
Args:
x: Starting X position
y: Starting Y position
enemy_type: Key into ENEMY_STATS dictionary
texture: Texture for the entity sprite
grid: Grid to add the entity to
"""
stats = ENEMY_STATS.get(enemy_type, ENEMY_STATS['goblin'])
fighter = Fighter(
hp=stats['hp'],
max_hp=stats['hp'],
attack=stats['attack'],
defense=stats['defense']
)
super().__init__(
x=x, y=y,
sprite=stats['sprite'],
name=stats['name'],
texture=texture,
grid=grid,
fighter=fighter
)
self.enemy_type = enemy_type
self.xp_reward = stats['xp']
# AI state
self.target: Optional[Actor] = None
self.path: List[Tuple[int, int]] = []
def create_player(x: int, y: int, texture: mcrfpy.Texture,
grid: mcrfpy.Grid) -> Player:
"""
Factory function to create the player.
Args:
x: Starting X position
y: Starting Y position
texture: Texture for player sprite
grid: Grid to add player to
Returns:
A new Player instance
"""
return Player(x, y, texture, grid)
def create_enemy(x: int, y: int, enemy_type: str,
texture: mcrfpy.Texture, grid: mcrfpy.Grid) -> Enemy:
"""
Factory function to create an enemy.
Args:
x: Starting X position
y: Starting Y position
enemy_type: Type of enemy ('goblin', 'orc', 'troll')
texture: Texture for enemy sprite
grid: Grid to add enemy to
Returns:
A new Enemy instance
"""
return Enemy(x, y, enemy_type, texture, grid)

313
docs/templates/complete/game.py vendored Normal file
View file

@ -0,0 +1,313 @@
"""
game.py - Main Entry Point for McRogueFace Complete Roguelike Template
This is the main game file that ties everything together:
- Scene setup
- Input handling
- Game loop
- Level transitions
To run: Copy this template to your McRogueFace scripts/ directory
and rename to game.py (or import from game.py).
"""
import mcrfpy
from typing import List, Optional
# Import game modules
from constants import (
SCREEN_WIDTH, SCREEN_HEIGHT,
GRID_X, GRID_Y, GRID_WIDTH, GRID_HEIGHT,
DUNGEON_WIDTH, DUNGEON_HEIGHT,
TEXTURE_PATH, FONT_PATH,
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
KEY_UP_LEFT, KEY_UP_RIGHT, KEY_DOWN_LEFT, KEY_DOWN_RIGHT,
KEY_WAIT, KEY_DESCEND,
MSG_WELCOME, MSG_DESCEND, MSG_BLOCKED, MSG_STAIRS, MSG_DEATH, MSG_NO_STAIRS,
FOV_RADIUS, COLOR_FOG, COLOR_REMEMBERED, COLOR_VISIBLE
)
from dungeon import Dungeon, generate_dungeon
from entities import Player, Enemy, create_player, create_enemy
from turns import TurnManager, GameState
from ui import GameUI, DeathScreen
class Game:
"""
Main game class that manages the complete roguelike experience.
"""
def __init__(self):
"""Initialize the game."""
# Load resources
self.texture = mcrfpy.Texture(TEXTURE_PATH, 16, 16)
self.font = mcrfpy.Font(FONT_PATH)
# Create scene
mcrfpy.createScene("game")
self.ui_collection = mcrfpy.sceneUI("game")
# Create grid
self.grid = mcrfpy.Grid(
DUNGEON_WIDTH, DUNGEON_HEIGHT,
self.texture,
GRID_X, GRID_Y,
GRID_WIDTH, GRID_HEIGHT
)
self.ui_collection.append(self.grid)
# Game state
self.dungeon: Optional[Dungeon] = None
self.player: Optional[Player] = None
self.enemies: List[Enemy] = []
self.turn_manager: Optional[TurnManager] = None
self.current_level = 1
# UI
self.game_ui = GameUI(self.font)
self.game_ui.add_to_scene(self.ui_collection)
self.death_screen: Optional[DeathScreen] = None
self.game_over = False
# Set up input handling
mcrfpy.keypressScene(self.handle_keypress)
# Start the game
self.new_game()
# Switch to game scene
mcrfpy.setScene("game")
def new_game(self) -> None:
"""Start a new game from level 1."""
self.current_level = 1
self.game_over = False
# Clear any death screen
if self.death_screen:
self.death_screen.remove_from_scene(self.ui_collection)
self.death_screen = None
# Generate first level
self.generate_level()
# Welcome message
self.game_ui.clear_messages()
self.game_ui.add_message(MSG_WELCOME, (255, 255, 100, 255))
def generate_level(self) -> None:
"""Generate a new dungeon level."""
# Clear existing entities from grid
while len(self.grid.entities) > 0:
self.grid.entities.remove(0)
self.enemies.clear()
# Generate dungeon
self.dungeon = generate_dungeon(self.current_level)
self.dungeon.apply_to_grid(self.grid)
# Create player at start position
start_x, start_y = self.dungeon.player_start
self.player = create_player(start_x, start_y, self.texture, self.grid)
self.player.dungeon_level = self.current_level
# Spawn enemies
enemy_spawns = self.dungeon.get_enemy_spawns()
for enemy_type, x, y in enemy_spawns:
enemy = create_enemy(x, y, enemy_type, self.texture, self.grid)
self.enemies.append(enemy)
# Set up turn manager
self.turn_manager = TurnManager(self.player, self.enemies, self.dungeon)
self.turn_manager.on_message = self.game_ui.add_message
self.turn_manager.on_player_death = self.on_player_death
# Update FOV
self.update_fov()
# Center camera on player
self.center_camera()
# Update UI
self.game_ui.update_level(self.current_level)
self.update_ui()
def descend(self) -> None:
"""Go down to the next dungeon level."""
# Check if player is on stairs
if self.player.pos != self.dungeon.stairs_pos:
self.game_ui.add_message(MSG_NO_STAIRS, (150, 150, 150, 255))
return
self.current_level += 1
self.game_ui.add_message(MSG_DESCEND % self.current_level, (100, 100, 255, 255))
# Keep player stats
old_hp = self.player.fighter.hp
old_max_hp = self.player.fighter.max_hp
old_attack = self.player.fighter.attack
old_defense = self.player.fighter.defense
old_xp = self.player.xp
old_level = self.player.level
# Generate new level
self.generate_level()
# Restore player stats
self.player.fighter.hp = old_hp
self.player.fighter.max_hp = old_max_hp
self.player.fighter.attack = old_attack
self.player.fighter.defense = old_defense
self.player.xp = old_xp
self.player.level = old_level
self.update_ui()
def update_fov(self) -> None:
"""Update field of view and apply to grid tiles."""
if not self.player or not self.dungeon:
return
# Use entity's built-in FOV calculation
self.player.entity.update_visibility()
# Apply visibility to tiles
for x in range(self.dungeon.width):
for y in range(self.dungeon.height):
point = self.grid.at(x, y)
tile = self.dungeon.get_tile(x, y)
if tile:
state = self.player.entity.at(x, y)
if state.visible:
# Currently visible
tile.explored = True
tile.visible = True
point.color_overlay = mcrfpy.Color(*COLOR_VISIBLE)
elif tile.explored:
# Explored but not visible
tile.visible = False
point.color_overlay = mcrfpy.Color(*COLOR_REMEMBERED)
else:
# Never seen
point.color_overlay = mcrfpy.Color(*COLOR_FOG)
def center_camera(self) -> None:
"""Center the camera on the player."""
if self.player:
self.grid.center = (self.player.x, self.player.y)
def update_ui(self) -> None:
"""Update all UI elements."""
if self.player:
self.game_ui.update_hp(
self.player.fighter.hp,
self.player.fighter.max_hp
)
def on_player_death(self) -> None:
"""Handle player death."""
self.game_over = True
self.game_ui.add_message(MSG_DEATH, (255, 0, 0, 255))
# Show death screen
self.death_screen = DeathScreen(self.font)
self.death_screen.add_to_scene(self.ui_collection)
def handle_keypress(self, key: str, state: str) -> None:
"""
Handle keyboard input.
Args:
key: Key name
state: "start" for key down, "end" for key up
"""
# Only handle key down events
if state != "start":
return
# Handle restart when dead
if self.game_over:
if key == "R":
self.new_game()
return
# Handle movement
dx, dy = 0, 0
if key in KEY_UP:
dy = -1
elif key in KEY_DOWN:
dy = 1
elif key in KEY_LEFT:
dx = -1
elif key in KEY_RIGHT:
dx = 1
elif key in KEY_UP_LEFT:
dx, dy = -1, -1
elif key in KEY_UP_RIGHT:
dx, dy = 1, -1
elif key in KEY_DOWN_LEFT:
dx, dy = -1, 1
elif key in KEY_DOWN_RIGHT:
dx, dy = 1, 1
elif key in KEY_WAIT:
# Skip turn
self.turn_manager.handle_wait()
self.after_turn()
return
elif key in KEY_DESCEND:
# Try to descend
self.descend()
return
elif key == "Escape":
# Quit game
mcrfpy.exit()
return
# Process movement/attack
if dx != 0 or dy != 0:
if self.turn_manager.handle_player_action(dx, dy):
self.after_turn()
else:
# Movement was blocked
self.game_ui.add_message(MSG_BLOCKED, (150, 150, 150, 255))
def after_turn(self) -> None:
"""Called after each player turn."""
# Update FOV
self.update_fov()
# Center camera
self.center_camera()
# Update UI
self.update_ui()
# Check if standing on stairs
if self.player.pos == self.dungeon.stairs_pos:
self.game_ui.add_message(MSG_STAIRS, (100, 255, 100, 255))
# Clean up dead enemies
self.enemies = [e for e in self.enemies if e.is_alive]
# =============================================================================
# ENTRY POINT
# =============================================================================
# Global game instance
game: Optional[Game] = None
def start_game():
"""Start the game."""
global game
game = Game()
# Auto-start when this script is loaded
start_game()

232
docs/templates/complete/turns.py vendored Normal file
View file

@ -0,0 +1,232 @@
"""
turns.py - Turn Management System for McRogueFace Roguelike
Handles the turn-based game flow: player turn, then enemy turns.
"""
from enum import Enum, auto
from typing import List, Optional, Callable, TYPE_CHECKING
from entities import Player, Enemy
from combat import try_attack, process_kill, CombatResult
from ai import process_enemy_turns
if TYPE_CHECKING:
from dungeon import Dungeon
class GameState(Enum):
"""Current state of the game."""
PLAYER_TURN = auto() # Waiting for player input
ENEMY_TURN = auto() # Processing enemy actions
PLAYER_DEAD = auto() # Player has died
VICTORY = auto() # Player has won (optional)
LEVEL_TRANSITION = auto() # Moving to next level
class TurnManager:
"""
Manages the turn-based game loop.
The game follows this flow:
1. Player takes action (move or attack)
2. If action was valid, enemies take turns
3. Check for game over conditions
4. Return to step 1
"""
def __init__(self, player: Player, enemies: List[Enemy], dungeon: 'Dungeon'):
"""
Initialize the turn manager.
Args:
player: The player entity
enemies: List of all enemies
dungeon: The dungeon map
"""
self.player = player
self.enemies = enemies
self.dungeon = dungeon
self.state = GameState.PLAYER_TURN
self.turn_count = 0
# Callbacks for game events
self.on_message: Optional[Callable[[str, tuple], None]] = None
self.on_player_death: Optional[Callable[[], None]] = None
self.on_enemy_death: Optional[Callable[[Enemy], None]] = None
self.on_turn_end: Optional[Callable[[int], None]] = None
def reset(self, player: Player, enemies: List[Enemy], dungeon: 'Dungeon') -> None:
"""Reset the turn manager with new game state."""
self.player = player
self.enemies = enemies
self.dungeon = dungeon
self.state = GameState.PLAYER_TURN
self.turn_count = 0
def add_message(self, message: str, color: tuple = (255, 255, 255, 255)) -> None:
"""Add a message to the log via callback."""
if self.on_message:
self.on_message(message, color)
def handle_player_action(self, dx: int, dy: int) -> bool:
"""
Handle a player movement or attack action.
Args:
dx: X direction (-1, 0, or 1)
dy: Y direction (-1, 0, or 1)
Returns:
True if the action consumed a turn, False otherwise
"""
if self.state != GameState.PLAYER_TURN:
return False
target_x = self.player.x + dx
target_y = self.player.y + dy
# Check for attack
result = try_attack(self.player, target_x, target_y, self.enemies)
if result:
# Player attacked something
self.add_message(result.message, result.message_color)
if result.killed:
# Process kill
xp = process_kill(self.player, result.defender)
self.enemies.remove(result.defender)
if xp > 0:
self.add_message(f"You gain {xp} XP!", (255, 255, 100, 255))
if self.on_enemy_death:
self.on_enemy_death(result.defender)
# Action consumed a turn
self._end_player_turn()
return True
# No attack - try to move
if self.dungeon.is_walkable(target_x, target_y):
# Check for enemy blocking
blocked = False
for enemy in self.enemies:
if enemy.is_alive and enemy.x == target_x and enemy.y == target_y:
blocked = True
break
if not blocked:
self.player.move_to(target_x, target_y)
self._end_player_turn()
return True
# Movement blocked
return False
def handle_wait(self) -> bool:
"""
Handle the player choosing to wait (skip turn).
Returns:
True (always consumes a turn)
"""
if self.state != GameState.PLAYER_TURN:
return False
self.add_message("You wait...", (150, 150, 150, 255))
self._end_player_turn()
return True
def _end_player_turn(self) -> None:
"""End the player's turn and process enemy turns."""
self.state = GameState.ENEMY_TURN
self._process_enemy_turns()
def _process_enemy_turns(self) -> None:
"""Process all enemy turns."""
# Get combat results from enemy actions
results = process_enemy_turns(
self.enemies,
self.player,
self.dungeon
)
# Report results
for result in results:
self.add_message(result.message, result.message_color)
# Check if player died
if not self.player.is_alive:
self.state = GameState.PLAYER_DEAD
if self.on_player_death:
self.on_player_death()
else:
# End turn
self.turn_count += 1
self.state = GameState.PLAYER_TURN
if self.on_turn_end:
self.on_turn_end(self.turn_count)
def is_player_turn(self) -> bool:
"""Check if it's the player's turn."""
return self.state == GameState.PLAYER_TURN
def is_game_over(self) -> bool:
"""Check if the game is over (player dead)."""
return self.state == GameState.PLAYER_DEAD
def get_enemy_count(self) -> int:
"""Get the number of living enemies."""
return sum(1 for e in self.enemies if e.is_alive)
class ActionResult:
"""Result of a player action."""
def __init__(self, success: bool, message: str = "",
color: tuple = (255, 255, 255, 255)):
self.success = success
self.message = message
self.color = color
def try_move_or_attack(player: Player, dx: int, dy: int,
dungeon: 'Dungeon', enemies: List[Enemy]) -> ActionResult:
"""
Attempt to move or attack in a direction.
This is a simpler, standalone function for games that don't want
the full TurnManager.
Args:
player: The player
dx: X direction
dy: Y direction
dungeon: The dungeon map
enemies: List of enemies
Returns:
ActionResult indicating success and any message
"""
target_x = player.x + dx
target_y = player.y + dy
# Check for attack
for enemy in enemies:
if enemy.is_alive and enemy.x == target_x and enemy.y == target_y:
result = try_attack(player, target_x, target_y, enemies)
if result:
if result.killed:
process_kill(player, enemy)
enemies.remove(enemy)
return ActionResult(True, result.message, result.message_color)
# Check for movement
if dungeon.is_walkable(target_x, target_y):
player.move_to(target_x, target_y)
return ActionResult(True)
return ActionResult(False, "You can't move there!", (150, 150, 150, 255))

330
docs/templates/complete/ui.py vendored Normal file
View file

@ -0,0 +1,330 @@
"""
ui.py - User Interface Components for McRogueFace Roguelike
Contains the health bar and message log UI elements.
"""
from typing import List, Tuple, Optional
from dataclasses import dataclass
import mcrfpy
from constants import (
HP_BAR_X, HP_BAR_Y, HP_BAR_WIDTH, HP_BAR_HEIGHT,
MSG_LOG_X, MSG_LOG_Y, MSG_LOG_WIDTH, MSG_LOG_HEIGHT, MSG_LOG_MAX_LINES,
LEVEL_DISPLAY_X, LEVEL_DISPLAY_Y,
COLOR_UI_BG, COLOR_UI_BORDER, COLOR_TEXT,
COLOR_HP_BAR_BG, COLOR_HP_BAR_FILL, COLOR_HP_BAR_WARNING, COLOR_HP_BAR_CRITICAL,
COLOR_MSG_DEFAULT
)
@dataclass
class Message:
"""A message in the message log."""
text: str
color: Tuple[int, int, int, int]
class HealthBar:
"""
Visual health bar displaying player HP.
Uses nested Frames: an outer background frame and an inner fill frame
that resizes based on HP percentage.
"""
def __init__(self, x: int = HP_BAR_X, y: int = HP_BAR_Y,
width: int = HP_BAR_WIDTH, height: int = HP_BAR_HEIGHT,
font: mcrfpy.Font = None):
"""
Create a health bar.
Args:
x: X position
y: Y position
width: Total width of the bar
height: Height of the bar
font: Font for the HP text
"""
self.x = x
self.y = y
self.width = width
self.height = height
self.font = font or mcrfpy.default_font
# Background frame
self.bg_frame = mcrfpy.Frame(x, y, width, height)
self.bg_frame.fill_color = mcrfpy.Color(*COLOR_HP_BAR_BG)
self.bg_frame.outline = 2
self.bg_frame.outline_color = mcrfpy.Color(*COLOR_UI_BORDER)
# Fill frame (inside background)
self.fill_frame = mcrfpy.Frame(x + 2, y + 2, width - 4, height - 4)
self.fill_frame.fill_color = mcrfpy.Color(*COLOR_HP_BAR_FILL)
self.fill_frame.outline = 0
# HP text
self.hp_text = mcrfpy.Caption("HP: 0 / 0", self.font, x + 8, y + 4)
self.hp_text.fill_color = mcrfpy.Color(*COLOR_TEXT)
self._max_fill_width = width - 4
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add all health bar components to a scene."""
ui.append(self.bg_frame)
ui.append(self.fill_frame)
ui.append(self.hp_text)
def update(self, current_hp: int, max_hp: int) -> None:
"""
Update the health bar display.
Args:
current_hp: Current hit points
max_hp: Maximum hit points
"""
# Calculate fill percentage
if max_hp <= 0:
percent = 0.0
else:
percent = max(0.0, min(1.0, current_hp / max_hp))
# Update fill bar width
self.fill_frame.w = int(self._max_fill_width * percent)
# Update color based on HP percentage
if percent > 0.6:
color = COLOR_HP_BAR_FILL
elif percent > 0.3:
color = COLOR_HP_BAR_WARNING
else:
color = COLOR_HP_BAR_CRITICAL
self.fill_frame.fill_color = mcrfpy.Color(*color)
# Update text
self.hp_text.text = f"HP: {current_hp} / {max_hp}"
class MessageLog:
"""
Scrolling message log displaying game events.
Uses a Frame container with Caption children for each line.
"""
def __init__(self, x: int = MSG_LOG_X, y: int = MSG_LOG_Y,
width: int = MSG_LOG_WIDTH, height: int = MSG_LOG_HEIGHT,
max_lines: int = MSG_LOG_MAX_LINES,
font: mcrfpy.Font = None):
"""
Create a message log.
Args:
x: X position
y: Y position
width: Width of the log
height: Height of the log
max_lines: Maximum number of visible lines
font: Font for the messages
"""
self.x = x
self.y = y
self.width = width
self.height = height
self.max_lines = max_lines
self.font = font or mcrfpy.default_font
# Container frame
self.frame = mcrfpy.Frame(x, y, width, height)
self.frame.fill_color = mcrfpy.Color(*COLOR_UI_BG)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(*COLOR_UI_BORDER)
# Message storage
self.messages: List[Message] = []
self.captions: List[mcrfpy.Caption] = []
# Line height (approximate based on font)
self.line_height = 18
# Create caption objects for each line
self._init_captions()
def _init_captions(self) -> None:
"""Initialize caption objects for message display."""
for i in range(self.max_lines):
caption = mcrfpy.Caption(
"",
self.font,
self.x + 5,
self.y + 5 + i * self.line_height
)
caption.fill_color = mcrfpy.Color(*COLOR_MSG_DEFAULT)
self.captions.append(caption)
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add the message log to a scene."""
ui.append(self.frame)
for caption in self.captions:
ui.append(caption)
def add_message(self, text: str,
color: Tuple[int, int, int, int] = COLOR_MSG_DEFAULT) -> None:
"""
Add a message to the log.
Args:
text: Message text
color: Text color as (R, G, B, A)
"""
self.messages.append(Message(text, color))
# Trim old messages
if len(self.messages) > 100:
self.messages = self.messages[-100:]
# Update display
self._update_display()
def _update_display(self) -> None:
"""Update the displayed messages."""
# Get the most recent messages
recent = self.messages[-self.max_lines:]
for i, caption in enumerate(self.captions):
if i < len(recent):
msg = recent[i]
caption.text = msg.text
caption.fill_color = mcrfpy.Color(*msg.color)
else:
caption.text = ""
def clear(self) -> None:
"""Clear all messages."""
self.messages.clear()
self._update_display()
class LevelDisplay:
"""Simple display showing current dungeon level."""
def __init__(self, x: int = LEVEL_DISPLAY_X, y: int = LEVEL_DISPLAY_Y,
font: mcrfpy.Font = None):
"""
Create a level display.
Args:
x: X position
y: Y position
font: Font for the text
"""
self.font = font or mcrfpy.default_font
self.caption = mcrfpy.Caption("Level: 1", self.font, x, y)
self.caption.fill_color = mcrfpy.Color(*COLOR_TEXT)
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add to a scene."""
ui.append(self.caption)
def update(self, level: int) -> None:
"""Update the displayed level."""
self.caption.text = f"Dungeon Level: {level}"
class GameUI:
"""
Container for all UI elements.
Provides a single point of access for updating the entire UI.
"""
def __init__(self, font: mcrfpy.Font = None):
"""
Create the game UI.
Args:
font: Font for all UI elements
"""
self.font = font or mcrfpy.default_font
# Create UI components
self.health_bar = HealthBar(font=self.font)
self.message_log = MessageLog(font=self.font)
self.level_display = LevelDisplay(font=self.font)
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add all UI elements to a scene."""
self.health_bar.add_to_scene(ui)
self.message_log.add_to_scene(ui)
self.level_display.add_to_scene(ui)
def update_hp(self, current_hp: int, max_hp: int) -> None:
"""Update the health bar."""
self.health_bar.update(current_hp, max_hp)
def add_message(self, text: str,
color: Tuple[int, int, int, int] = COLOR_MSG_DEFAULT) -> None:
"""Add a message to the log."""
self.message_log.add_message(text, color)
def update_level(self, level: int) -> None:
"""Update the dungeon level display."""
self.level_display.update(level)
def clear_messages(self) -> None:
"""Clear the message log."""
self.message_log.clear()
class DeathScreen:
"""Game over screen shown when player dies."""
def __init__(self, font: mcrfpy.Font = None):
"""
Create the death screen.
Args:
font: Font for text
"""
self.font = font or mcrfpy.default_font
self.elements: List = []
# Semi-transparent overlay
self.overlay = mcrfpy.Frame(0, 0, 1024, 768)
self.overlay.fill_color = mcrfpy.Color(0, 0, 0, 180)
self.elements.append(self.overlay)
# Death message
self.death_text = mcrfpy.Caption(
"YOU HAVE DIED",
self.font,
362, 300
)
self.death_text.fill_color = mcrfpy.Color(255, 0, 0, 255)
self.death_text.outline = 2
self.death_text.outline_color = mcrfpy.Color(0, 0, 0, 255)
self.elements.append(self.death_text)
# Restart prompt
self.restart_text = mcrfpy.Caption(
"Press R to restart",
self.font,
400, 400
)
self.restart_text.fill_color = mcrfpy.Color(200, 200, 200, 255)
self.elements.append(self.restart_text)
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add death screen elements to a scene."""
for element in self.elements:
ui.append(element)
def remove_from_scene(self, ui: mcrfpy.UICollection) -> None:
"""Remove death screen elements from a scene."""
for element in self.elements:
try:
ui.remove(element)
except (ValueError, RuntimeError):
pass

176
docs/templates/minimal/game.py vendored Normal file
View file

@ -0,0 +1,176 @@
"""
McRogueFace Minimal Template
============================
A starting point for simple roguelike prototypes.
This template demonstrates:
- Scene object pattern (preferred OOP approach)
- Grid-based movement with boundary checking
- Keyboard input handling
- Entity positioning on a grid
Usage:
Place this file in your McRogueFace scripts directory and run McRogueFace.
Use arrow keys to move the @ symbol. Press Escape to exit.
"""
import mcrfpy
# =============================================================================
# CONSTANTS
# =============================================================================
# Grid dimensions (in tiles)
GRID_WIDTH: int = 20
GRID_HEIGHT: int = 15
# Tile size in pixels (must match your sprite sheet)
TILE_SIZE: int = 16
# CP437 sprite indices (standard roguelike character mapping)
# In CP437, character codes map to sprite indices: '@' = 64, '.' = 46, etc.
SPRITE_PLAYER: int = 64 # '@' symbol
SPRITE_FLOOR: int = 46 # '.' symbol
# Colors (RGBA tuples)
COLOR_BACKGROUND: tuple[int, int, int] = (20, 20, 30)
# =============================================================================
# GAME STATE
# =============================================================================
# Player position in grid coordinates
player_x: int = GRID_WIDTH // 2
player_y: int = GRID_HEIGHT // 2
# Reference to player entity (set during setup)
player_entity: mcrfpy.Entity = None
# =============================================================================
# MOVEMENT LOGIC
# =============================================================================
def try_move(dx: int, dy: int) -> bool:
"""
Attempt to move the player by (dx, dy) tiles.
Args:
dx: Horizontal movement (-1 = left, +1 = right, 0 = none)
dy: Vertical movement (-1 = up, +1 = down, 0 = none)
Returns:
True if movement succeeded, False if blocked by boundary
"""
global player_x, player_y
new_x = player_x + dx
new_y = player_y + dy
# Boundary checking: ensure player stays within grid
if 0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT:
player_x = new_x
player_y = new_y
# Update the entity's position on the grid
player_entity.x = player_x
player_entity.y = player_y
return True
return False
# =============================================================================
# INPUT HANDLING
# =============================================================================
def handle_keypress(key: str, action: str) -> None:
"""
Handle keyboard input for the game scene.
Args:
key: The key that was pressed (e.g., "Up", "Down", "Escape", "a", "W")
action: Either "start" (key pressed) or "end" (key released)
Note:
We only process on "start" to avoid double-triggering on key release.
"""
if action != "start":
return
# Movement keys (both arrow keys and WASD)
if key == "Up" or key == "W" or key == "w":
try_move(0, -1)
elif key == "Down" or key == "S" or key == "s":
try_move(0, 1)
elif key == "Left" or key == "A" or key == "a":
try_move(-1, 0)
elif key == "Right" or key == "D" or key == "d":
try_move(1, 0)
# Exit on Escape
elif key == "Escape":
mcrfpy.exit()
# =============================================================================
# SCENE SETUP
# =============================================================================
def setup_game() -> mcrfpy.Scene:
"""
Create and configure the game scene.
Returns:
The configured Scene object, ready to be activated.
"""
global player_entity
# Create the scene using the OOP pattern (preferred over createScene)
scene = mcrfpy.Scene("game")
# Load the sprite sheet texture
# Adjust the path and tile size to match your assets
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", TILE_SIZE, TILE_SIZE)
# Create the game grid
# Grid(pos, size, grid_size) where:
# pos = pixel position on screen
# size = pixel dimensions of the grid display
# grid_size = number of tiles (columns, rows)
grid = mcrfpy.Grid(
pos=(32, 32),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture
)
grid.fill_color = mcrfpy.Color(*COLOR_BACKGROUND)
# Fill the grid with floor tiles
for x in range(GRID_WIDTH):
for y in range(GRID_HEIGHT):
point = grid.at(x, y)
point.tilesprite = SPRITE_FLOOR
point.walkable = True
point.transparent = True
# Create the player entity
player_entity = mcrfpy.Entity(
pos=(player_x, player_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player_entity)
# Add the grid to the scene's UI
scene.children.append(grid)
# Set up keyboard input handler for this scene
scene.on_key = handle_keypress
return scene
# =============================================================================
# MAIN ENTRY POINT
# =============================================================================
# Create and activate the game scene
game_scene = setup_game()
game_scene.activate()

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()