summaryrefslogtreecommitdiffhomepage
path: root/samples/13_path_finding_algorithms/01_breadth_first_search/app/main.rb
blob: 8b84f1c01f871c5adcd4c519845d73cd75187342 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
# A visual demonstration of a breadth first search
# Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html

# An animation that can respond to user input in real time

# A breadth first search expands in all directions one step at a time
# The frontier is a queue of cells to be expanded from
# The visited hash allows quick lookups of cells that have been expanded from
# The walls hash allows quick lookup of whether a cell is a wall

# The breadth first search starts by adding the red star to the frontier array
# and marking it as visited
# Each step a cell is removed from the front of the frontier array (queue)
# Unless the neighbor is a wall or visited, it is added to the frontier array
# The neighbor is then marked as visited

# The frontier is blue
# Visited cells are light brown
# Walls are camo green
# Even when walls are visited, they will maintain their wall color

# The star can be moved by clicking and dragging
# Walls can be added and removed by clicking and dragging

class BreadthFirstSearch
  attr_gtk

  def initialize(args)
    # Variables to edit the size and appearance of the grid
    # Freely customizable to user's liking
    args.state.grid.width     = 30
    args.state.grid.height    = 15
    args.state.grid.cell_size = 40

    # Stores which step of the animation is being rendered
    # When the user moves the star or messes with the walls,
    # the breadth first search is recalculated up to this step
    args.state.anim_steps = 0

    # At some step the animation will end,
    # and further steps won't change anything (the whole grid will be explored)
    # This step is roughly the grid's width * height
    # When anim_steps equals max_steps no more calculations will occur
    # and the slider will be at the end
    args.state.max_steps  = args.state.grid.width * args.state.grid.height

    # Whether the animation should play or not
    # If true, every tick moves anim_steps forward one
    # Pressing the stepwise animation buttons will pause the animation
    args.state.play       = true

    # The location of the star and walls of the grid
    # They can be modified to have a different initial grid
    # Walls are stored in a hash for quick look up when doing the search
    args.state.star       = [0, 0]
    args.state.walls      = {
      [3, 3] => true,
      [3, 4] => true,
      [3, 5] => true,
      [3, 6] => true,
      [3, 7] => true,
      [3, 8] => true,
      [3, 9] => true,
      [3, 10] => true,
      [3, 11] => true,
      [4, 3] => true,
      [4, 4] => true,
      [4, 5] => true,
      [4, 6] => true,
      [4, 7] => true,
      [4, 8] => true,
      [4, 9] => true,
      [4, 10] => true,
      [4, 11] => true,

      [13, 0] => true,
      [13, 1] => true,
      [13, 2] => true,
      [13, 3] => true,
      [13, 4] => true,
      [13, 5] => true,
      [13, 6] => true,
      [13, 7] => true,
      [13, 8] => true,
      [13, 9] => true,
      [13, 10] => true,
      [14, 0] => true,
      [14, 1] => true,
      [14, 2] => true,
      [14, 3] => true,
      [14, 4] => true,
      [14, 5] => true,
      [14, 6] => true,
      [14, 7] => true,
      [14, 8] => true,
      [14, 9] => true,
      [14, 10] => true,

      [21, 8] => true,
      [21, 9] => true,
      [21, 10] => true,
      [21, 11] => true,
      [21, 12] => true,
      [21, 13] => true,
      [21, 14] => true,
      [22, 8] => true,
      [22, 9] => true,
      [22, 10] => true,
      [22, 11] => true,
      [22, 12] => true,
      [22, 13] => true,
      [22, 14] => true,
      [23, 8] => true,
      [23, 9] => true,
      [24, 8] => true,
      [24, 9] => true,
      [25, 8] => true,
      [25, 9] => true,
    }

    # Variables that are used by the breadth first search
    # Storing cells that the search has visited, prevents unnecessary steps
    # Expanding the frontier of the search in order makes the search expand
    # from the center outward
    args.state.visited    = {}
    args.state.frontier   = []


    # What the user is currently editing on the grid
    # Possible values are: :none, :slider, :star, :remove_wall, :add_wall

    # We store this value, because we want to remember the value even when
    # the user's cursor is no longer over what they're interacting with, but
    # they are still clicking down on the mouse.
    args.state.click_and_drag = :none

    # Store the rects of the buttons that control the animation
    # They are here for user customization
    # Editing these might require recentering the text inside them
    # Those values can be found in the render_button methods
    args.state.buttons.left   = [450, 600, 50, 50]
    args.state.buttons.center = [500, 600, 200, 50]
    args.state.buttons.right  = [700, 600, 50, 50]

    # The variables below are related to the slider
    # They allow the user to customize them
    # They also give a central location for the render and input methods to get
    # information from
    # x & y are the coordinates of the leftmost part of the slider line
    args.state.slider.x = 400
    args.state.slider.y = 675
    # This is the width of the line
    args.state.slider.w = 360
    # This is the offset for the circle
    # Allows the center of the circle to be on the line,
    # as opposed to the upper right corner
    args.state.slider.offset = 20
    # This is the spacing between each of the notches on the slider
    # Notches are places where the circle can rest on the slider line
    # There needs to be a notch for each step before the maximum number of steps
    args.state.slider.spacing = args.state.slider.w.to_f / args.state.max_steps.to_f
  end

  # This method is called every frame/tick
  # Every tick, the current state of the search is rendered on the screen,
  # User input is processed, and
  # The next step in the search is calculated
  def tick
    render
    input
    # If animation is playing, and max steps have not been reached
    # Move the search a step forward
    if state.play && state.anim_steps < state.max_steps
      # Variable that tells the program what step to recalculate up to
      state.anim_steps += 1
      calc
    end
  end

  # Draws everything onto the screen
  def render
    render_buttons
    render_slider

    render_background
    render_visited
    render_frontier
    render_walls
    render_star
  end

  # The methods below subdivide the task of drawing everything to the screen

  # Draws the buttons that control the animation step and state
  def render_buttons
    render_left_button
    render_center_button
    render_right_button
  end

  # Draws the button which steps the search backward
  # Shows the user where to click to move the search backward
  def render_left_button
    # Draws the gray button, and a black border
    # The border separates the buttons visually
    outputs.solids  << [buttons.left, gray]
    outputs.borders << [buttons.left, black]

    # Renders an explanatory label in the center of the button
    # Explains to the user what the button does
    # If the button size is changed, the label might need to be edited as well
    # to keep the label in the center of the button
    label_x = buttons.left.x + 20
    label_y = buttons.left.y + 35
    outputs.labels  << [label_x, label_y, "<"]
  end

  def render_center_button
    # Draws the gray button, and a black border
    # The border separates the buttons visually
    outputs.solids  << [buttons.center, gray]
    outputs.borders << [buttons.center, black]

    # Renders an explanatory label in the center of the button
    # Explains to the user what the button does
    # If the button size is changed, the label might need to be edited as well
    # to keep the label in the center of the button
    label_x    = buttons.center.x + 37
    label_y    = buttons.center.y + 35
    label_text = state.play ? "Pause Animation" : "Play Animation"
    outputs.labels << [label_x, label_y, label_text]
  end

  def render_right_button
    # Draws the gray button, and a black border
    # The border separates the buttons visually
    outputs.solids  << [buttons.right, gray]
    outputs.borders << [buttons.right, black]

    # Renders an explanatory label in the center of the button
    # Explains to the user what the button does
    label_x = buttons.right.x + 20
    label_y = buttons.right.y + 35
    outputs.labels  << [label_x, label_y, ">"]
  end

  # Draws the slider so the user can move it and see the progress of the search
  def render_slider
    # Using primitives hides the line under the white circle of the slider
    # Draws the line
    outputs.primitives << [slider.x, slider.y, slider.x + slider.w, slider.y].line
    # The circle needs to be offset so that the center of the circle
    # overlaps the line instead of the upper right corner of the circle
    # The circle's x value is also moved based on the current seach step
    circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing)
    circle_y = (slider.y - slider.offset)
    circle_rect = [circle_x, circle_y, 37, 37]
    outputs.primitives << [circle_rect, 'circle-white.png'].sprite
  end

  # Draws what the grid looks like with nothing on it
  def render_background
    render_unvisited
    render_grid_lines
  end

  # Draws a rectangle the size of the entire grid to represent unvisited cells
  def render_unvisited
    outputs.solids << [scale_up([0, 0, grid.width, grid.height]), unvisited_color]
  end

  # Draws grid lines to show the division of the grid into cells
  def render_grid_lines
    for x in 0..grid.width
      outputs.lines << vertical_line(x)
    end

    for y in 0..grid.height
      outputs.lines << horizontal_line(y)
    end
  end

  # Easy way to draw vertical lines given an index
  def vertical_line column
    scale_up([column, 0, column, grid.height])
  end

  # Easy way to draw horizontal lines given an index
  def horizontal_line row
    scale_up([0, row, grid.width, row])
  end

  # Draws the area that is going to be searched from
  # The frontier is the most outward parts of the search
  def render_frontier
    outputs.solids << state.frontier.map do |cell|
      [scale_up([cell.x, cell.y]), frontier_color]
    end
  end

  # Draws the walls
  def render_walls
    outputs.solids << state.walls.map do |wall|
      [scale_up([wall.x, wall.y]), wall_color]
    end
  end

  # Renders cells that have been searched in the appropriate color
  def render_visited
    outputs.solids << state.visited.map do |cell|
      [scale_up([cell.x, cell.y]), visited_color]
    end
  end

  # Renders the star
  def render_star
    outputs.sprites << [scale_up(state.star), 'star.png']
  end

  # In code, the cells are represented as 1x1 rectangles
  # When drawn, the cells are larger than 1x1 rectangles
  # This method is used to scale up cells, and lines
  # Objects are scaled up according to the grid.cell_size variable
  # This allows for easy customization of the visual scale of the grid
  def scale_up(cell)
    # Prevents the original value of cell from being edited
    cell = cell.clone

    # If cell is just an x and y coordinate
    if cell.size == 2
      # Add a width and height of 1
      cell << 1
      cell << 1
    end

    # Scale all the values up
    cell.map! { |value| value * grid.cell_size }

    # Returns the scaled up cell
    cell
  end

  # This method processes user input every tick
  # This method allows the user to use the buttons, slider, and edit the grid
  # There are 2 types of input:
  #   Button Input
  #   Click and Drag Input
  #
  #   Button Input is used for the backward step and forward step buttons
  #   Input is detected by mouse up within the bounds of the rect
  #
  #   Click and Drag Input is used for moving the star, adding walls,
  #   removing walls, and the slider
  #
  #   When the mouse is down on the star, the click_and_drag variable is set to :star
  #   While click_and_drag equals :star, the cursor's position is used to calculate the
  #   appropriate drag behavior
  #
  #   When the mouse goes up click_and_drag is set to :none
  #
  #   A variable has to be used because the star has to continue being edited even
  #   when the cursor is no longer over the star
  #
  #   Similar things occur for the other Click and Drag inputs
  def input
    # Checks whether any of the buttons are being clicked
    input_buttons

    # The detection and processing of click and drag inputs are separate
    # The program has to remember that the user is dragging an object
    # even when the mouse is no longer over that object
    detect_click_and_drag
    process_click_and_drag
  end

  # Detects and Process input for each button
  def input_buttons
    input_left_button
    input_center_button
    input_next_step_button
  end

  # Checks if the previous step button is clicked
  # If it is, it pauses the animation and moves the search one step backward
  def input_left_button
    if left_button_clicked?
      state.play = false
      state.anim_steps -= 1
      recalculate
    end
  end

  # Controls the play/pause button
  # Inverses whether the animation is playing or not when clicked
  def input_center_button
    if center_button_clicked? or inputs.keyboard.key_down.space
      state.play = !state.play
    end
  end

  # Checks if the next step button is clicked
  # If it is, it pauses the animation and moves the search one step forward
  def input_next_step_button
    if right_button_clicked?
      state.play = false
      state.anim_steps += 1
      calc
    end
  end

  # Determines what the user is editing and stores the value
  # Storing the value allows the user to continue the same edit as long as the
  # mouse left click is held
  def detect_click_and_drag
    if inputs.mouse.up
      state.click_and_drag = :none
    elsif star_clicked?
      state.click_and_drag = :star
    elsif wall_clicked?
      state.click_and_drag = :remove_wall
    elsif grid_clicked?
      state.click_and_drag = :add_wall
    elsif slider_clicked?
      state.click_and_drag = :slider
    end
  end

  # Processes click and drag based on what the user is currently dragging
  def process_click_and_drag
    if state.click_and_drag == :star
      input_star
    elsif state.click_and_drag == :remove_wall
      input_remove_wall
    elsif state.click_and_drag == :add_wall
      input_add_wall
    elsif state.click_and_drag == :slider
      input_slider
    end
  end

  # Moves the star to the grid closest to the mouse
  # Only recalculates the search if the star changes position
  # Called whenever the user is editing the star (puts mouse down on star)
  def input_star
    old_star = state.star.clone
    state.star = cell_closest_to_mouse
    unless old_star == state.star
      recalculate
    end
  end

  # Removes walls that are under the cursor
  def input_remove_wall
    # The mouse needs to be inside the grid, because we only want to remove walls
    # the cursor is directly over
    # Recalculations should only occur when a wall is actually deleted
    if mouse_inside_grid?
      if state.walls.has_key?(cell_closest_to_mouse)
        state.walls.delete(cell_closest_to_mouse)
        recalculate
      end
    end
  end

  # Adds walls at cells under the cursor
  def input_add_wall
    if mouse_inside_grid?
      unless state.walls.has_key?(cell_closest_to_mouse)
        state.walls[cell_closest_to_mouse] = true
        recalculate
      end
    end
  end

  # This method is called when the user is editing the slider
  # It pauses the animation and moves the white circle to the closest integer point
  # on the slider
  # Changes the step of the search to be animated
  def input_slider
    state.play = false
    mouse_x = inputs.mouse.point.x

    # Bounds the mouse_x to the closest x value on the slider line
    mouse_x = slider.x if mouse_x < slider.x
    mouse_x = slider.x + slider.w if mouse_x > slider.x + slider.w

    # Sets the current search step to the one represented by the mouse x value
    # The slider's circle moves due to the render_slider method using anim_steps
    state.anim_steps = ((mouse_x - slider.x) / slider.spacing).to_i

    recalculate
  end

  # Whenever the user edits the grid,
  # The search has to be recalculated upto the current step
  # with the current grid as the initial state of the grid
  def recalculate
    # Resets the search
    state.frontier = []
    state.visited = {}

    # Moves the animation forward one step at a time
    state.anim_steps.times { calc }
  end


  # This method moves the search forward one step
  # When the animation is playing it is called every tick
  # And called whenever the current step of the animation needs to be recalculated

  # Moves the search forward one step
  # Parameter called_from_tick is true if it is called from the tick method
  # It is false when the search is being recalculated after user editing the grid
  def calc

    # The setup to the search
    # Runs once when the there is no frontier or visited cells
    if state.frontier.empty? && state.visited.empty?
      state.frontier << state.star
      state.visited[state.star] = true
    end

    # A step in the search
    unless state.frontier.empty?
      # Takes the next frontier cell
      new_frontier = state.frontier.shift
      # For each of its neighbors
      adjacent_neighbors(new_frontier).each do |neighbor|
        # That have not been visited and are not walls
        unless state.visited.has_key?(neighbor) || state.walls.has_key?(neighbor)
          # Add them to the frontier and mark them as visited
          state.frontier << neighbor
          state.visited[neighbor] = true
        end
      end
    end
  end


  # Returns a list of adjacent cells
  # Used to determine what the next cells to be added to the frontier are
  def adjacent_neighbors(cell)
    neighbors = []

    neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1
    neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1
    neighbors << [cell.x, cell.y - 1] unless cell.y == 0
    neighbors << [cell.x - 1, cell.y] unless cell.x == 0

    neighbors
  end

  # When the user grabs the star and puts their cursor to the far right
  # and moves up and down, the star is supposed to move along the grid as well
  # Finding the cell closest to the mouse helps with this
  def cell_closest_to_mouse
    # Closest cell to the mouse
    x = (inputs.mouse.point.x / grid.cell_size).to_i
    y = (inputs.mouse.point.y / grid.cell_size).to_i
    # Bound x and y to the grid
    x = grid.width - 1 if x > grid.width - 1
    y = grid.height - 1 if y > grid.height - 1
    # Return closest cell
    [x, y]
  end

  # These methods detect when the buttons are clicked
  def left_button_clicked?
    inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.left)
  end

  def center_button_clicked?
    inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.center)
  end

  def right_button_clicked?
    inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.right)
  end

  # Signal that the user is going to be moving the slider
  # Is the mouse down on the circle of the slider?
  def slider_clicked?
    circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing)
    circle_y = (slider.y - slider.offset)
    circle_rect = [circle_x, circle_y, 37, 37]
    inputs.mouse.down && inputs.mouse.point.inside_rect?(circle_rect)
  end

  # Signal that the user is going to be moving the star
  def star_clicked?
    inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.star))
  end

  # Signal that the user is going to be removing walls
  def wall_clicked?
    inputs.mouse.down && mouse_inside_a_wall?
  end

  # Signal that the user is going to be adding walls
  def grid_clicked?
    inputs.mouse.down && mouse_inside_grid?
  end

  # Returns whether the mouse is inside of a wall
  # Part of the condition that checks whether the user is removing a wall
  def mouse_inside_a_wall?
    state.walls.each_key do | wall |
      return true if inputs.mouse.point.inside_rect?(scale_up([wall.x, wall.y]))
    end

    false
  end

  # Returns whether the mouse is inside of a grid
  # Part of the condition that checks whether the user is adding a wall
  def mouse_inside_grid?
    inputs.mouse.point.inside_rect?(scale_up([0, 0, grid.width, grid.height]))
  end


  # These methods provide handy aliases to colors

  # Light brown
  def unvisited_color
    [221, 212, 213]
  end

  # Black
  def grid_line_color
    [255, 255, 255]
  end

  # Dark Brown
  def visited_color
    [204, 191, 179]
  end

  # Blue
  def frontier_color
    [103, 136, 204]
  end

  # Camo Green
  def wall_color
    [134, 134, 120]
  end

  # Button Background
  def gray
    [190, 190, 190]
  end

  # Button Outline
  def black
    [0, 0, 0]
  end

  # These methods make the code more concise
  def grid
    state.grid
  end

  def buttons
    state.buttons
  end

  def slider
    state.slider
  end
end

# Method that is called by DragonRuby periodically
# Used for updating animations and calculations
def tick args

  # Pressing r will reset the application
  if args.inputs.keyboard.key_down.r
    args.gtk.reset
    reset
    return
  end

  # Every tick, new args are passed, and the Breadth First Search tick is called
  $breadth_first_search ||= BreadthFirstSearch.new(args)
  $breadth_first_search.args = args
  $breadth_first_search.tick
end


def reset
  $breadth_first_search = nil
end