diff --git a/Breadcrumbs/app/main.rb b/Breadcrumbs/app/main.rb new file mode 100644 index 0000000..648805a --- /dev/null +++ b/Breadcrumbs/app/main.rb @@ -0,0 +1,545 @@ +class Breadcrumbs + attr_gtk + + # This method is called every frame/tick + # Every tick, the current state of the search is rendered on the screen, + # User input is processed, and + # The next step in the search is calculated + def tick + defaults + # If the grid has not been searched + if search.came_from.empty? + calc + # Calc Path + end + render + input + end + + def defaults + # Variables to edit the size and appearance of the grid + # Freely customizable to user's liking + grid.width ||= 30 + grid.height ||= 15 + grid.cell_size ||= 40 + grid.rect ||= [0, 0, grid.width, grid.height] + + # The location of the star and walls of the grid + # They can be modified to have a different initial grid + # Walls are stored in a hash for quick look up when doing the search + grid.star ||= [2, 8] + grid.target ||= [10, 5] + grid.walls ||= { + [3, 3] => true, + [3, 4] => true, + [3, 5] => true, + [3, 6] => true, + [3, 7] => true, + [3, 8] => true, + [3, 9] => true, + [3, 10] => true, + [3, 11] => true, + [4, 3] => true, + [4, 4] => true, + [4, 5] => true, + [4, 6] => true, + [4, 7] => true, + [4, 8] => true, + [4, 9] => true, + [4, 10] => true, + [4, 11] => true, + [13, 0] => true, + [13, 1] => true, + [13, 2] => true, + [13, 3] => true, + [13, 4] => true, + [13, 5] => true, + [13, 6] => true, + [13, 7] => true, + [13, 8] => true, + [13, 9] => true, + [13, 10] => true, + [14, 0] => true, + [14, 1] => true, + [14, 2] => true, + [14, 3] => true, + [14, 4] => true, + [14, 5] => true, + [14, 6] => true, + [14, 7] => true, + [14, 8] => true, + [14, 9] => true, + [14, 10] => true, + [21, 8] => true, + [21, 9] => true, + [21, 10] => true, + [21, 11] => true, + [21, 12] => true, + [21, 13] => true, + [21, 14] => true, + [22, 8] => true, + [22, 9] => true, + [22, 10] => true, + [22, 11] => true, + [22, 12] => true, + [22, 13] => true, + [22, 14] => true, + [23, 8] => true, + [23, 9] => true, + [24, 8] => true, + [24, 9] => true, + [25, 8] => true, + [25, 9] => true, + } + + # Variables that are used by the breadth first search + # Storing cells that the search has visited, prevents unnecessary steps + # Expanding the frontier of the search in order makes the search expand + # from the center outward + + # The cells from which the search is to expand + search.frontier ||= [] + # A hash of where each cell was expanded from + # The key is a cell, and the value is the cell it came from + search.came_from ||= {} + # Cells that are part of the path from the target to the star + search.path ||= {} + + # What the user is currently editing on the grid + # We store this value, because we want to remember the value even when + # the user's cursor is no longer over what they're interacting with, but + # they are still clicking down on the mouse. + state.current_input ||= :none + end + + def calc + # Setup the search to start from the star + search.frontier << grid.star + search.came_from[grid.star] = nil + + # Until there are no more cells to expand from + until search.frontier.empty? + # Takes the next frontier cell + new_frontier = search.frontier.shift + # For each of its neighbors + adjacent_neighbors(new_frontier).each do |neighbor| + # That have not been visited and are not walls + unless search.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) + # Add them to the frontier and mark them as visited in the first grid + # Unless the target has been visited + # Add the neighbor to the frontier and remember which cell it came from + search.frontier << neighbor + search.came_from[neighbor] = new_frontier + end + end + end + end + + + # Draws everything onto the screen + def render + render_background + # render_heat_map + render_walls + # render_path + # render_labels + render_arrows + render_star + render_target + unless grid.walls.has_key?(grid.target) + render_trail + end + end + + def render_trail(current_cell=grid.target) + return if current_cell == grid.star + parent_cell = search.came_from[current_cell] + if current_cell && parent_cell + outputs.lines << [(current_cell.x + 0.5) * grid.cell_size, (current_cell.y + 0.5) * grid.cell_size, + (parent_cell.x + 0.5) * grid.cell_size, (parent_cell.y + 0.5) * grid.cell_size, purple] + + end + render_trail(parent_cell) + end + + def render_arrows + search.came_from.each do |child, parent| + if parent && child + arrow_cell = [(child.x + parent.x) / 2, (child.y + parent.y) / 2] + if parent.x > child.x # If the parent cell is to the right of the child cell + outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 0] # Point the arrow to the right + elsif parent.x < child.x # If the parent cell is to the right of the child cell + outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 180] # Point the arrow to the right + elsif parent.y > child.y # If the parent cell is to the right of the child cell + outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 90] # Point the arrow to the right + elsif parent.y < child.y # If the parent cell is to the right of the child cell + outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 270] # Point the arrow to the right + end + end + end + end + + # The methods below subdivide the task of drawing everything to the screen + + # Draws what the grid looks like with nothing on it + def render_background + render_unvisited + render_grid_lines + end + + # Draws both grids + def render_unvisited + outputs.solids << [scale_up(grid.rect), unvisited_color] + end + + # Draws grid lines to show the division of the grid into cells + def render_grid_lines + for x in 0..grid.width + outputs.lines << vertical_line(x) + end + + for y in 0..grid.height + outputs.lines << horizontal_line(y) + end + end + + # Easy way to draw vertical lines given an index + def vertical_line column + scale_up([column, 0, column, grid.height]) + end + + # Easy way to draw horizontal lines given an index + def horizontal_line row + scale_up([0, row, grid.width, row]) + end + + # Draws the walls on both grids + def render_walls + grid.walls.each_key do |wall| + outputs.solids << [scale_up(wall), wall_color] + end + end + + # Renders the star on both grids + def render_star + outputs.sprites << [scale_up(grid.star), 'star.png'] + end + + # Renders the target on both grids + def render_target + outputs.sprites << [scale_up(grid.target), 'target.png'] + end + + # Labels the grids + def render_labels + outputs.labels << [200, 625, "Without early exit"] + end + + # Renders the path based off of the search.path hash + def render_path + # If the star and target are disconnected there will only be one path + # The path should not render in that case + unless search.path.size == 1 + search.path.each_key do | cell | + # Renders path on both grids + outputs.solids << [scale_up(cell), path_color] + end + end + end + + # Calculates the path from the target to the star after the search is over + # Relies on the came_from hash + # Fills the search.path hash, which is later rendered on screen + def calc_path + endpoint = grid.target + while endpoint + search.path[endpoint] = true + endpoint = search.came_from[endpoint] + end + end + + # In code, the cells are represented as 1x1 rectangles + # When drawn, the cells are larger than 1x1 rectangles + # This method is used to scale up cells, and lines + # Objects are scaled up according to the grid.cell_size variable + # This allows for easy customization of the visual scale of the grid + def scale_up(cell) + # Prevents the original value of cell from being edited + cell = cell.clone + + # If cell is just an x and y coordinate + if cell.size == 2 + # Add a width and height of 1 + cell << 1 + cell << 1 + end + + # Scale all the values up + cell.map! { |value| value * grid.cell_size } + + # Returns the scaled up cell + cell + end + + # This method processes user input every tick + # Any method with "1" is related to the first grid + # Any method with "2" is related to the second grid + def input + # The program has to remember that the user is dragging an object + # even when the mouse is no longer over that object + # So detecting input and processing input is separate + # detect_input + # process_input + if inputs.mouse.up + state.current_input = :none + elsif star_clicked? + state.current_input = :star + end + + if mouse_inside_grid? + unless grid.target == cell_closest_to_mouse + grid.target = cell_closest_to_mouse + end + if state.current_input == :star + unless grid.star == cell_closest_to_mouse + grid.star = cell_closest_to_mouse + end + end + end + end + + # Determines what the user is editing and stores the value + # Storing the value allows the user to continue the same edit as long as the + # mouse left click is held + def detect_input + # When the mouse is up, nothing is being edited + if inputs.mouse.up + state.current_input = :none + # When the star in the no second grid is clicked + elsif star_clicked? + state.current_input = :star + # When the target in the no second grid is clicked + elsif target_clicked? + state.current_input = :target + # When a wall in the first grid is clicked + elsif wall_clicked? + state.current_input = :remove_wall + # When the first grid is clicked + elsif grid_clicked? + state.current_input = :add_wall + end + end + + # Processes click and drag based on what the user is currently dragging + def process_input + if state.current_input == :star + input_star + elsif state.current_input == :target + input_target + elsif state.current_input == :remove_wall + input_remove_wall + elsif state.current_input == :add_wall + input_add_wall + end + end + + # Moves the star to the cell closest to the mouse in the first grid + # Only resets the search if the star changes position + # Called whenever the user is editing the star (puts mouse down on star) + def input_star + old_star = grid.star.clone + grid.star = cell_closest_to_mouse + unless old_star == grid.star + reset_search + end + end + + # Moves the target to the grid closest to the mouse in the first grid + # Only reset_searchs the search if the target changes position + # Called whenever the user is editing the target (puts mouse down on target) + def input_target + old_target = grid.target.clone + grid.target = cell_closest_to_mouse + unless old_target == grid.target + reset_search + end + end + + # Removes walls in the first grid that are under the cursor + def input_remove_wall + # The mouse needs to be inside the grid, because we only want to remove walls + # the cursor is directly over + # Recalculations should only occur when a wall is actually deleted + if mouse_inside_grid? + if grid.walls.has_key?(cell_closest_to_mouse) + grid.walls.delete(cell_closest_to_mouse) + reset_search + end + end + end + + # Adds a wall in the first grid in the cell the mouse is over + def input_add_wall + if mouse_inside_grid? + unless grid.walls.has_key?(cell_closest_to_mouse) + grid.walls[cell_closest_to_mouse] = true + reset_search + end + end + end + + + # Whenever the user edits the grid, + # The search has to be reset_searchd upto the current step + # with the current grid as the initial state of the grid + def reset_search + # Reset_Searchs the search + search.frontier = [] + search.came_from = {} + search.path = {} + end + + + # Returns a list of adjacent cells + # Used to determine what the next cells to be added to the frontier are + def adjacent_neighbors(cell) + neighbors = [] + + # Gets all the valid neighbors into the array + # From southern neighbor, clockwise + neighbors << [cell.x, cell.y - 1] unless cell.y == 0 + neighbors << [cell.x - 1, cell.y] unless cell.x == 0 + neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1 + neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1 + + # Sorts the neighbors so the rendered path is a zigzag path + # Cells in a diagonal direction are given priority + # Comment this line to see the difference + neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) } + + neighbors + end + + # Finds the vertical and horizontal distance of a cell from the star + # and returns the larger value + # This method is used to have a zigzag pattern in the rendered path + # A cell that is [5, 5] from the star, + # is explored before over a cell that is [0, 7] away. + # So, if possible, the search tries to go diagonal (zigzag) first + def proximity_to_star(x, y) + distance_x = (grid.star.x - x).abs + distance_y = (grid.star.y - y).abs + + if distance_x > distance_y + return distance_x + else + return distance_y + end + end + + # When the user grabs the star and puts their cursor to the far right + # and moves up and down, the star is supposed to move along the grid as well + # Finding the cell closest to the mouse helps with this + def cell_closest_to_mouse + # Closest cell to the mouse in the first grid + x = (inputs.mouse.point.x / grid.cell_size).to_i + y = (inputs.mouse.point.y / grid.cell_size).to_i + # Bound x and y to the grid + x = grid.width - 1 if x > grid.width - 1 + y = grid.height - 1 if y > grid.height - 1 + # Return closest cell + [x, y] + end + + # Signal that the user is going to be moving the star from the first grid + def star_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(grid.star)) + end + + # Signal that the user is going to be moving the target from the first grid + def target_clicked? + inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(grid.target)) + end + + # Signal that the user is going to be adding walls from the first grid + def grid_clicked? + inputs.mouse.down && mouse_inside_grid? + end + + # Returns whether the mouse is inside of the first grid + # Part of the condition that checks whether the user is adding a wall + def mouse_inside_grid? + inputs.mouse.point.inside_rect?(scale_up(grid.rect)) + end + + # These methods provide handy aliases to colors + + # Light brown + def unvisited_color + [221, 212, 213] + # [255, 255, 255] + end + + # Camo Green + def wall_color + [134, 134, 120] + end + + # Pastel White + def path_color + [231, 230, 228] + end + + def red + [255, 0, 0] + end + + def purple + [149, 64, 191] + end + + # Makes code more concise + def grid + state.grid + end + + def search + state.search + end +end + +# Method that is called by DragonRuby periodically +# Used for updating animations and calculations +def tick args + + # Pressing r will reset the application + if args.inputs.keyboard.key_down.r + args.gtk.reset + reset + return + end + + # Every tick, new args are passed, and the Breadth First Search tick is called + $breadcrumbs ||= Breadcrumbs.new(args) + $breadcrumbs.args = args + $breadcrumbs.tick +end + + +def reset + $breadcrumbs = nil +end + + # # Representation of how far away visited cells are from the star + # # Replaces the render_visited method + # # Visually demonstrates the effectiveness of early exit for pathfinding + # def render_heat_map + # # THIS CODE NEEDS SOME FIXING DUE TO REFACTORING + # search.came_from.each_key do | cell | + # distance = (grid.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs + # max_distance = grid.width + grid.height + # alpha = 255.to_i * distance.to_i / max_distance.to_i + # outputs.solids << [scale_up(visited_cell), red, alpha] + # # outputs.solids << [early_exit_scale_up(visited_cell), red, alpha] + # end + # end diff --git a/Breadcrumbs/arrow.png b/Breadcrumbs/arrow.png new file mode 100644 index 0000000..06e4a20 Binary files /dev/null and b/Breadcrumbs/arrow.png differ diff --git a/Breadcrumbs/star.png b/Breadcrumbs/star.png new file mode 100644 index 0000000..b37bb04 Binary files /dev/null and b/Breadcrumbs/star.png differ diff --git a/Breadcrumbs/target.png b/Breadcrumbs/target.png new file mode 100644 index 0000000..fb2223d Binary files /dev/null and b/Breadcrumbs/target.png differ diff --git a/README.md b/README.md index 63aedde..5e39edd 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,9 @@ Based off of https://www.redblobgames.com/pathfinding/a-star/introduction.html # Breadth First Search ![](gifs/Breadth_First_Search.gif) - # Detailed Breadth First Search ![](gifs/Detailed_Breadth_First_Search.gif) - # Early Exit Breadth First Search ![](gifs/Early_Exit_Breadth_First_Search.gif) @@ -31,3 +29,5 @@ Based off of https://www.redblobgames.com/pathfinding/a-star/introduction.html # Greedy With Walls ![](gifs/Greedy_With_Walls.gif) +# A* +![](gifs/A_Star.gif) diff --git a/gifs/A_Star.gif b/gifs/A_Star.gif new file mode 100644 index 0000000..ee66ff1 Binary files /dev/null and b/gifs/A_Star.gif differ