├── .gitignore ├── A_Star ├── app │ └── main.rb ├── circle-white.png ├── star.png └── target.png ├── Breadcrumbs ├── app │ └── main.rb ├── arrow.png ├── star.png └── target.png ├── Breadth_First_Search ├── app │ └── main.rb ├── circle-white.png └── star.png ├── Detailed_Breadth_First_Search ├── app │ └── main.rb ├── circle-white.png └── star.png ├── Early_Exit_Breadth_First_Search ├── app │ └── main.rb ├── star.png └── target.png ├── Heuristic ├── app │ └── main.rb ├── circle-white.png ├── star.png └── target.png ├── Heuristic_With_Walls ├── app │ └── main.rb ├── circle-white.png ├── star.png └── target.png ├── LICENSE ├── Movement_Costs ├── app │ └── main.rb ├── star.png └── target.png ├── README.md └── gifs ├── A_Star.gif ├── Breadth_First_Search.gif ├── Detailed_Breadth_First_Search.gif ├── Early_Exit_Breadth_First_Search.gif ├── Heuristic.gif ├── Heuristic_With_Walls.gif └── Movement_Costs.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | gifs/.DS_Store -------------------------------------------------------------------------------- /A_Star/circle-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/A_Star/circle-white.png -------------------------------------------------------------------------------- /A_Star/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/A_Star/star.png -------------------------------------------------------------------------------- /A_Star/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/A_Star/target.png -------------------------------------------------------------------------------- /Breadcrumbs/app/main.rb: -------------------------------------------------------------------------------- 1 | class Breadcrumbs 2 | attr_gtk 3 | 4 | # This method is called every frame/tick 5 | # Every tick, the current state of the search is rendered on the screen, 6 | # User input is processed, and 7 | # The next step in the search is calculated 8 | def tick 9 | defaults 10 | # If the grid has not been searched 11 | if search.came_from.empty? 12 | calc 13 | # Calc Path 14 | end 15 | render 16 | input 17 | end 18 | 19 | def defaults 20 | # Variables to edit the size and appearance of the grid 21 | # Freely customizable to user's liking 22 | grid.width ||= 30 23 | grid.height ||= 15 24 | grid.cell_size ||= 40 25 | grid.rect ||= [0, 0, grid.width, grid.height] 26 | 27 | # The location of the star and walls of the grid 28 | # They can be modified to have a different initial grid 29 | # Walls are stored in a hash for quick look up when doing the search 30 | grid.star ||= [2, 8] 31 | grid.target ||= [10, 5] 32 | grid.walls ||= { 33 | [3, 3] => true, 34 | [3, 4] => true, 35 | [3, 5] => true, 36 | [3, 6] => true, 37 | [3, 7] => true, 38 | [3, 8] => true, 39 | [3, 9] => true, 40 | [3, 10] => true, 41 | [3, 11] => true, 42 | [4, 3] => true, 43 | [4, 4] => true, 44 | [4, 5] => true, 45 | [4, 6] => true, 46 | [4, 7] => true, 47 | [4, 8] => true, 48 | [4, 9] => true, 49 | [4, 10] => true, 50 | [4, 11] => true, 51 | [13, 0] => true, 52 | [13, 1] => true, 53 | [13, 2] => true, 54 | [13, 3] => true, 55 | [13, 4] => true, 56 | [13, 5] => true, 57 | [13, 6] => true, 58 | [13, 7] => true, 59 | [13, 8] => true, 60 | [13, 9] => true, 61 | [13, 10] => true, 62 | [14, 0] => true, 63 | [14, 1] => true, 64 | [14, 2] => true, 65 | [14, 3] => true, 66 | [14, 4] => true, 67 | [14, 5] => true, 68 | [14, 6] => true, 69 | [14, 7] => true, 70 | [14, 8] => true, 71 | [14, 9] => true, 72 | [14, 10] => true, 73 | [21, 8] => true, 74 | [21, 9] => true, 75 | [21, 10] => true, 76 | [21, 11] => true, 77 | [21, 12] => true, 78 | [21, 13] => true, 79 | [21, 14] => true, 80 | [22, 8] => true, 81 | [22, 9] => true, 82 | [22, 10] => true, 83 | [22, 11] => true, 84 | [22, 12] => true, 85 | [22, 13] => true, 86 | [22, 14] => true, 87 | [23, 8] => true, 88 | [23, 9] => true, 89 | [24, 8] => true, 90 | [24, 9] => true, 91 | [25, 8] => true, 92 | [25, 9] => true, 93 | } 94 | 95 | # Variables that are used by the breadth first search 96 | # Storing cells that the search has visited, prevents unnecessary steps 97 | # Expanding the frontier of the search in order makes the search expand 98 | # from the center outward 99 | 100 | # The cells from which the search is to expand 101 | search.frontier ||= [] 102 | # A hash of where each cell was expanded from 103 | # The key is a cell, and the value is the cell it came from 104 | search.came_from ||= {} 105 | # Cells that are part of the path from the target to the star 106 | search.path ||= {} 107 | 108 | # What the user is currently editing on the grid 109 | # We store this value, because we want to remember the value even when 110 | # the user's cursor is no longer over what they're interacting with, but 111 | # they are still clicking down on the mouse. 112 | state.current_input ||= :none 113 | end 114 | 115 | def calc 116 | # Setup the search to start from the star 117 | search.frontier << grid.star 118 | search.came_from[grid.star] = nil 119 | 120 | # Until there are no more cells to expand from 121 | until search.frontier.empty? 122 | # Takes the next frontier cell 123 | new_frontier = search.frontier.shift 124 | # For each of its neighbors 125 | adjacent_neighbors(new_frontier).each do |neighbor| 126 | # That have not been visited and are not walls 127 | unless search.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) 128 | # Add them to the frontier and mark them as visited in the first grid 129 | # Unless the target has been visited 130 | # Add the neighbor to the frontier and remember which cell it came from 131 | search.frontier << neighbor 132 | search.came_from[neighbor] = new_frontier 133 | end 134 | end 135 | end 136 | end 137 | 138 | 139 | # Draws everything onto the screen 140 | def render 141 | render_background 142 | # render_heat_map 143 | render_walls 144 | # render_path 145 | # render_labels 146 | render_arrows 147 | render_star 148 | render_target 149 | unless grid.walls.has_key?(grid.target) 150 | render_trail 151 | end 152 | end 153 | 154 | def render_trail(current_cell=grid.target) 155 | return if current_cell == grid.star 156 | parent_cell = search.came_from[current_cell] 157 | if current_cell && parent_cell 158 | outputs.lines << [(current_cell.x + 0.5) * grid.cell_size, (current_cell.y + 0.5) * grid.cell_size, 159 | (parent_cell.x + 0.5) * grid.cell_size, (parent_cell.y + 0.5) * grid.cell_size, purple] 160 | 161 | end 162 | render_trail(parent_cell) 163 | end 164 | 165 | def render_arrows 166 | search.came_from.each do |child, parent| 167 | if parent && child 168 | arrow_cell = [(child.x + parent.x) / 2, (child.y + parent.y) / 2] 169 | if parent.x > child.x # If the parent cell is to the right of the child cell 170 | outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 0] # Point the arrow to the right 171 | elsif parent.x < child.x # If the parent cell is to the right of the child cell 172 | outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 180] # Point the arrow to the right 173 | elsif parent.y > child.y # If the parent cell is to the right of the child cell 174 | outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 90] # Point the arrow to the right 175 | elsif parent.y < child.y # If the parent cell is to the right of the child cell 176 | outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 270] # Point the arrow to the right 177 | end 178 | end 179 | end 180 | end 181 | 182 | # The methods below subdivide the task of drawing everything to the screen 183 | 184 | # Draws what the grid looks like with nothing on it 185 | def render_background 186 | render_unvisited 187 | render_grid_lines 188 | end 189 | 190 | # Draws both grids 191 | def render_unvisited 192 | outputs.solids << [scale_up(grid.rect), unvisited_color] 193 | end 194 | 195 | # Draws grid lines to show the division of the grid into cells 196 | def render_grid_lines 197 | for x in 0..grid.width 198 | outputs.lines << vertical_line(x) 199 | end 200 | 201 | for y in 0..grid.height 202 | outputs.lines << horizontal_line(y) 203 | end 204 | end 205 | 206 | # Easy way to draw vertical lines given an index 207 | def vertical_line column 208 | scale_up([column, 0, column, grid.height]) 209 | end 210 | 211 | # Easy way to draw horizontal lines given an index 212 | def horizontal_line row 213 | scale_up([0, row, grid.width, row]) 214 | end 215 | 216 | # Draws the walls on both grids 217 | def render_walls 218 | grid.walls.each_key do |wall| 219 | outputs.solids << [scale_up(wall), wall_color] 220 | end 221 | end 222 | 223 | # Renders the star on both grids 224 | def render_star 225 | outputs.sprites << [scale_up(grid.star), 'star.png'] 226 | end 227 | 228 | # Renders the target on both grids 229 | def render_target 230 | outputs.sprites << [scale_up(grid.target), 'target.png'] 231 | end 232 | 233 | # Labels the grids 234 | def render_labels 235 | outputs.labels << [200, 625, "Without early exit"] 236 | end 237 | 238 | # Renders the path based off of the search.path hash 239 | def render_path 240 | # If the star and target are disconnected there will only be one path 241 | # The path should not render in that case 242 | unless search.path.size == 1 243 | search.path.each_key do | cell | 244 | # Renders path on both grids 245 | outputs.solids << [scale_up(cell), path_color] 246 | end 247 | end 248 | end 249 | 250 | # Calculates the path from the target to the star after the search is over 251 | # Relies on the came_from hash 252 | # Fills the search.path hash, which is later rendered on screen 253 | def calc_path 254 | endpoint = grid.target 255 | while endpoint 256 | search.path[endpoint] = true 257 | endpoint = search.came_from[endpoint] 258 | end 259 | end 260 | 261 | # In code, the cells are represented as 1x1 rectangles 262 | # When drawn, the cells are larger than 1x1 rectangles 263 | # This method is used to scale up cells, and lines 264 | # Objects are scaled up according to the grid.cell_size variable 265 | # This allows for easy customization of the visual scale of the grid 266 | def scale_up(cell) 267 | # Prevents the original value of cell from being edited 268 | cell = cell.clone 269 | 270 | # If cell is just an x and y coordinate 271 | if cell.size == 2 272 | # Add a width and height of 1 273 | cell << 1 274 | cell << 1 275 | end 276 | 277 | # Scale all the values up 278 | cell.map! { |value| value * grid.cell_size } 279 | 280 | # Returns the scaled up cell 281 | cell 282 | end 283 | 284 | # This method processes user input every tick 285 | # Any method with "1" is related to the first grid 286 | # Any method with "2" is related to the second grid 287 | def input 288 | # The program has to remember that the user is dragging an object 289 | # even when the mouse is no longer over that object 290 | # So detecting input and processing input is separate 291 | # detect_input 292 | # process_input 293 | if inputs.mouse.up 294 | state.current_input = :none 295 | elsif star_clicked? 296 | state.current_input = :star 297 | end 298 | 299 | if mouse_inside_grid? 300 | unless grid.target == cell_closest_to_mouse 301 | grid.target = cell_closest_to_mouse 302 | end 303 | if state.current_input == :star 304 | unless grid.star == cell_closest_to_mouse 305 | grid.star = cell_closest_to_mouse 306 | end 307 | end 308 | end 309 | end 310 | 311 | # Determines what the user is editing and stores the value 312 | # Storing the value allows the user to continue the same edit as long as the 313 | # mouse left click is held 314 | def detect_input 315 | # When the mouse is up, nothing is being edited 316 | if inputs.mouse.up 317 | state.current_input = :none 318 | # When the star in the no second grid is clicked 319 | elsif star_clicked? 320 | state.current_input = :star 321 | # When the target in the no second grid is clicked 322 | elsif target_clicked? 323 | state.current_input = :target 324 | # When a wall in the first grid is clicked 325 | elsif wall_clicked? 326 | state.current_input = :remove_wall 327 | # When the first grid is clicked 328 | elsif grid_clicked? 329 | state.current_input = :add_wall 330 | end 331 | end 332 | 333 | # Processes click and drag based on what the user is currently dragging 334 | def process_input 335 | if state.current_input == :star 336 | input_star 337 | elsif state.current_input == :target 338 | input_target 339 | elsif state.current_input == :remove_wall 340 | input_remove_wall 341 | elsif state.current_input == :add_wall 342 | input_add_wall 343 | end 344 | end 345 | 346 | # Moves the star to the cell closest to the mouse in the first grid 347 | # Only resets the search if the star changes position 348 | # Called whenever the user is editing the star (puts mouse down on star) 349 | def input_star 350 | old_star = grid.star.clone 351 | grid.star = cell_closest_to_mouse 352 | unless old_star == grid.star 353 | reset_search 354 | end 355 | end 356 | 357 | # Moves the target to the grid closest to the mouse in the first grid 358 | # Only reset_searchs the search if the target changes position 359 | # Called whenever the user is editing the target (puts mouse down on target) 360 | def input_target 361 | old_target = grid.target.clone 362 | grid.target = cell_closest_to_mouse 363 | unless old_target == grid.target 364 | reset_search 365 | end 366 | end 367 | 368 | # Removes walls in the first grid that are under the cursor 369 | def input_remove_wall 370 | # The mouse needs to be inside the grid, because we only want to remove walls 371 | # the cursor is directly over 372 | # Recalculations should only occur when a wall is actually deleted 373 | if mouse_inside_grid? 374 | if grid.walls.has_key?(cell_closest_to_mouse) 375 | grid.walls.delete(cell_closest_to_mouse) 376 | reset_search 377 | end 378 | end 379 | end 380 | 381 | # Adds a wall in the first grid in the cell the mouse is over 382 | def input_add_wall 383 | if mouse_inside_grid? 384 | unless grid.walls.has_key?(cell_closest_to_mouse) 385 | grid.walls[cell_closest_to_mouse] = true 386 | reset_search 387 | end 388 | end 389 | end 390 | 391 | 392 | # Whenever the user edits the grid, 393 | # The search has to be reset_searchd upto the current step 394 | # with the current grid as the initial state of the grid 395 | def reset_search 396 | # Reset_Searchs the search 397 | search.frontier = [] 398 | search.came_from = {} 399 | search.path = {} 400 | end 401 | 402 | 403 | # Returns a list of adjacent cells 404 | # Used to determine what the next cells to be added to the frontier are 405 | def adjacent_neighbors(cell) 406 | neighbors = [] 407 | 408 | # Gets all the valid neighbors into the array 409 | # From southern neighbor, clockwise 410 | neighbors << [cell.x, cell.y - 1] unless cell.y == 0 411 | neighbors << [cell.x - 1, cell.y] unless cell.x == 0 412 | neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1 413 | neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1 414 | 415 | # Sorts the neighbors so the rendered path is a zigzag path 416 | # Cells in a diagonal direction are given priority 417 | # Comment this line to see the difference 418 | neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) } 419 | 420 | neighbors 421 | end 422 | 423 | # Finds the vertical and horizontal distance of a cell from the star 424 | # and returns the larger value 425 | # This method is used to have a zigzag pattern in the rendered path 426 | # A cell that is [5, 5] from the star, 427 | # is explored before over a cell that is [0, 7] away. 428 | # So, if possible, the search tries to go diagonal (zigzag) first 429 | def proximity_to_star(x, y) 430 | distance_x = (grid.star.x - x).abs 431 | distance_y = (grid.star.y - y).abs 432 | 433 | if distance_x > distance_y 434 | return distance_x 435 | else 436 | return distance_y 437 | end 438 | end 439 | 440 | # When the user grabs the star and puts their cursor to the far right 441 | # and moves up and down, the star is supposed to move along the grid as well 442 | # Finding the cell closest to the mouse helps with this 443 | def cell_closest_to_mouse 444 | # Closest cell to the mouse in the first grid 445 | x = (inputs.mouse.point.x / grid.cell_size).to_i 446 | y = (inputs.mouse.point.y / grid.cell_size).to_i 447 | # Bound x and y to the grid 448 | x = grid.width - 1 if x > grid.width - 1 449 | y = grid.height - 1 if y > grid.height - 1 450 | # Return closest cell 451 | [x, y] 452 | end 453 | 454 | # Signal that the user is going to be moving the star from the first grid 455 | def star_clicked? 456 | inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(grid.star)) 457 | end 458 | 459 | # Signal that the user is going to be moving the target from the first grid 460 | def target_clicked? 461 | inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(grid.target)) 462 | end 463 | 464 | # Signal that the user is going to be adding walls from the first grid 465 | def grid_clicked? 466 | inputs.mouse.down && mouse_inside_grid? 467 | end 468 | 469 | # Returns whether the mouse is inside of the first grid 470 | # Part of the condition that checks whether the user is adding a wall 471 | def mouse_inside_grid? 472 | inputs.mouse.point.inside_rect?(scale_up(grid.rect)) 473 | end 474 | 475 | # These methods provide handy aliases to colors 476 | 477 | # Light brown 478 | def unvisited_color 479 | [221, 212, 213] 480 | # [255, 255, 255] 481 | end 482 | 483 | # Camo Green 484 | def wall_color 485 | [134, 134, 120] 486 | end 487 | 488 | # Pastel White 489 | def path_color 490 | [231, 230, 228] 491 | end 492 | 493 | def red 494 | [255, 0, 0] 495 | end 496 | 497 | def purple 498 | [149, 64, 191] 499 | end 500 | 501 | # Makes code more concise 502 | def grid 503 | state.grid 504 | end 505 | 506 | def search 507 | state.search 508 | end 509 | end 510 | 511 | # Method that is called by DragonRuby periodically 512 | # Used for updating animations and calculations 513 | def tick args 514 | 515 | # Pressing r will reset the application 516 | if args.inputs.keyboard.key_down.r 517 | args.gtk.reset 518 | reset 519 | return 520 | end 521 | 522 | # Every tick, new args are passed, and the Breadth First Search tick is called 523 | $breadcrumbs ||= Breadcrumbs.new(args) 524 | $breadcrumbs.args = args 525 | $breadcrumbs.tick 526 | end 527 | 528 | 529 | def reset 530 | $breadcrumbs = nil 531 | end 532 | 533 | # # Representation of how far away visited cells are from the star 534 | # # Replaces the render_visited method 535 | # # Visually demonstrates the effectiveness of early exit for pathfinding 536 | # def render_heat_map 537 | # # THIS CODE NEEDS SOME FIXING DUE TO REFACTORING 538 | # search.came_from.each_key do | cell | 539 | # distance = (grid.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs 540 | # max_distance = grid.width + grid.height 541 | # alpha = 255.to_i * distance.to_i / max_distance.to_i 542 | # outputs.solids << [scale_up(visited_cell), red, alpha] 543 | # # outputs.solids << [early_exit_scale_up(visited_cell), red, alpha] 544 | # end 545 | # end 546 | -------------------------------------------------------------------------------- /Breadcrumbs/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Breadcrumbs/arrow.png -------------------------------------------------------------------------------- /Breadcrumbs/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Breadcrumbs/star.png -------------------------------------------------------------------------------- /Breadcrumbs/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Breadcrumbs/target.png -------------------------------------------------------------------------------- /Breadth_First_Search/app/main.rb: -------------------------------------------------------------------------------- 1 | # A visual demonstration of a breadth first search 2 | # Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html 3 | 4 | # An animation that can respond to user input in real time 5 | 6 | # A breadth first search expands in all directions one step at a time 7 | # The frontier is a queue of cells to be expanded from 8 | # The visited hash allows quick lookups of cells that have been expanded from 9 | # The walls hash allows quick lookup of whether a cell is a wall 10 | 11 | # The breadth first search starts by adding the red star to the frontier array 12 | # and marking it as visited 13 | # Each step a cell is removed from the front of the frontier array (queue) 14 | # Unless the neighbor is a wall or visited, it is added to the frontier array 15 | # The neighbor is then marked as visited 16 | 17 | # The frontier is blue 18 | # Visited cells are light brown 19 | # Walls are camo green 20 | # Even when walls are visited, they will maintain their wall color 21 | 22 | # The star can be moved by clicking and dragging 23 | # Walls can be added and removed by clicking and dragging 24 | 25 | class BreadthFirstSearch 26 | attr_gtk 27 | 28 | def initialize(args) 29 | # Variables to edit the size and appearance of the grid 30 | # Freely customizable to user's liking 31 | args.state.grid.width = 30 32 | args.state.grid.height = 15 33 | args.state.grid.cell_size = 40 34 | 35 | # Stores which step of the animation is being rendered 36 | # When the user moves the star or messes with the walls, 37 | # the breadth first search is recalculated up to this step 38 | args.state.anim_steps = 0 39 | 40 | # At some step the animation will end, 41 | # and further steps won't change anything (the whole grid will be explored) 42 | # This step is roughly the grid's width * height 43 | # When anim_steps equals max_steps no more calculations will occur 44 | # and the slider will be at the end 45 | args.state.max_steps = args.state.grid.width * args.state.grid.height 46 | 47 | # Whether the animation should play or not 48 | # If true, every tick moves anim_steps forward one 49 | # Pressing the stepwise animation buttons will pause the animation 50 | args.state.play = true 51 | 52 | # The location of the star and walls of the grid 53 | # They can be modified to have a different initial grid 54 | # Walls are stored in a hash for quick look up when doing the search 55 | args.state.star = [0, 0] 56 | args.state.walls = { 57 | [3, 3] => true, 58 | [3, 4] => true, 59 | [3, 5] => true, 60 | [3, 6] => true, 61 | [3, 7] => true, 62 | [3, 8] => true, 63 | [3, 9] => true, 64 | [3, 10] => true, 65 | [3, 11] => true, 66 | [4, 3] => true, 67 | [4, 4] => true, 68 | [4, 5] => true, 69 | [4, 6] => true, 70 | [4, 7] => true, 71 | [4, 8] => true, 72 | [4, 9] => true, 73 | [4, 10] => true, 74 | [4, 11] => true, 75 | 76 | [13, 0] => true, 77 | [13, 1] => true, 78 | [13, 2] => true, 79 | [13, 3] => true, 80 | [13, 4] => true, 81 | [13, 5] => true, 82 | [13, 6] => true, 83 | [13, 7] => true, 84 | [13, 8] => true, 85 | [13, 9] => true, 86 | [13, 10] => true, 87 | [14, 0] => true, 88 | [14, 1] => true, 89 | [14, 2] => true, 90 | [14, 3] => true, 91 | [14, 4] => true, 92 | [14, 5] => true, 93 | [14, 6] => true, 94 | [14, 7] => true, 95 | [14, 8] => true, 96 | [14, 9] => true, 97 | [14, 10] => true, 98 | 99 | [21, 8] => true, 100 | [21, 9] => true, 101 | [21, 10] => true, 102 | [21, 11] => true, 103 | [21, 12] => true, 104 | [21, 13] => true, 105 | [21, 14] => true, 106 | [22, 8] => true, 107 | [22, 9] => true, 108 | [22, 10] => true, 109 | [22, 11] => true, 110 | [22, 12] => true, 111 | [22, 13] => true, 112 | [22, 14] => true, 113 | [23, 8] => true, 114 | [23, 9] => true, 115 | [24, 8] => true, 116 | [24, 9] => true, 117 | [25, 8] => true, 118 | [25, 9] => true, 119 | } 120 | 121 | # Variables that are used by the breadth first search 122 | # Storing cells that the search has visited, prevents unnecessary steps 123 | # Expanding the frontier of the search in order makes the search expand 124 | # from the center outward 125 | args.state.visited = {} 126 | args.state.frontier = [] 127 | 128 | 129 | # What the user is currently editing on the grid 130 | # Possible values are: :none, :slider, :star, :remove_wall, :add_wall 131 | 132 | # We store this value, because we want to remember the value even when 133 | # the user's cursor is no longer over what they're interacting with, but 134 | # they are still clicking down on the mouse. 135 | args.state.click_and_drag = :none 136 | 137 | # Store the rects of the buttons that control the animation 138 | # They are here for user customization 139 | # Editing these might require recentering the text inside them 140 | # Those values can be found in the render_button methods 141 | args.state.buttons.left = [450, 600, 50, 50] 142 | args.state.buttons.center = [500, 600, 200, 50] 143 | args.state.buttons.right = [700, 600, 50, 50] 144 | 145 | # The variables below are related to the slider 146 | # They allow the user to customize them 147 | # They also give a central location for the render and input methods to get 148 | # information from 149 | # x & y are the coordinates of the leftmost part of the slider line 150 | args.state.slider.x = 400 151 | args.state.slider.y = 675 152 | # This is the width of the line 153 | args.state.slider.w = 360 154 | # This is the offset for the circle 155 | # Allows the center of the circle to be on the line, 156 | # as opposed to the upper right corner 157 | args.state.slider.offset = 20 158 | # This is the spacing between each of the notches on the slider 159 | # Notches are places where the circle can rest on the slider line 160 | # There needs to be a notch for each step before the maximum number of steps 161 | args.state.slider.spacing = args.state.slider.w.to_f / args.state.max_steps.to_f 162 | end 163 | 164 | # This method is called every frame/tick 165 | # Every tick, the current state of the search is rendered on the screen, 166 | # User input is processed, and 167 | # The next step in the search is calculated 168 | def tick 169 | render 170 | input 171 | # If animation is playing, and max steps have not been reached 172 | # Move the search a step forward 173 | if state.play && state.anim_steps < state.max_steps 174 | # Variable that tells the program what step to recalculate up to 175 | state.anim_steps += 1 176 | calc 177 | end 178 | end 179 | 180 | # Draws everything onto the screen 181 | def render 182 | render_buttons 183 | render_slider 184 | 185 | render_background 186 | render_visited 187 | render_frontier 188 | render_walls 189 | render_star 190 | end 191 | 192 | # The methods below subdivide the task of drawing everything to the screen 193 | 194 | # Draws the buttons that control the animation step and state 195 | def render_buttons 196 | render_left_button 197 | render_center_button 198 | render_right_button 199 | end 200 | 201 | # Draws the button which steps the search backward 202 | # Shows the user where to click to move the search backward 203 | def render_left_button 204 | # Draws the gray button, and a black border 205 | # The border separates the buttons visually 206 | outputs.solids << [buttons.left, gray] 207 | outputs.borders << [buttons.left, black] 208 | 209 | # Renders an explanatory label in the center of the button 210 | # Explains to the user what the button does 211 | # If the button size is changed, the label might need to be edited as well 212 | # to keep the label in the center of the button 213 | label_x = buttons.left.x + 20 214 | label_y = buttons.left.y + 35 215 | outputs.labels << [label_x, label_y, "<"] 216 | end 217 | 218 | def render_center_button 219 | # Draws the gray button, and a black border 220 | # The border separates the buttons visually 221 | outputs.solids << [buttons.center, gray] 222 | outputs.borders << [buttons.center, black] 223 | 224 | # Renders an explanatory label in the center of the button 225 | # Explains to the user what the button does 226 | # If the button size is changed, the label might need to be edited as well 227 | # to keep the label in the center of the button 228 | label_x = buttons.center.x + 37 229 | label_y = buttons.center.y + 35 230 | label_text = state.play ? "Pause Animation" : "Play Animation" 231 | outputs.labels << [label_x, label_y, label_text] 232 | end 233 | 234 | def render_right_button 235 | # Draws the gray button, and a black border 236 | # The border separates the buttons visually 237 | outputs.solids << [buttons.right, gray] 238 | outputs.borders << [buttons.right, black] 239 | 240 | # Renders an explanatory label in the center of the button 241 | # Explains to the user what the button does 242 | label_x = buttons.right.x + 20 243 | label_y = buttons.right.y + 35 244 | outputs.labels << [label_x, label_y, ">"] 245 | end 246 | 247 | # Draws the slider so the user can move it and see the progress of the search 248 | def render_slider 249 | # Using primitives hides the line under the white circle of the slider 250 | # Draws the line 251 | outputs.primitives << [slider.x, slider.y, slider.x + slider.w, slider.y].line 252 | # The circle needs to be offset so that the center of the circle 253 | # overlaps the line instead of the upper right corner of the circle 254 | # The circle's x value is also moved based on the current seach step 255 | circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing) 256 | circle_y = (slider.y - slider.offset) 257 | circle_rect = [circle_x, circle_y, 37, 37] 258 | outputs.primitives << [circle_rect, 'circle-white.png'].sprite 259 | end 260 | 261 | # Draws what the grid looks like with nothing on it 262 | def render_background 263 | render_unvisited 264 | render_grid_lines 265 | end 266 | 267 | # Draws a rectangle the size of the entire grid to represent unvisited cells 268 | def render_unvisited 269 | outputs.solids << [scale_up([0, 0, grid.width, grid.height]), unvisited_color] 270 | end 271 | 272 | # Draws grid lines to show the division of the grid into cells 273 | def render_grid_lines 274 | for x in 0..grid.width 275 | outputs.lines << vertical_line(x) 276 | end 277 | 278 | for y in 0..grid.height 279 | outputs.lines << horizontal_line(y) 280 | end 281 | end 282 | 283 | # Easy way to draw vertical lines given an index 284 | def vertical_line column 285 | scale_up([column, 0, column, grid.height]) 286 | end 287 | 288 | # Easy way to draw horizontal lines given an index 289 | def horizontal_line row 290 | scale_up([0, row, grid.width, row]) 291 | end 292 | 293 | # Draws the area that is going to be searched from 294 | # The frontier is the most outward parts of the search 295 | def render_frontier 296 | outputs.solids << state.frontier.map do |cell| 297 | [scale_up(cell), frontier_color] 298 | end 299 | end 300 | 301 | # Draws the walls 302 | def render_walls 303 | outputs.solids << state.walls.map do |wall| 304 | [scale_up(wall), wall_color] 305 | end 306 | end 307 | 308 | # Renders cells that have been searched in the appropriate color 309 | def render_visited 310 | outputs.solids << state.visited.map do |cell| 311 | [scale_up(cell), visited_color] 312 | end 313 | end 314 | 315 | # Renders the star 316 | def render_star 317 | outputs.sprites << [scale_up(state.star), 'star.png'] 318 | end 319 | 320 | # In code, the cells are represented as 1x1 rectangles 321 | # When drawn, the cells are larger than 1x1 rectangles 322 | # This method is used to scale up cells, and lines 323 | # Objects are scaled up according to the grid.cell_size variable 324 | # This allows for easy customization of the visual scale of the grid 325 | def scale_up(cell) 326 | # Prevents the original value of cell from being edited 327 | cell = cell.clone 328 | 329 | # If cell is just an x and y coordinate 330 | if cell.size == 2 331 | # Add a width and height of 1 332 | cell << 1 333 | cell << 1 334 | end 335 | 336 | # Scale all the values up 337 | cell.map! { |value| value * grid.cell_size } 338 | 339 | # Returns the scaled up cell 340 | cell 341 | end 342 | 343 | # This method processes user input every tick 344 | # This method allows the user to use the buttons, slider, and edit the grid 345 | # There are 2 types of input: 346 | # Button Input 347 | # Click and Drag Input 348 | # 349 | # Button Input is used for the backward step and forward step buttons 350 | # Input is detected by mouse up within the bounds of the rect 351 | # 352 | # Click and Drag Input is used for moving the star, adding walls, 353 | # removing walls, and the slider 354 | # 355 | # When the mouse is down on the star, the click_and_drag variable is set to :star 356 | # While click_and_drag equals :star, the cursor's position is used to calculate the 357 | # appropriate drag behavior 358 | # 359 | # When the mouse goes up click_and_drag is set to :none 360 | # 361 | # A variable has to be used because the star has to continue being edited even 362 | # when the cursor is no longer over the star 363 | # 364 | # Similar things occur for the other Click and Drag inputs 365 | def input 366 | # Checks whether any of the buttons are being clicked 367 | input_buttons 368 | 369 | # The detection and processing of click and drag inputs are separate 370 | # The program has to remember that the user is dragging an object 371 | # even when the mouse is no longer over that object 372 | detect_click_and_drag 373 | process_click_and_drag 374 | end 375 | 376 | # Detects and Process input for each button 377 | def input_buttons 378 | input_left_button 379 | input_center_button 380 | input_next_step_button 381 | end 382 | 383 | # Checks if the previous step button is clicked 384 | # If it is, it pauses the animation and moves the search one step backward 385 | def input_left_button 386 | if left_button_clicked? 387 | state.play = false 388 | state.anim_steps -= 1 389 | recalculate 390 | end 391 | end 392 | 393 | # Controls the play/pause button 394 | # Inverses whether the animation is playing or not when clicked 395 | def input_center_button 396 | if center_button_clicked? or inputs.keyboard.key_down.space 397 | state.play = !state.play 398 | end 399 | end 400 | 401 | # Checks if the next step button is clicked 402 | # If it is, it pauses the animation and moves the search one step forward 403 | def input_next_step_button 404 | if right_button_clicked? 405 | state.play = false 406 | state.anim_steps += 1 407 | calc 408 | end 409 | end 410 | 411 | # Determines what the user is editing and stores the value 412 | # Storing the value allows the user to continue the same edit as long as the 413 | # mouse left click is held 414 | def detect_click_and_drag 415 | if inputs.mouse.up 416 | state.click_and_drag = :none 417 | elsif star_clicked? 418 | state.click_and_drag = :star 419 | elsif wall_clicked? 420 | state.click_and_drag = :remove_wall 421 | elsif grid_clicked? 422 | state.click_and_drag = :add_wall 423 | elsif slider_clicked? 424 | state.click_and_drag = :slider 425 | end 426 | end 427 | 428 | # Processes click and drag based on what the user is currently dragging 429 | def process_click_and_drag 430 | if state.click_and_drag == :star 431 | input_star 432 | elsif state.click_and_drag == :remove_wall 433 | input_remove_wall 434 | elsif state.click_and_drag == :add_wall 435 | input_add_wall 436 | elsif state.click_and_drag == :slider 437 | input_slider 438 | end 439 | end 440 | 441 | # Moves the star to the grid closest to the mouse 442 | # Only recalculates the search if the star changes position 443 | # Called whenever the user is editing the star (puts mouse down on star) 444 | def input_star 445 | old_star = state.star.clone 446 | state.star = cell_closest_to_mouse 447 | unless old_star == state.star 448 | recalculate 449 | end 450 | end 451 | 452 | # Removes walls that are under the cursor 453 | def input_remove_wall 454 | # The mouse needs to be inside the grid, because we only want to remove walls 455 | # the cursor is directly over 456 | # Recalculations should only occur when a wall is actually deleted 457 | if mouse_inside_grid? 458 | if state.walls.has_key?(cell_closest_to_mouse) 459 | state.walls.delete(cell_closest_to_mouse) 460 | recalculate 461 | end 462 | end 463 | end 464 | 465 | # Adds walls at cells under the cursor 466 | def input_add_wall 467 | if mouse_inside_grid? 468 | unless state.walls.has_key?(cell_closest_to_mouse) 469 | state.walls[cell_closest_to_mouse] = true 470 | recalculate 471 | end 472 | end 473 | end 474 | 475 | # This method is called when the user is editing the slider 476 | # It pauses the animation and moves the white circle to the closest integer point 477 | # on the slider 478 | # Changes the step of the search to be animated 479 | def input_slider 480 | state.play = false 481 | mouse_x = inputs.mouse.point.x 482 | 483 | # Bounds the mouse_x to the closest x value on the slider line 484 | mouse_x = slider.x if mouse_x < slider.x 485 | mouse_x = slider.x + slider.w if mouse_x > slider.x + slider.w 486 | 487 | # Sets the current search step to the one represented by the mouse x value 488 | # The slider's circle moves due to the render_slider method using anim_steps 489 | state.anim_steps = ((mouse_x - slider.x) / slider.spacing).to_i 490 | 491 | recalculate 492 | end 493 | 494 | # Whenever the user edits the grid, 495 | # The search has to be recalculated upto the current step 496 | # with the current grid as the initial state of the grid 497 | def recalculate 498 | # Resets the search 499 | state.frontier = [] 500 | state.visited = {} 501 | 502 | # Moves the animation forward one step at a time 503 | state.anim_steps.times { calc } 504 | end 505 | 506 | 507 | # This method moves the search forward one step 508 | # When the animation is playing it is called every tick 509 | # And called whenever the current step of the animation needs to be recalculated 510 | 511 | # Moves the search forward one step 512 | # Parameter called_from_tick is true if it is called from the tick method 513 | # It is false when the search is being recalculated after user editing the grid 514 | def calc 515 | 516 | # The setup to the search 517 | # Runs once when the there is no frontier or visited cells 518 | if state.frontier.empty? && state.visited.empty? 519 | state.frontier << state.star 520 | state.visited[state.star] = true 521 | end 522 | 523 | # A step in the search 524 | unless state.frontier.empty? 525 | # Takes the next frontier cell 526 | new_frontier = state.frontier.shift 527 | # For each of its neighbors 528 | adjacent_neighbors(new_frontier).each do |neighbor| 529 | # That have not been visited and are not walls 530 | unless state.visited.has_key?(neighbor) || state.walls.has_key?(neighbor) 531 | # Add them to the frontier and mark them as visited 532 | state.frontier << neighbor 533 | state.visited[neighbor] = true 534 | end 535 | end 536 | end 537 | end 538 | 539 | 540 | # Returns a list of adjacent cells 541 | # Used to determine what the next cells to be added to the frontier are 542 | def adjacent_neighbors(cell) 543 | neighbors = [] 544 | 545 | neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1 546 | neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1 547 | neighbors << [cell.x, cell.y - 1] unless cell.y == 0 548 | neighbors << [cell.x - 1, cell.y] unless cell.x == 0 549 | 550 | neighbors 551 | end 552 | 553 | # When the user grabs the star and puts their cursor to the far right 554 | # and moves up and down, the star is supposed to move along the grid as well 555 | # Finding the cell closest to the mouse helps with this 556 | def cell_closest_to_mouse 557 | # Closest cell to the mouse 558 | x = (inputs.mouse.point.x / grid.cell_size).to_i 559 | y = (inputs.mouse.point.y / grid.cell_size).to_i 560 | # Bound x and y to the grid 561 | x = grid.width - 1 if x > grid.width - 1 562 | y = grid.height - 1 if y > grid.height - 1 563 | # Return closest cell 564 | [x, y] 565 | end 566 | 567 | # These methods detect when the buttons are clicked 568 | def left_button_clicked? 569 | inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.left) 570 | end 571 | 572 | def center_button_clicked? 573 | inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.center) 574 | end 575 | 576 | def right_button_clicked? 577 | inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.right) 578 | end 579 | 580 | # Signal that the user is going to be moving the slider 581 | # Is the mouse down on the circle of the slider? 582 | def slider_clicked? 583 | circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing) 584 | circle_y = (slider.y - slider.offset) 585 | circle_rect = [circle_x, circle_y, 37, 37] 586 | inputs.mouse.down && inputs.mouse.point.inside_rect?(circle_rect) 587 | end 588 | 589 | # Signal that the user is going to be moving the star 590 | def star_clicked? 591 | inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.star)) 592 | end 593 | 594 | # Signal that the user is going to be removing walls 595 | def wall_clicked? 596 | inputs.mouse.down && mouse_inside_a_wall? 597 | end 598 | 599 | # Signal that the user is going to be adding walls 600 | def grid_clicked? 601 | inputs.mouse.down && mouse_inside_grid? 602 | end 603 | 604 | # Returns whether the mouse is inside of a wall 605 | # Part of the condition that checks whether the user is removing a wall 606 | def mouse_inside_a_wall? 607 | state.walls.each_key do | wall | 608 | return true if inputs.mouse.point.inside_rect?(scale_up(wall)) 609 | end 610 | 611 | false 612 | end 613 | 614 | # Returns whether the mouse is inside of a grid 615 | # Part of the condition that checks whether the user is adding a wall 616 | def mouse_inside_grid? 617 | inputs.mouse.point.inside_rect?(scale_up([0, 0, grid.width, grid.height])) 618 | end 619 | 620 | 621 | # These methods provide handy aliases to colors 622 | 623 | # Light brown 624 | def unvisited_color 625 | [221, 212, 213] 626 | end 627 | 628 | # Black 629 | def grid_line_color 630 | [255, 255, 255] 631 | end 632 | 633 | # Dark Brown 634 | def visited_color 635 | [204, 191, 179] 636 | end 637 | 638 | # Blue 639 | def frontier_color 640 | [103, 136, 204] 641 | end 642 | 643 | # Camo Green 644 | def wall_color 645 | [134, 134, 120] 646 | end 647 | 648 | # Button Background 649 | def gray 650 | [190, 190, 190] 651 | end 652 | 653 | # Button Outline 654 | def black 655 | [0, 0, 0] 656 | end 657 | 658 | # These methods make the code more concise 659 | def grid 660 | state.grid 661 | end 662 | 663 | def buttons 664 | state.buttons 665 | end 666 | 667 | def slider 668 | state.slider 669 | end 670 | end 671 | 672 | # Method that is called by DragonRuby periodically 673 | # Used for updating animations and calculations 674 | def tick args 675 | 676 | # Pressing r will reset the application 677 | if args.inputs.keyboard.key_down.r 678 | args.gtk.reset 679 | reset 680 | return 681 | end 682 | 683 | # Every tick, new args are passed, and the Breadth First Search tick is called 684 | $breadth_first_search ||= BreadthFirstSearch.new(args) 685 | $breadth_first_search.args = args 686 | $breadth_first_search.tick 687 | end 688 | 689 | 690 | def reset 691 | $breadth_first_search = nil 692 | end 693 | -------------------------------------------------------------------------------- /Breadth_First_Search/circle-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Breadth_First_Search/circle-white.png -------------------------------------------------------------------------------- /Breadth_First_Search/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Breadth_First_Search/star.png -------------------------------------------------------------------------------- /Detailed_Breadth_First_Search/app/main.rb: -------------------------------------------------------------------------------- 1 | # A visual demonstration of a breadth first search 2 | # Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html 3 | 4 | # An animation that can respond to user input in real time 5 | 6 | # A breadth first search expands in all directions one step at a time 7 | # The frontier is a queue of cells to be expanded from 8 | # The visited hash allows quick lookups of cells that have been expanded from 9 | # The walls hash allows quick lookup of whether a cell is a wall 10 | 11 | # The breadth first search starts by adding the red star to the frontier array 12 | # and marking it as visited 13 | # Each step a cell is removed from the front of the frontier array (queue) 14 | # Unless the neighbor is a wall or visited, it is added to the frontier array 15 | # The neighbor is then marked as visited 16 | 17 | # The frontier is blue 18 | # Visited cells are light brown 19 | # Walls are camo green 20 | # Even when walls are visited, they will maintain their wall color 21 | 22 | # This search numbers the order in which new cells are explored 23 | # The next cell from where the search will continue is highlighted yellow 24 | # And the cells that will be considered for expansion are in semi-transparent green 25 | 26 | # The star can be moved by clicking and dragging 27 | # Walls can be added and removed by clicking and dragging 28 | 29 | class DetailedBreadthFirstSearch 30 | attr_gtk 31 | 32 | def initialize(args) 33 | # Variables to edit the size and appearance of the grid 34 | # Freely customizable to user's liking 35 | args.state.grid.width = 9 36 | args.state.grid.height = 4 37 | args.state.grid.cell_size = 90 38 | 39 | # Stores which step of the animation is being rendered 40 | # When the user moves the star or messes with the walls, 41 | # the breadth first search is recalculated up to this step 42 | args.state.anim_steps = 0 43 | 44 | # At some step the animation will end, 45 | # and further steps won't change anything (the whole grid will be explored) 46 | # This step is roughly the grid's width * height 47 | # When anim_steps equals max_steps no more calculations will occur 48 | # and the slider will be at the end 49 | args.state.max_steps = args.state.grid.width * args.state.grid.height 50 | 51 | # The location of the star and walls of the grid 52 | # They can be modified to have a different initial grid 53 | # Walls are stored in a hash for quick look up when doing the search 54 | args.state.star = [3, 2] 55 | args.state.walls = {} 56 | 57 | # Variables that are used by the breadth first search 58 | # Storing cells that the search has visited, prevents unnecessary steps 59 | # Expanding the frontier of the search in order makes the search expand 60 | # from the center outward 61 | args.state.visited = {} 62 | args.state.frontier = [] 63 | args.state.cell_numbers = [] 64 | 65 | 66 | 67 | # What the user is currently editing on the grid 68 | # Possible values are: :none, :slider, :star, :remove_wall, :add_wall 69 | 70 | # We store this value, because we want to remember the value even when 71 | # the user's cursor is no longer over what they're interacting with, but 72 | # they are still clicking down on the mouse. 73 | args.state.click_and_drag = :none 74 | 75 | # The x, y, w, h values for the buttons 76 | # Allow easy movement of the buttons location 77 | # A centralized location to get values to detect input and draw the buttons 78 | # Editing these values might mean needing to edit the label offsets 79 | # which can be found in the appropriate render button methods 80 | args.state.buttons.left = [450, 600, 160, 50] 81 | args.state.buttons.right = [610, 600, 160, 50] 82 | 83 | # The variables below are related to the slider 84 | # They allow the user to customize them 85 | # They also give a central location for the render and input methods to get 86 | # information from 87 | # x & y are the coordinates of the leftmost part of the slider line 88 | args.state.slider.x = 400 89 | args.state.slider.y = 675 90 | # This is the width of the line 91 | args.state.slider.w = 360 92 | # This is the offset for the circle 93 | # Allows the center of the circle to be on the line, 94 | # as opposed to the upper right corner 95 | args.state.slider.offset = 20 96 | # This is the spacing between each of the notches on the slider 97 | # Notches are places where the circle can rest on the slider line 98 | # There needs to be a notch for each step before the maximum number of steps 99 | args.state.slider.spacing = args.state.slider.w.to_f / args.state.max_steps.to_f 100 | end 101 | 102 | # This method is called every frame/tick 103 | # Every tick, the current state of the search is rendered on the screen, 104 | # User input is processed, and 105 | def tick 106 | render 107 | input 108 | end 109 | 110 | # This method is called from tick and renders everything every tick 111 | def render 112 | render_buttons 113 | render_slider 114 | 115 | render_background 116 | render_visited 117 | render_frontier 118 | render_walls 119 | render_star 120 | 121 | render_highlights 122 | render_cell_numbers 123 | end 124 | 125 | # The methods below subdivide the task of drawing everything to the screen 126 | 127 | # Draws the buttons that move the search backward or forward 128 | # These buttons are rendered so the user knows where to click to move the search 129 | def render_buttons 130 | render_left_button 131 | render_right_button 132 | end 133 | 134 | # Renders the button which steps the search backward 135 | # Shows the user where to click to move the search backward 136 | def render_left_button 137 | # Draws the gray button, and a black border 138 | # The border separates the buttons visually 139 | outputs.solids << [buttons.left, gray] 140 | outputs.borders << [buttons.left, black] 141 | 142 | # Renders an explanatory label in the center of the button 143 | # Explains to the user what the button does 144 | label_x = buttons.left.x + 05 145 | label_y = buttons.left.y + 35 146 | outputs.labels << [label_x, label_y, "< Step backward"] 147 | end 148 | 149 | # Renders the button which steps the search forward 150 | # Shows the user where to click to move the search forward 151 | def render_right_button 152 | # Draws the gray button, and a black border 153 | # The border separates the buttons visually 154 | outputs.solids << [buttons.right, gray] 155 | outputs.borders << [buttons.right, black] 156 | 157 | # Renders an explanatory label in the center of the button 158 | # Explains to the user what the button does 159 | label_x = buttons.right.x + 10 160 | label_y = buttons.right.y + 35 161 | outputs.labels << [label_x, label_y, "Step forward >"] 162 | end 163 | 164 | # Draws the slider so the user can move it and see the progress of the search 165 | def render_slider 166 | # Using primitives hides the line under the white circle of the slider 167 | # Draws the line 168 | outputs.primitives << [slider.x, slider.y, slider.x + slider.w, slider.y].line 169 | # The circle needs to be offset so that the center of the circle 170 | # overlaps the line instead of the upper right corner of the circle 171 | # The circle's x value is also moved based on the current seach step 172 | circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing) 173 | circle_y = (slider.y - slider.offset) 174 | circle_rect = [circle_x, circle_y, 37, 37] 175 | outputs.primitives << [circle_rect, 'circle-white.png'].sprite 176 | end 177 | 178 | # Draws what the grid looks like with nothing on it 179 | # Which is a bunch of unvisited cells 180 | # Drawn first so other things can draw on top of it 181 | def render_background 182 | render_unvisited 183 | 184 | # The grid lines make the cells appear separate 185 | render_grid_lines 186 | end 187 | 188 | # Draws a rectangle the size of the entire grid to represent unvisited cells 189 | # Unvisited cells are the default cell 190 | def render_unvisited 191 | background = [0, 0, grid.width, grid.height] 192 | outputs.solids << [scale_up(background), unvisited_color] 193 | end 194 | 195 | # Draws grid lines to show the division of the grid into cells 196 | def render_grid_lines 197 | for x in 0..grid.width 198 | outputs.lines << [scale_up(vertical_line(x)), grid_line_color] 199 | end 200 | 201 | for y in 0..grid.height 202 | outputs.lines << [scale_up(horizontal_line(y)), grid_line_color] 203 | end 204 | end 205 | 206 | # Easy way to get a vertical line given an index 207 | def vertical_line column 208 | [column, 0, column, grid.height] 209 | end 210 | 211 | # Easy way to get a horizontal line given an index 212 | def horizontal_line row 213 | [0, row, grid.width, row] 214 | end 215 | 216 | # Draws the area that is going to be searched from 217 | # The frontier is the most outward parts of the search 218 | def render_frontier 219 | state.frontier.each do |cell| 220 | outputs.solids << [scale_up(cell), frontier_color] 221 | end 222 | end 223 | 224 | # Draws the walls 225 | def render_walls 226 | state.walls.each_key do |wall| 227 | outputs.solids << [scale_up(wall), wall_color] 228 | end 229 | end 230 | 231 | # Renders cells that have been searched in the appropriate color 232 | def render_visited 233 | state.visited.each_key do |cell| 234 | outputs.solids << [scale_up(cell), visited_color] 235 | end 236 | end 237 | 238 | # Renders the star 239 | def render_star 240 | outputs.sprites << [scale_up(state.star), 'star.png'] 241 | end 242 | 243 | # Cells have a number rendered in them based on when they were explored 244 | # This is based off of their index in the cell_numbers array 245 | # Cells are added to this array the same time they are added to the frontier array 246 | def render_cell_numbers 247 | state.cell_numbers.each_with_index do |cell, index| 248 | # Math that approx centers the number in the cell 249 | label_x = (cell.x * grid.cell_size) + grid.cell_size / 2 - 5 250 | label_y = (cell.y * grid.cell_size) + (grid.cell_size / 2) + 5 251 | 252 | outputs.labels << [label_x, label_y, (index + 1).to_s] 253 | end 254 | end 255 | 256 | # The next frontier to be expanded is highlighted yellow 257 | # Its adjacent non-wall neighbors have their border highlighted green 258 | # This is to show the user how the search expands 259 | def render_highlights 260 | return if state.frontier.empty? 261 | 262 | # Highlight the next frontier to be expanded yellow 263 | next_frontier = state.frontier[0] 264 | outputs.solids << [scale_up(next_frontier), highlighter_yellow] 265 | 266 | # Neighbors have a semi-transparent green layer over them 267 | # Unless the neighbor is a wall 268 | adjacent_neighbors(next_frontier).each do |neighbor| 269 | unless state.walls.has_key?(neighbor) 270 | outputs.solids << [scale_up(neighbor), highlighter_green, 70] 271 | end 272 | end 273 | end 274 | 275 | 276 | # Cell Size is used when rendering to allow the grid to be scaled up or down 277 | # Cells in the frontier array and visited hash and walls hash are stored as x & y 278 | # Scaling up cells and lines when rendering allows omitting of width and height 279 | def scale_up(cell) 280 | # Prevents the original value of cell from being edited 281 | cell = cell.clone 282 | 283 | # If cell is just an x and y coordinate 284 | if cell.size == 2 285 | # Add a width and height of 1 286 | cell << 1 287 | cell << 1 288 | end 289 | 290 | # Scale all the values up 291 | cell.map! { |value| value * grid.cell_size } 292 | 293 | # Returns the scaled up cell 294 | cell 295 | end 296 | 297 | 298 | # This method processes user input every tick 299 | # This method allows the user to use the buttons, slider, and edit the grid 300 | # There are 2 types of input: 301 | # Button Input 302 | # Click and Drag Input 303 | # 304 | # Button Input is used for the backward step and forward step buttons 305 | # Input is detected by mouse up within the bounds of the rect 306 | # 307 | # Click and Drag Input is used for moving the star, adding walls, 308 | # removing walls, and the slider 309 | # 310 | # When the mouse is down on the star, the click_and_drag variable is set to :star 311 | # While click_and_drag equals :star, the cursor's position is used to calculate the 312 | # appropriate drag behavior 313 | # 314 | # When the mouse goes up click_and_drag is set to :none 315 | # 316 | # A variable has to be used because the star has to continue being edited even 317 | # when the cursor is no longer over the star 318 | # 319 | # Similar things occur for the other Click and Drag inputs 320 | def input 321 | # Processes inputs for the buttons 322 | input_buttons 323 | 324 | # Detects which if any click and drag input is occurring 325 | detect_click_and_drag 326 | 327 | # Does the appropriate click and drag input based on the click_and_drag variable 328 | process_click_and_drag 329 | end 330 | 331 | # Detects and Process input for each button 332 | def input_buttons 333 | input_left_button 334 | input_right_button 335 | end 336 | 337 | # Checks if the previous step button is clicked 338 | # If it is, it pauses the animation and moves the search one step backward 339 | def input_left_button 340 | if left_button_clicked? 341 | unless state.anim_steps == 0 342 | state.anim_steps -= 1 343 | recalculate 344 | end 345 | end 346 | end 347 | 348 | # Checks if the next step button is clicked 349 | # If it is, it pauses the animation and moves the search one step forward 350 | def input_right_button 351 | if right_button_clicked? 352 | unless state.anim_steps == state.max_steps 353 | state.anim_steps += 1 354 | # Although normally recalculate would be called here 355 | # because the right button only moves the search forward 356 | # We can just do that 357 | calc 358 | end 359 | end 360 | end 361 | 362 | # Whenever the user edits the grid, 363 | # The search has to be recalculated upto the current step 364 | 365 | def recalculate 366 | # Resets the search 367 | state.frontier = [] 368 | state.visited = {} 369 | state.cell_numbers = [] 370 | 371 | # Moves the animation forward one step at a time 372 | state.anim_steps.times { calc } 373 | end 374 | 375 | 376 | # Determines what the user is clicking and planning on dragging 377 | # Click and drag input is initiated by a click on the appropriate item 378 | # and ended by mouse up 379 | # Storing the value allows the user to continue the same edit as long as the 380 | # mouse left click is held 381 | def detect_click_and_drag 382 | if inputs.mouse.up 383 | state.click_and_drag = :none 384 | elsif star_clicked? 385 | state.click_and_drag = :star 386 | elsif wall_clicked? 387 | state.click_and_drag = :remove_wall 388 | elsif grid_clicked? 389 | state.click_and_drag = :add_wall 390 | elsif slider_clicked? 391 | state.click_and_drag = :slider 392 | end 393 | end 394 | 395 | # Processes input based on what the user is currently dragging 396 | def process_click_and_drag 397 | if state.click_and_drag == :slider 398 | input_slider 399 | elsif state.click_and_drag == :star 400 | input_star 401 | elsif state.click_and_drag == :remove_wall 402 | input_remove_wall 403 | elsif state.click_and_drag == :add_wall 404 | input_add_wall 405 | end 406 | end 407 | 408 | # This method is called when the user is dragging the slider 409 | # It moves the current animation step to the point represented by the slider 410 | def input_slider 411 | mouse_x = inputs.mouse.point.x 412 | 413 | # Bounds the mouse_x to the closest x value on the slider line 414 | mouse_x = slider.x if mouse_x < slider.x 415 | mouse_x = slider.x + slider.w if mouse_x > slider.x + slider.w 416 | 417 | # Sets the current search step to the one represented by the mouse x value 418 | # The slider's circle moves due to the render_slider method using anim_steps 419 | state.anim_steps = ((mouse_x - slider.x) / slider.spacing).to_i 420 | 421 | recalculate 422 | end 423 | 424 | # Moves the star to the grid closest to the mouse 425 | # Only recalculates the search if the star changes position 426 | # Called whenever the user is dragging the star 427 | def input_star 428 | old_star = state.star.clone 429 | state.star = cell_closest_to_mouse 430 | unless old_star == state.star 431 | recalculate 432 | end 433 | end 434 | 435 | # Removes walls that are under the cursor 436 | def input_remove_wall 437 | # The mouse needs to be inside the grid, because we only want to remove walls 438 | # the cursor is directly over 439 | # Recalculations should only occur when a wall is actually deleted 440 | if mouse_inside_grid? 441 | if state.walls.has_key?(cell_closest_to_mouse) 442 | state.walls.delete(cell_closest_to_mouse) 443 | recalculate 444 | end 445 | end 446 | end 447 | 448 | # Adds walls at cells under the cursor 449 | def input_add_wall 450 | # Adds a wall to the hash 451 | # We can use the grid closest to mouse, because the cursor is inside the grid 452 | if mouse_inside_grid? 453 | unless state.walls.has_key?(cell_closest_to_mouse) 454 | state.walls[cell_closest_to_mouse] = true 455 | recalculate 456 | end 457 | end 458 | end 459 | 460 | # This method moves the search forward one step 461 | # When the animation is playing it is called every tick 462 | # And called whenever the current step of the animation needs to be recalculated 463 | 464 | # Moves the search forward one step 465 | # Parameter called_from_tick is true if it is called from the tick method 466 | # It is false when the search is being recalculated after user editing the grid 467 | def calc 468 | # The setup to the search 469 | # Runs once when the there is no frontier or visited cells 470 | if state.frontier.empty? && state.visited.empty? 471 | state.frontier << state.star 472 | state.visited[state.star] = true 473 | end 474 | 475 | # A step in the search 476 | unless state.frontier.empty? 477 | # Takes the next frontier cell 478 | new_frontier = state.frontier.shift 479 | # For each of its neighbors 480 | adjacent_neighbors(new_frontier).each do |neighbor| 481 | # That have not been visited and are not walls 482 | unless state.visited.has_key?(neighbor) || state.walls.has_key?(neighbor) 483 | # Add them to the frontier and mark them as visited 484 | state.frontier << neighbor 485 | state.visited[neighbor] = true 486 | 487 | # Also assign them a frontier number 488 | state.cell_numbers << neighbor 489 | end 490 | end 491 | end 492 | end 493 | 494 | 495 | # Returns a list of adjacent cells 496 | # Used to determine what the next cells to be added to the frontier are 497 | def adjacent_neighbors cell 498 | neighbors = [] 499 | 500 | neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1 501 | neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1 502 | neighbors << [cell.x, cell.y - 1] unless cell.y == 0 503 | neighbors << [cell.x - 1, cell.y] unless cell.x == 0 504 | 505 | neighbors 506 | end 507 | 508 | # When the user grabs the star and puts their cursor to the far right 509 | # and moves up and down, the star is supposed to move along the grid as well 510 | # Finding the grid closest to the mouse helps with this 511 | def cell_closest_to_mouse 512 | x = (inputs.mouse.point.x / grid.cell_size).to_i 513 | y = (inputs.mouse.point.y / grid.cell_size).to_i 514 | x = grid.width - 1 if x > grid.width - 1 515 | y = grid.height - 1 if y > grid.height - 1 516 | [x, y] 517 | end 518 | 519 | 520 | # These methods detect when the buttons are clicked 521 | def left_button_clicked? 522 | (inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.left)) || inputs.keyboard.key_up.left 523 | end 524 | 525 | def right_button_clicked? 526 | (inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.right)) || inputs.keyboard.key_up.right 527 | end 528 | 529 | # Signal that the user is going to be moving the slider 530 | def slider_clicked? 531 | circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing) 532 | circle_y = (slider.y - slider.offset) 533 | circle_rect = [circle_x, circle_y, 37, 37] 534 | inputs.mouse.down && inputs.mouse.point.inside_rect?(circle_rect) 535 | end 536 | 537 | # Signal that the user is going to be moving the star 538 | def star_clicked? 539 | inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.star)) 540 | end 541 | 542 | # Signal that the user is going to be removing walls 543 | def wall_clicked? 544 | inputs.mouse.down && mouse_inside_a_wall? 545 | end 546 | 547 | # Signal that the user is going to be adding walls 548 | def grid_clicked? 549 | inputs.mouse.down && mouse_inside_grid? 550 | end 551 | 552 | # Returns whether the mouse is inside of a wall 553 | # Part of the condition that checks whether the user is removing a wall 554 | def mouse_inside_a_wall? 555 | state.walls.each_key do | wall | 556 | return true if inputs.mouse.point.inside_rect?(scale_up(wall)) 557 | end 558 | 559 | false 560 | end 561 | 562 | # Returns whether the mouse is inside of a grid 563 | # Part of the condition that checks whether the user is adding a wall 564 | def mouse_inside_grid? 565 | inputs.mouse.point.inside_rect?(scale_up([0, 0, grid.width, grid.height])) 566 | end 567 | 568 | # These methods provide handy aliases to colors 569 | 570 | # Light brown 571 | def unvisited_color 572 | [221, 212, 213] 573 | end 574 | 575 | # Black 576 | def grid_line_color 577 | [255, 255, 255] 578 | end 579 | 580 | # Dark Brown 581 | def visited_color 582 | [204, 191, 179] 583 | end 584 | 585 | # Blue 586 | def frontier_color 587 | [103, 136, 204] 588 | end 589 | 590 | # Camo Green 591 | def wall_color 592 | [134, 134, 120] 593 | end 594 | 595 | # Next frontier to be expanded 596 | def highlighter_yellow 597 | [214, 231, 125] 598 | end 599 | 600 | # The neighbors of the next frontier to be expanded 601 | def highlighter_green 602 | [65, 191, 127] 603 | end 604 | 605 | # Button background 606 | def gray 607 | [190, 190, 190] 608 | end 609 | 610 | # Button outline 611 | def black 612 | [0, 0, 0] 613 | end 614 | 615 | # These methods make the code more concise 616 | def grid 617 | state.grid 618 | end 619 | 620 | def buttons 621 | state.buttons 622 | end 623 | 624 | def slider 625 | state.slider 626 | end 627 | end 628 | 629 | 630 | def tick args 631 | # Pressing r resets the program 632 | if args.inputs.keyboard.key_down.r 633 | args.gtk.reset 634 | reset 635 | return 636 | end 637 | 638 | $detailed_breadth_first_search ||= DetailedBreadthFirstSearch.new(args) 639 | $detailed_breadth_first_search.args = args 640 | $detailed_breadth_first_search.tick 641 | end 642 | 643 | 644 | def reset 645 | $detailed_breadth_first_search = nil 646 | end 647 | -------------------------------------------------------------------------------- /Detailed_Breadth_First_Search/circle-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Detailed_Breadth_First_Search/circle-white.png -------------------------------------------------------------------------------- /Detailed_Breadth_First_Search/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Detailed_Breadth_First_Search/star.png -------------------------------------------------------------------------------- /Early_Exit_Breadth_First_Search/app/main.rb: -------------------------------------------------------------------------------- 1 | # Comparison of a breadth first search with and without early exit 2 | # Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html 3 | 4 | # Demonstrates the exploration difference caused by early exit 5 | # Also demonstrates how breadth first search is used for path generation 6 | 7 | # The left grid is a breadth first search without early exit 8 | # The right grid is a breadth first search with early exit 9 | # The red squares represent how far the search expanded 10 | # The darker the red, the farther the search proceeded 11 | # Comparison of the heat map reveals how much searching can be saved by early exit 12 | # The white path shows path generation via breadth first search 13 | class EarlyExitBreadthFirstSearch 14 | attr_gtk 15 | 16 | # This method is called every frame/tick 17 | # Every tick, the current state of the search is rendered on the screen, 18 | # User input is processed, and 19 | # The next step in the search is calculated 20 | def tick 21 | defaults 22 | # If the grid has not been searched 23 | if state.visited.empty? 24 | # Complete the search 25 | state.max_steps.times { step } 26 | # And calculate the path 27 | calc_path 28 | end 29 | render 30 | input 31 | end 32 | 33 | def defaults 34 | # Variables to edit the size and appearance of the grid 35 | # Freely customizable to user's liking 36 | grid.width ||= 15 37 | grid.height ||= 15 38 | grid.cell_size ||= 40 39 | grid.rect ||= [0, 0, grid.width, grid.height] 40 | 41 | # At some step the animation will end, 42 | # and further steps won't change anything (the whole grid.widthill be explored) 43 | # This step is roughly the grid's width * height 44 | # When anim_steps equals max_steps no more calculations will occur 45 | # and the slider will be at the end 46 | state.max_steps ||= args.state.grid.width * args.state.grid.height 47 | 48 | # The location of the star and walls of the grid 49 | # They can be modified to have a different initial grid 50 | # Walls are stored in a hash for quick look up when doing the search 51 | state.star ||= [2, 8] 52 | state.target ||= [10, 5] 53 | state.walls ||= {} 54 | 55 | # Variables that are used by the breadth first search 56 | # Storing cells that the search has visited, prevents unnecessary steps 57 | # Expanding the frontier of the search in order makes the search expand 58 | # from the center outward 59 | 60 | # Visited cells in the first grid 61 | state.visited ||= {} 62 | # Visited cells in the second grid 63 | state.early_exit_visited ||= {} 64 | # The cells from which the search is to expand 65 | state.frontier ||= [] 66 | # A hash of where each cell was expanded from 67 | # The key is a cell, and the value is the cell it came from 68 | state.came_from ||= {} 69 | # Cells that are part of the path from the target to the star 70 | state.path ||= {} 71 | 72 | # What the user is currently editing on the grid 73 | # We store this value, because we want to remember the value even when 74 | # the user's cursor is no longer over what they're interacting with, but 75 | # they are still clicking down on the mouse. 76 | state.current_input ||= :none 77 | end 78 | 79 | # Draws everything onto the screen 80 | def render 81 | render_background 82 | render_heat_map 83 | render_walls 84 | render_path 85 | render_star 86 | render_target 87 | render_labels 88 | end 89 | 90 | # The methods below subdivide the task of drawing everything to the screen 91 | 92 | # Draws what the grid looks like with nothing on it 93 | def render_background 94 | render_unvisited 95 | render_grid_lines 96 | end 97 | 98 | # Draws both grids 99 | def render_unvisited 100 | outputs.solids << [scale_up(grid.rect), unvisited_color] 101 | outputs.solids << [early_exit_scale_up(grid.rect), unvisited_color] 102 | end 103 | 104 | # Draws grid lines to show the division of the grid into cells 105 | def render_grid_lines 106 | for x in 0..grid.width 107 | outputs.lines << vertical_line(x) 108 | outputs.lines << early_exit_vertical_line(x) 109 | end 110 | 111 | for y in 0..grid.height 112 | outputs.lines << horizontal_line(y) 113 | outputs.lines << early_exit_horizontal_line(y) 114 | end 115 | end 116 | 117 | # Easy way to draw vertical lines given an index 118 | def vertical_line column 119 | scale_up([column, 0, column, grid.height]) 120 | end 121 | 122 | # Easy way to draw horizontal lines given an index 123 | def horizontal_line row 124 | scale_up([0, row, grid.width, row]) 125 | end 126 | 127 | # Easy way to draw vertical lines given an index 128 | def early_exit_vertical_line column 129 | scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height]) 130 | end 131 | 132 | # Easy way to draw horizontal lines given an index 133 | def early_exit_horizontal_line row 134 | scale_up([grid.width + 1, row, grid.width + grid.width + 1, row]) 135 | end 136 | 137 | # Draws the walls on both grids 138 | def render_walls 139 | state.walls.each_key do |wall| 140 | outputs.solids << [scale_up(wall), wall_color] 141 | outputs.solids << [early_exit_scale_up(wall), wall_color] 142 | end 143 | end 144 | 145 | # Renders the star on both grids 146 | def render_star 147 | outputs.sprites << [scale_up(state.star), 'star.png'] 148 | outputs.sprites << [early_exit_scale_up(state.star), 'star.png'] 149 | end 150 | 151 | # Renders the target on both grids 152 | def render_target 153 | outputs.sprites << [scale_up(state.target), 'target.png'] 154 | outputs.sprites << [early_exit_scale_up(state.target), 'target.png'] 155 | end 156 | 157 | # Labels the grids 158 | def render_labels 159 | outputs.labels << [200, 625, "Without early exit"] 160 | outputs.labels << [875, 625, "With early exit"] 161 | end 162 | 163 | # Renders the path based off of the state.path hash 164 | def render_path 165 | # If the star and target are disconnected there will only be one path 166 | # The path should not render in that case 167 | unless state.path.size == 1 168 | state.path.each_key do | cell | 169 | # Renders path on both grids 170 | outputs.solids << [scale_up(cell), path_color] 171 | outputs.solids << [early_exit_scale_up(cell), path_color] 172 | end 173 | end 174 | end 175 | 176 | # Calculates the path from the target to the star after the search is over 177 | # Relies on the came_from hash 178 | # Fills the state.path hash, which is later rendered on screen 179 | def calc_path 180 | endpoint = state.target 181 | while endpoint 182 | state.path[endpoint] = true 183 | endpoint = state.came_from[endpoint] 184 | end 185 | end 186 | 187 | # Representation of how far away visited cells are from the star 188 | # Replaces the render_visited method 189 | # Visually demonstrates the effectiveness of early exit for pathfinding 190 | def render_heat_map 191 | state.visited.each_key do | visited_cell | 192 | distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs 193 | max_distance = grid.width + grid.height 194 | alpha = 255.to_i * distance.to_i / max_distance.to_i 195 | outputs.solids << [scale_up(visited_cell), red, alpha] 196 | # outputs.solids << [early_exit_scale_up(visited_cell), red, alpha] 197 | end 198 | 199 | state.early_exit_visited.each_key do | visited_cell | 200 | distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs 201 | max_distance = grid.width + grid.height 202 | alpha = 255.to_i * distance.to_i / max_distance.to_i 203 | outputs.solids << [early_exit_scale_up(visited_cell), red, alpha] 204 | end 205 | end 206 | 207 | # Translates the given cell grid.width + 1 to the right and then scales up 208 | # Used to draw cells for the second grid 209 | # This method does not work for lines, 210 | # so separate methods exist for the grid lines 211 | def early_exit_scale_up(cell) 212 | cell_clone = cell.clone 213 | cell_clone.x += grid.width + 1 214 | scale_up(cell_clone) 215 | end 216 | 217 | # In code, the cells are represented as 1x1 rectangles 218 | # When drawn, the cells are larger than 1x1 rectangles 219 | # This method is used to scale up cells, and lines 220 | # Objects are scaled up according to the grid.cell_size variable 221 | # This allows for easy customization of the visual scale of the grid 222 | def scale_up(cell) 223 | # Prevents the original value of cell from being edited 224 | cell = cell.clone 225 | 226 | # If cell is just an x and y coordinate 227 | if cell.size == 2 228 | # Add a width and height of 1 229 | cell << 1 230 | cell << 1 231 | end 232 | 233 | # Scale all the values up 234 | cell.map! { |value| value * grid.cell_size } 235 | 236 | # Returns the scaled up cell 237 | cell 238 | end 239 | 240 | # This method processes user input every tick 241 | # Any method with "1" is related to the first grid 242 | # Any method with "2" is related to the second grid 243 | def input 244 | # The program has to remember that the user is dragging an object 245 | # even when the mouse is no longer over that object 246 | # So detecting input and processing input is separate 247 | detect_input 248 | process_input 249 | end 250 | 251 | # Determines what the user is editing and stores the value 252 | # Storing the value allows the user to continue the same edit as long as the 253 | # mouse left click is held 254 | def detect_input 255 | # When the mouse is up, nothing is being edited 256 | if inputs.mouse.up 257 | state.current_input = :none 258 | # When the star in the no second grid is clicked 259 | elsif star_clicked? 260 | state.current_input = :star 261 | # When the star in the second grid is clicked 262 | elsif star2_clicked? 263 | state.current_input = :star2 264 | # When the target in the no second grid is clicked 265 | elsif target_clicked? 266 | state.current_input = :target 267 | # When the target in the second grid is clicked 268 | elsif target2_clicked? 269 | state.current_input = :target2 270 | # When a wall in the first grid is clicked 271 | elsif wall_clicked? 272 | state.current_input = :remove_wall 273 | # When a wall in the second grid is clicked 274 | elsif wall2_clicked? 275 | state.current_input = :remove_wall2 276 | # When the first grid is clicked 277 | elsif grid_clicked? 278 | state.current_input = :add_wall 279 | # When the second grid is clicked 280 | elsif grid2_clicked? 281 | state.current_input = :add_wall2 282 | end 283 | end 284 | 285 | # Processes click and drag based on what the user is currently dragging 286 | def process_input 287 | if state.current_input == :star 288 | input_star 289 | elsif state.current_input == :star2 290 | input_star2 291 | elsif state.current_input == :target 292 | input_target 293 | elsif state.current_input == :target2 294 | input_target2 295 | elsif state.current_input == :remove_wall 296 | input_remove_wall 297 | elsif state.current_input == :remove_wall2 298 | input_remove_wall2 299 | elsif state.current_input == :add_wall 300 | input_add_wall 301 | elsif state.current_input == :add_wall2 302 | input_add_wall2 303 | end 304 | end 305 | 306 | # Moves the star to the cell closest to the mouse in the first grid 307 | # Only resets the search if the star changes position 308 | # Called whenever the user is editing the star (puts mouse down on star) 309 | def input_star 310 | old_star = state.star.clone 311 | state.star = cell_closest_to_mouse 312 | unless old_star == state.star 313 | reset_search 314 | end 315 | end 316 | 317 | # Moves the star to the cell closest to the mouse in the second grid 318 | # Only resets the search if the star changes position 319 | # Called whenever the user is editing the star (puts mouse down on star) 320 | def input_star2 321 | old_star = state.star.clone 322 | state.star = cell_closest_to_mouse2 323 | unless old_star == state.star 324 | reset_search 325 | end 326 | end 327 | 328 | # Moves the target to the grid closest to the mouse in the first grid 329 | # Only reset_searchs the search if the target changes position 330 | # Called whenever the user is editing the target (puts mouse down on target) 331 | def input_target 332 | old_target = state.target.clone 333 | state.target = cell_closest_to_mouse 334 | unless old_target == state.target 335 | reset_search 336 | end 337 | end 338 | 339 | # Moves the target to the cell closest to the mouse in the second grid 340 | # Only reset_searchs the search if the target changes position 341 | # Called whenever the user is editing the target (puts mouse down on target) 342 | def input_target2 343 | old_target = state.target.clone 344 | state.target = cell_closest_to_mouse2 345 | unless old_target == state.target 346 | reset_search 347 | end 348 | end 349 | 350 | # Removes walls in the first grid that are under the cursor 351 | def input_remove_wall 352 | # The mouse needs to be inside the grid, because we only want to remove walls 353 | # the cursor is directly over 354 | # Recalculations should only occur when a wall is actually deleted 355 | if mouse_inside_grid? 356 | if state.walls.has_key?(cell_closest_to_mouse) 357 | state.walls.delete(cell_closest_to_mouse) 358 | reset_search 359 | end 360 | end 361 | end 362 | 363 | # Removes walls in the second grid that are under the cursor 364 | def input_remove_wall2 365 | # The mouse needs to be inside the grid, because we only want to remove walls 366 | # the cursor is directly over 367 | # Recalculations should only occur when a wall is actually deleted 368 | if mouse_inside_grid2? 369 | if state.walls.has_key?(cell_closest_to_mouse2) 370 | state.walls.delete(cell_closest_to_mouse2) 371 | reset_search 372 | end 373 | end 374 | end 375 | 376 | # Adds a wall in the first grid in the cell the mouse is over 377 | def input_add_wall 378 | if mouse_inside_grid? 379 | unless state.walls.has_key?(cell_closest_to_mouse) 380 | state.walls[cell_closest_to_mouse] = true 381 | reset_search 382 | end 383 | end 384 | end 385 | 386 | 387 | # Adds a wall in the second grid in the cell the mouse is over 388 | def input_add_wall2 389 | if mouse_inside_grid2? 390 | unless state.walls.has_key?(cell_closest_to_mouse2) 391 | state.walls[cell_closest_to_mouse2] = true 392 | reset_search 393 | end 394 | end 395 | end 396 | 397 | # Whenever the user edits the grid, 398 | # The search has to be reset_searchd upto the current step 399 | # with the current grid as the initial state of the grid 400 | def reset_search 401 | # Reset_Searchs the search 402 | state.frontier = [] 403 | state.visited = {} 404 | state.early_exit_visited = {} 405 | state.came_from = {} 406 | state.path = {} 407 | end 408 | 409 | # Moves the search forward one step 410 | def step 411 | # The setup to the search 412 | # Runs once when there are no visited cells 413 | if state.visited.empty? 414 | state.visited[state.star] = true 415 | state.early_exit_visited[state.star] = true 416 | state.frontier << state.star 417 | state.came_from[state.star] = nil 418 | end 419 | 420 | # A step in the search 421 | unless state.frontier.empty? 422 | # Takes the next frontier cell 423 | new_frontier = state.frontier.shift 424 | # For each of its neighbors 425 | adjacent_neighbors(new_frontier).each do |neighbor| 426 | # That have not been visited and are not walls 427 | unless state.visited.has_key?(neighbor) || state.walls.has_key?(neighbor) 428 | # Add them to the frontier and mark them as visited in the first grid 429 | state.visited[neighbor] = true 430 | # Unless the target has been visited 431 | unless state.visited.has_key?(state.target) 432 | # Mark the neighbor as visited in the second grid as well 433 | state.early_exit_visited[neighbor] = true 434 | end 435 | 436 | # Add the neighbor to the frontier and remember which cell it came from 437 | state.frontier << neighbor 438 | state.came_from[neighbor] = new_frontier 439 | end 440 | end 441 | end 442 | end 443 | 444 | 445 | # Returns a list of adjacent cells 446 | # Used to determine what the next cells to be added to the frontier are 447 | def adjacent_neighbors(cell) 448 | neighbors = [] 449 | 450 | # Gets all the valid neighbors into the array 451 | # From southern neighbor, clockwise 452 | neighbors << [cell.x, cell.y - 1] unless cell.y == 0 453 | neighbors << [cell.x - 1, cell.y] unless cell.x == 0 454 | neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1 455 | neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1 456 | 457 | # Sorts the neighbors so the rendered path is a zigzag path 458 | # Cells in a diagonal direction are given priority 459 | # Comment this line to see the difference 460 | neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) } 461 | 462 | neighbors 463 | end 464 | 465 | # Finds the vertical and horizontal distance of a cell from the star 466 | # and returns the larger value 467 | # This method is used to have a zigzag pattern in the rendered path 468 | # A cell that is [5, 5] from the star, 469 | # is explored before over a cell that is [0, 7] away. 470 | # So, if possible, the search tries to go diagonal (zigzag) first 471 | def proximity_to_star(x, y) 472 | distance_x = (state.star.x - x).abs 473 | distance_y = (state.star.y - y).abs 474 | 475 | if distance_x > distance_y 476 | return distance_x 477 | else 478 | return distance_y 479 | end 480 | end 481 | 482 | # When the user grabs the star and puts their cursor to the far right 483 | # and moves up and down, the star is supposed to move along the grid as well 484 | # Finding the cell closest to the mouse helps with this 485 | def cell_closest_to_mouse 486 | # Closest cell to the mouse in the first grid 487 | x = (inputs.mouse.point.x / grid.cell_size).to_i 488 | y = (inputs.mouse.point.y / grid.cell_size).to_i 489 | # Bound x and y to the grid 490 | x = grid.width - 1 if x > grid.width - 1 491 | y = grid.height - 1 if y > grid.height - 1 492 | # Return closest cell 493 | [x, y] 494 | end 495 | 496 | # When the user grabs the star and puts their cursor to the far right 497 | # and moves up and down, the star is supposed to move along the grid as well 498 | # Finding the cell closest to the mouse in the second grid helps with this 499 | def cell_closest_to_mouse2 500 | # Closest cell grid to the mouse in the second 501 | x = (inputs.mouse.point.x / grid.cell_size).to_i 502 | y = (inputs.mouse.point.y / grid.cell_size).to_i 503 | # Translate the cell to the first grid 504 | x -= grid.width + 1 505 | # Bound x and y to the first grid 506 | x = grid.width - 1 if x > grid.width - 1 507 | y = grid.height - 1 if y > grid.height - 1 508 | # Return closest cell 509 | [x, y] 510 | end 511 | 512 | # Signal that the user is going to be moving the star from the first grid 513 | def star_clicked? 514 | inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.star)) 515 | end 516 | 517 | # Signal that the user is going to be moving the star from the second grid 518 | def star2_clicked? 519 | inputs.mouse.down && inputs.mouse.point.inside_rect?(early_exit_scale_up(state.star)) 520 | end 521 | 522 | # Signal that the user is going to be moving the target from the first grid 523 | def target_clicked? 524 | inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.target)) 525 | end 526 | 527 | # Signal that the user is going to be moving the target from the second grid 528 | def target2_clicked? 529 | inputs.mouse.down && inputs.mouse.point.inside_rect?(early_exit_scale_up(state.target)) 530 | end 531 | 532 | # Signal that the user is going to be removing walls from the first grid 533 | def wall_clicked? 534 | inputs.mouse.down && mouse_inside_wall? 535 | end 536 | 537 | # Signal that the user is going to be removing walls from the second grid 538 | def wall2_clicked? 539 | inputs.mouse.down && mouse_inside_wall2? 540 | end 541 | 542 | # Signal that the user is going to be adding walls from the first grid 543 | def grid_clicked? 544 | inputs.mouse.down && mouse_inside_grid? 545 | end 546 | 547 | # Signal that the user is going to be adding walls from the second grid 548 | def grid2_clicked? 549 | inputs.mouse.down && mouse_inside_grid2? 550 | end 551 | 552 | # Returns whether the mouse is inside of a wall in the first grid 553 | # Part of the condition that checks whether the user is removing a wall 554 | def mouse_inside_wall? 555 | state.walls.each_key do | wall | 556 | return true if inputs.mouse.point.inside_rect?(scale_up(wall)) 557 | end 558 | 559 | false 560 | end 561 | 562 | # Returns whether the mouse is inside of a wall in the second grid 563 | # Part of the condition that checks whether the user is removing a wall 564 | def mouse_inside_wall2? 565 | state.walls.each_key do | wall | 566 | return true if inputs.mouse.point.inside_rect?(early_exit_scale_up(wall)) 567 | end 568 | 569 | false 570 | end 571 | 572 | # Returns whether the mouse is inside of the first grid 573 | # Part of the condition that checks whether the user is adding a wall 574 | def mouse_inside_grid? 575 | inputs.mouse.point.inside_rect?(scale_up(grid.rect)) 576 | end 577 | 578 | # Returns whether the mouse is inside of the second grid 579 | # Part of the condition that checks whether the user is adding a wall 580 | def mouse_inside_grid2? 581 | inputs.mouse.point.inside_rect?(early_exit_scale_up(grid.rect)) 582 | end 583 | 584 | # These methods provide handy aliases to colors 585 | 586 | # Light brown 587 | def unvisited_color 588 | [221, 212, 213] 589 | end 590 | 591 | # Camo Green 592 | def wall_color 593 | [134, 134, 120] 594 | end 595 | 596 | # Pastel White 597 | def path_color 598 | [231, 230, 228] 599 | end 600 | 601 | def red 602 | [255, 0, 0] 603 | end 604 | 605 | # Makes code more concise 606 | def grid 607 | state.grid 608 | end 609 | end 610 | 611 | # Method that is called by DragonRuby periodically 612 | # Used for updating animations and calculations 613 | def tick args 614 | 615 | # Pressing r will reset the application 616 | if args.inputs.keyboard.key_down.r 617 | args.gtk.reset 618 | reset 619 | return 620 | end 621 | 622 | # Every tick, new args are passed, and the Breadth First Search tick is called 623 | $early_exit_breadth_first_search ||= EarlyExitBreadthFirstSearch.new(args) 624 | $early_exit_breadth_first_search.args = args 625 | $early_exit_breadth_first_search.tick 626 | end 627 | 628 | 629 | def reset 630 | $early_exit_breadth_first_search = nil 631 | end 632 | -------------------------------------------------------------------------------- /Early_Exit_Breadth_First_Search/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Early_Exit_Breadth_First_Search/star.png -------------------------------------------------------------------------------- /Early_Exit_Breadth_First_Search/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Early_Exit_Breadth_First_Search/target.png -------------------------------------------------------------------------------- /Heuristic/app/main.rb: -------------------------------------------------------------------------------- 1 | # This program is inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html 2 | 3 | # This time the heuristic search still explored less of the grid, hence finishing faster. 4 | # However, it did not find the shortest path between the star and the target. 5 | 6 | # The only difference between this app and Heuristic is the change of the starting position. 7 | 8 | class Heuristic_With_Walls 9 | attr_gtk 10 | 11 | def tick 12 | defaults 13 | render 14 | input 15 | # If animation is playing, and max steps have not been reached 16 | # Move the search a step forward 17 | if state.play && state.current_step < state.max_steps 18 | # Variable that tells the program what step to recalculate up to 19 | state.current_step += 1 20 | move_searches_one_step_forward 21 | end 22 | end 23 | 24 | def defaults 25 | # Variables to edit the size and appearance of the grid 26 | # Freely customizable to user's liking 27 | grid.width ||= 15 28 | grid.height ||= 15 29 | grid.cell_size ||= 40 30 | grid.rect ||= [0, 0, grid.width, grid.height] 31 | 32 | grid.star ||= [0, 2] 33 | grid.target ||= [14, 12] 34 | grid.walls ||= {} 35 | # There are no hills in the Heuristic Search Demo 36 | 37 | # What the user is currently editing on the grid 38 | # We store this value, because we want to remember the value even when 39 | # the user's cursor is no longer over what they're interacting with, but 40 | # they are still clicking down on the mouse. 41 | state.user_input ||= :none 42 | 43 | # These variables allow the breadth first search to take place 44 | # Came_from is a hash with a key of a cell and a value of the cell that was expanded from to find the key. 45 | # Used to prevent searching cells that have already been found 46 | # and to trace a path from the target back to the starting point. 47 | # Frontier is an array of cells to expand the search from. 48 | # The search is over when there are no more cells to search from. 49 | # Path stores the path from the target to the star, once the target has been found 50 | # It prevents calculating the path every tick. 51 | bfs.came_from ||= {} 52 | bfs.frontier ||= [] 53 | bfs.path ||= [] 54 | 55 | heuristic.came_from ||= {} 56 | heuristic.frontier ||= [] 57 | heuristic.path ||= [] 58 | 59 | # Stores which step of the animation is being rendered 60 | # When the user moves the star or messes with the walls, 61 | # the searches are recalculated up to this step 62 | 63 | # Unless the current step has a value 64 | unless state.current_step 65 | # Set the current step to 10 66 | state.current_step = 10 67 | # And calculate the searches up to step 10 68 | recalculate_searches 69 | end 70 | 71 | # At some step the animation will end, 72 | # and further steps won't change anything (the whole grid will be explored) 73 | # This step is roughly the grid's width * height 74 | # When anim_steps equals max_steps no more calculations will occur 75 | # and the slider will be at the end 76 | state.max_steps = grid.width * grid.height 77 | 78 | # Whether the animation should play or not 79 | # If true, every tick moves anim_steps forward one 80 | # Pressing the stepwise animation buttons will pause the animation 81 | # An if statement instead of the ||= operator is used for assigning a boolean value. 82 | # The || operator does not differentiate between nil and false. 83 | if state.play == nil 84 | state.play = false 85 | end 86 | 87 | # Store the rects of the buttons that control the animation 88 | # They are here for user customization 89 | # Editing these might require recentering the text inside them 90 | # Those values can be found in the render_button methods 91 | buttons.left = [470, 600, 50, 50] 92 | buttons.center = [520, 600, 200, 50] 93 | buttons.right = [720, 600, 50, 50] 94 | 95 | # The variables below are related to the slider 96 | # They allow the user to customize them 97 | # They also give a central location for the render and input methods to get 98 | # information from 99 | # x & y are the coordinates of the leftmost part of the slider line 100 | slider.x = 440 101 | slider.y = 675 102 | # This is the width of the line 103 | slider.w = 360 104 | # This is the offset for the circle 105 | # Allows the center of the circle to be on the line, 106 | # as opposed to the upper right corner 107 | slider.offset = 20 108 | # This is the spacing between each of the notches on the slider 109 | # Notches are places where the circle can rest on the slider line 110 | # There needs to be a notch for each step before the maximum number of steps 111 | slider.spacing = slider.w.to_f / state.max_steps.to_f 112 | end 113 | 114 | # All methods with render draw stuff on the screen 115 | # UI has buttons, the slider, and labels 116 | # The search specific rendering occurs in the respective methods 117 | def render 118 | render_ui 119 | render_bfs 120 | render_heuristic 121 | end 122 | 123 | def render_ui 124 | render_buttons 125 | render_slider 126 | render_labels 127 | end 128 | 129 | def render_buttons 130 | render_left_button 131 | render_center_button 132 | render_right_button 133 | end 134 | 135 | def render_bfs 136 | render_bfs_grid 137 | render_bfs_star 138 | render_bfs_target 139 | render_bfs_visited 140 | render_bfs_walls 141 | render_bfs_frontier 142 | render_bfs_path 143 | end 144 | 145 | def render_heuristic 146 | render_heuristic_grid 147 | render_heuristic_star 148 | render_heuristic_target 149 | render_heuristic_visited 150 | render_heuristic_walls 151 | render_heuristic_frontier 152 | render_heuristic_path 153 | end 154 | 155 | # This method handles user input every tick 156 | def input 157 | # Check and handle button input 158 | input_buttons 159 | 160 | # If the mouse was lifted this tick 161 | if inputs.mouse.up 162 | # Set current input to none 163 | state.user_input = :none 164 | end 165 | 166 | # If the mouse was clicked this tick 167 | if inputs.mouse.down 168 | # Determine what the user is editing and appropriately edit the state.user_input variable 169 | determine_input 170 | end 171 | 172 | # Process user input based on user_input variable and current mouse position 173 | process_input 174 | end 175 | 176 | # Determines what the user is editing 177 | # This method is called when the mouse is clicked down 178 | def determine_input 179 | if mouse_over_slider? 180 | state.user_input = :slider 181 | # If the mouse is over the star in the first grid 182 | elsif bfs_mouse_over_star? 183 | # The user is editing the star from the first grid 184 | state.user_input = :bfs_star 185 | # If the mouse is over the star in the second grid 186 | elsif heuristic_mouse_over_star? 187 | # The user is editing the star from the second grid 188 | state.user_input = :heuristic_star 189 | # If the mouse is over the target in the first grid 190 | elsif bfs_mouse_over_target? 191 | # The user is editing the target from the first grid 192 | state.user_input = :bfs_target 193 | # If the mouse is over the target in the second grid 194 | elsif heuristic_mouse_over_target? 195 | # The user is editing the target from the second grid 196 | state.user_input = :heuristic_target 197 | # If the mouse is over a wall in the first grid 198 | elsif bfs_mouse_over_wall? 199 | # The user is removing a wall from the first grid 200 | state.user_input = :bfs_remove_wall 201 | # If the mouse is over a wall in the second grid 202 | elsif heuristic_mouse_over_wall? 203 | # The user is removing a wall from the second grid 204 | state.user_input = :heuristic_remove_wall 205 | # If the mouse is over the first grid 206 | elsif bfs_mouse_over_grid? 207 | # The user is adding a wall from the first grid 208 | state.user_input = :bfs_add_wall 209 | # If the mouse is over the second grid 210 | elsif heuristic_mouse_over_grid? 211 | # The user is adding a wall from the second grid 212 | state.user_input = :heuristic_add_wall 213 | end 214 | end 215 | 216 | # Processes click and drag based on what the user is currently dragging 217 | def process_input 218 | if state.user_input == :slider 219 | process_input_slider 220 | elsif state.user_input == :bfs_star 221 | process_input_bfs_star 222 | elsif state.user_input == :heuristic_star 223 | process_input_heuristic_star 224 | elsif state.user_input == :bfs_target 225 | process_input_bfs_target 226 | elsif state.user_input == :heuristic_target 227 | process_input_heuristic_target 228 | elsif state.user_input == :bfs_remove_wall 229 | process_input_bfs_remove_wall 230 | elsif state.user_input == :heuristic_remove_wall 231 | process_input_heuristic_remove_wall 232 | elsif state.user_input == :bfs_add_wall 233 | process_input_bfs_add_wall 234 | elsif state.user_input == :heuristic_add_wall 235 | process_input_heuristic_add_wall 236 | end 237 | end 238 | 239 | def render_slider 240 | # Using primitives hides the line under the white circle of the slider 241 | # Draws the line 242 | outputs.primitives << [slider.x, slider.y, slider.x + slider.w, slider.y].line 243 | # The circle needs to be offset so that the center of the circle 244 | # overlaps the line instead of the upper right corner of the circle 245 | # The circle's x value is also moved based on the current seach step 246 | circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing) 247 | circle_y = (slider.y - slider.offset) 248 | circle_rect = [circle_x, circle_y, 37, 37] 249 | outputs.primitives << [circle_rect, 'circle-white.png'].sprite 250 | end 251 | 252 | def render_labels 253 | outputs.labels << [205, 625, "Breadth First Search"] 254 | outputs.labels << [820, 625, "Heuristic Best-First Search"] 255 | end 256 | 257 | def render_left_button 258 | # Draws the button_color button, and a black border 259 | # The border separates the buttons visually 260 | outputs.solids << [buttons.left, button_color] 261 | outputs.borders << [buttons.left] 262 | 263 | # Renders an explanatory label in the center of the button 264 | # Explains to the user what the button does 265 | # If the button size is changed, the label might need to be edited as well 266 | # to keep the label in the center of the button 267 | label_x = buttons.left.x + 20 268 | label_y = buttons.left.y + 35 269 | outputs.labels << [label_x, label_y, "<"] 270 | end 271 | 272 | def render_center_button 273 | # Draws the button_color button, and a black border 274 | # The border separates the buttons visually 275 | outputs.solids << [buttons.center, button_color] 276 | outputs.borders << [buttons.center] 277 | 278 | # Renders an explanatory label in the center of the button 279 | # Explains to the user what the button does 280 | # If the button size is changed, the label might need to be edited as well 281 | # to keep the label in the center of the button 282 | label_x = buttons.center.x + 37 283 | label_y = buttons.center.y + 35 284 | label_text = state.play ? "Pause Animation" : "Play Animation" 285 | outputs.labels << [label_x, label_y, label_text] 286 | end 287 | 288 | def render_right_button 289 | # Draws the button_color button, and a black border 290 | # The border separates the buttons visually 291 | outputs.solids << [buttons.right, button_color] 292 | outputs.borders << [buttons.right] 293 | 294 | # Renders an explanatory label in the center of the button 295 | # Explains to the user what the button does 296 | label_x = buttons.right.x + 20 297 | label_y = buttons.right.y + 35 298 | outputs.labels << [label_x, label_y, ">"] 299 | end 300 | 301 | def render_bfs_grid 302 | # A large rect the size of the grid 303 | outputs.solids << [bfs_scale_up(grid.rect), default_color] 304 | 305 | # The vertical grid lines 306 | for x in 0..grid.width 307 | outputs.lines << bfs_vertical_line(x) 308 | end 309 | 310 | # The horizontal grid lines 311 | for y in 0..grid.height 312 | outputs.lines << bfs_horizontal_line(y) 313 | end 314 | end 315 | 316 | def render_heuristic_grid 317 | # A large rect the size of the grid 318 | outputs.solids << [heuristic_scale_up(grid.rect), default_color] 319 | 320 | # The vertical grid lines 321 | for x in 0..grid.width 322 | outputs.lines << heuristic_vertical_line(x) 323 | end 324 | 325 | # The horizontal grid lines 326 | for y in 0..grid.height 327 | outputs.lines << heuristic_horizontal_line(y) 328 | end 329 | end 330 | 331 | # Returns a vertical line for a column of the first grid 332 | def bfs_vertical_line column 333 | bfs_scale_up([column, 0, column, grid.height]) 334 | end 335 | 336 | # Returns a horizontal line for a column of the first grid 337 | def bfs_horizontal_line row 338 | bfs_scale_up([0, row, grid.width, row]) 339 | end 340 | 341 | # Returns a vertical line for a column of the second grid 342 | def heuristic_vertical_line column 343 | bfs_scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height]) 344 | end 345 | 346 | # Returns a horizontal line for a column of the second grid 347 | def heuristic_horizontal_line row 348 | bfs_scale_up([grid.width + 1, row, grid.width + grid.width + 1, row]) 349 | end 350 | 351 | # Renders the star on the first grid 352 | def render_bfs_star 353 | outputs.sprites << [bfs_scale_up(grid.star), 'star.png'] 354 | end 355 | 356 | # Renders the star on the second grid 357 | def render_heuristic_star 358 | outputs.sprites << [heuristic_scale_up(grid.star), 'star.png'] 359 | end 360 | 361 | # Renders the target on the first grid 362 | def render_bfs_target 363 | outputs.sprites << [bfs_scale_up(grid.target), 'target.png'] 364 | end 365 | 366 | # Renders the target on the second grid 367 | def render_heuristic_target 368 | outputs.sprites << [heuristic_scale_up(grid.target), 'target.png'] 369 | end 370 | 371 | # Renders the walls on the first grid 372 | def render_bfs_walls 373 | grid.walls.each_key do | wall | 374 | outputs.solids << [bfs_scale_up(wall), wall_color] 375 | end 376 | end 377 | 378 | # Renders the walls on the second grid 379 | def render_heuristic_walls 380 | grid.walls.each_key do | wall | 381 | outputs.solids << [heuristic_scale_up(wall), wall_color] 382 | end 383 | end 384 | 385 | # Renders the visited cells on the first grid 386 | def render_bfs_visited 387 | bfs.came_from.each_key do | visited_cell | 388 | outputs.solids << [bfs_scale_up(visited_cell), visited_color] 389 | end 390 | end 391 | 392 | # Renders the visited cells on the second grid 393 | def render_heuristic_visited 394 | heuristic.came_from.each_key do | visited_cell | 395 | outputs.solids << [heuristic_scale_up(visited_cell), visited_color] 396 | end 397 | end 398 | 399 | # Renders the frontier cells on the first grid 400 | def render_bfs_frontier 401 | bfs.frontier.each do | frontier_cell | 402 | outputs.solids << [bfs_scale_up(frontier_cell), frontier_color, 200] 403 | end 404 | end 405 | 406 | # Renders the frontier cells on the second grid 407 | def render_heuristic_frontier 408 | heuristic.frontier.each do | frontier_cell | 409 | outputs.solids << [heuristic_scale_up(frontier_cell), frontier_color, 200] 410 | end 411 | end 412 | 413 | # Renders the path found by the breadth first search on the first grid 414 | def render_bfs_path 415 | bfs.path.each do | path | 416 | outputs.solids << [bfs_scale_up(path), path_color] 417 | end 418 | end 419 | 420 | # Renders the path found by the heuristic search on the second grid 421 | def render_heuristic_path 422 | heuristic.path.each do | path | 423 | outputs.solids << [heuristic_scale_up(path), path_color] 424 | end 425 | end 426 | 427 | # Returns the rect for the path between two cells based on their relative positions 428 | def get_path_between(cell_one, cell_two) 429 | path = nil 430 | 431 | # If cell one is above cell two 432 | if cell_one.x == cell_two.x and cell_one.y > cell_two.y 433 | # Path starts from the center of cell two and moves upward to the center of cell one 434 | path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4] 435 | # If cell one is below cell two 436 | elsif cell_one.x == cell_two.x and cell_one.y < cell_two.y 437 | # Path starts from the center of cell one and moves upward to the center of cell two 438 | path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4] 439 | # If cell one is to the left of cell two 440 | elsif cell_one.x > cell_two.x and cell_one.y == cell_two.y 441 | # Path starts from the center of cell two and moves rightward to the center of cell one 442 | path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4] 443 | # If cell one is to the right of cell two 444 | elsif cell_one.x < cell_two.x and cell_one.y == cell_two.y 445 | # Path starts from the center of cell one and moves rightward to the center of cell two 446 | path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4] 447 | end 448 | 449 | path 450 | end 451 | 452 | # In code, the cells are represented as 1x1 rectangles 453 | # When drawn, the cells are larger than 1x1 rectangles 454 | # This method is used to scale up cells, and lines 455 | # Objects are scaled up according to the grid.cell_size variable 456 | # This allows for easy customization of the visual scale of the grid 457 | # This method scales up cells for the first grid 458 | def bfs_scale_up(cell) 459 | # Prevents the original value of cell from being edited 460 | cell = cell.clone 461 | 462 | # If cell is just an x and y coordinate 463 | if cell.size == 2 464 | # Add a width and height of 1 465 | cell << 1 466 | cell << 1 467 | end 468 | 469 | # Scale all the values up 470 | cell.map! { |value| value * grid.cell_size } 471 | 472 | # Returns the scaled up cell 473 | cell 474 | end 475 | 476 | # Translates the given cell grid.width + 1 to the right and then scales up 477 | # Used to draw cells for the second grid 478 | # This method does not work for lines, 479 | # so separate methods exist for the grid lines 480 | def heuristic_scale_up(cell) 481 | # Prevents the original value of cell from being edited 482 | cell = cell.clone 483 | # Translates the cell to the second grid equivalent 484 | cell.x += grid.width + 1 485 | # Proceeds as if scaling up for the first grid 486 | bfs_scale_up(cell) 487 | end 488 | 489 | # Checks and handles input for the buttons 490 | # Called when the mouse is lifted 491 | def input_buttons 492 | input_left_button 493 | input_center_button 494 | input_right_button 495 | end 496 | 497 | # Checks if the previous step button is clicked 498 | # If it is, it pauses the animation and moves the search one step backward 499 | def input_left_button 500 | if left_button_clicked? 501 | state.play = false 502 | state.current_step -= 1 503 | recalculate_searches 504 | end 505 | end 506 | 507 | # Controls the play/pause button 508 | # Inverses whether the animation is playing or not when clicked 509 | def input_center_button 510 | if center_button_clicked? || inputs.keyboard.key_down.space 511 | state.play = !state.play 512 | end 513 | end 514 | 515 | # Checks if the next step button is clicked 516 | # If it is, it pauses the animation and moves the search one step forward 517 | def input_right_button 518 | if right_button_clicked? 519 | state.play = false 520 | state.current_step += 1 521 | move_searches_one_step_forward 522 | end 523 | end 524 | 525 | # These methods detect when the buttons are clicked 526 | def left_button_clicked? 527 | inputs.mouse.point.inside_rect?(buttons.left) && inputs.mouse.up 528 | end 529 | 530 | def center_button_clicked? 531 | inputs.mouse.point.inside_rect?(buttons.center) && inputs.mouse.up 532 | end 533 | 534 | def right_button_clicked? 535 | inputs.mouse.point.inside_rect?(buttons.right) && inputs.mouse.up 536 | end 537 | 538 | 539 | # Signal that the user is going to be moving the slider 540 | # Is the mouse over the circle of the slider? 541 | def mouse_over_slider? 542 | circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing) 543 | circle_y = (slider.y - slider.offset) 544 | circle_rect = [circle_x, circle_y, 37, 37] 545 | inputs.mouse.point.inside_rect?(circle_rect) 546 | end 547 | 548 | # Signal that the user is going to be moving the star from the first grid 549 | def bfs_mouse_over_star? 550 | inputs.mouse.point.inside_rect?(bfs_scale_up(grid.star)) 551 | end 552 | 553 | # Signal that the user is going to be moving the star from the second grid 554 | def heuristic_mouse_over_star? 555 | inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.star)) 556 | end 557 | 558 | # Signal that the user is going to be moving the target from the first grid 559 | def bfs_mouse_over_target? 560 | inputs.mouse.point.inside_rect?(bfs_scale_up(grid.target)) 561 | end 562 | 563 | # Signal that the user is going to be moving the target from the second grid 564 | def heuristic_mouse_over_target? 565 | inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.target)) 566 | end 567 | 568 | # Signal that the user is going to be removing walls from the first grid 569 | def bfs_mouse_over_wall? 570 | grid.walls.each_key do | wall | 571 | return true if inputs.mouse.point.inside_rect?(bfs_scale_up(wall)) 572 | end 573 | 574 | false 575 | end 576 | 577 | # Signal that the user is going to be removing walls from the second grid 578 | def heuristic_mouse_over_wall? 579 | grid.walls.each_key do | wall | 580 | return true if inputs.mouse.point.inside_rect?(heuristic_scale_up(wall)) 581 | end 582 | 583 | false 584 | end 585 | 586 | # Signal that the user is going to be adding walls from the first grid 587 | def bfs_mouse_over_grid? 588 | inputs.mouse.point.inside_rect?(bfs_scale_up(grid.rect)) 589 | end 590 | 591 | # Signal that the user is going to be adding walls from the second grid 592 | def heuristic_mouse_over_grid? 593 | inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.rect)) 594 | end 595 | 596 | # This method is called when the user is editing the slider 597 | # It pauses the animation and moves the white circle to the closest integer point 598 | # on the slider 599 | # Changes the step of the search to be animated 600 | def process_input_slider 601 | state.play = false 602 | mouse_x = inputs.mouse.point.x 603 | 604 | # Bounds the mouse_x to the closest x value on the slider line 605 | mouse_x = slider.x if mouse_x < slider.x 606 | mouse_x = slider.x + slider.w if mouse_x > slider.x + slider.w 607 | 608 | # Sets the current search step to the one represented by the mouse x value 609 | # The slider's circle moves due to the render_slider method using anim_steps 610 | state.current_step = ((mouse_x - slider.x) / slider.spacing).to_i 611 | 612 | recalculate_searches 613 | end 614 | 615 | # Moves the star to the cell closest to the mouse in the first grid 616 | # Only resets the search if the star changes position 617 | # Called whenever the user is editing the star (puts mouse down on star) 618 | def process_input_bfs_star 619 | old_star = grid.star.clone 620 | unless bfs_cell_closest_to_mouse == grid.target 621 | grid.star = bfs_cell_closest_to_mouse 622 | end 623 | unless old_star == grid.star 624 | recalculate_searches 625 | end 626 | end 627 | 628 | # Moves the star to the cell closest to the mouse in the second grid 629 | # Only resets the search if the star changes position 630 | # Called whenever the user is editing the star (puts mouse down on star) 631 | def process_input_heuristic_star 632 | old_star = grid.star.clone 633 | unless heuristic_cell_closest_to_mouse == grid.target 634 | grid.star = heuristic_cell_closest_to_mouse 635 | end 636 | unless old_star == grid.star 637 | recalculate_searches 638 | end 639 | end 640 | 641 | # Moves the target to the grid closest to the mouse in the first grid 642 | # Only recalculate_searchess the search if the target changes position 643 | # Called whenever the user is editing the target (puts mouse down on target) 644 | def process_input_bfs_target 645 | old_target = grid.target.clone 646 | unless bfs_cell_closest_to_mouse == grid.star 647 | grid.target = bfs_cell_closest_to_mouse 648 | end 649 | unless old_target == grid.target 650 | recalculate_searches 651 | end 652 | end 653 | 654 | # Moves the target to the cell closest to the mouse in the second grid 655 | # Only recalculate_searchess the search if the target changes position 656 | # Called whenever the user is editing the target (puts mouse down on target) 657 | def process_input_heuristic_target 658 | old_target = grid.target.clone 659 | unless heuristic_cell_closest_to_mouse == grid.star 660 | grid.target = heuristic_cell_closest_to_mouse 661 | end 662 | unless old_target == grid.target 663 | recalculate_searches 664 | end 665 | end 666 | 667 | # Removes walls in the first grid that are under the cursor 668 | def process_input_bfs_remove_wall 669 | # The mouse needs to be inside the grid, because we only want to remove walls 670 | # the cursor is directly over 671 | # Recalculations should only occur when a wall is actually deleted 672 | if bfs_mouse_over_grid? 673 | if grid.walls.has_key?(bfs_cell_closest_to_mouse) 674 | grid.walls.delete(bfs_cell_closest_to_mouse) 675 | recalculate_searches 676 | end 677 | end 678 | end 679 | 680 | # Removes walls in the second grid that are under the cursor 681 | def process_input_heuristic_remove_wall 682 | # The mouse needs to be inside the grid, because we only want to remove walls 683 | # the cursor is directly over 684 | # Recalculations should only occur when a wall is actually deleted 685 | if heuristic_mouse_over_grid? 686 | if grid.walls.has_key?(heuristic_cell_closest_to_mouse) 687 | grid.walls.delete(heuristic_cell_closest_to_mouse) 688 | recalculate_searches 689 | end 690 | end 691 | end 692 | # Adds a wall in the first grid in the cell the mouse is over 693 | def process_input_bfs_add_wall 694 | if bfs_mouse_over_grid? 695 | unless grid.walls.has_key?(bfs_cell_closest_to_mouse) 696 | grid.walls[bfs_cell_closest_to_mouse] = true 697 | recalculate_searches 698 | end 699 | end 700 | end 701 | 702 | # Adds a wall in the second grid in the cell the mouse is over 703 | def process_input_heuristic_add_wall 704 | if heuristic_mouse_over_grid? 705 | unless grid.walls.has_key?(heuristic_cell_closest_to_mouse) 706 | grid.walls[heuristic_cell_closest_to_mouse] = true 707 | recalculate_searches 708 | end 709 | end 710 | end 711 | 712 | # When the user grabs the star and puts their cursor to the far right 713 | # and moves up and down, the star is supposed to move along the grid as well 714 | # Finding the cell closest to the mouse helps with this 715 | def bfs_cell_closest_to_mouse 716 | # Closest cell to the mouse in the first grid 717 | x = (inputs.mouse.point.x / grid.cell_size).to_i 718 | y = (inputs.mouse.point.y / grid.cell_size).to_i 719 | # Bound x and y to the grid 720 | x = grid.width - 1 if x > grid.width - 1 721 | y = grid.height - 1 if y > grid.height - 1 722 | # Return closest cell 723 | [x, y] 724 | end 725 | 726 | # When the user grabs the star and puts their cursor to the far right 727 | # and moves up and down, the star is supposed to move along the grid as well 728 | # Finding the cell closest to the mouse in the second grid helps with this 729 | def heuristic_cell_closest_to_mouse 730 | # Closest cell grid to the mouse in the second 731 | x = (inputs.mouse.point.x / grid.cell_size).to_i 732 | y = (inputs.mouse.point.y / grid.cell_size).to_i 733 | # Translate the cell to the first grid 734 | x -= grid.width + 1 735 | # Bound x and y to the first grid 736 | x = 0 if x < 0 737 | y = 0 if y < 0 738 | x = grid.width - 1 if x > grid.width - 1 739 | y = grid.height - 1 if y > grid.height - 1 740 | # Return closest cell 741 | [x, y] 742 | end 743 | 744 | def recalculate_searches 745 | # Reset the searches 746 | bfs.came_from = {} 747 | bfs.frontier = [] 748 | bfs.path = [] 749 | heuristic.came_from = {} 750 | heuristic.frontier = [] 751 | heuristic.path = [] 752 | 753 | # Move the searches forward to the current step 754 | state.current_step.times { move_searches_one_step_forward } 755 | end 756 | 757 | def move_searches_one_step_forward 758 | bfs_one_step_forward 759 | heuristic_one_step_forward 760 | end 761 | 762 | def bfs_one_step_forward 763 | return if bfs.came_from.has_key?(grid.target) 764 | 765 | # Only runs at the beginning of the search as setup. 766 | if bfs.came_from.empty? 767 | bfs.frontier << grid.star 768 | bfs.came_from[grid.star] = nil 769 | end 770 | 771 | # A step in the search 772 | unless bfs.frontier.empty? 773 | # Takes the next frontier cell 774 | new_frontier = bfs.frontier.shift 775 | # For each of its neighbors 776 | adjacent_neighbors(new_frontier).each do |neighbor| 777 | # That have not been visited and are not walls 778 | unless bfs.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) 779 | # Add them to the frontier and mark them as visited 780 | bfs.frontier << neighbor 781 | bfs.came_from[neighbor] = new_frontier 782 | end 783 | end 784 | end 785 | 786 | # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line 787 | # Comment this line and let a path generate to see the difference 788 | bfs.frontier = bfs.frontier.sort_by {| cell | proximity_to_star(cell) } 789 | 790 | # If the search found the target 791 | if bfs.came_from.has_key?(grid.target) 792 | # Calculate the path between the target and star 793 | bfs_calc_path 794 | end 795 | end 796 | 797 | # Calculates the path between the target and star for the breadth first search 798 | # Only called when the breadth first search finds the target 799 | def bfs_calc_path 800 | # Start from the target 801 | endpoint = grid.target 802 | # And the cell it came from 803 | next_endpoint = bfs.came_from[endpoint] 804 | while endpoint and next_endpoint 805 | # Draw a path between these two cells and store it 806 | path = get_path_between(endpoint, next_endpoint) 807 | bfs.path << path 808 | # And get the next pair of cells 809 | endpoint = next_endpoint 810 | next_endpoint = bfs.came_from[endpoint] 811 | # Continue till there are no more cells 812 | end 813 | end 814 | 815 | # Moves the heuristic search forward one step 816 | # Can be called from tick while the animation is playing 817 | # Can also be called when recalculating the searches after the user edited the grid 818 | def heuristic_one_step_forward 819 | # Stop the search if the target has been found 820 | return if heuristic.came_from.has_key?(grid.target) 821 | 822 | # If the search has not begun 823 | if heuristic.came_from.empty? 824 | # Setup the search to begin from the star 825 | heuristic.frontier << grid.star 826 | heuristic.came_from[grid.star] = nil 827 | end 828 | 829 | # One step in the heuristic search 830 | 831 | # Unless there are no more cells to explore from 832 | unless heuristic.frontier.empty? 833 | # Get the next cell to explore from 834 | new_frontier = heuristic.frontier.shift 835 | # For each of its neighbors 836 | adjacent_neighbors(new_frontier).each do |neighbor| 837 | # That have not been visited and are not walls 838 | unless heuristic.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) 839 | # Add them to the frontier and mark them as visited 840 | heuristic.frontier << neighbor 841 | heuristic.came_from[neighbor] = new_frontier 842 | end 843 | end 844 | end 845 | 846 | # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line 847 | heuristic.frontier = heuristic.frontier.sort_by {| cell | proximity_to_star(cell) } 848 | # Sort the frontier so cells that are close to the target are then prioritized 849 | heuristic.frontier = heuristic.frontier.sort_by {| cell | heuristic_heuristic(cell) } 850 | 851 | # If the search found the target 852 | if heuristic.came_from.has_key?(grid.target) 853 | # Calculate the path between the target and star 854 | heuristic_calc_path 855 | end 856 | end 857 | 858 | # Returns one-dimensional absolute distance between cell and target 859 | # Returns a number to compare distances between cells and the target 860 | def heuristic_heuristic(cell) 861 | (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs 862 | end 863 | 864 | # Calculates the path between the target and star for the heuristic search 865 | # Only called when the heuristic search finds the target 866 | def heuristic_calc_path 867 | # Start from the target 868 | endpoint = grid.target 869 | # And the cell it came from 870 | next_endpoint = heuristic.came_from[endpoint] 871 | while endpoint and next_endpoint 872 | # Draw a path between these two cells and store it 873 | path = get_path_between(endpoint, next_endpoint) 874 | heuristic.path << path 875 | # And get the next pair of cells 876 | endpoint = next_endpoint 877 | next_endpoint = heuristic.came_from[endpoint] 878 | # Continue till there are no more cells 879 | end 880 | end 881 | 882 | # Returns a list of adjacent cells 883 | # Used to determine what the next cells to be added to the frontier are 884 | def adjacent_neighbors(cell) 885 | neighbors = [] 886 | 887 | # Gets all the valid neighbors into the array 888 | # From southern neighbor, clockwise 889 | neighbors << [cell.x , cell.y - 1] unless cell.y == 0 890 | neighbors << [cell.x - 1, cell.y ] unless cell.x == 0 891 | neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1 892 | neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1 893 | 894 | neighbors 895 | end 896 | 897 | # Finds the vertical and horizontal distance of a cell from the star 898 | # and returns the larger value 899 | # This method is used to have a zigzag pattern in the rendered path 900 | # A cell that is [5, 5] from the star, 901 | # is explored before over a cell that is [0, 7] away. 902 | # So, if possible, the search tries to go diagonal (zigzag) first 903 | def proximity_to_star(cell) 904 | distance_x = (grid.star.x - cell.x).abs 905 | distance_y = (grid.star.y - cell.y).abs 906 | 907 | if distance_x > distance_y 908 | return distance_x 909 | else 910 | return distance_y 911 | end 912 | end 913 | 914 | # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored. 915 | def grid 916 | state.grid 917 | end 918 | 919 | def buttons 920 | state.buttons 921 | end 922 | 923 | def slider 924 | state.slider 925 | end 926 | 927 | def bfs 928 | state.bfs 929 | end 930 | 931 | def heuristic 932 | state.heuristic 933 | end 934 | 935 | # Descriptive aliases for colors 936 | def default_color 937 | [221, 212, 213] # Light Brown 938 | end 939 | 940 | def wall_color 941 | [134, 134, 120] # Camo Green 942 | end 943 | 944 | def visited_color 945 | [204, 191, 179] # Dark Brown 946 | end 947 | 948 | def frontier_color 949 | [103, 136, 204] # Blue 950 | end 951 | 952 | def path_color 953 | [231, 230, 228] # Pastel White 954 | end 955 | 956 | def button_color 957 | [190, 190, 190] # Gray 958 | end 959 | end 960 | # Method that is called by DragonRuby periodically 961 | # Used for updating animations and calculations 962 | def tick args 963 | 964 | # Pressing r will reset the application 965 | if args.inputs.keyboard.key_down.r 966 | args.gtk.reset 967 | reset 968 | return 969 | end 970 | 971 | # Every tick, new args are passed, and the Breadth First Search tick is called 972 | $heuristic_with_walls ||= Heuristic_With_Walls.new(args) 973 | $heuristic_with_walls.args = args 974 | $heuristic_with_walls.tick 975 | end 976 | 977 | 978 | def reset 979 | $heuristic_with_walls = nil 980 | end 981 | -------------------------------------------------------------------------------- /Heuristic/circle-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Heuristic/circle-white.png -------------------------------------------------------------------------------- /Heuristic/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Heuristic/star.png -------------------------------------------------------------------------------- /Heuristic/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Heuristic/target.png -------------------------------------------------------------------------------- /Heuristic_With_Walls/app/main.rb: -------------------------------------------------------------------------------- 1 | # This program is inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html 2 | # The effectiveness of the Heuristic search algorithm is shown through this demonstration. 3 | # Notice that both searches find the shortest path 4 | # The heuristic search, however, explores less of the grid, and is therefore faster. 5 | # The heuristic search prioritizes searching cells that are closer to the target. 6 | # Make sure to look at the Heuristic with walls program to see some of the downsides of the heuristic algorithm. 7 | 8 | class Heuristic 9 | attr_gtk 10 | 11 | def tick 12 | defaults 13 | render 14 | input 15 | # If animation is playing, and max steps have not been reached 16 | # Move the search a step forward 17 | if state.play && state.current_step < state.max_steps 18 | # Variable that tells the program what step to recalculate up to 19 | state.current_step += 1 20 | move_searches_one_step_forward 21 | end 22 | end 23 | 24 | def defaults 25 | # Variables to edit the size and appearance of the grid 26 | # Freely customizable to user's liking 27 | grid.width ||= 15 28 | grid.height ||= 15 29 | grid.cell_size ||= 40 30 | grid.rect ||= [0, 0, grid.width, grid.height] 31 | 32 | grid.star ||= [0, 2] 33 | grid.target ||= [14, 12] 34 | grid.walls ||= { 35 | [2, 2] => true, 36 | [3, 2] => true, 37 | [4, 2] => true, 38 | [5, 2] => true, 39 | [6, 2] => true, 40 | [7, 2] => true, 41 | [8, 2] => true, 42 | [9, 2] => true, 43 | [10, 2] => true, 44 | [11, 2] => true, 45 | [12, 2] => true, 46 | [12, 3] => true, 47 | [12, 4] => true, 48 | [12, 5] => true, 49 | [12, 6] => true, 50 | [12, 7] => true, 51 | [12, 8] => true, 52 | [12, 9] => true, 53 | [12, 10] => true, 54 | [12, 11] => true, 55 | [12, 12] => true, 56 | [2, 12] => true, 57 | [3, 12] => true, 58 | [4, 12] => true, 59 | [5, 12] => true, 60 | [6, 12] => true, 61 | [7, 12] => true, 62 | [8, 12] => true, 63 | [9, 12] => true, 64 | [10, 12] => true, 65 | [11, 12] => true, 66 | [12, 12] => true 67 | } 68 | # There are no hills in the Heuristic Search Demo 69 | 70 | # What the user is currently editing on the grid 71 | # We store this value, because we want to remember the value even when 72 | # the user's cursor is no longer over what they're interacting with, but 73 | # they are still clicking down on the mouse. 74 | state.user_input ||= :none 75 | 76 | # These variables allow the breadth first search to take place 77 | # Came_from is a hash with a key of a cell and a value of the cell that was expanded from to find the key. 78 | # Used to prevent searching cells that have already been found 79 | # and to trace a path from the target back to the starting point. 80 | # Frontier is an array of cells to expand the search from. 81 | # The search is over when there are no more cells to search from. 82 | # Path stores the path from the target to the star, once the target has been found 83 | # It prevents calculating the path every tick. 84 | bfs.came_from ||= {} 85 | bfs.frontier ||= [] 86 | bfs.path ||= [] 87 | 88 | heuristic.came_from ||= {} 89 | heuristic.frontier ||= [] 90 | heuristic.path ||= [] 91 | 92 | # Stores which step of the animation is being rendered 93 | # When the user moves the star or messes with the walls, 94 | # the searches are recalculated up to this step 95 | 96 | # Unless the current step has a value 97 | unless state.current_step 98 | # Set the current step to 10 99 | state.current_step = 10 100 | # And calculate the searches up to step 10 101 | recalculate_searches 102 | end 103 | 104 | # At some step the animation will end, 105 | # and further steps won't change anything (the whole grid will be explored) 106 | # This step is roughly the grid's width * height 107 | # When anim_steps equals max_steps no more calculations will occur 108 | # and the slider will be at the end 109 | state.max_steps = grid.width * grid.height 110 | 111 | # Whether the animation should play or not 112 | # If true, every tick moves anim_steps forward one 113 | # Pressing the stepwise animation buttons will pause the animation 114 | # An if statement instead of the ||= operator is used for assigning a boolean value. 115 | # The || operator does not differentiate between nil and false. 116 | if state.play == nil 117 | state.play = false 118 | end 119 | 120 | # Store the rects of the buttons that control the animation 121 | # They are here for user customization 122 | # Editing these might require recentering the text inside them 123 | # Those values can be found in the render_button methods 124 | buttons.left = [470, 600, 50, 50] 125 | buttons.center = [520, 600, 200, 50] 126 | buttons.right = [720, 600, 50, 50] 127 | 128 | # The variables below are related to the slider 129 | # They allow the user to customize them 130 | # They also give a central location for the render and input methods to get 131 | # information from 132 | # x & y are the coordinates of the leftmost part of the slider line 133 | slider.x = 440 134 | slider.y = 675 135 | # This is the width of the line 136 | slider.w = 360 137 | # This is the offset for the circle 138 | # Allows the center of the circle to be on the line, 139 | # as opposed to the upper right corner 140 | slider.offset = 20 141 | # This is the spacing between each of the notches on the slider 142 | # Notches are places where the circle can rest on the slider line 143 | # There needs to be a notch for each step before the maximum number of steps 144 | slider.spacing = slider.w.to_f / state.max_steps.to_f 145 | end 146 | 147 | # All methods with render draw stuff on the screen 148 | # UI has buttons, the slider, and labels 149 | # The search specific rendering occurs in the respective methods 150 | def render 151 | render_ui 152 | render_bfs 153 | render_heuristic 154 | end 155 | 156 | def render_ui 157 | render_buttons 158 | render_slider 159 | render_labels 160 | end 161 | 162 | def render_buttons 163 | render_left_button 164 | render_center_button 165 | render_right_button 166 | end 167 | 168 | def render_bfs 169 | render_bfs_grid 170 | render_bfs_star 171 | render_bfs_target 172 | render_bfs_visited 173 | render_bfs_walls 174 | render_bfs_frontier 175 | render_bfs_path 176 | end 177 | 178 | def render_heuristic 179 | render_heuristic_grid 180 | render_heuristic_star 181 | render_heuristic_target 182 | render_heuristic_visited 183 | render_heuristic_walls 184 | render_heuristic_frontier 185 | render_heuristic_path 186 | end 187 | 188 | # This method handles user input every tick 189 | def input 190 | # Check and handle button input 191 | input_buttons 192 | 193 | # If the mouse was lifted this tick 194 | if inputs.mouse.up 195 | # Set current input to none 196 | state.user_input = :none 197 | end 198 | 199 | # If the mouse was clicked this tick 200 | if inputs.mouse.down 201 | # Determine what the user is editing and appropriately edit the state.user_input variable 202 | determine_input 203 | end 204 | 205 | # Process user input based on user_input variable and current mouse position 206 | process_input 207 | end 208 | 209 | # Determines what the user is editing 210 | # This method is called when the mouse is clicked down 211 | def determine_input 212 | if mouse_over_slider? 213 | state.user_input = :slider 214 | # If the mouse is over the star in the first grid 215 | elsif bfs_mouse_over_star? 216 | # The user is editing the star from the first grid 217 | state.user_input = :bfs_star 218 | # If the mouse is over the star in the second grid 219 | elsif heuristic_mouse_over_star? 220 | # The user is editing the star from the second grid 221 | state.user_input = :heuristic_star 222 | # If the mouse is over the target in the first grid 223 | elsif bfs_mouse_over_target? 224 | # The user is editing the target from the first grid 225 | state.user_input = :bfs_target 226 | # If the mouse is over the target in the second grid 227 | elsif heuristic_mouse_over_target? 228 | # The user is editing the target from the second grid 229 | state.user_input = :heuristic_target 230 | # If the mouse is over a wall in the first grid 231 | elsif bfs_mouse_over_wall? 232 | # The user is removing a wall from the first grid 233 | state.user_input = :bfs_remove_wall 234 | # If the mouse is over a wall in the second grid 235 | elsif heuristic_mouse_over_wall? 236 | # The user is removing a wall from the second grid 237 | state.user_input = :heuristic_remove_wall 238 | # If the mouse is over the first grid 239 | elsif bfs_mouse_over_grid? 240 | # The user is adding a wall from the first grid 241 | state.user_input = :bfs_add_wall 242 | # If the mouse is over the second grid 243 | elsif heuristic_mouse_over_grid? 244 | # The user is adding a wall from the second grid 245 | state.user_input = :heuristic_add_wall 246 | end 247 | end 248 | 249 | # Processes click and drag based on what the user is currently dragging 250 | def process_input 251 | if state.user_input == :slider 252 | process_input_slider 253 | elsif state.user_input == :bfs_star 254 | process_input_bfs_star 255 | elsif state.user_input == :heuristic_star 256 | process_input_heuristic_star 257 | elsif state.user_input == :bfs_target 258 | process_input_bfs_target 259 | elsif state.user_input == :heuristic_target 260 | process_input_heuristic_target 261 | elsif state.user_input == :bfs_remove_wall 262 | process_input_bfs_remove_wall 263 | elsif state.user_input == :heuristic_remove_wall 264 | process_input_heuristic_remove_wall 265 | elsif state.user_input == :bfs_add_wall 266 | process_input_bfs_add_wall 267 | elsif state.user_input == :heuristic_add_wall 268 | process_input_heuristic_add_wall 269 | end 270 | end 271 | 272 | def render_slider 273 | # Using primitives hides the line under the white circle of the slider 274 | # Draws the line 275 | outputs.primitives << [slider.x, slider.y, slider.x + slider.w, slider.y].line 276 | # The circle needs to be offset so that the center of the circle 277 | # overlaps the line instead of the upper right corner of the circle 278 | # The circle's x value is also moved based on the current seach step 279 | circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing) 280 | circle_y = (slider.y - slider.offset) 281 | circle_rect = [circle_x, circle_y, 37, 37] 282 | outputs.primitives << [circle_rect, 'circle-white.png'].sprite 283 | end 284 | 285 | def render_labels 286 | outputs.labels << [205, 625, "Breadth First Search"] 287 | outputs.labels << [820, 625, "Heuristic Best-First Search"] 288 | end 289 | 290 | def render_left_button 291 | # Draws the button_color button, and a black border 292 | # The border separates the buttons visually 293 | outputs.solids << [buttons.left, button_color] 294 | outputs.borders << [buttons.left] 295 | 296 | # Renders an explanatory label in the center of the button 297 | # Explains to the user what the button does 298 | # If the button size is changed, the label might need to be edited as well 299 | # to keep the label in the center of the button 300 | label_x = buttons.left.x + 20 301 | label_y = buttons.left.y + 35 302 | outputs.labels << [label_x, label_y, "<"] 303 | end 304 | 305 | def render_center_button 306 | # Draws the button_color button, and a black border 307 | # The border separates the buttons visually 308 | outputs.solids << [buttons.center, button_color] 309 | outputs.borders << [buttons.center] 310 | 311 | # Renders an explanatory label in the center of the button 312 | # Explains to the user what the button does 313 | # If the button size is changed, the label might need to be edited as well 314 | # to keep the label in the center of the button 315 | label_x = buttons.center.x + 37 316 | label_y = buttons.center.y + 35 317 | label_text = state.play ? "Pause Animation" : "Play Animation" 318 | outputs.labels << [label_x, label_y, label_text] 319 | end 320 | 321 | def render_right_button 322 | # Draws the button_color button, and a black border 323 | # The border separates the buttons visually 324 | outputs.solids << [buttons.right, button_color] 325 | outputs.borders << [buttons.right] 326 | 327 | # Renders an explanatory label in the center of the button 328 | # Explains to the user what the button does 329 | label_x = buttons.right.x + 20 330 | label_y = buttons.right.y + 35 331 | outputs.labels << [label_x, label_y, ">"] 332 | end 333 | 334 | def render_bfs_grid 335 | # A large rect the size of the grid 336 | outputs.solids << [bfs_scale_up(grid.rect), default_color] 337 | 338 | # The vertical grid lines 339 | for x in 0..grid.width 340 | outputs.lines << bfs_vertical_line(x) 341 | end 342 | 343 | # The horizontal grid lines 344 | for y in 0..grid.height 345 | outputs.lines << bfs_horizontal_line(y) 346 | end 347 | end 348 | 349 | def render_heuristic_grid 350 | # A large rect the size of the grid 351 | outputs.solids << [heuristic_scale_up(grid.rect), default_color] 352 | 353 | # The vertical grid lines 354 | for x in 0..grid.width 355 | outputs.lines << heuristic_vertical_line(x) 356 | end 357 | 358 | # The horizontal grid lines 359 | for y in 0..grid.height 360 | outputs.lines << heuristic_horizontal_line(y) 361 | end 362 | end 363 | 364 | # Returns a vertical line for a column of the first grid 365 | def bfs_vertical_line column 366 | bfs_scale_up([column, 0, column, grid.height]) 367 | end 368 | 369 | # Returns a horizontal line for a column of the first grid 370 | def bfs_horizontal_line row 371 | bfs_scale_up([0, row, grid.width, row]) 372 | end 373 | 374 | # Returns a vertical line for a column of the second grid 375 | def heuristic_vertical_line column 376 | bfs_scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height]) 377 | end 378 | 379 | # Returns a horizontal line for a column of the second grid 380 | def heuristic_horizontal_line row 381 | bfs_scale_up([grid.width + 1, row, grid.width + grid.width + 1, row]) 382 | end 383 | 384 | # Renders the star on the first grid 385 | def render_bfs_star 386 | outputs.sprites << [bfs_scale_up(grid.star), 'star.png'] 387 | end 388 | 389 | # Renders the star on the second grid 390 | def render_heuristic_star 391 | outputs.sprites << [heuristic_scale_up(grid.star), 'star.png'] 392 | end 393 | 394 | # Renders the target on the first grid 395 | def render_bfs_target 396 | outputs.sprites << [bfs_scale_up(grid.target), 'target.png'] 397 | end 398 | 399 | # Renders the target on the second grid 400 | def render_heuristic_target 401 | outputs.sprites << [heuristic_scale_up(grid.target), 'target.png'] 402 | end 403 | 404 | # Renders the walls on the first grid 405 | def render_bfs_walls 406 | grid.walls.each_key do | wall | 407 | outputs.solids << [bfs_scale_up(wall), wall_color] 408 | end 409 | end 410 | 411 | # Renders the walls on the second grid 412 | def render_heuristic_walls 413 | grid.walls.each_key do | wall | 414 | outputs.solids << [heuristic_scale_up(wall), wall_color] 415 | end 416 | end 417 | 418 | # Renders the visited cells on the first grid 419 | def render_bfs_visited 420 | bfs.came_from.each_key do | visited_cell | 421 | outputs.solids << [bfs_scale_up(visited_cell), visited_color] 422 | end 423 | end 424 | 425 | # Renders the visited cells on the second grid 426 | def render_heuristic_visited 427 | heuristic.came_from.each_key do | visited_cell | 428 | outputs.solids << [heuristic_scale_up(visited_cell), visited_color] 429 | end 430 | end 431 | 432 | # Renders the frontier cells on the first grid 433 | def render_bfs_frontier 434 | bfs.frontier.each do | frontier_cell | 435 | outputs.solids << [bfs_scale_up(frontier_cell), frontier_color, 200] 436 | end 437 | end 438 | 439 | # Renders the frontier cells on the second grid 440 | def render_heuristic_frontier 441 | heuristic.frontier.each do | frontier_cell | 442 | outputs.solids << [heuristic_scale_up(frontier_cell), frontier_color, 200] 443 | end 444 | end 445 | 446 | # Renders the path found by the breadth first search on the first grid 447 | def render_bfs_path 448 | bfs.path.each do | path | 449 | outputs.solids << [bfs_scale_up(path), path_color] 450 | end 451 | end 452 | 453 | # Renders the path found by the heuristic search on the second grid 454 | def render_heuristic_path 455 | heuristic.path.each do | path | 456 | outputs.solids << [heuristic_scale_up(path), path_color] 457 | end 458 | end 459 | 460 | # Returns the rect for the path between two cells based on their relative positions 461 | def get_path_between(cell_one, cell_two) 462 | path = [] 463 | 464 | # If cell one is above cell two 465 | if cell_one.x == cell_two.x and cell_one.y > cell_two.y 466 | # Path starts from the center of cell two and moves upward to the center of cell one 467 | path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4] 468 | # If cell one is below cell two 469 | elsif cell_one.x == cell_two.x and cell_one.y < cell_two.y 470 | # Path starts from the center of cell one and moves upward to the center of cell two 471 | path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4] 472 | # If cell one is to the left of cell two 473 | elsif cell_one.x > cell_two.x and cell_one.y == cell_two.y 474 | # Path starts from the center of cell two and moves rightward to the center of cell one 475 | path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4] 476 | # If cell one is to the right of cell two 477 | elsif cell_one.x < cell_two.x and cell_one.y == cell_two.y 478 | # Path starts from the center of cell one and moves rightward to the center of cell two 479 | path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4] 480 | end 481 | 482 | path 483 | end 484 | 485 | # In code, the cells are represented as 1x1 rectangles 486 | # When drawn, the cells are larger than 1x1 rectangles 487 | # This method is used to scale up cells, and lines 488 | # Objects are scaled up according to the grid.cell_size variable 489 | # This allows for easy customization of the visual scale of the grid 490 | # This method scales up cells for the first grid 491 | def bfs_scale_up(cell) 492 | # Prevents the original value of cell from being edited 493 | cell = cell.clone 494 | 495 | # If cell is just an x and y coordinate 496 | if cell.size == 2 497 | # Add a width and height of 1 498 | cell << 1 499 | cell << 1 500 | end 501 | 502 | # Scale all the values up 503 | cell.map! { |value| value * grid.cell_size } 504 | 505 | # Returns the scaled up cell 506 | cell 507 | end 508 | 509 | # Translates the given cell grid.width + 1 to the right and then scales up 510 | # Used to draw cells for the second grid 511 | # This method does not work for lines, 512 | # so separate methods exist for the grid lines 513 | def heuristic_scale_up(cell) 514 | # Prevents the original value of cell from being edited 515 | cell = cell.clone 516 | # Translates the cell to the second grid equivalent 517 | cell.x += grid.width + 1 518 | # Proceeds as if scaling up for the first grid 519 | bfs_scale_up(cell) 520 | end 521 | 522 | # Checks and handles input for the buttons 523 | # Called when the mouse is lifted 524 | def input_buttons 525 | input_left_button 526 | input_center_button 527 | input_right_button 528 | end 529 | 530 | # Checks if the previous step button is clicked 531 | # If it is, it pauses the animation and moves the search one step backward 532 | def input_left_button 533 | if left_button_clicked? 534 | state.play = false 535 | state.current_step -= 1 536 | recalculate_searches 537 | end 538 | end 539 | 540 | # Controls the play/pause button 541 | # Inverses whether the animation is playing or not when clicked 542 | def input_center_button 543 | if center_button_clicked? || inputs.keyboard.key_down.space 544 | state.play = !state.play 545 | end 546 | end 547 | 548 | # Checks if the next step button is clicked 549 | # If it is, it pauses the animation and moves the search one step forward 550 | def input_right_button 551 | if right_button_clicked? 552 | state.play = false 553 | state.current_step += 1 554 | move_searches_one_step_forward 555 | end 556 | end 557 | 558 | # These methods detect when the buttons are clicked 559 | def left_button_clicked? 560 | inputs.mouse.point.inside_rect?(buttons.left) && inputs.mouse.up 561 | end 562 | 563 | def center_button_clicked? 564 | inputs.mouse.point.inside_rect?(buttons.center) && inputs.mouse.up 565 | end 566 | 567 | def right_button_clicked? 568 | inputs.mouse.point.inside_rect?(buttons.right) && inputs.mouse.up 569 | end 570 | 571 | 572 | # Signal that the user is going to be moving the slider 573 | # Is the mouse over the circle of the slider? 574 | def mouse_over_slider? 575 | circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing) 576 | circle_y = (slider.y - slider.offset) 577 | circle_rect = [circle_x, circle_y, 37, 37] 578 | inputs.mouse.point.inside_rect?(circle_rect) 579 | end 580 | 581 | # Signal that the user is going to be moving the star from the first grid 582 | def bfs_mouse_over_star? 583 | inputs.mouse.point.inside_rect?(bfs_scale_up(grid.star)) 584 | end 585 | 586 | # Signal that the user is going to be moving the star from the second grid 587 | def heuristic_mouse_over_star? 588 | inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.star)) 589 | end 590 | 591 | # Signal that the user is going to be moving the target from the first grid 592 | def bfs_mouse_over_target? 593 | inputs.mouse.point.inside_rect?(bfs_scale_up(grid.target)) 594 | end 595 | 596 | # Signal that the user is going to be moving the target from the second grid 597 | def heuristic_mouse_over_target? 598 | inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.target)) 599 | end 600 | 601 | # Signal that the user is going to be removing walls from the first grid 602 | def bfs_mouse_over_wall? 603 | grid.walls.each_key do | wall | 604 | return true if inputs.mouse.point.inside_rect?(bfs_scale_up(wall)) 605 | end 606 | 607 | false 608 | end 609 | 610 | # Signal that the user is going to be removing walls from the second grid 611 | def heuristic_mouse_over_wall? 612 | grid.walls.each_key do | wall | 613 | return true if inputs.mouse.point.inside_rect?(heuristic_scale_up(wall)) 614 | end 615 | 616 | false 617 | end 618 | 619 | # Signal that the user is going to be adding walls from the first grid 620 | def bfs_mouse_over_grid? 621 | inputs.mouse.point.inside_rect?(bfs_scale_up(grid.rect)) 622 | end 623 | 624 | # Signal that the user is going to be adding walls from the second grid 625 | def heuristic_mouse_over_grid? 626 | inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.rect)) 627 | end 628 | 629 | # This method is called when the user is editing the slider 630 | # It pauses the animation and moves the white circle to the closest integer point 631 | # on the slider 632 | # Changes the step of the search to be animated 633 | def process_input_slider 634 | state.play = false 635 | mouse_x = inputs.mouse.point.x 636 | 637 | # Bounds the mouse_x to the closest x value on the slider line 638 | mouse_x = slider.x if mouse_x < slider.x 639 | mouse_x = slider.x + slider.w if mouse_x > slider.x + slider.w 640 | 641 | # Sets the current search step to the one represented by the mouse x value 642 | # The slider's circle moves due to the render_slider method using anim_steps 643 | state.current_step = ((mouse_x - slider.x) / slider.spacing).to_i 644 | 645 | recalculate_searches 646 | end 647 | 648 | # Moves the star to the cell closest to the mouse in the first grid 649 | # Only resets the search if the star changes position 650 | # Called whenever the user is editing the star (puts mouse down on star) 651 | def process_input_bfs_star 652 | old_star = grid.star.clone 653 | unless bfs_cell_closest_to_mouse == grid.target 654 | grid.star = bfs_cell_closest_to_mouse 655 | end 656 | unless old_star == grid.star 657 | recalculate_searches 658 | end 659 | end 660 | 661 | # Moves the star to the cell closest to the mouse in the second grid 662 | # Only resets the search if the star changes position 663 | # Called whenever the user is editing the star (puts mouse down on star) 664 | def process_input_heuristic_star 665 | old_star = grid.star.clone 666 | unless heuristic_cell_closest_to_mouse == grid.target 667 | grid.star = heuristic_cell_closest_to_mouse 668 | end 669 | unless old_star == grid.star 670 | recalculate_searches 671 | end 672 | end 673 | 674 | # Moves the target to the grid closest to the mouse in the first grid 675 | # Only recalculate_searchess the search if the target changes position 676 | # Called whenever the user is editing the target (puts mouse down on target) 677 | def process_input_bfs_target 678 | old_target = grid.target.clone 679 | unless bfs_cell_closest_to_mouse == grid.star 680 | grid.target = bfs_cell_closest_to_mouse 681 | end 682 | unless old_target == grid.target 683 | recalculate_searches 684 | end 685 | end 686 | 687 | # Moves the target to the cell closest to the mouse in the second grid 688 | # Only recalculate_searchess the search if the target changes position 689 | # Called whenever the user is editing the target (puts mouse down on target) 690 | def process_input_heuristic_target 691 | old_target = grid.target.clone 692 | unless heuristic_cell_closest_to_mouse == grid.star 693 | grid.target = heuristic_cell_closest_to_mouse 694 | end 695 | unless old_target == grid.target 696 | recalculate_searches 697 | end 698 | end 699 | 700 | # Removes walls in the first grid that are under the cursor 701 | def process_input_bfs_remove_wall 702 | # The mouse needs to be inside the grid, because we only want to remove walls 703 | # the cursor is directly over 704 | # Recalculations should only occur when a wall is actually deleted 705 | if bfs_mouse_over_grid? 706 | if grid.walls.has_key?(bfs_cell_closest_to_mouse) 707 | grid.walls.delete(bfs_cell_closest_to_mouse) 708 | recalculate_searches 709 | end 710 | end 711 | end 712 | 713 | # Removes walls in the second grid that are under the cursor 714 | def process_input_heuristic_remove_wall 715 | # The mouse needs to be inside the grid, because we only want to remove walls 716 | # the cursor is directly over 717 | # Recalculations should only occur when a wall is actually deleted 718 | if heuristic_mouse_over_grid? 719 | if grid.walls.has_key?(heuristic_cell_closest_to_mouse) 720 | grid.walls.delete(heuristic_cell_closest_to_mouse) 721 | recalculate_searches 722 | end 723 | end 724 | end 725 | # Adds a wall in the first grid in the cell the mouse is over 726 | def process_input_bfs_add_wall 727 | if bfs_mouse_over_grid? 728 | unless grid.walls.has_key?(bfs_cell_closest_to_mouse) 729 | grid.walls[bfs_cell_closest_to_mouse] = true 730 | recalculate_searches 731 | end 732 | end 733 | end 734 | 735 | # Adds a wall in the second grid in the cell the mouse is over 736 | def process_input_heuristic_add_wall 737 | if heuristic_mouse_over_grid? 738 | unless grid.walls.has_key?(heuristic_cell_closest_to_mouse) 739 | grid.walls[heuristic_cell_closest_to_mouse] = true 740 | recalculate_searches 741 | end 742 | end 743 | end 744 | 745 | # When the user grabs the star and puts their cursor to the far right 746 | # and moves up and down, the star is supposed to move along the grid as well 747 | # Finding the cell closest to the mouse helps with this 748 | def bfs_cell_closest_to_mouse 749 | # Closest cell to the mouse in the first grid 750 | x = (inputs.mouse.point.x / grid.cell_size).to_i 751 | y = (inputs.mouse.point.y / grid.cell_size).to_i 752 | # Bound x and y to the grid 753 | x = grid.width - 1 if x > grid.width - 1 754 | y = grid.height - 1 if y > grid.height - 1 755 | # Return closest cell 756 | [x, y] 757 | end 758 | 759 | # When the user grabs the star and puts their cursor to the far right 760 | # and moves up and down, the star is supposed to move along the grid as well 761 | # Finding the cell closest to the mouse in the second grid helps with this 762 | def heuristic_cell_closest_to_mouse 763 | # Closest cell grid to the mouse in the second 764 | x = (inputs.mouse.point.x / grid.cell_size).to_i 765 | y = (inputs.mouse.point.y / grid.cell_size).to_i 766 | # Translate the cell to the first grid 767 | x -= grid.width + 1 768 | # Bound x and y to the first grid 769 | x = 0 if x < 0 770 | y = 0 if y < 0 771 | x = grid.width - 1 if x > grid.width - 1 772 | y = grid.height - 1 if y > grid.height - 1 773 | # Return closest cell 774 | [x, y] 775 | end 776 | 777 | def recalculate_searches 778 | # Reset the searches 779 | bfs.came_from = {} 780 | bfs.frontier = [] 781 | bfs.path = [] 782 | heuristic.came_from = {} 783 | heuristic.frontier = [] 784 | heuristic.path = [] 785 | 786 | # Move the searches forward to the current step 787 | state.current_step.times { move_searches_one_step_forward } 788 | end 789 | 790 | def move_searches_one_step_forward 791 | bfs_one_step_forward 792 | heuristic_one_step_forward 793 | end 794 | 795 | def bfs_one_step_forward 796 | return if bfs.came_from.has_key?(grid.target) 797 | 798 | # Only runs at the beginning of the search as setup. 799 | if bfs.came_from.empty? 800 | bfs.frontier << grid.star 801 | bfs.came_from[grid.star] = nil 802 | end 803 | 804 | # A step in the search 805 | unless bfs.frontier.empty? 806 | # Takes the next frontier cell 807 | new_frontier = bfs.frontier.shift 808 | # For each of its neighbors 809 | adjacent_neighbors(new_frontier).each do |neighbor| 810 | # That have not been visited and are not walls 811 | unless bfs.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) 812 | # Add them to the frontier and mark them as visited 813 | bfs.frontier << neighbor 814 | bfs.came_from[neighbor] = new_frontier 815 | end 816 | end 817 | end 818 | 819 | # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line 820 | # Comment this line and let a path generate to see the difference 821 | bfs.frontier = bfs.frontier.sort_by {| cell | proximity_to_star(cell) } 822 | 823 | # If the search found the target 824 | if bfs.came_from.has_key?(grid.target) 825 | # Calculate the path between the target and star 826 | bfs_calc_path 827 | end 828 | end 829 | 830 | # Calculates the path between the target and star for the breadth first search 831 | # Only called when the breadth first search finds the target 832 | def bfs_calc_path 833 | # Start from the target 834 | endpoint = grid.target 835 | # And the cell it came from 836 | next_endpoint = bfs.came_from[endpoint] 837 | while endpoint and next_endpoint 838 | # Draw a path between these two cells and store it 839 | path = get_path_between(endpoint, next_endpoint) 840 | bfs.path << path 841 | # And get the next pair of cells 842 | endpoint = next_endpoint 843 | next_endpoint = bfs.came_from[endpoint] 844 | # Continue till there are no more cells 845 | end 846 | end 847 | 848 | # Moves the heuristic search forward one step 849 | # Can be called from tick while the animation is playing 850 | # Can also be called when recalculating the searches after the user edited the grid 851 | def heuristic_one_step_forward 852 | # Stop the search if the target has been found 853 | return if heuristic.came_from.has_key?(grid.target) 854 | 855 | # If the search has not begun 856 | if heuristic.came_from.empty? 857 | # Setup the search to begin from the star 858 | heuristic.frontier << grid.star 859 | heuristic.came_from[grid.star] = nil 860 | end 861 | 862 | # One step in the heuristic search 863 | 864 | # Unless there are no more cells to explore from 865 | unless heuristic.frontier.empty? 866 | # Get the next cell to explore from 867 | new_frontier = heuristic.frontier.shift 868 | # For each of its neighbors 869 | adjacent_neighbors(new_frontier).each do |neighbor| 870 | # That have not been visited and are not walls 871 | unless heuristic.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor) 872 | # Add them to the frontier and mark them as visited 873 | heuristic.frontier << neighbor 874 | heuristic.came_from[neighbor] = new_frontier 875 | end 876 | end 877 | end 878 | 879 | # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line 880 | heuristic.frontier = heuristic.frontier.sort_by {| cell | proximity_to_star(cell) } 881 | # Sort the frontier so cells that are close to the target are then prioritized 882 | heuristic.frontier = heuristic.frontier.sort_by {| cell | heuristic_heuristic(cell) } 883 | 884 | # If the search found the target 885 | if heuristic.came_from.has_key?(grid.target) 886 | # Calculate the path between the target and star 887 | heuristic_calc_path 888 | end 889 | end 890 | 891 | # Returns one-dimensional absolute distance between cell and target 892 | # Returns a number to compare distances between cells and the target 893 | def heuristic_heuristic(cell) 894 | (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs 895 | end 896 | 897 | # Calculates the path between the target and star for the heuristic search 898 | # Only called when the heuristic search finds the target 899 | def heuristic_calc_path 900 | # Start from the target 901 | endpoint = grid.target 902 | # And the cell it came from 903 | next_endpoint = heuristic.came_from[endpoint] 904 | while endpoint and next_endpoint 905 | # Draw a path between these two cells and store it 906 | path = get_path_between(endpoint, next_endpoint) 907 | heuristic.path << path 908 | # And get the next pair of cells 909 | endpoint = next_endpoint 910 | next_endpoint = heuristic.came_from[endpoint] 911 | # Continue till there are no more cells 912 | end 913 | end 914 | 915 | # Returns a list of adjacent cells 916 | # Used to determine what the next cells to be added to the frontier are 917 | def adjacent_neighbors(cell) 918 | neighbors = [] 919 | 920 | # Gets all the valid neighbors into the array 921 | # From southern neighbor, clockwise 922 | neighbors << [cell.x , cell.y - 1] unless cell.y == 0 923 | neighbors << [cell.x - 1, cell.y ] unless cell.x == 0 924 | neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1 925 | neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1 926 | 927 | neighbors 928 | end 929 | 930 | # Finds the vertical and horizontal distance of a cell from the star 931 | # and returns the larger value 932 | # This method is used to have a zigzag pattern in the rendered path 933 | # A cell that is [5, 5] from the star, 934 | # is explored before over a cell that is [0, 7] away. 935 | # So, if possible, the search tries to go diagonal (zigzag) first 936 | def proximity_to_star(cell) 937 | distance_x = (grid.star.x - cell.x).abs 938 | distance_y = (grid.star.y - cell.y).abs 939 | 940 | if distance_x > distance_y 941 | return distance_x 942 | else 943 | return distance_y 944 | end 945 | end 946 | 947 | # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored. 948 | def grid 949 | state.grid 950 | end 951 | 952 | def buttons 953 | state.buttons 954 | end 955 | 956 | def slider 957 | state.slider 958 | end 959 | 960 | def bfs 961 | state.bfs 962 | end 963 | 964 | def heuristic 965 | state.heuristic 966 | end 967 | 968 | # Descriptive aliases for colors 969 | def default_color 970 | [221, 212, 213] # Light Brown 971 | end 972 | 973 | def wall_color 974 | [134, 134, 120] # Camo Green 975 | end 976 | 977 | def visited_color 978 | [204, 191, 179] # Dark Brown 979 | end 980 | 981 | def frontier_color 982 | [103, 136, 204] # Blue 983 | end 984 | 985 | def path_color 986 | [231, 230, 228] # Pastel White 987 | end 988 | 989 | def button_color 990 | [190, 190, 190] # Gray 991 | end 992 | end 993 | # Method that is called by DragonRuby periodically 994 | # Used for updating animations and calculations 995 | def tick args 996 | 997 | # Pressing r will reset the application 998 | if args.inputs.keyboard.key_down.r 999 | args.gtk.reset 1000 | reset 1001 | return 1002 | end 1003 | 1004 | # Every tick, new args are passed, and the Breadth First Search tick is called 1005 | $heuristic ||= Heuristic.new(args) 1006 | $heuristic.args = args 1007 | $heuristic.tick 1008 | end 1009 | 1010 | 1011 | def reset 1012 | $heuristic = nil 1013 | end 1014 | -------------------------------------------------------------------------------- /Heuristic_With_Walls/circle-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Heuristic_With_Walls/circle-white.png -------------------------------------------------------------------------------- /Heuristic_With_Walls/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Heuristic_With_Walls/star.png -------------------------------------------------------------------------------- /Heuristic_With_Walls/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Heuristic_With_Walls/target.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sujay Vadlakonda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Movement_Costs/app/main.rb: -------------------------------------------------------------------------------- 1 | # Demonstrates how Dijkstra's Algorithm allows movement costs to be considered 2 | 3 | # Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html 4 | 5 | # The first grid is a breadth first search with an early exit. 6 | # It shows a heat map of all the cells that were visited by the search and their relative distance. 7 | 8 | # The second grid is an implementation of Dijkstra's algorithm. 9 | # Light green cells have 5 times the movement cost of regular cells. 10 | # The heat map will darken based on movement cost. 11 | 12 | # Dark green cells are walls, and the search cannot go through them. 13 | class Movement_Costs 14 | attr_gtk 15 | 16 | # This method is called every frame/tick 17 | # Every tick, the current state of the search is rendered on the screen, 18 | # User input is processed, and 19 | # The next step in the search is calculated 20 | def tick 21 | defaults 22 | render 23 | input 24 | calc 25 | end 26 | 27 | def defaults 28 | # Variables to edit the size and appearance of the grid 29 | # Freely customizable to user's liking 30 | grid.width ||= 10 31 | grid.height ||= 10 32 | grid.cell_size ||= 60 33 | grid.rect ||= [0, 0, grid.width, grid.height] 34 | 35 | # The location of the star and walls of the grid 36 | # They can be modified to have a different initial grid 37 | # Walls are stored in a hash for quick look up when doing the search 38 | state.star ||= [1, 5] 39 | state.target ||= [8, 4] 40 | state.walls ||= {[1, 1] => true, [2, 1] => true, [3, 1] => true, [1, 2] => true, [2, 2] => true, [3, 2] => true} 41 | state.hills ||= { 42 | [4, 1] => true, 43 | [5, 1] => true, 44 | [4, 2] => true, 45 | [5, 2] => true, 46 | [6, 2] => true, 47 | [4, 3] => true, 48 | [5, 3] => true, 49 | [6, 3] => true, 50 | [3, 4] => true, 51 | [4, 4] => true, 52 | [5, 4] => true, 53 | [6, 4] => true, 54 | [7, 4] => true, 55 | [3, 5] => true, 56 | [4, 5] => true, 57 | [5, 5] => true, 58 | [6, 5] => true, 59 | [7, 5] => true, 60 | [4, 6] => true, 61 | [5, 6] => true, 62 | [6, 6] => true, 63 | [7, 6] => true, 64 | [4, 7] => true, 65 | [5, 7] => true, 66 | [6, 7] => true, 67 | [4, 8] => true, 68 | [5, 8] => true, 69 | } 70 | 71 | # What the user is currently editing on the grid 72 | # We store this value, because we want to remember the value even when 73 | # the user's cursor is no longer over what they're interacting with, but 74 | # they are still clicking down on the mouse. 75 | state.user_input ||= :none 76 | 77 | # Values that are used for the breadth first search 78 | # Keeping track of what cells were visited prevents counting cells multiple times 79 | breadth_first_search.visited ||= {} 80 | # The cells from which the breadth first search will expand 81 | breadth_first_search.frontier ||= [] 82 | # Keeps track of which cell all cells were searched from 83 | # Used to recreate the path from the target to the star 84 | breadth_first_search.came_from ||= {} 85 | 86 | # Keeps track of the movement cost so far to be at a cell 87 | # Allows the costs of new cells to be quickly calculated 88 | # Also doubles as a way to check if cells have already been visited 89 | dijkstra_search.cost_so_far ||= {} 90 | # The cells from which the Dijkstra search will expand 91 | dijkstra_search.frontier ||= [] 92 | # Keeps track of which cell all cells were searched from 93 | # Used to recreate the path from the target to the star 94 | dijkstra_search.came_from ||= {} 95 | end 96 | 97 | # Draws everything onto the screen 98 | def render 99 | render_background 100 | 101 | render_heat_maps 102 | 103 | render_star 104 | render_target 105 | render_hills 106 | render_walls 107 | 108 | render_paths 109 | end 110 | # The methods below subdivide the task of drawing everything to the screen 111 | 112 | # Draws what the grid looks like with nothing on it 113 | def render_background 114 | render_unvisited 115 | render_grid_lines 116 | render_labels 117 | end 118 | 119 | # Draws two rectangles the size of the grid in the default cell color 120 | # Used as part of the background 121 | def render_unvisited 122 | outputs.solids << [scale_up(grid.rect), unvisited_color] 123 | outputs.solids << [move_and_scale_up(grid.rect), unvisited_color] 124 | end 125 | 126 | # Draws grid lines to show the division of the grid into cells 127 | def render_grid_lines 128 | for x in 0..grid.width 129 | outputs.lines << vertical_line(x) 130 | outputs.lines << shifted_vertical_line(x) 131 | end 132 | 133 | for y in 0..grid.height 134 | outputs.lines << horizontal_line(y) 135 | outputs.lines << shifted_horizontal_line(y) 136 | end 137 | end 138 | 139 | # Easy way to draw vertical lines given an index for the first grid 140 | def vertical_line column 141 | scale_up([column, 0, column, grid.height]) 142 | end 143 | 144 | # Easy way to draw horizontal lines given an index for the second grid 145 | def horizontal_line row 146 | scale_up([0, row, grid.width, row]) 147 | end 148 | 149 | # Easy way to draw vertical lines given an index for the first grid 150 | def shifted_vertical_line column 151 | scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height]) 152 | end 153 | 154 | # Easy way to draw horizontal lines given an index for the second grid 155 | def shifted_horizontal_line row 156 | scale_up([grid.width + 1, row, grid.width + grid.width + 1, row]) 157 | end 158 | 159 | # Labels the grids 160 | def render_labels 161 | outputs.labels << [175, 650, "Number of steps", 3] 162 | outputs.labels << [925, 650, "Distance", 3] 163 | end 164 | 165 | def render_paths 166 | render_breadth_first_search_path 167 | render_dijkstra_path 168 | end 169 | 170 | def render_heat_maps 171 | render_breadth_first_search_heat_map 172 | render_dijkstra_heat_map 173 | end 174 | 175 | # Renders the breadth first search on the first grid 176 | def render_breadth_first_search 177 | end 178 | 179 | # This heat map shows the cells explored by the breadth first search and how far they are from the star. 180 | def render_breadth_first_search_heat_map 181 | # For each cell explored 182 | breadth_first_search.visited.each_key do | visited_cell | 183 | # Find its distance from the star 184 | distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs 185 | max_distance = grid.width + grid.height 186 | # Get it as a percent of the maximum distance and scale to 255 for use as an alpha value 187 | alpha = 255.to_i * distance.to_i / max_distance.to_i 188 | outputs.solids << [scale_up(visited_cell), red, alpha] 189 | end 190 | end 191 | 192 | def render_breadth_first_search_path 193 | # If the search found the target 194 | if breadth_first_search.visited.has_key?(state.target) 195 | # Start from the target 196 | endpoint = state.target 197 | # And the cell it came from 198 | next_endpoint = breadth_first_search.came_from[endpoint] 199 | while endpoint and next_endpoint 200 | # Draw a path between these two cells 201 | path = get_path_between(endpoint, next_endpoint) 202 | outputs.solids << [scale_up(path), path_color] 203 | # And get the next pair of cells 204 | endpoint = next_endpoint 205 | next_endpoint = breadth_first_search.came_from[endpoint] 206 | # Continue till there are no more cells 207 | end 208 | end 209 | end 210 | 211 | # Renders the Dijkstra search on the second grid 212 | def render_dijkstra 213 | end 214 | 215 | def render_dijkstra_heat_map 216 | dijkstra_search.cost_so_far.each do |visited_cell, cost| 217 | max_cost = (grid.width + grid.height) #* 5 218 | alpha = 255.to_i * cost.to_i / max_cost.to_i 219 | outputs.solids << [move_and_scale_up(visited_cell), red, alpha] 220 | end 221 | end 222 | 223 | def render_dijkstra_path 224 | # If the search found the target 225 | if dijkstra_search.came_from.has_key?(state.target) 226 | # Get the target and the cell it came from 227 | endpoint = state.target 228 | next_endpoint = dijkstra_search.came_from[endpoint] 229 | while endpoint and next_endpoint 230 | # Draw a path between them 231 | path = get_path_between(endpoint, next_endpoint) 232 | outputs.solids << [move_and_scale_up(path), path_color] 233 | 234 | # Shift one cell down the path 235 | endpoint = next_endpoint 236 | next_endpoint = dijkstra_search.came_from[endpoint] 237 | 238 | # Repeat till the end of the path 239 | end 240 | end 241 | end 242 | 243 | # Renders the star on both grids 244 | def render_star 245 | outputs.sprites << [scale_up(state.star), 'star.png'] 246 | outputs.sprites << [move_and_scale_up(state.star), 'star.png'] 247 | end 248 | 249 | # Renders the target on both grids 250 | def render_target 251 | outputs.sprites << [scale_up(state.target), 'target.png'] 252 | outputs.sprites << [move_and_scale_up(state.target), 'target.png'] 253 | end 254 | 255 | def render_hills 256 | state.hills.each_key do |hill| 257 | outputs.solids << [scale_up(hill), hill_color] 258 | outputs.solids << [move_and_scale_up(hill), hill_color] 259 | end 260 | end 261 | 262 | # Draws the walls on both grids 263 | def render_walls 264 | state.walls.each_key do |wall| 265 | outputs.solids << [scale_up(wall), wall_color] 266 | outputs.solids << [move_and_scale_up(wall), wall_color] 267 | end 268 | end 269 | 270 | def get_path_between(cell_one, cell_two) 271 | path = nil 272 | if cell_one.x == cell_two.x 273 | if cell_one.y < cell_two.y 274 | path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4] 275 | else 276 | path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4] 277 | end 278 | else 279 | if cell_one.x < cell_two.x 280 | path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4] 281 | else 282 | path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4] 283 | end 284 | end 285 | path 286 | end 287 | 288 | # Representation of how far away visited cells are from the star 289 | # Replaces the render_visited method 290 | # Visually demonstrates the effectiveness of early exit for pathfinding 291 | def render_breadth_first_search_heat_map 292 | breadth_first_search.visited.each_key do | visited_cell | 293 | distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs 294 | max_distance = grid.width + grid.height 295 | alpha = 255.to_i * distance.to_i / max_distance.to_i 296 | outputs.solids << [scale_up(visited_cell), red, alpha] 297 | end 298 | end 299 | 300 | # Translates the given cell grid.width + 1 to the right and then scales up 301 | # Used to draw cells for the second grid 302 | # This method does not work for lines, 303 | # so separate methods exist for the grid lines 304 | def move_and_scale_up(cell) 305 | cell_clone = cell.clone 306 | cell_clone.x += grid.width + 1 307 | scale_up(cell_clone) 308 | end 309 | 310 | # In code, the cells are represented as 1x1 rectangles 311 | # When drawn, the cells are larger than 1x1 rectangles 312 | # This method is used to scale up cells, and lines 313 | # Objects are scaled up according to the grid.cell_size variable 314 | # This allows for easy customization of the visual scale of the grid 315 | def scale_up(cell) 316 | # Prevents the original value of cell from being edited 317 | cell = cell.clone 318 | 319 | # If cell is just an x and y coordinate 320 | if cell.size == 2 321 | # Add a width and height of 1 322 | cell << 1 323 | cell << 1 324 | end 325 | 326 | # Scale all the values up 327 | cell.map! { |value| value * grid.cell_size } 328 | 329 | # Returns the scaled up cell 330 | cell 331 | end 332 | 333 | # Handles user input every tick so the grid can be edited 334 | # Separate input detection and processing is needed 335 | # For example: Adding walls is started by clicking down on a hill, 336 | # but the mouse doesn't need to remain over hills to add walls 337 | def input 338 | # If the mouse was lifted this tick 339 | if inputs.mouse.up 340 | # Set current input to none 341 | state.user_input = :none 342 | end 343 | 344 | # If the mouse was clicked this tick 345 | if inputs.mouse.down 346 | # Determine what the user is editing and edit the state.user_input variable 347 | determine_input 348 | end 349 | 350 | # Process user input based on user_input variable and current mouse position 351 | process_input 352 | end 353 | 354 | # Determines what the user is editing and stores the value 355 | # This method is called the tick the mouse is clicked 356 | # Storing the value allows the user to continue the same edit as long as the 357 | # mouse left click is held 358 | def determine_input 359 | # If the mouse is over the star in the first grid 360 | if mouse_over_star? 361 | # The user is editing the star from the first grid 362 | state.user_input = :star 363 | # If the mouse is over the star in the second grid 364 | elsif mouse_over_star2? 365 | # The user is editing the star from the second grid 366 | state.user_input = :star2 367 | # If the mouse is over the target in the first grid 368 | elsif mouse_over_target? 369 | # The user is editing the target from the first grid 370 | state.user_input = :target 371 | # If the mouse is over the target in the second grid 372 | elsif mouse_over_target2? 373 | # The user is editing the target from the second grid 374 | state.user_input = :target2 375 | # If the mouse is over a wall in the first grid 376 | elsif mouse_over_wall? 377 | # The user is removing a wall from the first grid 378 | state.user_input = :remove_wall 379 | # If the mouse is over a wall in the second grid 380 | elsif mouse_over_wall2? 381 | # The user is removing a wall from the second grid 382 | state.user_input = :remove_wall2 383 | # If the mouse is over a hill in the first grid 384 | elsif mouse_over_hill? 385 | # The user is adding a wall from the first grid 386 | state.user_input = :add_wall 387 | # If the mouse is over a hill in the second grid 388 | elsif mouse_over_hill2? 389 | # The user is adding a wall from the second grid 390 | state.user_input = :add_wall2 391 | # If the mouse is over the first grid 392 | elsif mouse_over_grid? 393 | # The user is adding a hill from the first grid 394 | state.user_input = :add_hill 395 | # If the mouse is over the second grid 396 | elsif mouse_over_grid2? 397 | # The user is adding a hill from the second grid 398 | state.user_input = :add_hill2 399 | end 400 | end 401 | 402 | # Processes click and drag based on what the user is currently dragging 403 | def process_input 404 | if state.user_input == :star 405 | input_star 406 | elsif state.user_input == :star2 407 | input_star2 408 | elsif state.user_input == :target 409 | input_target 410 | elsif state.user_input == :target2 411 | input_target2 412 | elsif state.user_input == :remove_wall 413 | input_remove_wall 414 | elsif state.user_input == :remove_wall2 415 | input_remove_wall2 416 | elsif state.user_input == :add_hill 417 | input_add_hill 418 | elsif state.user_input == :add_hill2 419 | input_add_hill2 420 | elsif state.user_input == :add_wall 421 | input_add_wall 422 | elsif state.user_input == :add_wall2 423 | input_add_wall2 424 | end 425 | end 426 | 427 | # Calculates the two searches 428 | def calc 429 | # If the searches have not started 430 | if breadth_first_search.visited.empty? 431 | # Calculate the two searches 432 | calc_breadth_first 433 | calc_dijkstra 434 | end 435 | end 436 | 437 | 438 | def calc_breadth_first 439 | # Sets up the Breadth First Search 440 | breadth_first_search.visited[state.star] = true 441 | breadth_first_search.frontier << state.star 442 | breadth_first_search.came_from[state.star] = nil 443 | 444 | until breadth_first_search.frontier.empty? 445 | return if breadth_first_search.visited.has_key?(state.target) 446 | # A step in the search 447 | # Takes the next frontier cell 448 | new_frontier = breadth_first_search.frontier.shift 449 | # For each of its neighbors 450 | adjacent_neighbors(new_frontier).each do | neighbor | 451 | # That have not been visited and are not walls 452 | unless breadth_first_search.visited.has_key?(neighbor) || state.walls.has_key?(neighbor) 453 | # Add them to the frontier and mark them as visited in the first grid 454 | breadth_first_search.visited[neighbor] = true 455 | breadth_first_search.frontier << neighbor 456 | # Remember which cell the neighbor came from 457 | breadth_first_search.came_from[neighbor] = new_frontier 458 | end 459 | end 460 | end 461 | end 462 | 463 | # Calculates the Dijkstra Search from the beginning to the end 464 | 465 | def calc_dijkstra 466 | # The initial values for the Dijkstra search 467 | dijkstra_search.frontier << [state.star, 0] 468 | dijkstra_search.came_from[state.star] = nil 469 | dijkstra_search.cost_so_far[state.star] = 0 470 | 471 | # Until their are no more cells to be explored 472 | until dijkstra_search.frontier.empty? 473 | # Get the next cell to be explored from 474 | # We get the first element of the array which is the cell. The second element is the priority. 475 | current = dijkstra_search.frontier.shift[0] 476 | 477 | # Stop the search if we found the target 478 | return if current == state.target 479 | 480 | # For each of the neighbors 481 | adjacent_neighbors(current).each do | neighbor | 482 | # Unless this cell is a wall or has already been explored. 483 | unless dijkstra_search.came_from.has_key?(neighbor) or state.walls.has_key?(neighbor) 484 | # Calculate the movement cost of getting to this cell and memo 485 | new_cost = dijkstra_search.cost_so_far[current] + cost(neighbor) 486 | dijkstra_search.cost_so_far[neighbor] = new_cost 487 | 488 | # Add this neighbor to the cells too be explored 489 | dijkstra_search.frontier << [neighbor, new_cost] 490 | dijkstra_search.came_from[neighbor] = current 491 | end 492 | end 493 | 494 | # Sort the frontier so exploration occurs that have a low cost so far. 495 | # My implementation of a priority queue 496 | dijkstra_search.frontier = dijkstra_search.frontier.sort_by {|cell, priority| priority} 497 | end 498 | end 499 | 500 | def cost(cell) 501 | if state.hills.has_key?(cell) 502 | return 5 503 | else 504 | return 1 505 | end 506 | end 507 | 508 | 509 | 510 | 511 | # Moves the star to the cell closest to the mouse in the first grid 512 | # Only resets the search if the star changes position 513 | # Called whenever the user is editing the star (puts mouse down on star) 514 | def input_star 515 | old_star = state.star.clone 516 | unless cell_closest_to_mouse == state.target 517 | state.star = cell_closest_to_mouse 518 | end 519 | unless old_star == state.star 520 | reset_search 521 | end 522 | end 523 | 524 | # Moves the star to the cell closest to the mouse in the second grid 525 | # Only resets the search if the star changes position 526 | # Called whenever the user is editing the star (puts mouse down on star) 527 | def input_star2 528 | old_star = state.star.clone 529 | unless cell_closest_to_mouse2 == state.target 530 | state.star = cell_closest_to_mouse2 531 | end 532 | unless old_star == state.star 533 | reset_search 534 | end 535 | end 536 | 537 | # Moves the target to the grid closest to the mouse in the first grid 538 | # Only reset_searchs the search if the target changes position 539 | # Called whenever the user is editing the target (puts mouse down on target) 540 | def input_target 541 | old_target = state.target.clone 542 | unless cell_closest_to_mouse == state.star 543 | state.target = cell_closest_to_mouse 544 | end 545 | unless old_target == state.target 546 | reset_search 547 | end 548 | end 549 | 550 | # Moves the target to the cell closest to the mouse in the second grid 551 | # Only reset_searchs the search if the target changes position 552 | # Called whenever the user is editing the target (puts mouse down on target) 553 | def input_target2 554 | old_target = state.target.clone 555 | unless cell_closest_to_mouse2 == state.star 556 | state.target = cell_closest_to_mouse2 557 | end 558 | unless old_target == state.target 559 | reset_search 560 | end 561 | end 562 | 563 | # Removes walls in the first grid that are under the cursor 564 | def input_remove_wall 565 | # The mouse needs to be inside the grid, because we only want to remove walls 566 | # the cursor is directly over 567 | # Recalculations should only occur when a wall is actually deleted 568 | if mouse_over_grid? 569 | if state.walls.has_key?(cell_closest_to_mouse) or state.hills.has_key?(cell_closest_to_mouse) 570 | state.walls.delete(cell_closest_to_mouse) 571 | state.hills.delete(cell_closest_to_mouse) 572 | reset_search 573 | end 574 | end 575 | end 576 | 577 | # Removes walls in the second grid that are under the cursor 578 | def input_remove_wall2 579 | # The mouse needs to be inside the grid, because we only want to remove walls 580 | # the cursor is directly over 581 | # Recalculations should only occur when a wall is actually deleted 582 | if mouse_over_grid2? 583 | if state.walls.has_key?(cell_closest_to_mouse2) or state.hills.has_key?(cell_closest_to_mouse2) 584 | state.walls.delete(cell_closest_to_mouse2) 585 | state.hills.delete(cell_closest_to_mouse2) 586 | reset_search 587 | end 588 | end 589 | end 590 | 591 | # Adds a hill in the first grid in the cell the mouse is over 592 | def input_add_hill 593 | if mouse_over_grid? 594 | unless state.hills.has_key?(cell_closest_to_mouse) 595 | state.hills[cell_closest_to_mouse] = true 596 | reset_search 597 | end 598 | end 599 | end 600 | 601 | 602 | # Adds a hill in the second grid in the cell the mouse is over 603 | def input_add_hill2 604 | if mouse_over_grid2? 605 | unless state.hills.has_key?(cell_closest_to_mouse2) 606 | state.hills[cell_closest_to_mouse2] = true 607 | reset_search 608 | end 609 | end 610 | end 611 | 612 | # Adds a wall in the first grid in the cell the mouse is over 613 | def input_add_wall 614 | if mouse_over_grid? 615 | unless state.walls.has_key?(cell_closest_to_mouse) 616 | state.hills.delete(cell_closest_to_mouse) 617 | state.walls[cell_closest_to_mouse] = true 618 | reset_search 619 | end 620 | end 621 | end 622 | 623 | # Adds a wall in the second grid in the cell the mouse is over 624 | def input_add_wall2 625 | if mouse_over_grid2? 626 | unless state.walls.has_key?(cell_closest_to_mouse2) 627 | state.hills.delete(cell_closest_to_mouse2) 628 | state.walls[cell_closest_to_mouse2] = true 629 | reset_search 630 | end 631 | end 632 | end 633 | 634 | # Whenever the user edits the grid, 635 | # The search has to be reset_searchd upto the current step 636 | # with the current grid as the initial state of the grid 637 | def reset_search 638 | breadth_first_search.visited = {} 639 | breadth_first_search.frontier = [] 640 | breadth_first_search.came_from = {} 641 | 642 | dijkstra_search.frontier = [] 643 | dijkstra_search.came_from = {} 644 | dijkstra_search.cost_so_far = {} 645 | end 646 | 647 | 648 | 649 | # Returns a list of adjacent cells 650 | # Used to determine what the next cells to be added to the frontier are 651 | def adjacent_neighbors(cell) 652 | neighbors = [] 653 | 654 | # Gets all the valid neighbors into the array 655 | # From southern neighbor, clockwise 656 | neighbors << [cell.x , cell.y - 1] unless cell.y == 0 657 | neighbors << [cell.x - 1, cell.y ] unless cell.x == 0 658 | neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1 659 | neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1 660 | 661 | # Sorts the neighbors so the rendered path is a zigzag path 662 | # Cells in a diagonal direction are given priority 663 | # Comment this line to see the difference 664 | neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) } 665 | 666 | neighbors 667 | end 668 | 669 | # Finds the vertical and horizontal distance of a cell from the star 670 | # and returns the larger value 671 | # This method is used to have a zigzag pattern in the rendered path 672 | # A cell that is [5, 5] from the star, 673 | # is explored before over a cell that is [0, 7] away. 674 | # So, if possible, the search tries to go diagonal (zigzag) first 675 | def proximity_to_star(x, y) 676 | distance_x = (state.star.x - x).abs 677 | distance_y = (state.star.y - y).abs 678 | 679 | if distance_x > distance_y 680 | return distance_x 681 | else 682 | return distance_y 683 | end 684 | end 685 | 686 | # When the user grabs the star and puts their cursor to the far right 687 | # and moves up and down, the star is supposed to move along the grid as well 688 | # Finding the cell closest to the mouse helps with this 689 | def cell_closest_to_mouse 690 | # Closest cell to the mouse in the first grid 691 | x = (inputs.mouse.point.x / grid.cell_size).to_i 692 | y = (inputs.mouse.point.y / grid.cell_size).to_i 693 | # Bound x and y to the grid 694 | x = grid.width - 1 if x > grid.width - 1 695 | y = grid.height - 1 if y > grid.height - 1 696 | # Return closest cell 697 | [x, y] 698 | end 699 | 700 | # When the user grabs the star and puts their cursor to the far right 701 | # and moves up and down, the star is supposed to move along the grid as well 702 | # Finding the cell closest to the mouse in the second grid helps with this 703 | def cell_closest_to_mouse2 704 | # Closest cell grid to the mouse in the second 705 | x = (inputs.mouse.point.x / grid.cell_size).to_i 706 | y = (inputs.mouse.point.y / grid.cell_size).to_i 707 | # Translate the cell to the first grid 708 | x -= grid.width + 1 709 | # Bound x and y to the first grid 710 | x = 0 if x < 0 711 | y = 0 if y < 0 712 | x = grid.width - 1 if x > grid.width - 1 713 | y = grid.height - 1 if y > grid.height - 1 714 | # Return closest cell 715 | [x, y] 716 | end 717 | 718 | # Signal that the user is going to be moving the star from the first grid 719 | def mouse_over_star? 720 | inputs.mouse.point.inside_rect?(scale_up(state.star)) 721 | end 722 | 723 | # Signal that the user is going to be moving the star from the second grid 724 | def mouse_over_star2? 725 | inputs.mouse.point.inside_rect?(move_and_scale_up(state.star)) 726 | end 727 | 728 | # Signal that the user is going to be moving the target from the first grid 729 | def mouse_over_target? 730 | inputs.mouse.point.inside_rect?(scale_up(state.target)) 731 | end 732 | 733 | # Signal that the user is going to be moving the target from the second grid 734 | def mouse_over_target2? 735 | inputs.mouse.point.inside_rect?(move_and_scale_up(state.target)) 736 | end 737 | 738 | # Signal that the user is going to be removing walls from the first grid 739 | def mouse_over_wall? 740 | state.walls.each_key do | wall | 741 | return true if inputs.mouse.point.inside_rect?(scale_up(wall)) 742 | end 743 | 744 | false 745 | end 746 | 747 | # Signal that the user is going to be removing walls from the second grid 748 | def mouse_over_wall2? 749 | state.walls.each_key do | wall | 750 | return true if inputs.mouse.point.inside_rect?(move_and_scale_up(wall)) 751 | end 752 | 753 | false 754 | end 755 | 756 | # Signal that the user is going to be removing hills from the first grid 757 | def mouse_over_hill? 758 | state.hills.each_key do | hill | 759 | return true if inputs.mouse.point.inside_rect?(scale_up(hill)) 760 | end 761 | 762 | false 763 | end 764 | 765 | # Signal that the user is going to be removing hills from the second grid 766 | def mouse_over_hill2? 767 | state.hills.each_key do | hill | 768 | return true if inputs.mouse.point.inside_rect?(move_and_scale_up(hill)) 769 | end 770 | 771 | false 772 | end 773 | 774 | # Signal that the user is going to be adding walls from the first grid 775 | def mouse_over_grid? 776 | inputs.mouse.point.inside_rect?(scale_up(grid.rect)) 777 | end 778 | 779 | # Signal that the user is going to be adding walls from the second grid 780 | def mouse_over_grid2? 781 | inputs.mouse.point.inside_rect?(move_and_scale_up(grid.rect)) 782 | end 783 | 784 | # These methods provide handy aliases to colors 785 | 786 | # Light brown 787 | def unvisited_color 788 | [221, 212, 213] 789 | end 790 | 791 | # Camo Green 792 | def wall_color 793 | [134, 134, 120] 794 | end 795 | 796 | # Pastel White 797 | def path_color 798 | [231, 230, 228] 799 | end 800 | 801 | def red 802 | [255, 0, 0] 803 | end 804 | 805 | # A Green 806 | def hill_color 807 | [139, 173, 132] 808 | end 809 | 810 | # Makes code more concise 811 | def grid 812 | state.grid 813 | end 814 | 815 | def breadth_first_search 816 | state.breadth_first_search 817 | end 818 | 819 | def dijkstra_search 820 | state.dijkstra_search 821 | end 822 | end 823 | 824 | # Method that is called by DragonRuby periodically 825 | # Used for updating animations and calculations 826 | def tick args 827 | 828 | # Pressing r will reset the application 829 | if args.inputs.keyboard.key_down.r 830 | args.gtk.reset 831 | reset 832 | return 833 | end 834 | 835 | # Every tick, new args are passed, and the Dijkstra tick method is called 836 | $movement_costs ||= Movement_Costs.new(args) 837 | $movement_costs.args = args 838 | $movement_costs.tick 839 | end 840 | 841 | 842 | def reset 843 | $movement_costs = nil 844 | end 845 | -------------------------------------------------------------------------------- /Movement_Costs/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Movement_Costs/star.png -------------------------------------------------------------------------------- /Movement_Costs/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/Movement_Costs/target.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Info 2 | 3 | A series of algorithms that demonstrate the purpose of the A* algorithm 4 | Based off of https://www.redblobgames.com/pathfinding/a-star/introduction.html 5 | 6 | # How to Run 7 | 8 | 1. You need a license to [DragonRuby Game Toolkit](http://dragonruby.org) (**income assistance is provided to those who can't afford to pay**). 9 | 2. Download the code here and put all the files under the `mygame` directory located in the Game Toolkit environment. 10 | 3. Run `./dragonruby` to launch the game. 11 | 4. Change code to see the updates reflected live. 12 | 13 | 14 | # Breadth First Search 15 | ![](gifs/Breadth_First_Search.gif) 16 | 17 | # Detailed Breadth First Search 18 | ![](gifs/Detailed_Breadth_First_Search.gif) 19 | 20 | # Early Exit Breadth First Search 21 | ![](gifs/Early_Exit_Breadth_First_Search.gif) 22 | 23 | # Movement Costs 24 | ![](gifs/Movement_Costs.gif) 25 | 26 | # Heuristic 27 | ![](gifs/Heuristic.gif) 28 | 29 | # Heuristic With Walls 30 | ![](gifs/Heuristic_With_Walls.gif) 31 | 32 | # A* 33 | ![](gifs/A_Star.gif) 34 | -------------------------------------------------------------------------------- /gifs/A_Star.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/gifs/A_Star.gif -------------------------------------------------------------------------------- /gifs/Breadth_First_Search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/gifs/Breadth_First_Search.gif -------------------------------------------------------------------------------- /gifs/Detailed_Breadth_First_Search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/gifs/Detailed_Breadth_First_Search.gif -------------------------------------------------------------------------------- /gifs/Early_Exit_Breadth_First_Search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/gifs/Early_Exit_Breadth_First_Search.gif -------------------------------------------------------------------------------- /gifs/Heuristic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/gifs/Heuristic.gif -------------------------------------------------------------------------------- /gifs/Heuristic_With_Walls.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/gifs/Heuristic_With_Walls.gif -------------------------------------------------------------------------------- /gifs/Movement_Costs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sujayvadlakonda/DragonRubyAlgorithms/d9d0b85a26034657d7fa3e42ad1528c50be50e39/gifs/Movement_Costs.gif --------------------------------------------------------------------------------