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 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-04-09 23:23:35 -04:00
commit 1805b985bd
13 changed files with 388 additions and 765 deletions

View file

@ -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