diff --git a/.gitignore b/.gitignore index 17b7ca0..174f159 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ obj build lib obj +__pycache__ .cache/ 7DRL2025 Release/ diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/game.py new file mode 100644 index 0000000..00c9de2 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/game.py @@ -0,0 +1,33 @@ +import mcrfpy + +# Create a new scene called "hello" +mcrfpy.createScene("hello") + +# Switch to our new scene +mcrfpy.setScene("hello") + +# Get the UI container for our scene +ui = mcrfpy.sceneUI("hello") + +# Create a text caption +caption = mcrfpy.Caption("Hello Roguelike!", 400, 300) +caption.font_size = 32 +caption.fill_color = mcrfpy.Color(255, 255, 255) # White text + +# Add the caption to our scene +ui.append(caption) + +# Create a smaller instruction caption +instruction = mcrfpy.Caption("Press ESC to exit", 400, 350) +instruction.font_size = 16 +instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray +ui.append(instruction) + +# Set up a simple key handler +def handle_keys(key, state): + if state == "start" and key == "Escape": + mcrfpy.setScene(None) # This exits the game + +mcrfpy.keypressScene(handle_keys) + +print("Hello Roguelike is running!") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/setup_test.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/setup_test.py new file mode 100644 index 0000000..0b39a49 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/setup_test.py @@ -0,0 +1,55 @@ +import mcrfpy + +# Create our test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") +ui = mcrfpy.sceneUI("test") + +# Create a background frame +background = mcrfpy.Frame(0, 0, 1024, 768) +background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray +ui.append(background) + +# Title text +title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100) +title.font_size = 36 +title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow +ui.append(title) + +# Status text that will update +status_text = mcrfpy.Caption("Press any key to test input...", 512, 300) +status_text.font_size = 20 +status_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(status_text) + +# Instructions +instructions = [ + "Arrow Keys: Test movement input", + "Space: Test action input", + "Mouse Click: Test mouse input", + "ESC: Exit" +] + +y_offset = 400 +for instruction in instructions: + inst_caption = mcrfpy.Caption(instruction, 512, y_offset) + inst_caption.font_size = 16 + inst_caption.fill_color = mcrfpy.Color(150, 150, 150) + ui.append(inst_caption) + y_offset += 30 + +# Input handler +def handle_input(key, state): + if state != "start": + return + + if key == "Escape": + mcrfpy.setScene(None) + else: + status_text.text = f"You pressed: {key}" + status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green + +# Set up input handling +mcrfpy.keypressScene(handle_input) + +print("Setup test is running! Try pressing different keys.") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_1/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_1/code/game.py new file mode 100644 index 0000000..2f0c157 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_1/code/game.py @@ -0,0 +1,162 @@ +import mcrfpy + +# Window configuration +mcrfpy.createScene("game") +mcrfpy.setScene("game") + +window = mcrfpy.Window.get() +window.title = "McRogueFace Roguelike - Part 1" + +# Get the UI container for our scene +ui = mcrfpy.sceneUI("game") + +# Create a dark background +background = mcrfpy.Frame(0, 0, 1024, 768) +background.fill_color = mcrfpy.Color(0, 0, 0) +ui.append(background) + +# Load the ASCII tileset +tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + +# Create the game grid +GRID_WIDTH = 50 +GRID_HEIGHT = 30 + +grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset) +grid.position = (100, 100) +grid.size = (800, 480) +ui.append(grid) + +def create_room(): + """Create a room with walls around the edges""" + # Fill everything with floor tiles first + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.sprite_index = 46 # '.' character + cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor + + # Create walls around the edges + for x in range(GRID_WIDTH): + # Top wall + cell = grid.at(x, 0) + cell.walkable = False + cell.transparent = False + cell.sprite_index = 35 # '#' character + cell.color = mcrfpy.Color(100, 100, 100) # Gray walls + + # Bottom wall + cell = grid.at(x, GRID_HEIGHT - 1) + cell.walkable = False + cell.transparent = False + cell.sprite_index = 35 # '#' character + cell.color = mcrfpy.Color(100, 100, 100) + + for y in range(GRID_HEIGHT): + # Left wall + cell = grid.at(0, y) + cell.walkable = False + cell.transparent = False + cell.sprite_index = 35 # '#' character + cell.color = mcrfpy.Color(100, 100, 100) + + # Right wall + cell = grid.at(GRID_WIDTH - 1, y) + cell.walkable = False + cell.transparent = False + cell.sprite_index = 35 # '#' character + cell.color = mcrfpy.Color(100, 100, 100) + +# Create the room +create_room() + +# Create the player entity +player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid) +player.sprite_index = 64 # '@' character +player.color = mcrfpy.Color(255, 255, 255) # White + +def move_player(dx, dy): + """Move the player if the destination is walkable""" + # Calculate new position + new_x = player.x + dx + new_y = player.y + dy + + # Check bounds + if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT: + return + + # Check if the destination is walkable + destination = grid.at(new_x, new_y) + if destination.walkable: + # Move the player + player.x = new_x + player.y = new_y + +def handle_input(key, state): + """Handle keyboard input for player movement""" + # Only process key presses, not releases + if state != "start": + return + + # Movement deltas + dx, dy = 0, 0 + + # Arrow keys + if key == "Up": + dy = -1 + elif key == "Down": + dy = 1 + elif key == "Left": + dx = -1 + elif key == "Right": + dx = 1 + + # Numpad movement (for true roguelike feel!) + elif key == "Num7": # Northwest + dx, dy = -1, -1 + elif key == "Num8": # North + dy = -1 + elif key == "Num9": # Northeast + dx, dy = 1, -1 + elif key == "Num4": # West + dx = -1 + elif key == "Num6": # East + dx = 1 + elif key == "Num1": # Southwest + dx, dy = -1, 1 + elif key == "Num2": # South + dy = 1 + elif key == "Num3": # Southeast + dx, dy = 1, 1 + + # Escape to quit + elif key == "Escape": + mcrfpy.setScene(None) + return + + # If there's movement, try to move the player + if dx != 0 or dy != 0: + move_player(dx, dy) + +# Register the input handler +mcrfpy.keypressScene(handle_input) + +# Add UI elements +title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30) +title.font_size = 24 +title.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(title) + +instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60) +instructions.font_size = 16 +instructions.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(instructions) + +status = mcrfpy.Caption("@ You", 100, 600) +status.font_size = 18 +status.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(status) + +print("Part 1: The @ symbol moves!") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_2/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_2/code/game.py new file mode 100644 index 0000000..38eef78 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_2/code/game.py @@ -0,0 +1,217 @@ +import mcrfpy + +class GameObject: + """Base class for all game objects (player, monsters, items)""" + + def __init__(self, x, y, sprite_index, color, name, blocks=False): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount if possible""" + if not self.grid: + return + + new_x = self.x + dx + new_y = self.y + dy + + self.x = new_x + self.y = new_y + + if self._entity: + self._entity.x = new_x + self._entity.y = new_y + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + self.fill_with_walls() + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, color=(100, 100, 100)) + + def set_tile(self, x, y, walkable, transparent, sprite_index, color): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*color) + + def create_room(self, x1, y1, x2, y2): + """Carve out a room in the map""" + x1, x2 = min(x1, x2), max(x1, x2) + y1, y2 = min(y1, y2), max(y1, y2) + + for y in range(y1, y2 + 1): + for x in range(x1, x2 + 1): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(50, 50, 50)) + + def create_tunnel_h(self, x1, x2, y): + """Create a horizontal tunnel""" + for x in range(min(x1, x2), max(x1, x2) + 1): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(50, 50, 50)) + + def create_tunnel_v(self, y1, y2, x): + """Create a vertical tunnel""" + for y in range(min(y1, y2), max(y1, y2) + 1): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(50, 50, 50)) + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + + if not self.grid.at(x, y).walkable: + return True + + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return True + + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + + def get_blocking_entity_at(self, x, y): + """Return any blocking entity at the given position""" + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return entity + return None + +class Engine: + """Main game engine that manages game state""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 2" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(50, 30) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + self.game_map.create_room(10, 10, 20, 20) + self.game_map.create_room(30, 15, 40, 25) + self.game_map.create_room(15, 22, 25, 28) + + self.game_map.create_tunnel_h(20, 30, 15) + self.game_map.create_tunnel_v(20, 22, 20) + + self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True) + self.game_map.add_entity(self.player) + + npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True) + self.game_map.add_entity(npc) + self.entities.append(npc) + + potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False) + self.game_map.add_entity(potion) + self.entities.append(potion) + + def handle_movement(self, dx, dy): + """Handle player movement""" + new_x = self.player.x + dx + new_y = self.player.y + dy + + if not self.game_map.is_blocked(new_x, new_y): + self.player.move(dx, dy) + else: + target = self.game_map.get_blocking_entity_at(new_x, new_y) + if target: + print(f"You bump into the {target.name}!") + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + self.handle_movement(dx, dy) + elif key == "Escape": + mcrfpy.setScene(None) + + mcrfpy.keypressScene(handle_keys) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + +# Create and run the game +engine = Engine() +print("Part 2: Entities and Maps!") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_3/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_3/code/game.py new file mode 100644 index 0000000..1256ef9 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_3/code/game.py @@ -0,0 +1,312 @@ +import mcrfpy +import random + +class GameObject: + """Base class for all game objects""" + def __init__(self, x, y, sprite_index, color, name, blocks=False): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount""" + if not self.grid: + return + self.x += dx + self.y += dy + if self._entity: + self._entity.x = self.x + self._entity.y = self.y + +class RectangularRoom: + """A rectangular room with its position and size""" + + def __init__(self, x, y, width, height): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self): + """Return the center coordinates of the room""" + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self): + """Return the inner area of the room""" + return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1 + + def intersects(self, other): + """Return True if this room overlaps with another""" + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) + +def tunnel_between(start, end): + """Return an L-shaped tunnel between two points""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + corner_x = x2 + corner_y = y1 + else: + corner_x = x1 + corner_y = y2 + + # Generate the coordinates + for x in range(min(x1, corner_x), max(x1, corner_x) + 1): + yield x, y1 + for y in range(min(y1, corner_y), max(y1, corner_y) + 1): + yield corner_x, y + 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 + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + self.rooms = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, color=(100, 100, 100)) + + def set_tile(self, x, y, walkable, transparent, sprite_index, color): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*color) + + def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player): + """Generate a new dungeon map""" + self.fill_with_walls() + + for r in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + + x = random.randint(0, self.width - room_width - 1) + y = random.randint(0, self.height - room_height - 1) + + new_room = RectangularRoom(x, y, room_width, room_height) + + if any(new_room.intersects(other_room) for other_room in self.rooms): + continue + + self.carve_room(new_room) + + if len(self.rooms) == 0: + player.x, player.y = new_room.center + if player._entity: + player._entity.x, player._entity.y = new_room.center + else: + self.carve_tunnel(self.rooms[-1].center, new_room.center) + + self.rooms.append(new_room) + + def carve_room(self, room): + """Carve out a room""" + inner_x1, inner_y1, inner_x2, inner_y2 = room.inner + + for y in range(inner_y1, inner_y2): + for x in range(inner_x1, inner_x2): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(50, 50, 50)) + + def carve_tunnel(self, start, end): + """Carve a tunnel between two points""" + for x, y in tunnel_between(start, end): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(30, 30, 40)) + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + if not self.grid.at(x, y).walkable: + return True + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return True + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + +class Engine: + """Main game engine""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 3" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(80, 45) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + # Create player (before dungeon generation) + self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True) + + # Generate the dungeon + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player + ) + + # Add player to map + self.game_map.add_entity(self.player) + + # Add some monsters in random rooms + for i in range(5): + if i < len(self.game_map.rooms) - 1: # Don't spawn in first room + room = self.game_map.rooms[i + 1] + x, y = room.center + + # Create an orc + orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True) + self.game_map.add_entity(orc) + self.entities.append(orc) + + def handle_movement(self, dx, dy): + """Handle player movement""" + new_x = self.player.x + dx + new_y = self.player.y + dy + + if not self.game_map.is_blocked(new_x, new_y): + self.player.move(dx, dy) + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + self.handle_movement(dx, dy) + elif key == "Escape": + mcrfpy.setScene(None) + elif key == "Space": + # Regenerate the dungeon + self.regenerate_dungeon() + + mcrfpy.keypressScene(handle_keys) + + def regenerate_dungeon(self): + """Generate a new dungeon""" + # Clear existing entities + self.game_map.entities.clear() + self.game_map.rooms.clear() + self.entities.clear() + + # Clear the entity list in the grid + if self.game_map.grid: + self.game_map.grid.entities.clear() + + # Regenerate + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player + ) + + # Re-add player + self.game_map.add_entity(self.player) + + # Add new monsters + for i in range(5): + if i < len(self.game_map.rooms) - 1: + room = self.game_map.rooms[i + 1] + x, y = room.center + orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True) + self.game_map.add_entity(orc) + self.entities.append(orc) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + +# Create and run the game +engine = Engine() +print("Part 3: Procedural Dungeon Generation!") +print("Press SPACE to generate a new dungeon") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_4/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_4/code/game.py new file mode 100644 index 0000000..e5c23da --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_4/code/game.py @@ -0,0 +1,334 @@ +import mcrfpy +import random + +# Color configurations for visibility +COLORS_VISIBLE = { + 'wall': (100, 100, 100), + 'floor': (50, 50, 50), + 'tunnel': (30, 30, 40), +} + +class GameObject: + """Base class for all game objects""" + def __init__(self, x, y, sprite_index, color, name, blocks=False): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount""" + if not self.grid: + return + self.x += dx + self.y += dy + if self._entity: + self._entity.x = self.x + self._entity.y = self.y + # Update FOV when player moves + if self.name == "Player": + self.update_fov() + + def update_fov(self): + """Update field of view from this entity's position""" + if self._entity and self.grid: + self._entity.update_fov(radius=8) + +class RectangularRoom: + """A rectangular room with its position and size""" + + def __init__(self, x, y, width, height): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self): + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self): + return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1 + + def intersects(self, other): + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) + +def tunnel_between(start, end): + """Return an L-shaped tunnel between two points""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + corner_x = x2 + corner_y = y1 + else: + corner_x = x1 + corner_y = y2 + + for x in range(min(x1, corner_x), max(x1, corner_x) + 1): + yield x, y1 + for y in range(min(y1, corner_y), max(y1, corner_y) + 1): + yield corner_x, y + 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 + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + self.rooms = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + + # Enable perspective rendering (0 = first entity = player) + self.grid.perspective = 0 + + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, tile_type='wall') + + def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type]) + + def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player): + """Generate a new dungeon map""" + self.fill_with_walls() + + for r in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + + x = random.randint(0, self.width - room_width - 1) + y = random.randint(0, self.height - room_height - 1) + + new_room = RectangularRoom(x, y, room_width, room_height) + + if any(new_room.intersects(other_room) for other_room in self.rooms): + continue + + self.carve_room(new_room) + + if len(self.rooms) == 0: + player.x, player.y = new_room.center + if player._entity: + player._entity.x, player._entity.y = new_room.center + else: + self.carve_tunnel(self.rooms[-1].center, new_room.center) + + self.rooms.append(new_room) + + def carve_room(self, room): + """Carve out a room""" + inner_x1, inner_y1, inner_x2, inner_y2 = room.inner + + for y in range(inner_y1, inner_y2): + for x in range(inner_x1, inner_x2): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='floor') + + def carve_tunnel(self, start, end): + """Carve a tunnel between two points""" + for x, y in tunnel_between(start, end): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='tunnel') + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + if not self.grid.at(x, y).walkable: + return True + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return True + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + +class Engine: + """Main game engine""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + self.fov_radius = 8 + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 4" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(80, 45) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + # Create player + self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True) + + # Generate the dungeon + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player + ) + + # Add player to map + self.game_map.add_entity(self.player) + + # Add monsters in random rooms + for i in range(10): + if i < len(self.game_map.rooms) - 1: + room = self.game_map.rooms[i + 1] + x, y = room.center + + # Randomly offset from center + x += random.randint(-2, 2) + y += random.randint(-2, 2) + + # Make sure position is walkable + if self.game_map.grid.at(x, y).walkable: + if i % 2 == 0: + # Create an orc + orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True) + self.game_map.add_entity(orc) + self.entities.append(orc) + else: + # Create a troll + troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True) + self.game_map.add_entity(troll) + self.entities.append(troll) + + # Initial FOV calculation + self.player.update_fov() + + def handle_movement(self, dx, dy): + """Handle player movement""" + new_x = self.player.x + dx + new_y = self.player.y + dy + + if not self.game_map.is_blocked(new_x, new_y): + self.player.move(dx, dy) + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + self.handle_movement(dx, dy) + elif key == "Escape": + mcrfpy.setScene(None) + elif key == "v": + # Toggle FOV on/off + if self.game_map.grid.perspective == 0: + self.game_map.grid.perspective = -1 # Omniscient + print("FOV disabled - omniscient view") + else: + self.game_map.grid.perspective = 0 # Player perspective + print("FOV enabled - player perspective") + elif key == "Plus" or key == "Equals": + # Increase FOV radius + self.fov_radius = min(self.fov_radius + 1, 20) + self.player._entity.update_fov(radius=self.fov_radius) + print(f"FOV radius: {self.fov_radius}") + elif key == "Minus": + # Decrease FOV radius + self.fov_radius = max(self.fov_radius - 1, 3) + self.player._entity.update_fov(radius=self.fov_radius) + print(f"FOV radius: {self.fov_radius}") + + mcrfpy.keypressScene(handle_keys) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("Field of View", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + + # FOV indicator + self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100) + self.fov_text.font_size = 14 + self.fov_text.fill_color = mcrfpy.Color(150, 200, 255) + self.ui.append(self.fov_text) + +# Create and run the game +engine = Engine() +print("Part 4: Field of View!") +print("Press V to toggle FOV on/off") +print("Press +/- to adjust FOV radius") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_5/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_5/code/game.py new file mode 100644 index 0000000..3e5947f --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_5/code/game.py @@ -0,0 +1,388 @@ +import mcrfpy +import random + +# Color configurations +COLORS_VISIBLE = { + 'wall': (100, 100, 100), + 'floor': (50, 50, 50), + 'tunnel': (30, 30, 40), +} + +# Actions +class Action: + """Base class for all actions""" + pass + +class MovementAction(Action): + """Action for moving an entity""" + def __init__(self, dx, dy): + self.dx = dx + self.dy = dy + +class WaitAction(Action): + """Action for waiting/skipping turn""" + pass + +class GameObject: + """Base class for all game objects""" + def __init__(self, x, y, sprite_index, color, name, blocks=False): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount""" + if not self.grid: + return + self.x += dx + self.y += dy + if self._entity: + self._entity.x = self.x + self._entity.y = self.y + # Update FOV when player moves + if self.name == "Player": + self.update_fov() + + def update_fov(self): + """Update field of view from this entity's position""" + if self._entity and self.grid: + self._entity.update_fov(radius=8) + +class RectangularRoom: + """A rectangular room with its position and size""" + + def __init__(self, x, y, width, height): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self): + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self): + return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1 + + def intersects(self, other): + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) + +def tunnel_between(start, end): + """Return an L-shaped tunnel between two points""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + corner_x = x2 + corner_y = y1 + else: + corner_x = x1 + corner_y = y2 + + for x in range(min(x1, corner_x), max(x1, corner_x) + 1): + yield x, y1 + for y in range(min(y1, corner_y), max(y1, corner_y) + 1): + yield corner_x, y + 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 spawn_enemies_in_room(room, game_map, max_enemies=2): + """Spawn between 0 and max_enemies in a room""" + number_of_enemies = random.randint(0, max_enemies) + + enemies_spawned = [] + + for i in range(number_of_enemies): + # Try to find a valid position + attempts = 10 + while attempts > 0: + # Random position within room bounds + x = random.randint(room.x1 + 1, room.x2 - 1) + y = random.randint(room.y1 + 1, room.y2 - 1) + + # Check if position is valid + if not game_map.is_blocked(x, y): + # 80% chance for orc, 20% for troll + if random.random() < 0.8: + enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True) + else: + enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True) + + game_map.add_entity(enemy) + enemies_spawned.append(enemy) + break + + attempts -= 1 + + return enemies_spawned + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + self.rooms = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + + # Enable perspective rendering + self.grid.perspective = 0 + + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, tile_type='wall') + + def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type]) + + def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room): + """Generate a new dungeon map""" + self.fill_with_walls() + + for r in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + + x = random.randint(0, self.width - room_width - 1) + y = random.randint(0, self.height - room_height - 1) + + new_room = RectangularRoom(x, y, room_width, room_height) + + if any(new_room.intersects(other_room) for other_room in self.rooms): + continue + + self.carve_room(new_room) + + if len(self.rooms) == 0: + # First room - place player + player.x, player.y = new_room.center + if player._entity: + player._entity.x, player._entity.y = new_room.center + else: + # All other rooms - add tunnel and enemies + self.carve_tunnel(self.rooms[-1].center, new_room.center) + spawn_enemies_in_room(new_room, self, max_enemies_per_room) + + self.rooms.append(new_room) + + def carve_room(self, room): + """Carve out a room""" + inner_x1, inner_y1, inner_x2, inner_y2 = room.inner + + for y in range(inner_y1, inner_y2): + for x in range(inner_x1, inner_x2): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='floor') + + def carve_tunnel(self, start, end): + """Carve a tunnel between two points""" + for x, y in tunnel_between(start, end): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='tunnel') + + def get_blocking_entity_at(self, x, y): + """Return any blocking entity at the given position""" + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return entity + return None + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + + if not self.grid.at(x, y).walkable: + return True + + if self.get_blocking_entity_at(x, y): + return True + + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + +class Engine: + """Main game engine""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 5" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(80, 45) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + # Create player + self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True) + + # Generate the dungeon + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player, + max_enemies_per_room=2 + ) + + # Add player to map + self.game_map.add_entity(self.player) + + # Store reference to all entities + self.entities = [e for e in self.game_map.entities if e != self.player] + + # Initial FOV calculation + self.player.update_fov() + + def handle_player_turn(self, action): + """Process the player's action""" + if isinstance(action, MovementAction): + dest_x = self.player.x + action.dx + dest_y = self.player.y + action.dy + + # Check what's at the destination + target = self.game_map.get_blocking_entity_at(dest_x, dest_y) + + if target: + # We bumped into something! + print(f"You kick the {target.name} in the shins, much to its annoyance!") + self.status_text.text = f"You kick the {target.name}!" + elif not self.game_map.is_blocked(dest_x, dest_y): + # Move the player + self.player.move(action.dx, action.dy) + self.status_text.text = "" + else: + # Bumped into a wall + self.status_text.text = "Blocked!" + + elif isinstance(action, WaitAction): + self.status_text.text = "You wait..." + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + action = None + + # Movement keys + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + if dx == 0 and dy == 0: + action = WaitAction() + else: + action = MovementAction(dx, dy) + elif key == "Period": + action = WaitAction() + elif key == "Escape": + mcrfpy.setScene(None) + return + + # Process the action + if action: + self.handle_player_turn(action) + + mcrfpy.keypressScene(handle_keys) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("Placing Enemies", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + + # Status text + self.status_text = mcrfpy.Caption("", 512, 600) + self.status_text.font_size = 18 + self.status_text.fill_color = mcrfpy.Color(255, 200, 200) + self.ui.append(self.status_text) + + # Entity count + entity_count = len(self.entities) + count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100) + count_text.font_size = 14 + count_text.fill_color = mcrfpy.Color(150, 150, 255) + self.ui.append(count_text) + +# Create and run the game +engine = Engine() +print("Part 5: Placing Enemies!") +print("Try bumping into enemies - combat coming in Part 6!") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py new file mode 100644 index 0000000..b738dcc --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py @@ -0,0 +1,568 @@ +import mcrfpy +import random + +# Color configurations +COLORS_VISIBLE = { + 'wall': (100, 100, 100), + 'floor': (50, 50, 50), + 'tunnel': (30, 30, 40), +} + +# Message colors +COLOR_PLAYER_ATK = (230, 230, 230) +COLOR_ENEMY_ATK = (255, 200, 200) +COLOR_PLAYER_DIE = (255, 100, 100) +COLOR_ENEMY_DIE = (255, 165, 0) + +# Actions +class Action: + """Base class for all actions""" + pass + +class MovementAction(Action): + """Action for moving an entity""" + def __init__(self, dx, dy): + self.dx = dx + self.dy = dy + +class MeleeAction(Action): + """Action for melee attacks""" + def __init__(self, attacker, target): + self.attacker = attacker + self.target = target + + def perform(self): + """Execute the attack""" + if not self.target.is_alive: + return None + + damage = self.attacker.power - self.target.defense + + if damage > 0: + attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!" + self.target.take_damage(damage) + + # Choose color based on attacker + if self.attacker.name == "Player": + color = COLOR_PLAYER_ATK + else: + color = COLOR_ENEMY_ATK + + return attack_desc, color + else: + attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage." + return attack_desc, (150, 150, 150) + +class WaitAction(Action): + """Action for waiting/skipping turn""" + pass + +class GameObject: + """Base class for all game objects""" + def __init__(self, x, y, sprite_index, color, name, + blocks=False, hp=0, defense=0, power=0): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + # Combat stats + self.max_hp = hp + self.hp = hp + self.defense = defense + self.power = power + + @property + def is_alive(self): + """Returns True if this entity can act""" + return self.hp > 0 + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount""" + if not self.grid: + return + self.x += dx + self.y += dy + if self._entity: + self._entity.x = self.x + self._entity.y = self.y + # Update FOV when player moves + if self.name == "Player": + self.update_fov() + + def update_fov(self): + """Update field of view from this entity's position""" + if self._entity and self.grid: + self._entity.update_fov(radius=8) + + def take_damage(self, amount): + """Apply damage to this entity""" + self.hp -= amount + + # Check for death + if self.hp <= 0: + self.die() + + def die(self): + """Handle entity death""" + if self.name == "Player": + # Player death + self.sprite_index = 64 # Stay as @ + self.color = (127, 0, 0) # Dark red + if self._entity: + self._entity.color = mcrfpy.Color(127, 0, 0) + else: + # Enemy death + self.sprite_index = 37 # % character for corpse + self.color = (127, 0, 0) # Dark red + self.blocks = False # Corpses don't block + self.name = f"remains of {self.name}" + + if self._entity: + self._entity.sprite_index = 37 + self._entity.color = mcrfpy.Color(127, 0, 0) + +# Entity factories +def create_player(x, y): + """Create the player entity""" + return GameObject( + x=x, y=y, + sprite_index=64, # @ + color=(255, 255, 255), + name="Player", + blocks=True, + hp=30, + defense=2, + power=5 + ) + +def create_orc(x, y): + """Create an orc enemy""" + return GameObject( + x=x, y=y, + sprite_index=111, # o + color=(63, 127, 63), + name="Orc", + blocks=True, + hp=10, + defense=0, + power=3 + ) + +def create_troll(x, y): + """Create a troll enemy""" + return GameObject( + x=x, y=y, + sprite_index=84, # T + color=(0, 127, 0), + name="Troll", + blocks=True, + hp=16, + defense=1, + power=4 + ) + +class RectangularRoom: + """A rectangular room with its position and size""" + + def __init__(self, x, y, width, height): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self): + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self): + return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1 + + def intersects(self, other): + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) + +def tunnel_between(start, end): + """Return an L-shaped tunnel between two points""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + corner_x = x2 + corner_y = y1 + else: + corner_x = x1 + corner_y = y2 + + for x in range(min(x1, corner_x), max(x1, corner_x) + 1): + yield x, y1 + for y in range(min(y1, corner_y), max(y1, corner_y) + 1): + yield corner_x, y + 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 spawn_enemies_in_room(room, game_map, max_enemies=2): + """Spawn between 0 and max_enemies in a room""" + number_of_enemies = random.randint(0, max_enemies) + + enemies_spawned = [] + + for i in range(number_of_enemies): + attempts = 10 + while attempts > 0: + x = random.randint(room.x1 + 1, room.x2 - 1) + y = random.randint(room.y1 + 1, room.y2 - 1) + + if not game_map.is_blocked(x, y): + # 80% chance for orc, 20% for troll + if random.random() < 0.8: + enemy = create_orc(x, y) + else: + enemy = create_troll(x, y) + + game_map.add_entity(enemy) + enemies_spawned.append(enemy) + break + + attempts -= 1 + + return enemies_spawned + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + self.rooms = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + + # Enable perspective rendering + self.grid.perspective = 0 + + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, tile_type='wall') + + def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type]) + + def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room): + """Generate a new dungeon map""" + self.fill_with_walls() + + for r in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + + x = random.randint(0, self.width - room_width - 1) + y = random.randint(0, self.height - room_height - 1) + + new_room = RectangularRoom(x, y, room_width, room_height) + + if any(new_room.intersects(other_room) for other_room in self.rooms): + continue + + self.carve_room(new_room) + + if len(self.rooms) == 0: + # First room - place player + player.x, player.y = new_room.center + if player._entity: + player._entity.x, player._entity.y = new_room.center + else: + # All other rooms - add tunnel and enemies + self.carve_tunnel(self.rooms[-1].center, new_room.center) + spawn_enemies_in_room(new_room, self, max_enemies_per_room) + + self.rooms.append(new_room) + + def carve_room(self, room): + """Carve out a room""" + inner_x1, inner_y1, inner_x2, inner_y2 = room.inner + + for y in range(inner_y1, inner_y2): + for x in range(inner_x1, inner_x2): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='floor') + + def carve_tunnel(self, start, end): + """Carve a tunnel between two points""" + for x, y in tunnel_between(start, end): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='tunnel') + + def get_blocking_entity_at(self, x, y): + """Return any blocking entity at the given position""" + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return entity + return None + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + + if not self.grid.at(x, y).walkable: + return True + + if self.get_blocking_entity_at(x, y): + return True + + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + +class Engine: + """Main game engine""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + self.messages = [] # Simple message log + self.max_messages = 5 + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 6" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def add_message(self, text, color=(255, 255, 255)): + """Add a message to the log""" + self.messages.append((text, color)) + if len(self.messages) > self.max_messages: + self.messages.pop(0) + self.update_message_display() + + def update_message_display(self): + """Update the message display""" + # Clear old messages + for caption in self.message_captions: + # Remove from UI (McRogueFace doesn't have remove, so we hide it) + caption.text = "" + + # Display current messages + for i, (text, color) in enumerate(self.messages): + if i < len(self.message_captions): + self.message_captions[i].text = text + self.message_captions[i].fill_color = mcrfpy.Color(*color) + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(80, 45) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + # Create player + self.player = create_player(0, 0) + + # Generate the dungeon + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player, + max_enemies_per_room=2 + ) + + # Add player to map + self.game_map.add_entity(self.player) + + # Store reference to all entities + self.entities = [e for e in self.game_map.entities if e != self.player] + + # Initial FOV calculation + self.player.update_fov() + + # Welcome message + self.add_message("Welcome to the dungeon!", (100, 100, 255)) + + def handle_player_turn(self, action): + """Process the player's action""" + if not self.player.is_alive: + return + + if isinstance(action, MovementAction): + dest_x = self.player.x + action.dx + dest_y = self.player.y + action.dy + + # Check what's at the destination + target = self.game_map.get_blocking_entity_at(dest_x, dest_y) + + if target: + # Attack! + attack = MeleeAction(self.player, target) + result = attack.perform() + if result: + text, color = result + self.add_message(text, color) + + # Check if target died + if not target.is_alive: + death_msg = f"The {target.name.replace('remains of ', '')} is dead!" + self.add_message(death_msg, COLOR_ENEMY_DIE) + + elif not self.game_map.is_blocked(dest_x, dest_y): + # Move the player + self.player.move(action.dx, action.dy) + + elif isinstance(action, WaitAction): + pass # Do nothing + + # Enemy turns + self.handle_enemy_turns() + + def handle_enemy_turns(self): + """Let all enemies take their turn""" + for entity in self.entities: + if entity.is_alive: + # Simple AI: if player is adjacent, attack. Otherwise, do nothing. + dx = entity.x - self.player.x + dy = entity.y - self.player.y + distance = abs(dx) + abs(dy) + + if distance == 1: # Adjacent to player + attack = MeleeAction(entity, self.player) + result = attack.perform() + if result: + text, color = result + self.add_message(text, color) + + # Check if player died + if not self.player.is_alive: + self.add_message("You have died!", COLOR_PLAYER_DIE) + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + action = None + + # Movement keys + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + if dx == 0 and dy == 0: + action = WaitAction() + else: + action = MovementAction(dx, dy) + elif key == "Period": + action = WaitAction() + elif key == "Escape": + mcrfpy.setScene(None) + return + + # Process the action + if action: + self.handle_player_turn(action) + + mcrfpy.keypressScene(handle_keys) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("Combat System", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + + # Player stats + self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100) + self.hp_text.font_size = 18 + self.hp_text.fill_color = mcrfpy.Color(255, 100, 100) + self.ui.append(self.hp_text) + + # Message log + self.message_captions = [] + for i in range(self.max_messages): + caption = mcrfpy.Caption("", 50, 620 + i * 20) + caption.font_size = 14 + caption.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(caption) + self.message_captions.append(caption) + + # Timer to update HP display + def update_stats(dt): + self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}" + if self.player.hp <= 0: + self.hp_text.fill_color = mcrfpy.Color(127, 0, 0) + elif self.player.hp < self.player.max_hp // 3: + self.hp_text.fill_color = mcrfpy.Color(255, 100, 100) + else: + self.hp_text.fill_color = mcrfpy.Color(0, 255, 0) + + mcrfpy.setTimer("update_stats", update_stats, 100) + +# Create and run the game +engine = Engine() +print("Part 6: Combat System!") +print("Attack enemies to defeat them, but watch your HP!") \ No newline at end of file