163 lines
5.6 KiB
Python
163 lines
5.6 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Generate maze.txt geometry by tracing the outline of a binary maze image.
|
|
# The script expects the maze PNG to be white-on-black and writes walls/floor
|
|
# segments in the format ghostland.cpp consumes at runtime.
|
|
|
|
import matplotlib.image as mpimage
|
|
|
|
# TODO: parameterize this path via CLI flags or config.
|
|
img = mpimage.imread("/home/cowley/Pictures/maze.png")
|
|
rows = len(img)
|
|
cols = len(img[0])
|
|
|
|
|
|
def isfilled(val):
|
|
# Pixels brighter than ~20% are treated as walls.
|
|
return all(x > 0.2 for x in val)
|
|
|
|
|
|
def getval(img, x, y):
|
|
# Bounds-check helpers so we can safely probe virtual border pixels.
|
|
if y < 0 or y >= len(img):
|
|
return False
|
|
if x < 0 or x >= len(img[y]):
|
|
return False
|
|
return isfilled(img[y][x])
|
|
|
|
|
|
segments = []
|
|
# Sweep the image in four directions to emit axis-aligned wall segments.
|
|
# Horizontal sweep detecting transitions from empty -> filled above (top edges).
|
|
for iy in range(-1, rows + 1):
|
|
seg_start = None
|
|
for ix in range(-1, cols + 1):
|
|
if getval(img, ix, iy) and not getval(img, ix, iy - 1):
|
|
if seg_start is None:
|
|
seg_start = (ix - 0.5, iy - 0.5)
|
|
else:
|
|
if seg_start is not None:
|
|
segments.append((seg_start, (ix - 0.5, iy - 0.5), (0.0, 0.0, 1.0)))
|
|
seg_start = None
|
|
if seg_start is not None:
|
|
segments.append((seg_start, (cols - 0.5, iy - 0.5), (0.0, 0.0, 1.0)))
|
|
|
|
|
|
# Horizontal sweep detecting transitions from empty -> filled below (bottom edges).
|
|
for iy in range(-1, rows + 1):
|
|
seg_start = None
|
|
for ix in range(-1, cols + 1):
|
|
if getval(img, ix, iy) and not getval(img, ix, iy + 1):
|
|
if seg_start is None:
|
|
seg_start = (ix - 0.5, iy + 0.5)
|
|
else:
|
|
if seg_start is not None:
|
|
segments.append((seg_start, (ix - 0.5, iy + 0.5), (0.0, 0.0, -1.0)))
|
|
seg_start = None
|
|
if seg_start is not None:
|
|
segments.append((seg_start, (cols - 0.5, iy + 0.5), (0.0, 0.0, -1.0)))
|
|
|
|
# Vertical sweep detecting transitions from empty -> filled on the left.
|
|
for ix in range(-1, cols + 1):
|
|
seg_start = None
|
|
for iy in range(-1, rows + 1):
|
|
if getval(img, ix, iy) and not getval(img, ix - 1, iy):
|
|
if seg_start is None:
|
|
seg_start = (ix - 0.5, iy - 0.5)
|
|
else:
|
|
if seg_start is not None:
|
|
segments.append((seg_start, (ix - 0.5, iy - 0.5), (-1.0, 0.0, 0.0)))
|
|
seg_start = None
|
|
if seg_start is not None:
|
|
segments.append((seg_start, (ix - 0.5, rows - 0.5), (-1.0, 0.0, 0.0)))
|
|
|
|
# Vertical sweep detecting transitions from empty -> filled on the right.
|
|
for ix in range(-1, cols + 1):
|
|
seg_start = None
|
|
for iy in range(-1, rows + 1):
|
|
if getval(img, ix, iy) and not getval(img, ix + 1, iy):
|
|
if seg_start is None:
|
|
seg_start = (ix + 0.5, iy - 0.5)
|
|
else:
|
|
if seg_start is not None:
|
|
segments.append((seg_start, (ix + 0.5, iy - 0.5), (1.0, 0.0, 0.0)))
|
|
seg_start = None
|
|
if seg_start is not None:
|
|
segments.append((seg_start, (ix + 0.5, rows - 0.5), (1.0, 0.0, 0.0)))
|
|
|
|
print(segments[:10]) # Quick sanity check; remove or redirect for batch runs.
|
|
|
|
# Serialize into the legacy maze.txt format ghostland.cpp expects.
|
|
res = {}
|
|
res["players"] = []
|
|
# Hard-coded spawn until a better level editor exists.
|
|
player = {
|
|
"y": 2.5,
|
|
"x": 845.0,
|
|
"z": -5.0,
|
|
"yaw": 270.0,
|
|
}
|
|
res["players"].append(player)
|
|
res["surfaces"] = []
|
|
for segment in segments:
|
|
# Build quads with outward normals stored as the fifth element.
|
|
p1 = {"x": segment[0][0], "z": -segment[0][1], "y": 0.0}
|
|
p2 = {
|
|
"x": segment[1][0],
|
|
"z": -segment[1][1],
|
|
"y": 0.0,
|
|
}
|
|
p3 = {
|
|
"x": segment[1][0],
|
|
"z": -segment[1][1],
|
|
"y": 5.0,
|
|
}
|
|
p4 = {
|
|
"x": segment[0][0],
|
|
"z": -segment[0][1],
|
|
"y": 5.0,
|
|
}
|
|
surface = [p1, p2, p3, p4, segment[2]]
|
|
res["surfaces"].append(surface)
|
|
|
|
xmin = 0.0
|
|
xmax = 0.0
|
|
zmin = 0.0
|
|
zmax = 0.0
|
|
|
|
# Write the flattened surfaces followed by a floor bounding box so the game can detect footsteps.
|
|
with open("maze.txt", "w") as f:
|
|
f.write(f"{player['x']} {player['y']} {player['z']} {player['yaw']}\n")
|
|
f.write(f"{len(res['surfaces'])}\n")
|
|
for segment in res["surfaces"]:
|
|
# Track extrema to write a single floor plane at the end.
|
|
for point in segment[:4]:
|
|
if point["x"] < xmin:
|
|
xmin = point["x"]
|
|
if point["x"] > xmax:
|
|
xmax = point["x"]
|
|
if point["z"] < zmin:
|
|
zmin = point["z"]
|
|
if point["z"] > zmax:
|
|
zmax = point["x"]
|
|
point = segment[0]
|
|
norm = f"{segment[4][0]} {segment[4][1]} {segment[4][2]}"
|
|
f.write(f"{point['x']} {point['y']} {point['z']} {norm}\n")
|
|
point = segment[1]
|
|
f.write(f"{point['x']} {point['y']} {point['z']} {norm}\n")
|
|
point = segment[2]
|
|
f.write(f"{point['x']} {point['y']} {point['z']} {norm}\n")
|
|
point = segment[3]
|
|
f.write(f"{point['x']} {point['y']} {point['z']} {norm}\n")
|
|
point = segment[2]
|
|
f.write(f"{point['x']} {point['y']} {point['z']} {norm}\n")
|
|
point = segment[0]
|
|
f.write(f"{point['x']} {point['y']} {point['z']} {norm}\n")
|
|
f.write("\n")
|
|
f.write(f"{xmin} 0.0 {zmin} 0.0 1.0 0.0\n")
|
|
f.write(f"{xmax} 0.0 {zmin} 0.0 1.0 0.0\n")
|
|
f.write(f"{xmin} 0.0 {zmax} 0.0 1.0 0.0\n")
|
|
f.write(f"{xmax} 0.0 {zmax} 0.0 1.0 0.0\n")
|
|
f.write(f"{xmin} 0.0 {zmax} 0.0 1.0 0.0\n")
|
|
f.write(f"{xmax} 0.0 {zmin} 0.0 1.0 0.0\n")
|