From 1805b985bd639e8d2d1f38230973947fb64f85d9 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Thu, 9 Apr 2026 23:23:35 -0400 Subject: [PATCH] Update all 13 tutorial scripts to current enum-based API, refs #167 All tutorial parts (1-13) used the old string-based key/action comparison API removed in 6d5e99a. Every handle_keys function now uses mcrfpy.Key.* and mcrfpy.InputState.PRESSED enums. Additional fixes across all parts: - Replace manual FOV computation with ColorLayer.draw_fov() which handles FOV calculation and explored-state tracking in one call - Replace old grid.add_layer("color") with ColorLayer() constructor - Fix entity removal bug: entities.remove(index) -> remove(entity_ref) - Remove manual exploration tracking (draw_fov handles it internally) - Use tuple positions for compute_fov/is_in_fov: (x, y) not x, y All 14 parts (0-13) tested and passing in headless mode. Co-Authored-By: Claude Opus 4.6 --- .../part_01_grid_movement.py | 18 +-- .../part_02_tiles_collision.py | 14 +- .../part_03_dungeon_generation.py | 16 +- docs/tutorials/part_04_fov/part_04_fov.py | 79 ++++------ .../part_05_enemies/part_05_enemies.py | 87 ++++------- .../part_06_combat/part_06_combat.py | 81 ++++------ docs/tutorials/part_07_ui/part_07_ui.py | 80 ++++------ docs/tutorials/part_08_items/part_08_items.py | 95 ++++-------- .../part_09_ranged/part_09_ranged.py | 119 +++++---------- .../part_10_save_load/part_10_save_load.py | 141 ++++++----------- .../part_11_levels/part_11_levels.py | 143 ++++++------------ .../part_12_experience/part_12_experience.py | 141 ++++++----------- .../part_13_equipment/part_13_equipment.py | 139 ++++++----------- 13 files changed, 388 insertions(+), 765 deletions(-) diff --git a/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py b/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py index ef5ee40..8685668 100644 --- a/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py +++ b/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py @@ -75,30 +75,30 @@ pos_display.fill_color = mcrfpy.Color(200, 200, 100) pos_display.font_size = 16 scene.children.append(pos_display) -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: """Handle keyboard input to move the player. Args: - key: The key that was pressed (e.g., "W", "Up", "Space") - action: Either "start" (key pressed) or "end" (key released) + key: A mcrfpy.Key enum value (e.g., Key.W, Key.UP) + action: A mcrfpy.InputState enum value (PRESSED or RELEASED) """ # Only respond to key press, not release - if action != "start": + if action != mcrfpy.InputState.PRESSED: return # Get current player position px, py = int(player.x), int(player.y) # Calculate new position based on key - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: py -= 1 # Up decreases Y - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: py += 1 # Down increases Y - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: px -= 1 # Left decreases X - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: px += 1 # Right increases X - elif key == "Escape": + elif key == mcrfpy.Key.ESCAPE: mcrfpy.exit() return diff --git a/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py b/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py index e8a09d3..b19793a 100644 --- a/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py +++ b/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py @@ -160,9 +160,9 @@ scene.children.append(status_display) # Input Handling # ============================================================================= -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: """Handle keyboard input with collision detection.""" - if action != "start": + if action != mcrfpy.InputState.PRESSED: return # Get current position @@ -171,15 +171,15 @@ def handle_keys(key: str, action: str) -> None: # Calculate intended new position new_x, new_y = px, py - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: new_y -= 1 - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: new_y += 1 - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: new_x -= 1 - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: new_x += 1 - elif key == "Escape": + elif key == mcrfpy.Key.ESCAPE: mcrfpy.exit() return else: diff --git a/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py b/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py index d3e3874..2e3af3f 100644 --- a/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py +++ b/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py @@ -316,26 +316,26 @@ def regenerate_dungeon() -> None: pos_display.text = f"Position: ({new_x}, {new_y})" room_display.text = "New dungeon generated!" -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: """Handle keyboard input.""" - if action != "start": + if action != mcrfpy.InputState.PRESSED: return px, py = int(player.x), int(player.y) new_x, new_y = px, py - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: new_y -= 1 - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: new_y += 1 - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: new_x -= 1 - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: new_x += 1 - elif key == "R": + elif key == mcrfpy.Key.R: regenerate_dungeon() return - elif key == "Escape": + elif key == mcrfpy.Key.ESCAPE: mcrfpy.exit() return else: diff --git a/docs/tutorials/part_04_fov/part_04_fov.py b/docs/tutorials/part_04_fov/part_04_fov.py index c7c89d7..7c8c589 100644 --- a/docs/tutorials/part_04_fov/part_04_fov.py +++ b/docs/tutorials/part_04_fov/part_04_fov.py @@ -71,24 +71,9 @@ class RectangularRoom: # Exploration Tracking # ============================================================================= -# Track which tiles have been discovered (seen at least once) -explored: list[list[bool]] = [] - -def init_explored() -> None: - """Initialize the explored array to all False.""" - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - """Mark a tile as explored.""" - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - """Check if a tile has been explored.""" - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Note: The ColorLayer's draw_fov() method tracks exploration state +# internally - tiles that have been visible at least once are rendered +# with the 'discovered' color. No manual tracking needed! # ============================================================================= # Dungeon Generation (from Part 3, with transparent property) @@ -151,7 +136,6 @@ def carve_l_tunnel( def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]: """Generate a dungeon with rooms and tunnels.""" fill_with_walls(grid) - init_explored() # Reset exploration when generating new dungeon rooms: list[RectangularRoom] = [] @@ -188,28 +172,25 @@ def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]: def update_fov(grid: mcrfpy.Grid, fov_layer, player_x: int, player_y: int) -> None: """Update the field of view visualization. + Uses the ColorLayer's built-in draw_fov() method, which computes FOV + via libtcod and paints visibility colors in a single call. The layer + tracks explored state automatically. + Args: grid: The game grid fov_layer: The ColorLayer for FOV visualization player_x: Player's X position player_y: Player's Y position """ - # Compute FOV from player position - grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) - - # Update each tile's visibility - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if grid.is_in_fov(x, y): - # Currently visible - mark as explored and show clearly - mark_explored(x, y) - fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - # Previously seen but not currently visible - show dimmed - fov_layer.set(x, y, COLOR_DISCOVERED) - else: - # Never seen - hide completely - fov_layer.set(x, y, COLOR_UNKNOWN) + # draw_fov computes FOV and paints the color layer in one step. + # It tracks explored state internally so previously-seen tiles stay dimmed. + fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) # ============================================================================= # Collision Detection @@ -244,12 +225,12 @@ grid = mcrfpy.Grid( player_start_x, player_start_y = generate_dungeon(grid) # Add a color layer for FOV visualization (below entities) -fov_layer = grid.add_layer("color", z_index=-1) +# Create the layer object, then attach it to the grid +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) # Initialize the FOV layer to all black (unknown) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer.fill(COLOR_UNKNOWN) # Create the player player = mcrfpy.Entity( @@ -312,34 +293,32 @@ def regenerate_dungeon() -> None: player.y = new_y # Reset FOV layer to unknown - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) # Calculate new FOV update_fov(grid, fov_layer, new_x, new_y) pos_display.text = f"Position: ({new_x}, {new_y})" -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: """Handle keyboard input.""" - if action != "start": + if action != mcrfpy.InputState.PRESSED: return px, py = int(player.x), int(player.y) new_x, new_y = px, py - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: new_y -= 1 - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: new_y += 1 - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: new_x -= 1 - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: new_x += 1 - elif key == "R": + elif key == mcrfpy.Key.R: regenerate_dungeon() return - elif key == "Escape": + elif key == mcrfpy.Key.ESCAPE: mcrfpy.exit() return else: diff --git a/docs/tutorials/part_05_enemies/part_05_enemies.py b/docs/tutorials/part_05_enemies/part_05_enemies.py index 51d0299..ea0e379 100644 --- a/docs/tutorials/part_05_enemies/part_05_enemies.py +++ b/docs/tutorials/part_05_enemies/part_05_enemies.py @@ -120,23 +120,8 @@ class RectangularRoom: # Exploration Tracking (from Part 4) # ============================================================================= -explored: list[list[bool]] = [] - -def init_explored() -> None: - """Initialize the explored array to all False.""" - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - """Mark a tile as explored.""" - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - """Check if a tile has been explored.""" - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Note: The ColorLayer's draw_fov() method tracks exploration state +# internally - no manual tracking needed! # ============================================================================= # Dungeon Generation (from Part 4) @@ -293,20 +278,15 @@ def clear_enemies(target_grid: mcrfpy.Grid) -> None: """Remove all enemies from the grid.""" global entity_data - # Get list of enemies to remove (not the player) + # Collect enemies to remove (not the player) enemies_to_remove = [] for entity in target_grid.entities: if entity in entity_data and not entity_data[entity].get("is_player", False): enemies_to_remove.append(entity) - # Remove from grid and entity_data + # Remove from grid (by entity reference) and from entity_data for enemy in enemies_to_remove: - # Find and remove from grid.entities - for i, e in enumerate(target_grid.entities): - if e == enemy: - target_grid.entities.remove(i) - break - # Remove from entity_data + target_grid.entities.remove(enemy) if enemy in entity_data: del entity_data[enemy] @@ -329,7 +309,7 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: # Other entities are only visible if in FOV ex, ey = int(entity.x), int(entity.y) - entity.visible = target_grid.is_in_fov(ex, ey) + entity.visible = target_grid.is_in_fov((ex, ey)) # ============================================================================= # Field of View (from Part 4) @@ -337,21 +317,18 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: """Update the field of view visualization.""" - # Compute FOV from player position - target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) + # draw_fov computes FOV and paints the color layer in one step. + # It also tracks explored state internally. + target_fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) - # Update each tile's visibility - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if target_grid.is_in_fov(x, y): - mark_explored(x, y) - target_fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - target_fov_layer.set(x, y, COLOR_DISCOVERED) - else: - target_fov_layer.set(x, y, COLOR_UNKNOWN) - - # Update entity visibility + # Update entity visibility (draw_fov calls compute_fov internally, + # so is_in_fov() reflects the current FOV state) update_entity_visibility(target_grid) # ============================================================================= @@ -399,7 +376,6 @@ def generate_dungeon(target_grid: mcrfpy.Grid, texture: mcrfpy.Texture) -> tuple # Fill with walls fill_with_walls(target_grid) - init_explored() rooms: list[RectangularRoom] = [] @@ -454,7 +430,6 @@ grid = mcrfpy.Grid( # Generate the dungeon (without player first to get starting position) fill_with_walls(grid) -init_explored() rooms: list[RectangularRoom] = [] @@ -489,10 +464,9 @@ else: player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 # Add FOV layer -fov_layer = grid.add_layer("color", z_index=-1) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +fov_layer.fill(COLOR_UNKNOWN) # Create the player player = mcrfpy.Entity( @@ -574,7 +548,6 @@ def regenerate_dungeon() -> None: # Regenerate dungeon structure fill_with_walls(grid) - init_explored() rooms = [] @@ -618,37 +591,35 @@ def regenerate_dungeon() -> None: spawn_enemies_in_room(grid, room, texture) # Reset FOV layer - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) # Update FOV update_fov(grid, fov_layer, new_x, new_y) pos_display.text = f"Position: ({new_x}, {new_y})" status_display.text = "New dungeon generated!" -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: """Handle keyboard input.""" global player, grid, fov_layer - if action != "start": + if action != mcrfpy.InputState.PRESSED: return px, py = int(player.x), int(player.y) new_x, new_y = px, py - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: new_y -= 1 - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: new_y += 1 - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: new_x -= 1 - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: new_x += 1 - elif key == "R": + elif key == mcrfpy.Key.R: regenerate_dungeon() return - elif key == "Escape": + elif key == mcrfpy.Key.ESCAPE: mcrfpy.exit() return else: diff --git a/docs/tutorials/part_06_combat/part_06_combat.py b/docs/tutorials/part_06_combat/part_06_combat.py index fd6a62f..2239b15 100644 --- a/docs/tutorials/part_06_combat/part_06_combat.py +++ b/docs/tutorials/part_06_combat/part_06_combat.py @@ -159,27 +159,7 @@ class RectangularRoom: self.y2 >= other.y1 ) -# ============================================================================= -# Exploration Tracking -# ============================================================================= - -explored: list[list[bool]] = [] - -def init_explored() -> None: - """Initialize the explored array to all False.""" - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - """Mark a tile as explored.""" - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - """Check if a tile has been explored.""" - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Exploration tracking is handled internally by draw_fov() # ============================================================================= # Message Log @@ -357,9 +337,9 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: """Remove an entity from the grid and data storage.""" # Find and remove from grid - for i, e in enumerate(target_grid.entities): + for e in target_grid.entities: if e == entity: - target_grid.entities.remove(i) + target_grid.entities.remove(e) break # Remove from entity data @@ -487,21 +467,19 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: continue ex, ey = int(entity.x), int(entity.y) - entity.visible = target_grid.is_in_fov(ex, ey) + entity.visible = target_grid.is_in_fov((ex, ey)) def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: """Update the field of view visualization.""" - target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) - - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if target_grid.is_in_fov(x, y): - mark_explored(x, y) - target_fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - target_fov_layer.set(x, y, COLOR_DISCOVERED) - else: - target_fov_layer.set(x, y, COLOR_UNKNOWN) + # draw_fov computes FOV and paints the color layer in one step, + # and tracks exploration internally. + target_fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) update_entity_visibility(target_grid) @@ -595,7 +573,7 @@ def enemy_turn() -> None: ex, ey = int(enemy.x), int(enemy.y) # Only act if in player's FOV (aware of player) - if not grid.is_in_fov(ex, ey): + if not grid.is_in_fov((ex, ey)): continue # Check if adjacent to player @@ -691,7 +669,6 @@ grid = mcrfpy.Grid( # Generate initial dungeon structure fill_with_walls(grid) -init_explored() rooms: list[RectangularRoom] = [] @@ -726,10 +703,9 @@ else: player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 # Add FOV layer -fov_layer = grid.add_layer("color", z_index=-1) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +fov_layer.fill(COLOR_UNKNOWN) # Create the player player = mcrfpy.Entity( @@ -826,11 +802,10 @@ def restart_game() -> None: # Remove all entities from grid while len(grid.entities) > 0: - grid.entities.remove(0) + grid.entities.remove(grid.entities[0]) # Regenerate dungeon fill_with_walls(grid) - init_explored() clear_messages() rooms = [] @@ -889,9 +864,7 @@ def restart_game() -> None: spawn_enemies_in_room(grid, room, texture) # Reset FOV layer - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) # Update displays update_fov(grid, fov_layer, new_x, new_y) @@ -900,19 +873,19 @@ def restart_game() -> None: add_message("A new adventure begins!", mcrfpy.Color(100, 100, 255)) -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: """Handle keyboard input.""" global game_over - if action != "start": + if action != mcrfpy.InputState.PRESSED: return # Handle restart - if key == "R": + if key == mcrfpy.Key.R: restart_game() return - if key == "Escape": + if key == mcrfpy.Key.ESCAPE: mcrfpy.exit() return @@ -921,13 +894,13 @@ def handle_keys(key: str, action: str) -> None: return # Movement and attack - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: try_move_or_attack(0, -1) - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: try_move_or_attack(0, 1) - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: try_move_or_attack(-1, 0) - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: try_move_or_attack(1, 0) scene.on_key = handle_keys diff --git a/docs/tutorials/part_07_ui/part_07_ui.py b/docs/tutorials/part_07_ui/part_07_ui.py index 0ec3da3..1af5992 100644 --- a/docs/tutorials/part_07_ui/part_07_ui.py +++ b/docs/tutorials/part_07_ui/part_07_ui.py @@ -405,24 +405,7 @@ class RectangularRoom: self.y2 >= other.y1 ) -# ============================================================================= -# Exploration Tracking -# ============================================================================= - -explored: list[list[bool]] = [] - -def init_explored() -> None: - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Exploration tracking is handled internally by draw_fov() # ============================================================================= # Dungeon Generation @@ -546,10 +529,7 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc return None def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in entity_data: del entity_data[entity] @@ -633,20 +613,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: continue ex, ey = int(entity.x), int(entity.y) - entity.visible = target_grid.is_in_fov(ex, ey) + entity.visible = target_grid.is_in_fov((ex, ey)) def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: - target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) - - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if target_grid.is_in_fov(x, y): - mark_explored(x, y) - target_fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - target_fov_layer.set(x, y, COLOR_DISCOVERED) - else: - target_fov_layer.set(x, y, COLOR_UNKNOWN) + target_fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) update_entity_visibility(target_grid) @@ -719,7 +695,7 @@ def enemy_turn() -> None: ex, ey = int(enemy.x), int(enemy.y) - if not grid.is_in_fov(ex, ey): + if not grid.is_in_fov((ex, ey)): continue dx = player_x - ex @@ -791,7 +767,6 @@ grid = mcrfpy.Grid( # Generate initial dungeon structure fill_with_walls(grid) -init_explored() rooms: list[RectangularRoom] = [] @@ -826,10 +801,9 @@ else: player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 # Add FOV layer -fov_layer = grid.add_layer("color", z_index=-1) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +fov_layer.fill(COLOR_UNKNOWN) # Create the player player = mcrfpy.Entity( @@ -930,11 +904,11 @@ def restart_game() -> None: entity_data.clear() - while len(grid.entities) > 0: - grid.entities.remove(0) + entities_to_clear = list(grid.entities) + for e in entities_to_clear: + grid.entities.remove(e) fill_with_walls(grid) - init_explored() message_log.clear() rooms = [] @@ -989,9 +963,7 @@ def restart_game() -> None: continue spawn_enemies_in_room(grid, room, texture) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, new_x, new_y) @@ -999,30 +971,30 @@ def restart_game() -> None: update_ui() -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: global game_over - if action != "start": + if action != mcrfpy.InputState.PRESSED: return - if key == "R": + if key == mcrfpy.Key.R: restart_game() return - if key == "Escape": + if key == mcrfpy.Key.ESCAPE: mcrfpy.exit() return if game_over: return - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: try_move_or_attack(0, -1) - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: try_move_or_attack(0, 1) - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: try_move_or_attack(-1, 0) - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: try_move_or_attack(1, 0) scene.on_key = handle_keys diff --git a/docs/tutorials/part_08_items/part_08_items.py b/docs/tutorials/part_08_items/part_08_items.py index d3829d9..dcf3099 100644 --- a/docs/tutorials/part_08_items/part_08_items.py +++ b/docs/tutorials/part_08_items/part_08_items.py @@ -436,24 +436,7 @@ class RectangularRoom: self.y2 >= other.y1 ) -# ============================================================================= -# Exploration Tracking -# ============================================================================= - -explored: list[list[bool]] = [] - -def init_explored() -> None: - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Exploration tracking is handled internally by draw_fov() # ============================================================================= # Dungeon Generation @@ -711,10 +694,7 @@ def use_item(index: int) -> bool: def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: """Remove an item entity from the grid and item_data.""" - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in item_data: del item_data[entity] @@ -751,10 +731,7 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc return None def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in entity_data: del entity_data[entity] @@ -775,10 +752,7 @@ def clear_all_entities(target_grid: mcrfpy.Grid) -> None: if entity in item_data: del item_data[entity] - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) # ============================================================================= # Combat System @@ -852,20 +826,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: continue ex, ey = int(entity.x), int(entity.y) - entity.visible = target_grid.is_in_fov(ex, ey) + entity.visible = target_grid.is_in_fov((ex, ey)) def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: - target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) - - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if target_grid.is_in_fov(x, y): - mark_explored(x, y) - target_fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - target_fov_layer.set(x, y, COLOR_DISCOVERED) - else: - target_fov_layer.set(x, y, COLOR_UNKNOWN) + target_fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) update_entity_visibility(target_grid) @@ -938,7 +908,7 @@ def enemy_turn() -> None: ex, ey = int(enemy.x), int(enemy.y) - if not grid.is_in_fov(ex, ey): + if not grid.is_in_fov((ex, ey)): continue dx = player_x - ex @@ -1012,7 +982,6 @@ grid = mcrfpy.Grid( # Generate initial dungeon fill_with_walls(grid) -init_explored() rooms: list[RectangularRoom] = [] @@ -1047,10 +1016,9 @@ else: player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 # Add FOV layer -fov_layer = grid.add_layer("color", z_index=-1) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +fov_layer.fill(COLOR_UNKNOWN) # Create the player player = mcrfpy.Entity( @@ -1156,11 +1124,10 @@ def restart_game() -> None: entity_data.clear() item_data.clear() - while len(grid.entities) > 0: - grid.entities.remove(0) + for e in list(grid.entities): + grid.entities.remove(e) fill_with_walls(grid) - init_explored() message_log.clear() rooms = [] @@ -1219,9 +1186,7 @@ def restart_game() -> None: spawn_enemies_in_room(grid, room, texture) spawn_items_in_room(grid, room, texture) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, new_x, new_y) @@ -1229,17 +1194,17 @@ def restart_game() -> None: update_ui() -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: global game_over - if action != "start": + if action != mcrfpy.InputState.PRESSED: return - if key == "R": + if key == mcrfpy.Key.R: restart_game() return - if key == "Escape": + if key == mcrfpy.Key.ESCAPE: mcrfpy.exit() return @@ -1247,20 +1212,20 @@ def handle_keys(key: str, action: str) -> None: return # Movement - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: try_move_or_attack(0, -1) - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: try_move_or_attack(0, 1) - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: try_move_or_attack(-1, 0) - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: try_move_or_attack(1, 0) # Pickup - elif key == "G" or key == ",": + elif key == mcrfpy.Key.G: pickup_item() # Use items by number key - elif key in ["1", "2", "3", "4", "5"]: - index = int(key) - 1 + elif key in [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5]: + index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key) if use_item(index): enemy_turn() # Using an item takes a turn update_ui() diff --git a/docs/tutorials/part_09_ranged/part_09_ranged.py b/docs/tutorials/part_09_ranged/part_09_ranged.py index 5605629..827414c 100644 --- a/docs/tutorials/part_09_ranged/part_09_ranged.py +++ b/docs/tutorials/part_09_ranged/part_09_ranged.py @@ -457,24 +457,7 @@ class RectangularRoom: self.y2 >= other.y1 ) -# ============================================================================= -# Exploration Tracking -# ============================================================================= - -explored: list[list[bool]] = [] - -def init_explored() -> None: - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Exploration tracking is handled internally by draw_fov() # ============================================================================= # Dungeon Generation @@ -635,18 +618,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc return None def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in entity_data: del entity_data[entity] def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in item_data: del item_data[entity] @@ -666,10 +643,7 @@ def clear_all_entities(target_grid: mcrfpy.Grid) -> None: if entity in item_data: del item_data[entity] - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) # ============================================================================= # Targeting System @@ -701,11 +675,7 @@ def exit_targeting_mode() -> None: global game_mode, target_cursor, grid if target_cursor is not None: - # Remove cursor from grid - for i, e in enumerate(grid.entities): - if e == target_cursor: - grid.entities.remove(i) - break + grid.entities.remove(target_cursor) target_cursor = None game_mode = GameMode.NORMAL @@ -723,7 +693,7 @@ def move_cursor(dx: int, dy: int) -> None: return # Check if position is in FOV (can only target visible tiles) - if not grid.is_in_fov(new_x, new_y): + if not grid.is_in_fov((new_x, new_y)): message_log.add("You cannot see that location.", COLOR_INVALID) return @@ -936,20 +906,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: continue ex, ey = int(entity.x), int(entity.y) - entity.visible = target_grid.is_in_fov(ex, ey) + entity.visible = target_grid.is_in_fov((ex, ey)) def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: - target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) - - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if target_grid.is_in_fov(x, y): - mark_explored(x, y) - target_fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - target_fov_layer.set(x, y, COLOR_DISCOVERED) - else: - target_fov_layer.set(x, y, COLOR_UNKNOWN) + target_fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) update_entity_visibility(target_grid) @@ -1022,7 +988,7 @@ def enemy_turn() -> None: ex, ey = int(enemy.x), int(enemy.y) - if not grid.is_in_fov(ex, ey): + if not grid.is_in_fov((ex, ey)): continue dx = player_x - ex @@ -1095,7 +1061,6 @@ grid = mcrfpy.Grid( # Generate initial dungeon fill_with_walls(grid) -init_explored() rooms: list[RectangularRoom] = [] @@ -1130,10 +1095,9 @@ else: player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2 # Add FOV layer -fov_layer = grid.add_layer("color", z_index=-1) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +fov_layer.fill(COLOR_UNKNOWN) # Create the player player = mcrfpy.Entity( @@ -1247,11 +1211,10 @@ def restart_game() -> None: entity_data.clear() item_data.clear() - while len(grid.entities) > 0: - grid.entities.remove(0) + for e in list(grid.entities): + grid.entities.remove(e) fill_with_walls(grid) - init_explored() message_log.clear() rooms = [] @@ -1309,9 +1272,7 @@ def restart_game() -> None: spawn_enemies_in_room(grid, room, texture) spawn_items_in_room(grid, room, texture) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, new_x, new_y) @@ -1320,18 +1281,18 @@ def restart_game() -> None: mode_display.update(game_mode) update_ui() -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: global game_over, game_mode - if action != "start": + if action != mcrfpy.InputState.PRESSED: return # Always allow restart and quit - if key == "R": + if key == mcrfpy.Key.R: restart_game() return - if key == "Escape": + if key == mcrfpy.Key.ESCAPE: if game_mode == GameMode.TARGETING: exit_targeting_mode() message_log.add("Targeting cancelled.", COLOR_INFO) @@ -1349,41 +1310,41 @@ def handle_keys(key: str, action: str) -> None: else: handle_normal_input(key) -def handle_normal_input(key: str) -> None: +def handle_normal_input(key) -> None: """Handle input in normal game mode.""" # Movement - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: try_move_or_attack(0, -1) - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: try_move_or_attack(0, 1) - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: try_move_or_attack(-1, 0) - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: try_move_or_attack(1, 0) # Ranged attack (enter targeting mode) - elif key == "F": + elif key == mcrfpy.Key.F: enter_targeting_mode() # Pickup - elif key == "G" or key == ",": + elif key == mcrfpy.Key.G: pickup_item() # Use items - elif key in ["1", "2", "3", "4", "5"]: - index = int(key) - 1 + elif key in [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5]: + index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key) if use_item(index): enemy_turn() update_ui() -def handle_targeting_input(key: str) -> None: +def handle_targeting_input(key) -> None: """Handle input in targeting mode.""" - if key == "Up" or key == "W": + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: move_cursor(0, -1) - elif key == "Down" or key == "S": + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: move_cursor(0, 1) - elif key == "Left" or key == "A": + elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: move_cursor(-1, 0) - elif key == "Right" or key == "D": + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: move_cursor(1, 0) - elif key == "Return" or key == "Space": + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: confirm_target() scene.on_key = handle_keys diff --git a/docs/tutorials/part_10_save_load/part_10_save_load.py b/docs/tutorials/part_10_save_load/part_10_save_load.py index b0a911d..b5a606a 100644 --- a/docs/tutorials/part_10_save_load/part_10_save_load.py +++ b/docs/tutorials/part_10_save_load/part_10_save_load.py @@ -511,24 +511,7 @@ class RectangularRoom: self.y2 >= other.y1 ) -# ============================================================================= -# Exploration Tracking -# ============================================================================= - -explored: list[list[bool]] = [] - -def init_explored() -> None: - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Exploration tracking is handled internally by draw_fov() # ============================================================================= # Save/Load System @@ -540,7 +523,7 @@ def save_game() -> bool: Returns: True if save succeeded, False otherwise """ - global player, player_inventory, grid, explored, dungeon_level + global player, player_inventory, grid, dungeon_level try: # Collect tile data @@ -592,7 +575,6 @@ def save_game() -> bool: "inventory": player_inventory.to_dict() }, "tiles": tiles, - "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)], "enemies": enemies, "items": items_on_ground } @@ -615,7 +597,7 @@ def load_game() -> bool: Returns: True if load succeeded, False otherwise """ - global player, player_inventory, grid, explored, dungeon_level + global player, player_inventory, grid, dungeon_level global entity_data, item_data, fov_layer, game_over if not os.path.exists(SAVE_FILE): @@ -629,8 +611,8 @@ def load_game() -> bool: entity_data.clear() item_data.clear() - while len(grid.entities) > 0: - grid.entities.remove(0) + for e in list(grid.entities): + grid.entities.remove(e) # Restore dungeon level dungeon_level = save_data.get("dungeon_level", 1) @@ -645,10 +627,7 @@ def load_game() -> bool: cell.walkable = tile_data["walkable"] cell.transparent = tile_data["transparent"] - # Restore explored state - global explored - explored_data = save_data["explored"] - explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)] + # Exploration state is tracked internally by draw_fov() # Restore player player_data = save_data["player"] @@ -695,9 +674,7 @@ def load_game() -> bool: item_data[item_entity] = Item.from_dict(item_entry["item"]) # Reset FOV layer - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) # Compute initial FOV update_fov(grid, fov_layer, int(player.x), int(player.y)) @@ -890,18 +867,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc return None def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in entity_data: del entity_data[entity] def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in item_data: del item_data[entity] @@ -931,10 +902,7 @@ def exit_targeting_mode() -> None: global game_mode, target_cursor, grid if target_cursor is not None: - for i, e in enumerate(grid.entities): - if e == target_cursor: - grid.entities.remove(i) - break + grid.entities.remove(target_cursor) target_cursor = None game_mode = GameMode.NORMAL @@ -949,7 +917,7 @@ def move_cursor(dx: int, dy: int) -> None: if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT: return - if not grid.is_in_fov(new_x, new_y): + if not grid.is_in_fov((new_x, new_y)): message_log.add("You cannot see that location.", COLOR_INVALID) return @@ -1144,20 +1112,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: continue ex, ey = int(entity.x), int(entity.y) - entity.visible = target_grid.is_in_fov(ex, ey) + entity.visible = target_grid.is_in_fov((ex, ey)) def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: - target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) - - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if target_grid.is_in_fov(x, y): - mark_explored(x, y) - target_fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - target_fov_layer.set(x, y, COLOR_DISCOVERED) - else: - target_fov_layer.set(x, y, COLOR_UNKNOWN) + target_fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) update_entity_visibility(target_grid) @@ -1230,7 +1194,7 @@ def enemy_turn() -> None: ex, ey = int(enemy.x), int(enemy.y) - if not grid.is_in_fov(ex, ey): + if not grid.is_in_fov((ex, ey)): continue dx = player_x - ex @@ -1299,11 +1263,10 @@ def generate_new_game() -> None: entity_data.clear() item_data.clear() - while len(grid.entities) > 0: - grid.entities.remove(0) + for e in list(grid.entities): + grid.entities.remove(e) fill_with_walls(grid) - init_explored() message_log.clear() rooms: list[RectangularRoom] = [] @@ -1361,9 +1324,7 @@ def generate_new_game() -> None: spawn_enemies_in_room(grid, room, texture) spawn_items_in_room(grid, room, texture) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, new_x, new_y) @@ -1390,10 +1351,9 @@ grid = mcrfpy.Grid( ) # Add FOV layer -fov_layer = grid.add_layer("color", z_index=-1) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +fov_layer.fill(COLOR_UNKNOWN) # Add grid to scene scene.children.append(grid) @@ -1456,9 +1416,6 @@ message_log.add_to_scene(scene) # Initialize Game (Load or New) # ============================================================================= -# Initialize explored array -init_explored() - # Try to load existing save, otherwise generate new game if has_save_file(): message_log.add("Found saved game. Loading...", COLOR_INFO) @@ -1475,20 +1432,20 @@ else: # Input Handling # ============================================================================= -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: global game_over, game_mode - if action != "start": + if action != mcrfpy.InputState.PRESSED: return # Always allow restart - if key == "R": + if key == mcrfpy.Key.R: delete_save() generate_new_game() message_log.add("A new adventure begins!", COLOR_INFO) return - if key == "Escape": + if key == mcrfpy.Key.ESCAPE: if game_mode == GameMode.TARGETING: exit_targeting_mode() message_log.add("Targeting cancelled.", COLOR_INFO) @@ -1500,14 +1457,8 @@ def handle_keys(key: str, action: str) -> None: mcrfpy.exit() return - # Save game (Ctrl+S or just S when not moving) - if key == "S" and game_mode == GameMode.NORMAL: - # Check if this is meant to be a save (could add modifier check) - # For simplicity, we will use a dedicated save key - pass - # Dedicated save with period key - if key == "Period" and game_mode == GameMode.NORMAL and not game_over: + if key == mcrfpy.Key.PERIOD and game_mode == GameMode.NORMAL and not game_over: save_game() return @@ -1520,39 +1471,39 @@ def handle_keys(key: str, action: str) -> None: else: handle_normal_input(key) -def handle_normal_input(key: str) -> None: +def handle_normal_input(key) -> None: # Movement - if key == "W" or key == "Up": + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: try_move_or_attack(0, -1) - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: try_move_or_attack(0, 1) - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: try_move_or_attack(-1, 0) - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: try_move_or_attack(1, 0) # Ranged attack - elif key == "F": + elif key == mcrfpy.Key.F: enter_targeting_mode() # Pickup - elif key == "G" or key == ",": + elif key == mcrfpy.Key.G: pickup_item() # Use items - elif key in ["1", "2", "3", "4", "5"]: - index = int(key) - 1 + elif key in [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5]: + index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key) if use_item(index): enemy_turn() update_ui() -def handle_targeting_input(key: str) -> None: - if key == "Up" or key == "W": +def handle_targeting_input(key) -> None: + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: move_cursor(0, -1) - elif key == "Down" or key == "S": + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: move_cursor(0, 1) - elif key == "Left" or key == "A": + elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: move_cursor(-1, 0) - elif key == "Right" or key == "D": + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: move_cursor(1, 0) - elif key == "Return" or key == "Space": + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: confirm_target() scene.on_key = handle_keys diff --git a/docs/tutorials/part_11_levels/part_11_levels.py b/docs/tutorials/part_11_levels/part_11_levels.py index da2573c..5380ac6 100644 --- a/docs/tutorials/part_11_levels/part_11_levels.py +++ b/docs/tutorials/part_11_levels/part_11_levels.py @@ -567,24 +567,7 @@ class RectangularRoom: self.y2 >= other.y1 ) -# ============================================================================= -# Exploration Tracking -# ============================================================================= - -explored: list[list[bool]] = [] - -def init_explored() -> None: - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Exploration tracking is handled automatically by ColorLayer.draw_fov() # ============================================================================= # Dungeon Generation @@ -821,18 +804,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc return None def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in entity_data: del entity_data[entity] def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in item_data: del item_data[entity] @@ -851,11 +828,7 @@ def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None: del entity_data[entity] if entity in item_data: del item_data[entity] - - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) # ============================================================================= # Level Transition @@ -883,7 +856,6 @@ def descend_stairs() -> bool: clear_entities_except_player(grid) # Generate new dungeon - init_explored() player_start = generate_dungeon(grid, dungeon_level) # Move player to starting position @@ -896,9 +868,7 @@ def descend_stairs() -> bool: spawn_entities_for_level(grid, texture, dungeon_level) # Reset FOV - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, player_start[0], player_start[1]) @@ -977,7 +947,7 @@ def spawn_entities_for_level(target_grid: mcrfpy.Grid, tex: mcrfpy.Texture, leve def save_game() -> bool: """Save the current game state to a JSON file.""" - global player, player_inventory, grid, explored, dungeon_level, stairs_position + global player, player_inventory, grid, dungeon_level, stairs_position try: tiles = [] @@ -1026,7 +996,6 @@ def save_game() -> bool: "inventory": player_inventory.to_dict() }, "tiles": tiles, - "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)], "enemies": enemies, "items": items_on_ground } @@ -1043,7 +1012,7 @@ def save_game() -> bool: def load_game() -> bool: """Load a saved game from JSON file.""" - global player, player_inventory, grid, explored, dungeon_level + global player, player_inventory, grid, dungeon_level global entity_data, item_data, fov_layer, game_over, stairs_position if not os.path.exists(SAVE_FILE): @@ -1057,7 +1026,7 @@ def load_game() -> bool: item_data.clear() while len(grid.entities) > 0: - grid.entities.remove(0) + grid.entities.pop(0) dungeon_level = save_data.get("dungeon_level", 1) stairs_position = tuple(save_data.get("stairs_position", [0, 0])) @@ -1071,10 +1040,6 @@ def load_game() -> bool: cell.walkable = tile_data["walkable"] cell.transparent = tile_data["transparent"] - global explored - explored_data = save_data["explored"] - explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)] - player_data = save_data["player"] player = mcrfpy.Entity( grid_pos=(player_data["x"], player_data["y"]), @@ -1114,9 +1079,7 @@ def load_game() -> bool: grid.entities.append(item_entity) item_data[item_entity] = Item.from_dict(item_entry["item"]) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, int(player.x), int(player.y)) @@ -1168,10 +1131,7 @@ def exit_targeting_mode() -> None: global game_mode, target_cursor, grid if target_cursor is not None: - for i, e in enumerate(grid.entities): - if e == target_cursor: - grid.entities.remove(i) - break + grid.entities.remove(target_cursor) target_cursor = None game_mode = GameMode.NORMAL @@ -1186,7 +1146,7 @@ def move_cursor(dx: int, dy: int) -> None: if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT: return - if not grid.is_in_fov(new_x, new_y): + if not grid.is_in_fov((new_x, new_y)): message_log.add("You cannot see that location.", COLOR_INVALID) return @@ -1380,21 +1340,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: continue ex, ey = int(entity.x), int(entity.y) - entity.visible = target_grid.is_in_fov(ex, ey) + entity.visible = target_grid.is_in_fov((ex, ey)) def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: - target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) - - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if target_grid.is_in_fov(x, y): - mark_explored(x, y) - target_fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - target_fov_layer.set(x, y, COLOR_DISCOVERED) - else: - target_fov_layer.set(x, y, COLOR_UNKNOWN) - + target_fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) update_entity_visibility(target_grid) # ============================================================================= @@ -1466,7 +1421,7 @@ def enemy_turn() -> None: ex, ey = int(enemy.x), int(enemy.y) - if not grid.is_in_fov(ex, ey): + if not grid.is_in_fov((ex, ey)): continue dx = player_x - ex @@ -1535,9 +1490,8 @@ def generate_new_game() -> None: item_data.clear() while len(grid.entities) > 0: - grid.entities.remove(0) + grid.entities.pop(0) - init_explored() message_log.clear() player_start = generate_dungeon(grid, dungeon_level) @@ -1562,9 +1516,7 @@ def generate_new_game() -> None: spawn_entities_for_level(grid, texture, dungeon_level) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, player_start[0], player_start[1]) @@ -1587,10 +1539,9 @@ grid = mcrfpy.Grid( zoom=1.0 ) -fov_layer = grid.add_layer("color", z_index=-1) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +fov_layer.fill(COLOR_UNKNOWN) scene.children.append(grid) @@ -1639,8 +1590,6 @@ message_log.add_to_scene(scene) # Initialize Game # ============================================================================= -init_explored() - if has_save_file(): message_log.add("Found saved game. Loading...", COLOR_INFO) if not load_game(): @@ -1656,19 +1605,19 @@ else: # Input Handling # ============================================================================= -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: global game_over, game_mode - if action != "start": + if action != mcrfpy.InputState.PRESSED: return - if key == "R": + if key == mcrfpy.Key.R: delete_save() generate_new_game() message_log.add("A new adventure begins!", COLOR_INFO) return - if key == "Escape": + if key == mcrfpy.Key.ESCAPE: if game_mode == GameMode.TARGETING: exit_targeting_mode() message_log.add("Targeting cancelled.", COLOR_INFO) @@ -1679,7 +1628,7 @@ def handle_keys(key: str, action: str) -> None: mcrfpy.exit() return - if key == "Period" and game_mode == GameMode.NORMAL and not game_over: + if key == mcrfpy.Key.PERIOD and game_mode == GameMode.NORMAL and not game_over: save_game() return @@ -1691,38 +1640,38 @@ def handle_keys(key: str, action: str) -> None: else: handle_normal_input(key) -def handle_normal_input(key: str) -> None: - if key == "W" or key == "Up": +def handle_normal_input(key) -> None: + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: try_move_or_attack(0, -1) - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: try_move_or_attack(0, 1) - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: try_move_or_attack(-1, 0) - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: try_move_or_attack(1, 0) - elif key == "F": + elif key == mcrfpy.Key.F: enter_targeting_mode() - elif key == "G" or key == ",": + elif key == mcrfpy.Key.G or key == mcrfpy.Key.COMMA: pickup_item() - elif key == "Period" and mcrfpy.keypressed("LShift"): + elif key == mcrfpy.Key.PERIOD and mcrfpy.keyboard.shift: # Shift+. (>) to descend stairs descend_stairs() - elif key in ["1", "2", "3", "4", "5"]: - index = int(key) - 1 + elif key in (mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5): + index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key) if use_item(index): enemy_turn() update_ui() -def handle_targeting_input(key: str) -> None: - if key == "Up" or key == "W": +def handle_targeting_input(key) -> None: + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: move_cursor(0, -1) - elif key == "Down" or key == "S": + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: move_cursor(0, 1) - elif key == "Left" or key == "A": + elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: move_cursor(-1, 0) - elif key == "Right" or key == "D": + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: move_cursor(1, 0) - elif key == "Return" or key == "Space": + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: confirm_target() scene.on_key = handle_keys diff --git a/docs/tutorials/part_12_experience/part_12_experience.py b/docs/tutorials/part_12_experience/part_12_experience.py index 6a3034e..af9bf0a 100644 --- a/docs/tutorials/part_12_experience/part_12_experience.py +++ b/docs/tutorials/part_12_experience/part_12_experience.py @@ -723,24 +723,7 @@ class RectangularRoom: self.y2 >= other.y1 ) -# ============================================================================= -# Exploration Tracking -# ============================================================================= - -explored: list[list[bool]] = [] - -def init_explored() -> None: - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Exploration tracking is handled automatically by ColorLayer.draw_fov() # ============================================================================= # Dungeon Generation @@ -972,18 +955,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc return None def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in entity_data: del entity_data[entity] def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in item_data: del item_data[entity] @@ -1001,11 +978,7 @@ def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None: del entity_data[entity] if entity in item_data: del item_data[entity] - - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) # ============================================================================= # XP and Level Up @@ -1059,7 +1032,6 @@ def descend_stairs() -> bool: clear_entities_except_player(grid) - init_explored() player_start = generate_dungeon(grid, dungeon_level) player.x = player_start[0] @@ -1067,9 +1039,7 @@ def descend_stairs() -> bool: spawn_entities_for_level(grid, texture, dungeon_level) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, player_start[0], player_start[1]) @@ -1084,7 +1054,7 @@ def descend_stairs() -> bool: # ============================================================================= def save_game() -> bool: - global player, player_inventory, grid, explored, dungeon_level, stairs_position + global player, player_inventory, grid, dungeon_level, stairs_position try: tiles = [] @@ -1133,7 +1103,6 @@ def save_game() -> bool: "inventory": player_inventory.to_dict() }, "tiles": tiles, - "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)], "enemies": enemies, "items": items_on_ground } @@ -1149,7 +1118,7 @@ def save_game() -> bool: return False def load_game() -> bool: - global player, player_inventory, grid, explored, dungeon_level + global player, player_inventory, grid, dungeon_level global entity_data, item_data, fov_layer, game_over, stairs_position if not os.path.exists(SAVE_FILE): @@ -1163,7 +1132,7 @@ def load_game() -> bool: item_data.clear() while len(grid.entities) > 0: - grid.entities.remove(0) + grid.entities.pop(0) dungeon_level = save_data.get("dungeon_level", 1) stairs_position = tuple(save_data.get("stairs_position", [0, 0])) @@ -1177,10 +1146,6 @@ def load_game() -> bool: cell.walkable = tile_data["walkable"] cell.transparent = tile_data["transparent"] - global explored - explored_data = save_data["explored"] - explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)] - player_data = save_data["player"] player = mcrfpy.Entity( grid_pos=(player_data["x"], player_data["y"]), @@ -1220,9 +1185,7 @@ def load_game() -> bool: grid.entities.append(item_entity) item_data[item_entity] = Item.from_dict(item_entry["item"]) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, int(player.x), int(player.y)) @@ -1274,10 +1237,7 @@ def exit_targeting_mode() -> None: global game_mode, target_cursor, grid if target_cursor is not None: - for i, e in enumerate(grid.entities): - if e == target_cursor: - grid.entities.remove(i) - break + grid.entities.remove(target_cursor) target_cursor = None game_mode = GameMode.NORMAL @@ -1292,7 +1252,7 @@ def move_cursor(dx: int, dy: int) -> None: if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT: return - if not grid.is_in_fov(new_x, new_y): + if not grid.is_in_fov((new_x, new_y)): message_log.add("You cannot see that location.", COLOR_INVALID) return @@ -1488,21 +1448,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: continue ex, ey = int(entity.x), int(entity.y) - entity.visible = target_grid.is_in_fov(ex, ey) + entity.visible = target_grid.is_in_fov((ex, ey)) def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: - target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) - - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if target_grid.is_in_fov(x, y): - mark_explored(x, y) - target_fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - target_fov_layer.set(x, y, COLOR_DISCOVERED) - else: - target_fov_layer.set(x, y, COLOR_UNKNOWN) - + target_fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) update_entity_visibility(target_grid) # ============================================================================= @@ -1574,7 +1529,7 @@ def enemy_turn() -> None: ex, ey = int(enemy.x), int(enemy.y) - if not grid.is_in_fov(ex, ey): + if not grid.is_in_fov((ex, ey)): continue dx = player_x - ex @@ -1644,9 +1599,8 @@ def generate_new_game() -> None: item_data.clear() while len(grid.entities) > 0: - grid.entities.remove(0) + grid.entities.pop(0) - init_explored() message_log.clear() player_start = generate_dungeon(grid, dungeon_level) @@ -1673,9 +1627,7 @@ def generate_new_game() -> None: spawn_entities_for_level(grid, texture, dungeon_level) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, player_start[0], player_start[1]) @@ -1698,10 +1650,9 @@ grid = mcrfpy.Grid( zoom=1.0 ) -fov_layer = grid.add_layer("color", z_index=-1) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +fov_layer.fill(COLOR_UNKNOWN) scene.children.append(grid) @@ -1756,8 +1707,6 @@ message_log.add_to_scene(scene) # Initialize Game # ============================================================================= -init_explored() - if has_save_file(): message_log.add("Found saved game. Loading...", COLOR_INFO) if not load_game(): @@ -1773,19 +1722,19 @@ else: # Input Handling # ============================================================================= -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: global game_over, game_mode - if action != "start": + if action != mcrfpy.InputState.PRESSED: return - if key == "R": + if key == mcrfpy.Key.R: delete_save() generate_new_game() message_log.add("A new adventure begins!", COLOR_INFO) return - if key == "Escape": + if key == mcrfpy.Key.ESCAPE: if game_mode == GameMode.TARGETING: exit_targeting_mode() message_log.add("Targeting cancelled.", COLOR_INFO) @@ -1796,7 +1745,7 @@ def handle_keys(key: str, action: str) -> None: mcrfpy.exit() return - if key == "Period" and game_mode == GameMode.NORMAL and not game_over: + if key == mcrfpy.Key.PERIOD and game_mode == GameMode.NORMAL and not game_over: # Check for shift to descend descend_stairs() return @@ -1809,35 +1758,35 @@ def handle_keys(key: str, action: str) -> None: else: handle_normal_input(key) -def handle_normal_input(key: str) -> None: - if key == "W" or key == "Up": +def handle_normal_input(key) -> None: + if key == mcrfpy.Key.W or key == mcrfpy.Key.UP: try_move_or_attack(0, -1) - elif key == "S" or key == "Down": + elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN: try_move_or_attack(0, 1) - elif key == "A" or key == "Left": + elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT: try_move_or_attack(-1, 0) - elif key == "D" or key == "Right": + elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT: try_move_or_attack(1, 0) - elif key == "F": + elif key == mcrfpy.Key.F: enter_targeting_mode() - elif key == "G" or key == ",": + elif key == mcrfpy.Key.G or key == mcrfpy.Key.COMMA: pickup_item() - elif key in ["1", "2", "3", "4", "5"]: - index = int(key) - 1 + elif key in (mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5): + index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key) if use_item(index): enemy_turn() update_ui() -def handle_targeting_input(key: str) -> None: - if key == "Up" or key == "W": +def handle_targeting_input(key) -> None: + if key == mcrfpy.Key.UP or key == mcrfpy.Key.W: move_cursor(0, -1) - elif key == "Down" or key == "S": + elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S: move_cursor(0, 1) - elif key == "Left" or key == "A": + elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A: move_cursor(-1, 0) - elif key == "Right" or key == "D": + elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D: move_cursor(1, 0) - elif key == "Return" or key == "Space": + elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE: confirm_target() scene.on_key = handle_keys diff --git a/docs/tutorials/part_13_equipment/part_13_equipment.py b/docs/tutorials/part_13_equipment/part_13_equipment.py index bca3f3d..da454bf 100644 --- a/docs/tutorials/part_13_equipment/part_13_equipment.py +++ b/docs/tutorials/part_13_equipment/part_13_equipment.py @@ -801,24 +801,7 @@ class RectangularRoom: def intersects(self, other: "RectangularRoom") -> bool: return self.x1 <= other.x2 and self.x2 >= other.x1 and self.y1 <= other.y2 and self.y2 >= other.y1 -# ============================================================================= -# Exploration Tracking -# ============================================================================= - -explored: list[list[bool]] = [] - -def init_explored() -> None: - global explored - explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)] - -def mark_explored(x: int, y: int) -> None: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - explored[y][x] = True - -def is_explored(x: int, y: int) -> bool: - if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT: - return explored[y][x] - return False +# Exploration tracking is handled automatically by ColorLayer.draw_fov() # ============================================================================= # Dungeon Generation (abbreviated for space) @@ -1014,18 +997,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc return None def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in entity_data: del entity_data[entity] def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None: - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) if entity in item_data: del item_data[entity] @@ -1041,10 +1018,7 @@ def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None: del entity_data[entity] if entity in item_data: del item_data[entity] - for i, e in enumerate(target_grid.entities): - if e == entity: - target_grid.entities.remove(i) - break + target_grid.entities.remove(entity) # ============================================================================= # Equipment Actions @@ -1174,14 +1148,11 @@ def descend_stairs() -> bool: dungeon_level += 1 clear_entities_except_player(grid) - init_explored() player_start = generate_dungeon(grid, dungeon_level) player.x, player.y = player_start spawn_entities_for_level(grid, texture, dungeon_level) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, player_start[0], player_start[1]) message_log.add(f"You descend to level {dungeon_level}...", COLOR_DESCEND) @@ -1194,7 +1165,7 @@ def descend_stairs() -> bool: # ============================================================================= def save_game() -> bool: - global player, player_inventory, grid, explored, dungeon_level, stairs_position + global player, player_inventory, grid, dungeon_level, stairs_position try: tiles = [] @@ -1238,7 +1209,6 @@ def save_game() -> bool: "inventory": player_inventory.to_dict() }, "tiles": tiles, - "explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)], "enemies": enemies, "items": items_on_ground } @@ -1253,7 +1223,7 @@ def save_game() -> bool: return False def load_game() -> bool: - global player, player_inventory, grid, explored, dungeon_level + global player, player_inventory, grid, dungeon_level global entity_data, item_data, fov_layer, game_over, stairs_position if not os.path.exists(SAVE_FILE): @@ -1266,7 +1236,7 @@ def load_game() -> bool: entity_data.clear() item_data.clear() while len(grid.entities) > 0: - grid.entities.remove(0) + grid.entities.pop(0) dungeon_level = save_data.get("dungeon_level", 1) stairs_position = tuple(save_data.get("stairs_position", [0, 0])) @@ -1280,9 +1250,6 @@ def load_game() -> bool: cell.walkable = tile_data["walkable"] cell.transparent = tile_data["transparent"] - global explored - explored = save_data["explored"] - player_data = save_data["player"] player = mcrfpy.Entity( grid_pos=(player_data["x"], player_data["y"]), @@ -1321,9 +1288,7 @@ def load_game() -> bool: grid.entities.append(item_entity) item_data[item_entity] = item - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, int(player.x), int(player.y)) game_over = False @@ -1362,10 +1327,7 @@ def enter_targeting_mode() -> None: def exit_targeting_mode() -> None: global game_mode, target_cursor if target_cursor: - for i, e in enumerate(grid.entities): - if e == target_cursor: - grid.entities.remove(i) - break + grid.entities.remove(target_cursor) target_cursor = None game_mode = GameMode.NORMAL mode_display.update(game_mode) @@ -1375,7 +1337,7 @@ def move_cursor(dx: int, dy: int) -> None: new_x, new_y = target_x + dx, target_y + dy if not (0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT): return - if not grid.is_in_fov(new_x, new_y): + if not grid.is_in_fov((new_x, new_y)): message_log.add("Cannot see that location.", COLOR_INVALID) return distance = abs(new_x - int(player.x)) + abs(new_y - int(player.y)) @@ -1515,19 +1477,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None: if entity == player or entity == target_cursor: entity.visible = True else: - entity.visible = target_grid.is_in_fov(int(entity.x), int(entity.y)) + entity.visible = target_grid.is_in_fov((int(entity.x), int(entity.y))) def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None: - target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - if target_grid.is_in_fov(x, y): - mark_explored(x, y) - target_fov_layer.set(x, y, COLOR_VISIBLE) - elif is_explored(x, y): - target_fov_layer.set(x, y, COLOR_DISCOVERED) - else: - target_fov_layer.set(x, y, COLOR_UNKNOWN) + target_fov_layer.draw_fov( + (player_x, player_y), + radius=FOV_RADIUS, + visible=COLOR_VISIBLE, + discovered=COLOR_DISCOVERED, + unknown=COLOR_UNKNOWN + ) update_entity_visibility(target_grid) # ============================================================================= @@ -1577,7 +1536,7 @@ def enemy_turn() -> None: if not fighter.is_alive: continue ex, ey = int(entity.x), int(entity.y) - if not grid.is_in_fov(ex, ey): + if not grid.is_in_fov((ex, ey)): continue dx, dy = px - ex, py - ey @@ -1624,9 +1583,8 @@ def generate_new_game() -> None: entity_data.clear() item_data.clear() while len(grid.entities) > 0: - grid.entities.remove(0) + grid.entities.pop(0) - init_explored() message_log.clear() player_start = generate_dungeon(grid, dungeon_level) @@ -1642,9 +1600,7 @@ def generate_new_game() -> None: player_inventory = Inventory(capacity=10) spawn_entities_for_level(grid, texture, dungeon_level) - for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) + fov_layer.fill(COLOR_UNKNOWN) update_fov(grid, fov_layer, player_start[0], player_start[1]) mode_display.update(game_mode) @@ -1666,10 +1622,9 @@ grid = mcrfpy.Grid( zoom=1.0 ) -fov_layer = grid.add_layer("color", z_index=-1) -for y in range(GRID_HEIGHT): - for x in range(GRID_WIDTH): - fov_layer.set(x, y, COLOR_UNKNOWN) +fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov") +grid.add_layer(fov_layer) +fov_layer.fill(COLOR_UNKNOWN) scene.children.append(grid) @@ -1706,8 +1661,6 @@ message_log = MessageLog(x=20, y=768 - UI_BOTTOM_HEIGHT + 10, width=990, height= message_log.add_to_scene(scene) # Initialize -init_explored() - if has_save_file(): message_log.add("Loading saved game...", COLOR_INFO) if not load_game(): @@ -1722,19 +1675,19 @@ else: # Input Handling # ============================================================================= -def handle_keys(key: str, action: str) -> None: +def handle_keys(key, action) -> None: global game_over, game_mode - if action != "start": + if action != mcrfpy.InputState.PRESSED: return - if key == "R": + if key == mcrfpy.Key.R: delete_save() generate_new_game() message_log.add("New adventure begins!", COLOR_INFO) return - if key == "Escape": + if key == mcrfpy.Key.ESCAPE: if game_mode == GameMode.TARGETING: exit_targeting_mode() message_log.add("Targeting cancelled.", COLOR_INFO) @@ -1752,39 +1705,39 @@ def handle_keys(key: str, action: str) -> None: else: handle_normal_input(key) -def handle_normal_input(key: str) -> None: - if key in ("W", "Up"): +def handle_normal_input(key) -> None: + if key in (mcrfpy.Key.W, mcrfpy.Key.UP): try_move_or_attack(0, -1) - elif key in ("S", "Down"): + elif key in (mcrfpy.Key.S, mcrfpy.Key.DOWN): try_move_or_attack(0, 1) - elif key in ("A", "Left"): + elif key in (mcrfpy.Key.A, mcrfpy.Key.LEFT): try_move_or_attack(-1, 0) - elif key in ("D", "Right"): + elif key in (mcrfpy.Key.D, mcrfpy.Key.RIGHT): try_move_or_attack(1, 0) - elif key == "F": + elif key == mcrfpy.Key.F: enter_targeting_mode() - elif key in ("G", ","): + elif key in (mcrfpy.Key.G, mcrfpy.Key.COMMA): pickup_item() - elif key == "Period": + elif key == mcrfpy.Key.PERIOD: descend_stairs() - elif key == "E": + elif key == mcrfpy.Key.E: message_log.add("Press 1-5 to equip an item from inventory.", COLOR_INFO) - elif key in "12345": - index = int(key) - 1 + elif key in (mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5): + index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key) if use_item(index): enemy_turn() update_ui() -def handle_targeting_input(key: str) -> None: - if key in ("Up", "W"): +def handle_targeting_input(key) -> None: + if key in (mcrfpy.Key.UP, mcrfpy.Key.W): move_cursor(0, -1) - elif key in ("Down", "S"): + elif key in (mcrfpy.Key.DOWN, mcrfpy.Key.S): move_cursor(0, 1) - elif key in ("Left", "A"): + elif key in (mcrfpy.Key.LEFT, mcrfpy.Key.A): move_cursor(-1, 0) - elif key in ("Right", "D"): + elif key in (mcrfpy.Key.RIGHT, mcrfpy.Key.D): move_cursor(1, 0) - elif key in ("Return", "Space"): + elif key in (mcrfpy.Key.ENTER, mcrfpy.Key.SPACE): confirm_target() scene.on_key = handle_keys