Surn's picture
Add customizable colors for maze drawing in CLI/API
7bfe358
from .grid import generate_maze, generate_maze_iterative, solve_maze
from .hex import generate_hex_maze, generate_hex_maze_iterative, solve_hex_maze
from .draw import draw_maze, draw_hex_maze, draw_tri_maze
from .tri import generate_tri_maze, generate_tri_maze_iterative, solve_tri_maze
import argparse
import sys
import logging
import os
import time
from pathlib import Path
def run_maze(shape='rect', size=(30,20), cell=20, use_iterative=False, draw=True, output=None, show_solution=True, show_doors=False, seed=0, color_dict={'wall':'black', 'floor':'white', 'start':'green', 'door':'#B0B0B0', 'path':'red'}):
"""
Control function to generate, solve, and optionally draw a maze.
Args:
shape (str): 'rect' for rectangular grid mazes or 'hex' for hexagonal mazes.
size (tuple|int): For 'rect', a (width, height) tuple. For 'hex', an integer radius.
cell (int): Cell size in pixels for drawing (rect) or approximate hex size (hex).
use_iterative (bool): If True and shape is 'rect', use the iterative DFS generator.
draw (bool): If True, render the maze and path with PIL; otherwise only return data.
output (str|None): If provided, save the rendered image to this path; if None, show the image.
show_solution (bool): If True, overlay the solution path on the drawn maze.
show_doors (bool): If True, draw thin grey outlines around each cell to show doors.
seed (int): Random seed for maze generation (0 for random).
color_dict (dict): Optional dict to specify colors for 'wall', 'floor', 'start', 'door', and 'path' in drawing.
Returns:
tuple: (maze, path) where maze is either a 2D list (rect) or dict (hex), and
path is a list of coordinates in the corresponding coordinate system.
"""
logging.debug('run_maze called: shape=%s size=%s cell=%s iterative=%s draw=%s output=%s show_solution=%s',
shape, size, cell, use_iterative, draw, output, show_solution)
shape_key = (shape or 'rect').lower()
if shape_key in ('rect', 'rectangle', 'grid'):
# Normalize size into (w, h)
if isinstance(size, int):
w, h = size, size
else:
try:
w, h = int(size[0]), int(size[1])
except Exception:
raise ValueError("For rect shape, size must be an int or a (w,h) tuple")
if use_iterative:
maze = generate_maze_iterative(w, h, seed=seed)
else:
maze = generate_maze(w, h, seed=seed)
path = solve_maze(maze)
if draw:
draw_maze(maze, path if show_solution else [], cell=cell, save_path=output, show_doors=show_doors, color_dict=color_dict)
return maze, path
if shape_key in ('tri', 'triangle', 'triangular'):
# triangular grids are currently implemented as rectangular compatibility wrappers
if isinstance(size, int):
w, h = size, size
else:
try:
w, h = int(size[0]), int(size[1])
except Exception:
raise ValueError("For tri shape, size must be an int or a (w,h) tuple")
if use_iterative:
maze = generate_tri_maze_iterative(w, h, seed=seed)
else:
maze = generate_tri_maze(w, h, seed=seed)
path = solve_tri_maze(maze)
if draw:
draw_tri_maze(maze, path if show_solution else [], cell=cell, save_path=output, show_doors=show_doors, color_dict=color_dict)
return maze, path
if shape_key in ('hex', 'hexagon', 'hexagonal'):
try:
radius = int(size)
except Exception:
raise ValueError("For hex shape, size must be an integer radius")
# allow choosing iterative implementation explicitly
if use_iterative:
maze = generate_hex_maze_iterative(radius, seed=seed)
else:
maze = generate_hex_maze(radius, seed=seed)
path = solve_hex_maze(maze)
if draw:
draw_hex_maze(maze, path if show_solution else [], cell_size=cell, save_path=output, show_doors=show_doors, color_dict=color_dict)
return maze, path
raise ValueError(f"Unknown shape: {shape}")
def _parse_size_arg(shape, size_str):
"""Parse the --size CLI argument into the appropriate value for run_maze.
For rectangular mazes accept either an integer (square) or WIDTHxHEIGHT.
For hex mazes accept a single integer radius.
"""
if shape in ('rect', 'rectangle', 'grid', 'tri', 'triangle', 'triangular'):
if isinstance(size_str, (list, tuple)):
return int(size_str[0]), int(size_str[1])
s = str(size_str)
if 'x' in s.lower():
parts = s.lower().split('x')
if len(parts) != 2:
raise ValueError("Invalid rect size format, expected WIDTHxHEIGHT")
return int(parts[0]), int(parts[1])
return int(s)
# hex
return int(size_str)
def main(argv=None):
parser = argparse.ArgumentParser(prog='run_maze', description='Generate/solve/draw mazes')
parser.add_argument('-s', '--shape', choices=['rect', 'hex', 'tri'], default='rect', help='Maze shape')
parser.add_argument('-z', '--size', default='30x20', help='Size: for rect WIDTHxHEIGHT or N; for hex radius')
parser.add_argument('-c', '--cell', type=int, default=20, help='Cell size in pixels')
parser.add_argument('-i', '--iterative', action='store_true', help='Use iterative generator')
parser.add_argument('--no-draw', dest='draw', action='store_false', help="Don't render the maze")
group = parser.add_mutually_exclusive_group()
group.add_argument('-o', '--output', default=None, help='Path to save rendered image (if omitted, image will be shown)')
group.add_argument('-O', '--out-dir', default=None, help='Directory to save rendered image(s); filename auto-generated')
parser.add_argument('-v', '--verbose', action='count', default=0, help='Increase verbosity (use -v or -vv)')
parser.add_argument('--show-solution', action='store_true', default=False,
help='Print path length and endpoints after solving')
parser.add_argument('--seed', type=int, default=0, help='Random seed (0 for random)')
parser.add_argument('--show-doors', action='store_true', default=False, help='Draw thin grey outlines for doors')
parser.add_argument('--color-wall', default='black', help='Color for walls in drawing')
parser.add_argument('--color-floor', default='white', help='Color for floors in drawing')
parser.add_argument('--color-start', default='green', help='Color for start cell in drawing')
parser.add_argument('--color-door', default='#B0B0B0', help='Color for doors in drawing')
parser.add_argument('--color-path', default='red', help='Color for solution path in drawing')
args = parser.parse_args(argv)
# configure logging according to verbosity
if args.verbose >= 2:
level = logging.DEBUG
elif args.verbose == 1:
level = logging.INFO
else:
level = logging.WARNING
logging.basicConfig(level=level, format='%(levelname)s: %(message)s')
try:
size = _parse_size_arg(args.shape, args.size)
except Exception as e:
parser.error(str(e))
# Ensure rectangular mazes have odd dimensions to allow a valid path
if args.shape in ('rect', 'rectangle', 'grid', 'tri', 'triangle', 'triangular'):
if isinstance(size, (list, tuple)):
w, h = int(size[0]), int(size[1])
changed = False
if w % 2 == 0:
w += 1
changed = True
if h % 2 == 0:
h += 1
changed = True
if changed:
logging.info("Adjusted rectangular size to odd dimensions: %dx%d", w, h)
size = (w, h)
else:
s = int(size)
if s % 2 == 0:
s += 1
logging.info("Adjusted rectangular size to odd dimension: %d", s)
size = s
# determine output path: explicit output > out_dir auto-generated > None
output_path = None
if args.output:
output_path = args.output
elif args.out_dir:
outdir = Path(args.out_dir)
try:
outdir.mkdir(parents=True, exist_ok=True)
except Exception as e:
parser.error(f"Failed to create out-dir '{args.out_dir}': {e}")
ts = time.strftime('%Y%m%d_%H%M%S')
shape_key = args.shape.lower()
if shape_key in ('rect', 'rectangle', 'grid', 'tri', 'triangle', 'triangular'):
if isinstance(size, tuple):
w, h = size
if shape_key in ('tri', 'triangle', 'triangular'):
fname = f"maze_tri_{w}x{h}_{ts}.png"
else:
fname = f"maze_rect_{w}x{h}_{ts}.png"
else:
s = int(size)
if shape_key in ('tri', 'triangle', 'triangular'):
fname = f"maze_tri_{s}x{s}_{ts}.png"
else:
fname = f"maze_rect_{s}x{s}_{ts}.png"
else:
# hex
radius = int(size)
fname = f"maze_hex_r{radius}_{ts}.png"
# append seed to filename when a non-zero seed is provided
if args.seed:
name, ext = os.path.splitext(fname)
fname = f"{name}_s{args.seed}{ext}"
output_path = str(outdir / fname)
logging.info("Auto-generated output path: %s", output_path)
color_dict = {
'wall': args.color_wall,
'floor': args.color_floor,
'start': args.color_start,
'door': args.color_door,
'path': args.color_path
}
maze, path = run_maze(shape=args.shape, size=size, cell=args.cell, use_iterative=args.iterative, draw=args.draw, output=output_path, show_solution=args.show_solution, seed=args.seed, color_dict=color_dict)
if args.show_solution:
if not path:
print("No solution path found.")
else:
try:
start = path[0]
end = path[-1]
print(f"Path length: {len(path)}")
print(f"Start: {start}")
print(f"End: {end}")
except Exception:
# Fallback: print minimal info
print(f"Path length: {len(path)}; start/end unavailable")
return maze, path
if __name__ == '__main__':
main()