Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
695 changes: 695 additions & 0 deletions AStarPathfinding/blog.md

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions AStarPathfinding/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
**[Step-By-Step Technical Blog Guide](https://hq.bitproject.org/how-to-write-a-technical-blog/)**



\### :pushpin: Step 1

**TITLE:**

Introduction to AI with A* Pathfinding



**TOPIC:**

Artificial Intelligence



**DESCRIPTION (5-7+ sentences):**

The A* algorithm is used for pathfinding in games and maps. Any time a programmer needs some entity to find the shortest distance from point A to point B, one of the go to algorithms to try first is A*.

A* is an extension on another algorithm Dijkstra's shortest path. A* differs in that it has knowledge about the location of its goal and estimates how far away it is which places it into the realm of artificial intelligence.

I create the environment first, then move to implementing the algorithm and finally add some visualization flair so the reader gets a more intuitive feel on how the algorithm works.



\### :pushpin: Step 2

:family: **TARGET AUDIENCE (3-5+ sentences):**

Any programmer trying to find the shortest distance from point A to point B in a graph or grid. These would typically be people that are in game dev or robotics. I go over each piece of code and why it's needed so technically anyone should be able to follow it but I do not go over Python basics so I would not recommend this to someone that has not programmed before.



\### :pushpin: Step 3

\> Outline your learning/teaching structure:



**Beginning (2-3+ sentences):**

Introduce A*, some applications, how it works, and why it works. I add some pictures to illustrate the f(n) function and its components so the reader can see for themselves that the f(n) function makes sense.



**Middle (2-3+ sentences):**

I move on to how I created the environment and show code snippets along the way. Then I implement A*. I explain it from the point of view of a 'lazy programmer'. By this I mean that when I create the environment with the Node and Grid classes I only add fields and methods related to the environment. Once I start the A* implementation do I add fields specific to A* and pathfinding to prevent frontloading too much information at once.



**End (2-3+ sentences):**

After A* is done I move onto how the reader can visualize the algorithm which only requires a few more lines of code since most of the overhead was made along the way. I create a few suggestions the reader can work on if they are interested in building atop the code they have just written. Finally, as an extra aside I show the reader what happens when you mess around with the heuristic function to prove its importance and what happens when its overestimated.
133 changes: 133 additions & 0 deletions AStarPathfinding/solution/astar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import heapq
import math
import collections
import random

import grid
from macros import *

# calculating using the manhattan distances
def calculate_h_score(curr_node, target_node):
curr_row, curr_col = curr_node.coordinates
target_row, target_col = target_node.coordinates
distance = abs(curr_row - target_row) + abs(curr_col - target_col)

return distance


def init_h(main_grid, target_node):
for i in range(main_grid.num_of_rows):
for j in range(main_grid.num_of_cols):
node = main_grid.grid_data[i][j]
node.h_score = calculate_h_score(node, target_node)


# determines if it is possible to travel to this neighbor
def is_valid_neighbor(main_grid, neighbor_coor):

n_row, n_col = neighbor_coor
# check bounds of grid
if n_row >= main_grid.num_of_rows or n_col >= main_grid.num_of_cols:
return False
if n_row < 0 or n_col < 0:
return False

neighbor_node = main_grid.get_node(neighbor_coor)
if neighbor_node.color == OBSTACLE:
neighbor_node.checked = True # Using this to check if recalculating A* is necessary
return False

return True


# finds the neighbors of the current_node and
# stores them in a list to be returned
def get_neighbors(main_grid, curr_node):
row, col = curr_node.coordinates
neighbor_list = []

possible_neighbors = [
(row-1, col), #Northern
(row+1, col), #Southern
(row, col-1), #Eastern
(row, col+1) #Western
]

for neighbor_coor in possible_neighbors:
if is_valid_neighbor(main_grid, neighbor_coor):
neighbor_node = main_grid.get_node(neighbor_coor)
neighbor_list.append(neighbor_node)

return neighbor_list


def reconstruct_path(node):
path = collections.deque([])
# This will stop at the seeker because
# the seeker has no predecessor
while node.predecessor:
path.appendleft((node.coordinates, PATH))
node = node.predecessor

return path


def astar_search(main_grid):

astar_pathing = collections.deque([])

# First lets get our seeker and target so we know
# where to begin and where we are going
seeker_node = main_grid.get_node(main_grid.seeker_coor)
target_node = main_grid.get_node(main_grid.target_coor)

# set the h score of every node in the grid based
# on the the best estimate that we can obtain
init_h(main_grid, target_node)

# we know the seeker is where we are starting
# so the seeker's distance from start is zero
seeker_node.g_score = 0

# commonly referred to as the open set.
# set of nodes that we want to explore
nodes_of_interest = []
heapq.heappush(nodes_of_interest, seeker_node)

while nodes_of_interest: #while list is not empty

#get node with lowest f score in nodes of interest
curr_node = heapq.heappop(nodes_of_interest)
if curr_node == target_node:
# Yay we are done!
return astar_pathing + reconstruct_path(curr_node)

# colors A*'s decisions
curr_node.checked = True
astar_pathing.append((curr_node.coordinates, INSPECTED))

neighbors_of_curr_node = get_neighbors(main_grid, curr_node)

for neighbor in neighbors_of_curr_node:

# colors A*'s decisions
if not neighbor.checked:
astar_pathing.append((neighbor.coordinates, INTERESTING))

# The distance between any two adjacent nodes is always 1
# in this grid, hence + 1. Otherwise the added value would be the
# weight of the edge between the current node and the neighbor
temp_g = curr_node.g_score + 1

# If the path from curr_node to this neighbor is better
# than any previously know path, set curr_node as its
# predecessor and update its g_score
if temp_g < neighbor.g_score:
neighbor.predecessor = curr_node
neighbor.g_score = temp_g

if neighbor not in nodes_of_interest:
heapq.heappush(nodes_of_interest, neighbor)

return astar_pathing

86 changes: 86 additions & 0 deletions AStarPathfinding/solution/grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from macros import *
import pygame
import random
import math


class Node:
def __init__(self, coordinates, color=EMPTY):
self.color = color
self.coordinates = coordinates
self.g_score = math.inf
self.h_score = math.inf
self.predecessor = None
self.checked = False

# Since f is a function of g and h, we will call
# a function to fetch the f score
def get_f_score(self):
return self.g_score + self.h_score

# used to compare two nodes in the heap
def __lt__(self, other):
if self.get_f_score() == other.get_f_score():
return self.g_score > other.g_score
return self.get_f_score() < other.get_f_score()

# define equality between two nodes
def __eq__(self, other):
return self.coordinates == other.coordinates

def reset(self):
self.g_score = math.inf
self.h_score = math.inf
self.predecessor = None
self.checked = False


class Grid:
def __init__(self, num_of_rows=15, num_of_cols=25):
self.num_of_rows = num_of_rows
self.num_of_cols = num_of_cols
self.grid_data = [[Node((row,col)) for col in range(num_of_cols)] for row in range(num_of_rows)]
alpha_x = .1 # needs to be between 0 and 1
alpha_y = .1 # needs to be between 0 and 1
self.seeker_coor = (
int((num_of_rows - 1) * alpha_y),
int((num_of_cols - 1) * alpha_x))
self.target_coor = (
int(num_of_rows * (1-alpha_y)),
int( num_of_cols * (1-alpha_x)))
self.grid_data[self.seeker_coor[0]][self.seeker_coor[1]] = \
Node(self.seeker_coor, SEEKER)
self.grid_data[self.target_coor[0]][self.target_coor[1]] = \
Node(self.target_coor, TARGET)

# take mouse position and convert it to grid coordinates
def convert_to_sq_coor(self, pos):
col = pos[0] // (WIDTH + MARGIN)
row = pos[1] // (HEIGHT + MARGIN)
#make sure col and row arent out of bounds
if col >= self.num_of_cols:
col = self.num_of_cols - 1
if row >= self.num_of_rows:
row = self.num_of_rows - 1
return (row, col)

# Returns the node at the given grid coordinates
def get_node(self, coordinates):
row, col = coordinates
return self.grid_data[row][col]

# Sets the color of the node at the given grid coors
def set_color(self, node_coor, color):
node = self.get_node(node_coor)
#don't draw over the seeker and target
if is_needed(node.color):
return
node.color = color

# creates a basic random maze in the grid
def create_maze(self):
for i in range(self.num_of_rows):
for j in range(self.num_of_cols):
rand_val = random.randint(1,10)
if rand_val <= 3:
self.set_color((i,j), OBSTACLE)
32 changes: 32 additions & 0 deletions AStarPathfinding/solution/macros.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#### MACROS ####

# size of squares in grid
WIDTH = 20
HEIGHT = 20
MARGIN = 3

# All the possible colors of nodes
SEEKER = (0,0,200) # Dark Blue
PATH = (79,148,205) # Light Blue
INTERESTING = (25,255,25) # Green
INSPECTED = (230,40,40) # Red
OBSTACLE = (0,0,0) # Black
BACKGROUND = (25,25,35) # Light Grey
EMPTY = (240,240,255) # Off White
TARGET = (148,0,212) # Magenta

# functions for pygame initialization
def get_total_pixel_width(squares_x):
return (MARGIN + WIDTH) * squares_x + MARGIN

def get_total_pixel_height(squares_y):
return (MARGIN + HEIGHT) * squares_y + MARGIN

def is_needed(color):
needed_colors = [
SEEKER,
TARGET
]
if color in needed_colors:
return True
return False
72 changes: 72 additions & 0 deletions AStarPathfinding/solution/main_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import pygame

# importing our files
import grid
import astar
from macros import *

main_grid = grid.Grid()
main_grid.create_maze()

path = astar.astar_search(main_grid)

# initializes all imported pygame modules
pygame.init()

# Set the size of the screen
total_height = get_total_pixel_height(main_grid.num_of_rows)
total_width = get_total_pixel_width(main_grid.num_of_cols)
WINDOW_SIZE = (total_width, total_height)
screen = pygame.display.set_mode(WINDOW_SIZE)

# Create a title for our window
pygame.display.set_caption("A* Pathfinder")

# This clock manages how fast the screen updates
clock = pygame.time.Clock()

# loop until the user clicks the close button
done = False
start = False

# ==== MAIN GAME LOOP ====
while not done:
# ---- Main event loop ----
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
if event.type == pygame.MOUSEBUTTONDOWN:
start = True

if start:
if path:
node_coor, color = path.popleft()
main_grid.set_color(node_coor, color)

# Clears the screen and sets a background color
# requires an RGB 3-tuple
screen.fill(BACKGROUND)

# ---- Draw the Grid Here ----
for row in range(main_grid.num_of_rows):
for col in range (main_grid.num_of_cols):
node = main_grid.get_node((row,col))
pygame.draw.rect(
screen,
node.color,
[get_total_pixel_width(col),
get_total_pixel_height(row),
WIDTH,
HEIGHT]
)
# ----------------------------

# Updates the whole screen with what we've drawn.
pygame.display.flip()

# set framerate of our game
clock.tick(100)

pygame.quit()