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()