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