diff --git a/roguelike_tutorial/PART_6_8_IMPLEMENTATION_PLAN.md b/roguelike_tutorial/PART_6_8_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..033b8e2 --- /dev/null +++ b/roguelike_tutorial/PART_6_8_IMPLEMENTATION_PLAN.md @@ -0,0 +1,393 @@ +# McRogueFace Tutorial Parts 6-8: Implementation Plan + +**Date**: Monday, July 28, 2025 +**Target Delivery**: Tuesday, July 29, 2025 + +## Executive Summary + +This document outlines the implementation plan for Parts 6-8 of the McRogueFace roguelike tutorial, adapting the libtcod Python tutorial to McRogueFace's architecture. The key discovery is that Python classes can successfully inherit from `mcrfpy.Entity` and store custom attributes, enabling a clean, Pythonic implementation. + +## Key Architectural Insights + +### Entity Inheritance Works! +```python +class GameEntity(mcrfpy.Entity): + def __init__(self, x, y, **kwargs): + super().__init__(x=x, y=y, **kwargs) + # Custom attributes work perfectly! + self.hp = 10 + self.inventory = [] + self.any_attribute = "works" +``` + +This completely changes our approach from wrapper patterns to direct inheritance. + +--- + +## Part 6: Doing (and Taking) Some Damage + +### Overview +Implement a combat system with HP tracking, damage calculation, and death mechanics using entity inheritance. + +### Core Components + +#### 1. CombatEntity Base Class +```python +class CombatEntity(mcrfpy.Entity): + """Base class for entities that can fight and take damage""" + def __init__(self, x, y, hp=10, defense=0, power=1, **kwargs): + super().__init__(x=x, y=y, **kwargs) + # Combat stats as direct attributes + self.hp = hp + self.max_hp = hp + self.defense = defense + self.power = power + self.is_alive = True + self.blocks_movement = True + + def calculate_damage(self, attacker): + """Simple damage formula: power - defense""" + return max(0, attacker.power - self.defense) + + def take_damage(self, damage, attacker=None): + """Apply damage and handle death""" + self.hp = max(0, self.hp - damage) + + if self.hp == 0 and self.is_alive: + self.is_alive = False + self.on_death(attacker) + + def on_death(self, killer=None): + """Handle death - override in subclasses""" + self.sprite_index = self.sprite_index + 180 # Corpse offset + self.blocks_movement = False +``` + +#### 2. Entity Types +```python +class PlayerEntity(CombatEntity): + """Player: HP=30, Defense=2, Power=5""" + def __init__(self, x, y, **kwargs): + kwargs['sprite_index'] = 64 # Hero sprite + super().__init__(x=x, y=y, hp=30, defense=2, power=5, **kwargs) + self.entity_type = "player" + +class OrcEntity(CombatEntity): + """Orc: HP=10, Defense=0, Power=3""" + def __init__(self, x, y, **kwargs): + kwargs['sprite_index'] = 65 # Orc sprite + super().__init__(x=x, y=y, hp=10, defense=0, power=3, **kwargs) + self.entity_type = "orc" + +class TrollEntity(CombatEntity): + """Troll: HP=16, Defense=1, Power=4""" + def __init__(self, x, y, **kwargs): + kwargs['sprite_index'] = 66 # Troll sprite + super().__init__(x=x, y=y, hp=16, defense=1, power=4, **kwargs) + self.entity_type = "troll" +``` + +#### 3. Combat Integration +- Extend `on_bump()` from Part 5 to include combat +- Add attack animations (quick bump toward target) +- Console messages initially, UI messages in Part 7 +- Death changes sprite and removes blocking + +### Key Differences from Original Tutorial +- No Fighter component - stats are direct attributes +- No AI component - behavior in entity methods +- Integrated animations for visual feedback +- Simpler architecture overall + +--- + +## Part 7: Creating the Interface + +### Overview +Add visual UI elements including health bars, message logs, and colored feedback for combat events. + +### Core Components + +#### 1. Health Bar +```python +class HealthBar: + """Health bar that reads entity HP directly""" + def __init__(self, entity, pos=(10, 740), size=(200, 20)): + self.entity = entity # Direct reference! + + # Background (dark red) + self.bg = mcrfpy.Frame(pos=pos, size=size) + self.bg.fill_color = mcrfpy.Color(64, 16, 16) + + # Foreground (green) + self.fg = mcrfpy.Frame(pos=pos, size=size) + self.fg.fill_color = mcrfpy.Color(0, 96, 0) + + # Text overlay + self.text = mcrfpy.Caption( + pos=(pos[0] + 5, pos[1] + 2), + text=f"HP: {entity.hp}/{entity.max_hp}" + ) + + def update(self): + """Update based on entity's current HP""" + ratio = self.entity.hp / self.entity.max_hp + self.fg.w = int(self.bg.w * ratio) + self.text.text = f"HP: {self.entity.hp}/{self.entity.max_hp}" + + # Color changes at low health + if ratio < 0.25: + self.fg.fill_color = mcrfpy.Color(196, 16, 16) # Red + elif ratio < 0.5: + self.fg.fill_color = mcrfpy.Color(196, 196, 16) # Yellow +``` + +#### 2. Message Log +```python +class MessageLog: + """Scrolling message log for combat feedback""" + def __init__(self, pos=(10, 600), size=(400, 120), max_messages=6): + self.frame = mcrfpy.Frame(pos=pos, size=size) + self.messages = [] # List of (text, color) tuples + self.captions = [] # Pre-allocated Caption pool + + def add_message(self, text, color=None): + """Add message with optional color""" + # Handle duplicate detection (x2, x3, etc.) + # Update caption display +``` + +#### 3. Color System +```python +class Colors: + # Combat colors + PLAYER_ATTACK = mcrfpy.Color(224, 224, 224) + ENEMY_ATTACK = mcrfpy.Color(255, 192, 192) + PLAYER_DEATH = mcrfpy.Color(255, 48, 48) + ENEMY_DEATH = mcrfpy.Color(255, 160, 48) + HEALTH_RECOVERED = mcrfpy.Color(0, 255, 0) +``` + +### UI Layout +- Health bar at bottom of screen +- Message log above health bar +- Direct binding to entity attributes +- Real-time updates during gameplay + +--- + +## Part 8: Items and Inventory + +### Overview +Implement items as entities, inventory management, and a hotbar-style UI for item usage. + +### Core Components + +#### 1. Item Entities +```python +class ItemEntity(mcrfpy.Entity): + """Base class for pickupable items""" + def __init__(self, x, y, name, sprite, **kwargs): + kwargs['sprite_index'] = sprite + super().__init__(x=x, y=y, **kwargs) + self.item_name = name + self.blocks_movement = False + self.item_type = "generic" + +class HealingPotion(ItemEntity): + """Consumable healing item""" + def __init__(self, x, y, healing_amount=4): + super().__init__(x, y, "Healing Potion", sprite=33) + self.healing_amount = healing_amount + self.item_type = "consumable" + + def use(self, user): + """Use the potion - returns (success, message)""" + if hasattr(user, 'hp'): + healed = min(self.healing_amount, user.max_hp - user.hp) + if healed > 0: + user.hp += healed + return True, f"You heal {healed} HP!" +``` + +#### 2. Inventory System +```python +class InventoryMixin: + """Mixin for entities with inventory""" + def __init__(self, *args, capacity=10, **kwargs): + super().__init__(*args, **kwargs) + self.inventory = [] + self.inventory_capacity = capacity + + def pickup_item(self, item): + """Pick up an item entity""" + if len(self.inventory) >= self.inventory_capacity: + return False, "Inventory full!" + self.inventory.append(item) + item.die() # Remove from grid + return True, f"Picked up {item.item_name}." +``` + +#### 3. Inventory UI +```python +class InventoryDisplay: + """Hotbar-style inventory display""" + def __init__(self, entity, pos=(200, 700), slots=10): + # Create slot frames and sprites + # Number keys 1-9, 0 for slots + # Highlight selected slot + # Update based on entity.inventory +``` + +### Key Features +- Items exist as entities on the grid +- Direct inventory attribute on player +- Hotkey-based usage (1-9, 0) +- Visual hotbar display +- Item effects (healing, future: damage boost, etc.) + +--- + +## Implementation Timeline + +### Tuesday Morning (Priority 1: Core Systems) +1. **8:00-9:30**: Implement CombatEntity and entity types +2. **9:30-10:30**: Add combat to bump interactions +3. **10:30-11:30**: Basic health display (text or simple bar) +4. **11:30-12:00**: ItemEntity and pickup system + +### Tuesday Afternoon (Priority 2: Integration) +1. **1:00-2:00**: Message log implementation +2. **2:00-3:00**: Full health bar with colors +3. **3:00-4:00**: Inventory UI (hotbar) +4. **4:00-5:00**: Testing and bug fixes + +### Tuesday Evening (Priority 3: Polish) +1. **5:00-6:00**: Combat animations and effects +2. **6:00-7:00**: Sound integration (use CoS splat sounds) +3. **7:00-8:00**: Additional item types +4. **8:00-9:00**: Documentation and cleanup + +--- + +## Testing Strategy + +### Automated Tests +```python +# tests/test_part6_combat.py +- Test damage calculation +- Test death mechanics +- Test combat messages + +# tests/test_part7_ui.py +- Test health bar updates +- Test message log scrolling +- Test color system + +# tests/test_part8_inventory.py +- Test item pickup/drop +- Test inventory capacity +- Test item usage +``` + +### Visual Tests +- Screenshot combat states +- Verify UI element positioning +- Check animation smoothness + +--- + +## File Structure +``` +roguelike_tutorial/ +├── part_6.py # Combat implementation +├── part_7.py # UI enhancements +├── part_8.py # Inventory system +├── combat.py # Shared combat utilities +├── ui_components.py # Reusable UI classes +├── colors.py # Color definitions +└── items.py # Item definitions +``` + +--- + +## Risk Mitigation + +### Potential Issues +1. **Performance**: Many UI updates per frame + - Solution: Update only on state changes + +2. **Entity Collection Bugs**: Known segfault issues + - Solution: Use index-based access when needed + +3. **Animation Timing**: Complex with turn-based combat + - Solution: Queue animations, process sequentially + +### Fallback Options +1. Start with console messages, add UI later +2. Simple health numbers before bars +3. Basic inventory list before hotbar + +--- + +## Success Criteria + +### Part 6 +- [x] Entities can have HP and take damage +- [x] Death changes sprite and walkability +- [x] Combat messages appear +- [x] Player can kill enemies + +### Part 7 +- [x] Health bar shows current/max HP +- [x] Messages appear in scrolling log +- [x] Colors differentiate message types +- [x] UI updates in real-time + +### Part 8 +- [x] Items can be picked up +- [x] Inventory has capacity limit +- [x] Items can be used/consumed +- [x] Hotbar shows inventory items + +--- + +## Notes for Implementation + +1. **Keep It Simple**: Start with minimum viable features +2. **Build Incrementally**: Test each component before integrating +3. **Use Part 5**: Leverage existing entity interaction system +4. **Document Well**: Clear comments for tutorial purposes +5. **Visual Feedback**: McRogueFace excels at animations - use them! + +--- + +## Comparison with Original Tutorial + +### What We Keep +- Same combat formula (power - defense) +- Same entity stats (Player, Orc, Troll) +- Same item types (healing potions to start) +- Same UI elements (health bar, message log) + +### What's Different +- Direct inheritance instead of components +- Integrated animations and visual effects +- Hotbar inventory instead of menu +- Built-in sound support +- Cleaner architecture overall + +### What's Better +- More Pythonic with real inheritance +- Better visual feedback +- Smoother animations +- Simpler to understand +- Leverages McRogueFace's strengths + +--- + +## Conclusion + +This implementation plan leverages McRogueFace's support for Python entity inheritance to create a clean, intuitive tutorial series. By using direct attributes instead of components, we simplify the architecture while maintaining all the functionality of the original tutorial. The addition of animations, sound effects, and rich UI elements showcases McRogueFace's capabilities while keeping the code beginner-friendly. + +The Tuesday delivery timeline is aggressive but achievable by focusing on core functionality first, then integration, then polish. The modular design allows for easy testing and incremental development. \ No newline at end of file diff --git a/roguelike_tutorial/README_PART_01.md b/roguelike_tutorial/README_PART_01.md new file mode 100644 index 0000000..4d8e156 --- /dev/null +++ b/roguelike_tutorial/README_PART_01.md @@ -0,0 +1,100 @@ +# Simple TCOD Tutorial Part 1 - Drawing the player sprite and moving it around + +This is Part 1 of the Simple TCOD Tutorial adapted for McRogueFace. It implements the sophisticated, refactored TCOD tutorial approach with professional architecture from day one. + +## Running the Code + +From your tutorial build directory (separate from the engine development build): +```bash +cd simple_tcod_tutorial/build +./mcrogueface scripts/main.py +``` + +Note: The `scripts` folder should be a symlink to your `simple_tcod_tutorial` directory. + +## Architecture Overview + +### Package Structure +``` +simple_tcod_tutorial/ +├── main.py # Entry point - ties everything together +├── game/ # Game package with proper separation +│ ├── __init__.py +│ ├── entity.py # Entity class - all game objects +│ ├── engine.py # Engine class - game coordinator +│ ├── actions.py # Action classes - command pattern +│ └── input_handlers.py # Input handling - extensible system +``` + +### Key Concepts Demonstrated + +1. **Entity-Centric Design** + - Everything in the game is an Entity + - Entities have position, appearance, and behavior + - Designed to scale to items, NPCs, and effects + +2. **Action-Based Command Pattern** + - All player actions are Action objects + - Separates input from game logic + - Enables undo, replay, and AI using same system + +3. **Professional Input Handling** + - BaseEventHandler for different input contexts + - Complete movement key support (arrows, numpad, vi, WASD) + - Ready for menus, targeting, and other modes + +4. **Engine as Coordinator** + - Manages game state without becoming a god object + - Delegates to appropriate systems + - Clean boundaries between systems + +5. **Type Safety** + - Full type annotations throughout + - Forward references with TYPE_CHECKING + - Modern Python best practices + +## Differences from Vanilla McRogueFace Tutorial + +### Removed +- Animation system (instant movement instead) +- Complex UI elements (focus on core mechanics) +- Real-time features (pure turn-based) +- Visual effects (camera following, smooth scrolling) +- Entity color property (sprites handle appearance) + +### Added +- Complete movement key support +- Professional architecture patterns +- Proper package structure +- Type annotations +- Action-based design +- Extensible handler system +- Proper exit handling (Escape/Q actually quits) + +### Adapted +- Grid rendering with proper centering +- Simplified entity system (position + sprite ID) +- Using simple_tutorial.png sprite sheet (12 sprites) +- Floor tiles using ground sprites (indices 1 and 2) +- Direct sprite indices instead of character mapping + +## Learning Objectives + +Students completing Part 1 will understand: +- How to structure a game project professionally +- The value of entity-centric design +- Command pattern for game actions +- Input handling that scales to complex UIs +- Type-driven development in Python +- Architecture that grows without refactoring + +## What's Next + +Part 2 will add: +- The GameMap class for world representation +- Tile-based movement and collision +- Multiple entities in the world +- Basic terrain (walls and floors) +- Rendering order for entities + +The architecture we've built in Part 1 makes these additions natural and painless, demonstrating the value of starting with good patterns. \ No newline at end of file diff --git a/roguelike_tutorial/README_PART_02.md b/roguelike_tutorial/README_PART_02.md new file mode 100644 index 0000000..a34f78f --- /dev/null +++ b/roguelike_tutorial/README_PART_02.md @@ -0,0 +1,82 @@ +# Simple TCOD Tutorial Part 2 - The generic Entity, the map, and walls + +This is Part 2 of the Simple TCOD Tutorial adapted for McRogueFace. Building on Part 1's foundation, we now introduce proper world representation and collision detection. + +## Running the Code + +From your tutorial build directory: +```bash +cd simple_tcod_tutorial/build +./mcrogueface scripts/main.py +``` + +## New Architecture Components + +### GameMap Class (`game/game_map.py`) +The GameMap inherits from `mcrfpy.Grid` and adds: +- **Tile Management**: Uses Grid's built-in point system with walkable property +- **Entity Container**: Manages entity lifecycle with `add_entity()` and `remove_entity()` +- **Spatial Queries**: `get_entities_at()`, `get_blocking_entity_at()`, `is_walkable()` +- **Direct Integration**: Leverages Grid's walkable and tilesprite properties + +### Tiles System (`game/tiles.py`) +- **Simple Tile Types**: Using NamedTuple for clean tile definitions +- **Tile Types**: Floor (walkable) and Wall (blocks movement) +- **Grid Integration**: Maps directly to Grid point properties +- **Future-Ready**: Includes transparency for FOV system in Part 4 + +### Entity Placement System +- **Bidirectional References**: Entities know their map, maps track their entities +- **`place()` Method**: Handles all bookkeeping when entities move between maps +- **Lifecycle Management**: Automatic cleanup when entities leave maps + +## Key Changes from Part 1 + +### Engine Updates +- Replaced direct grid management with GameMap +- Engine creates and configures the GameMap +- Player is placed using the new `place()` method + +### Movement System +- MovementAction now checks `is_walkable()` before moving +- Collision detection for both walls and blocking entities +- Clean separation between validation and execution + +### Visual Changes +- Walls rendered as trees (sprite index 3) +- Border of walls around the map edge +- Floor tiles still use alternating pattern + +## Architectural Benefits + +### McRogueFace Integration +- **No NumPy Dependency**: Uses Grid's native tile management +- **Direct Walkability**: Grid points have built-in walkable property +- **Unified System**: Visual and logical tile data in one place + +### Separation of Concerns +- **GameMap**: Knows about tiles and spatial relationships +- **Engine**: Coordinates high-level game state +- **Entity**: Manages its own lifecycle through `place()` +- **Actions**: Validate their own preconditions + +### Extensibility +- Easy to add new tile types +- Simple to implement different map generation +- Ready for FOV, pathfinding, and complex queries +- Entity system scales to items and NPCs + +### Type Safety +- TYPE_CHECKING imports prevent circular dependencies +- Proper type hints throughout +- Forward references maintain clean architecture + +## What's Next + +Part 3 will add: +- Procedural dungeon generation +- Room and corridor creation +- Multiple entities in the world +- Foundation for enemy placement + +The architecture established in Part 2 makes these additions straightforward, demonstrating the value of proper design from the beginning. \ No newline at end of file diff --git a/roguelike_tutorial/README_PART_03.md b/roguelike_tutorial/README_PART_03.md new file mode 100644 index 0000000..7bd9696 --- /dev/null +++ b/roguelike_tutorial/README_PART_03.md @@ -0,0 +1,87 @@ +# Simple TCOD Tutorial Part 3 - Generating a dungeon + +This is Part 3 of the Simple TCOD Tutorial adapted for McRogueFace. We now add procedural dungeon generation to create interesting, playable levels. + +## Running the Code + +From your tutorial build directory: +```bash +cd simple_tcod_tutorial/build +./mcrogueface scripts/main.py +``` + +## New Features + +### Procedural Generation Module (`game/procgen.py`) + +This dedicated module demonstrates separation of concerns - dungeon generation logic is kept separate from the game map implementation. + +#### RectangularRoom Class +- **Clean Abstraction**: Represents a room with position and dimensions +- **Utility Properties**: + - `center` - Returns room center for connections + - `inner` - Returns slice objects for efficient carving +- **Intersection Detection**: `intersects()` method prevents overlapping rooms + +#### Tunnel Generation +- **L-Shaped Corridors**: Simple but effective connection method +- **Iterator Pattern**: `tunnel_between()` yields coordinates efficiently +- **Random Variation**: 50/50 chance of horizontal-first vs vertical-first + +#### Dungeon Generation Algorithm +```python +def generate_dungeon(max_rooms, room_min_size, room_max_size, + map_width, map_height, engine) -> GameMap: +``` +- **Simple Algorithm**: Try to place random rooms, reject overlaps +- **Automatic Connection**: Each room connects to the previous one +- **Player Placement**: First room contains the player +- **Entity-Centric**: Uses `player.place()` for proper lifecycle + +## Architecture Benefits + +### Modular Design +- Generation logic separate from GameMap +- Easy to swap algorithms later +- Room class reusable for other features + +### Forward Thinking +- Engine parameter anticipates entity spawning +- Room list available for future features +- Iterator-based tunnel generation is memory efficient + +### Clean Integration +- Works seamlessly with existing entity placement +- Respects GameMap's tile management +- No special cases or hacks needed + +## Visual Changes + +- Map size increased to 80x45 for better dungeons +- Zoom reduced to 1.0 to see more of the map +- Random room layouts each time +- Connected rooms and corridors + +## Algorithm Details + +The generation follows these steps: +1. Start with a map filled with walls +2. Try to place up to `max_rooms` rooms +3. For each room attempt: + - Generate random size and position + - Check for intersections with existing rooms + - If valid, carve out the room + - Connect to previous room (if any) +4. Place player in center of first room + +This simple algorithm creates playable dungeons while being easy to understand and modify. + +## What's Next + +Part 4 will add: +- Field of View (FOV) system +- Explored vs unexplored areas +- Light and dark tile rendering +- Torch radius around player + +The modular dungeon generation makes it easy to add these visual features without touching the generation code. \ No newline at end of file diff --git a/roguelike_tutorial/README_PART_04.md b/roguelike_tutorial/README_PART_04.md new file mode 100644 index 0000000..8b38e4f --- /dev/null +++ b/roguelike_tutorial/README_PART_04.md @@ -0,0 +1,131 @@ +# Part 4: Field of View and Exploration + +## Overview + +Part 4 introduces the Field of View (FOV) system, transforming our fully-visible dungeon into an atmospheric exploration experience. We leverage McRogueFace's built-in FOV capabilities and perspective system for efficient rendering. + +## What's New in Part 4 + +### Field of View System +- **FOV Calculation**: Using `Grid.compute_fov()` with configurable radius +- **Perspective System**: Grid tracks which entity is the viewer +- **Visibility States**: Unexplored (black), explored (dark), visible (lit) +- **Automatic Updates**: FOV recalculates on player movement + +### Implementation Details + +#### FOV with McRogueFace's Grid + +Unlike TCOD which uses numpy arrays for visibility tracking, McRogueFace's Grid has built-in FOV support: + +```python +# In GameMap.update_fov() +self.compute_fov(viewer_x, viewer_y, radius, light_walls=True, algorithm=mcrfpy.FOV_BASIC) +``` + +The Grid automatically: +- Tracks which tiles have been explored +- Applies appropriate color overlays (shroud, dark, light) +- Updates entity visibility based on FOV + +#### Perspective System + +McRogueFace uses a perspective-based rendering approach: + +```python +# Set the viewer +self.game_map.perspective = self.player + +# Grid automatically renders from this entity's viewpoint +``` + +This is more efficient than manually updating tile colors every turn. + +#### Color Overlays + +We define overlay colors but let the Grid handle application: + +```python +# In tiles.py +SHROUD = mcrfpy.Color(0, 0, 0, 255) # Unexplored +DARK = mcrfpy.Color(100, 100, 150, 128) # Explored but not visible +LIGHT = mcrfpy.Color(255, 255, 255, 0) # Currently visible +``` + +### Key Differences from TCOD + +| TCOD Approach | McRogueFace Approach | +|---------------|----------------------| +| `visible` and `explored` numpy arrays | Grid's built-in FOV state | +| Manual tile color switching | Automatic overlay system | +| `tcod.map.compute_fov()` | `Grid.compute_fov()` | +| Render conditionals for each tile | Perspective-based rendering | + +### Movement and FOV Updates + +The action system now updates FOV after player movement: + +```python +# In MovementAction.perform() +if self.entity == engine.player: + engine.update_fov() +``` + +## Architecture Notes + +### Why Grid Perspective? + +The perspective system provides several benefits: +1. **Efficiency**: No per-tile color updates needed +2. **Flexibility**: Easy to switch viewpoints (for debugging or features) +3. **Automatic**: Grid handles all rendering details +4. **Clean**: Separates game logic from rendering concerns + +### Entity Visibility + +Entities automatically update their visibility state: + +```python +# After FOV calculation +self.player.update_visibility() +``` + +This ensures entities are only rendered when visible to the current perspective. + +## Files Modified + +- `game/tiles.py`: Added FOV color overlay constants +- `game/game_map.py`: Added `update_fov()` method +- `game/engine.py`: Added FOV initialization and update method +- `game/actions.py`: Update FOV after player movement +- `main.py`: Updated part description + +## What's Next + +Part 5 will add enemies to our dungeon, introducing: +- Enemy entities with AI +- Combat system +- Turn-based gameplay +- Health and damage + +The FOV system will make enemies appear and disappear as you explore, adding tension and strategy to the gameplay. + +## Learning Points + +1. **Leverage Framework Features**: Use McRogueFace's built-in systems rather than reimplementing +2. **Perspective-Based Design**: Think in terms of viewpoints, not global state +3. **Automatic Systems**: Let the framework handle rendering details +4. **Clean Integration**: FOV updates fit naturally into the action system + +## Running Part 4 + +```bash +cd simple_tcod_tutorial/build +./mcrogueface scripts/main.py +``` + +You'll now see: +- Black unexplored areas +- Dark blue tint on previously seen areas +- Full brightness only in your field of view +- Smooth exploration as you move through the dungeon \ No newline at end of file diff --git a/roguelike_tutorial/README_PART_05.md b/roguelike_tutorial/README_PART_05.md new file mode 100644 index 0000000..d85ae21 --- /dev/null +++ b/roguelike_tutorial/README_PART_05.md @@ -0,0 +1,169 @@ +# Part 5: Placing Enemies and Fighting Them + +## Overview + +Part 5 brings our dungeon to life with enemies! We add rats and spiders that populate the rooms, implement a combat system with melee attacks, and handle entity death by turning creatures into gravestones. + +## What's New in Part 5 + +### Actor System +- **Actor Class**: Extends Entity with combat stats (HP, defense, power) +- **Combat Properties**: Health tracking, damage calculation, alive status +- **Death Handling**: Entities become gravestones when killed + +### Enemy Types +Using our sprite sheet, we have two enemy types: +- **Rat** (sprite 5): 10 HP, 0 defense, 3 power - Common enemy +- **Spider** (sprite 4): 16 HP, 1 defense, 4 power - Tougher enemy + +### Combat System + +#### Bump-to-Attack +When the player tries to move into an enemy: +```python +# In MovementAction.perform() +target = engine.game_map.get_blocking_entity_at(dest_x, dest_y) +if target: + if self.entity == engine.player: + from game.entity import Actor + if isinstance(target, Actor) and target != engine.player: + return MeleeAction(self.entity, self.dx, self.dy).perform(engine) +``` + +#### Damage Calculation +Simple formula with defense reduction: +```python +damage = attacker.power - target.defense +``` + +#### Death System +Dead entities become gravestones: +```python +def die(self) -> None: + """Handle death by becoming a gravestone.""" + self.sprite_index = 6 # Tombstone sprite + self.blocks_movement = False + self.name = f"Grave of {self.name}" +``` + +### Entity Factories + +Factory functions create pre-configured entities: +```python +def rat(x: int, y: int, texture: mcrfpy.Texture) -> Actor: + return Actor( + x=x, y=y, + sprite_id=5, # Rat sprite + texture=texture, + name="Rat", + hp=10, defense=0, power=3, + ) +``` + +### Dungeon Population + +Enemies are placed randomly in rooms: +```python +def place_entities(room, dungeon, max_monsters, texture): + number_of_monsters = random.randint(0, max_monsters) + + for _ in range(number_of_monsters): + x = random.randint(room.x1 + 1, room.x2 - 1) + y = random.randint(room.y1 + 1, room.y2 - 1) + + if not any(entity.x == x and entity.y == y for entity in dungeon.entities): + # 80% rats, 20% spiders + if random.random() < 0.8: + monster = entity_factories.rat(x, y, texture) + else: + monster = entity_factories.spider(x, y, texture) + monster.place(x, y, dungeon) +``` + +## Key Implementation Details + +### FOV and Enemy Visibility +Enemies are automatically shown/hidden by the FOV system: +```python +def update_fov(self) -> None: + # Update visibility for all entities + for entity in self.game_map.entities: + entity.update_visibility() +``` + +### Action System Extension +The action system now handles combat: +- **MovementAction**: Detects collision, triggers attack +- **MeleeAction**: New action for melee combat +- Actions remain decoupled from entity logic + +### Gravestone System +Instead of removing dead entities: +- Sprite changes to tombstone (index 6) +- Name changes to "Grave of [Name]" +- No longer blocks movement +- Remains visible as dungeon decoration + +## Architecture Notes + +### Why Actor Extends Entity? +- Maintains entity hierarchy +- Combat stats only for creatures +- Future items/decorations won't have HP +- Clean separation of concerns + +### Why Factory Functions? +- Centralized entity configuration +- Easy to add new enemy types +- Consistent stat management +- Type-safe entity creation + +### Combat in Actions +Combat logic lives in actions, not entities: +- Entities store stats +- Actions perform combat +- Clean separation of data and behavior +- Extensible for future combat types + +## Files Modified + +- `game/entity.py`: Added Actor class with combat stats and death handling +- `game/entity_factories.py`: New module with entity creation functions +- `game/actions.py`: Added MeleeAction for combat +- `game/procgen.py`: Added enemy placement in rooms +- `game/engine.py`: Updated to use Actor type and handle all entity visibility +- `main.py`: Updated to use entity factories and Part 5 description + +## What's Next + +Part 6 will enhance the combat experience with: +- Health display UI +- Game over conditions +- Combat messages window +- More strategic combat mechanics + +## Learning Points + +1. **Entity Specialization**: Use inheritance to add features to specific entity types +2. **Factory Pattern**: Centralize object creation for consistency +3. **State Transformation**: Dead entities become decorations, not deletions +4. **Action Extensions**: Combat fits naturally into the action system +5. **Automatic Systems**: FOV handles entity visibility without special code + +## Running Part 5 + +```bash +cd simple_tcod_tutorial/build +./mcrogueface scripts/main.py +``` + +You'll now encounter rats and spiders as you explore! Walk into them to attack. Dead enemies become gravestones that mark your battles. + +## Sprite Adaptations + +Following our sprite sheet (`sprite_sheet.md`), we made these thematic changes: +- Orcs → Rats (same stats, different sprite) +- Trolls → Spiders (same stats, different sprite) +- Corpses → Gravestones (all use same tombstone sprite) + +The gameplay remains identical to the TCOD tutorial, just with different visual theming. \ No newline at end of file diff --git a/roguelike_tutorial/README_PART_06.md b/roguelike_tutorial/README_PART_06.md new file mode 100644 index 0000000..1348a9f --- /dev/null +++ b/roguelike_tutorial/README_PART_06.md @@ -0,0 +1,187 @@ +# Part 6: Doing (and Taking) Damage + +## Overview + +Part 6 transforms our basic combat into a complete gameplay loop with visual feedback, enemy AI, and win/lose conditions. We add a health bar, message log, enemy AI that pursues the player, and proper game over handling. + +## What's New in Part 6 + +### User Interface Components + +#### Health Bar +A visual representation of the player's current health: +```python +class HealthBar: + def create_ui(self) -> List[mcrfpy.UIDrawable]: + # Dark red background + self.background = mcrfpy.Frame(pos=(x, y), size=(width, height)) + self.background.fill_color = mcrfpy.Color(100, 0, 0, 255) + + # Bright colored bar (green/yellow/red based on HP) + self.bar = mcrfpy.Frame(pos=(x, y), size=(width, height)) + + # Text overlay showing HP numbers + self.text = mcrfpy.Caption(pos=(x+5, y+2), + text=f"HP: {hp}/{max_hp}") +``` + +The bar changes color based on health percentage: +- Green (>60% health) +- Yellow (30-60% health) +- Red (<30% health) + +#### Message Log +A scrolling combat log that replaces console print statements: +```python +class MessageLog: + def __init__(self, max_messages: int = 5): + self.messages: deque[str] = deque(maxlen=max_messages) + + def add_message(self, message: str) -> None: + self.messages.append(message) + self.update_display() +``` + +Messages include: +- Combat actions ("Rat attacks Player for 3 hit points.") +- Death notifications ("Spider is dead!") +- Game state changes ("You have died! Press Escape to quit.") + +### Enemy AI System + +#### Basic AI Component +Enemies now actively pursue and attack the player: +```python +class BasicAI: + def take_turn(self, engine: Engine) -> None: + distance = max(abs(dx), abs(dy)) # Chebyshev distance + + if distance <= 1: + # Adjacent: Attack! + MeleeAction(self.entity, attack_dx, attack_dy).perform(engine) + elif distance <= 6: + # Can see player: Move closer + MovementAction(self.entity, move_dx, move_dy).perform(engine) +``` + +#### Turn-Based System +After each player action, all enemies take their turn: +```python +def handle_enemy_turns(self) -> None: + for entity in self.game_map.entities: + if isinstance(entity, Actor) and entity.ai and entity.is_alive: + entity.ai.take_turn(self) +``` + +### Game Over Condition + +When the player dies: +1. Game state flag is set (`engine.game_over = True`) +2. Player becomes a gravestone (sprite changes) +3. Input is restricted (only Escape works) +4. Death message appears in the message log + +```python +def handle_player_death(self) -> None: + self.game_over = True + self.message_log.add_message("You have died! Press Escape to quit.") +``` + +## Architecture Improvements + +### UI Module (`game/ui.py`) +Separates UI concerns from game logic: +- `MessageLog`: Manages combat messages +- `HealthBar`: Displays player health +- Clean interface for updating displays + +### AI Module (`game/ai.py`) +Encapsulates enemy behavior: +- `BasicAI`: Simple pursue-and-attack behavior +- Extensible for different AI types +- Uses existing action system + +### Turn Management +Player actions trigger enemy turns: +- Movement → Enemy turns +- Attack → Enemy turns +- Wait → Enemy turns +- Maintains turn-based feel + +## Key Implementation Details + +### UI Updates +Health bar updates occur: +- After player takes damage +- Automatically via `engine.update_ui()` +- Color changes based on HP percentage + +### Message Flow +Combat messages follow this pattern: +1. Action generates message text +2. `engine.message_log.add_message(text)` +3. Message appears in UI Caption +4. Old messages scroll up + +### AI Decision Making +Basic AI uses simple rules: +1. Check if player is adjacent → Attack +2. Check if player is visible (within 6 tiles) → Move toward +3. Otherwise → Do nothing + +### Game State Management +The `game_over` flag prevents: +- Player movement +- Player attacks +- Player waiting +- But allows Escape to quit + +## Files Modified + +- `game/ui.py`: New module for UI components +- `game/ai.py`: New module for enemy AI +- `game/engine.py`: Added UI setup, enemy turns, game over handling +- `game/entity.py`: Added AI component to Actor +- `game/entity_factories.py`: Attached AI to enemies +- `game/actions.py`: Integrated message log, added enemy turn triggers +- `main.py`: Updated part description + +## What's Next + +Part 7 will expand the user interface further with: +- More detailed entity inspection +- Possibly inventory display +- Additional UI panels +- Mouse interaction + +## Learning Points + +1. **UI Separation**: Keep UI logic separate from game logic +2. **Component Systems**: AI as a component allows different behaviors +3. **Turn-Based Flow**: Player action → Enemy reactions creates tactical gameplay +4. **Visual Feedback**: Health bars and message logs improve player understanding +5. **State Management**: Game over flag controls available actions + +## Running Part 6 + +```bash +cd simple_tcod_tutorial/build +./mcrogueface scripts/main.py +``` + +You'll now see: +- Health bar at the top showing your current HP +- Message log at the bottom showing combat events +- Enemies that chase you when you're nearby +- Enemies that attack when adjacent +- Death state when HP reaches 0 + +## Combat Strategy + +With enemy AI active, combat becomes more tactical: +- Enemies pursue when they see you +- Fighting in corridors limits how many can attack +- Running away is sometimes the best option +- Health management becomes critical + +The game now has a complete combat loop with clear win/lose conditions! \ No newline at end of file diff --git a/roguelike_tutorial/README_PART_07.md b/roguelike_tutorial/README_PART_07.md new file mode 100644 index 0000000..92034d8 --- /dev/null +++ b/roguelike_tutorial/README_PART_07.md @@ -0,0 +1,204 @@ +# Part 7: Creating the User Interface + +## Overview + +Part 7 significantly enhances the user interface, transforming our roguelike from a basic game into a more polished experience. We add mouse interaction, help displays, information panels, and better visual feedback systems. + +## What's New in Part 7 + +### Mouse Interaction + +#### Click-to-Inspect System +Since McRogueFace doesn't have mouse motion events, we use click events to show entity information: +```python +def grid_click_handler(pixel_x, pixel_y, button, state): + # Convert pixel coordinates to grid coordinates + grid_x = int(pixel_x / (self.tile_size * self.zoom)) + grid_y = int(pixel_y / (self.tile_size * self.zoom)) + + # Update hover display for this position + self.update_mouse_hover(grid_x, grid_y) +``` + +Click displays show: +- Entity names +- Current HP for living creatures +- Multiple entities if stacked (e.g., "Grave of Rat") + +#### Mouse Handler Registration +The click handler is registered as a local function to avoid issues with bound methods: +```python +# Use a local function instead of a bound method +self.game_map.click = grid_click_handler +``` + +### Help System + +#### Toggle Help Display +Press `?`, `H`, or `F1` to show/hide help: +```python +class HelpDisplay: + def toggle(self) -> None: + self.visible = not self.visible + self.panel.frame.visible = self.visible +``` + +The help panel includes: +- Movement controls for all input methods +- Combat instructions +- Mouse usage tips +- Gameplay strategies + +### Information Panels + +#### Player Stats Panel +Always-visible panel showing: +- Player name +- Current/Max HP +- Power and Defense stats +- Current grid position + +```python +class InfoPanel: + def create_ui(self, title: str) -> List[mcrfpy.Drawable]: + # Semi-transparent background frame + self.frame = mcrfpy.Frame(pos=(x, y), size=(width, height)) + self.frame.fill_color = mcrfpy.Color(20, 20, 40, 200) + + # Title and content captions as children + self.frame.children.append(self.title_caption) + self.frame.children.append(self.content_caption) +``` + +#### Reusable Panel System +The `InfoPanel` class provides: +- Titled panels with borders +- Semi-transparent backgrounds +- Easy content updates +- Consistent visual style + +### Enhanced UI Components + +#### MouseHoverDisplay Class +Manages tooltip-style hover information: +- Follows mouse position +- Shows/hides automatically +- Offset to avoid cursor overlap +- Multiple entity support + +#### UI Module Organization +Clean separation of UI components: +- `MessageLog`: Combat messages +- `HealthBar`: HP visualization +- `MouseHoverDisplay`: Entity inspection +- `InfoPanel`: Generic information display +- `HelpDisplay`: Keyboard controls + +## Architecture Improvements + +### UI Composition +Using McRogueFace's parent-child system: +```python +# Add caption as child of frame +self.frame.children.append(self.text_caption) +``` + +Benefits: +- Automatic relative positioning +- Group visibility control +- Clean hierarchy + +### Event Handler Extensions +Input handler now manages: +- Keyboard input (existing) +- Mouse motion (new) +- Mouse clicks (prepared for future) +- UI toggles (help display) + +### Dynamic Content Updates +All UI elements support real-time updates: +```python +def update_stats_panel(self) -> None: + stats_text = f"""Name: {self.player.name} +HP: {self.player.hp}/{self.player.max_hp} +Power: {self.player.power} +Defense: {self.player.defense}""" + self.stats_panel.update_content(stats_text) +``` + +## Key Implementation Details + +### Mouse Coordinate Conversion +Pixel to grid conversion: +```python +grid_x = int(x / (self.engine.tile_size * self.engine.zoom)) +grid_y = int(y / (self.engine.tile_size * self.engine.zoom)) +``` + +### Visibility Management +UI elements can be toggled: +- Help panel starts hidden +- Mouse hover hides when not over entities +- Panels can be shown/hidden dynamically + +### Color and Transparency +UI uses semi-transparent overlays: +- Panel backgrounds: `Color(20, 20, 40, 200)` +- Hover tooltips: `Color(255, 255, 200, 255)` +- Borders and outlines for readability + +## Files Modified + +- `game/ui.py`: Added MouseHoverDisplay, InfoPanel, HelpDisplay classes +- `game/engine.py`: Integrated new UI components, mouse hover handling +- `game/input_handlers.py`: Added mouse motion handling, help toggle +- `main.py`: Registered mouse handlers, updated part description + +## What's Next + +Part 8 will add items and inventory: +- Collectible items (potions, equipment) +- Inventory management UI +- Item usage mechanics +- Equipment system + +## Learning Points + +1. **UI Composition**: Use parent-child relationships for complex UI +2. **Event Delegation**: Separate input handling from UI updates +3. **Information Layers**: Multiple UI systems can coexist (hover, panels, help) +4. **Visual Polish**: Small touches like transparency and borders improve UX +5. **Reusable Components**: Generic panels can be specialized for different uses + +## Running Part 7 + +```bash +cd simple_tcod_tutorial/build +./mcrogueface scripts/main.py +``` + +New features to try: +- Click on entities to see their details +- Press ? or H to toggle help display +- Watch the stats panel update as you take damage +- See entity HP in hover tooltips +- Notice the visual polish in UI panels + +## UI Design Principles + +### Consistency +- All panels use similar visual style +- Consistent color scheme +- Uniform text sizing + +### Non-Intrusive +- Semi-transparent panels don't block view +- Hover info appears near cursor +- Help can be toggled off + +### Information Hierarchy +- Critical info (health) always visible +- Contextual info (hover) on demand +- Help info toggleable + +The UI now provides a professional feel while maintaining the roguelike aesthetic! \ No newline at end of file diff --git a/roguelike_tutorial/README_PART_08.md b/roguelike_tutorial/README_PART_08.md new file mode 100644 index 0000000..a6dfe2b --- /dev/null +++ b/roguelike_tutorial/README_PART_08.md @@ -0,0 +1,297 @@ +# Part 8: Items and Inventory + +## Overview + +Part 8 transforms our roguelike into a proper loot-driven game by adding items that can be collected, managed, and used. We implement a flexible inventory system with capacity limits, create consumable items like healing potions, and build UI for inventory management. + +## What's New in Part 8 + +### Parent-Child Entity Architecture + +#### Flexible Entity Ownership +Entities now have parent containers, allowing them to exist in different contexts: +```python +class Entity(mcrfpy.Entity): + def __init__(self, parent: Optional[Union[GameMap, Inventory]] = None): + self.parent = parent + + @property + def gamemap(self) -> Optional[GameMap]: + """Get the GameMap through the parent chain""" + if isinstance(self.parent, Inventory): + return self.parent.gamemap + return self.parent +``` + +Benefits: +- Items can exist in the world or in inventories +- Clean ownership transfer when picking up/dropping +- Automatic visibility management + +### Inventory System + +#### Container-Based Design +The inventory acts like a specialized entity container: +```python +class Inventory: + def __init__(self, capacity: int): + self.capacity = capacity + self.items: List[Item] = [] + self.parent: Optional[Actor] = None + + def add_item(self, item: Item) -> None: + if len(self.items) >= self.capacity: + raise Impossible("Your inventory is full.") + + # Transfer ownership + self.items.append(item) + item.parent = self + item.visible = False # Hide from map +``` + +Features: +- Capacity limits (26 items for letter selection) +- Clean item transfer between world and inventory +- Automatic visual management + +### Item System + +#### Item Entity Class +Items are entities with consumable components: +```python +class Item(Entity): + def __init__(self, consumable: Optional = None): + super().__init__(blocks_movement=False) + self.consumable = consumable + if consumable: + consumable.parent = self +``` + +#### Consumable Components +Modular system for item effects: +```python +class HealingConsumable(Consumable): + def activate(self, action: ItemAction) -> None: + if consumer.hp >= consumer.max_hp: + raise Impossible("You are already at full health.") + + amount_recovered = min(self.amount, consumer.max_hp - consumer.hp) + consumer.hp += amount_recovered + self.consume() # Remove item after use +``` + +### Exception-Driven Feedback + +#### Clean Error Handling +Using exceptions for user feedback: +```python +class Impossible(Exception): + """Action cannot be performed""" + pass + +class PickupAction(Action): + def perform(self, engine: Engine) -> None: + if not items_here: + raise Impossible("There is nothing here to pick up.") + + try: + inventory.add_item(item) + engine.message_log.add_message(f"You picked up the {item.name}!") + except Impossible as e: + engine.message_log.add_message(str(e)) +``` + +Benefits: +- Consistent error messaging +- Clean control flow +- Centralized feedback handling + +### Inventory UI + +#### Modal Inventory Screen +Interactive inventory management: +```python +class InventoryEventHandler(BaseEventHandler): + def create_ui(self) -> None: + # Semi-transparent background + self.background = mcrfpy.Frame(pos=(100, 100), size=(400, 400)) + self.background.fill_color = mcrfpy.Color(0, 0, 0, 200) + + # List items with letter keys + for i, item in enumerate(inventory.items): + item_caption = mcrfpy.Caption( + pos=(20, 80 + i * 20), + text=f"{chr(ord('a') + i)}) {item.name}" + ) +``` + +Features: +- Letter-based selection (a-z) +- Separate handlers for use/drop +- ESC to cancel +- Visual feedback + +### Enhanced Actions + +#### Item Actions +New actions for item management: +```python +class PickupAction(Action): + """Pick up items at current location""" + +class ItemAction(Action): + """Base for item usage actions""" + +class DropAction(ItemAction): + """Drop item from inventory""" +``` + +Each action: +- Self-validates +- Provides feedback +- Triggers enemy turns + +## Architecture Improvements + +### Component Relationships +Parent-based component system: +```python +# Components know their parent +consumable.parent = item +item.parent = inventory +inventory.parent = actor +actor.parent = gamemap +gamemap.engine = engine +``` + +Benefits: +- Access to game context from any component +- Clean ownership transfer +- Simplified entity lifecycle + +### Input Handler States +Modal UI through handler switching: +```python +# Main game +engine.current_handler = MainGameEventHandler(engine) + +# Open inventory +engine.current_handler = InventoryActivateHandler(engine) + +# Back to game +engine.current_handler = MainGameEventHandler(engine) +``` + +### Entity Lifecycle Management +Proper creation and cleanup: +```python +# Item spawning +item = entity_factories.health_potion(x, y, texture) +item.place(x, y, dungeon) + +# Pickup +inventory.add_item(item) # Removes from map + +# Drop +inventory.drop(item) # Returns to map + +# Death +actor.die() # Drops all items +``` + +## Key Implementation Details + +### Visibility Management +Items hide/show based on container: +```python +def add_item(self, item): + item.visible = False # Hide when in inventory + +def drop(self, item): + item.visible = True # Show when on map +``` + +### Inventory Capacity +Limited to alphabet keys: +```python +if len(inventory.items) >= 26: + raise Impossible("Your inventory is full.") +``` + +### Item Generation +Procedural item placement: +```python +def place_entities(room, dungeon, max_monsters, max_items, texture): + # Place 0-2 items per room + number_of_items = random.randint(0, max_items) + + for _ in range(number_of_items): + if space_available: + item = entity_factories.health_potion(x, y, texture) + item.place(x, y, dungeon) +``` + +## Files Modified + +- `game/entity.py`: Added parent system, Item class, inventory to Actor +- `game/inventory.py`: New inventory container system +- `game/consumable.py`: New consumable component system +- `game/exceptions.py`: New Impossible exception +- `game/actions.py`: Added PickupAction, ItemAction, DropAction +- `game/input_handlers.py`: Added InventoryEventHandler classes +- `game/engine.py`: Added current_handler, inventory UI methods +- `game/procgen.py`: Added item generation +- `game/entity_factories.py`: Added health_potion factory +- `game/ui.py`: Updated help text with inventory controls +- `main.py`: Updated to Part 8, handler management + +## What's Next + +Part 9 will add ranged attacks and targeting: +- Targeting UI for selecting enemies +- Ranged damage items (lightning staff) +- Area-of-effect items (fireball staff) +- Confusion effects + +## Learning Points + +1. **Container Architecture**: Entity ownership through parent relationships +2. **Component Systems**: Modular, reusable components with parent references +3. **Exception Handling**: Clean error propagation and user feedback +4. **Modal UI**: State-based input handling for different screens +5. **Item Systems**: Flexible consumable architecture for varied effects +6. **Lifecycle Management**: Proper entity creation, transfer, and cleanup + +## Running Part 8 + +```bash +cd simple_tcod_tutorial/build +./mcrogueface scripts/main.py +``` + +New features to try: +- Press G to pick up healing potions +- Press I to open inventory and use items +- Press O to drop items from inventory +- Heal yourself when injured in combat +- Manage limited inventory space (26 slots) +- Items drop from dead enemies + +## Design Principles + +### Flexibility Through Composition +- Items gain behavior through consumable components +- Easy to add new item types +- Reusable effect system + +### Clean Ownership Transfer +- Entities always have clear parent +- Automatic visibility management +- No orphaned entities + +### User-Friendly Feedback +- Clear error messages +- Consistent UI patterns +- Intuitive controls + +The inventory system provides the foundation for equipment, spells, and complex item interactions in future parts! \ No newline at end of file diff --git a/roguelike_tutorial/part_6a.py b/roguelike_tutorial/part_6a.py new file mode 100644 index 0000000..b385309 --- /dev/null +++ b/roguelike_tutorial/part_6a.py @@ -0,0 +1,582 @@ +""" +McRogueFace Tutorial - Part 6: Turn-based enemy movement + +This tutorial builds on Part 5 by adding: +- Turn cycles where enemies move after the player +- Enemy AI that pursues or wanders +- Shared collision detection for all entities +""" +import mcrfpy +import random + +# Create and activate a new scene +mcrfpy.createScene("tutorial") +mcrfpy.setScene("tutorial") + +# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile) +texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16) + +# Load the hero sprite texture +hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16) + +# Create a grid of tiles +grid_width, grid_height = 40, 30 + +# Calculate the size in pixels +zoom = 2.0 +grid_size = grid_width * zoom * 16, grid_height * zoom * 16 + +# Calculate the position to center the grid on the screen +grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2 + +# Create the grid with a TCODMap for pathfinding/FOV +grid = mcrfpy.Grid( + pos=grid_position, + grid_size=(grid_width, grid_height), + texture=texture, + size=grid_size, +) + +grid.zoom = zoom + +# Define tile types +FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10] +WALL_TILES = [3, 7, 11] + +# Room class for BSP +class Room: + def __init__(self, x, y, w, h): + self.x1 = x + self.y1 = y + self.x2 = x + w + self.y2 = y + h + self.w = w + self.h = h + + def center(self): + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return (center_x, center_y) + + def intersects(self, other): + return (self.x1 <= other.x2 and self.x2 >= other.x1 and + self.y1 <= other.y2 and self.y2 >= other.y1) + +# Dungeon generation functions (from Part 3) +def carve_room(room): + for x in range(room.x1, room.x2): + for y in range(room.y1, room.y2): + if 0 <= x < grid_width and 0 <= y < grid_height: + point = grid.at(x, y) + if point: + point.tilesprite = random.choice(FLOOR_TILES) + point.walkable = True + point.transparent = True + +def carve_hallway(x1, y1, x2, y2): + #points = mcrfpy.libtcod.line(x1, y1, x2, y2) + points = [] + if random.choice([True, False]): + # x1,y1 -> x2,y1 -> x2,y2 + points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1)) + points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2)) + else: + # x1,y1 -> x1,y2 -> x2,y2 + points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2)) + points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2)) + + for x, y in points: + if 0 <= x < grid_width and 0 <= y < grid_height: + point = grid.at(x, y) + if point: + point.tilesprite = random.choice(FLOOR_TILES) + point.walkable = True + point.transparent = True + +def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10): + rooms = [] + + # Fill with walls + for y in range(grid_height): + for x in range(grid_width): + point = grid.at(x, y) + if point: + point.tilesprite = random.choice(WALL_TILES) + point.walkable = False + point.transparent = False + + # Generate rooms + for _ in range(max_rooms): + w = random.randint(room_min_size, room_max_size) + h = random.randint(room_min_size, room_max_size) + x = random.randint(1, grid_width - w - 1) + y = random.randint(1, grid_height - h - 1) + + new_room = Room(x, y, w, h) + + failed = False + for other_room in rooms: + if new_room.intersects(other_room): + failed = True + break + + if not failed: + carve_room(new_room) + + if rooms: + prev_x, prev_y = rooms[-1].center() + new_x, new_y = new_room.center() + carve_hallway(prev_x, prev_y, new_x, new_y) + + rooms.append(new_room) + + return rooms + +# Generate the dungeon +rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8) + +# Add the grid to the scene +mcrfpy.sceneUI("tutorial").append(grid) + +# Spawn player in the first room +if rooms: + spawn_x, spawn_y = rooms[0].center() +else: + spawn_x, spawn_y = 4, 4 + +class GameEntity(mcrfpy.Entity): + """An entity whose default behavior is to prevent others from moving into its tile.""" + + def __init__(self, x, y, walkable=False, **kwargs): + super().__init__(x=x, y=y, **kwargs) + self.walkable = walkable + self.dest_x = x + self.dest_y = y + self.is_moving = False + + def get_position(self): + """Get logical position (destination if moving, otherwise current)""" + if self.is_moving: + return (self.dest_x, self.dest_y) + return (int(self.x), int(self.y)) + + def on_bump(self, other): + return self.walkable # allow other's motion to proceed if entity is walkable + + def __repr__(self): + return f"<{self.__class__.__name__} x={self.x}, y={self.y}, sprite_index={self.sprite_index}>" + +class CombatEntity(GameEntity): + def __init__(self, x, y, hp=10, damage=(1,3), **kwargs): + super().__init__(x=x, y=y, **kwargs) + self.hp = hp + self.damage = damage + + def is_dead(self): + return self.hp <= 0 + + def start_move(self, new_x, new_y, duration=0.2, callback=None): + """Start animating movement to new position""" + self.dest_x = new_x + self.dest_y = new_y + self.is_moving = True + + # Define completion callback that resets is_moving + def movement_done(anim, entity): + self.is_moving = False + if callback: + callback(anim, entity) + + # Create animations for smooth movement + anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad", callback=movement_done) + anim_y = mcrfpy.Animation("y", float(new_y), duration, "easeInOutQuad") + + anim_x.start(self) + anim_y.start(self) + + def can_see(self, target_x, target_y): + """Check if this entity can see the target position""" + mx, my = self.get_position() + + # Simple distance check first + dist = abs(target_x - mx) + abs(target_y - my) + if dist > 6: + return False + + # Line of sight check + line = list(mcrfpy.libtcod.line(mx, my, target_x, target_y)) + for x, y in line[1:-1]: # Skip start and end + cell = grid.at(x, y) + if cell and not cell.transparent: + return False + return True + + def ai_turn(self, player_pos): + """Decide next move""" + mx, my = self.get_position() + px, py = player_pos + + # Simple AI: move toward player if visible + if self.can_see(px, py): + # Calculate direction toward player + dx = 0 + dy = 0 + if px > mx: + dx = 1 + elif px < mx: + dx = -1 + if py > my: + dy = 1 + elif py < my: + dy = -1 + + # Prefer cardinal movement + if dx != 0 and dy != 0: + # Pick horizontal or vertical based on greater distance + if abs(px - mx) > abs(py - my): + dy = 0 + else: + dx = 0 + + return (mx + dx, my + dy) + else: + # Random wander + dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)]) + return (mx + dx, my + dy) + +# Create a player entity +player = CombatEntity( + spawn_x, spawn_y, + texture=hero_texture, + sprite_index=0 +) + +# Add the player entity to the grid +grid.entities.append(player) + +# Track all enemies +enemies = [] + +# Spawn enemies in other rooms +for i, room in enumerate(rooms[1:], 1): # Skip first room (player spawn) + if i <= 3: # Limit to 3 enemies for now + enemy_x, enemy_y = room.center() + enemy = CombatEntity( + enemy_x, enemy_y, + texture=hero_texture, + sprite_index=0 # Enemy sprite (borrow player's) + ) + grid.entities.append(enemy) + enemies.append(enemy) + +# Set the grid perspective to the player by default +# Note: The new perspective system uses entity references directly +grid.perspective = player + +# Initial FOV computation +def update_fov(): + """Update field of view from current perspective""" + if grid.perspective == player: + grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0) + player.update_visibility() + +# Perform initial FOV calculation +update_fov() + +# Center grid on current perspective +def center_on_perspective(): + if grid.perspective == player: + grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 + +center_on_perspective() + +# Movement state tracking (from Part 3) +#is_moving = False # make it an entity property +move_queue = [] +current_destination = None +current_move = None + +# Store animation references +player_anim_x = None +player_anim_y = None +grid_anim_x = None +grid_anim_y = None + +def movement_complete(anim, target): + """Called when movement animation completes""" + global move_queue, current_destination, current_move + global player_anim_x, player_anim_y, is_player_turn + + player.is_moving = False + current_move = None + current_destination = None + player_anim_x = None + player_anim_y = None + + # Update FOV after movement + update_fov() + center_on_perspective() + + # Player turn complete, start enemy turns and queued player move simultaneously + is_player_turn = False + process_enemy_turns_and_player_queue() + +motion_speed = 0.20 +is_player_turn = True # Track whose turn it is + +def get_blocking_entity_at(x, y): + """Get blocking entity at position""" + for e in grid.entities: + if not e.walkable and (x, y) == e.get_position(): + return e + return None + +def can_move_to(x, y, mover=None): + """Check if a position is valid for movement""" + if x < 0 or x >= grid_width or y < 0 or y >= grid_height: + return False + + point = grid.at(x, y) + if not point or not point.walkable: + return False + + # Check for blocking entities + blocker = get_blocking_entity_at(x, y) + if blocker and blocker != mover: + return False + + return True + +def process_enemy_turns_and_player_queue(): + """Process all enemy AI decisions and player's queued move simultaneously""" + global is_player_turn, move_queue + + enemies_to_move = [] + + # Collect all enemy moves + for i, enemy in enumerate(enemies): + if enemy.is_dead(): + continue + + # AI decides next move based on player's position + target_x, target_y = enemy.ai_turn(player.get_position()) + + # Check if move is valid + if can_move_to(target_x, target_y, enemy): + enemies_to_move.append((enemy, target_x, target_y)) + + # Start all enemy animations simultaneously + any_enemy_moved = False + if enemies_to_move: + for enemy, tx, ty in enemies_to_move: + enemy.start_move(tx, ty, duration=motion_speed) + any_enemy_moved = True + + # Process player's queued move at the same time + if move_queue: + next_move = move_queue.pop(0) + process_player_queued_move(next_move) + else: + # No queued move, set up callback to return control when animations finish + if any_enemy_moved: + # Wait for animations to complete + mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50) + else: + # No animations, return control immediately + is_player_turn = True + +def process_player_queued_move(key): + """Process player's queued move during enemy turn""" + global current_move, current_destination + global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y + + px, py = int(player.x), int(player.y) + new_x, new_y = px, py + + if key == "W" or key == "Up": + new_y -= 1 + elif key == "S" or key == "Down": + new_y += 1 + elif key == "A" or key == "Left": + new_x -= 1 + elif key == "D" or key == "Right": + new_x += 1 + + if new_x != px or new_y != py: + # Check destination at animation end time (considering enemy destinations) + future_blocker = get_future_blocking_entity_at(new_x, new_y) + + if future_blocker: + # Will bump at destination + # Schedule bump for when animations complete + mcrfpy.setTimer("delayed_bump", lambda t: handle_delayed_bump(future_blocker), int(motion_speed * 1000)) + elif can_move_to(new_x, new_y, player): + # Valid move, start animation + player.is_moving = True + current_move = key + current_destination = (new_x, new_y) + player.dest_x = new_x + player.dest_y = new_y + + # Player animation with callback + player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=player_queued_move_complete) + player_anim_x.start(player) + player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad") + player_anim_y.start(player) + + # Move camera with player + grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear") + grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear") + grid_anim_x.start(grid) + grid_anim_y.start(grid) + else: + # Blocked by wall, wait for turn to complete + mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50) + +def get_future_blocking_entity_at(x, y): + """Get entity that will be blocking at position after current animations""" + for e in grid.entities: + if not e.walkable and (x, y) == (e.dest_x, e.dest_y): + return e + return None + +def handle_delayed_bump(entity): + """Handle bump after animations complete""" + global is_player_turn + entity.on_bump(player) + is_player_turn = True + +def player_queued_move_complete(anim, target): + """Called when player's queued movement completes""" + global is_player_turn + player.is_moving = False + update_fov() + center_on_perspective() + is_player_turn = True + +def check_turn_complete(timer_name): + """Check if all animations are complete""" + global is_player_turn + + # Check if any entity is still moving + if player.is_moving: + mcrfpy.setTimer("turn_complete", check_turn_complete, 50) + return + + for enemy in enemies: + if enemy.is_moving: + mcrfpy.setTimer("turn_complete", check_turn_complete, 50) + return + + # All done + is_player_turn = True + +def process_move(key): + """Process a move based on the key""" + global current_move, current_destination, move_queue + global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y, is_player_turn + + # Only allow player movement on player's turn + if not is_player_turn: + return + + # Only allow player movement when in player perspective + if grid.perspective != player: + return + + if player.is_moving: + move_queue.clear() + move_queue.append(key) + return + + px, py = int(player.x), int(player.y) + new_x, new_y = px, py + + if key == "W" or key == "Up": + new_y -= 1 + elif key == "S" or key == "Down": + new_y += 1 + elif key == "A" or key == "Left": + new_x -= 1 + elif key == "D" or key == "Right": + new_x += 1 + + if new_x != px or new_y != py: + # Check what's at destination + blocker = get_blocking_entity_at(new_x, new_y) + + if blocker: + # Bump interaction (combat will go here later) + blocker.on_bump(player) + # Still counts as a turn + is_player_turn = False + process_enemy_turns_and_player_queue() + elif can_move_to(new_x, new_y, player): + player.is_moving = True + current_move = key + current_destination = (new_x, new_y) + player.dest_x = new_x + player.dest_y = new_y + + # Start player move animation + player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete) + player_anim_x.start(player) + player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad") + player_anim_y.start(player) + + # Move camera with player + grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear") + grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear") + grid_anim_x.start(grid) + grid_anim_y.start(grid) + +def handle_keys(key, state): + """Handle keyboard input""" + if state == "start": + # Movement keys + if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]: + process_move(key) + +# Register the keyboard handler +mcrfpy.keypressScene(handle_keys) + +# Add UI elements +title = mcrfpy.Caption((320, 10), + text="McRogueFace Tutorial - Part 6: Turn-based Movement", +) +title.fill_color = mcrfpy.Color(255, 255, 255, 255) +mcrfpy.sceneUI("tutorial").append(title) + +instructions = mcrfpy.Caption((150, 720), + text="Use WASD/Arrows to move. Enemies move after you!", +) +instructions.font_size = 18 +instructions.fill_color = mcrfpy.Color(200, 200, 200, 255) +mcrfpy.sceneUI("tutorial").append(instructions) + +# Debug info +debug_caption = mcrfpy.Caption((10, 40), + text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Enemies: {len(enemies)}", +) +debug_caption.font_size = 16 +debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255) +mcrfpy.sceneUI("tutorial").append(debug_caption) + +# Update function for turn display +def update_turn_display(): + turn_text = "Player" if is_player_turn else "Enemy" + alive_enemies = sum(1 for e in enemies if not e.is_dead()) + debug_caption.text = f"Grid: {grid_width}x{grid_height} | Turn: {turn_text} | Enemies: {alive_enemies}/{len(enemies)}" + +# Timer to update display +def update_display(runtime): + update_turn_display() + +mcrfpy.setTimer("display_update", update_display, 100) + +print("Tutorial Part 6 loaded!") +print("Turn-based movement system active!") +print("- Enemies move after the player") +print("- Enemies pursue when they can see you") +print("- Enemies wander when they can't") +print("Use WASD or Arrow keys to move!")