import math import logging from PIL import Image, ImageDraw def draw_maze(maze, path, cell=20, save_path=None, show_doors=False, color_dict={'wall':'black', 'floor':'white', 'start':'green', 'door':'#B0B0B0', 'path':'red'}): """ Draw the maze and an optional path using PIL. Args: maze (list[list[int]]): 2D grid with 1 for walls and 0 for open cells. path (list[tuple[int,int]]): sequence of (x, y) coordinates forming a path through the maze. cell (int): pixel size of one maze cell when rendering. Default is 20. save_path (str|None): if given, save the rendered image to this file path; if None, show the image instead. show_doors (bool): if True, draw thin grey outlines around each cell to indicate doors. color_dict (dict): optional dict to specify colors for 'wall', 'floor', 'start', 'door', and 'path'. """ h,w = len(maze),len(maze[0]) img = Image.new('RGB', (w*cell,h*cell), color_dict.get('floor', 'white')) draw = ImageDraw.Draw(img) # fill start cell (0,0) with green floor draw.rectangle([0, 0, cell, cell], fill=color_dict.get('start', 'green')) # Optional thin grey outlines for each cell (like hex show_doors) if show_doors: for y in range(h): for x in range(w): draw.rectangle( [x * cell, y * cell, (x + 1) * cell, (y + 1) * cell], outline=color_dict.get('door', '#B0B0B0'), width=1 ) # Draw solid walls for y in range(h): for x in range(w): if maze[y][x]: draw.rectangle([x*cell,y*cell,(x+1)*cell,(y+1)*cell], fill=color_dict.get('wall', 'black')) # Draw 2px exterior border, leaving an opening at the exit cell (w-1, h-1) img_w, img_h = w * cell, h * cell border_width = 4 draw.rectangle([0, 0, img_w - 1, img_h - 1], outline=color_dict.get('wall', 'black'), width=border_width) ex0, ey0 = (w - 1) * cell, (h - 1) * cell draw.line([(img_w - 1, ey0), (img_w - 1, ey0 + cell)], fill=color_dict.get('door', '#B0B0B0'), width=border_width + 2) draw.line([(ex0, img_h - 1), (ex0 + cell, img_h - 1)], fill=color_dict.get('door', '#B0B0B0'), width=border_width + 2) if path: for i in range(len(path)-1): x1,y1 = path[i]; x2,y2 = path[i+1] draw.line([(x1*cell+cell//2,y1*cell+cell//2),(x2*cell+cell//2,y2*cell+cell//2)], fill=color_dict.get('path', 'red'), width=3) if save_path: try: img.save(save_path) logging.info("Saved rectangular maze image to %s", save_path) except Exception: logging.exception("Failed to save image to %s", save_path) return img def draw_hex_maze(maze, path, cell_size=45, rotation_degrees=0, save_path=None, show_doors=False, color_dict={'wall':'black', 'floor':'white', 'start':'green', 'door':'#B0B0B0', 'path':'red'}): """ Draw a hex maze (walls only where no passage) and optional path using PIL. Args: maze (dict): mapping (q, r) -> list of (dq, dr) neighbor offsets. path (list[tuple[int,int]]): sequence of axial coordinates to draw as red line. cell_size (int): approximate pixel size of a hex cell. save_path (str|None): if given, save image to file instead of showing. rotation_degrees (float): rotation of hexagons in degrees (default 0 = flat-topped). save_path (str|None): if provided, save the rendered image to this path; if None, show the image. show_doors (bool): if True, draw thin grey outlines around each hex to show doors. color_dict (dict): optional dict to specify colors for 'wall', 'floor', 'start', and 'door'. """ rot = math.radians(rotation_degrees) if not maze: return r_max = max((max(abs(q),abs(r)) for q,r in maze), default=5) w = int(3*(r_max+2)*cell_size) h = int(math.sqrt(3)*2*(r_max+2)*cell_size) img = Image.new('RGB', (w,h), color_dict.get('floor', 'white')) d = ImageDraw.Draw(img) dirs = [(1,0),(0,1),(-1,1),(-1,0),(0,-1),(1,-1)] if show_doors: for q,r in maze: # light grey outlines cx = cell_size*1.5*q + w//2 cy = cell_size*math.sqrt(3)*(r + q/2) + h//2 pts = [(cx + cell_size*math.cos(rot + i*math.pi/3), cy + cell_size*math.sin(rot + i*math.pi/3)) for i in range(6)] d.polygon(pts, outline=color_dict.get('door', '#B0B0B0'), width=2) # fill start hex (0,0) with green floor so the starting cell is visibly marked if (0, 0) in maze: cx = w//2 cy = h//2 start_pts = [(cx + cell_size*math.cos(rot + i*math.pi/3), cy + cell_size*math.sin(rot + i*math.pi/3)) for i in range(6)] d.polygon(start_pts, fill=color_dict.get('start', 'green')) for q,r in maze: # black blocked walls cx = cell_size*1.5*q + w//2 cy = cell_size*math.sqrt(3)*(r + q/2) + h//2 for i,(dq,dr) in enumerate(dirs): if (dq,dr) not in maze.get((q,r),[]): a = rot + i*math.pi/3 x1 = cx + cell_size*math.cos(a) y1 = cy + cell_size*math.sin(a) x2 = cx + cell_size*math.cos(a+math.pi/3) y2 = cy + cell_size*math.sin(a+math.pi/3) d.line([(x1,y1),(x2,y2)], fill=color_dict.get('wall', 'black'), width=6) # force clear exit to outside on end cell (outer side with no neighbor) end = max(maze, key=lambda p: abs(p[0])+abs(p[1])+abs(p[0]+p[1])) cx = cell_size*1.5*end[0] + w//2 cy = cell_size*math.sqrt(3)*(end[1] + end[0]/2) + h//2 for i,(dq,dr) in enumerate(dirs): neigh = (end[0]+dq, end[1]+dr) if neigh not in maze: # outer side a = rot + i*math.pi/3 x1 = cx + cell_size*math.cos(a) y1 = cy + cell_size*math.sin(a) x2 = cx + cell_size*math.cos(a+math.pi/3) y2 = cy + cell_size*math.sin(a+math.pi/3) d.line([(x1,y1),(x2,y2)], fill=color_dict.get('floor', 'white'), width=26) break if path: pts = [(cell_size*1.5*q + w//2, cell_size*math.sqrt(3)*(r + q/2) + h//2) for q,r in path] d.line(pts, fill=color_dict.get('path', 'red'), width=7) if save_path: try: img.save(save_path) logging.info("Saved hex maze image to %s", save_path) except Exception: logging.exception("Failed to save image to %s", save_path) return img def draw_tri_maze(maze, path, cell=40, save_path=None, show_doors=False, color_dict={'wall':'black', 'floor':'white', 'start':'green', 'door':'#B0B0B0', 'path':'red'}): """ Draw a triangular-grid maze using PIL. Maze format expected: maze (dict): mapping (x, y) -> list of (dx, dy) neighbor offsets that are open. Each cell is an equilateral triangle. Coordinates are grid indices (x:col, y:row). Args: maze (dict): triangle adjacency mapping. path (list[tuple[int,int]]): list of (x,y) cells forming a path. cell (int): side length of each triangle in pixels. save_path (str|None): save to file if provided, otherwise show. show_doors (bool): if True draw thin grey outlines around each triangle. color_dict (dict): optional dict to specify colors for 'wall', 'floor', 'start', 'door', and 'path'. """ if not maze: return # determine bounds xs = [p[0] for p in maze] ys = [p[1] for p in maze] min_x, max_x = min(xs), max(xs) min_y, max_y = min(ys), max(ys) w = max_x - min_x + 1 h = max_y - min_y + 1 side = cell h_tri = side * math.sqrt(3) / 2.0 offset_x = side # margin offset_y = side img_w = int(offset_x*2 + (w-1) * (side/2.0) + side + 0.5) img_h = int(offset_y*2 + h * h_tri + 0.5) img = Image.new('RGB', (img_w, img_h), color_dict.get('floor', 'white')) d = ImageDraw.Draw(img) def cell_vertices(x,y): cx = offset_x + (x - min_x) * (side/2.0) cy = offset_y + (y - min_y) * h_tri up = ((x + y) % 2 == 0) if up: v0 = (cx, cy) v1 = (cx - side/2.0, cy + h_tri) v2 = (cx + side/2.0, cy + h_tri) else: v0 = (cx, cy + h_tri) v1 = (cx - side/2.0, cy) v2 = (cx + side/2.0, cy) return up, v0, v1, v2 # fill start triangle (0,0) with green floor so the starting cell is visibly marked if (0, 0) in maze: _, v0_0, v1_0, v2_0 = cell_vertices(0, 0) d.polygon([v0_0, v1_0, v2_0], fill=color_dict.get('start', 'green')) # optional thin grey outlines for each triangle if show_doors: for (x,y) in maze: up, v0, v1, v2 = cell_vertices(x,y) pts = [v0, v1, v2] d.polygon(pts, outline=color_dict.get('door', '#B0B0B0'), width=1) # draw black walls where no passage for (x,y) in maze: up, v0, v1, v2 = cell_vertices(x,y) # map direction offsets to edge vertex pairs if up: edge_map = { (-1,0): (v0, v1), # left (1,0): (v0, v2), # right (0,1): (v1, v2), # down/base } else: edge_map = { (-1,0): (v1, v0), # left (1,0): (v2, v0), # right (0,-1): (v1, v2), # up/base } for off,verts in edge_map.items(): if off not in maze.get((x,y), []): d.line([verts[0], verts[1]], fill=color_dict.get('wall', 'black'), width=6) # force clear exit on end cell (outer side with no neighbor) # choose end as max by x+y end = max(maze, key=lambda p: p[0] + p[1]) ex, ey = end up, v0, v1, v2 = cell_vertices(ex, ey) if up: dir_order = [(-1,0),(1,0),(0,1)] edge_order = [(v0,v1),(v0,v2),(v1,v2)] else: dir_order = [(-1,0),(1,0),(0,-1)] edge_order = [(v1,v0),(v2,v0),(v1,v2)] for off,edge in zip(dir_order, edge_order): neigh = (ex + off[0], ey + off[1]) if neigh not in maze: d.line([edge[0], edge[1]], fill=color_dict.get('floor', 'white'), width=26) break # draw path if provided (connect centroids) if path: pts = [] for x,y in path: up, v0, v1, v2 = cell_vertices(x,y) # centroid average of vertices cx = (v0[0] + v1[0] + v2[0]) / 3.0 cy = (v0[1] + v1[1] + v2[1]) / 3.0 pts.append((cx, cy)) d.line(pts, fill=color_dict.get('path', 'red'), width=7) if save_path: try: img.save(save_path) logging.info("Saved triangular maze image to %s", save_path) except Exception: logging.exception("Failed to save tri image to %s", save_path) return img