Spaces:
Sleeping
Sleeping
| 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() | |