feat(tcod): complete Dijkstra pathfinding implementation with critical PyArg fix
- Add complete Dijkstra pathfinding to UIGrid class - compute_dijkstra(), get_dijkstra_distance(), get_dijkstra_path() - Full TCODMap and TCODDijkstra integration - Proper memory management in constructors/destructors - Create mcrfpy.libtcod submodule with Python bindings - dijkstra_compute(), dijkstra_get_distance(), dijkstra_get_path() - line() function for drawing corridors - Foundation for future FOV and pathfinding algorithms - Fix critical PyArg bug in UIGridPoint color setter - PyObject_to_sfColor() now handles both Color objects and tuples - Prevents "SystemError: new style getargs format but argument is not a tuple" - Proper error handling and exception propagation - Add comprehensive test suite - test_dijkstra_simple.py validates all pathfinding operations - dijkstra_test.py for headless testing with screenshots - dijkstra_interactive.py for full user interaction demos - Consolidate and clean up test files - Removed 6 duplicate/broken demo attempts - Two clean versions: headless test + interactive demo Part of TCOD integration sprint for RoguelikeDev Tutorial Event. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
192d1ae1dd
commit
c1e02070f4
|
@ -27,3 +27,5 @@ forest_fire_CA.py
|
||||||
mcrogueface.github.io
|
mcrogueface.github.io
|
||||||
scripts/
|
scripts/
|
||||||
test_*
|
test_*
|
||||||
|
|
||||||
|
tcod_reference
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import mcrfpy
|
||||||
|
e = mcrfpy.Entity(0, 0)
|
||||||
|
print("Entity attributes:", dir(e))
|
||||||
|
print("\nEntity repr:", repr(e))
|
|
@ -0,0 +1,244 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Dijkstra Pathfinding Interactive Demo
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Interactive visualization showing Dijkstra pathfinding between entities.
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
- Press 1/2/3 to select the first entity
|
||||||
|
- Press A/B/C to select the second entity
|
||||||
|
- Space to clear selection
|
||||||
|
- Q or ESC to quit
|
||||||
|
|
||||||
|
The path between selected entities is automatically highlighted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
WALL_COLOR = mcrfpy.Color(60, 30, 30)
|
||||||
|
FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
|
||||||
|
PATH_COLOR = mcrfpy.Color(200, 250, 220)
|
||||||
|
ENTITY_COLORS = [
|
||||||
|
mcrfpy.Color(255, 100, 100), # Entity 1 - Red
|
||||||
|
mcrfpy.Color(100, 255, 100), # Entity 2 - Green
|
||||||
|
mcrfpy.Color(100, 100, 255), # Entity 3 - Blue
|
||||||
|
]
|
||||||
|
|
||||||
|
# Global state
|
||||||
|
grid = None
|
||||||
|
entities = []
|
||||||
|
first_point = None
|
||||||
|
second_point = None
|
||||||
|
|
||||||
|
def create_map():
|
||||||
|
"""Create the interactive map with the layout specified by the user"""
|
||||||
|
global grid, entities
|
||||||
|
|
||||||
|
mcrfpy.createScene("dijkstra_interactive")
|
||||||
|
|
||||||
|
# Create grid - 14x10 as specified
|
||||||
|
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||||
|
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
||||||
|
# Define the map layout from user's specification
|
||||||
|
# . = floor, W = wall, E = entity position
|
||||||
|
map_layout = [
|
||||||
|
"..............", # Row 0
|
||||||
|
"..W.....WWWW..", # Row 1
|
||||||
|
"..W.W...W.EW..", # Row 2
|
||||||
|
"..W.....W..W..", # Row 3
|
||||||
|
"..W...E.WWWW..", # Row 4
|
||||||
|
"E.W...........", # Row 5
|
||||||
|
"..W...........", # Row 6
|
||||||
|
"..W...........", # Row 7
|
||||||
|
"..W.WWW.......", # Row 8
|
||||||
|
"..............", # Row 9
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create the map
|
||||||
|
entity_positions = []
|
||||||
|
for y, row in enumerate(map_layout):
|
||||||
|
for x, char in enumerate(row):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
|
||||||
|
if char == 'W':
|
||||||
|
# Wall
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.color = WALL_COLOR
|
||||||
|
else:
|
||||||
|
# Floor
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
cell.color = FLOOR_COLOR
|
||||||
|
|
||||||
|
if char == 'E':
|
||||||
|
# Entity position
|
||||||
|
entity_positions.append((x, y))
|
||||||
|
|
||||||
|
# Create entities at marked positions
|
||||||
|
entities = []
|
||||||
|
for i, (x, y) in enumerate(entity_positions):
|
||||||
|
entity = mcrfpy.Entity(x, y)
|
||||||
|
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||||
|
grid.entities.append(entity)
|
||||||
|
entities.append(entity)
|
||||||
|
|
||||||
|
return grid
|
||||||
|
|
||||||
|
def clear_path_highlight():
|
||||||
|
"""Clear any existing path highlighting"""
|
||||||
|
# Reset all floor tiles to original color
|
||||||
|
for y in range(grid.grid_y):
|
||||||
|
for x in range(grid.grid_x):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
if cell.walkable:
|
||||||
|
cell.color = FLOOR_COLOR
|
||||||
|
|
||||||
|
def highlight_path():
|
||||||
|
"""Highlight the path between selected entities"""
|
||||||
|
if first_point is None or second_point is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clear previous highlighting
|
||||||
|
clear_path_highlight()
|
||||||
|
|
||||||
|
# Get entities
|
||||||
|
entity1 = entities[first_point]
|
||||||
|
entity2 = entities[second_point]
|
||||||
|
|
||||||
|
# Compute Dijkstra from first entity
|
||||||
|
grid.compute_dijkstra(int(entity1.x), int(entity1.y))
|
||||||
|
|
||||||
|
# Get path to second entity
|
||||||
|
path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y))
|
||||||
|
|
||||||
|
if path:
|
||||||
|
# Highlight the path
|
||||||
|
for x, y in path:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
if cell.walkable:
|
||||||
|
cell.color = PATH_COLOR
|
||||||
|
|
||||||
|
# Also highlight start and end with entity colors
|
||||||
|
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point]
|
||||||
|
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point]
|
||||||
|
|
||||||
|
# Update info
|
||||||
|
distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y))
|
||||||
|
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units"
|
||||||
|
else:
|
||||||
|
info_text.text = f"No path between Entity {first_point+1} and Entity {second_point+1}"
|
||||||
|
|
||||||
|
def handle_keypress(scene_name, keycode):
|
||||||
|
"""Handle keyboard input"""
|
||||||
|
global first_point, second_point
|
||||||
|
|
||||||
|
# Number keys for first entity
|
||||||
|
if keycode == 49: # '1'
|
||||||
|
first_point = 0
|
||||||
|
status_text.text = f"First: Entity 1 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
|
||||||
|
highlight_path()
|
||||||
|
elif keycode == 50: # '2'
|
||||||
|
first_point = 1
|
||||||
|
status_text.text = f"First: Entity 2 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
|
||||||
|
highlight_path()
|
||||||
|
elif keycode == 51: # '3'
|
||||||
|
first_point = 2
|
||||||
|
status_text.text = f"First: Entity 3 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
|
||||||
|
highlight_path()
|
||||||
|
|
||||||
|
# Letter keys for second entity
|
||||||
|
elif keycode == 65 or keycode == 97: # 'A' or 'a'
|
||||||
|
second_point = 0
|
||||||
|
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 1"
|
||||||
|
highlight_path()
|
||||||
|
elif keycode == 66 or keycode == 98: # 'B' or 'b'
|
||||||
|
second_point = 1
|
||||||
|
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 2"
|
||||||
|
highlight_path()
|
||||||
|
elif keycode == 67 or keycode == 99: # 'C' or 'c'
|
||||||
|
second_point = 2
|
||||||
|
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 3"
|
||||||
|
highlight_path()
|
||||||
|
|
||||||
|
# Clear selection
|
||||||
|
elif keycode == 32: # Space
|
||||||
|
first_point = None
|
||||||
|
second_point = None
|
||||||
|
clear_path_highlight()
|
||||||
|
status_text.text = "Press 1/2/3 for first entity, A/B/C for second"
|
||||||
|
info_text.text = "Space to clear, Q to quit"
|
||||||
|
|
||||||
|
# Quit
|
||||||
|
elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||||
|
print("\nExiting Dijkstra interactive demo...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Create the visualization
|
||||||
|
print("Dijkstra Pathfinding Interactive Demo")
|
||||||
|
print("=====================================")
|
||||||
|
print("Controls:")
|
||||||
|
print(" 1/2/3 - Select first entity")
|
||||||
|
print(" A/B/C - Select second entity")
|
||||||
|
print(" Space - Clear selection")
|
||||||
|
print(" Q/ESC - Quit")
|
||||||
|
|
||||||
|
# Create map
|
||||||
|
grid = create_map()
|
||||||
|
|
||||||
|
# Set up UI
|
||||||
|
ui = mcrfpy.sceneUI("dijkstra_interactive")
|
||||||
|
ui.append(grid)
|
||||||
|
|
||||||
|
# Scale and position grid for better visibility
|
||||||
|
grid.size = (560, 400) # 14*40, 10*40
|
||||||
|
grid.position = (120, 60)
|
||||||
|
|
||||||
|
# Add title
|
||||||
|
title = mcrfpy.Caption("Dijkstra Pathfinding Interactive", 250, 10)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
ui.append(title)
|
||||||
|
|
||||||
|
# Add status text
|
||||||
|
status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480)
|
||||||
|
status_text.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
ui.append(status_text)
|
||||||
|
|
||||||
|
# Add info text
|
||||||
|
info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500)
|
||||||
|
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
ui.append(info_text)
|
||||||
|
|
||||||
|
# Add legend
|
||||||
|
legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 540)
|
||||||
|
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
|
ui.append(legend1)
|
||||||
|
|
||||||
|
legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 560)
|
||||||
|
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
|
ui.append(legend2)
|
||||||
|
|
||||||
|
# Mark entity positions with colored indicators
|
||||||
|
for i, entity in enumerate(entities):
|
||||||
|
marker = mcrfpy.Caption(str(i+1),
|
||||||
|
120 + int(entity.x) * 40 + 15,
|
||||||
|
60 + int(entity.y) * 40 + 10)
|
||||||
|
marker.fill_color = ENTITY_COLORS[i]
|
||||||
|
marker.outline = 1
|
||||||
|
marker.outline_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
ui.append(marker)
|
||||||
|
|
||||||
|
# Set up input handling
|
||||||
|
mcrfpy.keypressScene(handle_keypress)
|
||||||
|
|
||||||
|
# Show the scene
|
||||||
|
mcrfpy.setScene("dijkstra_interactive")
|
||||||
|
|
||||||
|
print("\nVisualization ready!")
|
||||||
|
print("Entities are at:")
|
||||||
|
for i, entity in enumerate(entities):
|
||||||
|
print(f" Entity {i+1}: ({int(entity.x)}, {int(entity.y)})")
|
|
@ -0,0 +1,146 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Dijkstra Pathfinding Test - Headless
|
||||||
|
====================================
|
||||||
|
|
||||||
|
Tests all Dijkstra functionality and generates a screenshot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def create_test_map():
|
||||||
|
"""Create a test map with obstacles"""
|
||||||
|
mcrfpy.createScene("dijkstra_test")
|
||||||
|
|
||||||
|
# Create grid
|
||||||
|
grid = mcrfpy.Grid(grid_x=20, grid_y=12)
|
||||||
|
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
||||||
|
# Initialize all cells as walkable floor
|
||||||
|
for y in range(12):
|
||||||
|
for x in range(20):
|
||||||
|
grid.at(x, y).walkable = True
|
||||||
|
grid.at(x, y).transparent = True
|
||||||
|
grid.at(x, y).color = mcrfpy.Color(200, 200, 220)
|
||||||
|
|
||||||
|
# Add walls to create interesting paths
|
||||||
|
walls = [
|
||||||
|
# Vertical wall in the middle
|
||||||
|
(10, 1), (10, 2), (10, 3), (10, 4), (10, 5), (10, 6), (10, 7), (10, 8),
|
||||||
|
# Horizontal walls
|
||||||
|
(2, 6), (3, 6), (4, 6), (5, 6), (6, 6),
|
||||||
|
(14, 6), (15, 6), (16, 6), (17, 6),
|
||||||
|
# Some scattered obstacles
|
||||||
|
(5, 2), (15, 2), (5, 9), (15, 9)
|
||||||
|
]
|
||||||
|
|
||||||
|
for x, y in walls:
|
||||||
|
grid.at(x, y).walkable = False
|
||||||
|
grid.at(x, y).color = mcrfpy.Color(60, 30, 30)
|
||||||
|
|
||||||
|
# Place test entities
|
||||||
|
entities = []
|
||||||
|
positions = [(2, 2), (17, 2), (9, 10)]
|
||||||
|
colors = [
|
||||||
|
mcrfpy.Color(255, 100, 100), # Red
|
||||||
|
mcrfpy.Color(100, 255, 100), # Green
|
||||||
|
mcrfpy.Color(100, 100, 255) # Blue
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (x, y) in enumerate(positions):
|
||||||
|
entity = mcrfpy.Entity(x, y)
|
||||||
|
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||||
|
grid.entities.append(entity)
|
||||||
|
entities.append(entity)
|
||||||
|
# Mark entity positions
|
||||||
|
grid.at(x, y).color = colors[i]
|
||||||
|
|
||||||
|
return grid, entities
|
||||||
|
|
||||||
|
def test_dijkstra(grid, entities):
|
||||||
|
"""Test Dijkstra pathfinding between all entity pairs"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i in range(len(entities)):
|
||||||
|
for j in range(len(entities)):
|
||||||
|
if i != j:
|
||||||
|
# Compute Dijkstra from entity i
|
||||||
|
e1 = entities[i]
|
||||||
|
e2 = entities[j]
|
||||||
|
grid.compute_dijkstra(int(e1.x), int(e1.y))
|
||||||
|
|
||||||
|
# Get distance and path to entity j
|
||||||
|
distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y))
|
||||||
|
path = grid.get_dijkstra_path(int(e2.x), int(e2.y))
|
||||||
|
|
||||||
|
if path:
|
||||||
|
results.append(f"Path {i+1}→{j+1}: {len(path)} steps, {distance:.1f} units")
|
||||||
|
|
||||||
|
# Color one interesting path
|
||||||
|
if i == 0 and j == 2: # Path from 1 to 3
|
||||||
|
for x, y in path[1:-1]: # Skip endpoints
|
||||||
|
if grid.at(x, y).walkable:
|
||||||
|
grid.at(x, y).color = mcrfpy.Color(200, 250, 220)
|
||||||
|
else:
|
||||||
|
results.append(f"Path {i+1}→{j+1}: No path found!")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def run_test(runtime):
|
||||||
|
"""Timer callback to run tests and take screenshot"""
|
||||||
|
# Run pathfinding tests
|
||||||
|
results = test_dijkstra(grid, entities)
|
||||||
|
|
||||||
|
# Update display with results
|
||||||
|
y_pos = 380
|
||||||
|
for result in results:
|
||||||
|
caption = mcrfpy.Caption(result, 50, y_pos)
|
||||||
|
caption.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
ui.append(caption)
|
||||||
|
y_pos += 20
|
||||||
|
|
||||||
|
# Take screenshot
|
||||||
|
mcrfpy.setTimer("screenshot", lambda rt: take_screenshot(), 500)
|
||||||
|
|
||||||
|
def take_screenshot():
|
||||||
|
"""Take screenshot and exit"""
|
||||||
|
try:
|
||||||
|
automation.screenshot("dijkstra_test.png")
|
||||||
|
print("Screenshot saved: dijkstra_test.png")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Screenshot failed: {e}")
|
||||||
|
|
||||||
|
# Exit
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Create test map
|
||||||
|
print("Creating Dijkstra pathfinding test...")
|
||||||
|
grid, entities = create_test_map()
|
||||||
|
|
||||||
|
# Set up UI
|
||||||
|
ui = mcrfpy.sceneUI("dijkstra_test")
|
||||||
|
ui.append(grid)
|
||||||
|
|
||||||
|
# Position and scale grid
|
||||||
|
grid.position = (50, 50)
|
||||||
|
grid.size = (500, 300)
|
||||||
|
|
||||||
|
# Add title
|
||||||
|
title = mcrfpy.Caption("Dijkstra Pathfinding Test", 200, 10)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
ui.append(title)
|
||||||
|
|
||||||
|
# Add legend
|
||||||
|
legend = mcrfpy.Caption("Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3", 50, 360)
|
||||||
|
legend.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
ui.append(legend)
|
||||||
|
|
||||||
|
# Set scene
|
||||||
|
mcrfpy.setScene("dijkstra_test")
|
||||||
|
|
||||||
|
# Run test after scene loads
|
||||||
|
mcrfpy.setTimer("test", run_test, 100)
|
||||||
|
|
||||||
|
print("Running Dijkstra tests...")
|
Loading…
Reference in New Issue