McRogueFace/docs/templates/complete/game.py

313 lines
9.2 KiB
Python

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