├── .gitignore ├── README.md ├── block.py ├── demo.gif ├── main.py ├── requirements.txt ├── screen.py └── search.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A-star-search 2 | Visually find the shortest path using A* algorithm. 3 | 4 | ![demo](https://raw.githubusercontent.com/LogicJake/A-star-search/master/demo.gif) 5 | 6 | ### how to run it 7 | Better to use python3 8 | #### install requirements 9 | ``` 10 | pip install -r requirements.txt 11 | ``` 12 | #### start 13 | ``` 14 | python main.py 15 | ``` 16 | ### description 17 | * Green block represents starting block 18 | * Blue blocks represent obstacle blocks 19 | * Red block represents finding block 20 | * Blocks with yellow border are in the close list 21 | * Blocks with green border are in the open list 22 | * Arrow inside block points to his parent block 23 | 24 | 25 | 26 | 27 | As you can see in the gif, you should click blocks to set starting block, obstacle blocks and finding block. If you don't know what to do, you can read the title on the screen. You can have zero or more than one obstacle blocks, just click until you think the number of obstacle blocks are enough. And then press space key to choose a finding block. 28 | 29 | 30 | After you choose all blocks, the search will start. You can press space key to step the search. 31 | -------------------------------------------------------------------------------- /block.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author: LogicJake 3 | # @Date: 2019-01-07 12:10:48 4 | # @Last Modified time: 2019-01-07 12:29:50 5 | import pygame 6 | 7 | 8 | WIDTH_NUM = 8 # number of block in width 9 | HEIGHT_NUM = 6 # number of block in height 10 | BLOCK_SIZE = 80 # size of block 11 | 12 | # some colors 13 | BLACK = (0, 0, 0) 14 | GREEN = (0, 128, 0) 15 | RED = (255, 0, 0) 16 | BLUE = (0, 0, 255) 17 | YELLOW = (255, 255, 0) 18 | PURPLE = (128, 0, 128) 19 | WHITE = (255, 255, 255) 20 | 21 | 22 | class Block(): 23 | 24 | def __init__(self, screen, x, y): 25 | self.screen = screen 26 | self.x = x 27 | self.y = y 28 | self.location_x = x * BLOCK_SIZE 29 | self.location_y = y * BLOCK_SIZE 30 | 31 | self.fill_color = BLACK 32 | self.border_color = PURPLE 33 | 34 | # f = g + h 35 | self.f = 0 36 | self.g = 0 37 | self.h = 0 38 | 39 | self.text_size = BLOCK_SIZE // 6 40 | self.type = 0 41 | self.father_direction = -1 42 | 43 | def draw_text(self, text, x, y): 44 | font = pygame.font.Font('freesansbold.ttf', self.text_size) 45 | text_surface = font.render(text, True, WHITE) 46 | text_rect = text_surface.get_rect() 47 | text_rect.center = (x, y) 48 | self.screen.blit(text_surface, text_rect) 49 | 50 | def draw_direction(self): 51 | # down right 52 | if self.father_direction == 0: 53 | pygame.draw.polygon(self.screen, WHITE, ( 54 | (self.location_x + 3 * BLOCK_SIZE // 4, 55 | self.location_y + 3 * BLOCK_SIZE // 4), 56 | (self.location_x + 5 * BLOCK_SIZE // 8, 57 | self.location_y + 3 * BLOCK_SIZE // 4), 58 | (self.location_x + 3 * BLOCK_SIZE // 4, 59 | self.location_y + 5 * BLOCK_SIZE // 8), 60 | )) 61 | pygame.draw.line(self.screen, WHITE, 62 | (self.location_x + BLOCK_SIZE // 4, 63 | self.location_y + BLOCK_SIZE // 4), 64 | (self.location_x + 3 * BLOCK_SIZE // 4, 65 | self.location_y + 3 * BLOCK_SIZE // 4)) 66 | # down 67 | elif self.father_direction == 1: 68 | pygame.draw.polygon(self.screen, WHITE, ( 69 | (self.location_x + BLOCK_SIZE // 2, 70 | self.location_y + BLOCK_SIZE - BLOCK_SIZE // 8), 71 | (self.location_x + 3 * BLOCK_SIZE // 8, 72 | self.location_y + BLOCK_SIZE - BLOCK_SIZE // 4), 73 | (self.location_x + 5 * BLOCK_SIZE // 8, 74 | self.location_y + BLOCK_SIZE - BLOCK_SIZE // 4), 75 | )) 76 | pygame.draw.line(self.screen, WHITE, 77 | (self.location_x + BLOCK_SIZE // 2, 78 | self.location_y + BLOCK_SIZE // 8), 79 | (self.location_x + BLOCK_SIZE // 2, 80 | self.location_y + 3 * BLOCK_SIZE // 4)) 81 | # down left 82 | elif self.father_direction == 2: 83 | pygame.draw.polygon(self.screen, WHITE, ( 84 | (self.location_x + BLOCK_SIZE // 4, 85 | self.location_y + 3 * BLOCK_SIZE // 4), 86 | (self.location_x + BLOCK_SIZE // 4, 87 | self.location_y + 5 * BLOCK_SIZE // 8), 88 | (self.location_x + 3 * BLOCK_SIZE // 8, 89 | self.location_y + 3 * BLOCK_SIZE // 4), 90 | )) 91 | pygame.draw.line(self.screen, WHITE, 92 | (self.location_x + 3 * BLOCK_SIZE // 4, 93 | self.location_y + BLOCK_SIZE // 4), 94 | (self.location_x + BLOCK_SIZE // 4, 95 | self.location_y + 3 * BLOCK_SIZE // 4)) 96 | # right 97 | elif self.father_direction == 3: 98 | pygame.draw.polygon(self.screen, WHITE, ( 99 | (self.location_x + 7 * BLOCK_SIZE // 8, 100 | self.location_y + BLOCK_SIZE // 2), 101 | (self.location_x + 3 * BLOCK_SIZE // 4, 102 | self.location_y + 3 * BLOCK_SIZE // 8), 103 | (self.location_x + 3 * BLOCK_SIZE // 4, 104 | self.location_y + 5 * BLOCK_SIZE // 8), 105 | )) 106 | pygame.draw.line(self.screen, WHITE, 107 | (self.location_x + BLOCK_SIZE // 8, 108 | self.location_y + BLOCK_SIZE // 2), 109 | (self.location_x + 3 * BLOCK_SIZE // 4, 110 | self.location_y + BLOCK_SIZE // 2)) 111 | 112 | # left 113 | elif self.father_direction == 4: 114 | pygame.draw.polygon(self.screen, WHITE, ( 115 | (self.location_x + BLOCK_SIZE // 8, 116 | self.location_y + BLOCK_SIZE // 2), 117 | (self.location_x + BLOCK_SIZE // 4, 118 | self.location_y + 3 * BLOCK_SIZE // 8), 119 | (self.location_x + BLOCK_SIZE // 4, 120 | self.location_y + 5 * BLOCK_SIZE // 8), 121 | )) 122 | pygame.draw.line(self.screen, WHITE, 123 | (self.location_x + BLOCK_SIZE // 4, 124 | self.location_y + BLOCK_SIZE // 2), 125 | (self.location_x + 7 * BLOCK_SIZE // 8, 126 | self.location_y + BLOCK_SIZE // 2)) 127 | # up right 128 | elif self.father_direction == 5: 129 | pygame.draw.polygon(self.screen, WHITE, ( 130 | (self.location_x + 3 * BLOCK_SIZE // 4, 131 | self.location_y + BLOCK_SIZE // 4), 132 | (self.location_x + 3 * BLOCK_SIZE // 4, 133 | self.location_y + 3 * BLOCK_SIZE // 8), 134 | (self.location_x + 5 * BLOCK_SIZE // 8, 135 | self.location_y + BLOCK_SIZE // 4), 136 | )) 137 | pygame.draw.line(self.screen, WHITE, 138 | (self.location_x + BLOCK_SIZE // 4, 139 | self.location_y + 3 * BLOCK_SIZE // 4), 140 | (self.location_x + 3 * BLOCK_SIZE // 4, 141 | self.location_y + BLOCK_SIZE // 4)) 142 | # up 143 | elif self.father_direction == 6: 144 | pygame.draw.polygon(self.screen, WHITE, ( 145 | (self.location_x + BLOCK_SIZE // 2, 146 | self.location_y + BLOCK_SIZE // 8), 147 | (self.location_x + 3 * BLOCK_SIZE // 8, 148 | self.location_y + BLOCK_SIZE // 4), 149 | (self.location_x + 5 * BLOCK_SIZE // 8, 150 | self.location_y + BLOCK_SIZE // 4), 151 | )) 152 | pygame.draw.line(self.screen, WHITE, 153 | (self.location_x + BLOCK_SIZE // 2, 154 | self.location_y + BLOCK_SIZE // 4), 155 | (self.location_x + BLOCK_SIZE // 2, 156 | self.location_y + 7 * BLOCK_SIZE // 8)) 157 | # up left 158 | elif self.father_direction == 7: 159 | pygame.draw.polygon(self.screen, WHITE, ( 160 | (self.location_x + BLOCK_SIZE // 4, 161 | self.location_y + BLOCK_SIZE // 4), 162 | (self.location_x + BLOCK_SIZE // 4, 163 | self.location_y + 3 * BLOCK_SIZE // 8), 164 | (self.location_x + 3 * BLOCK_SIZE // 8, 165 | self.location_y + BLOCK_SIZE // 4), 166 | )) 167 | pygame.draw.line(self.screen, WHITE, 168 | (self.location_x + BLOCK_SIZE // 4, 169 | self.location_y + BLOCK_SIZE // 4), 170 | (self.location_x + 3 * BLOCK_SIZE // 4, 171 | self.location_y + 3 * BLOCK_SIZE // 4)) 172 | 173 | def draw_rect(self): 174 | pygame.draw.rect(self.screen, self.fill_color, [ 175 | self.location_x, self.location_y, BLOCK_SIZE, BLOCK_SIZE], 0) 176 | 177 | pygame.draw.rect(self.screen, self.border_color, [ 178 | self.location_x, self.location_y, BLOCK_SIZE, BLOCK_SIZE], 3) 179 | 180 | def draw(self): 181 | self.draw_rect() 182 | 183 | padding_x = 10 184 | padding_y = 10 185 | if self.type != 0: 186 | self.draw_text(str(self.f), self.location_x + 187 | padding_x, self.location_y + padding_y) 188 | self.draw_text(str(self.g), self.location_x + padding_x, 189 | self.location_y + BLOCK_SIZE - padding_y) 190 | self.draw_text(str(self.h), self.location_x + BLOCK_SIZE - padding_x, 191 | self.location_y + BLOCK_SIZE - padding_y) 192 | 193 | self.draw_direction() 194 | 195 | def set_start(self): 196 | self.fill_color = GREEN 197 | 198 | def set_obstacle(self): 199 | if self.fill_color == BLACK: 200 | self.fill_color = BLUE 201 | return True 202 | else: 203 | return False 204 | 205 | def set_end(self): 206 | if self.fill_color == BLACK: 207 | self.fill_color = RED 208 | return True 209 | else: 210 | return False 211 | 212 | def set_open(self): 213 | self.type = 1 214 | self.border_color = GREEN 215 | 216 | def set_close(self): 217 | self.border_color = YELLOW 218 | 219 | def set_g(self, g): 220 | self.g = g 221 | self.f = self.g + self.h 222 | 223 | def set_h(self, h): 224 | self.h = h 225 | self.f = self.g + self.h 226 | 227 | def set_father(self, father, direction): 228 | self.father = father 229 | if direction != -1: 230 | self.father_direction = direction 231 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LogicJake/A-star-search/c5b024e1ecbe97b5ebe12545c1b35e1484441c86/demo.gif -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author: LogicJake 3 | # @Date: 2019-01-06 16:52:35 4 | # @Last Modified time: 2019-01-07 13:10:25 5 | import pygame 6 | import sys 7 | from pygame import locals 8 | from screen import Screen 9 | from search import A_star 10 | 11 | 12 | def main(): 13 | pygame.init() 14 | screen = Screen() 15 | 16 | step = 1 17 | exit = False 18 | pygame.display.set_caption('click block to set starting block') 19 | while True: 20 | for event in pygame.event.get(): 21 | if event.type == pygame.MOUSEBUTTONDOWN and step == 1: 22 | click_x, click_y = pygame.mouse.get_pos() 23 | screen.set_start(click_x, click_y) 24 | step = 2 25 | pygame.display.set_caption( 26 | 'click block to set obstacle blocks, press space to next step') 27 | elif event.type == pygame.MOUSEBUTTONDOWN and step == 2: 28 | click_x, click_y = pygame.mouse.get_pos() 29 | screen.set_obstacle(click_x, click_y) 30 | elif event.type == pygame.MOUSEBUTTONDOWN and step == 3: 31 | click_x, click_y = pygame.mouse.get_pos() 32 | exit = screen.set_end(click_x, click_y) 33 | # exit = True 34 | elif event.type == locals.KEYUP and event.key == locals.K_SPACE and step == 2: 35 | step = 3 36 | pygame.display.set_caption( 37 | 'click block to set finishing block') 38 | elif event.type == locals.QUIT or (event.type == locals.KEYUP and event.key == locals.K_ESCAPE): 39 | pygame.quit() 40 | sys.exit() 41 | screen.draw() 42 | pygame.display.flip() 43 | if exit: 44 | break 45 | 46 | pygame.display.set_caption('press space to step the search') 47 | 48 | search = A_star(screen) 49 | while True: 50 | for event in pygame.event.get(): 51 | if event.type == locals.QUIT or (event.type == locals.KEYUP and event.key == locals.K_ESCAPE): 52 | pygame.quit() 53 | sys.exit() 54 | elif event.type == locals.KEYUP and event.key == locals.K_SPACE and not search.over: 55 | search.step() 56 | screen.draw() 57 | pygame.display.flip() 58 | 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygame==1.9.4 2 | -------------------------------------------------------------------------------- /screen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author: LogicJake 3 | # @Date: 2019-01-07 12:12:05 4 | # @Last Modified time: 2019-01-07 12:30:08 5 | import pygame 6 | from block import Block 7 | from block import WIDTH_NUM, HEIGHT_NUM, BLOCK_SIZE 8 | 9 | 10 | SCREEN_WIDTH = WIDTH_NUM * BLOCK_SIZE 11 | SCREEN_HEIGHT = HEIGHT_NUM * BLOCK_SIZE 12 | 13 | 14 | class Screen(): 15 | 16 | def __init__(self): 17 | self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) 18 | self.blocks = [] 19 | for j in range(HEIGHT_NUM): 20 | for i in range(WIDTH_NUM): 21 | self.blocks.append(Block(self.screen, i, j)) 22 | self.start_block_index = None 23 | self.end_block_index = None 24 | self.obstacle_blocks_index = [] 25 | 26 | def draw(self): 27 | for block in self.blocks: 28 | block.draw() 29 | 30 | def get_click_block_index(self, click_x, click_y): 31 | x = click_x // BLOCK_SIZE 32 | y = click_y // BLOCK_SIZE 33 | 34 | select_block = y * WIDTH_NUM + x 35 | return select_block 36 | 37 | def set_start(self, click_x, click_y): 38 | block_index = self.get_click_block_index(click_x, click_y) 39 | self.blocks[block_index].set_start() 40 | self.start_block_index = block_index 41 | 42 | def set_obstacle(self, click_x, click_y): 43 | block_index = self.get_click_block_index(click_x, click_y) 44 | if self.blocks[block_index].set_obstacle(): 45 | self.obstacle_blocks_index.append(block_index) 46 | 47 | def set_end(self, click_x, click_y): 48 | block_index = self.get_click_block_index(click_x, click_y) 49 | if self.blocks[block_index].set_end(): 50 | self.end_block_index = block_index 51 | return True 52 | else: 53 | return False 54 | -------------------------------------------------------------------------------- /search.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author: LogicJake 3 | # @Date: 2019-01-07 12:13:14 4 | # @Last Modified time: 2019-01-07 12:31:38 5 | from screen import WIDTH_NUM, HEIGHT_NUM 6 | 7 | cost_1 = 10 8 | cost_2 = 14 9 | 10 | 11 | class A_star(): 12 | 13 | def __init__(self, screen): 14 | self.open_list = [] 15 | self.close_list = [] 16 | 17 | self.screen = screen 18 | self.over = False 19 | 20 | start_block = screen.start_block_index 21 | self.cur_block = start_block 22 | 23 | self.screen.blocks[start_block].set_g(0) 24 | self.screen.blocks[start_block].set_close() 25 | self.close_list.append(start_block) 26 | self.add_open_list(start_block) 27 | 28 | def cal_direction(self, x_plus, y_plus): 29 | if y_plus == -1: 30 | return x_plus + 1 31 | elif y_plus == 0: 32 | return 3 if x_plus == -1 else 4 33 | elif y_plus == 1: 34 | return x_plus + 6 35 | 36 | def add_open_list(self, block_index): 37 | block = self.screen.blocks[block_index] 38 | x = block.x 39 | y = block.y 40 | for x_plus in [-1, 0, 1]: 41 | for y_plus in [-1, 0, 1]: 42 | if not (x_plus == 0 and y_plus == 0): 43 | x_temp = x + x_plus 44 | y_temp = y + y_plus 45 | 46 | temp_index = y_temp * WIDTH_NUM + x_temp 47 | 48 | if self.is_ok(x_temp, y_temp, x_plus, y_plus): 49 | if temp_index in self.open_list: 50 | old_father = self.screen.blocks[block_index].father 51 | 52 | self.screen.blocks[ 53 | temp_index].set_father(block_index, -1) 54 | new_g = self.cal_g(temp_index, x_plus, y_plus) 55 | if new_g < self.screen.blocks[temp_index].g: 56 | self.screen.blocks[temp_index].set_g(new_g) 57 | direction = self.cal_direction(x_plus, y_plus) 58 | self.screen.blocks[ 59 | temp_index].set_father(block_index, direction) 60 | else: 61 | self.screen.blocks[ 62 | temp_index].set_father(old_father, -1) 63 | else: 64 | self.open_list.append(temp_index) 65 | self.screen.blocks[temp_index].set_open() 66 | 67 | direction = self.cal_direction(x_plus, y_plus) 68 | self.screen.blocks[ 69 | temp_index].set_father(block_index, direction) 70 | 71 | g = self.cal_g(temp_index, x_plus, y_plus) 72 | h = self.cal_h(x_temp, y_temp) 73 | self.screen.blocks[temp_index].set_h(h) 74 | self.screen.blocks[temp_index].set_g(g) 75 | 76 | if temp_index == self.screen.end_block_index: 77 | self.over = True 78 | print('everything is ok!') 79 | 80 | def cal_g(self, block_index, x_plus, y_plus): 81 | father_index = self.screen.blocks[block_index].father 82 | father_g = self.screen.blocks[father_index].g 83 | if x_plus == 0 or y_plus == 0: 84 | return father_g + cost_1 85 | else: 86 | return father_g + cost_2 87 | 88 | def distance(self, x1, y1, x2, y2): 89 | return (abs(x2 - x1) + abs(y2 - y1)) * cost_1 90 | 91 | def cal_h(self, x, y): 92 | end_block_index = self.screen.end_block_index 93 | end_block = self.screen.blocks[end_block_index] 94 | 95 | end_x = end_block.x 96 | end_y = end_block.y 97 | 98 | return self.distance(x, y, end_x, end_y) 99 | 100 | def is_ok(self, x, y, x_plus, y_plus): 101 | index = y * WIDTH_NUM + x 102 | 103 | if x_plus == 1 and y_plus == 1: 104 | x_temp = x 105 | y_temp = y - 1 106 | temp_index = y_temp * WIDTH_NUM + x_temp 107 | if temp_index in self.screen.obstacle_blocks_index: 108 | return False 109 | 110 | x_temp = x - 1 111 | y_temp = y 112 | temp_index = y_temp * WIDTH_NUM + x_temp 113 | if temp_index in self.screen.obstacle_blocks_index: 114 | return False 115 | elif x_plus == -1 and y_plus == 1: 116 | x_temp = x 117 | y_temp = y - 1 118 | temp_index = y_temp * WIDTH_NUM + x_temp 119 | if temp_index in self.screen.obstacle_blocks_index: 120 | return False 121 | 122 | x_temp = x + 1 123 | y_temp = y 124 | temp_index = y_temp * WIDTH_NUM + x_temp 125 | if temp_index in self.screen.obstacle_blocks_index: 126 | return False 127 | elif x_plus == 1 and y_plus == -1: 128 | x_temp = x 129 | y_temp = y + 1 130 | temp_index = y_temp * WIDTH_NUM + x_temp 131 | if temp_index in self.screen.obstacle_blocks_index: 132 | return False 133 | 134 | x_temp = x - 1 135 | y_temp = y 136 | temp_index = y_temp * WIDTH_NUM + x_temp 137 | if temp_index in self.screen.obstacle_blocks_index: 138 | return False 139 | elif x_plus == -1 and y_plus == -1: 140 | x_temp = x 141 | y_temp = y + 1 142 | temp_index = y_temp * WIDTH_NUM + x_temp 143 | if temp_index in self.screen.obstacle_blocks_index: 144 | return False 145 | 146 | x_temp = x + 1 147 | y_temp = y 148 | temp_index = y_temp * WIDTH_NUM + x_temp 149 | if temp_index in self.screen.obstacle_blocks_index: 150 | return False 151 | 152 | if x < 0 or x >= WIDTH_NUM or y >= HEIGHT_NUM or y < 0: 153 | return False 154 | elif index in self.close_list or index in self.screen.obstacle_blocks_index: 155 | return False 156 | else: 157 | return True 158 | 159 | def step(self): 160 | if len(self.open_list) == 0: 161 | self.screen.over = True 162 | print('fail to find a path') 163 | return 164 | 165 | min_f = 99999 166 | min_index = -1 167 | for index in self.open_list: 168 | f = self.screen.blocks[index].f 169 | if f <= min_f: 170 | min_f = f 171 | min_index = index 172 | self.cur_block = min_index 173 | self.screen.blocks[min_index].set_close() 174 | self.close_list.append(min_index) 175 | self.open_list.remove(min_index) 176 | self.add_open_list(min_index) 177 | --------------------------------------------------------------------------------