├── .gitignore ├── README.md ├── mortoray_path_finding ├── __init__.py ├── draw.py ├── maze.py └── tutorial_1.py ├── tutorial_1_1.py ├── tutorial_1_2.py ├── tutorial_1_3.py └── tutorial_1_interactive.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Path Finding with Edaqa Mortoray 2 | 3 | This repository contains the source code that is required for the [Path Finding]() classes. 4 | 5 | 6 | ## Setup 7 | 8 | Ensure you are using a recent version of Python, 3.6.1 or higher. 9 | 10 | Install PyGame, which is used to create graphics. 11 | 12 | `python3 -m pip install -U pygame` 13 | 14 | -------------------------------------------------------------------------------- /mortoray_path_finding/__init__.py: -------------------------------------------------------------------------------- 1 | from . import tutorial_1 2 | from . import draw, maze 3 | -------------------------------------------------------------------------------- /mortoray_path_finding/draw.py: -------------------------------------------------------------------------------- 1 | import pygame, math, random, types, copy 2 | from enum import Enum 3 | from . import maze 4 | 5 | pygame.init() 6 | pygame.display.set_caption("Path Finding Demo") 7 | cell_font = pygame.font.SysFont(pygame.font.get_default_font(), 25) 8 | 9 | def trans_rect( r, off ): 10 | return [r[0] + off[0], r[1] + off[1], r[2], r[3]] 11 | 12 | def main_loop(ui): 13 | screen = pygame.display.set_mode((1000,800)) 14 | 15 | clock = pygame.time.Clock() 16 | clock.tick() 17 | i = 0 18 | while True: 19 | event = pygame.event.poll() 20 | if event.type == pygame.QUIT: 21 | break 22 | elif event.type == pygame.KEYDOWN: 23 | if event.key == pygame.K_ESCAPE: 24 | break 25 | if event.key == pygame.K_RIGHT: 26 | ui.step(1) 27 | if event.key == pygame.K_LEFT: 28 | ui.step(-1) 29 | if event.key == pygame.K_r: 30 | ui.reset() 31 | 32 | ui.draw(screen) 33 | 34 | pygame.display.update() 35 | clock.tick(60) 36 | 37 | 38 | 39 | pygame.quit() 40 | 41 | 42 | class Finder: 43 | def __init__(self): 44 | self.path = None 45 | self.board = None 46 | 47 | def set_board(self, board): 48 | self.board = board 49 | 50 | def set_path(self, path): 51 | self.path = path 52 | 53 | def run(self): 54 | main_loop(self) 55 | 56 | def draw(self, surface): 57 | if self.board == None: 58 | return 59 | 60 | draw_board(surface, surface.get_rect(), self.board) 61 | if self.path != None: 62 | draw_path(surface, surface.get_rect(), self.board, self.path) 63 | 64 | 65 | def step(self, steps): 66 | pass 67 | 68 | def reset(self): 69 | pass 70 | 71 | 72 | class BoardMetrics: 73 | def __init__(self, area, board): 74 | self.area = area 75 | self.spacing = 3 76 | self.left = area[0] + self.spacing 77 | self.top = area[1] + self.spacing 78 | self.width = area[2] - area[0] - 2 * self.spacing 79 | self.height = area[3] - area[1] - 2 * self.spacing 80 | self.num_y = board.get_size()[1] 81 | self.num_x = board.get_size()[0] 82 | self.cy = self.height / self.num_y 83 | self.cx = self.width / self.num_x 84 | 85 | def cell_rect(self, pos): 86 | return [self.left + pos[0] * self.cx, self.top + pos[1] * self.cy, self.cx - self.spacing, self.cy - self.spacing] 87 | 88 | def cell_center(self, pos): 89 | rct = self.cell_rect(pos) 90 | return [rct[0]+rct[2]//2, rct[1] + rct[3]//2] 91 | 92 | def draw_board(surface, area, board): 93 | pygame.draw.rect(surface, (0,0,0), area) 94 | metrics = BoardMetrics(area, board) 95 | 96 | colors = { 97 | maze.CellType.Empty: (40,40,40), 98 | maze.CellType.Block: (128,100,0), 99 | } 100 | marks = { 101 | maze.CellMark.Start: (110,110,0), 102 | maze.CellMark.End: (0,110,0), 103 | } 104 | for y in range(0,metrics.num_y): 105 | for x in range(0,metrics.num_x): 106 | cell = board.at([x,y]) 107 | clr = colors.get(cell.type, (100,100,0)) 108 | cell_rect = metrics.cell_rect( [x, y] ) 109 | 110 | pygame.draw.rect(surface, clr, cell_rect) 111 | 112 | if cell.count != math.inf: 113 | number = cell_font.render( "{}".format(cell.count), True, (255,255,255)) 114 | surface.blit(number, trans_rect(number.get_rect(), 115 | [cell_rect[0] + (cell_rect[2] - number.get_rect()[2])//2, 116 | cell_rect[1] + (cell_rect[3] -number.get_rect()[3])//2] 117 | )) 118 | 119 | mark = marks.get(cell.mark, None) 120 | if mark != None: 121 | pygame.draw.rect(surface, mark, cell_rect, metrics.spacing) 122 | 123 | 124 | def draw_path(surface, area, board, path): 125 | metrics = BoardMetrics(area, board) 126 | for i in range(0,len(path)-1): 127 | ctr_a = metrics.cell_center( path[i].pos ) 128 | ctr_b = metrics.cell_center( path[i+1].pos ) 129 | pygame.draw.line(surface, (120,220,0), ctr_a, ctr_b, metrics.spacing ) 130 | 131 | 132 | -------------------------------------------------------------------------------- /mortoray_path_finding/maze.py: -------------------------------------------------------------------------------- 1 | import math, random, types, copy 2 | from enum import Enum 3 | 4 | class CellType(Enum): 5 | Empty = 1 6 | Block = 2 7 | 8 | class CellMark(Enum): 9 | No = 0 10 | Start = 1 11 | End = 2 12 | 13 | class Cell: 14 | def __init__(self, type = CellType.Empty, pos = None): 15 | self.type = type 16 | self.count = 0 17 | self.mark = CellMark.No 18 | self.path_from = None 19 | self.pos = pos 20 | 21 | 22 | class CellGrid: 23 | def __init__(self, board): 24 | self.board = board 25 | 26 | def get_size(self): 27 | return [len(self.board), len(self.board[0])] 28 | 29 | def at(self, pos): 30 | return self.board[pos[0]][pos[1]] 31 | 32 | def clone(self): 33 | return CellGrid( copy.deepcopy(self.board) ) 34 | 35 | def clear_count(self, count): 36 | for o in self.board: 37 | for i in o: 38 | i.count = count 39 | i.path_from = None 40 | 41 | def is_valid_point(self, pos): 42 | sz = self.get_size() 43 | return pos[0] >= 0 and pos[1] >= 0 and pos[0] < sz[0] and pos[1] < sz[1] 44 | 45 | 46 | def create_empty_maze( x, y ): 47 | return types.SimpleNamespace( 48 | board = CellGrid( [[Cell(type = CellType.Empty, pos=[ix,iy]) for iy in range(y)] for ix in range(x)] ), 49 | start = [random.randrange(0,x), random.randrange(0,y)], 50 | end = [random.randrange(0,x), random.randrange(0,y)]) 51 | 52 | def create_wall_maze( x, y ): 53 | board = [[Cell(type = CellType.Empty, pos=[ix,iy]) for iy in range(y)] for ix in range(x)] 54 | for i in range(0,x): 55 | board[i][int(y//2)].type = CellType.Block 56 | for i in range(0,y): 57 | board[int(x//2)][i].type = CellType.Block 58 | 59 | board[random.randint(0,x//2-1)][int(y//2)].type = CellType.Empty 60 | board[random.randint(x//2+1,x-1)][int(y//2)].type = CellType.Empty 61 | board[int(x//2)][random.randint(0,y//2-1)].type = CellType.Empty 62 | board[int(x//2)][random.randint(y//2+1,y-1)].type = CellType.Empty 63 | 64 | return types.SimpleNamespace( board = CellGrid(board), 65 | start = [random.randrange(0,x//2), random.randrange(y//2+1,y)], 66 | end = [random.randrange(x//2+1,x), random.randrange(0,y//2)] ) 67 | 68 | 69 | def add_point(a,b): 70 | return [a[0] + b[0], a[1] + b[1]] 71 | -------------------------------------------------------------------------------- /mortoray_path_finding/tutorial_1.py: -------------------------------------------------------------------------------- 1 | from . import maze 2 | import math 3 | 4 | def fill_shortest_path(board, start, end, max_distance = math.inf): 5 | """ Creates a duplicate of the board and fills the `Cell.count` field with the distance from the start to that cell. """ 6 | nboard = board.clone() 7 | nboard.clear_count(math.inf) 8 | 9 | # mark the start and end for the UI 10 | nboard.at( start ).mark = maze.CellMark.Start 11 | nboard.at( end ).mark = maze.CellMark.End 12 | 13 | # we start here, thus a distance of 0 14 | open_list = [ start ] 15 | nboard.at( start ).count = 0 16 | 17 | # (x,y) offsets from current cell 18 | neighbours = [ [-1,0], [1,0], [0,-1], [0,1] ] 19 | while open_list: 20 | cur_pos = open_list.pop(0) 21 | cur_cell = nboard.at( cur_pos ) 22 | 23 | for neighbour in neighbours: 24 | ncell_pos = maze.add_point(cur_pos, neighbour) 25 | if not nboard.is_valid_point(ncell_pos): 26 | continue 27 | 28 | cell = nboard.at( ncell_pos ) 29 | 30 | if cell.type != maze.CellType.Empty: 31 | continue 32 | 33 | dist = cur_cell.count + 1 34 | if dist > max_distance: 35 | continue 36 | 37 | if cell.count > dist: 38 | cell.count = dist 39 | cell.path_from = cur_cell 40 | open_list.append(ncell_pos) 41 | 42 | return nboard 43 | 44 | def backtrack_to_start(board, end): 45 | """ Returns the path to the end, assuming the board has been filled in via fill_shortest_path """ 46 | cell = board.at( end ) 47 | path = [] 48 | while cell != None: 49 | path.append(cell) 50 | cell = cell.path_from 51 | 52 | return path 53 | 54 | -------------------------------------------------------------------------------- /tutorial_1_1.py: -------------------------------------------------------------------------------- 1 | import mortoray_path_finding as mpf 2 | 3 | maze = mpf.maze.create_wall_maze( 20, 12 ) 4 | 5 | finder = mpf.draw.Finder() 6 | finder.set_board(maze.board) 7 | finder.run() 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial_1_2.py: -------------------------------------------------------------------------------- 1 | import mortoray_path_finding as mpf 2 | 3 | maze = mpf.maze.create_wall_maze( 20, 12 ) 4 | filled = mpf.tutorial_1.fill_shortest_path(maze.board, maze.start, maze.end) 5 | 6 | finder = mpf.draw.Finder() 7 | finder.set_board(filled) 8 | finder.run() 9 | 10 | -------------------------------------------------------------------------------- /tutorial_1_3.py: -------------------------------------------------------------------------------- 1 | import mortoray_path_finding as mpf 2 | 3 | maze = mpf.maze.create_wall_maze( 20, 12 ) 4 | filled = mpf.tutorial_1.fill_shortest_path(maze.board, maze.start, maze.end) 5 | path = mpf.tutorial_1.backtrack_to_start(filled, maze.end) 6 | 7 | finder = mpf.draw.Finder() 8 | finder.set_board(filled) 9 | finder.set_path(path) 10 | finder.run() 11 | 12 | -------------------------------------------------------------------------------- /tutorial_1_interactive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import mortoray_path_finding as mpf 3 | 4 | class MyFinder(mpf.draw.Finder): 5 | """Integrate into the simple UI """ 6 | def __init__(self): 7 | self.reset() 8 | 9 | def step(self, frames): 10 | self.max_distance = max( 0, self.max_distance + frames ) 11 | self.result = mpf.tutorial_1.fill_shortest_path(self.game.board, self.game.start, self.game.end, max_distance = self.max_distance) 12 | self.set_board(self.result) 13 | self.set_path(mpf.tutorial_1.backtrack_to_start(self.result, self.game.end)) 14 | 15 | def reset(self): 16 | self.game = mpf.maze.create_wall_maze(20,10) 17 | self.max_distance = 18 18 | self.step(0) 19 | 20 | 21 | header_text = """Keys: 22 | Left - Lower maximum distance 23 | Right - Increase maximum distance 24 | R - create a new maze 25 | Esc - Exit""" 26 | print( header_text ) 27 | 28 | finder = MyFinder() 29 | finder.run() 30 | --------------------------------------------------------------------------------