summaryrefslogtreecommitdiffhomepage
path: root/samples
diff options
context:
space:
mode:
authorAmir Rajan <[email protected]>2021-01-18 12:08:34 -0600
committerAmir Rajan <[email protected]>2021-01-18 12:08:34 -0600
commita4b9c048a1d751f5226833bb0c527ba1a8ac5d09 (patch)
tree3f2535e7a6272e796d50e7f07c906d4c9eb1b14a /samples
parenta24a71805b1924ae7f80776c736f94575c171d2c (diff)
downloaddragonruby-game-toolkit-contrib-a4b9c048a1d751f5226833bb0c527ba1a8ac5d09.tar.gz
dragonruby-game-toolkit-contrib-a4b9c048a1d751f5226833bb0c527ba1a8ac5d09.zip
Synced with 2.3.
Diffstat (limited to 'samples')
-rw-r--r--samples/01_rendering_basics/07_sound_synthesis/app/main.rb833
-rw-r--r--samples/01_rendering_basics/07_sound_synthesis/sprites/square-white.pngbin0 -> 279 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/app/main.rb255
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/metadata/game_metadata.txt6
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/metadata/icon.pngbin0 -> 157056 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/black.pngbin0 -> 1882 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/blue.pngbin0 -> 2901 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/gray.pngbin0 -> 3006 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/green.pngbin0 -> 2887 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/indigo.pngbin0 -> 2433 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/orange.pngbin0 -> 2670 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/red.pngbin0 -> 2233 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/violet.pngbin0 -> 2439 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/white.pngbin0 -> 1754 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/yellow.pngbin0 -> 2456 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/black.pngbin0 -> 2602 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/blue.pngbin0 -> 4842 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/gray.pngbin0 -> 5184 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/green.pngbin0 -> 4695 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/indigo.pngbin0 -> 4918 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/orange.pngbin0 -> 4825 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/red.pngbin0 -> 3753 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/violet.pngbin0 -> 5069 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/white.pngbin0 -> 5326 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/yellow.pngbin0 -> 5249 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/black.pngbin0 -> 264 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/blue.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/gray.pngbin0 -> 493 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/green.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/indigo.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/orange.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/red.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/violet.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/white.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/yellow.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-0.pngbin0 -> 12896 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-1.pngbin0 -> 2964 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-2.pngbin0 -> 3047 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-3.pngbin0 -> 2655 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-4.pngbin0 -> 2725 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-5.pngbin0 -> 2655 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-0.pngbin0 -> 267 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-1.pngbin0 -> 4585 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-2.pngbin0 -> 4675 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-3.pngbin0 -> 4724 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-4.pngbin0 -> 4773 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-5.pngbin0 -> 4742 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-6.pngbin0 -> 4665 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-sheet.pngbin0 -> 2584 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/lowrez-ship-blue.pngbin0 -> 109 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/lowrez-ship-red.pngbin0 -> 104 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/simple-mood-16x16.pngbin0 -> 14424 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/star.pngbin0 -> 711 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/tiny-star.pngbin0 -> 112 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/black.pngbin0 -> 250 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/blue.pngbin0 -> 283 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/gray.pngbin0 -> 251 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/green.pngbin0 -> 283 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/indigo.pngbin0 -> 283 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/orange.pngbin0 -> 282 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/red.pngbin0 -> 274 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/violet.pngbin0 -> 284 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/white.pngbin0 -> 279 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/square/yellow.pngbin0 -> 286 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0000.pngbin0 -> 93 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0001.pngbin0 -> 99 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0010.pngbin0 -> 98 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0011.pngbin0 -> 101 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0100.pngbin0 -> 98 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0101.pngbin0 -> 100 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0110.pngbin0 -> 100 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0111.pngbin0 -> 102 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1000.pngbin0 -> 99 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1001.pngbin0 -> 101 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1010.pngbin0 -> 100 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1011.pngbin0 -> 101 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1100.pngbin0 -> 101 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1101.pngbin0 -> 102 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1110.pngbin0 -> 101 bytes
-rw-r--r--samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1111.pngbin0 -> 102 bytes
-rw-r--r--samples/04_physics_and_collisions/07_jump_physics/app/main.rb196
-rw-r--r--samples/04_physics_and_collisions/07_jump_physics/replay.txt124
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/app/ball.rb87
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/app/block.rb159
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/app/cannon.rb20
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/app/main.rb117
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/app/peg.rb182
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/app/vector2d.rb49
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/docs/LinearCollider.md13
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_1.pngbin0 -> 315601 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_2.pngbin0 -> 394289 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_3.pngbin0 -> 339651 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_4.pngbin0 -> 18828 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_5.pngbin0 -> 24240 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_6.pngbin0 -> 375696 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/ball.pngbin0 -> 38235 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/border-black.pngbin0 -> 908 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-black.pngbin0 -> 1882 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-blue.pngbin0 -> 2901 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-gray.pngbin0 -> 3006 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-green.pngbin0 -> 2887 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-indigo.pngbin0 -> 2433 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-orange.pngbin0 -> 2670 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-red.pngbin0 -> 2233 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-violet.pngbin0 -> 2439 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-white.pngbin0 -> 1754 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-yellow.pngbin0 -> 2456 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-0.pngbin0 -> 12896 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-1.pngbin0 -> 2964 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-2.pngbin0 -> 3047 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-3.pngbin0 -> 2655 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-4.pngbin0 -> 2725 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-5.pngbin0 -> 2655 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-0.pngbin0 -> 267 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-1.pngbin0 -> 4585 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-2.pngbin0 -> 4675 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-3.pngbin0 -> 4724 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-4.pngbin0 -> 4773 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-5.pngbin0 -> 4742 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-6.pngbin0 -> 4665 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-sheet.pngbin0 -> 2584 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-black.pngbin0 -> 2602 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-blue.pngbin0 -> 4842 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-gray.pngbin0 -> 5184 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-green.pngbin0 -> 4695 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-indigo.pngbin0 -> 4918 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-orange.pngbin0 -> 4825 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-red.pngbin0 -> 3753 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-violet.pngbin0 -> 5069 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-white.pngbin0 -> 5326 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-yellow.pngbin0 -> 5249 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-black.pngbin0 -> 264 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-blue.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-gray.pngbin0 -> 493 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-green.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-indigo.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-orange.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-red.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-violet.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-white.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-yellow.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-black.pngbin0 -> 250 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-blue.pngbin0 -> 283 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-gray.pngbin0 -> 251 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-green.pngbin0 -> 283 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-indigo.pngbin0 -> 283 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-orange.pngbin0 -> 282 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-red.pngbin0 -> 274 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-violet.pngbin0 -> 284 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-white.pngbin0 -> 279 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-yellow.pngbin0 -> 286 bytes
-rw-r--r--samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/star.pngbin0 -> 711 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/LICENSE21
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/app/ball.rb166
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/app/blocks.rb618
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/app/linear_collider.rb180
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/app/main.rb171
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/app/paddle.rb53
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/app/rectangle.rb90
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/app/square_collider.rb29
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/app/vector2d.rb49
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/data/.gitkeep1
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/docs/LinearCollider.md13
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_1.pngbin0 -> 315601 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_2.pngbin0 -> 394289 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_3.pngbin0 -> 339651 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_4.pngbin0 -> 18828 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_5.pngbin0 -> 24240 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_6.pngbin0 -> 375696 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/fonts/.gitkeep1
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/metadata/game_metadata.txt6
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/metadata/icon.pngbin0 -> 157056 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sounds/.gitkeep1
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball.pngbin0 -> 38235 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball10.pngbin0 -> 175 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball15.pngbin0 -> 237 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/border-black.pngbin0 -> 908 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-black.pngbin0 -> 1882 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-blue.pngbin0 -> 2901 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-gray.pngbin0 -> 3006 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-green.pngbin0 -> 2887 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-indigo.pngbin0 -> 2433 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-orange.pngbin0 -> 2670 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-red.pngbin0 -> 2233 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-violet.pngbin0 -> 2439 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-white.pngbin0 -> 1754 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-yellow.pngbin0 -> 2456 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-0.pngbin0 -> 12896 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-1.pngbin0 -> 2964 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-2.pngbin0 -> 3047 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-3.pngbin0 -> 2655 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-4.pngbin0 -> 2725 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-5.pngbin0 -> 2655 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-0.pngbin0 -> 267 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-1.pngbin0 -> 4585 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-2.pngbin0 -> 4675 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-3.pngbin0 -> 4724 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-4.pngbin0 -> 4773 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-5.pngbin0 -> 4742 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-6.pngbin0 -> 4665 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-sheet.pngbin0 -> 2584 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/glowCircle.pngbin0 -> 358771 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-black.pngbin0 -> 2602 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-blue.pngbin0 -> 4842 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-gray.pngbin0 -> 5184 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-green.pngbin0 -> 4695 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-indigo.pngbin0 -> 4918 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-orange.pngbin0 -> 4825 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-red.pngbin0 -> 3753 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-violet.pngbin0 -> 5069 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-white.pngbin0 -> 5326 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-yellow.pngbin0 -> 5249 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-black.pngbin0 -> 264 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-blue.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-gray.pngbin0 -> 493 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-green.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-indigo.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-orange.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-red.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-violet.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-white.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-yellow.pngbin0 -> 361 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare.pngbin0 -> 10629 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small.pngbin0 -> 835 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small_black.pngbin0 -> 820 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small_white.pngbin0 -> 531 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_white.pngbin0 -> 12514 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-black.pngbin0 -> 250 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-blue.pngbin0 -> 283 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-gray.pngbin0 -> 251 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-green.pngbin0 -> 283 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-indigo.pngbin0 -> 283 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-orange.pngbin0 -> 282 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-red.pngbin0 -> 274 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-violet.pngbin0 -> 284 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-white-2.pngbin0 -> 238 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-white.pngbin0 -> 279 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-yellow.pngbin0 -> 286 bytes
-rw-r--r--samples/04_physics_and_collisions/09_arbitrary_collision/sprites/star.pngbin0 -> 711 bytes
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb31
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb166
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb189
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb53
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb29
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb50
-rw-r--r--samples/07_advanced_rendering/07_splitscreen_camera/app/main.rb395
-rw-r--r--samples/07_advanced_rendering/07_splitscreen_camera/run.bat6
-rw-r--r--samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_down_standing.pngbin0 -> 186 bytes
-rw-r--r--samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_left_standing.pngbin0 -> 173 bytes
-rw-r--r--samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_right_standing.pngbin0 -> 170 bytes
-rw-r--r--samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_up_standing.pngbin0 -> 165 bytes
-rw-r--r--samples/07_advanced_rendering/07_splitscreen_camera/sprites/rooms/camera_room.pngbin0 -> 2469 bytes
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/exception_raising_tests.rb2
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/gen_docs.rb2
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/geometry_tests.rb2
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/http_tests.rb2
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/object_to_primitive_tests.rb1
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/parsing_tests.rb2
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/require_tests.rb38
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/run-tests.sh10
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/serialize_deserialize_tests.rb2
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/state_serialization_experimental_tests.rb1
-rw-r--r--samples/10_advanced_debugging/03_unit_tests/suggest_autocompletion_tests.rb38
-rw-r--r--samples/12_c_extensions/README.md414
-rw-r--r--samples/13_path_finding_algorithms/01_breadth_first_search/app/main.rb692
-rw-r--r--samples/13_path_finding_algorithms/01_breadth_first_search/circle-white.pngbin0 -> 1754 bytes
-rw-r--r--samples/13_path_finding_algorithms/01_breadth_first_search/star.pngbin0 -> 4892 bytes
-rw-r--r--samples/13_path_finding_algorithms/02_detailed_breadth_first_search/app/main.rb646
-rw-r--r--samples/13_path_finding_algorithms/02_detailed_breadth_first_search/circle-white.pngbin0 -> 1754 bytes
-rw-r--r--samples/13_path_finding_algorithms/02_detailed_breadth_first_search/star.pngbin0 -> 4892 bytes
-rw-r--r--samples/13_path_finding_algorithms/03_breadcrumbs/app/main.rb545
-rw-r--r--samples/13_path_finding_algorithms/03_breadcrumbs/arrow.pngbin0 -> 5762 bytes
-rw-r--r--samples/13_path_finding_algorithms/03_breadcrumbs/star.pngbin0 -> 4892 bytes
-rw-r--r--samples/13_path_finding_algorithms/03_breadcrumbs/target.pngbin0 -> 5112 bytes
-rw-r--r--samples/13_path_finding_algorithms/04_early_exit/app/main.rb631
-rw-r--r--samples/13_path_finding_algorithms/04_early_exit/star.pngbin0 -> 4892 bytes
-rw-r--r--samples/13_path_finding_algorithms/04_early_exit/target.pngbin0 -> 5112 bytes
-rw-r--r--samples/13_path_finding_algorithms/05_dijkstra/app/main.rb844
-rw-r--r--samples/13_path_finding_algorithms/05_dijkstra/star.pngbin0 -> 4892 bytes
-rw-r--r--samples/13_path_finding_algorithms/05_dijkstra/target.pngbin0 -> 5112 bytes
-rw-r--r--samples/13_path_finding_algorithms/06_heuristic/app/main.rb980
-rw-r--r--samples/13_path_finding_algorithms/06_heuristic/circle-white.pngbin0 -> 1754 bytes
-rw-r--r--samples/13_path_finding_algorithms/06_heuristic/star.pngbin0 -> 4892 bytes
-rw-r--r--samples/13_path_finding_algorithms/06_heuristic/target.pngbin0 -> 5112 bytes
-rw-r--r--samples/13_path_finding_algorithms/07_heuristic_with_walls/app/main.rb1013
-rw-r--r--samples/13_path_finding_algorithms/07_heuristic_with_walls/circle-white.pngbin0 -> 1754 bytes
-rw-r--r--samples/13_path_finding_algorithms/07_heuristic_with_walls/star.pngbin0 -> 4892 bytes
-rw-r--r--samples/13_path_finding_algorithms/07_heuristic_with_walls/target.pngbin0 -> 5112 bytes
-rw-r--r--samples/13_path_finding_algorithms/08_a_star/app/main.rb1029
-rw-r--r--samples/13_path_finding_algorithms/08_a_star/circle-white.pngbin0 -> 1754 bytes
-rw-r--r--samples/13_path_finding_algorithms/08_a_star/star.pngbin0 -> 4892 bytes
-rw-r--r--samples/13_path_finding_algorithms/08_a_star/target.pngbin0 -> 5112 bytes
-rw-r--r--samples/99_genre_3d/01_3d_cube/app/main.rb50
-rw-r--r--samples/99_genre_3d/01_3d_cube/sprites/square-blue.pngbin0 -> 283 bytes
-rw-r--r--samples/99_genre_3d/02_wireframe/app/main.rb150
-rw-r--r--samples/99_genre_3d/02_wireframe/data/shuttle.off284
-rw-r--r--samples/99_genre_3d/02_wireframe/data/what-is-this.txt1
-rw-r--r--samples/99_genre_arcade/twinstick/app/main.rb151
-rw-r--r--samples/99_genre_arcade/twinstick/sprites/circle-white.pngbin0 -> 1754 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/app/main.rb529
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/border-black.pngbin0 -> 908 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-black.pngbin0 -> 1882 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-blue.pngbin0 -> 2901 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-gray.pngbin0 -> 3006 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-green.pngbin0 -> 2887 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-indigo.pngbin0 -> 2433 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-orange.pngbin0 -> 2670 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-red.pngbin0 -> 2233 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-violet.pngbin0 -> 2439 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-white.pngbin0 -> 1754 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-yellow.pngbin0 -> 2456 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-0.pngbin0 -> 12896 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-1.pngbin0 -> 2964 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-2.pngbin0 -> 3047 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-3.pngbin0 -> 2655 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-4.pngbin0 -> 2725 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-5.pngbin0 -> 2655 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-0.pngbin0 -> 267 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-1.pngbin0 -> 4585 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-2.pngbin0 -> 4675 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-3.pngbin0 -> 4724 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-4.pngbin0 -> 4773 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-5.pngbin0 -> 4742 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-6.pngbin0 -> 4665 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-sheet.pngbin0 -> 2584 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-black.pngbin0 -> 2602 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-blue.pngbin0 -> 4842 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-gray.pngbin0 -> 5184 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-green.pngbin0 -> 4695 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-indigo.pngbin0 -> 4918 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-orange.pngbin0 -> 4825 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-red.pngbin0 -> 3753 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-violet.pngbin0 -> 5069 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-white.pngbin0 -> 5326 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-yellow.pngbin0 -> 5249 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-black.pngbin0 -> 264 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-blue.pngbin0 -> 361 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-gray.pngbin0 -> 493 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-green.pngbin0 -> 361 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-indigo.pngbin0 -> 361 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-orange.pngbin0 -> 361 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-red.pngbin0 -> 361 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-violet.pngbin0 -> 361 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-white.pngbin0 -> 361 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-yellow.pngbin0 -> 361 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/roy-0.pngbin0 -> 20775 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/roy-1.pngbin0 -> 3132 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-black.pngbin0 -> 250 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-blue.pngbin0 -> 283 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-gray.pngbin0 -> 251 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-green.pngbin0 -> 283 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-indigo.pngbin0 -> 283 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-orange.pngbin0 -> 282 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-red.pngbin0 -> 274 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-violet.pngbin0 -> 284 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-white.pngbin0 -> 279 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-yellow.pngbin0 -> 286 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/star.pngbin0 -> 711 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/water-1.pngbin0 -> 656794 bytes
-rw-r--r--samples/99_genre_rpg_tactical/gameboard_movement/sprites/water-2.pngbin0 -> 655810 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/app/main.rb189
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/run.bat6
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/circle-blue.pngbin0 -> 2901 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/circle-gray.pngbin0 -> 3006 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/circle-green.pngbin0 -> 2887 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/circle-orange.pngbin0 -> 2670 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-0.pngbin0 -> 267 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-1.pngbin0 -> 4585 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-2.pngbin0 -> 4675 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-3.pngbin0 -> 4724 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-4.pngbin0 -> 4773 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-5.pngbin0 -> 4742 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-6.pngbin0 -> 4665 bytes
-rw-r--r--samples/99_genre_rpg_tactical/taking_turns/sprites/star.pngbin0 -> 711 bytes
374 files changed, 12302 insertions, 311 deletions
diff --git a/samples/01_rendering_basics/07_sound_synthesis/app/main.rb b/samples/01_rendering_basics/07_sound_synthesis/app/main.rb
index fc58126..391ce68 100644
--- a/samples/01_rendering_basics/07_sound_synthesis/app/main.rb
+++ b/samples/01_rendering_basics/07_sound_synthesis/app/main.rb
@@ -1,356 +1,593 @@
-def tick args
- defaults args
- render args
- input args
- process_audio_queue args
-end
+begin # region: top level tick methods
+ def tick args
+ defaults args
+ render args
+ input args
+ process_audio_queue args
+ end
-def defaults args
- args.state.sine_waves ||= {}
- args.state.audio_queue ||= []
- args.state.buttons ||= [
- (frequency_buttons args),
- (note_buttons args),
- (bell_buttons args)
- ].flatten
-end
+ def defaults args
+ args.state.sine_waves ||= {}
+ args.state.square_waves ||= {}
+ args.state.saw_tooth_waves ||= {}
+ args.state.triangle_waves ||= {}
+ args.state.audio_queue ||= []
+ args.state.buttons ||= [
+ (frequency_buttons args),
+ (sine_wave_note_buttons args),
+ (bell_buttons args),
+ (square_wave_note_buttons args),
+ (saw_tooth_wave_note_buttons args),
+ (triangle_wave_note_buttons args),
+ ].flatten
+ end
-def frequency_buttons args
- [
- (button args,
- row: 4.0, col: 0, text: "300hz",
- frequency: 300,
- method_to_call: :play_sine_wave),
- (button args,
- row: 5.0, col: 0, text: "400hz",
- frequency: 400,
- method_to_call: :play_sine_wave),
- (button args,
- row: 6.0, col: 0, text: "500hz",
- frequency: 500,
- method_to_call: :play_sine_wave),
- ]
-end
+ def render args
+ args.outputs.borders << args.state.buttons.map { |b| b[:border] }
+ args.outputs.labels << args.state.buttons.map { |b| b[:label] }
+ args.outputs.labels << args.layout
+ .rect(row: 0, col: 11.5)
+ .yield_self { |r| r.merge y: r.y + r.h }
+ .merge(text: "This is a Pro only feature. Click here to watch the YouTube video if you are on the Standard License.",
+ alignment_enum: 1)
+ end
-def play_sine_wave args, sender
- queue_sine_wave args,
- frequency: sender[:frequency],
- duration: 1.seconds,
- fade_out: true
-end
+ def input args
+ args.state.buttons.each do |b|
+ if args.inputs.mouse.click.inside_rect? b[:rect]
+ parameter_string = (b.slice :frequency, :note, :octave).map { |k, v| "#{k}: #{v}" }.join ", "
+ args.gtk.notify! "#{b[:method_to_call]} #{parameter_string}"
+ send b[:method_to_call], args, b
+ end
+ end
-def note_buttons args
- [
- (button args,
- row: 1.5, col: 3, text: "C4",
- note: :c, octave: 4, method_to_call: :play_note),
- (button args,
- row: 2.5, col: 3, text: "D4",
- note: :d, octave: 4, method_to_call: :play_note),
- (button args,
- row: 3.5, col: 3, text: "E4",
- note: :e, octave: 4, method_to_call: :play_note),
- (button args,
- row: 4.5, col: 3, text: "F4",
- note: :f, octave: 4, method_to_call: :play_note),
- (button args,
- row: 5.5, col: 3, text: "G4",
- note: :g, octave: 4, method_to_call: :play_note),
- (button args,
- row: 6.5, col: 3, text: "A5",
- note: :a, octave: 5, method_to_call: :play_note),
- (button args,
- row: 7.5, col: 3, text: "B5",
- note: :b, octave: 5, method_to_call: :play_note),
- (button args,
- row: 8.5, col: 3, text: "C5",
- note: :c, octave: 5, method_to_call: :play_note),
- ]
-end
+ if args.inputs.mouse.click.inside_rect? (args.layout.rect(row: 0).yield_self { |r| r.merge y: r.y + r.h.half, h: r.h.half })
+ args.gtk.openurl 'https://www.youtube.com/watch?v=zEzovM5jT-k&ab_channel=AmirRajan'
+ end
+ end
-def play_note args, sender
- queue_sine_wave args,
- frequency: (frequency_for note: sender[:note],
- octave: sender[:octave]),
- duration: 1.seconds,
- fade_out: true
-end
+ def process_audio_queue args
+ to_queue = args.state.audio_queue.find_all { |v| v[:queue_at] <= args.tick_count }
+ args.state.audio_queue -= to_queue
+ to_queue.each { |a| args.audio[a[:id]] = a }
-def bell_buttons args
- [
- (button args,
- row: 1.5, col: 6, text: "Bell C4",
- note: :c, octave: 4, method_to_call: :play_bell),
- (button args,
- row: 2.5, col: 6, text: "Bell D4",
- note: :d, octave: 4, method_to_call: :play_bell),
- (button args,
- row: 3.5, col: 6, text: "Bell E4",
- note: :e, octave: 4, method_to_call: :play_bell),
- (button args,
- row: 4.5, col: 6, text: "Bell F4",
- note: :f, octave: 4, method_to_call: :play_bell),
- (button args,
- row: 5.5, col: 6, text: "Bell G4",
- note: :g, octave: 4, method_to_call: :play_bell),
- (button args,
- row: 6.5, col: 6, text: "Bell A5",
- note: :a, octave: 5, method_to_call: :play_bell),
- (button args,
- row: 7.5, col: 6, text: "Bell B5",
- note: :b, octave: 5, method_to_call: :play_bell),
- (button args,
- row: 8.5, col: 6, text: "Bell C5",
- note: :c, octave: 5, method_to_call: :play_bell),
- ]
-end
+ args.audio.find_all { |k, v| v[:decay_rate] }
+ .each { |k, v| v[:gain] -= v[:decay_rate] }
-def play_bell args, sender
- queue_bell args,
- frequency: (frequency_for note: sender[:note],
- octave: sender[:octave]),
- duration: 2.seconds,
- fade_out: true
-end
+ sounds_to_stop = args.audio
+ .find_all { |k, v| v[:stop_at] && args.state.tick_count >= v[:stop_at] }
+ .map { |k, v| k }
-def render args
- args.outputs.borders << args.state.buttons.map { |b| b[:border] }
- args.outputs.labels << args.state.buttons.map { |b| b[:label] }
- args.outputs.labels << args.layout
- .rect(row: 0,
- col: 11.5)
- .yield_self { |r| r.merge y: r.y + r.h }
- .merge(text: "This is a Pro only feature. Click here to watch the YouTube video if you are on the Standard License.",
- alignment_enum: 1)
+ sounds_to_stop.each { |k| args.audio.delete k }
+ end
end
-def input args
- args.state.buttons.each do |b|
- if args.inputs.mouse.click.inside_rect? b[:rect]
- parameter_string = (b.slice :frequency, :note, :octave).map { |k, v| "#{k}: #{v}" }.join ", "
- args.gtk.notify! "#{b[:method_to_call]} #{parameter_string}"
- send b[:method_to_call], args, b
- end
- end
+begin # region: button definitions, ui layout, callback functions
+ def button args, opts
+ button_def = opts.merge rect: (args.layout.rect (opts.merge w: 2, h: 1))
+
+ button_def[:border] = button_def[:rect].merge r: 0, g: 0, b: 0
+
+ label_offset_x = 5
+ label_offset_y = 30
+
+ button_def[:label] = button_def[:rect].merge text: opts[:text],
+ size_enum: -2.5,
+ x: button_def[:rect].x + label_offset_x,
+ y: button_def[:rect].y + label_offset_y
- if args.inputs.mouse.click.inside_rect? (args.layout.rect(row: 0).yield_self { |r| r.merge y: r.y + r.h.half, h: r.h.half })
- args.gtk.openurl 'https://www.youtube.com/watch?v=zEzovM5jT-k&ab_channel=AmirRajan'
+ button_def
end
-end
-def process_audio_queue args
- to_queue = args.state.audio_queue.find_all { |v| v[:queue_at] <= args.tick_count }
- args.state.audio_queue -= to_queue
+ def play_sine_wave args, sender
+ queue_sine_wave args,
+ frequency: sender[:frequency],
+ duration: 1.seconds,
+ fade_out: true
+ end
- to_queue.each do |a|
- args.audio[a[:id]] = a
+ def play_note args, sender
+ method_to_call = :queue_sine_wave
+ method_to_call = :queue_square_wave if sender[:type] == :square
+ method_to_call = :queue_saw_tooth_wave if sender[:type] == :saw_tooth
+ method_to_call = :queue_triangle_wave if sender[:type] == :triangle
+ method_to_call = :queue_bell if sender[:type] == :bell
+
+ send method_to_call, args,
+ frequency: (frequency_for note: sender[:note], octave: sender[:octave]),
+ duration: 1.seconds,
+ fade_out: true
end
- args.audio.each do |k, v|
- if v[:decay_rate]
- v[:gain] -= v[:decay_rate]
- end
+ def frequency_buttons args
+ [
+ (button args,
+ row: 4.0, col: 0, text: "300hz",
+ frequency: 300,
+ method_to_call: :play_sine_wave),
+ (button args,
+ row: 5.0, col: 0, text: "400hz",
+ frequency: 400,
+ method_to_call: :play_sine_wave),
+ (button args,
+ row: 6.0, col: 0, text: "500hz",
+ frequency: 500,
+ method_to_call: :play_sine_wave),
+ ]
end
- sounds_to_stop = args.audio.find_all do |k, v|
- v[:stop_at] && args.state.tick_count >= v[:stop_at]
+ def sine_wave_note_buttons args
+ [
+ (button args,
+ row: 1.5, col: 2, text: "Sine C4",
+ note: :c, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 2, text: "Sine D4",
+ note: :d, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 2, text: "Sine E4",
+ note: :e, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 2, text: "Sine F4",
+ note: :f, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 2, text: "Sine G4",
+ note: :g, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 2, text: "Sine A5",
+ note: :a, octave: 5, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 2, text: "Sine B5",
+ note: :b, octave: 5, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 2, text: "Sine C5",
+ note: :c, octave: 5, type: :sine, method_to_call: :play_note),
+ ]
end
- sounds_to_stop.each do |(k, v)|
- args.audio.delete k
+ def square_wave_note_buttons args
+ [
+ (button args,
+ row: 1.5, col: 6, text: "Square C4",
+ note: :c, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 6, text: "Square D4",
+ note: :d, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 6, text: "Square E4",
+ note: :e, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 6, text: "Square F4",
+ note: :f, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 6, text: "Square G4",
+ note: :g, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 6, text: "Square A5",
+ note: :a, octave: 5, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 6, text: "Square B5",
+ note: :b, octave: 5, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 6, text: "Square C5",
+ note: :c, octave: 5, type: :square, method_to_call: :play_note),
+ ]
+ end
+ def saw_tooth_wave_note_buttons args
+ [
+ (button args,
+ row: 1.5, col: 8, text: "Saw C4",
+ note: :c, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 8, text: "Saw D4",
+ note: :d, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 8, text: "Saw E4",
+ note: :e, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 8, text: "Saw F4",
+ note: :f, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 8, text: "Saw G4",
+ note: :g, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 8, text: "Saw A5",
+ note: :a, octave: 5, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 8, text: "Saw B5",
+ note: :b, octave: 5, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 8, text: "Saw C5",
+ note: :c, octave: 5, type: :saw_tooth, method_to_call: :play_note),
+ ]
end
-end
-def graph_sine_wave args, sine_wave, frequency
- if args.state.tick_count != args.state.graphed_at
- args.outputs.static_lines.clear
- args.outputs.static_sprites.clear
+ def triangle_wave_note_buttons args
+ [
+ (button args,
+ row: 1.5, col: 10, text: "Triangle C4",
+ note: :c, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 10, text: "Triangle D4",
+ note: :d, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 10, text: "Triangle E4",
+ note: :e, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 10, text: "Triangle F4",
+ note: :f, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 10, text: "Triangle G4",
+ note: :g, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 10, text: "Triangle A5",
+ note: :a, octave: 5, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 10, text: "Triangle B5",
+ note: :b, octave: 5, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 10, text: "Triangle C5",
+ note: :c, octave: 5, type: :triangle, method_to_call: :play_note),
+ ]
end
- r, g, b = frequency.to_i % 80, frequency.to_i % 128, frequency.to_i % 255
- center_row = args.layout.rect(row: 5, col: 9)
- x_scale = 20
- y_scale = 100
- max_points = 20
-
- points = sine_wave
- if sine_wave.length > max_points
- resolution = sine_wave.length.idiv max_points
- points = sine_wave.find_all
- .with_index { |y, i| i % resolution == 0 }
+ def bell_buttons args
+ [
+ (button args,
+ row: 1.5, col: 4, text: "Bell C4",
+ note: :c, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 4, text: "Bell D4",
+ note: :d, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 4, text: "Bell E4",
+ note: :e, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 4, text: "Bell F4",
+ note: :f, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 4, text: "Bell G4",
+ note: :g, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 4, text: "Bell A5",
+ note: :a, octave: 5, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 4, text: "Bell B5",
+ note: :b, octave: 5, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 4, text: "Bell C5",
+ note: :c, octave: 5, type: :bell, method_to_call: :play_note),
+ ]
end
+end
- args.outputs.static_lines << points.map_with_index do |y, x|
- next_y = points[x + 1]
+begin # region: wave generation
+ begin # sine wave
+ def defaults_sine_wave_for
+ { frequency: 440, sample_rate: 48000 }
+ end
- if next_y
- {
- x: center_row.x + (x * x_scale),
- y: center_row.y + center_row.h.half + y_scale * y,
- x2: center_row.x + ((x + 1) * x_scale),
- y2: center_row.y + center_row.h.half + y_scale * next_y,
- r: r,
- g: g,
- b: b
- }
+ def sine_wave_for opts = {}
+ opts = defaults_sine_wave_for.merge opts
+ frequency = opts[:frequency]
+ sample_rate = opts[:sample_rate]
+ period_size = (sample_rate.fdiv frequency).ceil
+ period_size.map_with_index do |i|
+ Math::sin((2.0 * Math::PI) / (sample_rate.to_f / frequency.to_f) * i)
+ end.to_a
+ end
+
+ def defaults_queue_sine_wave
+ { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 }
+ end
+
+ def queue_sine_wave args, opts = {}
+ opts = defaults_queue_sine_wave.merge opts
+ frequency = opts[:frequency]
+ sample_rate = 48000
+
+ sine_wave = sine_wave_for frequency: frequency, sample_rate: sample_rate
+ args.state.sine_waves[frequency] ||= sine_wave_for frequency: frequency, sample_rate: sample_rate
+
+ proc = lambda do
+ generate_audio_data args.state.sine_waves[frequency], sample_rate
+ end
+
+ audio_state = new_audio_state args, opts
+ audio_state[:input] = [1, sample_rate, proc]
+ queue_audio args, audio_state: audio_state, wave: sine_wave
end
end
- args.outputs.static_sprites << points.map_with_index do |y, x|
- {
- x: (center_row.x + (x * x_scale)) - 1,
- y: (center_row.y + center_row.h.half + y_scale * y) - 1,
- w: 2,
- h: 2,
- path: 'sprites/square-black.png'
- }
+ begin # region: square wave
+ def defaults_square_wave_for
+ { frequency: 440, sample_rate: 48000 }
+ end
+
+ def square_wave_for opts = {}
+ opts = defaults_square_wave_for.merge opts
+ sine_wave = sine_wave_for opts
+ sine_wave.map do |v|
+ if v >= 0
+ 1.0
+ else
+ -1.0
+ end
+ end.to_a
+ end
+
+ def defaults_queue_square_wave
+ { frequency: 440, duration: 60, gain: 0.3, fade_out: false, queue_in: 0 }
+ end
+
+ def queue_square_wave args, opts = {}
+ opts = defaults_queue_square_wave.merge opts
+ frequency = opts[:frequency]
+ sample_rate = 48000
+
+ square_wave = square_wave_for frequency: frequency, sample_rate: sample_rate
+ args.state.square_waves[frequency] ||= square_wave_for frequency: frequency, sample_rate: sample_rate
+
+ proc = lambda do
+ generate_audio_data args.state.square_waves[frequency], sample_rate
+ end
+
+ audio_state = new_audio_state args, opts
+ audio_state[:input] = [1, sample_rate, proc]
+ queue_audio args, audio_state: audio_state, wave: square_wave
+ end
end
- args.state.graphed_at = args.state.tick_count
-end
+ begin # region: saw tooth wave
+ def defaults_saw_tooth_wave_for
+ { frequency: 440, sample_rate: 48000 }
+ end
-def defaults_period_sine_wave_for
- { frequency: 440, sample_rate: 48000 }
-end
+ def saw_tooth_wave_for opts = {}
+ opts = defaults_saw_tooth_wave_for.merge opts
+ sine_wave = sine_wave_for opts
+ period_size = sine_wave.length
+ sine_wave.map_with_index do |v, i|
+ (((i % period_size).fdiv period_size) * 2) - 1
+ end
+ end
-def sine_wave_for opts = { }
- opts = defaults_period_sine_wave_for.merge opts
- frequency = opts[:frequency]
- sample_rate = opts[:sample_rate]
- period_size = (sample_rate.fdiv frequency).ceil
- period_size.map_with_index do |i|
- Math::sin((2.0 * Math::PI) / (sample_rate.to_f / frequency.to_f) * i)
- end.to_a
-end
+ def defaults_queue_saw_tooth_wave
+ { frequency: 440, duration: 60, gain: 0.3, fade_out: false, queue_in: 0 }
+ end
-def generate_audio_data sine_wave, sample_rate
- sample_size = (sample_rate.fdiv (1000.fdiv 60)).ceil
- copy_count = (sample_size.fdiv sine_wave.length).ceil
- sine_wave * copy_count
-end
+ def queue_saw_tooth_wave args, opts = {}
+ opts = defaults_queue_saw_tooth_wave.merge opts
+ frequency = opts[:frequency]
+ sample_rate = 48000
-def defaults_queue_sine_wave
- { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 }
-end
+ saw_tooth_wave = saw_tooth_wave_for frequency: frequency, sample_rate: sample_rate
+ args.state.saw_tooth_waves[frequency] ||= saw_tooth_wave_for frequency: frequency, sample_rate: sample_rate
+
+ proc = lambda do
+ generate_audio_data args.state.saw_tooth_waves[frequency], sample_rate
+ end
-def queue_sine_wave args, opts = { }
- opts = defaults_queue_sine_wave.merge opts
- decay_rate = 0
- decay_rate = 1.fdiv(opts[:duration]) * opts[:gain] if opts[:fade_out]
- frequency = opts[:frequency]
- sample_rate = 48000
-
- audio_state = {
- id: (new_id! args),
- frequency: frequency,
- sample_rate: 48000,
- stop_at: args.tick_count + opts[:queue_in] + opts[:duration],
- gain: opts[:gain].to_f,
- queue_at: args.state.tick_count + opts[:queue_in],
- decay_rate: decay_rate,
- pitch: 1.0,
- looping: true,
- paused: false
- }
-
- sine_wave = sine_wave_for frequency: frequency, sample_rate: sample_rate
- args.state.sine_waves[frequency] ||= sine_wave_for frequency: frequency, sample_rate: sample_rate
-
- proc = lambda do
- generate_audio_data args.state.sine_waves[frequency], sample_rate
+ audio_state = new_audio_state args, opts
+ audio_state[:input] = [1, sample_rate, proc]
+ queue_audio args, audio_state: audio_state, wave: saw_tooth_wave
+ end
end
- audio_state[:input] = [1, sample_rate, proc]
- graph_sine_wave args, sine_wave, frequency
- args.state.audio_queue << audio_state
-end
+ begin # region: triangle wave
+ def defaults_triangle_wave_for
+ { frequency: 440, sample_rate: 48000 }
+ end
-def defaults_queue_bell
- { frequency: 440, duration: 1.seconds, queue_in: 0 }
-end
+ def triangle_wave_for opts = {}
+ opts = defaults_saw_tooth_wave_for.merge opts
+ sine_wave = sine_wave_for opts
+ period_size = sine_wave.length
+ sine_wave.map_with_index do |v, i|
+ ratio = (i.fdiv period_size)
+ if ratio <= 0.5
+ (ratio * 4) - 1
+ else
+ ratio -= 0.5
+ 1 - (ratio * 4)
+ end
+ end
+ end
-def queue_bell args, opts = {}
- (bell_to_sine_waves (defaults_queue_bell.merge opts)).each { |b| queue_sine_wave args, b }
-end
+ def defaults_queue_triangle_wave
+ { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 }
+ end
-def bell_harmonics
- [
- { frequency_ratio: 0.5, duration_ratio: 1.00 },
- { frequency_ratio: 1.0, duration_ratio: 0.80 },
- { frequency_ratio: 2.0, duration_ratio: 0.60 },
- { frequency_ratio: 3.0, duration_ratio: 0.40 },
- { frequency_ratio: 4.2, duration_ratio: 0.25 },
- { frequency_ratio: 5.4, duration_ratio: 0.20 },
- { frequency_ratio: 6.8, duration_ratio: 0.15 }
- ]
-end
+ def queue_triangle_wave args, opts = {}
+ opts = defaults_queue_triangle_wave.merge opts
+ frequency = opts[:frequency]
+ sample_rate = 48000
-def bell_to_sine_waves opts
- bell_harmonics.map do |b|
- {
- frequency: opts[:frequency] * b[:frequency_ratio],
- duration: opts[:duration] * b[:duration_ratio],
- queue_in: opts[:queue_in],
- gain: (1.fdiv bell_harmonics.length),
- fade_out: true
- }
+ triangle_wave = triangle_wave_for frequency: frequency, sample_rate: sample_rate
+ args.state.triangle_waves[frequency] ||= triangle_wave_for frequency: frequency, sample_rate: sample_rate
+
+ proc = lambda do
+ generate_audio_data args.state.triangle_waves[frequency], sample_rate
+ end
+
+ audio_state = new_audio_state args, opts
+ audio_state[:input] = [1, sample_rate, proc]
+ queue_audio args, audio_state: audio_state, wave: triangle_wave
+ end
end
-end
-def defaults_frequency_for
- { note: :a, octave: 5, sharp: false, flat: false }
-end
+ begin # region: bell
+ def defaults_queue_bell
+ { frequency: 440, duration: 1.seconds, queue_in: 0 }
+ end
-def frequency_for opts = {}
- opts = defaults_frequency_for.merge opts
- octave_offset_multiplier = opts[:octave] - 5
- note = note_frequencies_octave_5[opts[:note]]
- if octave_offset_multiplier < 0
- note = note * 1 / (octave_offset_multiplier.abs + 1)
- elsif octave_offset_multiplier > 0
- note = note * (octave_offset_multiplier.abs + 1) / 1
+ def queue_bell args, opts = {}
+ (bell_to_sine_waves (defaults_queue_bell.merge opts)).each { |b| queue_sine_wave args, b }
+ end
+
+ def bell_harmonics
+ [
+ { frequency_ratio: 0.5, duration_ratio: 1.00 },
+ { frequency_ratio: 1.0, duration_ratio: 0.80 },
+ { frequency_ratio: 2.0, duration_ratio: 0.60 },
+ { frequency_ratio: 3.0, duration_ratio: 0.40 },
+ { frequency_ratio: 4.2, duration_ratio: 0.25 },
+ { frequency_ratio: 5.4, duration_ratio: 0.20 },
+ { frequency_ratio: 6.8, duration_ratio: 0.15 }
+ ]
+ end
+
+ def defaults_bell_to_sine_waves
+ { frequency: 440, duration: 1.seconds, queue_in: 0 }
+ end
+
+ def bell_to_sine_waves opts = {}
+ opts = defaults_bell_to_sine_waves.merge opts
+ bell_harmonics.map do |b|
+ {
+ frequency: opts[:frequency] * b[:frequency_ratio],
+ duration: opts[:duration] * b[:duration_ratio],
+ queue_in: opts[:queue_in],
+ gain: (1.fdiv bell_harmonics.length),
+ fade_out: true
+ }
+ end
+ end
end
- note
-end
-def note_frequencies_octave_5
- {
- a: 440.0,
- a_sharp: 466.16, b_flat: 466.16,
- b: 493.88,
- c: 523.25,
- c_sharp: 554.37, d_flat: 587.33,
- d: 587.33,
- d_sharp: 622.25, e_flat: 659.25,
- e: 659.25,
- f: 698.25,
- f_sharp: 739.99, g_flat: 739.99,
- g: 783.99,
- g_sharp: 830.61, a_flat: 830.61
- }
-end
+ begin # audio entity construction
+ def generate_audio_data sine_wave, sample_rate
+ sample_size = (sample_rate.fdiv (1000.fdiv 60)).ceil
+ copy_count = (sample_size.fdiv sine_wave.length).ceil
+ sine_wave * copy_count
+ end
-def new_id! args
- args.state.audio_id ||= 0
- args.state.audio_id += 1
-end
+ def defaults_new_audio_state
+ { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 }
+ end
-def button args, opts
- button_def = opts.merge rect: (args.layout.rect (opts.merge w: 2, h: 1))
+ def new_audio_state args, opts = {}
+ opts = defaults_new_audio_state.merge opts
+ decay_rate = 0
+ decay_rate = 1.fdiv(opts[:duration]) * opts[:gain] if opts[:fade_out]
+ frequency = opts[:frequency]
+ sample_rate = 48000
- button_def[:border] = button_def[:rect].merge r: 0, g: 0, b: 0
+ {
+ id: (new_id! args),
+ frequency: frequency,
+ sample_rate: 48000,
+ stop_at: args.tick_count + opts[:queue_in] + opts[:duration],
+ gain: opts[:gain].to_f,
+ queue_at: args.state.tick_count + opts[:queue_in],
+ decay_rate: decay_rate,
+ pitch: 1.0,
+ looping: true,
+ paused: false
+ }
+ end
- font_size_enum = args.layout.font_relative_size_enum 0
- label_offset_x = 4
- label_offset_y = button_def[:rect].h.half + button_def[:rect].h.idiv(4)
+ def queue_audio args, opts = {}
+ graph_wave args, opts[:wave], opts[:audio_state][:frequency]
+ args.state.audio_queue << opts[:audio_state]
+ end
- button_def[:label] = button_def[:rect].merge text: opts[:text],
- size_enum: font_size_enum,
- x: button_def[:rect].x + label_offset_x,
- y: button_def[:rect].y + label_offset_y
+ def new_id! args
+ args.state.audio_id ||= 0
+ args.state.audio_id += 1
+ end
+
+ def graph_wave args, wave, frequency
+ if args.state.tick_count != args.state.graphed_at
+ args.outputs.static_lines.clear
+ args.outputs.static_sprites.clear
+ end
+
+ wave = wave
+
+ r, g, b = frequency.to_i % 85,
+ frequency.to_i % 170,
+ frequency.to_i % 255
+
+ starting_rect = args.layout.rect(row: 5, col: 13)
+ x_scale = 10
+ y_scale = 100
+ max_points = 25
+
+ points = wave
+ if wave.length > max_points
+ resolution = wave.length.idiv max_points
+ points = wave.find_all.with_index { |y, i| (i % resolution == 0) }
+ end
+
+ args.outputs.static_lines << points.map_with_index do |y, x|
+ next_y = points[x + 1]
+
+ if next_y
+ {
+ x: starting_rect.x + (x * x_scale),
+ y: starting_rect.y + starting_rect.h.half + y_scale * y,
+ x2: starting_rect.x + ((x + 1) * x_scale),
+ y2: starting_rect.y + starting_rect.h.half + y_scale * next_y,
+ r: r,
+ g: g,
+ b: b
+ }
+ end
+ end
+
+ args.outputs.static_sprites << points.map_with_index do |y, x|
+ {
+ x: (starting_rect.x + (x * x_scale)) - 2,
+ y: (starting_rect.y + starting_rect.h.half + y_scale * y) - 2,
+ w: 4,
+ h: 4,
+ path: 'sprites/square-white.png',
+ r: r,
+ g: g,
+ b: b
+ }
+ end
+
+ args.state.graphed_at = args.state.tick_count
+ end
+ end
+
+ begin # region: musical note mapping
+ def defaults_frequency_for
+ { note: :a, octave: 5, sharp: false, flat: false }
+ end
- button_def
+ def frequency_for opts = {}
+ opts = defaults_frequency_for.merge opts
+ octave_offset_multiplier = opts[:octave] - 5
+ note = note_frequencies_octave_5[opts[:note]]
+ if octave_offset_multiplier < 0
+ note = note * 1 / (octave_offset_multiplier.abs + 1)
+ elsif octave_offset_multiplier > 0
+ note = note * (octave_offset_multiplier.abs + 1) / 1
+ end
+ note
+ end
+
+ def note_frequencies_octave_5
+ {
+ a: 440.0,
+ a_sharp: 466.16, b_flat: 466.16,
+ b: 493.88,
+ c: 523.25,
+ c_sharp: 554.37, d_flat: 587.33,
+ d: 587.33,
+ d_sharp: 622.25, e_flat: 659.25,
+ e: 659.25,
+ f: 698.25,
+ f_sharp: 739.99, g_flat: 739.99,
+ g: 783.99,
+ g_sharp: 830.61, a_flat: 830.61
+ }
+ end
+ end
end
$gtk.reset
diff --git a/samples/01_rendering_basics/07_sound_synthesis/sprites/square-white.png b/samples/01_rendering_basics/07_sound_synthesis/sprites/square-white.png
new file mode 100644
index 0000000..378c565
--- /dev/null
+++ b/samples/01_rendering_basics/07_sound_synthesis/sprites/square-white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/app/main.rb b/samples/04_physics_and_collisions/06_box_collision_3/app/main.rb
new file mode 100644
index 0000000..2e32eee
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/app/main.rb
@@ -0,0 +1,255 @@
+class Game
+ attr_gtk
+
+ def tick
+ defaults
+ render
+ input_edit_map
+ input_player
+ calc_player
+ end
+
+ def defaults
+ state.gravity = -0.4
+ state.drag = 0.15
+ state.tile_size = 32
+ state.player.size = 16
+ state.player.jump_power = 12
+
+ state.tiles ||= []
+ state.player.y ||= 800
+ state.player.x ||= 100
+ state.player.dy ||= 0
+ state.player.dx ||= 0
+ state.player.jumped_down_at ||= 0
+ state.player.jumped_at ||= 0
+
+ calc_player_rect if !state.player.rect
+ end
+
+ def render
+ outputs.labels << [10, 10.from_top, "tile: click to add a tile, hold X key and click to delete a tile."]
+ outputs.labels << [10, 35.from_top, "move: use left and right to move, space to jump, down and space to jump down."]
+ outputs.labels << [10, 55.from_top, " You can jump through or jump down through tiles with a height of 1."]
+ outputs.background_color = [80, 80, 80]
+ outputs.sprites << tiles.map(&:sprite)
+ outputs.sprites << (player.rect.merge path: 'sprites/square/green.png')
+
+ mouse_overlay = {
+ x: (inputs.mouse.x.ifloor state.tile_size),
+ y: (inputs.mouse.y.ifloor state.tile_size),
+ w: state.tile_size,
+ h: state.tile_size,
+ a: 100
+ }
+
+ mouse_overlay = mouse_overlay.merge r: 255 if state.delete_mode
+
+ if state.mouse_held
+ outputs.primitives << mouse_overlay.border
+ else
+ outputs.primitives << mouse_overlay.solid
+ end
+ end
+
+ def input_edit_map
+ state.mouse_held = true if inputs.mouse.down
+ state.mouse_held = false if inputs.mouse.up
+
+ if inputs.keyboard.x
+ state.delete_mode = true
+ elsif inputs.keyboard.key_up.x
+ state.delete_mode = false
+ end
+
+ return unless state.mouse_held
+
+ ordinal = { x: (inputs.mouse.x.idiv state.tile_size),
+ y: (inputs.mouse.y.idiv state.tile_size) }
+
+ found = find_tile ordinal
+ if !found && !state.delete_mode
+ tiles << (state.new_entity :tile, ordinal)
+ recompute_tiles
+ elsif found && state.delete_mode
+ tiles.delete found
+ recompute_tiles
+ end
+ end
+
+ def input_player
+ player.dx += inputs.left_right
+
+ if inputs.keyboard.key_down.space && inputs.keyboard.down
+ player.dy = player.jump_power * -1
+ player.jumped_at = 0
+ player.jumped_down_at = state.tick_count
+ elsif inputs.keyboard.key_down.space
+ player.dy = player.jump_power
+ player.jumped_at = state.tick_count
+ player.jumped_down_at = 0
+ end
+ end
+
+ def calc_player
+ calc_player_rect
+ calc_below
+ calc_left
+ calc_right
+ calc_above
+ calc_player_dy
+ calc_player_dx
+ reset_player if player_off_stage?
+ end
+
+ def calc_player_rect
+ player.rect = current_player_rect
+ player.next_rect = player.rect.merge x: player.x + player.dx,
+ y: player.y + player.dy
+ player.prev_rect = player.rect.merge x: player.x - player.dx,
+ y: player.y - player.dy
+ end
+
+ def calc_below
+ return unless player.dy <= 0
+ tiles_below = find_tiles { |t| t.rect.top <= player.y }
+ collision = find_colliding_tile tiles_below, (player.rect.merge y: player.next_rect.y)
+ return unless collision
+ if collision.neighbors.b == :none && player.jumped_down_at.elapsed_time < 10
+ player.dy = -1
+ else
+ player.y = collision.rect.y + state.tile_size
+ player.dy = 0
+ end
+ end
+
+ def calc_left
+ return unless player.dx < 0
+ tiles_left = find_tiles { |t| t.rect.right <= player.prev_rect.left }
+ collision = find_colliding_tile tiles_left, (player.rect.merge x: player.next_rect.x)
+ return unless collision
+ player.x = collision.rect.right
+ player.dx = 0
+ end
+
+ def calc_right
+ return unless player.dx > 0
+ tiles_right = find_tiles { |t| t.rect.left >= player.prev_rect.right }
+ collision = find_colliding_tile tiles_right, (player.rect.merge x: player.next_rect.x)
+ return unless collision
+ player.x = collision.rect.left - player.rect.w
+ player.dx = 0
+ end
+
+ def calc_above
+ return unless player.dy > 0
+ tiles_above = find_tiles { |t| t.rect.y >= player.y }
+ collision = find_colliding_tile tiles_above, (player.rect.merge y: player.next_rect.y)
+ return unless collision
+ return if collision.neighbors.t == :none
+ player.dy = 0
+ player.y = collision.rect.bottom - player.rect.h
+ end
+
+ def calc_player_dx
+ player.y += player.dy
+ player.dy += state.gravity
+ player.dy += player.dy * state.drag ** 2 * -1
+ end
+
+ def calc_player_dy
+ player.dx = player.dx.clamp(-5, 5)
+ player.dx *= 0.9
+ player.x += player.dx
+ end
+
+ def reset_player
+ player.x = 100
+ player.y = 720
+ player.dy = 0
+ end
+
+ def recompute_tiles
+ tiles.each do |t|
+ t.w = state.tile_size
+ t.h = state.tile_size
+ t.neighbors = tile_neighbors t, tiles
+
+ t.rect = [t.x * state.tile_size,
+ t.y * state.tile_size,
+ state.tile_size,
+ state.tile_size].rect.to_hash
+
+ sprite_sub_path = t.neighbors.mask.map { |m| flip_bit m }.join("")
+
+ t.sprite = {
+ x: t.x * state.tile_size,
+ y: t.y * state.tile_size,
+ w: state.tile_size,
+ h: state.tile_size,
+ path: "sprites/tile/wall-#{sprite_sub_path}.png"
+ }
+ end
+ end
+
+ def flip_bit bit
+ return 0 if bit == 1
+ return 1
+ end
+
+ def player
+ state.player
+ end
+
+ def player_off_stage?
+ player.rect.top < grid.bottom ||
+ player.rect.right < grid.left ||
+ player.rect.left > grid.right
+ end
+
+ def current_player_rect
+ { x: player.x, y: player.y, w: player.size, h: player.size }
+ end
+
+ def tiles
+ state.tiles
+ end
+
+ def find_tile ordinal
+ tiles.find { |t| t.x == ordinal.x && t.y == ordinal.y }
+ end
+
+ def find_tiles &block
+ tiles.find_all(&block)
+ end
+
+ def find_colliding_tile tiles, target
+ tiles.find { |t| t.rect.intersect_rect? target }
+ end
+
+ def tile_neighbors tile, other_points
+ t = find_tile x: tile.x + 0, y: tile.y + 1
+ r = find_tile x: tile.x + 1, y: tile.y + 0
+ b = find_tile x: tile.x + 0, y: tile.y - 1
+ l = find_tile x: tile.x - 1, y: tile.y + 0
+
+ tile_t, tile_r, tile_b, tile_l = 0
+
+ tile_t = 1 if t
+ tile_r = 1 if r
+ tile_b = 1 if b
+ tile_l = 1 if l
+
+ state.new_entity :neighbors, mask: [tile_t, tile_r, tile_b, tile_l],
+ t: t ? :some : :none,
+ b: b ? :some : :none,
+ l: l ? :some : :none,
+ r: r ? :some : :none
+ end
+end
+
+def tick args
+ $game ||= Game.new
+ $game.args = args
+ $game.tick
+end
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/metadata/game_metadata.txt b/samples/04_physics_and_collisions/06_box_collision_3/metadata/game_metadata.txt
new file mode 100644
index 0000000..16cef1d
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/metadata/game_metadata.txt
@@ -0,0 +1,6 @@
+devid=amirrajan
+devtitle=Amir Rajan
+gameid=hello-world
+gametitle=Hello World
+version=1.0
+icon=metadata/icon.png
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/metadata/icon.png b/samples/04_physics_and_collisions/06_box_collision_3/metadata/icon.png
new file mode 100644
index 0000000..e20e8c2
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/metadata/icon.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/black.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/black.png
new file mode 100644
index 0000000..c98e23d
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/blue.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/blue.png
new file mode 100644
index 0000000..1726d2a
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/gray.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/gray.png
new file mode 100644
index 0000000..960f191
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/green.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/green.png
new file mode 100644
index 0000000..43cf7ee
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/indigo.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/indigo.png
new file mode 100644
index 0000000..598e240
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/orange.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/orange.png
new file mode 100644
index 0000000..5604a42
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/red.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/red.png
new file mode 100644
index 0000000..7f17ca6
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/violet.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/violet.png
new file mode 100644
index 0000000..681d210
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/white.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/yellow.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/yellow.png
new file mode 100644
index 0000000..94992eb
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/circle/yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/black.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/black.png
new file mode 100644
index 0000000..f50c872
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/blue.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/blue.png
new file mode 100644
index 0000000..1696bae
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/gray.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/gray.png
new file mode 100644
index 0000000..e8c4c5a
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/green.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/green.png
new file mode 100644
index 0000000..a700602
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/indigo.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/indigo.png
new file mode 100644
index 0000000..15f6f4f
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/orange.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/orange.png
new file mode 100644
index 0000000..1587173
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/red.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/red.png
new file mode 100644
index 0000000..d442f39
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/violet.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/violet.png
new file mode 100644
index 0000000..3be5731
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/white.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/white.png
new file mode 100644
index 0000000..c1ad970
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/yellow.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/yellow.png
new file mode 100644
index 0000000..63f5f34
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/hexagon/yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/black.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/black.png
new file mode 100644
index 0000000..fa9e463
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/blue.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/blue.png
new file mode 100644
index 0000000..a3d8524
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/gray.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/gray.png
new file mode 100644
index 0000000..85dcc1d
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/green.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/green.png
new file mode 100644
index 0000000..ec2773e
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/indigo.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/indigo.png
new file mode 100644
index 0000000..e6be50c
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/orange.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/orange.png
new file mode 100644
index 0000000..154d81c
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/red.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/red.png
new file mode 100644
index 0000000..3448c4d
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/violet.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/violet.png
new file mode 100644
index 0000000..f09bf21
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/white.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/white.png
new file mode 100644
index 0000000..a45793d
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/yellow.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/yellow.png
new file mode 100644
index 0000000..9be20c7
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/isometric/yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-0.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-0.png
new file mode 100644
index 0000000..fb179af
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-0.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-1.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-1.png
new file mode 100644
index 0000000..8cfe531
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-1.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-2.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-2.png
new file mode 100644
index 0000000..cb462e1
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-2.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-3.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-3.png
new file mode 100644
index 0000000..04c4977
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-3.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-4.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-4.png
new file mode 100644
index 0000000..b29fa3d
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-4.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-5.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-5.png
new file mode 100644
index 0000000..99f4e74
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/dragon-5.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-0.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-0.png
new file mode 100644
index 0000000..f48636f
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-0.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-1.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-1.png
new file mode 100644
index 0000000..b4018d9
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-1.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-2.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-2.png
new file mode 100644
index 0000000..3abaedd
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-2.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-3.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-3.png
new file mode 100644
index 0000000..fe94a5a
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-3.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-4.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-4.png
new file mode 100644
index 0000000..ed04237
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-4.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-5.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-5.png
new file mode 100644
index 0000000..2cd8f06
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-5.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-6.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-6.png
new file mode 100644
index 0000000..e55909c
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-6.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-sheet.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-sheet.png
new file mode 100644
index 0000000..8559a5c
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/explosion-sheet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/lowrez-ship-blue.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/lowrez-ship-blue.png
new file mode 100644
index 0000000..7a3d3aa
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/lowrez-ship-blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/lowrez-ship-red.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/lowrez-ship-red.png
new file mode 100644
index 0000000..dd1a1d4
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/lowrez-ship-red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/simple-mood-16x16.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/simple-mood-16x16.png
new file mode 100644
index 0000000..0eca11e
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/simple-mood-16x16.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/star.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/star.png
new file mode 100644
index 0000000..e0ee0f9
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/star.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/tiny-star.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/tiny-star.png
new file mode 100644
index 0000000..e04786a
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/misc/tiny-star.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/black.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/black.png
new file mode 100644
index 0000000..cea7bd7
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/blue.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/blue.png
new file mode 100644
index 0000000..b840849
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/gray.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/gray.png
new file mode 100644
index 0000000..2142b30
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/green.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/green.png
new file mode 100644
index 0000000..5ef7f75
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/indigo.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/indigo.png
new file mode 100644
index 0000000..2384108
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/orange.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/orange.png
new file mode 100644
index 0000000..bb1eee7
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/red.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/red.png
new file mode 100644
index 0000000..3ed5f13
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/violet.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/violet.png
new file mode 100644
index 0000000..333540c
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/white.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/white.png
new file mode 100644
index 0000000..378c565
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/yellow.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/yellow.png
new file mode 100644
index 0000000..0edeeec
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/square/yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0000.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0000.png
new file mode 100644
index 0000000..469795c
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0000.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0001.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0001.png
new file mode 100644
index 0000000..afcac7a
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0001.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0010.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0010.png
new file mode 100644
index 0000000..b791e98
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0010.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0011.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0011.png
new file mode 100644
index 0000000..9e7d664
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0011.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0100.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0100.png
new file mode 100644
index 0000000..e49aadb
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0100.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0101.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0101.png
new file mode 100644
index 0000000..b040a4a
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0101.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0110.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0110.png
new file mode 100644
index 0000000..2273582
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0110.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0111.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0111.png
new file mode 100644
index 0000000..ae2faca
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-0111.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1000.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1000.png
new file mode 100644
index 0000000..900990d
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1000.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1001.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1001.png
new file mode 100644
index 0000000..45aa962
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1001.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1010.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1010.png
new file mode 100644
index 0000000..9333835
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1010.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1011.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1011.png
new file mode 100644
index 0000000..439f135
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1011.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1100.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1100.png
new file mode 100644
index 0000000..67a2433
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1100.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1101.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1101.png
new file mode 100644
index 0000000..8e06769
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1101.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1110.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1110.png
new file mode 100644
index 0000000..d92e46c
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1110.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1111.png b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1111.png
new file mode 100644
index 0000000..e5ce32f
--- /dev/null
+++ b/samples/04_physics_and_collisions/06_box_collision_3/sprites/tile/wall-1111.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/07_jump_physics/app/main.rb b/samples/04_physics_and_collisions/07_jump_physics/app/main.rb
new file mode 100644
index 0000000..3fcb9e9
--- /dev/null
+++ b/samples/04_physics_and_collisions/07_jump_physics/app/main.rb
@@ -0,0 +1,196 @@
+=begin
+
+ Reminders:
+
+ - args.state.new_entity: Used when we want to create a new object, like a sprite or button.
+ For example, if we want to create a new button, we would declare it as a new entity and
+ then define its properties. (Remember, you can use state to define ANY property and it will
+ be retained across frames.)
+
+ - args.outputs.solids: An array. The values generate a solid.
+ The parameters for a solid are [X, Y, WIDTH, HEIGHT, RED, GREEN, BLUE]
+ For more information about solids, go to mygame/documentation/03-solids-and-borders.md.
+
+ - num1.greater(num2): Returns the greater value.
+
+ - Hashes: Collection of unique keys and their corresponding values. The value can be found
+ using their keys.
+
+ - ARRAY#inside_rect?: Returns true or false depending on if the point is inside the rect.
+
+=end
+
+# This sample app is a game that requires the user to jump from one platform to the next.
+# As the player successfully clears platforms, they become smaller and move faster.
+
+class VerticalPlatformer
+ attr_gtk
+
+ # declares vertical platformer as new entity
+ def s
+ state.vertical_platformer ||= state.new_entity(:vertical_platformer)
+ state.vertical_platformer
+ end
+
+ # creates a new platform using a hash
+ def new_platform hash
+ s.new_entity_strict(:platform, hash) # platform key
+ end
+
+ # calls methods needed for game to run properly
+ def tick
+ defaults
+ render
+ calc
+ input
+ end
+
+ # Sets default values
+ def defaults
+ s.platforms ||= [ # initializes platforms collection with two platforms using hashes
+ new_platform(x: 0, y: 0, w: 700, h: 32, dx: 1, speed: 0, rect: nil),
+ new_platform(x: 0, y: 300, w: 700, h: 32, dx: 1, speed: 0, rect: nil), # 300 pixels higher
+ ]
+
+ s.tick_count = args.state.tick_count
+ s.gravity = -0.3 # what goes up must come down because of gravity
+ s.player.platforms_cleared ||= 0 # counts how many platforms the player has successfully cleared
+ s.player.x ||= 0 # sets player values
+ s.player.y ||= 100
+ s.player.w ||= 64
+ s.player.h ||= 64
+ s.player.dy ||= 0 # change in position
+ s.player.dx ||= 0
+ s.player_jump_power = 15
+ s.player_jump_power_duration = 10
+ s.player_max_run_speed = 5
+ s.player_speed_slowdown_rate = 0.9
+ s.player_acceleration = 1
+ s.camera ||= { y: -100 } # shows view on screen (as the player moves upward, the camera does too)
+ end
+
+ # Outputs objects onto the screen
+ def render
+ outputs.solids << s.platforms.map do |p| # outputs platforms onto screen
+ [p.x + 300, p.y - s.camera[:y], p.w, p.h] # add 300 to place platform in horizontal center
+ # don't forget, position of platform is denoted by bottom left hand corner
+ end
+
+ # outputs player using hash
+ outputs.solids << {
+ x: s.player.x + 300, # player positioned on top of platform
+ y: s.player.y - s.camera[:y],
+ w: s.player.w,
+ h: s.player.h,
+ r: 100, # color saturation
+ g: 100,
+ b: 200
+ }
+ end
+
+ # Performs calculations
+ def calc
+ s.platforms.each do |p| # for each platform in the collection
+ p.rect = [p.x, p.y, p.w, p.h] # set the definition
+ end
+
+ # sets player point by adding half the player's width to the player's x
+ s.player.point = [s.player.x + s.player.w.half, s.player.y] # change + to - and see what happens!
+
+ # search the platforms collection to find if the player's point is inside the rect of a platform
+ collision = s.platforms.find { |p| s.player.point.inside_rect? p.rect }
+
+ # if collision occurred and player is moving down (or not moving vertically at all)
+ if collision && s.player.dy <= 0
+ s.player.y = collision.rect.y + collision.rect.h - 2 # player positioned on top of platform
+ s.player.dy = 0 if s.player.dy < 0 # player stops moving vertically
+ if !s.player.platform
+ s.player.dx = 0 # no horizontal movement
+ end
+ # changes horizontal position of player by multiplying collision change in x (dx) by speed and adding it to current x
+ s.player.x += collision.dx * collision.speed
+ s.player.platform = collision # player is on the platform that it collided with (or landed on)
+ if s.player.falling # if player is falling
+ s.player.dx = 0 # no horizontal movement
+ end
+ s.player.falling = false
+ s.player.jumped_at = nil
+ else
+ s.player.platform = nil # player is not on a platform
+ s.player.y += s.player.dy # velocity is the change in position
+ s.player.dy += s.gravity # acceleration is the change in velocity; what goes up must come down
+ end
+
+ s.platforms.each do |p| # for each platform in the collection
+ p.x += p.dx * p.speed # x is incremented by product of dx and speed (causes platform to move horizontally)
+ # changes platform's x so it moves left and right across the screen (between -300 and 300 pixels)
+ if p.x < -300 # if platform goes too far left
+ p.dx *= -1 # dx is scaled down
+ p.x = -300 # as far left as possible within scope
+ elsif p.x > (1000 - p.w) # if platform's x is greater than 300
+ p.dx *= -1
+ p.x = (1000 - p.w) # set to 300 (as far right as possible within scope)
+ end
+ end
+
+ delta = (s.player.y - s.camera[:y] - 100) # used to position camera view
+
+ if delta > -200
+ s.camera[:y] += delta * 0.01 # allows player to see view as they move upwards
+ s.player.x += s.player.dx # velocity is change in position; change in x increases by dx
+
+ # searches platform collection to find platforms located more than 300 pixels above the player
+ has_platforms = s.platforms.find { |p| p.y > (s.player.y + 300) }
+ if !has_platforms # if there are no platforms 300 pixels above the player
+ width = 700 - (700 * (0.1 * s.player.platforms_cleared)) # the next platform is smaller than previous
+ s.player.platforms_cleared += 1 # player successfully cleared another platform
+ last_platform = s.platforms[-1] # platform just cleared becomes last platform
+ # another platform is created 300 pixels above the last platform, and this
+ # new platform has a smaller width and moves faster than all previous platforms
+ s.platforms << new_platform(x: (700 - width) * rand, # random x position
+ y: last_platform.y + 300,
+ w: width,
+ h: 32,
+ dx: 1.randomize(:sign), # random change in x
+ speed: 2 * s.player.platforms_cleared,
+ rect: nil)
+ end
+ else
+ s.as_hash.clear # otherwise clear the hash (no new platform is necessary)
+ end
+ end
+
+ # Takes input from the user to move the player
+ def input
+ if inputs.keyboard.space # if the space bar is pressed
+ s.player.jumped_at ||= s.tick_count # set to current frame
+
+ # if the time that has passed since the jump is less than the duration of a jump (10 frames)
+ # and the player is not falling
+ if s.player.jumped_at.elapsed_time < s.player_jump_power_duration && !s.player.falling
+ s.player.dy = s.player_jump_power # player jumps up
+ end
+ end
+
+ if inputs.keyboard.key_up.space # if space bar is in "up" state
+ s.player.falling = true # player is falling
+ end
+
+ if inputs.keyboard.left # if left key is pressed
+ s.player.dx -= s.player_acceleration # player's position changes, decremented by acceleration
+ s.player.dx = s.player.dx.greater(-s.player_max_run_speed) # dx is either current dx or -5, whichever is greater
+ elsif inputs.keyboard.right # if right key is pressed
+ s.player.dx += s.player_acceleration # player's position changes, incremented by acceleration
+ s.player.dx = s.player.dx.lesser(s.player_max_run_speed) # dx is either current dx or 5, whichever is lesser
+ else
+ s.player.dx *= s.player_speed_slowdown_rate # scales dx down
+ end
+ end
+end
+
+$game = VerticalPlatformer.new
+
+def tick args
+ $game.args = args
+ $game.tick
+end
diff --git a/samples/04_physics_and_collisions/07_jump_physics/replay.txt b/samples/04_physics_and_collisions/07_jump_physics/replay.txt
new file mode 100644
index 0000000..966dbcd
--- /dev/null
+++ b/samples/04_physics_and_collisions/07_jump_physics/replay.txt
@@ -0,0 +1,124 @@
+replay_version 2.0
+stopped_at 1260
+seed 100
+recorded_at Sun Sep 29 22:23:22 2019
+[:key_down_raw, 1073741903, 0, 2, 1, 45]
+[:key_down_raw, 1073741903, 0, 2, 2, 70]
+[:key_down_raw, 1073741903, 0, 2, 3, 72]
+[:key_down_raw, 1073741903, 0, 2, 4, 74]
+[:key_down_raw, 1073741903, 0, 2, 5, 76]
+[:key_down_raw, 1073741903, 0, 2, 6, 78]
+[:key_down_raw, 1073741903, 0, 2, 7, 80]
+[:key_down_raw, 1073741903, 0, 2, 8, 82]
+[:key_down_raw, 1073741903, 0, 2, 9, 84]
+[:key_down_raw, 1073741903, 0, 2, 10, 86]
+[:key_down_raw, 1073741903, 0, 2, 11, 88]
+[:key_up_raw, 1073741903, 0, 2, 12, 89]
+[:key_down_raw, 1073741904, 0, 2, 13, 92]
+[:key_down_raw, 1073741903, 0, 2, 14, 112]
+[:key_up_raw, 1073741904, 0, 2, 15, 115]
+[:key_down_raw, 1073741903, 0, 2, 16, 137]
+[:key_down_raw, 1073741903, 0, 2, 17, 139]
+[:key_down_raw, 8, 0, 2, 18, 141]
+[:key_up_raw, 1073741903, 0, 2, 19, 146]
+[:key_down_raw, 1073741903, 0, 2, 20, 147]
+[:key_up_raw, 1073741903, 0, 2, 21, 147]
+[:key_down_raw, 1073741904, 0, 2, 22, 154]
+[:key_up_raw, 8, 0, 2, 23, 172]
+[:key_down_raw, 1073741903, 0, 2, 24, 175]
+[:key_up_raw, 1073741904, 0, 2, 25, 175]
+[:key_down_raw, 32, 0, 2, 26, 194]
+[:key_down_raw, 1073741904, 0, 2, 27, 201]
+[:key_up_raw, 1073741903, 0, 2, 28, 202]
+[:key_down_raw, 1073741903, 0, 2, 29, 222]
+[:key_up_raw, 1073741904, 0, 2, 30, 226]
+[:key_up_raw, 32, 0, 2, 31, 233]
+[:key_down_raw, 1073741903, 0, 2, 32, 247]
+[:key_down_raw, 1073741903, 0, 2, 33, 249]
+[:key_down_raw, 1073741903, 0, 2, 34, 251]
+[:key_down_raw, 1073741903, 0, 2, 35, 253]
+[:key_down_raw, 1073741903, 0, 2, 36, 255]
+[:key_down_raw, 1073741903, 0, 2, 37, 257]
+[:key_down_raw, 1073741903, 0, 2, 38, 259]
+[:key_down_raw, 1073741903, 0, 2, 39, 261]
+[:key_down_raw, 1073741903, 0, 2, 40, 263]
+[:key_down_raw, 1073741903, 0, 2, 41, 265]
+[:key_down_raw, 1073741903, 0, 2, 42, 267]
+[:key_down_raw, 1073741903, 0, 2, 43, 269]
+[:key_down_raw, 1073741903, 0, 2, 44, 271]
+[:key_down_raw, 1073741903, 0, 2, 45, 273]
+[:key_down_raw, 1073741903, 0, 2, 46, 275]
+[:key_up_raw, 1073741903, 0, 2, 47, 276]
+[:key_down_raw, 1073741904, 0, 2, 48, 280]
+[:key_up_raw, 1073741904, 0, 2, 49, 297]
+[:key_down_raw, 1073741903, 0, 2, 50, 300]
+[:key_down_raw, 32, 0, 2, 51, 303]
+[:key_up_raw, 32, 0, 2, 52, 308]
+[:key_up_raw, 1073741903, 0, 2, 53, 334]
+[:key_down_raw, 1073741904, 0, 2, 54, 336]
+[:key_down_raw, 1073741903, 0, 2, 55, 355]
+[:key_up_raw, 1073741904, 0, 2, 56, 355]
+[:key_up_raw, 1073741903, 0, 2, 57, 373]
+[:key_down_raw, 1073741904, 0, 2, 58, 380]
+[:key_down_raw, 32, 0, 2, 59, 398]
+[:key_up_raw, 32, 0, 2, 60, 404]
+[:key_up_raw, 1073741904, 0, 2, 61, 490]
+[:key_down_raw, 32, 0, 2, 62, 510]
+[:key_up_raw, 32, 0, 2, 63, 516]
+[:key_down_raw, 1073741903, 0, 2, 64, 517]
+[:key_up_raw, 1073741903, 0, 2, 65, 537]
+[:key_down_raw, 1073741904, 0, 2, 66, 538]
+[:key_up_raw, 1073741904, 0, 2, 67, 553]
+[:key_down_raw, 32, 0, 2, 68, 653]
+[:key_up_raw, 32, 0, 2, 69, 659]
+[:key_down_raw, 1073741903, 0, 2, 70, 661]
+[:key_down_raw, 1073741903, 0, 2, 71, 686]
+[:key_up_raw, 1073741903, 0, 2, 72, 687]
+[:key_down_raw, 1073741904, 0, 2, 73, 690]
+[:key_down_raw, 1073741904, 0, 2, 74, 715]
+[:key_down_raw, 1073741904, 0, 2, 75, 717]
+[:key_down_raw, 1073741904, 0, 2, 76, 719]
+[:key_down_raw, 1073741904, 0, 2, 77, 721]
+[:key_down_raw, 1073741904, 0, 2, 78, 723]
+[:key_down_raw, 1073741904, 0, 2, 79, 725]
+[:key_down_raw, 1073741904, 0, 2, 80, 727]
+[:key_down_raw, 1073741904, 0, 2, 81, 729]
+[:key_up_raw, 1073741904, 0, 2, 82, 731]
+[:key_down_raw, 1073741903, 0, 2, 83, 740]
+[:key_down_raw, 32, 0, 2, 84, 749]
+[:key_up_raw, 32, 0, 2, 85, 756]
+[:key_up_raw, 1073741903, 0, 2, 86, 789]
+[:key_down_raw, 1073741904, 0, 2, 87, 792]
+[:key_down_raw, 1073741904, 0, 2, 88, 817]
+[:key_up_raw, 1073741904, 0, 2, 89, 817]
+[:key_down_raw, 32, 0, 2, 90, 844]
+[:key_down_raw, 1073741903, 0, 2, 91, 846]
+[:key_up_raw, 32, 0, 2, 92, 852]
+[:key_down_raw, 1073741903, 0, 2, 93, 871]
+[:key_down_raw, 1073741903, 0, 2, 94, 873]
+[:key_down_raw, 1073741903, 0, 2, 95, 875]
+[:key_down_raw, 1073741903, 0, 2, 96, 877]
+[:key_down_raw, 1073741903, 0, 2, 97, 879]
+[:key_down_raw, 1073741903, 0, 2, 98, 881]
+[:key_down_raw, 1073741903, 0, 2, 99, 883]
+[:key_down_raw, 1073741903, 0, 2, 100, 885]
+[:key_down_raw, 1073741903, 0, 2, 101, 887]
+[:key_up_raw, 1073741903, 0, 2, 102, 887]
+[:key_down_raw, 1073741904, 0, 2, 103, 897]
+[:key_up_raw, 1073741904, 0, 2, 104, 901]
+[:key_down_raw, 32, 0, 2, 105, 948]
+[:key_up_raw, 32, 0, 2, 106, 953]
+[:key_down_raw, 1073741904, 0, 2, 107, 977]
+[:key_up_raw, 1073741904, 0, 2, 108, 991]
+[:key_down_raw, 32, 0, 2, 109, 1056]
+[:key_up_raw, 32, 0, 2, 110, 1060]
+[:key_down_raw, 1073741904, 0, 2, 111, 1065]
+[:key_down_raw, 1073741904, 0, 2, 112, 1090]
+[:key_down_raw, 1073741904, 0, 2, 113, 1092]
+[:key_down_raw, 1073741904, 0, 2, 114, 1094]
+[:key_down_raw, 1073741904, 0, 2, 115, 1096]
+[:key_up_raw, 1073741904, 0, 2, 116, 1098]
+[:key_down_raw, 1073741903, 0, 2, 117, 1108]
+[:key_up_raw, 1073741903, 0, 2, 118, 1127]
+[:key_down_raw, 1073742051, 1024, 2, 119, 1258]
+[:key_down_raw, 113, 1024, 2, 120, 1259]
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/app/ball.rb b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/ball.rb
new file mode 100644
index 0000000..80b61e3
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/ball.rb
@@ -0,0 +1,87 @@
+GRAVITY = -0.08
+
+class Ball
+ attr_accessor :velocity, :center, :radius, :collision_enabled
+
+ def initialize args
+ #Start the ball in the top center
+ #@x = args.grid.w / 2
+ #@y = args.grid.h - 20
+
+ @velocity = {x: 0, y: 0}
+ #@width = 20
+ #@height = @width
+ @radius = 20.0 / 2.0
+ @center = {x: (args.grid.w / 2), y: (args.grid.h)}
+
+ #@left_wall = (args.state.board_width + args.grid.w / 8)
+ #@right_wall = @left_wall + args.state.board_width
+ @left_wall = 0
+ @right_wall = $args.grid.right
+
+ @max_velocity = 7
+ @collision_enabled = true
+ end
+
+ #Move the ball according to its velocity
+ def update args
+ @center.x += @velocity.x
+ @center.y += @velocity.y
+ @velocity.y += GRAVITY
+
+ alpha = 0.2
+ if @center.y-@radius <= 0
+ @velocity.y = (@velocity.y.abs*0.7).abs
+ @velocity.x = (@velocity.x.abs*0.9).abs * ((@velocity.x < 0) ? -1 : 1)
+
+ if @velocity.y.abs() < alpha
+ @velocity.y=0
+ end
+ if @velocity.x.abs() < alpha
+ @velocity.x=0
+ end
+ end
+
+ if @center.x > args.grid.right+@radius*2
+ @center.x = 0-@radius
+ elsif @center.x< 0-@radius*2
+ @center.x = args.grid.right + @radius
+ end
+ end
+
+ def wallBounds args
+ #if @x < @left_wall || @x + @width > @right_wall
+ #@velocity.x *= -1.1
+ #if @velocity.x > @max_velocity
+ #@velocity.x = @max_velocity
+ #elsif @velocity.x < @max_velocity * -1
+ #@velocity.x = @max_velocity * -1
+ #end
+ #end
+ #if @y < 0 || @y + @height > args.grid.h
+ #@velocity.y *= -1.1
+ #if @velocity.y > @max_velocity
+ #@velocity.y = @max_velocity
+ #elsif @velocity.y < @max_velocity * -1
+ #@velocity.y = @max_velocity * -1
+ #end
+ #end
+ end
+
+ #render the ball to the screen
+ def draw args
+ #args.outputs.solids << [@x, @y, @width, @height, 255, 255, 0];
+ args.outputs.sprites << [
+ @center.x-@radius,
+ @center.y-@radius,
+ @radius*2,
+ @radius*2,
+ "sprites/circle-white.png",
+ 0,
+ 255,
+ 255, #r
+ 0, #g
+ 255 #b
+ ]
+ end
+ end
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/app/block.rb b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/block.rb
new file mode 100644
index 0000000..9254ee4
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/block.rb
@@ -0,0 +1,159 @@
+DEGREES_TO_RADIANS = Math::PI / 180
+
+class Block
+ def initialize(x, y, block_size, rotation)
+ @x = x
+ @y = y
+ @block_size = block_size
+ @rotation = rotation
+
+ #The repel velocity?
+ @velocity = {x: 2, y: 0}
+
+ horizontal_offset = (3 * block_size) * Math.cos(rotation * DEGREES_TO_RADIANS)
+ vertical_offset = block_size * Math.sin(rotation * DEGREES_TO_RADIANS)
+
+ if rotation >= 0
+ theta = 90 - rotation
+ #The line doesn't visually line up exactly with the edge of the sprite, so artificially move it a bit
+ modifier = 5
+ x_offset = modifier * Math.cos(theta * DEGREES_TO_RADIANS)
+ y_offset = modifier * Math.sin(theta * DEGREES_TO_RADIANS)
+ @x1 = @x - x_offset
+ @y1 = @y + y_offset
+ @x2 = @x1 + horizontal_offset
+ @y2 = @y1 + (vertical_offset * 3)
+
+ @imaginary_line = [ @x1, @y1, @x2, @y2 ]
+ else
+ theta = 90 + rotation
+ x_offset = @block_size * Math.cos(theta * DEGREES_TO_RADIANS)
+ y_offset = @block_size * Math.sin(theta * DEGREES_TO_RADIANS)
+ @x1 = @x + x_offset
+ @y1 = @y + y_offset + 19
+ @x2 = @x1 + horizontal_offset
+ @y2 = @y1 + (vertical_offset * 3)
+
+ @imaginary_line = [ @x1, @y1, @x2, @y2 ]
+ end
+
+ end
+
+ def draw args
+ args.outputs.sprites << [
+ @x,
+ @y,
+ @block_size*3,
+ @block_size,
+ "sprites/square-green.png",
+ @rotation
+ ]
+
+ args.outputs.lines << @imaginary_line
+ args.outputs.solids << @debug_shape
+ end
+
+ def multiply_matricies
+ end
+
+ def calc args
+ if collision? args
+ collide args
+ end
+ end
+
+ #Determine if the ball and block are touching
+ def collision? args
+ #The minimum area enclosed by the center of the ball and the 2 corners of the block
+ #If the area ever drops below this value, we know there is a collision
+ min_area = ((@block_size * 3) * args.state.ball.radius) / 2
+
+ #https://www.mathopenref.com/coordtrianglearea.html
+ ax = @x1
+ ay = @y1
+ bx = @x2
+ by = @y2
+ cx = args.state.ball.center.x
+ cy = args.state.ball.center.y
+
+ current_area = (ax*(by-cy)+bx*(cy-ay)+cx*(ay-by))/2
+
+ collision = false
+ if @rotation >= 0
+ if (current_area < min_area &&
+ current_area > 0 &&
+ args.state.ball.center.y > @y1 &&
+ args.state.ball.center.x < @x2)
+
+ collision = true
+ end
+ else
+ if (current_area < min_area &&
+ current_area > 0 &&
+ args.state.ball.center.y > @y2 &&
+ args.state.ball.center.x > @x1)
+
+ collision = true
+ end
+ end
+
+ return collision
+ end
+
+ def collide args
+ #Slope of the block
+ slope = (@y2 - @y1) / (@x2 - @x1)
+
+ #Create a unit vector and tilt it (@rotation) number of degrees
+ x = -Math.cos(@rotation * DEGREES_TO_RADIANS)
+ y = Math.sin(@rotation * DEGREES_TO_RADIANS)
+
+ #Find the vector that is perpendicular to the slope
+ perpVect = { x: x, y: y }
+ mag = (perpVect.x**2 + perpVect.y**2)**0.5 # find the magniude of the perpVect
+ perpVect = {x: perpVect.x/(mag), y: perpVect.y/(mag)} # divide the perpVect by the magniude to make it a unit vector
+
+ previousPosition = { # calculate an ESTIMATE of the previousPosition of the ball
+ x:args.state.ball.center.x-args.state.ball.velocity.x,
+ y:args.state.ball.center.y-args.state.ball.velocity.y
+ }
+
+ velocityMag = (args.state.ball.velocity.x**2 + args.state.ball.velocity.y**2)**0.5 # the current velocity magnitude of the ball
+ theta_ball = Math.atan2(args.state.ball.velocity.y, args.state.ball.velocity.x) #the angle of the ball's velocity
+ theta_repel = (180 * DEGREES_TO_RADIANS) - theta_ball + (@rotation * DEGREES_TO_RADIANS)
+
+ fbx = velocityMag * Math.cos(theta_ball) #the x component of the ball's velocity
+ fby = velocityMag * Math.sin(theta_ball) #the y component of the ball's velocity
+
+ frx = velocityMag * Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx
+ fry = velocityMag * Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby
+
+ args.state.display_value = velocityMag
+ fsumx = fbx+frx #sum of x forces
+ fsumy = fby+fry #sum of y forces
+ fr = velocityMag #fr is the resulting magnitude
+ thetaNew = Math.atan2(fsumy, fsumx) #thetaNew is the resulting angle
+
+ xnew = fr*Math.cos(thetaNew) #resulting x velocity
+ ynew = fr*Math.sin(thetaNew) #resulting y velocity
+
+ dampener = 0.3
+ ynew *= dampener * 0.5
+
+ #If the bounce is very low, that means the ball is rolling and we don't want to dampenen the X velocity
+ if ynew > -0.1
+ xnew *= dampener
+ end
+
+ #Add the sine component of gravity back in (X component)
+ gravity_x = 4 * Math.sin(@rotation * DEGREES_TO_RADIANS)
+ xnew += gravity_x
+
+ args.state.ball.velocity.x = -xnew
+ args.state.ball.velocity.y = -ynew
+
+ #Set the position of the ball to the previous position so it doesn't warp throught the block
+ args.state.ball.center.x = previousPosition.x
+ args.state.ball.center.y = previousPosition.y
+ end
+end
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/app/cannon.rb b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/cannon.rb
new file mode 100644
index 0000000..4df68fe
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/cannon.rb
@@ -0,0 +1,20 @@
+class Cannon
+ def initialize args
+ @pointA = {x: args.grid.right/2,y: args.grid.top}
+ @pointB = {x: args.inputs.mouse.x, y: args.inputs.mouse.y}
+ end
+ def update args
+ activeBall = args.state.ball
+ @pointB = {x: args.inputs.mouse.x, y: args.inputs.mouse.y}
+
+ if args.inputs.mouse.click
+ alpha = 0.01
+ activeBall.velocity.y = (@pointB.y - @pointA.y) * alpha
+ activeBall.velocity.x = (@pointB.x - @pointA.x) * alpha
+ activeBall.center = {x: (args.grid.w / 2), y: (args.grid.h)}
+ end
+ end
+ def render args
+ args.outputs.lines << [@pointA.x, @pointA.y, @pointB.x, @pointB.y]
+ end
+end
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/app/main.rb b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/main.rb
new file mode 100644
index 0000000..f5883ad
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/main.rb
@@ -0,0 +1,117 @@
+INFINITY= 10**10
+
+require 'app/vector2d.rb'
+require 'app/peg.rb'
+require 'app/block.rb'
+require 'app/ball.rb'
+require 'app/cannon.rb'
+
+
+#Method to init default values
+def defaults args
+ args.state.pegs ||= []
+ args.state.blocks ||= []
+ args.state.cannon ||= Cannon.new args
+ args.state.ball ||= Ball.new args
+ args.state.horizontal_offset ||= 0
+ init_pegs args
+ init_blocks args
+
+ args.state.display_value ||= "test"
+end
+
+begin :default_methods
+ def init_pegs args
+ num_horizontal_pegs = 14
+ num_rows = 5
+
+ return unless args.state.pegs.count < num_rows * num_horizontal_pegs
+
+ block_size = 32
+ block_spacing = 50
+ total_width = num_horizontal_pegs * (block_size + block_spacing)
+ starting_offset = (args.grid.w - total_width) / 2 + block_size
+
+ for i in (0...num_rows)
+ for j in (0...num_horizontal_pegs)
+ row_offset = 0
+ if i % 2 == 0
+ row_offset = 20
+ else
+ row_offset = -20
+ end
+ args.state.pegs.append(Peg.new(j * (block_size+block_spacing) + starting_offset + row_offset, (args.grid.h - block_size * 2) - (i * block_size * 2)-90, block_size))
+ end
+ end
+
+ end
+
+ def init_blocks args
+ return unless args.state.blocks.count < 10
+
+ #Sprites are rotated in degrees, but the Ruby math functions work on radians
+ radians_to_degrees = Math::PI / 180
+
+ block_size = 25
+ #Rotation angle (in degrees) of the blocks
+ rotation = 30
+ vertical_offset = block_size * Math.sin(rotation * radians_to_degrees)
+ horizontal_offset = (3 * block_size) * Math.cos(rotation * radians_to_degrees)
+ center = args.grid.w / 2
+
+ for i in (0...5)
+ #Create a ramp of blocks. Not going to be perfect because of the float to integer conversion and anisotropic to isotropic coversion
+ args.state.blocks.append(Block.new((center + 100 + (i * horizontal_offset)).to_i, 100 + (vertical_offset * i) + (i * block_size), block_size, rotation))
+ args.state.blocks.append(Block.new((center - 100 - (i * horizontal_offset)).to_i, 100 + (vertical_offset * i) + (i * block_size), block_size, -rotation))
+ end
+ end
+end
+
+#Render loop
+def render args
+ args.outputs.borders << args.state.game_area
+ render_pegs args
+ render_blocks args
+ args.state.cannon.render args
+ args.state.ball.draw args
+end
+
+begin :render_methods
+ #Draw the pegs in a grid pattern
+ def render_pegs args
+ args.state.pegs.each do |peg|
+ peg.draw args
+ end
+ end
+
+ def render_blocks args
+ args.state.blocks.each do |block|
+ block.draw args
+ end
+ end
+
+end
+
+#Calls all methods necessary for performing calculations
+def calc args
+ args.state.pegs.each do |peg|
+ peg.calc args
+ end
+
+ args.state.blocks.each do |block|
+ block.calc args
+ end
+
+ args.state.ball.update args
+ args.state.cannon.update args
+end
+
+begin :calc_methods
+
+end
+
+def tick args
+ defaults args
+ render args
+ calc args
+end
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/app/peg.rb b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/peg.rb
new file mode 100644
index 0000000..52045de
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/peg.rb
@@ -0,0 +1,182 @@
+class Peg
+ def initialize(x, y, block_size)
+ @x = x # x cordinate of the LEFT side of the peg
+ @y = y # y cordinate of the RIGHT side of the peg
+ @block_size = block_size # diameter of the peg
+
+ @radius = @block_size/2.0 # radius of the peg
+ @center = { # cordinatees of the CENTER of the peg
+ x: @x+@block_size/2.0,
+ y: @y+@block_size/2.0
+ }
+
+ @r = 255 # color of the peg
+ @g = 0
+ @b = 0
+
+ @velocity = {x: 2, y: 0}
+ end
+
+ def draw args
+ args.outputs.sprites << [ # draw the peg according to the @x, @y, @radius, and the RGB
+ @x,
+ @y,
+ @radius*2.0,
+ @radius*2.0,
+ "sprites/circle-white.png",
+ 0,
+ 255,
+ @r, #r
+ @g, #g
+ @b #b
+ ]
+ end
+
+
+ def calc args
+ if collisionWithBounce? args # if the is a collision with the bouncing ball
+ collide args
+ @r = 0
+ @b = 0
+ @g = 255
+ else
+ end
+ end
+
+
+ # do two circles (the ball and this peg) intersect
+ def collisionWithBounce? args
+ squareDistance = ( # the squared distance between the ball's center and this peg's center
+ (args.state.ball.center.x - @center.x) ** 2.0 +
+ (args.state.ball.center.y - @center.y) ** 2.0
+ )
+ radiusSum = ( # the sum of the radius squared of the this peg and the ball
+ (args.state.ball.radius + @radius) ** 2.0
+ )
+ # if the squareDistance is less or equal to radiusSum, then there is a radial intersection between the ball and this peg
+ return (squareDistance <= radiusSum)
+ end
+
+ # ! The following links explain the getRepelMagnitude function !
+ # https://raw.githubusercontent.com/DragonRuby/dragonruby-game-toolkit-physics/master/docs/docImages/LinearCollider_4.png
+ # https://raw.githubusercontent.com/DragonRuby/dragonruby-game-toolkit-physics/master/docs/docImages/LinearCollider_5.png
+ # https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/LinearCollider.md
+ def getRepelMagnitude (args, fbx, fby, vrx, vry, ballMag)
+ a = fbx ; b = vrx ; c = fby
+ d = vry ; e = ballMag
+ if b**2 + d**2 == 0
+ #unexpected
+ end
+
+ x1 = (-a*b+-c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 + d**2 - a**2 * d**2)**0.5)/(b**2 + d**2)
+ x2 = -((a*b + c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 * d**2 - a**2 * d**2)**0.5)/(b**2 + d**2))
+
+ err = 0.00001
+ o = ((fbx + x1*vrx)**2 + (fby + x1*vry)**2 ) ** 0.5
+ p = ((fbx + x2*vrx)**2 + (fby + x2*vry)**2 ) ** 0.5
+ r = 0
+
+ if (ballMag >= o-err and ballMag <= o+err)
+ r = x1
+ elsif (ballMag >= p-err and ballMag <= p+err)
+ r = x2
+ else
+ #unexpected
+ end
+
+ if (args.state.ball.center.x > @center.x)
+ return x2*-1
+ end
+
+ return x2
+
+ #return r
+ end
+
+ #this sets the new velocity of the ball once it has collided with this peg
+ def collide args
+ normalOfRCCollision = [ #this is the normal of the collision in COMPONENT FORM
+ {x: @center.x, y: @center.y}, #see https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.mathscard.co.uk%2Fonline%2Fcircle-coordinate-geometry%2F&psig=AOvVaw2GcD-e2-nJR_IUKpw3hO98&ust=1605731315521000&source=images&cd=vfe&ved=0CAIQjRxqFwoTCMjBo7e1iu0CFQAAAAAdAAAAABAD
+ {x: args.state.ball.center.x, y: args.state.ball.center.y},
+ ]
+
+ normalSlope = ( #normalSlope is the slope of normalOfRCCollision
+ (normalOfRCCollision[1].y - normalOfRCCollision[0].y) /
+ (normalOfRCCollision[1].x - normalOfRCCollision[0].x)
+ )
+ slope = normalSlope**-1.0 * -1 # slope is the slope of the tangent
+ # args.state.display_value = slope
+ pointA = { # pointA and pointB are using the var slope to tangent in COMPONENT FORM
+ x: args.state.ball.center.x-1,
+ y: -(slope-args.state.ball.center.y)
+ }
+ pointB = {
+ x: args.state.ball.center.x+1,
+ y: slope+args.state.ball.center.y
+ }
+
+ perpVect = {x: pointB.x - pointA.x, y:pointB.y - pointA.y} # perpVect is to be VECTOR of the perpendicular tangent
+ mag = (perpVect.x**2 + perpVect.y**2)**0.5 # find the magniude of the perpVect
+ perpVect = {x: perpVect.x/(mag), y: perpVect.y/(mag)} # divide the perpVect by the magniude to make it a unit vector
+ perpVect = {x: -perpVect.y, y: perpVect.x} # swap the x and y and multiply by -1 to make the vector perpendicular
+ args.state.display_value = perpVect
+ if perpVect.y > 0 #ensure perpVect points upward
+ perpVect = {x: perpVect.x*-1, y: perpVect.y*-1}
+ end
+
+ previousPosition = { # calculate an ESTIMATE of the previousPosition of the ball
+ x:args.state.ball.center.x-args.state.ball.velocity.x,
+ y:args.state.ball.center.y-args.state.ball.velocity.y
+ }
+
+ yInterc = pointA.y + -slope*pointA.x
+ if slope == INFINITY # the perpVect presently either points in the correct dirrection or it is 180 degrees off we need to correct this
+ if previousPosition.x < pointA.x
+ perpVect = {x: perpVect.x*-1, y: perpVect.y*-1}
+ yInterc = -INFINITY
+ end
+ elsif previousPosition.y < slope*previousPosition.x + yInterc # check if ball is bellow or above the collider to determine if perpVect is - or +
+ perpVect = {x: perpVect.x*-1, y: perpVect.y*-1}
+ end
+
+ velocityMag = # the current velocity magnitude of the ball
+ (args.state.ball.velocity.x**2 + args.state.ball.velocity.y**2)**0.5
+ theta_ball=
+ Math.atan2(args.state.ball.velocity.y,args.state.ball.velocity.x) #the angle of the ball's velocity
+ theta_repel=
+ Math.atan2(args.state.ball.center.y,args.state.ball.center.x) #the angle of the repelling force(perpVect)
+
+ fbx = velocityMag * Math.cos(theta_ball) #the x component of the ball's velocity
+ fby = velocityMag * Math.sin(theta_ball) #the y component of the ball's velocity
+ repelMag = getRepelMagnitude( # the magniude of the collision vector
+ args,
+ fbx,
+ fby,
+ perpVect.x,
+ perpVect.y,
+ (args.state.ball.velocity.x**2 + args.state.ball.velocity.y**2)**0.5
+ )
+ frx = repelMag* Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx
+ fry = repelMag* Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby
+
+ fsumx = fbx+frx # sum of x forces
+ fsumy = fby+fry # sum of y forces
+ fr = velocityMag # fr is the resulting magnitude
+ thetaNew = Math.atan2(fsumy, fsumx) # thetaNew is the resulting angle
+ xnew = fr*Math.cos(thetaNew) # resulting x velocity
+ ynew = fr*Math.sin(thetaNew) # resulting y velocity
+ if (args.state.ball.center.x >= @center.x) # this is necessary for the ball colliding on the right side of the peg
+ xnew=xnew.abs
+ end
+
+ args.state.ball.velocity.x = xnew # set the x-velocity to the new velocity
+ if args.state.ball.center.y > @center.y # if the ball is above the middle of the peg we need to temporarily ignore some of the gravity
+ args.state.ball.velocity.y = ynew + GRAVITY * 0.01
+ else
+ args.state.ball.velocity.y = ynew - GRAVITY * 0.01 # if the ball is bellow the middle of the peg we need to temporarily increase the power of the gravity
+ end
+
+ args.state.ball.center.x+= args.state.ball.velocity.x # update the position of the ball so it never looks like the ball is intersecting the circle
+ args.state.ball.center.y+= args.state.ball.velocity.y
+ end
+end
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/app/vector2d.rb b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/vector2d.rb
new file mode 100644
index 0000000..9cb1954
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/app/vector2d.rb
@@ -0,0 +1,49 @@
+class Vector2d
+ attr_accessor :x, :y
+
+ def initialize x=0, y=0
+ @x=x
+ @y=y
+ end
+
+ #returns a vector multiplied by scalar x
+ #x [float] scalar
+ def mult x
+ r = Vector2d.new(0,0)
+ r.x=@x*x
+ r.y=@y*x
+ r
+ end
+
+ # vect [Vector2d] vector to copy
+ def copy vect
+ Vector2d.new(@x, @y)
+ end
+
+ #returns a new vector equivalent to this+vect
+ #vect [Vector2d] vector to add to self
+ def add vect
+ Vector2d.new(@x+vect.x,@y+vect.y)
+ end
+
+ #returns a new vector equivalent to this-vect
+ #vect [Vector2d] vector to subtract to self
+ def sub vect
+ Vector2d.new(@x-vect.c, @y-vect.y)
+ end
+
+ #return the magnitude of the vector
+ def mag
+ ((@x**2)+(@y**2))**0.5
+ end
+
+ #returns a new normalize version of the vector
+ def normalize
+ Vector2d.new(@x/mag, @y/mag)
+ end
+
+ #TODO delet?
+ def distABS vect
+ (((vect.x-@x)**2+(vect.y-@y)**2)**0.5).abs()
+ end
+ end \ No newline at end of file
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/LinearCollider.md b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/LinearCollider.md
new file mode 100644
index 0000000..41227a2
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/LinearCollider.md
@@ -0,0 +1,13 @@
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_1.png?raw=true" width="300" height="271">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_2.png?raw=true" width="300" height="271">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_3.png?raw=true" width="300" height="271">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_4.png?raw=true" width="634" height="72">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_5.png?raw=true" width="1226" height="92">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_6.png?raw=true" width="300" height="271">
+<br/>
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_1.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_1.png
new file mode 100644
index 0000000..75c8271
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_1.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_2.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_2.png
new file mode 100644
index 0000000..231a362
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_2.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_3.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_3.png
new file mode 100644
index 0000000..5e552d3
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_3.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_4.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_4.png
new file mode 100644
index 0000000..c1284a3
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_4.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_5.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_5.png
new file mode 100644
index 0000000..035282c
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_5.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_6.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_6.png
new file mode 100644
index 0000000..b1dadcb
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/docs/docImages/LinearCollider_6.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/ball.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/ball.png
new file mode 100644
index 0000000..aec6fa4
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/ball.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/border-black.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/border-black.png
new file mode 100644
index 0000000..c9d0bad
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/border-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-black.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-black.png
new file mode 100644
index 0000000..c98e23d
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-blue.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-blue.png
new file mode 100644
index 0000000..1726d2a
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-gray.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-gray.png
new file mode 100644
index 0000000..960f191
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-green.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-green.png
new file mode 100644
index 0000000..43cf7ee
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-indigo.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-indigo.png
new file mode 100644
index 0000000..598e240
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-orange.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-orange.png
new file mode 100644
index 0000000..5604a42
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-red.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-red.png
new file mode 100644
index 0000000..7f17ca6
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-violet.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-violet.png
new file mode 100644
index 0000000..681d210
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-white.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-yellow.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-yellow.png
new file mode 100644
index 0000000..94992eb
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/circle-yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-0.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-0.png
new file mode 100644
index 0000000..fb179af
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-0.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-1.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-1.png
new file mode 100644
index 0000000..8cfe531
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-1.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-2.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-2.png
new file mode 100644
index 0000000..cb462e1
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-2.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-3.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-3.png
new file mode 100644
index 0000000..04c4977
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-3.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-4.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-4.png
new file mode 100644
index 0000000..b29fa3d
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-4.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-5.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-5.png
new file mode 100644
index 0000000..99f4e74
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/dragon-5.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-0.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-0.png
new file mode 100644
index 0000000..f48636f
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-0.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-1.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-1.png
new file mode 100644
index 0000000..b4018d9
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-1.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-2.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-2.png
new file mode 100644
index 0000000..3abaedd
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-2.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-3.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-3.png
new file mode 100644
index 0000000..fe94a5a
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-3.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-4.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-4.png
new file mode 100644
index 0000000..ed04237
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-4.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-5.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-5.png
new file mode 100644
index 0000000..2cd8f06
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-5.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-6.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-6.png
new file mode 100644
index 0000000..e55909c
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-6.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-sheet.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-sheet.png
new file mode 100644
index 0000000..8559a5c
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/explosion-sheet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-black.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-black.png
new file mode 100644
index 0000000..f50c872
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-blue.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-blue.png
new file mode 100644
index 0000000..1696bae
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-gray.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-gray.png
new file mode 100644
index 0000000..e8c4c5a
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-green.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-green.png
new file mode 100644
index 0000000..a700602
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-indigo.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-indigo.png
new file mode 100644
index 0000000..15f6f4f
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-orange.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-orange.png
new file mode 100644
index 0000000..1587173
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-red.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-red.png
new file mode 100644
index 0000000..d442f39
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-violet.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-violet.png
new file mode 100644
index 0000000..3be5731
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-white.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-white.png
new file mode 100644
index 0000000..c1ad970
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-yellow.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-yellow.png
new file mode 100644
index 0000000..63f5f34
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/hexagon-yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-black.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-black.png
new file mode 100644
index 0000000..fa9e463
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-blue.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-blue.png
new file mode 100644
index 0000000..a3d8524
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-gray.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-gray.png
new file mode 100644
index 0000000..85dcc1d
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-green.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-green.png
new file mode 100644
index 0000000..ec2773e
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-indigo.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-indigo.png
new file mode 100644
index 0000000..e6be50c
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-orange.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-orange.png
new file mode 100644
index 0000000..154d81c
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-red.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-red.png
new file mode 100644
index 0000000..3448c4d
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-violet.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-violet.png
new file mode 100644
index 0000000..f09bf21
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-white.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-white.png
new file mode 100644
index 0000000..a45793d
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-yellow.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-yellow.png
new file mode 100644
index 0000000..9be20c7
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/isometric-yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-black.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-black.png
new file mode 100644
index 0000000..cea7bd7
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-blue.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-blue.png
new file mode 100644
index 0000000..b840849
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-gray.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-gray.png
new file mode 100644
index 0000000..2142b30
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-green.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-green.png
new file mode 100644
index 0000000..5ef7f75
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-indigo.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-indigo.png
new file mode 100644
index 0000000..2384108
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-orange.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-orange.png
new file mode 100644
index 0000000..bb1eee7
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-red.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-red.png
new file mode 100644
index 0000000..3ed5f13
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-violet.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-violet.png
new file mode 100644
index 0000000..333540c
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-white.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-white.png
new file mode 100644
index 0000000..378c565
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-yellow.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-yellow.png
new file mode 100644
index 0000000..0edeeec
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/square-yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/star.png b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/star.png
new file mode 100644
index 0000000..e0ee0f9
--- /dev/null
+++ b/samples/04_physics_and_collisions/08_bouncing_on_collision/sprites/star.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/LICENSE b/samples/04_physics_and_collisions/09_arbitrary_collision/LICENSE
new file mode 100644
index 0000000..ac7ea12
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 DragonRuby
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/app/ball.rb b/samples/04_physics_and_collisions/09_arbitrary_collision/app/ball.rb
new file mode 100644
index 0000000..cc50b18
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/app/ball.rb
@@ -0,0 +1,166 @@
+
+class Ball
+ attr_accessor :velocity, :child, :parent, :number, :leastChain
+ attr_reader :x, :y, :hypotenuse, :width, :height
+
+ def initialize args, number, leastChain, parent, child
+ #Start the ball in the top center
+ @number = number
+ @leastChain = leastChain
+ @x = args.grid.w / 2
+ @y = args.grid.h - 20
+
+ @velocity = Vector2d.new(2, -2)
+ @width = 10
+ @height = 10
+
+ @left_wall = (args.state.board_width + args.grid.w / 8)
+ @right_wall = @left_wall + args.state.board_width
+
+ @max_velocity = MAX_VELOCITY
+
+ @child = child
+ @parent = parent
+
+ @past = [{x: @x, y: @y}]
+ @next = nil
+ end
+
+ def reassignLeastChain (lc=nil)
+ if (lc == nil)
+ lc = @number
+ end
+ @leastChain = lc
+ if (parent != nil)
+ @parent.reassignLeastChain(lc)
+ end
+
+ end
+
+ def makeLeader args
+ if isLeader
+ return
+ end
+ @parent.reassignLeastChain
+ args.state.ballParents.push(self)
+ @parent = nil
+
+ end
+
+ def isLeader
+ return (parent == nil)
+ end
+
+ def receiveNext (p)
+ #trace!
+ if parent != nil
+ @x = p[:x]
+ @y = p[:y]
+ @velocity = p[:velocity]
+ #puts @x.to_s + "|" + @y.to_s + "|"[email protected]_s
+ @past.append(p)
+ if (@past.length >= BALL_DISTANCE)
+ if (@child != nil)
+ @child.receiveNext(@past[0])
+ @past.shift
+ end
+ end
+ end
+ end
+
+ #Move the ball according to its velocity
+ def update args
+
+ if isLeader
+ wallBounds args
+ @x += @velocity.x
+ @y += @velocity.y
+ @past.append({x: @x, y: @y, velocity: @velocity})
+ #puts @past
+
+ if (@past.length >= BALL_DISTANCE)
+ if (@child != nil)
+ @child.receiveNext(@past[0])
+ @past.shift
+ end
+ end
+
+ else
+ puts "unexpected"
+ raise "unexpected"
+ end
+ end
+
+ def wallBounds args
+ b= false
+ if @x < @left_wall
+ @velocity.x = @velocity.x.abs() * 1
+ b=true
+ elsif @x + @width > @right_wall
+ @velocity.x = @velocity.x.abs() * -1
+ b=true
+ end
+ if @y < 0
+ @velocity.y = @velocity.y.abs() * 1
+ b=true
+ elsif @y + @height > args.grid.h
+ @velocity.y = @velocity.y.abs() * -1
+ b=true
+ end
+ mag = (@velocity.x**2.0 + @velocity.y**2.0)**0.5
+ if (b == true && mag < MAX_VELOCITY)
+ @velocity.x*=1.1;
+ @velocity.y*=1.1;
+ end
+
+ end
+
+ #render the ball to the screen
+ def draw args
+
+ #update args
+ #args.outputs.solids << [@x, @y, @width, @height, 255, 255, 0];
+ #args.outputs.sprits << {
+ #x: @x,
+ #y: @y,
+ #w: @width,
+ #h: @height,
+ #path: "sprites/ball10.png"
+ #}
+ #args.outputs.sprites <<[@x, @y, @width, @height, "sprites/ball10.png"]
+ args.outputs.sprites << {x: @x, y: @y, w: @width, h: @height, path:"sprites/ball10.png" }
+ end
+
+ def getDraw args
+ #wallBounds args
+ #update args
+ #args.outputs.labels << [@x, @y, @number.to_s + "|" + @leastChain.to_s]
+ return [@x, @y, @width, @height, "sprites/ball10.png"]
+ end
+
+ def getPoints args
+ points = [
+ {x:@x+@width/2, y: @y},
+ {x:@x+@width, y:@y+@height/2},
+ {x:@x+@width/2,y:@y+@height},
+ {x:@x,y:@y+@height/2}
+ ]
+ #psize = 5.0
+ #for p in points
+ #args.outputs.solids << [p.x-psize/2.0, p.y-psize/2.0, psize, psize, 0, 0, 0];
+ #end
+ return points
+ end
+
+ def serialize
+ {x: @x, y:@y}
+ end
+
+ def inspect
+ serialize.to_s
+ end
+
+ def to_s
+ serialize.to_s
+ end
+ end
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/app/blocks.rb b/samples/04_physics_and_collisions/09_arbitrary_collision/app/blocks.rb
new file mode 100644
index 0000000..118809f
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/app/blocks.rb
@@ -0,0 +1,618 @@
+MAX_COUNT=100
+
+def universalUpdateOne args, shape
+ didHit = false
+ hitters = []
+ #puts shape.to_s
+ toCollide = nil
+ for b in args.state.balls
+ if [b.x, b.y, b.width, b.height].intersect_rect?(shape.bold)
+ didSquare = false
+ for s in shape.squareColliders
+ if (s.collision?(args, b))
+ didSquare = true
+ didHit = true
+ #s.collide(args, b)
+ toCollide = s
+ #hitter = b
+ hitters.append(b)
+ end #end if
+ end #end for
+ if (didSquare == false)
+ for c in shape.colliders
+ #puts args.state.ball.velocity
+ if c.collision?(args, b.getPoints(args),b)
+ #c.collide args, b
+ toCollide = c
+ didHit = true
+ hitters.append(b)
+ end #end if
+ end #end for
+ end #end if
+ end#end if
+ end#end for
+ if (didHit)
+ shape.count=0
+ hitters = hitters.uniq
+ for hitter in hitters
+ hitter.makeLeader args
+ #toCollide.collide(args, hitter)
+ if shape.home == "squares"
+ args.state.squares.delete(shape)
+ elsif shape.home == "tshapes"
+ args.state.tshapes.delete(shape)
+ else shape.home == "lines"
+ args.state.lines.delete(shape)
+ end
+ end
+
+ #puts "HIT!" + hitter.number
+ end
+end
+
+def universalUpdate args, shape
+ #puts shape.home
+ if (shape.count <= 1)
+ universalUpdateOne args, shape
+ return
+ end
+
+ didHit = false
+ hitter = nil
+ for b in args.state.ballParents
+ if [b.x, b.y, b.width, b.height].intersect_rect?(shape.bold)
+ didSquare = false
+ for s in shape.squareColliders
+ if (s.collision?(args, b))
+ didSquare = true
+ didHit = true
+ s.collide(args, b)
+ hitter = b
+ end
+ end
+ if (didSquare == false)
+ for c in shape.colliders
+ #puts args.state.ball.velocity
+ if c.collision?(args, b.getPoints(args),b)
+ c.collide args, b
+ didHit = true
+ hitter = b
+ end
+ end
+ end
+ end
+ end
+ if (didHit)
+ shape.count=shape.count-1
+ shape.damageCount.append([(hitter.leastChain+1 - hitter.number)-1, args.state.tick_count])
+
+ end
+ i=0
+ while i < shape.damageCount.length
+ if shape.damageCount[i][0] <= 0
+ shape.damageCount.delete_at(i)
+ i-=1
+ elsif shape.damageCount[i][1].elapsed_time > BALL_DISTANCE and shape.damageCount[i][0] > 1
+ shape.count-=1
+ shape.damageCount[i][0]-=1
+ shape.damageCount[i][1] = args.state.tick_count
+ end
+ i+=1
+ end
+end
+
+
+class Square
+ attr_accessor :count, :x, :y, :home, :bold, :squareColliders, :colliders, :damageCount
+ def initialize(args, x, y, block_size, orientation, block_offset)
+ @x = x * block_size
+ @y = y * block_size
+ @block_size = block_size
+ @block_offset = block_offset
+ @orientation = orientation
+ @damageCount = []
+ @home = 'squares'
+
+
+ Kernel.srand()
+ @r = rand(255)
+ @g = rand(255)
+ @b = rand(255)
+
+ @count = rand(MAX_COUNT)+1
+
+ x_offset = (args.state.board_width + args.grid.w / 8) + @block_offset / 2
+ @x_adjusted = @x + x_offset
+ @y_adjusted = @y
+ @size_adjusted = @block_size * 2 - @block_offset
+
+ hypotenuse=args.state.ball_hypotenuse
+ @bold = [(@x_adjusted-hypotenuse/2)-1, (@y_adjusted-hypotenuse/2)-1, @size_adjusted + hypotenuse + 2, @size_adjusted + hypotenuse + 2]
+
+ @points = [
+ {x:@x_adjusted, y:@y_adjusted},
+ {x:@x_adjusted+@size_adjusted, y:@y_adjusted},
+ {x:@x_adjusted+@size_adjusted, y:@y_adjusted+@size_adjusted},
+ {x:@x_adjusted, y:@y_adjusted+@size_adjusted}
+ ]
+ @squareColliders = [
+ SquareCollider.new(@points[0].x,@points[0].y,{x:-1,y:-1}),
+ SquareCollider.new(@points[1].x-COLLISIONWIDTH,@points[1].y,{x:1,y:-1}),
+ SquareCollider.new(@points[2].x-COLLISIONWIDTH,@points[2].y-COLLISIONWIDTH,{x:1,y:1}),
+ SquareCollider.new(@points[3].x,@points[3].y-COLLISIONWIDTH,{x:-1,y:1}),
+ ]
+ @colliders = [
+ LinearCollider.new(@points[0],@points[1], :neg),
+ LinearCollider.new(@points[1],@points[2], :neg),
+ LinearCollider.new(@points[2],@points[3], :pos),
+ LinearCollider.new(@points[0],@points[3], :pos)
+ ]
+ end
+
+ def draw(args)
+ #Offset the coordinates to the edge of the game area
+ x_offset = (args.state.board_width + args.grid.w / 8) + @block_offset / 2
+ #args.outputs.solids << [@x + x_offset, @y, @block_size * 2 - @block_offset, @block_size * 2 - @block_offset, @r, @g, @b]
+ args.outputs.solids <<{x: (@x + x_offset), y: (@y), w: (@block_size * 2 - @block_offset), h: (@block_size * 2 - @block_offset), r: @r , g: @g , b: @b }
+ #args.outputs.solids << @bold.append([255,0,0])
+ args.outputs.labels << [@x + x_offset + (@block_size * 2 - @block_offset)/2, (@y) + (@block_size * 2 - @block_offset)/2, @count.to_s]
+
+ end
+
+ def update args
+ universalUpdate args, self
+ end
+end
+
+class TShape
+ attr_accessor :count, :x, :y, :home, :bold, :squareColliders, :colliders, :damageCount
+ def initialize(args, x, y, block_size, orientation, block_offset)
+ @x = x * block_size
+ @y = y * block_size
+ @block_size = block_size
+ @block_offset = block_offset
+ @orientation = orientation
+ @damageCount = []
+ @home = "tshapes"
+
+ Kernel.srand()
+ @r = rand(255)
+ @g = rand(255)
+ @b = rand(255)
+
+ @count = rand(MAX_COUNT)+1
+
+
+ @shapePoints = getShapePoints(args)
+ minX={x:INFINITY, y:0}
+ minY={x:0, y:INFINITY}
+ maxX={x:-INFINITY, y:0}
+ maxY={x:0, y:-INFINITY}
+ for p in @shapePoints
+ if p.x < minX.x
+ minX = p
+ end
+ if p.x > maxX.x
+ maxX = p
+ end
+ if p.y < minY.y
+ minY = p
+ end
+ if p.y > maxY.y
+ maxY = p
+ end
+ end
+
+
+ hypotenuse=args.state.ball_hypotenuse
+
+ @bold = [(minX.x-hypotenuse/2)-1, (minY.y-hypotenuse/2)-1, -((minX.x-hypotenuse/2)-1)+(maxX.x + hypotenuse + 2), -((minY.y-hypotenuse/2)-1)+(maxY.y + hypotenuse + 2)]
+ end
+ def getShapePoints(args)
+ points=[]
+ x_offset = (args.state.board_width + args.grid.w / 8) + (@block_offset / 2)
+
+ if @orientation == :right
+ #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
+ #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2, @block_size, @r, @g, @b]
+ points = [
+ {x:@x + x_offset, y:@y},
+ {x:(@x + x_offset)+(@block_size - @block_offset), y:@y},
+ {x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size},
+ {x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size},
+ {x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size+@block_size},
+ {x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size+@block_size},
+ {x:(@x + x_offset)+(@block_size - @block_offset), y:@y+ @block_size * 3 - @block_offset},
+ {x:@x + x_offset , y:@y+ @block_size * 3 - @block_offset}
+ ]
+ @squareColliders = [
+ SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}),
+ SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}),
+ SquareCollider.new(points[2].x,points[2].y-COLLISIONWIDTH,{x:1,y:-1}),
+ SquareCollider.new(points[3].x-COLLISIONWIDTH,points[3].y,{x:1,y:-1}),
+ SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y-COLLISIONWIDTH,{x:1,y:1}),
+ SquareCollider.new(points[5].x,points[5].y,{x:1,y:1}),
+ SquareCollider.new(points[6].x-COLLISIONWIDTH,points[6].y-COLLISIONWIDTH,{x:1,y:1}),
+ SquareCollider.new(points[7].x,points[7].y-COLLISIONWIDTH,{x:-1,y:1}),
+ ]
+ @colliders = [
+ LinearCollider.new(points[0],points[1], :neg),
+ LinearCollider.new(points[1],points[2], :neg),
+ LinearCollider.new(points[2],points[3], :neg),
+ LinearCollider.new(points[3],points[4], :neg),
+ LinearCollider.new(points[4],points[5], :pos),
+ LinearCollider.new(points[5],points[6], :neg),
+ LinearCollider.new(points[6],points[7], :pos),
+ LinearCollider.new(points[0],points[7], :pos)
+ ]
+ elsif @orientation == :up
+ #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size, @block_size * 2, @r, @g, @b]
+ points = [
+ {x:@x + x_offset, y:@y},
+ {x:(@x + x_offset)+(@block_size * 3 - @block_offset), y:@y},
+ {x:(@x + x_offset)+(@block_size * 3 - @block_offset), y:@y+(@block_size - @block_offset)},
+ {x:@x + x_offset + @block_size + @block_size, y:@y+(@block_size - @block_offset)},
+ {x:@x + x_offset + @block_size + @block_size, y:@y+@block_size*2},
+ {x:@x + x_offset + @block_size, y:@y+@block_size*2},
+ {x:@x + x_offset + @block_size, y:@y+(@block_size - @block_offset)},
+ {x:@x + x_offset, y:@y+(@block_size - @block_offset)}
+ ]
+ @squareColliders = [
+ SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}),
+ SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}),
+ SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y-COLLISIONWIDTH,{x:1,y:1}),
+ SquareCollider.new(points[3].x,points[3].y,{x:1,y:1}),
+ SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y-COLLISIONWIDTH,{x:1,y:1}),
+ SquareCollider.new(points[5].x,points[5].y-COLLISIONWIDTH,{x:-1,y:1}),
+ SquareCollider.new(points[6].x-COLLISIONWIDTH,points[6].y,{x:-1,y:1}),
+ SquareCollider.new(points[7].x,points[7].y-COLLISIONWIDTH,{x:-1,y:1}),
+ ]
+ @colliders = [
+ LinearCollider.new(points[0],points[1], :neg),
+ LinearCollider.new(points[1],points[2], :neg),
+ LinearCollider.new(points[2],points[3], :pos),
+ LinearCollider.new(points[3],points[4], :neg),
+ LinearCollider.new(points[4],points[5], :pos),
+ LinearCollider.new(points[5],points[6], :neg),
+ LinearCollider.new(points[6],points[7], :pos),
+ LinearCollider.new(points[0],points[7], :pos)
+ ]
+ elsif @orientation == :left
+ #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
+ #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ xh = @x + x_offset
+ #points = [
+ #{x:@x + x_offset, y:@y},
+ #{x:(@x + x_offset)+(@block_size - @block_offset), y:@y},
+ #{x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size},
+ #{x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size},
+ #{x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size+@block_size},
+ #{x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size+@block_size},
+ #{x:(@x + x_offset)+(@block_size - @block_offset), y:@y+ @block_size * 3 - @block_offset},
+ #{x:@x + x_offset , y:@y+ @block_size * 3 - @block_offset}
+ #]
+ points = [
+ {x:@x + x_offset + @block_size, y:@y},
+ {x:@x + x_offset + @block_size + (@block_size - @block_offset), y:@y},
+ {x:@x + x_offset + @block_size + (@block_size - @block_offset),y:@y+@block_size*3- @block_offset},
+ {x:@x + x_offset + @block_size, y:@y+@block_size*3- @block_offset},
+ {x:@x + x_offset+@block_size, y:@y+@block_size*2- @block_offset},
+ {x:@x + x_offset, y:@y+@block_size*2- @block_offset},
+ {x:@x + x_offset, y:@y+@block_size},
+ {x:@x + x_offset+@block_size, y:@y+@block_size}
+ ]
+ @squareColliders = [
+ SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}),
+ SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}),
+ SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y-COLLISIONWIDTH,{x:1,y:1}),
+ SquareCollider.new(points[3].x,points[3].y-COLLISIONWIDTH,{x:-1,y:1}),
+ SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y,{x:-1,y:1}),
+ SquareCollider.new(points[5].x,points[5].y-COLLISIONWIDTH,{x:-1,y:1}),
+ SquareCollider.new(points[6].x,points[6].y,{x:-1,y:-1}),
+ SquareCollider.new(points[7].x-COLLISIONWIDTH,points[7].y-COLLISIONWIDTH,{x:-1,y:-1}),
+ ]
+ @colliders = [
+ LinearCollider.new(points[0],points[1], :neg),
+ LinearCollider.new(points[1],points[2], :neg),
+ LinearCollider.new(points[2],points[3], :pos),
+ LinearCollider.new(points[3],points[4], :neg),
+ LinearCollider.new(points[4],points[5], :pos),
+ LinearCollider.new(points[5],points[6], :neg),
+ LinearCollider.new(points[6],points[7], :neg),
+ LinearCollider.new(points[0],points[7], :pos)
+ ]
+ elsif @orientation == :down
+ #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 2 - @block_offset, @r, @g, @b]
+
+ points = [
+ {x:@x + x_offset, y:@y+(@block_size*2)-@block_offset},
+ {x:@x + x_offset+ @block_size*3-@block_offset, y:@y+(@block_size*2)-@block_offset},
+ {x:@x + x_offset+ @block_size*3-@block_offset, y:@y+(@block_size)},
+ {x:@x + x_offset+ @block_size*2-@block_offset, y:@y+(@block_size)},
+ {x:@x + x_offset+ @block_size*2-@block_offset, y:@y},#
+ {x:@x + x_offset+ @block_size, y:@y},#
+ {x:@x + x_offset + @block_size, y:@y+(@block_size)},
+ {x:@x + x_offset, y:@y+(@block_size)}
+ ]
+ @squareColliders = [
+ SquareCollider.new(points[0].x,points[0].y-COLLISIONWIDTH,{x:-1,y:1}),
+ SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y-COLLISIONWIDTH,{x:1,y:1}),
+ SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y,{x:1,y:-1}),
+ SquareCollider.new(points[3].x,points[3].y-COLLISIONWIDTH,{x:1,y:-1}),
+ SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y,{x:1,y:-1}),
+ SquareCollider.new(points[5].x,points[5].y,{x:-1,y:-1}),
+ SquareCollider.new(points[6].x-COLLISIONWIDTH,points[6].y-COLLISIONWIDTH,{x:-1,y:-1}),
+ SquareCollider.new(points[7].x,points[7].y,{x:-1,y:-1}),
+ ]
+ @colliders = [
+ LinearCollider.new(points[0],points[1], :pos),
+ LinearCollider.new(points[1],points[2], :pos),
+ LinearCollider.new(points[2],points[3], :neg),
+ LinearCollider.new(points[3],points[4], :pos),
+ LinearCollider.new(points[4],points[5], :neg),
+ LinearCollider.new(points[5],points[6], :pos),
+ LinearCollider.new(points[6],points[7], :neg),
+ LinearCollider.new(points[0],points[7], :neg)
+ ]
+ end
+ return points
+ end
+
+ def draw(args)
+ #Offset the coordinates to the edge of the game area
+ x_offset = (args.state.board_width + args.grid.w / 8) + (@block_offset / 2)
+
+ if @orientation == :right
+ #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
+ args.outputs.solids << {x: (@x + x_offset), y: @y, w: @block_size - @block_offset, h: (@block_size * 3 - @block_offset), r: @r , g: @g, b: @b}
+ #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2, @block_size, @r, @g, @b]
+ args.outputs.solids << {x: (@x + x_offset), y: (@y + @block_size), w: (@block_size * 2), h: (@block_size), r: @r , g: @g, b: @b }
+ elsif @orientation == :up
+ #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ args.outputs.solids << {x: (@x + x_offset), y: (@y), w: (@block_size * 3 - @block_offset), h: (@block_size - @block_offset), r: @r , g: @g, b: @b}
+ #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size, @block_size * 2, @r, @g, @b]
+ args.outputs.solids << {x: (@x + x_offset + @block_size), y: (@y), w: (@block_size), h: (@block_size * 2), r: @r , g: @g, b: @b}
+ elsif @orientation == :left
+ #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
+ args.outputs.solids << {x: (@x + x_offset + @block_size), y: (@y), w: (@block_size - @block_offset), h: (@block_size * 3 - @block_offset), r: @r , g: @g, b: @b}
+ #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ args.outputs.solids << {x: (@x + x_offset), y: (@y + @block_size), w: (@block_size * 2 - @block_offset), h: (@block_size - @block_offset), r: @r , g: @g, b: @b}
+ elsif @orientation == :down
+ #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ args.outputs.solids << {x: (@x + x_offset), y: (@y + @block_size), w: (@block_size * 3 - @block_offset), h: (@block_size - @block_offset), r: @r , g: @g, b: @b}
+ #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 2 - @block_offset, @r, @g, @b]
+ args.outputs.solids << {x: (@x + x_offset + @block_size), y: (@y), w: (@block_size - @block_offset), h: ( @block_size * 2 - @block_offset), r: @r , g: @g, b: @b}
+ end
+
+ #psize = 5.0
+ #for p in @shapePoints
+ #args.outputs.solids << [p.x-psize/2, p.y-psize/2, psize, psize, 0, 0, 0]
+ #end
+ args.outputs.labels << [@x + x_offset + (@block_size * 2 - @block_offset)/2, (@y) + (@block_size * 2 - @block_offset)/2, @count.to_s]
+
+ end
+
+ def updateOne_old args
+ didHit = false
+ hitter = nil
+ toCollide = nil
+ for b in args.state.balls
+ if [b.x, b.y, b.width, b.height].intersect_rect?(@bold)
+ didSquare = false
+ for s in @squareColliders
+ if (s.collision?(args, b))
+ didSquare = true
+ didHit = true
+ #s.collide(args, b)
+ toCollide = s
+ hitter = b
+ break
+ end
+ end
+ if (didSquare == false)
+ for c in @colliders
+ #puts args.state.ball.velocity
+ if c.collision?(args, b.getPoints(args),b)
+ #c.collide args, b
+ toCollide = c
+ didHit = true
+ hitter = b
+ break
+ end
+ end
+ end
+ end
+ if didHit
+ break
+ end
+ end
+ if (didHit)
+ @count=0
+ hitter.makeLeader args
+ #toCollide.collide(args, hitter)
+ args.state.tshapes.delete(self)
+ #puts "HIT!" + hitter.number
+ end
+ end
+
+ def update_old args
+ if (@count == 1)
+ updateOne args
+ return
+ end
+ didHit = false
+ hitter = nil
+ for b in args.state.ballParents
+ if [b.x, b.y, b.width, b.height].intersect_rect?(@bold)
+ didSquare = false
+ for s in @squareColliders
+ if (s.collision?(args, b))
+ didSquare = true
+ didHit=true
+ s.collide(args, b)
+ hitter = b
+ end
+ end
+ if (didSquare == false)
+ for c in @colliders
+ #puts args.state.ball.velocity
+ if c.collision?(args, b.getPoints(args), b)
+ c.collide args, b
+ didHit=true
+ hitter = b
+ end
+ end
+ end
+ end
+ end
+ if (didHit)
+ @count=@count-1
+ @damageCount.append([(hitter.leastChain+1 - hitter.number)-1, args.state.tick_count])
+
+ if (@count == 0)
+ args.state.tshapes.delete(self)
+ return
+ end
+ end
+ i=0
+
+ while i < @damageCount.length
+ if @damageCount[i][0] <= 0
+ @damageCount.delete_at(i)
+ i-=1
+ elsif @damageCount[i][1].elapsed_time > BALL_DISTANCE
+ @count-=1
+ @damageCount[i][0]-=1
+ end
+ if (@count == 0)
+ args.state.tshapes.delete(self)
+ return
+ end
+ i+=1
+ end
+ end #end update
+
+ def update args
+ universalUpdate args, self
+ end
+
+end
+
+class Line
+ attr_accessor :count, :x, :y, :home, :bold, :squareColliders, :colliders, :damageCount
+ def initialize(args, x, y, block_size, orientation, block_offset)
+ @x = x * block_size
+ @y = y * block_size
+ @block_size = block_size
+ @block_offset = block_offset
+ @orientation = orientation
+ @damageCount = []
+ @home = "lines"
+
+ Kernel.srand()
+ @r = rand(255)
+ @g = rand(255)
+ @b = rand(255)
+
+ @count = rand(MAX_COUNT)+1
+
+ @shapePoints = getShapePoints(args)
+ minX={x:INFINITY, y:0}
+ minY={x:0, y:INFINITY}
+ maxX={x:-INFINITY, y:0}
+ maxY={x:0, y:-INFINITY}
+ for p in @shapePoints
+ if p.x < minX.x
+ minX = p
+ end
+ if p.x > maxX.x
+ maxX = p
+ end
+ if p.y < minY.y
+ minY = p
+ end
+ if p.y > maxY.y
+ maxY = p
+ end
+ end
+
+
+ hypotenuse=args.state.ball_hypotenuse
+
+ @bold = [(minX.x-hypotenuse/2)-1, (minY.y-hypotenuse/2)-1, -((minX.x-hypotenuse/2)-1)+(maxX.x + hypotenuse + 2), -((minY.y-hypotenuse/2)-1)+(maxY.y + hypotenuse + 2)]
+ end
+
+ def getShapePoints(args)
+ points=[]
+ x_offset = (args.state.board_width + args.grid.w / 8) + (@block_offset / 2)
+
+ if @orientation == :right
+ #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ xa =@x + x_offset
+ ya =@y
+ wa =@block_size * 3 - @block_offset
+ ha =(@block_size - @block_offset)
+ elsif @orientation == :up
+ #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
+ xa =@x + x_offset
+ ya =@y
+ wa =@block_size - @block_offset
+ ha =@block_size * 3 - @block_offset
+
+ elsif @orientation == :left
+ #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ xa =@x + x_offset
+ ya =@y
+ wa =@block_size * 3 - @block_offset
+ ha =@block_size - @block_offset
+ elsif @orientation == :down
+ #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
+ xa =@x + x_offset
+ ya =@y
+ wa =@block_size - @block_offset
+ ha =@block_size * 3 - @block_offset
+ end
+ points = [
+ {x: xa, y:ya},
+ {x: xa + wa,y:ya},
+ {x: xa + wa,y:ya+ha},
+ {x: xa, y:ya+ha},
+ ]
+ @squareColliders = [
+ SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}),
+ SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}),
+ SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y-COLLISIONWIDTH,{x:1,y:1}),
+ SquareCollider.new(points[3].x,points[3].y-COLLISIONWIDTH,{x:-1,y:1}),
+ ]
+ @colliders = [
+ LinearCollider.new(points[0],points[1], :neg),
+ LinearCollider.new(points[1],points[2], :neg),
+ LinearCollider.new(points[2],points[3], :pos),
+ LinearCollider.new(points[0],points[3], :pos),
+ ]
+ return points
+ end
+
+ def update args
+ universalUpdate args, self
+ end
+
+ def draw(args)
+ x_offset = (args.state.board_width + args.grid.w / 8) + @block_offset / 2
+
+ if @orientation == :right
+ args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ elsif @orientation == :up
+ args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
+ elsif @orientation == :left
+ args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
+ elsif @orientation == :down
+ args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
+ end
+
+ args.outputs.labels << [@x + x_offset + (@block_size * 2 - @block_offset)/2, (@y) + (@block_size * 2 - @block_offset)/2, @count.to_s]
+
+ end
+end
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/app/linear_collider.rb b/samples/04_physics_and_collisions/09_arbitrary_collision/app/linear_collider.rb
new file mode 100644
index 0000000..9571669
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/app/linear_collider.rb
@@ -0,0 +1,180 @@
+
+COLLISIONWIDTH=8
+
+class LinearCollider
+ attr_reader :pointA, :pointB
+ def initialize (pointA, pointB, mode,collisionWidth=COLLISIONWIDTH)
+ @pointA = pointA
+ @pointB = pointB
+ @mode = mode
+ @collisionWidth = collisionWidth
+
+ if (@pointA.x > @pointB.x)
+ @pointA, @pointB = @pointB, @pointA
+ end
+
+ @linearCollider_collision_once = false
+ end
+
+ def collisionSlope args
+ if (@[email protected] == 0)
+ return INFINITY
+ end
+ return (@pointB.y - @pointA.y) / (@pointB.x - @pointA.x)
+ end
+
+
+ def collision? (args, points, ball=nil)
+
+ slope = collisionSlope args
+ result = false
+
+ # calculate a vector with a magnitude of (1/2)collisionWidth and a direction perpendicular to the collision line
+ vect=nil;mag=nil;vect=nil;
+ if @mode == :both
+ vect = {x: @pointB.x - @pointA.x, y:@pointB.y - @pointA.y}
+ mag = (vect.x**2 + vect.y**2)**0.5
+ vect = {y: -1*(vect.x/(mag))*@collisionWidth*0.5, x: (vect.y/(mag))*@collisionWidth*0.5}
+ else
+ vect = {x: @pointB.x - @pointA.x, y:@pointB.y - @pointA.y}
+ mag = (vect.x**2 + vect.y**2)**0.5
+ vect = {y: -1*(vect.x/(mag))*@collisionWidth, x: (vect.y/(mag))*@collisionWidth}
+ end
+
+ rpointA=nil;rpointB=nil;rpointC=nil;rpointD=nil;
+ if @mode == :pos
+ rpointA = {x:@pointA.x + vect.x, y:@pointA.y + vect.y}
+ rpointB = {x:@pointB.x + vect.x, y:@pointB.y + vect.y}
+ rpointC = {x:@pointB.x, y:@pointB.y}
+ rpointD = {x:@pointA.x, y:@pointA.y}
+ elsif @mode == :neg
+ rpointA = {x:@pointA.x, y:@pointA.y}
+ rpointB = {x:@pointB.x, y:@pointB.y}
+ rpointC = {x:@pointB.x - vect.x, y:@pointB.y - vect.y}
+ rpointD = {x:@pointA.x - vect.x, y:@pointA.y - vect.y}
+ elsif @mode == :both
+ rpointA = {x:@pointA.x + vect.x, y:@pointA.y + vect.y}
+ rpointB = {x:@pointB.x + vect.x, y:@pointB.y + vect.y}
+ rpointC = {x:@pointB.x - vect.x, y:@pointB.y - vect.y}
+ rpointD = {x:@pointA.x - vect.x, y:@pointA.y - vect.y}
+ end
+ #four point rectangle
+
+
+
+ if ball != nil
+ xs = [rpointA.x,rpointB.x,rpointC.x,rpointD.x]
+ ys = [rpointA.y,rpointB.y,rpointC.y,rpointD.y]
+ correct = 1
+ rect1 = [ball.x, ball.y, ball.width, ball.height]
+ #$r1 = rect1
+ rect2 = [xs.min-correct,ys.min-correct,(xs.max-xs.min)+correct*2,(ys.max-ys.min)+correct*2]
+ #$r2 = rect2
+ if rect1.intersect_rect?(rect2) == false
+ return false
+ end
+ end
+
+
+ #area of a triangle
+ triArea = -> (a,b,c) { ((a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y))/2.0).abs }
+
+ #if at least on point is in the rectangle then collision? is true - otherwise false
+ for point in points
+ #Check whether a given point lies inside a rectangle or not:
+ #if the sum of the area of traingls, PAB, PBC, PCD, PAD equal the area of the rec, then an intersection has occured
+ areaRec = triArea.call(rpointA, rpointB, rpointC)+triArea.call(rpointA, rpointC, rpointD)
+ areaSum = [
+ triArea.call(point, rpointA, rpointB),triArea.call(point, rpointB, rpointC),
+ triArea.call(point, rpointC, rpointD),triArea.call(point, rpointA, rpointD)
+ ].inject(0){|sum,x| sum + x }
+ e = 0.0001 #allow for minor error
+ if areaRec>= areaSum-e and areaRec<= areaSum+e
+ result = true
+ #return true
+ break
+ end
+ end
+
+ #args.outputs.lines << [@pointA.x, @pointA.y, @pointB.x, @pointB.y, 000, 000, 000]
+ #args.outputs.lines << [rpointA.x, rpointA.y, rpointB.x, rpointB.y, 255, 000, 000]
+ #args.outputs.lines << [rpointC.x, rpointC.y, rpointD.x, rpointD.y, 000, 000, 255]
+
+
+ #puts (rpointA.x.to_s + " " + rpointA.y.to_s + " " + rpointB.x.to_s + " "+ rpointB.y.to_s)
+ return result
+ end #end collision?
+
+ def getRepelMagnitude (fbx, fby, vrx, vry, ballMag)
+ a = fbx ; b = vrx ; c = fby
+ d = vry ; e = ballMag
+ if b**2 + d**2 == 0
+ #unexpected
+ end
+ x1 = (-a*b+-c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 + d**2 - a**2 * d**2)**0.5)/(b**2 + d**2)
+ x2 = -((a*b + c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 * d**2 - a**2 * d**2)**0.5)/(b**2 + d**2))
+ err = 0.00001
+ o = ((fbx + x1*vrx)**2 + (fby + x1*vry)**2 ) ** 0.5
+ p = ((fbx + x2*vrx)**2 + (fby + x2*vry)**2 ) ** 0.5
+ r = 0
+ if (ballMag >= o-err and ballMag <= o+err)
+ r = x1
+ elsif (ballMag >= p-err and ballMag <= p+err)
+ r = x2
+ else
+ #unexpected
+ end
+ return r
+ end
+
+ def collide args, ball
+ slope = collisionSlope args
+
+ # perpVect: normal vector perpendicular to collision
+ perpVect = {x: @pointB.x - @pointA.x, y:@pointB.y - @pointA.y}
+ mag = (perpVect.x**2 + perpVect.y**2)**0.5
+ perpVect = {x: perpVect.x/(mag), y: perpVect.y/(mag)}
+ perpVect = {x: -perpVect.y, y: perpVect.x}
+ if perpVect.y > 0 #ensure perpVect points upward
+ perpVect = {x: perpVect.x*-1, y: perpVect.y*-1}
+ end
+ previousPosition = {
+ x:ball.x-ball.velocity.x,
+ y:ball.y-ball.velocity.y
+ }
+ yInterc = @pointA.y + -slope*@pointA.x
+ if slope == INFINITY
+ if previousPosition.x < @pointA.x
+ perpVect = {x: perpVect.x*-1, y: perpVect.y*-1}
+ yInterc = -INFINITY
+ end
+ elsif previousPosition.y < slope*previousPosition.x + yInterc #check if ball is bellow or above the collider to determine if perpVect is - or +
+ perpVect = {x: perpVect.x*-1, y: perpVect.y*-1}
+ end
+
+ velocityMag = (ball.velocity.x**2 + ball.velocity.y**2)**0.5
+ theta_ball=Math.atan2(ball.velocity.y,ball.velocity.x) #the angle of the ball's velocity
+ theta_repel=Math.atan2(perpVect.y,perpVect.x) #the angle of the repelling force(perpVect)
+
+ fbx = velocityMag * Math.cos(theta_ball) #the x component of the ball's velocity
+ fby = velocityMag * Math.sin(theta_ball) #the y component of the ball's velocity
+
+ #the magnitude of the repelling force
+ repelMag = getRepelMagnitude(fbx, fby, perpVect.x, perpVect.y, (ball.velocity.x**2 + ball.velocity.y**2)**0.5)
+ frx = repelMag* Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx
+ fry = repelMag* Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby
+
+ fsumx = fbx+frx #sum of x forces
+ fsumy = fby+fry #sum of y forces
+ fr = velocityMag#fr is the resulting magnitude
+ thetaNew = Math.atan2(fsumy, fsumx) #thetaNew is the resulting angle
+ xnew = fr*Math.cos(thetaNew)#resulting x velocity
+ ynew = fr*Math.sin(thetaNew)#resulting y velocity
+ if (velocityMag < MAX_VELOCITY)
+ ball.velocity = Vector2d.new(xnew*1.1, ynew*1.1)
+ else
+ ball.velocity = Vector2d.new(xnew, ynew)
+ end
+
+ end
+end
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/app/main.rb b/samples/04_physics_and_collisions/09_arbitrary_collision/app/main.rb
new file mode 100644
index 0000000..b2905fb
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/app/main.rb
@@ -0,0 +1,171 @@
+INFINITY= 10**10
+MAX_VELOCITY = 8.0
+BALL_COUNT = 90
+BALL_DISTANCE = 20
+require 'app/vector2d.rb'
+require 'app/blocks.rb'
+require 'app/ball.rb'
+require 'app/rectangle.rb'
+require 'app/linear_collider.rb'
+require 'app/square_collider.rb'
+
+
+
+#Method to init default values
+def defaults args
+ args.state.board_width ||= args.grid.w / 4
+ args.state.board_height ||= args.grid.h
+ args.state.game_area ||= [(args.state.board_width + args.grid.w / 8), 0, args.state.board_width, args.grid.h]
+ args.state.balls ||= []
+ args.state.num_balls ||= 0
+ args.state.ball_created_at ||= args.state.tick_count
+ args.state.ball_hypotenuse = (10**2 + 10**2)**0.5
+ args.state.ballParents ||=nil
+
+ init_blocks args
+ init_balls args
+end
+
+begin :default_methods
+ def init_blocks args
+ block_size = args.state.board_width / 8
+ #Space inbetween each block
+ block_offset = 4
+
+ args.state.squares ||=[
+ Square.new(args, 2, 0, block_size, :right, block_offset),
+ Square.new(args, 5, 0, block_size, :right, block_offset),
+ Square.new(args, 6, 7, block_size, :right, block_offset)
+ ]
+
+
+ #Possible orientations are :right, :left, :up, :down
+
+
+ args.state.tshapes ||= [
+ TShape.new(args, 0, 6, block_size, :left, block_offset),
+ TShape.new(args, 3, 3, block_size, :down, block_offset),
+ TShape.new(args, 0, 3, block_size, :right, block_offset),
+ TShape.new(args, 0, 11, block_size, :up, block_offset)
+ ]
+
+ args.state.lines ||= [
+ Line.new(args,3, 8, block_size, :down, block_offset),
+ Line.new(args, 7, 3, block_size, :up, block_offset),
+ Line.new(args, 3, 7, block_size, :right, block_offset)
+ ]
+
+ #exit()
+ end
+
+ def init_balls args
+ return unless args.state.num_balls < BALL_COUNT
+
+
+ #only create a new ball every 10 ticks
+ return unless args.state.ball_created_at.elapsed_time > 10
+
+ if (args.state.num_balls == 0)
+ args.state.balls.append(Ball.new(args,args.state.num_balls,BALL_COUNT-1, nil, nil))
+ args.state.ballParents = [args.state.balls[0]]
+ else
+ args.state.balls.append(Ball.new(args,args.state.num_balls,BALL_COUNT-1, args.state.balls.last, nil) )
+ args.state.balls[-2].child = args.state.balls[-1]
+ end
+ args.state.ball_created_at = args.state.tick_count
+ args.state.num_balls += 1
+ end
+end
+
+#Render loop
+def render args
+ bgClr = {r:10, g:10, b:200}
+ bgClr = {r:255-30, g:255-30, b:255-30}
+
+ args.outputs.solids << [0, 0, $args.grid.right, $args.grid.top, bgClr[:r], bgClr[:g], bgClr[:b]];
+ args.outputs.borders << args.state.game_area
+
+ render_instructions args
+ render_shapes args
+
+ render_balls args
+
+ #args.state.rectangle.draw args
+
+ args.outputs.sprites << [$args.grid.right-(args.state.board_width + args.grid.w / 8), 0, $args.grid.right, $args.grid.top, "sprites/square-white-2.png", 0, 255, bgClr[:r], bgClr[:g], bgClr[:b]]
+ args.outputs.sprites << [0, 0, (args.state.board_width + args.grid.w / 8), $args.grid.top, "sprites/square-white-2.png", 0, 255, bgClr[:r], bgClr[:g], bgClr[:b]]
+
+end
+
+begin :render_methods
+ def render_instructions args
+ #gtk.current_framerate
+ args.outputs.labels << [20, $args.grid.top-20, "FPS: " + $gtk.current_framerate.to_s]
+ if (args.state.balls != nil && args.state.balls[0] != nil)
+ bx = args.state.balls[0].velocity.x
+ by = args.state.balls[0].velocity.y
+ bmg = (bx**2.0 + by**2.0)**0.5
+ args.outputs.labels << [20, $args.grid.top-20-20, "V: " + bmg.to_s ]
+ end
+
+
+ end
+
+ def render_shapes args
+ for s in args.state.squares
+ s.draw args
+ end
+
+ for l in args.state.lines
+ l.draw args
+ end
+
+ for t in args.state.tshapes
+ t.draw args
+ end
+
+
+ end
+
+ def render_balls args
+ #args.state.balls.each do |ball|
+ #ball.draw args
+ #end
+
+ args.outputs.sprites << args.state.balls.map do |ball|
+ ball.getDraw args
+ end
+ end
+end
+
+#Calls all methods necessary for performing calculations
+def calc args
+ for b in args.state.ballParents
+ b.update args
+ end
+
+ for s in args.state.squares
+ s.update args
+ end
+
+ for l in args.state.lines
+ l.update args
+ end
+
+ for t in args.state.tshapes
+ t.update args
+ end
+
+
+
+end
+
+begin :calc_methods
+
+end
+
+def tick args
+ defaults args
+ render args
+ calc args
+end
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/app/paddle.rb b/samples/04_physics_and_collisions/09_arbitrary_collision/app/paddle.rb
new file mode 100644
index 0000000..a4fe710
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/app/paddle.rb
@@ -0,0 +1,53 @@
+class Paddle
+ attr_accessor :enabled
+
+ def initialize ()
+ @x=WIDTH/2
+ @y=100
+ @width=100
+ @height=20
+ @speed=10
+
+ @xyCollision = LinearCollider.new({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5})
+ @xyCollision2 = LinearCollider.new({x: @x,y: @y}, {x: @x+@width, y: @y}, :pos)
+ @xyCollision3 = LinearCollider.new({x: @x,y: @y}, {x: @x, y: @y+@height+5})
+ @xyCollision4 = LinearCollider.new({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5}, :pos)
+
+ @enabled = true
+ end
+
+ def update args
+ @xyCollision.resetPoints({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5})
+ @xyCollision2.resetPoints({x: @x,y: @y}, {x: @x+@width, y: @y})
+ @xyCollision3.resetPoints({x: @x,y: @y}, {x: @x, y: @y+@height+5})
+ @xyCollision4.resetPoints({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5})
+
+ @xyCollision.update args
+ @xyCollision2.update args
+ @xyCollision3.update args
+ @xyCollision4.update args
+
+ args.inputs.keyboard.key_held.left ||= false
+ args.inputs.keyboard.key_held.right ||= false
+
+ if not (args.inputs.keyboard.key_held.left == args.inputs.keyboard.key_held.right)
+ if args.inputs.keyboard.key_held.left && @enabled
+ @x-=@speed
+ elsif args.inputs.keyboard.key_held.right && @enabled
+ @x+=@speed
+ end
+ end
+
+ xmin =WIDTH/4
+ xmax = 3*(WIDTH/4)
+ @x = (@x+@width > xmax) ? xmax-@width : (@x<xmin) ? xmin : @x;
+ end
+
+ def render args
+ args.outputs.solids << [@x,@y,@width,@height,255,0,0];
+ end
+
+ def rect
+ [@x, @y, @width, @height]
+ end
+end
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/app/rectangle.rb b/samples/04_physics_and_collisions/09_arbitrary_collision/app/rectangle.rb
new file mode 100644
index 0000000..f0906b7
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/app/rectangle.rb
@@ -0,0 +1,90 @@
+class Rectangle
+ def initialize args
+
+ @image = "sprites/roundSquare_white.png"
+ @width = 160.0
+ @height = 80.0
+ @x=$args.grid.right/2.0 - @width/2.0
+ @y=$args.grid.top/2.0 - @height/2.0
+
+ @xtmp = @width * (1.0/10.0)
+ @ytmp = @height * (1.0/10.0)
+
+ #ball0 = args.state.balls[0]
+ #hypotenuse = (args.state.balls[0].width**2 + args.state.balls[0].height**2)**0.5
+ hypotenuse=args.state.ball_hypotenuse
+ @boldXY = {x:(@x-hypotenuse/2)-1, y:(@y-hypotenuse/2)-1}
+ @boldWidth = @width + hypotenuse + 2
+ @boldHeight = @height + hypotenuse + 2
+ @bold = [(@x-hypotenuse/2)-1,(@y-hypotenuse/2)-1,@width + hypotenuse + 2,@height + hypotenuse + 2]
+
+
+ @points = [
+ {x:@x, y:@y+@ytmp},
+ {x:@x+@xtmp, y:@y},
+ {x:@x+@width-@xtmp, y:@y},
+ {x:@x+@width, y:@y+@ytmp},
+ {x:@x+@width, y:@y+@height-@ytmp},#
+ {x:@x+@width-@xtmp, y:@y+@height},
+ {x:@x+@xtmp, y:@y+@height},
+ {x:@x, y:@y+@height-@ytmp}
+ ]
+
+ @colliders = []
+ #i = 0
+ #while i < @points.length-1
+ #@colliders.append(LinearCollider.new(@points[i],@points[i+1],:pos))
+ #i+=1
+ #end
+ @colliders.append(LinearCollider.new(@points[0],@points[1], :neg))
+ @colliders.append(LinearCollider.new(@points[1],@points[2], :neg))
+ @colliders.append(LinearCollider.new(@points[2],@points[3], :neg))
+ @colliders.append(LinearCollider.new(@points[3],@points[4], :neg))
+ @colliders.append(LinearCollider.new(@points[4],@points[5], :pos))
+ @colliders.append(LinearCollider.new(@points[5],@points[6], :pos))
+ @colliders.append(LinearCollider.new(@points[6],@points[7], :pos))
+ @colliders.append(LinearCollider.new(@points[0],@points[7], :pos))
+
+ end
+
+ def update args
+
+ for b in args.state.balls
+ if [b.x, b.y, b.width, b.height].intersect_rect?(@bold)
+ for c in @colliders
+ if c.collision?(args, b.getPoints(args),b)
+ c.collide args, b
+ end
+ end
+ end
+ end
+ end
+
+ def draw args
+ args.outputs.sprites << [
+ @x, # X
+ @y, # Y
+ @width, # W
+ @height, # H
+ @image, # PATH
+ 0, # ANGLE
+ 255, # ALPHA
+ 219, # RED_SATURATION
+ 112, # GREEN_SATURATION
+ 147 # BLUE_SATURATION
+ ]
+ #args.outputs.sprites << [@x, @y, @width, @height, "sprites/roundSquare_small_black.png"]
+ end
+
+ def serialize
+ {x: @x, y:@y}
+ end
+
+ def inspect
+ serialize.to_s
+ end
+
+ def to_s
+ serialize.to_s
+ end
+end
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/app/square_collider.rb b/samples/04_physics_and_collisions/09_arbitrary_collision/app/square_collider.rb
new file mode 100644
index 0000000..1a403f2
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/app/square_collider.rb
@@ -0,0 +1,29 @@
+
+class SquareCollider
+ def initialize x,y,direction,size=COLLISIONWIDTH
+ @x = x
+ @y = y
+ @size = size
+ @direction = direction
+
+ end
+ def collision? args, ball
+ #args.outputs.solids << [@x, @y, @size, @size, 000, 255, 255]
+
+
+ return [@x,@y,@size,@size].intersect_rect?([ball.x,ball.y,ball.width,ball.height])
+ end
+
+ def collide args, ball
+ vmag = (ball.velocity.x**2.0 +ball.velocity.y**2.0)**0.5
+ a = ((2.0**0.5)*vmag)/2.0
+ if vmag < MAX_VELOCITY
+ ball.velocity.x = (a) * @direction.x * 1.1
+ ball.velocity.y = (a) * @direction.y * 1.1
+ else
+ ball.velocity.x = (a) * @direction.x
+ ball.velocity.y = (a) * @direction.y
+ end
+
+ end
+end
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/app/vector2d.rb b/samples/04_physics_and_collisions/09_arbitrary_collision/app/vector2d.rb
new file mode 100644
index 0000000..9cb1954
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/app/vector2d.rb
@@ -0,0 +1,49 @@
+class Vector2d
+ attr_accessor :x, :y
+
+ def initialize x=0, y=0
+ @x=x
+ @y=y
+ end
+
+ #returns a vector multiplied by scalar x
+ #x [float] scalar
+ def mult x
+ r = Vector2d.new(0,0)
+ r.x=@x*x
+ r.y=@y*x
+ r
+ end
+
+ # vect [Vector2d] vector to copy
+ def copy vect
+ Vector2d.new(@x, @y)
+ end
+
+ #returns a new vector equivalent to this+vect
+ #vect [Vector2d] vector to add to self
+ def add vect
+ Vector2d.new(@x+vect.x,@y+vect.y)
+ end
+
+ #returns a new vector equivalent to this-vect
+ #vect [Vector2d] vector to subtract to self
+ def sub vect
+ Vector2d.new(@x-vect.c, @y-vect.y)
+ end
+
+ #return the magnitude of the vector
+ def mag
+ ((@x**2)+(@y**2))**0.5
+ end
+
+ #returns a new normalize version of the vector
+ def normalize
+ Vector2d.new(@x/mag, @y/mag)
+ end
+
+ #TODO delet?
+ def distABS vect
+ (((vect.x-@x)**2+(vect.y-@y)**2)**0.5).abs()
+ end
+ end \ No newline at end of file
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/data/.gitkeep b/samples/04_physics_and_collisions/09_arbitrary_collision/data/.gitkeep
new file mode 100644
index 0000000..c1ffee3
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/data/.gitkeep
@@ -0,0 +1 @@
+Put level data and other txt files here. \ No newline at end of file
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/docs/LinearCollider.md b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/LinearCollider.md
new file mode 100644
index 0000000..41227a2
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/LinearCollider.md
@@ -0,0 +1,13 @@
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_1.png?raw=true" width="300" height="271">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_2.png?raw=true" width="300" height="271">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_3.png?raw=true" width="300" height="271">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_4.png?raw=true" width="634" height="72">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_5.png?raw=true" width="1226" height="92">
+<br/>
+<img src="https://github.com/DragonRuby/dragonruby-game-toolkit-physics/blob/master/docs/docImages/LinearCollider_6.png?raw=true" width="300" height="271">
+<br/>
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_1.png b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_1.png
new file mode 100644
index 0000000..75c8271
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_1.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_2.png b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_2.png
new file mode 100644
index 0000000..231a362
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_2.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_3.png b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_3.png
new file mode 100644
index 0000000..5e552d3
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_3.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_4.png b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_4.png
new file mode 100644
index 0000000..c1284a3
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_4.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_5.png b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_5.png
new file mode 100644
index 0000000..035282c
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_5.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_6.png b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_6.png
new file mode 100644
index 0000000..b1dadcb
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/docs/docImages/LinearCollider_6.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/fonts/.gitkeep b/samples/04_physics_and_collisions/09_arbitrary_collision/fonts/.gitkeep
new file mode 100644
index 0000000..a03e35e
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/fonts/.gitkeep
@@ -0,0 +1 @@
+Put your custom fonts here. \ No newline at end of file
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/metadata/game_metadata.txt b/samples/04_physics_and_collisions/09_arbitrary_collision/metadata/game_metadata.txt
new file mode 100644
index 0000000..ed35638
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/metadata/game_metadata.txt
@@ -0,0 +1,6 @@
+devid=canicvs
+devtitle=UTD Students
+gameid=physics-holedown-sample
+gametitle=Physics Holedown Sample App
+version=0.1
+icon=metadata/icon.png
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/metadata/icon.png b/samples/04_physics_and_collisions/09_arbitrary_collision/metadata/icon.png
new file mode 100644
index 0000000..e20e8c2
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/metadata/icon.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sounds/.gitkeep b/samples/04_physics_and_collisions/09_arbitrary_collision/sounds/.gitkeep
new file mode 100644
index 0000000..a99ec00
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sounds/.gitkeep
@@ -0,0 +1 @@
+Put your sounds here. \ No newline at end of file
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball.png
new file mode 100644
index 0000000..aec6fa4
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball10.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball10.png
new file mode 100644
index 0000000..a1826f4
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball10.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball15.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball15.png
new file mode 100644
index 0000000..b81d582
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/ball15.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/border-black.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/border-black.png
new file mode 100644
index 0000000..c9d0bad
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/border-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-black.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-black.png
new file mode 100644
index 0000000..c98e23d
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-blue.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-blue.png
new file mode 100644
index 0000000..1726d2a
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-gray.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-gray.png
new file mode 100644
index 0000000..960f191
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-green.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-green.png
new file mode 100644
index 0000000..43cf7ee
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-indigo.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-indigo.png
new file mode 100644
index 0000000..598e240
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-orange.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-orange.png
new file mode 100644
index 0000000..5604a42
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-red.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-red.png
new file mode 100644
index 0000000..7f17ca6
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-violet.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-violet.png
new file mode 100644
index 0000000..681d210
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-white.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-yellow.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-yellow.png
new file mode 100644
index 0000000..94992eb
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/circle-yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-0.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-0.png
new file mode 100644
index 0000000..fb179af
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-0.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-1.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-1.png
new file mode 100644
index 0000000..8cfe531
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-1.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-2.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-2.png
new file mode 100644
index 0000000..cb462e1
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-2.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-3.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-3.png
new file mode 100644
index 0000000..04c4977
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-3.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-4.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-4.png
new file mode 100644
index 0000000..b29fa3d
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-4.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-5.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-5.png
new file mode 100644
index 0000000..99f4e74
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/dragon-5.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-0.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-0.png
new file mode 100644
index 0000000..f48636f
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-0.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-1.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-1.png
new file mode 100644
index 0000000..b4018d9
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-1.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-2.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-2.png
new file mode 100644
index 0000000..3abaedd
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-2.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-3.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-3.png
new file mode 100644
index 0000000..fe94a5a
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-3.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-4.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-4.png
new file mode 100644
index 0000000..ed04237
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-4.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-5.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-5.png
new file mode 100644
index 0000000..2cd8f06
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-5.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-6.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-6.png
new file mode 100644
index 0000000..e55909c
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-6.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-sheet.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-sheet.png
new file mode 100644
index 0000000..8559a5c
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/explosion-sheet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/glowCircle.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/glowCircle.png
new file mode 100644
index 0000000..c8be83e
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/glowCircle.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-black.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-black.png
new file mode 100644
index 0000000..f50c872
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-blue.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-blue.png
new file mode 100644
index 0000000..1696bae
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-gray.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-gray.png
new file mode 100644
index 0000000..e8c4c5a
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-green.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-green.png
new file mode 100644
index 0000000..a700602
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-indigo.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-indigo.png
new file mode 100644
index 0000000..15f6f4f
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-orange.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-orange.png
new file mode 100644
index 0000000..1587173
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-red.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-red.png
new file mode 100644
index 0000000..d442f39
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-violet.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-violet.png
new file mode 100644
index 0000000..3be5731
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-white.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-white.png
new file mode 100644
index 0000000..c1ad970
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-yellow.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-yellow.png
new file mode 100644
index 0000000..63f5f34
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/hexagon-yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-black.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-black.png
new file mode 100644
index 0000000..fa9e463
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-blue.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-blue.png
new file mode 100644
index 0000000..a3d8524
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-gray.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-gray.png
new file mode 100644
index 0000000..85dcc1d
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-green.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-green.png
new file mode 100644
index 0000000..ec2773e
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-indigo.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-indigo.png
new file mode 100644
index 0000000..e6be50c
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-orange.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-orange.png
new file mode 100644
index 0000000..154d81c
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-red.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-red.png
new file mode 100644
index 0000000..3448c4d
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-violet.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-violet.png
new file mode 100644
index 0000000..f09bf21
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-white.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-white.png
new file mode 100644
index 0000000..a45793d
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-yellow.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-yellow.png
new file mode 100644
index 0000000..9be20c7
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/isometric-yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare.png
new file mode 100644
index 0000000..7890fc7
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small.png
new file mode 100644
index 0000000..b8d15e8
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small_black.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small_black.png
new file mode 100644
index 0000000..6d1e903
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small_black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small_white.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small_white.png
new file mode 100644
index 0000000..304e209
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_small_white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_white.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_white.png
new file mode 100644
index 0000000..00601c8
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/roundSquare_white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-black.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-black.png
new file mode 100644
index 0000000..cea7bd7
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-black.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-blue.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-blue.png
new file mode 100644
index 0000000..b840849
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-blue.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-gray.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-gray.png
new file mode 100644
index 0000000..2142b30
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-gray.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-green.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-green.png
new file mode 100644
index 0000000..5ef7f75
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-green.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-indigo.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-indigo.png
new file mode 100644
index 0000000..2384108
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-indigo.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-orange.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-orange.png
new file mode 100644
index 0000000..bb1eee7
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-orange.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-red.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-red.png
new file mode 100644
index 0000000..3ed5f13
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-red.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-violet.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-violet.png
new file mode 100644
index 0000000..333540c
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-violet.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-white-2.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-white-2.png
new file mode 100644
index 0000000..df3ff3a
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-white-2.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-white.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-white.png
new file mode 100644
index 0000000..378c565
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-white.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-yellow.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-yellow.png
new file mode 100644
index 0000000..0edeeec
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/square-yellow.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/star.png b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/star.png
new file mode 100644
index 0000000..e0ee0f9
--- /dev/null
+++ b/samples/04_physics_and_collisions/09_arbitrary_collision/sprites/star.png
Binary files differ
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb
new file mode 100644
index 0000000..bdfe153
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb
@@ -0,0 +1,31 @@
+class Ball
+ #TODO limit accessors?
+ attr_accessor :xy, :width, :height, :velocity
+
+
+ #@xy [Vector2d] x,y position
+ #@velocity [Vector2d] velocity of ball
+ def initialize
+ @xy = Vector2d.new(WIDTH/2,500)
+ @velocity = Vector2d.new(4,-4)
+ @width = 20
+ @height = 20
+ end
+
+ #move the ball according to its velocity
+ def update args
+ end
+
+ #render the ball to the screen
+ def render args
+ args.outputs.solids << [@xy.x,@xy.y,@width,@height,255,0,255];
+ #args.outputs.labels << [20,HEIGHT-50,"velocity: " [email protected]_s+","[email protected]_s + " magnitude:" + @velocity.mag.to_s]
+ end
+
+ def rect
+ [@xy.x,@xy.y,@width,@height]
+ end
+
+end
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb
new file mode 100644
index 0000000..69ada5b
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb
@@ -0,0 +1,166 @@
+#The LinearCollider (theoretically) produces collisions upon a line segment defined point.y two x,y cordinates
+
+class LinearCollider
+
+ #start [Array of length 2] start of the line segment as a x,y cordinate
+ #last [Array of length 2] end of the line segment as a x,y cordinate
+
+ #inorder for the LinearCollider to be functional the line segment must be said to have a thickness
+ #(as it is unlikly that a colliding object will land exactly on the linesegment)
+
+ #extension defines if the line's thickness extends negatively or positively
+ #extension :pos extends positively
+ #extension :neg extends negatively
+
+ #thickness [float] how thick the line should be (should always be atleast as large as the magnitude of the colliding object)
+ def initialize (pointA, pointB, extension=:neg, thickness=10)
+ @pointA = pointA
+ @pointB = pointB
+ @thickness = thickness
+ @extension = extension
+
+ @pointAExtended={
+ x: @pointA.x + @thickness*(@extension == :neg ? -1 : 1),
+ y: @pointA.y + @thickness*(@extension == :neg ? -1 : 1)
+ }
+ @pointBExtended={
+ x: @pointB.x + @thickness*(@extension == :neg ? -1 : 1),
+ y: @pointB.y + @thickness*(@extension == :neg ? -1 : 1)
+ }
+
+ end
+
+ def resetPoints(pointA,pointB)
+ @pointA = pointA
+ @pointB = pointB
+
+ @pointAExtended={
+ x:@pointA.x + @thickness*(@extension == :neg ? -1 : 1),
+ y:@pointA.y + @thickness*(@extension == :neg ? -1 : 1)
+ }
+ @pointBExtended={
+ x:@pointB.x + @thickness*(@extension == :neg ? -1 : 1),
+ y:@pointB.y + @thickness*(@extension == :neg ? -1 : 1)
+ }
+ end
+
+ #TODO: Ugly function
+ def slope (pointA, pointB)
+ return (pointB.x==pointA.x) ? INFINITY : (pointB.y+-pointA.y)/(pointB.x+-pointA.x)
+ end
+
+ #TODO: Ugly function
+ def intercept(pointA, pointB)
+ if (slope(pointA, pointB) == INFINITY)
+ -INFINITY
+ elsif slope(pointA, pointB) == -1*INFINITY
+ INFINITY
+ else
+ pointA.y+-1.0*(slope(pointA, pointB)*pointA.x)
+ end
+ end
+
+ def calcY(pointA, pointB, x)
+ return slope(pointA, pointB)*x + intercept(pointA, pointB)
+ end
+
+ #test if a collision has occurred
+ def isCollision? (point)
+ #INFINITY slop breaks down when trying to determin collision, ergo it requires a special test
+ if slope(@pointA, @pointB) == INFINITY &&
+ point.x >= [@pointA.x,@pointB.x].min+(@extension == :pos ? -@thickness : 0) &&
+ point.x <= [@pointA.x,@pointB.x].max+(@extension == :neg ? @thickness : 0) &&
+ point.y >= [@pointA.y,@pointB.y].min && point.y <= [@pointA.y,@pointB.y].max
+ return true
+ end
+
+ isNegInLine = @extension == :neg &&
+ point.y <= slope(@pointA, @pointB)*point.x+intercept(@pointA,@pointB) &&
+ point.y >= point.x*slope(@pointAExtended, @pointBExtended)+intercept(@pointAExtended,@pointBExtended)
+ isPosInLine = @extension == :pos &&
+ point.y >= slope(@pointA, @pointB)*point.x+intercept(@pointA,@pointB) &&
+ point.y <= point.x*slope(@pointAExtended, @pointBExtended)+intercept(@pointAExtended,@pointBExtended)
+ isInBoxBounds = point.x >= [@pointA.x,@pointB.x].min &&
+ point.x <= [@pointA.x,@pointB.x].max &&
+ point.y >= [@pointA.y,@pointB.y].min+(@extension == :neg ? -@thickness : 0) &&
+ point.y <= [@pointA.y,@pointB.y].max+(@extension == :pos ? @thickness : 0)
+
+ return isInBoxBounds && (isNegInLine || isPosInLine)
+
+ end
+
+ def getRepelMagnitude (fbx, fby, vrx, vry, args)
+ a = fbx ; b = vrx ; c = fby
+ d = vry ; e = args.state.ball.velocity.mag
+
+ if b**2 + d**2 == 0
+ puts "magnitude error"
+ end
+
+ x1 = (-a*b+-c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 + d**2 - a**2 * d**2)**0.5)/(b**2 + d**2)
+ x2 = -((a*b + c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 * d**2 - a**2 * d**2)**0.5)/(b**2 + d**2))
+ return ((a+x1*b)**2 + (c+x1*d)**2 == e**2) ? x1 : x2
+ end
+
+ def update args
+ #each of the four points on the square ball - NOTE simple to extend to a circle
+ points= [ {x: args.state.ball.xy.x, y: args.state.ball.xy.y},
+ {x: args.state.ball.xy.x+args.state.ball.width, y: args.state.ball.xy.y},
+ {x: args.state.ball.xy.x, y: args.state.ball.xy.y+args.state.ball.height},
+ {x: args.state.ball.xy.x+args.state.ball.width, y: args.state.ball.xy.y + args.state.ball.height}
+ ]
+
+ #for each point p in points
+ for point in points
+ #isCollision.md has more information on this section
+ #TODO: section can certainly be simplifyed
+ if isCollision?(point)
+ u = Vector2d.new(1.0,((slope(@pointA, @pointB)==0) ? INFINITY : -1/slope(@pointA, @pointB))*1.0).normalize #normal perpendicular (to line segment) vector
+
+ #the vector with the repeling force can be u or -u depending of where the ball was coming from in relation to the line segment
+ previousBallPosition=Vector2d.new(point.x-args.state.ball.velocity.x,point.y-args.state.ball.velocity.y)
+ choiceA = (u.mult(1))
+ choiceB = (u.mult(-1))
+ vectorRepel = nil
+
+ if (slope(@pointA, @pointB))!=INFINITY && u.y < 0
+ choiceA, choiceB = choiceB, choiceA
+ end
+ vectorRepel = (previousBallPosition.y > calcY(@pointA, @pointB, previousBallPosition.x)) ? choiceA : choiceB
+
+ #vectorRepel = (previousBallPosition.y > slope(@pointA, @pointB)*previousBallPosition.x+intercept(@pointA,@pointB)) ? choiceA : choiceB)
+ if (slope(@pointA, @pointB) == INFINITY) #slope INFINITY breaks down in the above test, ergo it requires a custom test
+ vectorRepel = (previousBallPosition.x > @pointA.x) ? (u.mult(1)) : (u.mult(-1))
+ end
+ #puts (" " + $t[0].to_s + "," + $t[1].to_s + " " + $t[2].to_s + "," + $t[3].to_s + " " + " " + u.x.to_s + "," + u.y.to_s)
+ #vectorRepel now has the repeling force
+
+ mag = args.state.ball.velocity.mag
+ theta_ball=Math.atan2(args.state.ball.velocity.y,args.state.ball.velocity.x) #the angle of the ball's velocity
+ theta_repel=Math.atan2(vectorRepel.y,vectorRepel.x) #the angle of the repeling force
+ #puts ("theta:" + theta_ball.to_s + " " + theta_repel.to_s) #theta okay
+
+ fbx = mag * Math.cos(theta_ball) #the x component of the ball's velocity
+ fby = mag * Math.sin(theta_ball) #the y component of the ball's velocity
+
+ repelMag = getRepelMagnitude(fbx, fby, vectorRepel.x, vectorRepel.y, args)
+
+ frx = repelMag* Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx
+ fry = repelMag* Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby
+
+ fsumx = fbx+frx #sum of x forces
+ fsumy = fby+fry #sum of y forces
+ fr = mag#fr is the resulting magnitude
+ thetaNew = Math.atan2(fsumy, fsumx) #thetaNew is the resulting angle
+ xnew = fr*Math.cos(thetaNew) #resulting x velocity
+ ynew = fr*Math.sin(thetaNew) #resulting y velocity
+
+ args.state.ball.velocity = Vector2d.new(xnew,ynew)
+ #args.state.ball.xy.add(args.state.ball.velocity)
+ break #no need to check the other points ?
+ else
+ end
+ end
+ end #end update
+
+end
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb
new file mode 100644
index 0000000..adde51f
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb
@@ -0,0 +1,189 @@
+# coding: utf-8
+INFINITY= 10**10
+WIDTH=1280
+HEIGHT=720
+
+require 'app/vector2d.rb'
+require 'app/paddle.rb'
+require 'app/ball.rb'
+require 'app/linear_collider.rb'
+
+#Method to init default values
+def defaults args
+ args.state.game_board ||= [(args.grid.w / 2 - args.grid.w / 4), 0, (args.grid.w / 2), args.grid.h]
+ args.state.bricks ||= []
+ args.state.num_bricks ||= 0
+ args.state.game_over_at ||= 0
+ args.state.paddle ||= Paddle.new
+ args.state.ball ||= Ball.new
+ args.state.westWall ||= LinearCollider.new({x: args.grid.w/4, y: 0}, {x: args.grid.w/4, y: args.grid.h}, :pos)
+ args.state.eastWall ||= LinearCollider.new({x: 3*args.grid.w*0.25, y: 0}, {x: 3*args.grid.w*0.25, y: args.grid.h})
+ args.state.southWall ||= LinearCollider.new({x: 0, y: 0}, {x: args.grid.w, y: 0})
+ args.state.northWall ||= LinearCollider.new({x: 0, y:args.grid.h}, {x: args.grid.w, y: args.grid.h}, :pos)
+
+ #args.state.testWall ||= LinearCollider.new({x:0 , y:0},{x:args.grid.w, y:args.grid.h})
+end
+
+#Render loop
+def render args
+ render_instructions args
+ render_board args
+ render_bricks args
+end
+
+begin :render_methods
+ #Method to display the instructions of the game
+ def render_instructions args
+ args.outputs.labels << [225, args.grid.h - 30, "← and → to move the paddle left and right", 0, 1]
+ end
+
+ def render_board args
+ args.outputs.borders << args.state.game_board
+ end
+
+ def render_bricks args
+ args.outputs.solids << args.state.bricks.map(&:rect)
+ end
+end
+
+#Calls all methods necessary for performing calculations
+def calc args
+ add_new_bricks args
+ reset_game args
+ calc_collision args
+ win_game args
+
+ args.state.westWall.update args
+ args.state.eastWall.update args
+ args.state.southWall.update args
+ args.state.northWall.update args
+ args.state.paddle.update args
+ args.state.ball.update args
+
+ #args.state.testWall.update args
+
+ args.state.paddle.render args
+ args.state.ball.render args
+end
+
+begin :calc_methods
+ def add_new_bricks args
+ return if args.state.num_bricks > 40
+
+ #Width of the game board is 640px
+ brick_width = (args.grid.w / 2) / 10
+ brick_height = brick_width / 2
+
+ (4).map_with_index do |y|
+ #Make a box that is 10 bricks wide and 4 bricks tall
+ args.state.bricks += (10).map_with_index do |x|
+ args.state.new_entity(:brick) do |b|
+ b.x = x * brick_width + (args.grid.w / 2 - args.grid.w / 4)
+ b.y = args.grid.h - ((y + 1) * brick_height)
+ b.rect = [b.x + 1, b.y - 1, brick_width - 2, brick_height - 2, 235, 50 * y, 52]
+
+ #Add linear colliders to the brick
+ b.collider_bottom = LinearCollider.new([(b.x-2), (b.y-5)], [(b.x+brick_width+1), (b.y-5)], :pos, brick_height)
+ b.collider_right = LinearCollider.new([(b.x+brick_width+1), (b.y-5)], [(b.x+brick_width+1), (b.y+brick_height+1)], :pos)
+ b.collider_left = LinearCollider.new([(b.x-2), (b.y-5)], [(b.x-2), (b.y+brick_height+1)], :neg)
+ b.collider_top = LinearCollider.new([(b.x-2), (b.y+brick_height+1)], [(b.x+brick_width+1), (b.y+brick_height+1)], :neg)
+
+ # @xyCollision = LinearCollider.new({x: @x,y: @y+@height}, {x: @x+@width, y: @y+@height})
+ # @xyCollision2 = LinearCollider.new({x: @x,y: @y}, {x: @x+@width, y: @y}, :pos)
+ # @xyCollision3 = LinearCollider.new({x: @x,y: @y}, {x: @x, y: @y+@height})
+ # @xyCollision4 = LinearCollider.new({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height}, :pos)
+
+ b.broken = false
+
+ args.state.num_bricks += 1
+ end
+ end
+ end
+ end
+
+ def reset_game args
+ if args.state.ball.xy.y < 20 && args.state.game_over_at.elapsed_time > 60
+ #Freeze the ball
+ args.state.ball.velocity.x = 0
+ args.state.ball.velocity.y = 0
+ #Freeze the paddle
+ args.state.paddle.enabled = false
+
+ args.state.game_over_at = args.state.tick_count
+ end
+
+ if args.state.game_over_at.elapsed_time < 60 && args.state.tick_count > 60 && args.state.bricks.count != 0
+ #Display a "Game over" message
+ args.outputs.labels << [100, 100, "GAME OVER", 10]
+ end
+
+ #If 60 frames have passed since the game ended, restart the game
+ if args.state.game_over_at != 0 && args.state.game_over_at.elapsed_time == 60
+ # FIXME: only put value types in state
+ args.state.ball = Ball.new
+
+ # FIXME: only put value types in state
+ args.state.paddle = Paddle.new
+
+ args.state.bricks = []
+ args.state.num_bricks = 0
+ end
+ end
+
+ def calc_collision args
+ #Remove the brick if it is hit with the ball
+ ball = args.state.ball
+ ball_rect = [ball.xy.x, ball.xy.y, 20, 20]
+
+ #Loop through each brick to see if the ball is colliding with it
+ args.state.bricks.each do |b|
+ if b.rect.intersect_rect?(ball_rect)
+ #Run the linear collider for the brick if there is a collision
+ b[:collider_bottom].update args
+ b[:collider_right].update args
+ b[:collider_left].update args
+ b[:collider_top].update args
+
+ b.broken = true
+ end
+ end
+
+ args.state.bricks = args.state.bricks.reject(&:broken)
+ end
+
+ def win_game args
+ if args.state.bricks.count == 0 && args.state.game_over_at.elapsed_time > 60
+ #Freeze the ball
+ args.state.ball.velocity.x = 0
+ args.state.ball.velocity.y = 0
+ #Freeze the paddle
+ args.state.paddle.enabled = false
+
+ args.state.game_over_at = args.state.tick_count
+ end
+
+ if args.state.game_over_at.elapsed_time < 60 && args.state.tick_count > 60 && args.state.bricks.count == 0
+ #Display a "Game over" message
+ args.outputs.labels << [100, 100, "CONGRATULATIONS!", 10]
+ end
+ end
+
+end
+
+def tick args
+ defaults args
+ render args
+ calc args
+
+ #args.outputs.lines << [0, 0, args.grid.w, args.grid.h]
+
+ #$tc+=1
+ #if $tc == 5
+ #$train << [args.state.ball.xy.x, args.state.ball.xy.y]
+ #$tc = 0
+ #end
+ #for t in $train
+
+ #args.outputs.solids << [t[0],t[1],5,5,255,0,0];
+ #end
+end
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb
new file mode 100644
index 0000000..a4fe710
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb
@@ -0,0 +1,53 @@
+class Paddle
+ attr_accessor :enabled
+
+ def initialize ()
+ @x=WIDTH/2
+ @y=100
+ @width=100
+ @height=20
+ @speed=10
+
+ @xyCollision = LinearCollider.new({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5})
+ @xyCollision2 = LinearCollider.new({x: @x,y: @y}, {x: @x+@width, y: @y}, :pos)
+ @xyCollision3 = LinearCollider.new({x: @x,y: @y}, {x: @x, y: @y+@height+5})
+ @xyCollision4 = LinearCollider.new({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5}, :pos)
+
+ @enabled = true
+ end
+
+ def update args
+ @xyCollision.resetPoints({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5})
+ @xyCollision2.resetPoints({x: @x,y: @y}, {x: @x+@width, y: @y})
+ @xyCollision3.resetPoints({x: @x,y: @y}, {x: @x, y: @y+@height+5})
+ @xyCollision4.resetPoints({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5})
+
+ @xyCollision.update args
+ @xyCollision2.update args
+ @xyCollision3.update args
+ @xyCollision4.update args
+
+ args.inputs.keyboard.key_held.left ||= false
+ args.inputs.keyboard.key_held.right ||= false
+
+ if not (args.inputs.keyboard.key_held.left == args.inputs.keyboard.key_held.right)
+ if args.inputs.keyboard.key_held.left && @enabled
+ @x-=@speed
+ elsif args.inputs.keyboard.key_held.right && @enabled
+ @x+=@speed
+ end
+ end
+
+ xmin =WIDTH/4
+ xmax = 3*(WIDTH/4)
+ @x = (@x+@width > xmax) ? xmax-@width : (@x<xmin) ? xmin : @x;
+ end
+
+ def render args
+ args.outputs.solids << [@x,@y,@width,@height,255,0,0];
+ end
+
+ def rect
+ [@x, @y, @width, @height]
+ end
+end
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb
new file mode 100644
index 0000000..db71ff6
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb
@@ -0,0 +1,29 @@
+# For advanced users:
+# You can put some quick verification tests here, any method
+# that starts with the `test_` will be run when you save this file.
+
+# Here is an example test and game
+
+# To run the test: ./dragonruby mygame --eval app/tests.rb --no-tick
+
+class MySuperHappyFunGame
+ attr_gtk
+
+ def tick
+ outputs.solids << [100, 100, 300, 300]
+ end
+end
+
+def test_universe args, assert
+ game = MySuperHappyFunGame.new
+ game.args = args
+ game.tick
+ assert.true! args.outputs.solids.length == 1, "failure: a solid was not added after tick"
+ assert.false! 1 == 2, "failure: some how, 1 equals 2, the world is ending"
+ puts "test_universe completed successfully"
+end
+
+puts "running tests"
+$gtk.reset 100
+$gtk.log_level = :off
+$gtk.tests.start
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb
new file mode 100644
index 0000000..97cf286
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb
@@ -0,0 +1,50 @@
+
+class Vector2d
+ attr_accessor :x, :y
+
+ def initialize x=0, y=0
+ @x=x
+ @y=y
+ end
+
+ #returns a vector multiplied by scalar x
+ #x [float] scalar
+ def mult x
+ r = Vector2d.new(0,0)
+ r.x=@x*x
+ r.y=@y*x
+ r
+ end
+
+ # vect [Vector2d] vector to copy
+ def copy vect
+ Vector2d.new(@x, @y)
+ end
+
+ #returns a new vector equivalent to this+vect
+ #vect [Vector2d] vector to add to self
+ def add vect
+ Vector2d.new(@x+vect.x,@y+vect.y)
+ end
+
+ #returns a new vector equivalent to this-vect
+ #vect [Vector2d] vector to subtract to self
+ def sub vect
+ Vector2d.new(@x-vect.c, @y-vect.y)
+ end
+
+ #return the magnitude of the vector
+ def mag
+ ((@x**2)+(@y**2))**0.5
+ end
+
+ #returns a new normalize version of the vector
+ def normalize
+ Vector2d.new(@x/mag, @y/mag)
+ end
+
+ #TODO delet?
+ def distABS vect
+ (((vect.x-@x)**2+(vect.y-@y)**2)**0.5).abs()
+ end
+end
diff --git a/samples/07_advanced_rendering/07_splitscreen_camera/app/main.rb b/samples/07_advanced_rendering/07_splitscreen_camera/app/main.rb
new file mode 100644
index 0000000..9b08e1e
--- /dev/null
+++ b/samples/07_advanced_rendering/07_splitscreen_camera/app/main.rb
@@ -0,0 +1,395 @@
+class CameraMovement
+ attr_accessor :state, :inputs, :outputs, :grid
+
+ #==============================================================================================
+ #Serialize
+ def serialize
+ {state: state, inputs: inputs, outputs: outputs, grid: grid }
+ end
+
+ def inspect
+ serialize.to_s
+ end
+
+ def to_s
+ serialize.to_s
+ end
+
+ #==============================================================================================
+ #Tick
+ def tick
+ defaults
+ calc
+ render
+ input
+ end
+
+ #==============================================================================================
+ #Default functions
+ def defaults
+ outputs[:scene].background_color = [0,0,0]
+ state.trauma ||= 0.0
+ state.trauma_power ||= 2
+ state.player_cyan ||= new_player_cyan
+ state.player_magenta ||= new_player_magenta
+ state.camera_magenta ||= new_camera_magenta
+ state.camera_cyan ||= new_camera_cyan
+ state.camera_center ||= new_camera_center
+ state.room ||= new_room
+ end
+
+ def default_player x, y, w, h, sprite_path
+ state.new_entity(:player,
+ { x: x,
+ y: y,
+ dy: 0,
+ dx: 0,
+ w: w,
+ h: h,
+ damage: 0,
+ dead: false,
+ orientation: "down",
+ max_alpha: 255,
+ sprite_path: sprite_path})
+ end
+
+ def default_floor_tile x, y, w, h, sprite_path
+ state.new_entity(:room,
+ { x: x,
+ y: y,
+ w: w,
+ h: h,
+ sprite_path: sprite_path})
+ end
+
+ def default_camera x, y, w, h
+ state.new_entity(:camera,
+ { x: x,
+ y: y,
+ dx: 0,
+ dy: 0,
+ w: w,
+ h: h})
+ end
+
+ def new_player_cyan
+ default_player(0, 0, 64, 64,
+ "sprites/player/player_#{state.player_cyan.orientation}_standing.png")
+ end
+
+ def new_player_magenta
+ default_player(64, 0, 64, 64,
+ "sprites/player/player_#{state.player_magenta.orientation}_standing.png")
+ end
+
+ def new_camera_magenta
+ default_camera(0,0,720,720)
+ end
+
+ def new_camera_cyan
+ default_camera(0,0,720,720)
+ end
+
+ def new_camera_center
+ default_camera(0,0,1280,720)
+ end
+
+ def new_room
+ default_floor_tile(0,0,1024,1024,'sprites/rooms/camera_room.png')
+ end
+
+ #==============================================================================================
+ #Calculation functions
+ def calc
+ calc_camera_magenta
+ calc_camera_cyan
+ calc_camera_center
+ calc_player_cyan
+ calc_player_magenta
+ calc_trauma_decay
+ end
+
+ def center_camera_tolerance
+ return Math.sqrt(((state.player_magenta.x - state.player_cyan.x) ** 2) +
+ ((state.player_magenta.y - state.player_cyan.y) ** 2)) > 640
+ end
+
+ def calc_player_cyan
+ state.player_cyan.x += state.player_cyan.dx
+ state.player_cyan.y += state.player_cyan.dy
+ end
+
+ def calc_player_magenta
+ state.player_magenta.x += state.player_magenta.dx
+ state.player_magenta.y += state.player_magenta.dy
+ end
+
+ def calc_camera_center
+ timeScale = 1
+ midX = (state.player_magenta.x + state.player_cyan.x)/2
+ midY = (state.player_magenta.y + state.player_cyan.y)/2
+ targetX = midX - state.camera_center.w/2
+ targetY = midY - state.camera_center.h/2
+ state.camera_center.x += (targetX - state.camera_center.x) * 0.1 * timeScale
+ state.camera_center.y += (targetY - state.camera_center.y) * 0.1 * timeScale
+ end
+
+
+ def calc_camera_magenta
+ timeScale = 1
+ targetX = state.player_magenta.x + state.player_magenta.w - state.camera_magenta.w/2
+ targetY = state.player_magenta.y + state.player_magenta.h - state.camera_magenta.h/2
+ state.camera_magenta.x += (targetX - state.camera_magenta.x) * 0.1 * timeScale
+ state.camera_magenta.y += (targetY - state.camera_magenta.y) * 0.1 * timeScale
+ end
+
+ def calc_camera_cyan
+ timeScale = 1
+ targetX = state.player_cyan.x + state.player_cyan.w - state.camera_cyan.w/2
+ targetY = state.player_cyan.y + state.player_cyan.h - state.camera_cyan.h/2
+ state.camera_cyan.x += (targetX - state.camera_cyan.x) * 0.1 * timeScale
+ state.camera_cyan.y += (targetY - state.camera_cyan.y) * 0.1 * timeScale
+ end
+
+ def calc_player_quadrant angle
+ if angle < 45 and angle > -45 and state.player_cyan.x < state.player_magenta.x
+ return 1
+ elsif angle < 45 and angle > -45 and state.player_cyan.x > state.player_magenta.x
+ return 3
+ elsif (angle > 45 or angle < -45) and state.player_cyan.y < state.player_magenta.y
+ return 2
+ elsif (angle > 45 or angle < -45) and state.player_cyan.y > state.player_magenta.y
+ return 4
+ end
+ end
+
+ def calc_camera_shake
+ state.trauma
+ end
+
+ def calc_trauma_decay
+ state.trauma = state.trauma * 0.9
+ end
+
+ def calc_random_float_range(min, max)
+ rand * (max-min) + min
+ end
+
+ #==============================================================================================
+ #Render Functions
+ def render
+ render_floor
+ render_player_cyan
+ render_player_magenta
+ if center_camera_tolerance
+ render_split_camera_scene
+ else
+ render_camera_center_scene
+ end
+ end
+
+ def render_player_cyan
+ outputs[:scene].sprites << {x: state.player_cyan.x,
+ y: state.player_cyan.y,
+ w: state.player_cyan.w,
+ h: state.player_cyan.h,
+ path: "sprites/player/player_#{state.player_cyan.orientation}_standing.png",
+ r: 0,
+ g: 255,
+ b: 255}
+ end
+
+ def render_player_magenta
+ outputs[:scene].sprites << {x: state.player_magenta.x,
+ y: state.player_magenta.y,
+ w: state.player_magenta.w,
+ h: state.player_magenta.h,
+ path: "sprites/player/player_#{state.player_magenta.orientation}_standing.png",
+ r: 255,
+ g: 0,
+ b: 255}
+ end
+
+ def render_floor
+ outputs[:scene].sprites << [state.room.x, state.room.y,
+ state.room.w, state.room.h,
+ state.room.sprite_path]
+ end
+
+ def render_camera_center_scene
+ zoomFactor = 1
+ outputs[:scene].width = state.room.w
+ outputs[:scene].height = state.room.h
+
+ maxAngle = 10.0
+ maxOffset = 20.0
+ angle = maxAngle * calc_camera_shake * calc_random_float_range(-1,1)
+ offsetX = 32 - (maxOffset * calc_camera_shake * calc_random_float_range(-1,1))
+ offsetY = 32 - (maxOffset * calc_camera_shake * calc_random_float_range(-1,1))
+
+ outputs.sprites << {x: (-state.camera_center.x - offsetX)/zoomFactor,
+ y: (-state.camera_center.y - offsetY)/zoomFactor,
+ w: outputs[:scene].width/zoomFactor,
+ h: outputs[:scene].height/zoomFactor,
+ path: :scene,
+ angle: angle,
+ source_w: -1,
+ source_h: -1}
+ outputs.labels << [128,64,"#{state.trauma.round(1)}",8,2,255,0,255,255]
+ end
+
+ def render_split_camera_scene
+ outputs[:scene].width = state.room.w
+ outputs[:scene].height = state.room.h
+ render_camera_magenta_scene
+ render_camera_cyan_scene
+
+ angle = Math.atan((state.player_magenta.y - state.player_cyan.y)/(state.player_magenta.x- state.player_cyan.x)) * 180/Math::PI
+ output_split_camera angle
+
+ end
+
+ def render_camera_magenta_scene
+ zoomFactor = 1
+ offsetX = 32
+ offsetY = 32
+
+ outputs[:scene_magenta].sprites << {x: (-state.camera_magenta.x*2),
+ y: (-state.camera_magenta.y),
+ w: outputs[:scene].width*2,
+ h: outputs[:scene].height,
+ path: :scene}
+
+ end
+
+ def render_camera_cyan_scene
+ zoomFactor = 1
+ offsetX = 32
+ offsetY = 32
+ outputs[:scene_cyan].sprites << {x: (-state.camera_cyan.x*2),
+ y: (-state.camera_cyan.y),
+ w: outputs[:scene].width*2,
+ h: outputs[:scene].height,
+ path: :scene}
+ end
+
+ def output_split_camera angle
+ #TODO: Clean this up!
+ quadrant = calc_player_quadrant angle
+ outputs.labels << [128,64,"#{quadrant}",8,2,255,0,255,255]
+ if quadrant == 1
+ set_camera_attributes(w: 640, h: 720, m_x: 640, m_y: 0, c_x: 0, c_y: 0)
+
+ elsif quadrant == 2
+ set_camera_attributes(w: 1280, h: 360, m_x: 0, m_y: 360, c_x: 0, c_y: 0)
+
+ elsif quadrant == 3
+ set_camera_attributes(w: 640, h: 720, m_x: 0, m_y: 0, c_x: 640, c_y: 0)
+
+ elsif quadrant == 4
+ set_camera_attributes(w: 1280, h: 360, m_x: 0, m_y: 0, c_x: 0, c_y: 360)
+
+ end
+ end
+
+ def set_camera_attributes(w: 0, h: 0, m_x: 0, m_y: 0, c_x: 0, c_y: 0)
+ state.camera_cyan.w = w + 64
+ state.camera_cyan.h = h + 64
+ outputs[:scene_cyan].width = (w) * 2
+ outputs[:scene_cyan].height = h
+
+ state.camera_magenta.w = w + 64
+ state.camera_magenta.h = h + 64
+ outputs[:scene_magenta].width = (w) * 2
+ outputs[:scene_magenta].height = h
+ outputs.sprites << {x: m_x,
+ y: m_y,
+ w: w,
+ h: h,
+ path: :scene_magenta}
+ outputs.sprites << {x: c_x,
+ y: c_y,
+ w: w,
+ h: h,
+ path: :scene_cyan}
+ end
+
+ def add_trauma amount
+ state.trauma = [state.trauma + amount, 1.0].min
+ end
+
+ def remove_trauma amount
+ state.trauma = [state.trauma - amount, 0.0].max
+ end
+ #==============================================================================================
+ #Input functions
+ def input
+ input_move_cyan
+ input_move_magenta
+
+ if inputs.keyboard.key_down.t
+ add_trauma(0.5)
+ elsif inputs.keyboard.key_down.y
+ remove_trauma(0.1)
+ end
+ end
+
+ def input_move_cyan
+ if inputs.keyboard.key_held.up
+ state.player_cyan.dy = 5
+ state.player_cyan.orientation = "up"
+ elsif inputs.keyboard.key_held.down
+ state.player_cyan.dy = -5
+ state.player_cyan.orientation = "down"
+ else
+ state.player_cyan.dy *= 0.8
+ end
+ if inputs.keyboard.key_held.left
+ state.player_cyan.dx = -5
+ state.player_cyan.orientation = "left"
+ elsif inputs.keyboard.key_held.right
+ state.player_cyan.dx = 5
+ state.player_cyan.orientation = "right"
+ else
+ state.player_cyan.dx *= 0.8
+ end
+
+ outputs.labels << [128,512,"#{state.player_cyan.x.round()}",8,2,0,255,255,255]
+ outputs.labels << [128,480,"#{state.player_cyan.y.round()}",8,2,0,255,255,255]
+ end
+
+ def input_move_magenta
+ if inputs.keyboard.key_held.w
+ state.player_magenta.dy = 5
+ state.player_magenta.orientation = "up"
+ elsif inputs.keyboard.key_held.s
+ state.player_magenta.dy = -5
+ state.player_magenta.orientation = "down"
+ else
+ state.player_magenta.dy *= 0.8
+ end
+ if inputs.keyboard.key_held.a
+ state.player_magenta.dx = -5
+ state.player_magenta.orientation = "left"
+ elsif inputs.keyboard.key_held.d
+ state.player_magenta.dx = 5
+ state.player_magenta.orientation = "right"
+ else
+ state.player_magenta.dx *= 0.8
+ end
+
+ outputs.labels << [128,360,"#{state.player_magenta.x.round()}",8,2,255,0,255,255]
+ outputs.labels << [128,328,"#{state.player_magenta.y.round()}",8,2,255,0,255,255]
+ end
+end
+
+$camera_movement = CameraMovement.new
+
+def tick args
+ args.outputs.background_color = [0,0,0]
+ $camera_movement.inputs = args.inputs
+ $camera_movement.outputs = args.outputs
+ $camera_movement.state = args.state
+ $camera_movement.grid = args.grid
+ $camera_movement.tick
+end
diff --git a/samples/07_advanced_rendering/07_splitscreen_camera/run.bat b/samples/07_advanced_rendering/07_splitscreen_camera/run.bat
new file mode 100644
index 0000000..08e7ed8
--- /dev/null
+++ b/samples/07_advanced_rendering/07_splitscreen_camera/run.bat
@@ -0,0 +1,6 @@
+cd /d %~dp0
+
+cd ..
+cd ..
+cd ..
+dragonruby samples/99_camera/splitscreen
diff --git a/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_down_standing.png b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_down_standing.png
new file mode 100644
index 0000000..9e27489
--- /dev/null
+++ b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_down_standing.png
Binary files differ
diff --git a/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_left_standing.png b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_left_standing.png
new file mode 100644
index 0000000..2fea35b
--- /dev/null
+++ b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_left_standing.png
Binary files differ
diff --git a/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_right_standing.png b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_right_standing.png
new file mode 100644
index 0000000..6f3402e
--- /dev/null
+++ b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_right_standing.png
Binary files differ
diff --git a/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_up_standing.png b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_up_standing.png
new file mode 100644
index 0000000..9ebdca7
--- /dev/null
+++ b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/player/player_up_standing.png
Binary files differ
diff --git a/samples/07_advanced_rendering/07_splitscreen_camera/sprites/rooms/camera_room.png b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/rooms/camera_room.png
new file mode 100644
index 0000000..e0ca375
--- /dev/null
+++ b/samples/07_advanced_rendering/07_splitscreen_camera/sprites/rooms/camera_room.png
Binary files differ
diff --git a/samples/10_advanced_debugging/03_unit_tests/exception_raising_tests.rb b/samples/10_advanced_debugging/03_unit_tests/exception_raising_tests.rb
index 57efdb2..b7e1223 100644
--- a/samples/10_advanced_debugging/03_unit_tests/exception_raising_tests.rb
+++ b/samples/10_advanced_debugging/03_unit_tests/exception_raising_tests.rb
@@ -15,7 +15,5 @@ def test_exception_in_newing_object args, assert
end
end
-puts "running tests"
$gtk.reset 100
$gtk.log_level = :off
-$gtk.tests.start
diff --git a/samples/10_advanced_debugging/03_unit_tests/gen_docs.rb b/samples/10_advanced_debugging/03_unit_tests/gen_docs.rb
index 0e41326..a627653 100644
--- a/samples/10_advanced_debugging/03_unit_tests/gen_docs.rb
+++ b/samples/10_advanced_debugging/03_unit_tests/gen_docs.rb
@@ -1,2 +1,2 @@
-# sh ./amir-build-and-run.sh --eval samples/99_zz_gtk_unit_tests/gen_docs.rb --no-tick
+# ./dragonruby mygame --eval samples/99_zz_gtk_unit_tests/gen_docs.rb --no-tick
Kernel.export_docs!
diff --git a/samples/10_advanced_debugging/03_unit_tests/geometry_tests.rb b/samples/10_advanced_debugging/03_unit_tests/geometry_tests.rb
index d823d78..fb61af3 100644
--- a/samples/10_advanced_debugging/03_unit_tests/geometry_tests.rb
+++ b/samples/10_advanced_debugging/03_unit_tests/geometry_tests.rb
@@ -110,7 +110,5 @@ begin :scale_rect
end
end
-puts "running tests"
$gtk.reset 100
$gtk.log_level = :off
-$gtk.tests.start
diff --git a/samples/10_advanced_debugging/03_unit_tests/http_tests.rb b/samples/10_advanced_debugging/03_unit_tests/http_tests.rb
index 1132f85..01a22b3 100644
--- a/samples/10_advanced_debugging/03_unit_tests/http_tests.rb
+++ b/samples/10_advanced_debugging/03_unit_tests/http_tests.rb
@@ -18,7 +18,5 @@ def test_http args, assert
try_assert_or_schedule args, assert
end
-puts "running tests"
$gtk.reset 100
$gtk.log_level = :off
-$gtk.tests.start
diff --git a/samples/10_advanced_debugging/03_unit_tests/object_to_primitive_tests.rb b/samples/10_advanced_debugging/03_unit_tests/object_to_primitive_tests.rb
index 4686c6e..8ba2654 100644
--- a/samples/10_advanced_debugging/03_unit_tests/object_to_primitive_tests.rb
+++ b/samples/10_advanced_debugging/03_unit_tests/object_to_primitive_tests.rb
@@ -14,4 +14,3 @@ end
$gtk.reset 100
$gtk.log_level = :off
-$gtk.tests.start
diff --git a/samples/10_advanced_debugging/03_unit_tests/parsing_tests.rb b/samples/10_advanced_debugging/03_unit_tests/parsing_tests.rb
index 4dede2b..7fa1591 100644
--- a/samples/10_advanced_debugging/03_unit_tests/parsing_tests.rb
+++ b/samples/10_advanced_debugging/03_unit_tests/parsing_tests.rb
@@ -23,7 +23,5 @@ S
assert.equal! result, expected, "Parsing xml failed."
end
-puts "running tests"
$gtk.reset 100
$gtk.log_level = :off
-$gtk.tests.start
diff --git a/samples/10_advanced_debugging/03_unit_tests/require_tests.rb b/samples/10_advanced_debugging/03_unit_tests/require_tests.rb
new file mode 100644
index 0000000..7ebfc3a
--- /dev/null
+++ b/samples/10_advanced_debugging/03_unit_tests/require_tests.rb
@@ -0,0 +1,38 @@
+def write_src path, src
+ $gtk.write_file path, src
+end
+
+write_src 'app/unit_testing_game.rb', <<-S
+module UnitTesting
+ class Game
+ end
+end
+S
+
+write_src 'lib/unit_testing_lib.rb', <<-S
+module UnitTesting
+ class Lib
+ end
+end
+S
+
+write_src 'app/nested/unit_testing_nested.rb', <<-S
+module UnitTesting
+ class Nested
+ end
+end
+S
+
+require 'app/unit_testing_game.rb'
+require 'app/nested/unit_testing_nested.rb'
+require 'lib/unit_testing_lib.rb'
+
+def test_require args, assert
+ UnitTesting::Game.new
+ UnitTesting::Lib.new
+ UnitTesting::Nested.new
+ $gtk.exec 'rm ./mygame/app/unit_testing_game.rb'
+ $gtk.exec 'rm ./mygame/app/nested/unit_testing_nested.rb'
+ $gtk.exec 'rm ./mygame/lib/unit_testing_lib.rb'
+ assert.ok!
+end
diff --git a/samples/10_advanced_debugging/03_unit_tests/run-tests.sh b/samples/10_advanced_debugging/03_unit_tests/run-tests.sh
new file mode 100644
index 0000000..e551edf
--- /dev/null
+++ b/samples/10_advanced_debugging/03_unit_tests/run-tests.sh
@@ -0,0 +1,10 @@
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/require_tests.rb
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/gen_docs.rb
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/geometry_tests.rb
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/http_tests.rb
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/object_to_primitive_tests.rb
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/parsing_tests.rb
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/require_tests.rb
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/serialize_deserialize_tests.rb
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/state_serialization_experimental_tests.rb
+./dragonruby mygame --test samples/10_advanced_debugging/03_unit_tests/suggest_autocompletion_tests.rb
diff --git a/samples/10_advanced_debugging/03_unit_tests/serialize_deserialize_tests.rb b/samples/10_advanced_debugging/03_unit_tests/serialize_deserialize_tests.rb
index e67b1ee..8505bdf 100644
--- a/samples/10_advanced_debugging/03_unit_tests/serialize_deserialize_tests.rb
+++ b/samples/10_advanced_debugging/03_unit_tests/serialize_deserialize_tests.rb
@@ -113,5 +113,3 @@ def test_by_reference_state_strict_entities args, assert
deserialized_state = args.gtk.deserialize_state serialized_state
assert.equal! deserialized_state.strict_entity.one, deserialized_state.strict_entity.two
end
-
-$tests.start
diff --git a/samples/10_advanced_debugging/03_unit_tests/state_serialization_experimental_tests.rb b/samples/10_advanced_debugging/03_unit_tests/state_serialization_experimental_tests.rb
index ffd8064..50258a8 100644
--- a/samples/10_advanced_debugging/03_unit_tests/state_serialization_experimental_tests.rb
+++ b/samples/10_advanced_debugging/03_unit_tests/state_serialization_experimental_tests.rb
@@ -104,4 +104,3 @@ end
$gtk.reset 100
$gtk.log_level = :off
-$gtk.tests.start
diff --git a/samples/10_advanced_debugging/03_unit_tests/suggest_autocompletion_tests.rb b/samples/10_advanced_debugging/03_unit_tests/suggest_autocompletion_tests.rb
new file mode 100644
index 0000000..e286a6c
--- /dev/null
+++ b/samples/10_advanced_debugging/03_unit_tests/suggest_autocompletion_tests.rb
@@ -0,0 +1,38 @@
+def default_suggest_autocompletion args
+ {
+ index: 4,
+ text: "args.",
+ __meta__: {
+ other_options: [
+ {
+ index: Fixnum,
+ file: "app/main.rb"
+ }
+ ]
+ }
+ }
+end
+
+def assert_completion source, *expected
+ results = suggest_autocompletion text: (source.strip.gsub ":cursor", ""),
+ index: (source.strip.index ":cursor")
+
+ puts results
+end
+
+def test_args_completion args, assert
+ $gtk.write_file_root "autocomplete.txt", ($gtk.suggest_autocompletion text: <<-S, index: 128).join("\n")
+require 'app/game.rb'
+
+def tick args
+ args.gtk.suppress_mailbox = false
+ $game ||= Game.new
+ $game.args = args
+ $game.args.
+ $game.tick
+end
+S
+
+ puts "contents:"
+ puts ($gtk.read_file "autocomplete.txt")
+end
diff --git a/samples/12_c_extensions/README.md b/samples/12_c_extensions/README.md
new file mode 100644
index 0000000..8950e53
--- /dev/null
+++ b/samples/12_c_extensions/README.md
@@ -0,0 +1,414 @@
+# dragonruby-bind doc
+
+This document describes how to use `dragonruby-bind` and also covers some
+implementation details that will help to build the right mental model
+
+### Hello World
+
+Create a simple file `bridge.c` with the following content:
+
+```c
+double square(double d) {
+ return d * d;
+}
+```
+
+Now, generate bindings:
+
+```bash
+dragonruby-bind bridge.c --output=bindings.c
+```
+
+The output file `bindings.c` will contain something like the following:
+
+```c
+#include <mruby.h>
+#include <string.h>
+#include <assert.h>
+#include <mruby/string.h>
+#include <mruby/data.h>
+#include <dragonruby.h>
+#include "bridge.c"
+
+// MRuby `typedef`s mrb_int in the mruby/value.h
+// Then `#define`s mrb_int in mruby.h
+// We need to undo the macro and avoid it's usage
+// FIXME: I'm surely doing something wrong
+#ifdef mrb_int
+#undef mrb_int
+#endif
+
+void *(*drb_symbol_lookup)(const char *sym) = NULL;
+
+static void (*drb_free_foreign_object_f)(mrb_state *, void *);
+static struct RClass *(*mrb_module_get_f)(mrb_state *, const char *);
+static mrb_int (*mrb_get_args_f)(mrb_state *, mrb_args_format, ...);
+static struct RClass *(*mrb_module_get_under_f)(mrb_state *, struct RClass *, const char *);
+static struct RClass *(*mrb_class_get_under_f)(mrb_state *, struct RClass *, const char *);
+static struct RClass *(*mrb_define_module_under_f)(mrb_state *, struct RClass *, const char *);
+static void (*mrb_define_module_function_f)(mrb_state *, struct RClass *, const char *, mrb_func_t, mrb_aspec);
+static struct RClass *(*mrb_define_class_under_f)(mrb_state *, struct RClass *, const char *, struct RClass *);
+static void (*mrb_define_method_f)(mrb_state *, struct RClass *, const char *, mrb_func_t, mrb_aspec);
+static void (*mrb_define_class_method_f)(mrb_state *, struct RClass *, const char *, mrb_func_t, mrb_aspec);
+static struct RData *(*mrb_data_object_alloc_f)(mrb_state *, struct RClass *, void *, const mrb_data_type *);
+static mrb_value (*mrb_str_new_cstr_f)(mrb_state *, const char *);
+static void (*mrb_raise_f)(mrb_state *, struct RClass *, const char *);
+static struct RClass *(*mrb_exc_get_f)(mrb_state *, const char *);
+static void drb_free_foreign_object_indirect(mrb_state *state, void *pointer) {
+ drb_free_foreign_object_f(state, pointer);
+}
+static double drb_ffi__ZTSd_FromRuby(mrb_state *state, mrb_value self) {
+ return mrb_float(self);
+}
+static mrb_value drb_ffi__ZTSd_ToRuby(mrb_state *state, double value) {
+ return mrb_float_value(state, value);
+}
+static mrb_value drb_ffi_square_Binding(mrb_state *state, mrb_value value) {
+ mrb_value *args = 0;
+ mrb_int argc = 0;
+ mrb_get_args_f(state, "*", &args, &argc);
+ double d_0 = drb_ffi__ZTSd_FromRuby(state, args[0]);
+ double ret_val = square(d_0);
+ return drb_ffi__ZTSd_ToRuby(state, ret_val);
+}
+static int drb_ffi_init_indirect_functions(void *(*lookup)(const char *));
+DRB_FFI_EXPORT
+void drb_register_c_extensions(void *(*lookup)(const char *), mrb_state *state, struct RClass *FFI) {
+ if (drb_ffi_init_indirect_functions(lookup))
+ return;
+ struct RClass *module = mrb_define_module_under_f(state, FFI, "CExt");
+ struct RClass *object_class = state->object_class;
+ mrb_define_module_function_f(state, module, "square", drb_ffi_square_Binding, MRB_ARGS_REQ(1));
+}
+static int drb_ffi_init_indirect_functions(void *(*lookup)(const char *fnname)) {
+ drb_symbol_lookup = lookup;
+ if (!(drb_free_foreign_object_f = (void (*)(mrb_state *, void *)) lookup("drb_free_foreign_object"))) return -1;
+ if (!(mrb_class_get_under_f = (struct RClass *(*)(mrb_state *, struct RClass *, const char *)) lookup("mrb_class_get_under"))) return -1;
+ if (!(mrb_data_object_alloc_f = (struct RData *(*)(mrb_state *, struct RClass *, void *, const mrb_data_type *)) lookup("mrb_data_object_alloc"))) return -1;
+ if (!(mrb_define_class_method_f = (void (*)(mrb_state *, struct RClass *, const char *, mrb_func_t, mrb_aspec)) lookup("mrb_define_class_method"))) return -1;
+ if (!(mrb_define_class_under_f = (struct RClass *(*)(mrb_state *, struct RClass *, const char *, struct RClass *)) lookup("mrb_define_class_under"))) return -1;
+ if (!(mrb_define_method_f = (void (*)(mrb_state *, struct RClass *, const char *, mrb_func_t, mrb_aspec)) lookup("mrb_define_method"))) return -1;
+ if (!(mrb_define_module_function_f = (void (*)(mrb_state *, struct RClass *, const char *, mrb_func_t, mrb_aspec)) lookup("mrb_define_module_function"))) return -1;
+ if (!(mrb_define_module_under_f = (struct RClass *(*)(mrb_state *, struct RClass *, const char *)) lookup("mrb_define_module_under"))) return -1;
+ if (!(mrb_exc_get_f = (struct RClass *(*)(mrb_state *, const char *)) lookup("mrb_exc_get"))) return -1;
+ if (!(mrb_get_args_f = (mrb_int (*)(mrb_state *, mrb_args_format, ...)) lookup("mrb_get_args"))) return -1;
+ if (!(mrb_module_get_f = (struct RClass *(*)(mrb_state *, const char *)) lookup("mrb_module_get"))) return -1;
+ if (!(mrb_module_get_under_f = (struct RClass *(*)(mrb_state *, struct RClass *, const char *)) lookup("mrb_module_get_under"))) return -1;
+ if (!(mrb_raise_f = (void (*)(mrb_state *, struct RClass *, const char *)) lookup("mrb_raise"))) return -1;
+ if (!(mrb_str_new_cstr_f = (mrb_value (*)(mrb_state *, const char *)) lookup("mrb_str_new_cstr"))) return -1;
+ return 0;
+}
+```
+
+Compile this as a shared library:
+
+```c
+# DRB_ROOT is the path to the gtk source directory
+clang -shared \
+ -isystem $DRB_ROOT/mruby/include/ \
+ -isystem $DRB_ROOT/ \
+ -o native/macos/bindings.dylib \
+ bindings.c
+```
+
+Now, somewhere within `main.rb` do the following:
+
+```rb
+# Teach DRGTK about the bindings
+$gtk.ffi_misc.dlopen("bindings")
+# Use the `square` function seamlessly
+puts FFI::CExt::square(42)
+# yeilds: 1764.0
+```
+
+This is the bare minimum that is needed to start using the C extensions.
+
+Now, more complex parts.
+
+### DRB FFI
+
+It is a good idea to include `dragonruby.h` into the C bridging file. It comes with
+a number of helpful macros. As an example, you can specify for which functions
+to generate bindings or change the name under which the function is available from
+DRGTK:
+
+```c
+#include <dragonruby.h>
+
+/// Binding for this function won't be generated
+void something_useless() {
+ /// ....
+}
+
+/// This one is accessible from Ruby as `square`
+DRB_FFI
+double square(double d) {
+ return d * d;
+}
+
+/// This one is accessible from Ruby as `sqr_int`
+DRB_FFI_NAME("sqr_int")
+int ffi_square_int(int d) {
+ return d * d;
+}
+```
+
+_Note: It is always a good idea to mark your functions with `DRB_FFI` or
+`DRB_FFI_NAME` explicitly. Support for unannotated functions is meant for
+third-party libraries._
+
+### Structs
+
+It is possible to use C structs from DRGTK. Here is a C example and its
+corresponding usage from Ruby.
+
+```c
+#include <dragonruby.h>
+
+typedef struct Point {
+ int x;
+ int y;
+} Point;
+
+DRB_FFI
+void printPoint(Point p) {
+ printf("Point(%d, %d)\n", p.x, p.y);
+}
+
+DRB_FFI
+Point fourtyPoint() {
+ Point p;
+ p.x = 42;
+ p.y = 42;
+ return p;
+}
+```
+
+Here is how to use it in the Ruby code:
+
+```ruby
+$gtk.ffi_misc.gtk_dlopen("bindings")
+include FFI::CExt
+p = Point.new
+p.x = 15
+p.y = 22
+printPoint(p)
+
+p2 = fourtyPoint()
+puts "Point(#{p2.x}, #{p2.y}) (from Ruby)"
+p2.x = 152
+puts "Point(#{p2.x}, #{p2.y}) (from Ruby)"
+```
+
+The output will be:
+
+```
+Point(15, 22)
+Point(42, 42) (from Ruby)
+Point(152, 42) (from Ruby)
+```
+
+### Pointers
+
+Here is how to use pointers:
+
+```c
+#include <dragonruby.h>
+
+DRB_FFI
+int *createInts(int size) {
+ int *ints = calloc(size, sizeof(int));
+ return ints;
+}
+
+DRB_FFI
+void printInts(int *ints, int size) {
+ printf("int array: ");
+ for (int i = 0; i < size; i++) {
+ printf("%d ", ints[i]);
+ }
+ printf("\n");
+}
+
+DRB_FFI
+void free_ints(int *ints) {
+ free(ints);
+}
+```
+
+The usage:
+
+```rb
+$gtk.ffi_misc.gtk_dlopen("bindings")
+include FFI::CExt
+ints = createInts(10)
+10.times do |i|
+ ints[i] = i
+end
+printInts(ints, 10)
+freeInts(ints)
+
+p = IntPointer.new
+p[0] = 15
+printInts(p, 1)
+puts "print int from ruby: #{p[0]}"
+```
+
+The output:
+
+```
+print ints from C: 0 1 2 3 4 5 6 7 8 9
+print ints from C: 15
+print int from ruby: 15
+```
+
+Important part here is the memory ownership.
+In the case of `createInts` the points is allocated in the C land, and therefore
+it should be deallocated in the C land. It is a responsibility of developer to
+take care of this memory.
+
+In the case of `IntPointer.new` the pointer is allocated in the Ruby land, and
+therefore it will be deallocated by DRGTK, without any need to worry about the
+ownership.
+
+### C Strings
+
+Technically, the C string is a just a pointer, i.e.: `char *`. But we provide
+some convenient sugar. Here is an example:
+
+```c
+#include <dragonruby.h>
+
+DRB_FFI
+char *allocateString() {
+ char *str = calloc(6, sizeof(char));
+ str[0] = 'h';
+ str[1] = 'e';
+ str[2] = 'l';
+ str[3] = 'l';
+ str[4] = 'o';
+ str[5] = '\n';
+ return str;
+}
+
+DRB_FFI
+void freeString(char *s) {
+ free(s);
+}
+
+DRB_FFI
+void printString(char *s) {
+ printf("hello from C: %s\n", s);
+}
+
+DRB_FFI
+char *getStaticString() {
+ return "Some static string";
+}
+```
+
+Usage:
+
+```rb
+$gtk.ffi_misc.gtk_dlopen("bindings")
+include FFI::CExt
+s1 = allocateString()
+printString(s1)
+freeString(s1)
+
+printString("or Ruby?")
+
+s2 = getStaticString()
+printString(s2)
+puts "print C string from Ruby #{s2.str}"
+```
+
+Output:
+
+```
+hello from C: hello
+hello from C: or Ruby?
+hello from C: Some static string
+print C string from Ruby: Some static string
+```
+
+General rules are the same as with any other pointers, but there are few more
+additional notes:
+
+ - `CharPointer`s have `str` method that returns a normal Ruby string
+ - Ruby strings automatically converted to `char *`, as in the `printString("or Ruby?")`
+
+### CExt
+
+In order to avoid clashes and name collisions all the bridging functions are put
+under a separate module (or namespace) under `FFI`. By default, the name `CExt`
+is used, but it can be changed to anything else via the `--ffi-module` module, e.g.:
+
+```bash
+dragonruby-bind --ffi-module=CoolStuff bridge.c
+```
+
+Then one can use `include FFI::CoolStuff` instead.
+
+### Pitfalls
+
+There is no so-called marshalling when it comes to structs. When you read or
+write to a struct field you are writing to the underlying C struct, which brings
+some unexpected results. The following structs can be easily used from Ruby:
+
+```c
+typedef struct Point {
+ int x;
+ int y;
+} Point;
+
+typedef struct Size {
+ int width;
+ int height;
+} Size;
+
+typedef struct Rectangle {
+ Point origin;
+ Size size;
+} Rectangle;
+```
+
+```rb
+o = Point.new
+o.x = 15
+o.y = 25
+
+s = Size.new
+s.width = 150
+s.height = 250
+
+r = Rectangle.new
+r.origin = o
+r.size = s
+
+
+puts "#{r.origin.x}, #{r.origin.y}" #1
+
+r.origin.x = 42 #2
+
+puts "#{r.origin.x}, #{r.origin.y}" #3
+
+p = r.origin
+p.x = 42
+r.origin = p
+puts "#{r.origin.x}, #{r.origin.y}" #4
+```
+
+In this example `15, 25` will be printed at line `#1`, after the assignment at `#2`
+the same string will be printed `15, 25` at line `#3`.
+That's because each `.` in Ruby returns a new object, in this case `p.origin`
+returns a copy of the original `Point`. The right way to handle this case is right
+before `#4`.
+
+### Rough edges
+
+Currently, there are no type checks and no checks on the number of arguments.
+If you call a C function that expects an integer with a double - it may give some
+garbage. If you call a C function with more or fewer arguments, then it may give
+some garbage, or crash. This will come in later.
+
diff --git a/samples/13_path_finding_algorithms/01_breadth_first_search/app/main.rb b/samples/13_path_finding_algorithms/01_breadth_first_search/app/main.rb
new file mode 100644
index 0000000..7f98509
--- /dev/null
+++ b/samples/13_path_finding_algorithms/01_breadth_first_search/app/main.rb
@@ -0,0 +1,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), frontier_color]
+ end
+ end
+
+ # Draws the walls
+ def render_walls
+ outputs.solids << state.walls.map do |wall|
+ [scale_up(wall), 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), 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))
+ 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
diff --git a/samples/13_path_finding_algorithms/01_breadth_first_search/circle-white.png b/samples/13_path_finding_algorithms/01_breadth_first_search/circle-white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/13_path_finding_algorithms/01_breadth_first_search/circle-white.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/01_breadth_first_search/star.png b/samples/13_path_finding_algorithms/01_breadth_first_search/star.png
new file mode 100644
index 0000000..b37bb04
--- /dev/null
+++ b/samples/13_path_finding_algorithms/01_breadth_first_search/star.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/02_detailed_breadth_first_search/app/main.rb b/samples/13_path_finding_algorithms/02_detailed_breadth_first_search/app/main.rb
new file mode 100644
index 0000000..810ff7b
--- /dev/null
+++ b/samples/13_path_finding_algorithms/02_detailed_breadth_first_search/app/main.rb
@@ -0,0 +1,646 @@
+# 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
+
+# This search numbers the order in which new cells are explored
+# The next cell from where the search will continue is highlighted yellow
+# And the cells that will be considered for expansion are in semi-transparent green
+
+# The star can be moved by clicking and dragging
+# Walls can be added and removed by clicking and dragging
+
+class DetailedBreadthFirstSearch
+ 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 = 9
+ args.state.grid.height = 4
+ args.state.grid.cell_size = 90
+
+ # 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
+
+ # 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 = [3, 2]
+ args.state.walls = {}
+
+ # 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 = []
+ args.state.cell_numbers = []
+
+
+
+ # 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
+
+ # The x, y, w, h values for the buttons
+ # Allow easy movement of the buttons location
+ # A centralized location to get values to detect input and draw the buttons
+ # Editing these values might mean needing to edit the label offsets
+ # which can be found in the appropriate render button methods
+ args.state.buttons.left = [450, 600, 160, 50]
+ args.state.buttons.right = [610, 600, 160, 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
+ def tick
+ render
+ input
+ end
+
+ # This method is called from tick and renders everything every tick
+ def render
+ render_buttons
+ render_slider
+
+ render_background
+ render_visited
+ render_frontier
+ render_walls
+ render_star
+
+ render_highlights
+ render_cell_numbers
+ end
+
+ # The methods below subdivide the task of drawing everything to the screen
+
+ # Draws the buttons that move the search backward or forward
+ # These buttons are rendered so the user knows where to click to move the search
+ def render_buttons
+ render_left_button
+ render_right_button
+ end
+
+ # Renders 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
+ label_x = buttons.left.x + 05
+ label_y = buttons.left.y + 35
+ outputs.labels << [label_x, label_y, "< Step backward"]
+ end
+
+ # Renders the button which steps the search forward
+ # Shows the user where to click to move the search forward
+ 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 + 10
+ label_y = buttons.right.y + 35
+ outputs.labels << [label_x, label_y, "Step forward >"]
+ 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
+ # Which is a bunch of unvisited cells
+ # Drawn first so other things can draw on top of it
+ def render_background
+ render_unvisited
+
+ # The grid lines make the cells appear separate
+ render_grid_lines
+ end
+
+ # Draws a rectangle the size of the entire grid to represent unvisited cells
+ # Unvisited cells are the default cell
+ def render_unvisited
+ background = [0, 0, grid.width, grid.height]
+ outputs.solids << [scale_up(background), 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 << [scale_up(vertical_line(x)), grid_line_color]
+ end
+
+ for y in 0..grid.height
+ outputs.lines << [scale_up(horizontal_line(y)), grid_line_color]
+ end
+ end
+
+ # Easy way to get a vertical line given an index
+ def vertical_line column
+ [column, 0, column, grid.height]
+ end
+
+ # Easy way to get a horizontal line given an index
+ def horizontal_line row
+ [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
+ state.frontier.each do |cell|
+ outputs.solids << [scale_up(cell), frontier_color]
+ end
+ end
+
+ # Draws the walls
+ def render_walls
+ state.walls.each_key do |wall|
+ outputs.solids << [scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders cells that have been searched in the appropriate color
+ def render_visited
+ state.visited.each_key do |cell|
+ outputs.solids << [scale_up(cell), visited_color]
+ end
+ end
+
+ # Renders the star
+ def render_star
+ outputs.sprites << [scale_up(state.star), 'star.png']
+ end
+
+ # Cells have a number rendered in them based on when they were explored
+ # This is based off of their index in the cell_numbers array
+ # Cells are added to this array the same time they are added to the frontier array
+ def render_cell_numbers
+ state.cell_numbers.each_with_index do |cell, index|
+ # Math that approx centers the number in the cell
+ label_x = (cell.x * grid.cell_size) + grid.cell_size / 2 - 5
+ label_y = (cell.y * grid.cell_size) + (grid.cell_size / 2) + 5
+
+ outputs.labels << [label_x, label_y, (index + 1).to_s]
+ end
+ end
+
+ # The next frontier to be expanded is highlighted yellow
+ # Its adjacent non-wall neighbors have their border highlighted green
+ # This is to show the user how the search expands
+ def render_highlights
+ return if state.frontier.empty?
+
+ # Highlight the next frontier to be expanded yellow
+ next_frontier = state.frontier[0]
+ outputs.solids << [scale_up(next_frontier), highlighter_yellow]
+
+ # Neighbors have a semi-transparent green layer over them
+ # Unless the neighbor is a wall
+ adjacent_neighbors(next_frontier).each do |neighbor|
+ unless state.walls.has_key?(neighbor)
+ outputs.solids << [scale_up(neighbor), highlighter_green, 70]
+ end
+ end
+ end
+
+
+ # Cell Size is used when rendering to allow the grid to be scaled up or down
+ # Cells in the frontier array and visited hash and walls hash are stored as x & y
+ # Scaling up cells and lines when rendering allows omitting of width and height
+ 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
+ # Processes inputs for the buttons
+ input_buttons
+
+ # Detects which if any click and drag input is occurring
+ detect_click_and_drag
+
+ # Does the appropriate click and drag input based on the click_and_drag variable
+ process_click_and_drag
+ end
+
+ # Detects and Process input for each button
+ def input_buttons
+ input_left_button
+ input_right_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?
+ unless state.anim_steps == 0
+ state.anim_steps -= 1
+ recalculate
+ end
+ 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_right_button
+ if right_button_clicked?
+ unless state.anim_steps == state.max_steps
+ state.anim_steps += 1
+ # Although normally recalculate would be called here
+ # because the right button only moves the search forward
+ # We can just do that
+ calc
+ end
+ end
+ end
+
+ # Whenever the user edits the grid,
+ # The search has to be recalculated upto the current step
+
+ def recalculate
+ # Resets the search
+ state.frontier = []
+ state.visited = {}
+ state.cell_numbers = []
+
+ # Moves the animation forward one step at a time
+ state.anim_steps.times { calc }
+ end
+
+
+ # Determines what the user is clicking and planning on dragging
+ # Click and drag input is initiated by a click on the appropriate item
+ # and ended by mouse up
+ # 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 input based on what the user is currently dragging
+ def process_click_and_drag
+ if state.click_and_drag == :slider
+ input_slider
+ elsif 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
+ end
+ end
+
+ # This method is called when the user is dragging the slider
+ # It moves the current animation step to the point represented by the slider
+ def input_slider
+ 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
+
+ # Moves the star to the grid closest to the mouse
+ # Only recalculates the search if the star changes position
+ # Called whenever the user is dragging the 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
+ # Adds a wall to the hash
+ # We can use the grid closest to mouse, because the cursor is inside the grid
+ 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 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
+
+ # Also assign them a frontier number
+ state.cell_numbers << neighbor
+ 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 grid closest to the mouse helps with this
+ def cell_closest_to_mouse
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ [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)) || inputs.keyboard.key_up.left
+ end
+
+ def right_button_clicked?
+ (inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.right)) || inputs.keyboard.key_up.right
+ end
+
+ # Signal that the user is going to be moving 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))
+ 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
+
+ # Next frontier to be expanded
+ def highlighter_yellow
+ [214, 231, 125]
+ end
+
+ # The neighbors of the next frontier to be expanded
+ def highlighter_green
+ [65, 191, 127]
+ 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
+
+
+def tick args
+ # Pressing r resets the program
+ if args.inputs.keyboard.key_down.r
+ args.gtk.reset
+ reset
+ return
+ end
+
+ $detailed_breadth_first_search ||= DetailedBreadthFirstSearch.new(args)
+ $detailed_breadth_first_search.args = args
+ $detailed_breadth_first_search.tick
+end
+
+
+def reset
+ $detailed_breadth_first_search = nil
+end
diff --git a/samples/13_path_finding_algorithms/02_detailed_breadth_first_search/circle-white.png b/samples/13_path_finding_algorithms/02_detailed_breadth_first_search/circle-white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/13_path_finding_algorithms/02_detailed_breadth_first_search/circle-white.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/02_detailed_breadth_first_search/star.png b/samples/13_path_finding_algorithms/02_detailed_breadth_first_search/star.png
new file mode 100644
index 0000000..b37bb04
--- /dev/null
+++ b/samples/13_path_finding_algorithms/02_detailed_breadth_first_search/star.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/03_breadcrumbs/app/main.rb b/samples/13_path_finding_algorithms/03_breadcrumbs/app/main.rb
new file mode 100644
index 0000000..648805a
--- /dev/null
+++ b/samples/13_path_finding_algorithms/03_breadcrumbs/app/main.rb
@@ -0,0 +1,545 @@
+class Breadcrumbs
+ attr_gtk
+
+ # This method is called every frame/tick
+ # Every tick, the current state of the search is rendered on the screen,
+ # User input is processed, and
+ # The next step in the search is calculated
+ def tick
+ defaults
+ # If the grid has not been searched
+ if search.came_from.empty?
+ calc
+ # Calc Path
+ end
+ render
+ input
+ end
+
+ def defaults
+ # Variables to edit the size and appearance of the grid
+ # Freely customizable to user's liking
+ grid.width ||= 30
+ grid.height ||= 15
+ grid.cell_size ||= 40
+ grid.rect ||= [0, 0, grid.width, grid.height]
+
+ # The location of the star and walls of the grid
+ # They can be modified to have a different initial grid
+ # Walls are stored in a hash for quick look up when doing the search
+ grid.star ||= [2, 8]
+ grid.target ||= [10, 5]
+ grid.walls ||= {
+ [3, 3] => true,
+ [3, 4] => true,
+ [3, 5] => true,
+ [3, 6] => true,
+ [3, 7] => true,
+ [3, 8] => true,
+ [3, 9] => true,
+ [3, 10] => true,
+ [3, 11] => true,
+ [4, 3] => true,
+ [4, 4] => true,
+ [4, 5] => true,
+ [4, 6] => true,
+ [4, 7] => true,
+ [4, 8] => true,
+ [4, 9] => true,
+ [4, 10] => true,
+ [4, 11] => true,
+ [13, 0] => true,
+ [13, 1] => true,
+ [13, 2] => true,
+ [13, 3] => true,
+ [13, 4] => true,
+ [13, 5] => true,
+ [13, 6] => true,
+ [13, 7] => true,
+ [13, 8] => true,
+ [13, 9] => true,
+ [13, 10] => true,
+ [14, 0] => true,
+ [14, 1] => true,
+ [14, 2] => true,
+ [14, 3] => true,
+ [14, 4] => true,
+ [14, 5] => true,
+ [14, 6] => true,
+ [14, 7] => true,
+ [14, 8] => true,
+ [14, 9] => true,
+ [14, 10] => true,
+ [21, 8] => true,
+ [21, 9] => true,
+ [21, 10] => true,
+ [21, 11] => true,
+ [21, 12] => true,
+ [21, 13] => true,
+ [21, 14] => true,
+ [22, 8] => true,
+ [22, 9] => true,
+ [22, 10] => true,
+ [22, 11] => true,
+ [22, 12] => true,
+ [22, 13] => true,
+ [22, 14] => true,
+ [23, 8] => true,
+ [23, 9] => true,
+ [24, 8] => true,
+ [24, 9] => true,
+ [25, 8] => true,
+ [25, 9] => true,
+ }
+
+ # Variables that are used by the breadth first search
+ # Storing cells that the search has visited, prevents unnecessary steps
+ # Expanding the frontier of the search in order makes the search expand
+ # from the center outward
+
+ # The cells from which the search is to expand
+ search.frontier ||= []
+ # A hash of where each cell was expanded from
+ # The key is a cell, and the value is the cell it came from
+ search.came_from ||= {}
+ # Cells that are part of the path from the target to the star
+ search.path ||= {}
+
+ # What the user is currently editing on the grid
+ # We store this value, because we want to remember the value even when
+ # the user's cursor is no longer over what they're interacting with, but
+ # they are still clicking down on the mouse.
+ state.current_input ||= :none
+ end
+
+ def calc
+ # Setup the search to start from the star
+ search.frontier << grid.star
+ search.came_from[grid.star] = nil
+
+ # Until there are no more cells to expand from
+ until search.frontier.empty?
+ # Takes the next frontier cell
+ new_frontier = search.frontier.shift
+ # For each of its neighbors
+ adjacent_neighbors(new_frontier).each do |neighbor|
+ # That have not been visited and are not walls
+ unless search.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor)
+ # Add them to the frontier and mark them as visited in the first grid
+ # Unless the target has been visited
+ # Add the neighbor to the frontier and remember which cell it came from
+ search.frontier << neighbor
+ search.came_from[neighbor] = new_frontier
+ end
+ end
+ end
+ end
+
+
+ # Draws everything onto the screen
+ def render
+ render_background
+ # render_heat_map
+ render_walls
+ # render_path
+ # render_labels
+ render_arrows
+ render_star
+ render_target
+ unless grid.walls.has_key?(grid.target)
+ render_trail
+ end
+ end
+
+ def render_trail(current_cell=grid.target)
+ return if current_cell == grid.star
+ parent_cell = search.came_from[current_cell]
+ if current_cell && parent_cell
+ outputs.lines << [(current_cell.x + 0.5) * grid.cell_size, (current_cell.y + 0.5) * grid.cell_size,
+ (parent_cell.x + 0.5) * grid.cell_size, (parent_cell.y + 0.5) * grid.cell_size, purple]
+
+ end
+ render_trail(parent_cell)
+ end
+
+ def render_arrows
+ search.came_from.each do |child, parent|
+ if parent && child
+ arrow_cell = [(child.x + parent.x) / 2, (child.y + parent.y) / 2]
+ if parent.x > child.x # If the parent cell is to the right of the child cell
+ outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 0] # Point the arrow to the right
+ elsif parent.x < child.x # If the parent cell is to the right of the child cell
+ outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 180] # Point the arrow to the right
+ elsif parent.y > child.y # If the parent cell is to the right of the child cell
+ outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 90] # Point the arrow to the right
+ elsif parent.y < child.y # If the parent cell is to the right of the child cell
+ outputs.sprites << [scale_up(arrow_cell), 'arrow.png', 270] # Point the arrow to the right
+ end
+ end
+ end
+ end
+
+ # The methods below subdivide the task of drawing everything to the screen
+
+ # Draws what the grid looks like with nothing on it
+ def render_background
+ render_unvisited
+ render_grid_lines
+ end
+
+ # Draws both grids
+ def render_unvisited
+ outputs.solids << [scale_up(grid.rect), unvisited_color]
+ end
+
+ # Draws grid lines to show the division of the grid into cells
+ def render_grid_lines
+ for x in 0..grid.width
+ outputs.lines << vertical_line(x)
+ end
+
+ for y in 0..grid.height
+ outputs.lines << horizontal_line(y)
+ end
+ end
+
+ # Easy way to draw vertical lines given an index
+ def vertical_line column
+ scale_up([column, 0, column, grid.height])
+ end
+
+ # Easy way to draw horizontal lines given an index
+ def horizontal_line row
+ scale_up([0, row, grid.width, row])
+ end
+
+ # Draws the walls on both grids
+ def render_walls
+ grid.walls.each_key do |wall|
+ outputs.solids << [scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders the star on both grids
+ def render_star
+ outputs.sprites << [scale_up(grid.star), 'star.png']
+ end
+
+ # Renders the target on both grids
+ def render_target
+ outputs.sprites << [scale_up(grid.target), 'target.png']
+ end
+
+ # Labels the grids
+ def render_labels
+ outputs.labels << [200, 625, "Without early exit"]
+ end
+
+ # Renders the path based off of the search.path hash
+ def render_path
+ # If the star and target are disconnected there will only be one path
+ # The path should not render in that case
+ unless search.path.size == 1
+ search.path.each_key do | cell |
+ # Renders path on both grids
+ outputs.solids << [scale_up(cell), path_color]
+ end
+ end
+ end
+
+ # Calculates the path from the target to the star after the search is over
+ # Relies on the came_from hash
+ # Fills the search.path hash, which is later rendered on screen
+ def calc_path
+ endpoint = grid.target
+ while endpoint
+ search.path[endpoint] = true
+ endpoint = search.came_from[endpoint]
+ end
+ end
+
+ # In code, the cells are represented as 1x1 rectangles
+ # When drawn, the cells are larger than 1x1 rectangles
+ # This method is used to scale up cells, and lines
+ # Objects are scaled up according to the grid.cell_size variable
+ # This allows for easy customization of the visual scale of the grid
+ def scale_up(cell)
+ # Prevents the original value of cell from being edited
+ cell = cell.clone
+
+ # If cell is just an x and y coordinate
+ if cell.size == 2
+ # Add a width and height of 1
+ cell << 1
+ cell << 1
+ end
+
+ # Scale all the values up
+ cell.map! { |value| value * grid.cell_size }
+
+ # Returns the scaled up cell
+ cell
+ end
+
+ # This method processes user input every tick
+ # Any method with "1" is related to the first grid
+ # Any method with "2" is related to the second grid
+ def input
+ # The program has to remember that the user is dragging an object
+ # even when the mouse is no longer over that object
+ # So detecting input and processing input is separate
+ # detect_input
+ # process_input
+ if inputs.mouse.up
+ state.current_input = :none
+ elsif star_clicked?
+ state.current_input = :star
+ end
+
+ if mouse_inside_grid?
+ unless grid.target == cell_closest_to_mouse
+ grid.target = cell_closest_to_mouse
+ end
+ if state.current_input == :star
+ unless grid.star == cell_closest_to_mouse
+ grid.star = cell_closest_to_mouse
+ end
+ end
+ end
+ end
+
+ # Determines what the user is editing and stores the value
+ # Storing the value allows the user to continue the same edit as long as the
+ # mouse left click is held
+ def detect_input
+ # When the mouse is up, nothing is being edited
+ if inputs.mouse.up
+ state.current_input = :none
+ # When the star in the no second grid is clicked
+ elsif star_clicked?
+ state.current_input = :star
+ # When the target in the no second grid is clicked
+ elsif target_clicked?
+ state.current_input = :target
+ # When a wall in the first grid is clicked
+ elsif wall_clicked?
+ state.current_input = :remove_wall
+ # When the first grid is clicked
+ elsif grid_clicked?
+ state.current_input = :add_wall
+ end
+ end
+
+ # Processes click and drag based on what the user is currently dragging
+ def process_input
+ if state.current_input == :star
+ input_star
+ elsif state.current_input == :target
+ input_target
+ elsif state.current_input == :remove_wall
+ input_remove_wall
+ elsif state.current_input == :add_wall
+ input_add_wall
+ end
+ end
+
+ # Moves the star to the cell closest to the mouse in the first grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def input_star
+ old_star = grid.star.clone
+ grid.star = cell_closest_to_mouse
+ unless old_star == grid.star
+ reset_search
+ end
+ end
+
+ # Moves the target to the grid closest to the mouse in the first grid
+ # Only reset_searchs the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def input_target
+ old_target = grid.target.clone
+ grid.target = cell_closest_to_mouse
+ unless old_target == grid.target
+ reset_search
+ end
+ end
+
+ # Removes walls in the first grid that are under the cursor
+ def input_remove_wall
+ # The mouse needs to be inside the grid, because we only want to remove walls
+ # the cursor is directly over
+ # Recalculations should only occur when a wall is actually deleted
+ if mouse_inside_grid?
+ if grid.walls.has_key?(cell_closest_to_mouse)
+ grid.walls.delete(cell_closest_to_mouse)
+ reset_search
+ end
+ end
+ end
+
+ # Adds a wall in the first grid in the cell the mouse is over
+ def input_add_wall
+ if mouse_inside_grid?
+ unless grid.walls.has_key?(cell_closest_to_mouse)
+ grid.walls[cell_closest_to_mouse] = true
+ reset_search
+ end
+ end
+ end
+
+
+ # Whenever the user edits the grid,
+ # The search has to be reset_searchd upto the current step
+ # with the current grid as the initial state of the grid
+ def reset_search
+ # Reset_Searchs the search
+ search.frontier = []
+ search.came_from = {}
+ search.path = {}
+ end
+
+
+ # Returns a list of adjacent cells
+ # Used to determine what the next cells to be added to the frontier are
+ def adjacent_neighbors(cell)
+ neighbors = []
+
+ # Gets all the valid neighbors into the array
+ # From southern neighbor, clockwise
+ neighbors << [cell.x, cell.y - 1] unless cell.y == 0
+ neighbors << [cell.x - 1, cell.y] unless cell.x == 0
+ neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1
+ neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1
+
+ # Sorts the neighbors so the rendered path is a zigzag path
+ # Cells in a diagonal direction are given priority
+ # Comment this line to see the difference
+ neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) }
+
+ neighbors
+ end
+
+ # Finds the vertical and horizontal distance of a cell from the star
+ # and returns the larger value
+ # This method is used to have a zigzag pattern in the rendered path
+ # A cell that is [5, 5] from the star,
+ # is explored before over a cell that is [0, 7] away.
+ # So, if possible, the search tries to go diagonal (zigzag) first
+ def proximity_to_star(x, y)
+ distance_x = (grid.star.x - x).abs
+ distance_y = (grid.star.y - y).abs
+
+ if distance_x > distance_y
+ return distance_x
+ else
+ return distance_y
+ end
+ end
+
+ # When the user grabs the star and puts their cursor to the far right
+ # and moves up and down, the star is supposed to move along the grid as well
+ # Finding the cell closest to the mouse helps with this
+ def cell_closest_to_mouse
+ # Closest cell to the mouse in the first grid
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Bound x and y to the grid
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ # Signal that the user is going to be moving the star from the first grid
+ def star_clicked?
+ inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(grid.star))
+ end
+
+ # Signal that the user is going to be moving the target from the first grid
+ def target_clicked?
+ inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(grid.target))
+ end
+
+ # Signal that the user is going to be adding walls from the first grid
+ def grid_clicked?
+ inputs.mouse.down && mouse_inside_grid?
+ end
+
+ # Returns whether the mouse is inside of the first grid
+ # Part of the condition that checks whether the user is adding a wall
+ def mouse_inside_grid?
+ inputs.mouse.point.inside_rect?(scale_up(grid.rect))
+ end
+
+ # These methods provide handy aliases to colors
+
+ # Light brown
+ def unvisited_color
+ [221, 212, 213]
+ # [255, 255, 255]
+ end
+
+ # Camo Green
+ def wall_color
+ [134, 134, 120]
+ end
+
+ # Pastel White
+ def path_color
+ [231, 230, 228]
+ end
+
+ def red
+ [255, 0, 0]
+ end
+
+ def purple
+ [149, 64, 191]
+ end
+
+ # Makes code more concise
+ def grid
+ state.grid
+ end
+
+ def search
+ state.search
+ end
+end
+
+# Method that is called by DragonRuby periodically
+# Used for updating animations and calculations
+def tick args
+
+ # Pressing r will reset the application
+ if args.inputs.keyboard.key_down.r
+ args.gtk.reset
+ reset
+ return
+ end
+
+ # Every tick, new args are passed, and the Breadth First Search tick is called
+ $breadcrumbs ||= Breadcrumbs.new(args)
+ $breadcrumbs.args = args
+ $breadcrumbs.tick
+end
+
+
+def reset
+ $breadcrumbs = nil
+end
+
+ # # Representation of how far away visited cells are from the star
+ # # Replaces the render_visited method
+ # # Visually demonstrates the effectiveness of early exit for pathfinding
+ # def render_heat_map
+ # # THIS CODE NEEDS SOME FIXING DUE TO REFACTORING
+ # search.came_from.each_key do | cell |
+ # distance = (grid.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs
+ # max_distance = grid.width + grid.height
+ # alpha = 255.to_i * distance.to_i / max_distance.to_i
+ # outputs.solids << [scale_up(visited_cell), red, alpha]
+ # # outputs.solids << [early_exit_scale_up(visited_cell), red, alpha]
+ # end
+ # end
diff --git a/samples/13_path_finding_algorithms/03_breadcrumbs/arrow.png b/samples/13_path_finding_algorithms/03_breadcrumbs/arrow.png
new file mode 100644
index 0000000..06e4a20
--- /dev/null
+++ b/samples/13_path_finding_algorithms/03_breadcrumbs/arrow.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/03_breadcrumbs/star.png b/samples/13_path_finding_algorithms/03_breadcrumbs/star.png
new file mode 100644
index 0000000..b37bb04
--- /dev/null
+++ b/samples/13_path_finding_algorithms/03_breadcrumbs/star.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/03_breadcrumbs/target.png b/samples/13_path_finding_algorithms/03_breadcrumbs/target.png
new file mode 100644
index 0000000..fb2223d
--- /dev/null
+++ b/samples/13_path_finding_algorithms/03_breadcrumbs/target.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/04_early_exit/app/main.rb b/samples/13_path_finding_algorithms/04_early_exit/app/main.rb
new file mode 100644
index 0000000..1e9305b
--- /dev/null
+++ b/samples/13_path_finding_algorithms/04_early_exit/app/main.rb
@@ -0,0 +1,631 @@
+# Comparison of a breadth first search with and without early exit
+# Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html
+
+# Demonstrates the exploration difference caused by early exit
+# Also demonstrates how breadth first search is used for path generation
+
+# The left grid is a breadth first search without early exit
+# The right grid is a breadth first search with early exit
+# The red squares represent how far the search expanded
+# The darker the red, the farther the search proceeded
+# Comparison of the heat map reveals how much searching can be saved by early exit
+# The white path shows path generation via breadth first search
+class EarlyExitBreadthFirstSearch
+ attr_gtk
+
+ # This method is called every frame/tick
+ # Every tick, the current state of the search is rendered on the screen,
+ # User input is processed, and
+ # The next step in the search is calculated
+ def tick
+ defaults
+ # If the grid has not been searched
+ if state.visited.empty?
+ # Complete the search
+ state.max_steps.times { step }
+ # And calculate the path
+ calc_path
+ end
+ render
+ input
+ end
+
+ def defaults
+ # Variables to edit the size and appearance of the grid
+ # Freely customizable to user's liking
+ grid.width ||= 15
+ grid.height ||= 15
+ grid.cell_size ||= 40
+ grid.rect ||= [0, 0, grid.width, grid.height]
+
+ # At some step the animation will end,
+ # and further steps won't change anything (the whole grid.widthill 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
+ state.max_steps ||= args.state.grid.width * args.state.grid.height
+
+ # The location of the star and walls of the grid
+ # They can be modified to have a different initial grid
+ # Walls are stored in a hash for quick look up when doing the search
+ state.star ||= [2, 8]
+ state.target ||= [10, 5]
+ state.walls ||= {}
+
+ # 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
+
+ # Visited cells in the first grid
+ state.visited ||= {}
+ # Visited cells in the second grid
+ state.early_exit_visited ||= {}
+ # The cells from which the search is to expand
+ state.frontier ||= []
+ # A hash of where each cell was expanded from
+ # The key is a cell, and the value is the cell it came from
+ state.came_from ||= {}
+ # Cells that are part of the path from the target to the star
+ state.path ||= {}
+
+ # What the user is currently editing on the grid
+ # We store this value, because we want to remember the value even when
+ # the user's cursor is no longer over what they're interacting with, but
+ # they are still clicking down on the mouse.
+ state.current_input ||= :none
+ end
+
+ # Draws everything onto the screen
+ def render
+ render_background
+ render_heat_map
+ render_walls
+ render_path
+ render_star
+ render_target
+ render_labels
+ end
+
+ # The methods below subdivide the task of drawing everything to the screen
+
+ # Draws what the grid looks like with nothing on it
+ def render_background
+ render_unvisited
+ render_grid_lines
+ end
+
+ # Draws both grids
+ def render_unvisited
+ outputs.solids << [scale_up(grid.rect), unvisited_color]
+ outputs.solids << [early_exit_scale_up(grid.rect), unvisited_color]
+ end
+
+ # Draws grid lines to show the division of the grid into cells
+ def render_grid_lines
+ for x in 0..grid.width
+ outputs.lines << vertical_line(x)
+ outputs.lines << early_exit_vertical_line(x)
+ end
+
+ for y in 0..grid.height
+ outputs.lines << horizontal_line(y)
+ outputs.lines << early_exit_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
+
+ # Easy way to draw vertical lines given an index
+ def early_exit_vertical_line column
+ scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height])
+ end
+
+ # Easy way to draw horizontal lines given an index
+ def early_exit_horizontal_line row
+ scale_up([grid.width + 1, row, grid.width + grid.width + 1, row])
+ end
+
+ # Draws the walls on both grids
+ def render_walls
+ state.walls.each_key do |wall|
+ outputs.solids << [scale_up(wall), wall_color]
+ outputs.solids << [early_exit_scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders the star on both grids
+ def render_star
+ outputs.sprites << [scale_up(state.star), 'star.png']
+ outputs.sprites << [early_exit_scale_up(state.star), 'star.png']
+ end
+
+ # Renders the target on both grids
+ def render_target
+ outputs.sprites << [scale_up(state.target), 'target.png']
+ outputs.sprites << [early_exit_scale_up(state.target), 'target.png']
+ end
+
+ # Labels the grids
+ def render_labels
+ outputs.labels << [200, 625, "Without early exit"]
+ outputs.labels << [875, 625, "With early exit"]
+ end
+
+ # Renders the path based off of the state.path hash
+ def render_path
+ # If the star and target are disconnected there will only be one path
+ # The path should not render in that case
+ unless state.path.size == 1
+ state.path.each_key do | cell |
+ # Renders path on both grids
+ outputs.solids << [scale_up(cell), path_color]
+ outputs.solids << [early_exit_scale_up(cell), path_color]
+ end
+ end
+ end
+
+ # Calculates the path from the target to the star after the search is over
+ # Relies on the came_from hash
+ # Fills the state.path hash, which is later rendered on screen
+ def calc_path
+ endpoint = state.target
+ while endpoint
+ state.path[endpoint] = true
+ endpoint = state.came_from[endpoint]
+ end
+ end
+
+ # Representation of how far away visited cells are from the star
+ # Replaces the render_visited method
+ # Visually demonstrates the effectiveness of early exit for pathfinding
+ def render_heat_map
+ state.visited.each_key do | visited_cell |
+ distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs
+ max_distance = grid.width + grid.height
+ alpha = 255.to_i * distance.to_i / max_distance.to_i
+ outputs.solids << [scale_up(visited_cell), red, alpha]
+ # outputs.solids << [early_exit_scale_up(visited_cell), red, alpha]
+ end
+
+ state.early_exit_visited.each_key do | visited_cell |
+ distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs
+ max_distance = grid.width + grid.height
+ alpha = 255.to_i * distance.to_i / max_distance.to_i
+ outputs.solids << [early_exit_scale_up(visited_cell), red, alpha]
+ end
+ end
+
+ # Translates the given cell grid.width + 1 to the right and then scales up
+ # Used to draw cells for the second grid
+ # This method does not work for lines,
+ # so separate methods exist for the grid lines
+ def early_exit_scale_up(cell)
+ cell_clone = cell.clone
+ cell_clone.x += grid.width + 1
+ scale_up(cell_clone)
+ end
+
+ # In code, the cells are represented as 1x1 rectangles
+ # When drawn, the cells are larger than 1x1 rectangles
+ # This method is used to scale up cells, and lines
+ # Objects are scaled up according to the grid.cell_size variable
+ # This allows for easy customization of the visual scale of the grid
+ def scale_up(cell)
+ # Prevents the original value of cell from being edited
+ cell = cell.clone
+
+ # If cell is just an x and y coordinate
+ if cell.size == 2
+ # Add a width and height of 1
+ cell << 1
+ cell << 1
+ end
+
+ # Scale all the values up
+ cell.map! { |value| value * grid.cell_size }
+
+ # Returns the scaled up cell
+ cell
+ end
+
+ # This method processes user input every tick
+ # Any method with "1" is related to the first grid
+ # Any method with "2" is related to the second grid
+ def input
+ # The program has to remember that the user is dragging an object
+ # even when the mouse is no longer over that object
+ # So detecting input and processing input is separate
+ detect_input
+ process_input
+ end
+
+ # Determines what the user is editing and stores the value
+ # Storing the value allows the user to continue the same edit as long as the
+ # mouse left click is held
+ def detect_input
+ # When the mouse is up, nothing is being edited
+ if inputs.mouse.up
+ state.current_input = :none
+ # When the star in the no second grid is clicked
+ elsif star_clicked?
+ state.current_input = :star
+ # When the star in the second grid is clicked
+ elsif star2_clicked?
+ state.current_input = :star2
+ # When the target in the no second grid is clicked
+ elsif target_clicked?
+ state.current_input = :target
+ # When the target in the second grid is clicked
+ elsif target2_clicked?
+ state.current_input = :target2
+ # When a wall in the first grid is clicked
+ elsif wall_clicked?
+ state.current_input = :remove_wall
+ # When a wall in the second grid is clicked
+ elsif wall2_clicked?
+ state.current_input = :remove_wall2
+ # When the first grid is clicked
+ elsif grid_clicked?
+ state.current_input = :add_wall
+ # When the second grid is clicked
+ elsif grid2_clicked?
+ state.current_input = :add_wall2
+ end
+ end
+
+ # Processes click and drag based on what the user is currently dragging
+ def process_input
+ if state.current_input == :star
+ input_star
+ elsif state.current_input == :star2
+ input_star2
+ elsif state.current_input == :target
+ input_target
+ elsif state.current_input == :target2
+ input_target2
+ elsif state.current_input == :remove_wall
+ input_remove_wall
+ elsif state.current_input == :remove_wall2
+ input_remove_wall2
+ elsif state.current_input == :add_wall
+ input_add_wall
+ elsif state.current_input == :add_wall2
+ input_add_wall2
+ end
+ end
+
+ # Moves the star to the cell closest to the mouse in the first grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def input_star
+ old_star = state.star.clone
+ state.star = cell_closest_to_mouse
+ unless old_star == state.star
+ reset_search
+ end
+ end
+
+ # Moves the star to the cell closest to the mouse in the second grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def input_star2
+ old_star = state.star.clone
+ state.star = cell_closest_to_mouse2
+ unless old_star == state.star
+ reset_search
+ end
+ end
+
+ # Moves the target to the grid closest to the mouse in the first grid
+ # Only reset_searchs the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def input_target
+ old_target = state.target.clone
+ state.target = cell_closest_to_mouse
+ unless old_target == state.target
+ reset_search
+ end
+ end
+
+ # Moves the target to the cell closest to the mouse in the second grid
+ # Only reset_searchs the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def input_target2
+ old_target = state.target.clone
+ state.target = cell_closest_to_mouse2
+ unless old_target == state.target
+ reset_search
+ end
+ end
+
+ # Removes walls in the first grid that are under the cursor
+ def input_remove_wall
+ # The mouse needs to be inside the grid, because we only want to remove walls
+ # the cursor is directly over
+ # Recalculations should only occur when a wall is actually deleted
+ if mouse_inside_grid?
+ if state.walls.has_key?(cell_closest_to_mouse)
+ state.walls.delete(cell_closest_to_mouse)
+ reset_search
+ end
+ end
+ end
+
+ # Removes walls in the second grid that are under the cursor
+ def input_remove_wall2
+ # 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_grid2?
+ if state.walls.has_key?(cell_closest_to_mouse2)
+ state.walls.delete(cell_closest_to_mouse2)
+ reset_search
+ end
+ end
+ end
+
+ # Adds a wall in the first grid in the cell the mouse is over
+ def input_add_wall
+ if mouse_inside_grid?
+ unless state.walls.has_key?(cell_closest_to_mouse)
+ state.walls[cell_closest_to_mouse] = true
+ reset_search
+ end
+ end
+ end
+
+
+ # Adds a wall in the second grid in the cell the mouse is over
+ def input_add_wall2
+ if mouse_inside_grid2?
+ unless state.walls.has_key?(cell_closest_to_mouse2)
+ state.walls[cell_closest_to_mouse2] = true
+ reset_search
+ end
+ end
+ end
+
+ # Whenever the user edits the grid,
+ # The search has to be reset_searchd upto the current step
+ # with the current grid as the initial state of the grid
+ def reset_search
+ # Reset_Searchs the search
+ state.frontier = []
+ state.visited = {}
+ state.early_exit_visited = {}
+ state.came_from = {}
+ state.path = {}
+ end
+
+ # Moves the search forward one step
+ def step
+ # The setup to the search
+ # Runs once when there are no visited cells
+ if state.visited.empty?
+ state.visited[state.star] = true
+ state.early_exit_visited[state.star] = true
+ state.frontier << state.star
+ state.came_from[state.star] = nil
+ 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 in the first grid
+ state.visited[neighbor] = true
+ # Unless the target has been visited
+ unless state.visited.has_key?(state.target)
+ # Mark the neighbor as visited in the second grid as well
+ state.early_exit_visited[neighbor] = true
+ end
+
+ # Add the neighbor to the frontier and remember which cell it came from
+ state.frontier << neighbor
+ state.came_from[neighbor] = new_frontier
+ 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 = []
+
+ # Gets all the valid neighbors into the array
+ # From southern neighbor, clockwise
+ neighbors << [cell.x, cell.y - 1] unless cell.y == 0
+ neighbors << [cell.x - 1, cell.y] unless cell.x == 0
+ neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1
+ neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1
+
+ # Sorts the neighbors so the rendered path is a zigzag path
+ # Cells in a diagonal direction are given priority
+ # Comment this line to see the difference
+ neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) }
+
+ neighbors
+ end
+
+ # Finds the vertical and horizontal distance of a cell from the star
+ # and returns the larger value
+ # This method is used to have a zigzag pattern in the rendered path
+ # A cell that is [5, 5] from the star,
+ # is explored before over a cell that is [0, 7] away.
+ # So, if possible, the search tries to go diagonal (zigzag) first
+ def proximity_to_star(x, y)
+ distance_x = (state.star.x - x).abs
+ distance_y = (state.star.y - y).abs
+
+ if distance_x > distance_y
+ return distance_x
+ else
+ return distance_y
+ end
+ end
+
+ # When the user grabs the star and puts their cursor to the far right
+ # and moves up and down, the star is supposed to move along the grid as well
+ # Finding the cell closest to the mouse helps with this
+ def cell_closest_to_mouse
+ # Closest cell to the mouse in the first grid
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Bound x and y to the grid
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ # 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 in the second grid helps with this
+ def cell_closest_to_mouse2
+ # Closest cell grid to the mouse in the second
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Translate the cell to the first grid
+ x -= grid.width + 1
+ # Bound x and y to the first grid
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ # Signal that the user is going to be moving the star from the first grid
+ def star_clicked?
+ inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.star))
+ end
+
+ # Signal that the user is going to be moving the star from the second grid
+ def star2_clicked?
+ inputs.mouse.down && inputs.mouse.point.inside_rect?(early_exit_scale_up(state.star))
+ end
+
+ # Signal that the user is going to be moving the target from the first grid
+ def target_clicked?
+ inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.target))
+ end
+
+ # Signal that the user is going to be moving the target from the second grid
+ def target2_clicked?
+ inputs.mouse.down && inputs.mouse.point.inside_rect?(early_exit_scale_up(state.target))
+ end
+
+ # Signal that the user is going to be removing walls from the first grid
+ def wall_clicked?
+ inputs.mouse.down && mouse_inside_wall?
+ end
+
+ # Signal that the user is going to be removing walls from the second grid
+ def wall2_clicked?
+ inputs.mouse.down && mouse_inside_wall2?
+ end
+
+ # Signal that the user is going to be adding walls from the first grid
+ def grid_clicked?
+ inputs.mouse.down && mouse_inside_grid?
+ end
+
+ # Signal that the user is going to be adding walls from the second grid
+ def grid2_clicked?
+ inputs.mouse.down && mouse_inside_grid2?
+ end
+
+ # Returns whether the mouse is inside of a wall in the first grid
+ # Part of the condition that checks whether the user is removing a wall
+ def mouse_inside_wall?
+ state.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(scale_up(wall))
+ end
+
+ false
+ end
+
+ # Returns whether the mouse is inside of a wall in the second grid
+ # Part of the condition that checks whether the user is removing a wall
+ def mouse_inside_wall2?
+ state.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(early_exit_scale_up(wall))
+ end
+
+ false
+ end
+
+ # Returns whether the mouse is inside of the first grid
+ # Part of the condition that checks whether the user is adding a wall
+ def mouse_inside_grid?
+ inputs.mouse.point.inside_rect?(scale_up(grid.rect))
+ end
+
+ # Returns whether the mouse is inside of the second grid
+ # Part of the condition that checks whether the user is adding a wall
+ def mouse_inside_grid2?
+ inputs.mouse.point.inside_rect?(early_exit_scale_up(grid.rect))
+ end
+
+ # These methods provide handy aliases to colors
+
+ # Light brown
+ def unvisited_color
+ [221, 212, 213]
+ end
+
+ # Camo Green
+ def wall_color
+ [134, 134, 120]
+ end
+
+ # Pastel White
+ def path_color
+ [231, 230, 228]
+ end
+
+ def red
+ [255, 0, 0]
+ end
+
+ # Makes code more concise
+ def grid
+ state.grid
+ 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
+ $early_exit_breadth_first_search ||= EarlyExitBreadthFirstSearch.new(args)
+ $early_exit_breadth_first_search.args = args
+ $early_exit_breadth_first_search.tick
+end
+
+
+def reset
+ $early_exit_breadth_first_search = nil
+end
diff --git a/samples/13_path_finding_algorithms/04_early_exit/star.png b/samples/13_path_finding_algorithms/04_early_exit/star.png
new file mode 100644
index 0000000..b37bb04
--- /dev/null
+++ b/samples/13_path_finding_algorithms/04_early_exit/star.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/04_early_exit/target.png b/samples/13_path_finding_algorithms/04_early_exit/target.png
new file mode 100644
index 0000000..fb2223d
--- /dev/null
+++ b/samples/13_path_finding_algorithms/04_early_exit/target.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/05_dijkstra/app/main.rb b/samples/13_path_finding_algorithms/05_dijkstra/app/main.rb
new file mode 100644
index 0000000..b335447
--- /dev/null
+++ b/samples/13_path_finding_algorithms/05_dijkstra/app/main.rb
@@ -0,0 +1,844 @@
+# Demonstrates how Dijkstra's Algorithm allows movement costs to be considered
+
+# Inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html
+
+# The first grid is a breadth first search with an early exit.
+# It shows a heat map of all the cells that were visited by the search and their relative distance.
+
+# The second grid is an implementation of Dijkstra's algorithm.
+# Light green cells have 5 times the movement cost of regular cells.
+# The heat map will darken based on movement cost.
+
+# Dark green cells are walls, and the search cannot go through them.
+class Movement_Costs
+ attr_gtk
+
+ # This method is called every frame/tick
+ # Every tick, the current state of the search is rendered on the screen,
+ # User input is processed, and
+ # The next step in the search is calculated
+ def tick
+ defaults
+ render
+ input
+ calc
+ end
+
+ def defaults
+ # Variables to edit the size and appearance of the grid
+ # Freely customizable to user's liking
+ grid.width ||= 10
+ grid.height ||= 10
+ grid.cell_size ||= 60
+ grid.rect ||= [0, 0, grid.width, grid.height]
+
+ # The location of the star and walls of the grid
+ # They can be modified to have a different initial grid
+ # Walls are stored in a hash for quick look up when doing the search
+ state.star ||= [1, 5]
+ state.target ||= [8, 4]
+ state.walls ||= {[1, 1] => true, [2, 1] => true, [3, 1] => true, [1, 2] => true, [2, 2] => true, [3, 2] => true}
+ state.hills ||= {
+ [4, 1] => true,
+ [5, 1] => true,
+ [4, 2] => true,
+ [5, 2] => true,
+ [6, 2] => true,
+ [4, 3] => true,
+ [5, 3] => true,
+ [6, 3] => true,
+ [3, 4] => true,
+ [4, 4] => true,
+ [5, 4] => true,
+ [6, 4] => true,
+ [7, 4] => true,
+ [3, 5] => true,
+ [4, 5] => true,
+ [5, 5] => true,
+ [6, 5] => true,
+ [7, 5] => true,
+ [4, 6] => true,
+ [5, 6] => true,
+ [6, 6] => true,
+ [7, 6] => true,
+ [4, 7] => true,
+ [5, 7] => true,
+ [6, 7] => true,
+ [4, 8] => true,
+ [5, 8] => true,
+ }
+
+ # What the user is currently editing on the grid
+ # We store this value, because we want to remember the value even when
+ # the user's cursor is no longer over what they're interacting with, but
+ # they are still clicking down on the mouse.
+ state.user_input ||= :none
+
+ # Values that are used for the breadth first search
+ # Keeping track of what cells were visited prevents counting cells multiple times
+ breadth_first_search.visited ||= {}
+ # The cells from which the breadth first search will expand
+ breadth_first_search.frontier ||= []
+ # Keeps track of which cell all cells were searched from
+ # Used to recreate the path from the target to the star
+ breadth_first_search.came_from ||= {}
+
+ # Keeps track of the movement cost so far to be at a cell
+ # Allows the costs of new cells to be quickly calculated
+ # Also doubles as a way to check if cells have already been visited
+ dijkstra_search.cost_so_far ||= {}
+ # The cells from which the Dijkstra search will expand
+ dijkstra_search.frontier ||= []
+ # Keeps track of which cell all cells were searched from
+ # Used to recreate the path from the target to the star
+ dijkstra_search.came_from ||= {}
+ end
+
+ # Draws everything onto the screen
+ def render
+ render_background
+
+ render_heat_maps
+
+ render_star
+ render_target
+ render_hills
+ render_walls
+
+ render_paths
+ end
+ # The methods below subdivide the task of drawing everything to the screen
+
+ # Draws what the grid looks like with nothing on it
+ def render_background
+ render_unvisited
+ render_grid_lines
+ render_labels
+ end
+
+ # Draws two rectangles the size of the grid in the default cell color
+ # Used as part of the background
+ def render_unvisited
+ outputs.solids << [scale_up(grid.rect), unvisited_color]
+ outputs.solids << [move_and_scale_up(grid.rect), unvisited_color]
+ end
+
+ # Draws grid lines to show the division of the grid into cells
+ def render_grid_lines
+ for x in 0..grid.width
+ outputs.lines << vertical_line(x)
+ outputs.lines << shifted_vertical_line(x)
+ end
+
+ for y in 0..grid.height
+ outputs.lines << horizontal_line(y)
+ outputs.lines << shifted_horizontal_line(y)
+ end
+ end
+
+ # Easy way to draw vertical lines given an index for the first grid
+ def vertical_line column
+ scale_up([column, 0, column, grid.height])
+ end
+
+ # Easy way to draw horizontal lines given an index for the second grid
+ def horizontal_line row
+ scale_up([0, row, grid.width, row])
+ end
+
+ # Easy way to draw vertical lines given an index for the first grid
+ def shifted_vertical_line column
+ scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height])
+ end
+
+ # Easy way to draw horizontal lines given an index for the second grid
+ def shifted_horizontal_line row
+ scale_up([grid.width + 1, row, grid.width + grid.width + 1, row])
+ end
+
+ # Labels the grids
+ def render_labels
+ outputs.labels << [175, 650, "Number of steps", 3]
+ outputs.labels << [925, 650, "Distance", 3]
+ end
+
+ def render_paths
+ render_breadth_first_search_path
+ render_dijkstra_path
+ end
+
+ def render_heat_maps
+ render_breadth_first_search_heat_map
+ render_dijkstra_heat_map
+ end
+
+ # Renders the breadth first search on the first grid
+ def render_breadth_first_search
+ end
+
+ # This heat map shows the cells explored by the breadth first search and how far they are from the star.
+ def render_breadth_first_search_heat_map
+ # For each cell explored
+ breadth_first_search.visited.each_key do | visited_cell |
+ # Find its distance from the star
+ distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs
+ max_distance = grid.width + grid.height
+ # Get it as a percent of the maximum distance and scale to 255 for use as an alpha value
+ alpha = 255.to_i * distance.to_i / max_distance.to_i
+ outputs.solids << [scale_up(visited_cell), red, alpha]
+ end
+ end
+
+ def render_breadth_first_search_path
+ # If the search found the target
+ if breadth_first_search.visited.has_key?(state.target)
+ # Start from the target
+ endpoint = state.target
+ # And the cell it came from
+ next_endpoint = breadth_first_search.came_from[endpoint]
+ while endpoint and next_endpoint
+ # Draw a path between these two cells
+ path = get_path_between(endpoint, next_endpoint)
+ outputs.solids << [scale_up(path), path_color]
+ # And get the next pair of cells
+ endpoint = next_endpoint
+ next_endpoint = breadth_first_search.came_from[endpoint]
+ # Continue till there are no more cells
+ end
+ end
+ end
+
+ # Renders the Dijkstra search on the second grid
+ def render_dijkstra
+ end
+
+ def render_dijkstra_heat_map
+ dijkstra_search.cost_so_far.each do |visited_cell, cost|
+ max_cost = (grid.width + grid.height) #* 5
+ alpha = 255.to_i * cost.to_i / max_cost.to_i
+ outputs.solids << [move_and_scale_up(visited_cell), red, alpha]
+ end
+ end
+
+ def render_dijkstra_path
+ # If the search found the target
+ if dijkstra_search.came_from.has_key?(state.target)
+ # Get the target and the cell it came from
+ endpoint = state.target
+ next_endpoint = dijkstra_search.came_from[endpoint]
+ while endpoint and next_endpoint
+ # Draw a path between them
+ path = get_path_between(endpoint, next_endpoint)
+ outputs.solids << [move_and_scale_up(path), path_color]
+
+ # Shift one cell down the path
+ endpoint = next_endpoint
+ next_endpoint = dijkstra_search.came_from[endpoint]
+
+ # Repeat till the end of the path
+ end
+ end
+ end
+
+ # Renders the star on both grids
+ def render_star
+ outputs.sprites << [scale_up(state.star), 'star.png']
+ outputs.sprites << [move_and_scale_up(state.star), 'star.png']
+ end
+
+ # Renders the target on both grids
+ def render_target
+ outputs.sprites << [scale_up(state.target), 'target.png']
+ outputs.sprites << [move_and_scale_up(state.target), 'target.png']
+ end
+
+ def render_hills
+ state.hills.each_key do |hill|
+ outputs.solids << [scale_up(hill), hill_color]
+ outputs.solids << [move_and_scale_up(hill), hill_color]
+ end
+ end
+
+ # Draws the walls on both grids
+ def render_walls
+ state.walls.each_key do |wall|
+ outputs.solids << [scale_up(wall), wall_color]
+ outputs.solids << [move_and_scale_up(wall), wall_color]
+ end
+ end
+
+ def get_path_between(cell_one, cell_two)
+ path = nil
+ if cell_one.x == cell_two.x
+ if cell_one.y < cell_two.y
+ path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4]
+ else
+ path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4]
+ end
+ else
+ if cell_one.x < cell_two.x
+ path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4]
+ else
+ path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4]
+ end
+ end
+ path
+ end
+
+ # Representation of how far away visited cells are from the star
+ # Replaces the render_visited method
+ # Visually demonstrates the effectiveness of early exit for pathfinding
+ def render_breadth_first_search_heat_map
+ breadth_first_search.visited.each_key do | visited_cell |
+ distance = (state.star.x - visited_cell.x).abs + (state.star.y - visited_cell.y).abs
+ max_distance = grid.width + grid.height
+ alpha = 255.to_i * distance.to_i / max_distance.to_i
+ outputs.solids << [scale_up(visited_cell), red, alpha]
+ end
+ end
+
+ # Translates the given cell grid.width + 1 to the right and then scales up
+ # Used to draw cells for the second grid
+ # This method does not work for lines,
+ # so separate methods exist for the grid lines
+ def move_and_scale_up(cell)
+ cell_clone = cell.clone
+ cell_clone.x += grid.width + 1
+ scale_up(cell_clone)
+ 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
+
+ # Handles user input every tick so the grid can be edited
+ # Separate input detection and processing is needed
+ # For example: Adding walls is started by clicking down on a hill,
+ # but the mouse doesn't need to remain over hills to add walls
+ def input
+ # If the mouse was lifted this tick
+ if inputs.mouse.up
+ # Set current input to none
+ state.user_input = :none
+ end
+
+ # If the mouse was clicked this tick
+ if inputs.mouse.down
+ # Determine what the user is editing and edit the state.user_input variable
+ determine_input
+ end
+
+ # Process user input based on user_input variable and current mouse position
+ process_input
+ end
+
+ # Determines what the user is editing and stores the value
+ # This method is called the tick the mouse is clicked
+ # Storing the value allows the user to continue the same edit as long as the
+ # mouse left click is held
+ def determine_input
+ # If the mouse is over the star in the first grid
+ if mouse_over_star?
+ # The user is editing the star from the first grid
+ state.user_input = :star
+ # If the mouse is over the star in the second grid
+ elsif mouse_over_star2?
+ # The user is editing the star from the second grid
+ state.user_input = :star2
+ # If the mouse is over the target in the first grid
+ elsif mouse_over_target?
+ # The user is editing the target from the first grid
+ state.user_input = :target
+ # If the mouse is over the target in the second grid
+ elsif mouse_over_target2?
+ # The user is editing the target from the second grid
+ state.user_input = :target2
+ # If the mouse is over a wall in the first grid
+ elsif mouse_over_wall?
+ # The user is removing a wall from the first grid
+ state.user_input = :remove_wall
+ # If the mouse is over a wall in the second grid
+ elsif mouse_over_wall2?
+ # The user is removing a wall from the second grid
+ state.user_input = :remove_wall2
+ # If the mouse is over a hill in the first grid
+ elsif mouse_over_hill?
+ # The user is adding a wall from the first grid
+ state.user_input = :add_wall
+ # If the mouse is over a hill in the second grid
+ elsif mouse_over_hill2?
+ # The user is adding a wall from the second grid
+ state.user_input = :add_wall2
+ # If the mouse is over the first grid
+ elsif mouse_over_grid?
+ # The user is adding a hill from the first grid
+ state.user_input = :add_hill
+ # If the mouse is over the second grid
+ elsif mouse_over_grid2?
+ # The user is adding a hill from the second grid
+ state.user_input = :add_hill2
+ end
+ end
+
+ # Processes click and drag based on what the user is currently dragging
+ def process_input
+ if state.user_input == :star
+ input_star
+ elsif state.user_input == :star2
+ input_star2
+ elsif state.user_input == :target
+ input_target
+ elsif state.user_input == :target2
+ input_target2
+ elsif state.user_input == :remove_wall
+ input_remove_wall
+ elsif state.user_input == :remove_wall2
+ input_remove_wall2
+ elsif state.user_input == :add_hill
+ input_add_hill
+ elsif state.user_input == :add_hill2
+ input_add_hill2
+ elsif state.user_input == :add_wall
+ input_add_wall
+ elsif state.user_input == :add_wall2
+ input_add_wall2
+ end
+ end
+
+ # Calculates the two searches
+ def calc
+ # If the searches have not started
+ if breadth_first_search.visited.empty?
+ # Calculate the two searches
+ calc_breadth_first
+ calc_dijkstra
+ end
+ end
+
+
+ def calc_breadth_first
+ # Sets up the Breadth First Search
+ breadth_first_search.visited[state.star] = true
+ breadth_first_search.frontier << state.star
+ breadth_first_search.came_from[state.star] = nil
+
+ until breadth_first_search.frontier.empty?
+ return if breadth_first_search.visited.has_key?(state.target)
+ # A step in the search
+ # Takes the next frontier cell
+ new_frontier = breadth_first_search.frontier.shift
+ # For each of its neighbors
+ adjacent_neighbors(new_frontier).each do | neighbor |
+ # That have not been visited and are not walls
+ unless breadth_first_search.visited.has_key?(neighbor) || state.walls.has_key?(neighbor)
+ # Add them to the frontier and mark them as visited in the first grid
+ breadth_first_search.visited[neighbor] = true
+ breadth_first_search.frontier << neighbor
+ # Remember which cell the neighbor came from
+ breadth_first_search.came_from[neighbor] = new_frontier
+ end
+ end
+ end
+ end
+
+ # Calculates the Dijkstra Search from the beginning to the end
+
+ def calc_dijkstra
+ # The initial values for the Dijkstra search
+ dijkstra_search.frontier << [state.star, 0]
+ dijkstra_search.came_from[state.star] = nil
+ dijkstra_search.cost_so_far[state.star] = 0
+
+ # Until their are no more cells to be explored
+ until dijkstra_search.frontier.empty?
+ # Get the next cell to be explored from
+ # We get the first element of the array which is the cell. The second element is the priority.
+ current = dijkstra_search.frontier.shift[0]
+
+ # Stop the search if we found the target
+ return if current == state.target
+
+ # For each of the neighbors
+ adjacent_neighbors(current).each do | neighbor |
+ # Unless this cell is a wall or has already been explored.
+ unless dijkstra_search.came_from.has_key?(neighbor) or state.walls.has_key?(neighbor)
+ # Calculate the movement cost of getting to this cell and memo
+ new_cost = dijkstra_search.cost_so_far[current] + cost(neighbor)
+ dijkstra_search.cost_so_far[neighbor] = new_cost
+
+ # Add this neighbor to the cells too be explored
+ dijkstra_search.frontier << [neighbor, new_cost]
+ dijkstra_search.came_from[neighbor] = current
+ end
+ end
+
+ # Sort the frontier so exploration occurs that have a low cost so far.
+ # My implementation of a priority queue
+ dijkstra_search.frontier = dijkstra_search.frontier.sort_by {|cell, priority| priority}
+ end
+ end
+
+ def cost(cell)
+ if state.hills.has_key?(cell)
+ return 5
+ else
+ return 1
+ end
+ end
+
+
+
+
+ # Moves the star to the cell closest to the mouse in the first grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def input_star
+ old_star = state.star.clone
+ unless cell_closest_to_mouse == state.target
+ state.star = cell_closest_to_mouse
+ end
+ unless old_star == state.star
+ reset_search
+ end
+ end
+
+ # Moves the star to the cell closest to the mouse in the second grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def input_star2
+ old_star = state.star.clone
+ unless cell_closest_to_mouse2 == state.target
+ state.star = cell_closest_to_mouse2
+ end
+ unless old_star == state.star
+ reset_search
+ end
+ end
+
+ # Moves the target to the grid closest to the mouse in the first grid
+ # Only reset_searchs the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def input_target
+ old_target = state.target.clone
+ unless cell_closest_to_mouse == state.star
+ state.target = cell_closest_to_mouse
+ end
+ unless old_target == state.target
+ reset_search
+ end
+ end
+
+ # Moves the target to the cell closest to the mouse in the second grid
+ # Only reset_searchs the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def input_target2
+ old_target = state.target.clone
+ unless cell_closest_to_mouse2 == state.star
+ state.target = cell_closest_to_mouse2
+ end
+ unless old_target == state.target
+ reset_search
+ end
+ end
+
+ # Removes walls in the first grid that are under the cursor
+ def input_remove_wall
+ # The mouse needs to be inside the grid, because we only want to remove walls
+ # the cursor is directly over
+ # Recalculations should only occur when a wall is actually deleted
+ if mouse_over_grid?
+ if state.walls.has_key?(cell_closest_to_mouse) or state.hills.has_key?(cell_closest_to_mouse)
+ state.walls.delete(cell_closest_to_mouse)
+ state.hills.delete(cell_closest_to_mouse)
+ reset_search
+ end
+ end
+ end
+
+ # Removes walls in the second grid that are under the cursor
+ def input_remove_wall2
+ # 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_over_grid2?
+ if state.walls.has_key?(cell_closest_to_mouse2) or state.hills.has_key?(cell_closest_to_mouse2)
+ state.walls.delete(cell_closest_to_mouse2)
+ state.hills.delete(cell_closest_to_mouse2)
+ reset_search
+ end
+ end
+ end
+
+ # Adds a hill in the first grid in the cell the mouse is over
+ def input_add_hill
+ if mouse_over_grid?
+ unless state.hills.has_key?(cell_closest_to_mouse)
+ state.hills[cell_closest_to_mouse] = true
+ reset_search
+ end
+ end
+ end
+
+
+ # Adds a hill in the second grid in the cell the mouse is over
+ def input_add_hill2
+ if mouse_over_grid2?
+ unless state.hills.has_key?(cell_closest_to_mouse2)
+ state.hills[cell_closest_to_mouse2] = true
+ reset_search
+ end
+ end
+ end
+
+ # Adds a wall in the first grid in the cell the mouse is over
+ def input_add_wall
+ if mouse_over_grid?
+ unless state.walls.has_key?(cell_closest_to_mouse)
+ state.hills.delete(cell_closest_to_mouse)
+ state.walls[cell_closest_to_mouse] = true
+ reset_search
+ end
+ end
+ end
+
+ # Adds a wall in the second grid in the cell the mouse is over
+ def input_add_wall2
+ if mouse_over_grid2?
+ unless state.walls.has_key?(cell_closest_to_mouse2)
+ state.hills.delete(cell_closest_to_mouse2)
+ state.walls[cell_closest_to_mouse2] = true
+ reset_search
+ end
+ end
+ end
+
+ # Whenever the user edits the grid,
+ # The search has to be reset_searchd upto the current step
+ # with the current grid as the initial state of the grid
+ def reset_search
+ breadth_first_search.visited = {}
+ breadth_first_search.frontier = []
+ breadth_first_search.came_from = {}
+
+ dijkstra_search.frontier = []
+ dijkstra_search.came_from = {}
+ dijkstra_search.cost_so_far = {}
+ end
+
+
+
+ # Returns a list of adjacent cells
+ # Used to determine what the next cells to be added to the frontier are
+ def adjacent_neighbors(cell)
+ neighbors = []
+
+ # Gets all the valid neighbors into the array
+ # From southern neighbor, clockwise
+ neighbors << [cell.x , cell.y - 1] unless cell.y == 0
+ neighbors << [cell.x - 1, cell.y ] unless cell.x == 0
+ neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1
+ neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1
+
+ # Sorts the neighbors so the rendered path is a zigzag path
+ # Cells in a diagonal direction are given priority
+ # Comment this line to see the difference
+ neighbors = neighbors.sort_by { |neighbor_x, neighbor_y| proximity_to_star(neighbor_x, neighbor_y) }
+
+ neighbors
+ end
+
+ # Finds the vertical and horizontal distance of a cell from the star
+ # and returns the larger value
+ # This method is used to have a zigzag pattern in the rendered path
+ # A cell that is [5, 5] from the star,
+ # is explored before over a cell that is [0, 7] away.
+ # So, if possible, the search tries to go diagonal (zigzag) first
+ def proximity_to_star(x, y)
+ distance_x = (state.star.x - x).abs
+ distance_y = (state.star.y - y).abs
+
+ if distance_x > distance_y
+ return distance_x
+ else
+ return distance_y
+ end
+ end
+
+ # When the user grabs the star and puts their cursor to the far right
+ # and moves up and down, the star is supposed to move along the grid as well
+ # Finding the cell closest to the mouse helps with this
+ def cell_closest_to_mouse
+ # Closest cell to the mouse in the first grid
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Bound x and y to the grid
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ # 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 in the second grid helps with this
+ def cell_closest_to_mouse2
+ # Closest cell grid to the mouse in the second
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Translate the cell to the first grid
+ x -= grid.width + 1
+ # Bound x and y to the first grid
+ x = 0 if x < 0
+ y = 0 if y < 0
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ # Signal that the user is going to be moving the star from the first grid
+ def mouse_over_star?
+ inputs.mouse.point.inside_rect?(scale_up(state.star))
+ end
+
+ # Signal that the user is going to be moving the star from the second grid
+ def mouse_over_star2?
+ inputs.mouse.point.inside_rect?(move_and_scale_up(state.star))
+ end
+
+ # Signal that the user is going to be moving the target from the first grid
+ def mouse_over_target?
+ inputs.mouse.point.inside_rect?(scale_up(state.target))
+ end
+
+ # Signal that the user is going to be moving the target from the second grid
+ def mouse_over_target2?
+ inputs.mouse.point.inside_rect?(move_and_scale_up(state.target))
+ end
+
+ # Signal that the user is going to be removing walls from the first grid
+ def mouse_over_wall?
+ state.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(scale_up(wall))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be removing walls from the second grid
+ def mouse_over_wall2?
+ state.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(move_and_scale_up(wall))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be removing hills from the first grid
+ def mouse_over_hill?
+ state.hills.each_key do | hill |
+ return true if inputs.mouse.point.inside_rect?(scale_up(hill))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be removing hills from the second grid
+ def mouse_over_hill2?
+ state.hills.each_key do | hill |
+ return true if inputs.mouse.point.inside_rect?(move_and_scale_up(hill))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be adding walls from the first grid
+ def mouse_over_grid?
+ inputs.mouse.point.inside_rect?(scale_up(grid.rect))
+ end
+
+ # Signal that the user is going to be adding walls from the second grid
+ def mouse_over_grid2?
+ inputs.mouse.point.inside_rect?(move_and_scale_up(grid.rect))
+ end
+
+ # These methods provide handy aliases to colors
+
+ # Light brown
+ def unvisited_color
+ [221, 212, 213]
+ end
+
+ # Camo Green
+ def wall_color
+ [134, 134, 120]
+ end
+
+ # Pastel White
+ def path_color
+ [231, 230, 228]
+ end
+
+ def red
+ [255, 0, 0]
+ end
+
+ # A Green
+ def hill_color
+ [139, 173, 132]
+ end
+
+ # Makes code more concise
+ def grid
+ state.grid
+ end
+
+ def breadth_first_search
+ state.breadth_first_search
+ end
+
+ def dijkstra_search
+ state.dijkstra_search
+ end
+end
+
+# Method that is called by DragonRuby periodically
+# Used for updating animations and calculations
+def tick args
+
+ # Pressing r will reset the application
+ if args.inputs.keyboard.key_down.r
+ args.gtk.reset
+ reset
+ return
+ end
+
+ # Every tick, new args are passed, and the Dijkstra tick method is called
+ $movement_costs ||= Movement_Costs.new(args)
+ $movement_costs.args = args
+ $movement_costs.tick
+end
+
+
+def reset
+ $movement_costs = nil
+end
diff --git a/samples/13_path_finding_algorithms/05_dijkstra/star.png b/samples/13_path_finding_algorithms/05_dijkstra/star.png
new file mode 100644
index 0000000..b37bb04
--- /dev/null
+++ b/samples/13_path_finding_algorithms/05_dijkstra/star.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/05_dijkstra/target.png b/samples/13_path_finding_algorithms/05_dijkstra/target.png
new file mode 100644
index 0000000..fb2223d
--- /dev/null
+++ b/samples/13_path_finding_algorithms/05_dijkstra/target.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/06_heuristic/app/main.rb b/samples/13_path_finding_algorithms/06_heuristic/app/main.rb
new file mode 100644
index 0000000..af466a5
--- /dev/null
+++ b/samples/13_path_finding_algorithms/06_heuristic/app/main.rb
@@ -0,0 +1,980 @@
+# This program is inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html
+
+# This time the heuristic search still explored less of the grid, hence finishing faster.
+# However, it did not find the shortest path between the star and the target.
+
+# The only difference between this app and Heuristic is the change of the starting position.
+
+class Heuristic_With_Walls
+ attr_gtk
+
+ def tick
+ defaults
+ render
+ input
+ # If animation is playing, and max steps have not been reached
+ # Move the search a step forward
+ if state.play && state.current_step < state.max_steps
+ # Variable that tells the program what step to recalculate up to
+ state.current_step += 1
+ move_searches_one_step_forward
+ end
+ end
+
+ def defaults
+ # Variables to edit the size and appearance of the grid
+ # Freely customizable to user's liking
+ grid.width ||= 15
+ grid.height ||= 15
+ grid.cell_size ||= 40
+ grid.rect ||= [0, 0, grid.width, grid.height]
+
+ grid.star ||= [0, 2]
+ grid.target ||= [14, 12]
+ grid.walls ||= {}
+ # There are no hills in the Heuristic Search Demo
+
+ # What the user is currently editing on the grid
+ # We store this value, because we want to remember the value even when
+ # the user's cursor is no longer over what they're interacting with, but
+ # they are still clicking down on the mouse.
+ state.user_input ||= :none
+
+ # These variables allow the breadth first search to take place
+ # 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.
+ # Used to prevent searching cells that have already been found
+ # and to trace a path from the target back to the starting point.
+ # Frontier is an array of cells to expand the search from.
+ # The search is over when there are no more cells to search from.
+ # Path stores the path from the target to the star, once the target has been found
+ # It prevents calculating the path every tick.
+ bfs.came_from ||= {}
+ bfs.frontier ||= []
+ bfs.path ||= []
+
+ heuristic.came_from ||= {}
+ heuristic.frontier ||= []
+ heuristic.path ||= []
+
+ # Stores which step of the animation is being rendered
+ # When the user moves the star or messes with the walls,
+ # the searches are recalculated up to this step
+
+ # Unless the current step has a value
+ unless state.current_step
+ # Set the current step to 10
+ state.current_step = 10
+ # And calculate the searches up to step 10
+ recalculate_searches
+ end
+
+ # 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
+ state.max_steps = grid.width * 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
+ # An if statement instead of the ||= operator is used for assigning a boolean value.
+ # The || operator does not differentiate between nil and false.
+ if state.play == nil
+ state.play = false
+ end
+
+ # 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
+ buttons.left = [470, 600, 50, 50]
+ buttons.center = [520, 600, 200, 50]
+ buttons.right = [720, 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
+ slider.x = 440
+ slider.y = 675
+ # This is the width of the line
+ 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
+ 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
+ slider.spacing = slider.w.to_f / state.max_steps.to_f
+ end
+
+ # All methods with render draw stuff on the screen
+ # UI has buttons, the slider, and labels
+ # The search specific rendering occurs in the respective methods
+ def render
+ render_ui
+ render_bfs
+ render_heuristic
+ end
+
+ def render_ui
+ render_buttons
+ render_slider
+ render_labels
+ end
+
+ def render_buttons
+ render_left_button
+ render_center_button
+ render_right_button
+ end
+
+ def render_bfs
+ render_bfs_grid
+ render_bfs_star
+ render_bfs_target
+ render_bfs_visited
+ render_bfs_walls
+ render_bfs_frontier
+ render_bfs_path
+ end
+
+ def render_heuristic
+ render_heuristic_grid
+ render_heuristic_star
+ render_heuristic_target
+ render_heuristic_visited
+ render_heuristic_walls
+ render_heuristic_frontier
+ render_heuristic_path
+ end
+
+ # This method handles user input every tick
+ def input
+ # Check and handle button input
+ input_buttons
+
+ # If the mouse was lifted this tick
+ if inputs.mouse.up
+ # Set current input to none
+ state.user_input = :none
+ end
+
+ # If the mouse was clicked this tick
+ if inputs.mouse.down
+ # Determine what the user is editing and appropriately edit the state.user_input variable
+ determine_input
+ end
+
+ # Process user input based on user_input variable and current mouse position
+ process_input
+ end
+
+ # Determines what the user is editing
+ # This method is called when the mouse is clicked down
+ def determine_input
+ if mouse_over_slider?
+ state.user_input = :slider
+ # If the mouse is over the star in the first grid
+ elsif bfs_mouse_over_star?
+ # The user is editing the star from the first grid
+ state.user_input = :bfs_star
+ # If the mouse is over the star in the second grid
+ elsif heuristic_mouse_over_star?
+ # The user is editing the star from the second grid
+ state.user_input = :heuristic_star
+ # If the mouse is over the target in the first grid
+ elsif bfs_mouse_over_target?
+ # The user is editing the target from the first grid
+ state.user_input = :bfs_target
+ # If the mouse is over the target in the second grid
+ elsif heuristic_mouse_over_target?
+ # The user is editing the target from the second grid
+ state.user_input = :heuristic_target
+ # If the mouse is over a wall in the first grid
+ elsif bfs_mouse_over_wall?
+ # The user is removing a wall from the first grid
+ state.user_input = :bfs_remove_wall
+ # If the mouse is over a wall in the second grid
+ elsif heuristic_mouse_over_wall?
+ # The user is removing a wall from the second grid
+ state.user_input = :heuristic_remove_wall
+ # If the mouse is over the first grid
+ elsif bfs_mouse_over_grid?
+ # The user is adding a wall from the first grid
+ state.user_input = :bfs_add_wall
+ # If the mouse is over the second grid
+ elsif heuristic_mouse_over_grid?
+ # The user is adding a wall from the second grid
+ state.user_input = :heuristic_add_wall
+ end
+ end
+
+ # Processes click and drag based on what the user is currently dragging
+ def process_input
+ if state.user_input == :slider
+ process_input_slider
+ elsif state.user_input == :bfs_star
+ process_input_bfs_star
+ elsif state.user_input == :heuristic_star
+ process_input_heuristic_star
+ elsif state.user_input == :bfs_target
+ process_input_bfs_target
+ elsif state.user_input == :heuristic_target
+ process_input_heuristic_target
+ elsif state.user_input == :bfs_remove_wall
+ process_input_bfs_remove_wall
+ elsif state.user_input == :heuristic_remove_wall
+ process_input_heuristic_remove_wall
+ elsif state.user_input == :bfs_add_wall
+ process_input_bfs_add_wall
+ elsif state.user_input == :heuristic_add_wall
+ process_input_heuristic_add_wall
+ end
+ end
+
+ 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.current_step * 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
+
+ def render_labels
+ outputs.labels << [205, 625, "Breadth First Search"]
+ outputs.labels << [820, 625, "Heuristic Best-First Search"]
+ end
+
+ def render_left_button
+ # Draws the button_color button, and a black border
+ # The border separates the buttons visually
+ outputs.solids << [buttons.left, button_color]
+ outputs.borders << [buttons.left]
+
+ # 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 button_color button, and a black border
+ # The border separates the buttons visually
+ outputs.solids << [buttons.center, button_color]
+ outputs.borders << [buttons.center]
+
+ # 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 button_color button, and a black border
+ # The border separates the buttons visually
+ outputs.solids << [buttons.right, button_color]
+ outputs.borders << [buttons.right]
+
+ # 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
+
+ def render_bfs_grid
+ # A large rect the size of the grid
+ outputs.solids << [bfs_scale_up(grid.rect), default_color]
+
+ # The vertical grid lines
+ for x in 0..grid.width
+ outputs.lines << bfs_vertical_line(x)
+ end
+
+ # The horizontal grid lines
+ for y in 0..grid.height
+ outputs.lines << bfs_horizontal_line(y)
+ end
+ end
+
+ def render_heuristic_grid
+ # A large rect the size of the grid
+ outputs.solids << [heuristic_scale_up(grid.rect), default_color]
+
+ # The vertical grid lines
+ for x in 0..grid.width
+ outputs.lines << heuristic_vertical_line(x)
+ end
+
+ # The horizontal grid lines
+ for y in 0..grid.height
+ outputs.lines << heuristic_horizontal_line(y)
+ end
+ end
+
+ # Returns a vertical line for a column of the first grid
+ def bfs_vertical_line column
+ bfs_scale_up([column, 0, column, grid.height])
+ end
+
+ # Returns a horizontal line for a column of the first grid
+ def bfs_horizontal_line row
+ bfs_scale_up([0, row, grid.width, row])
+ end
+
+ # Returns a vertical line for a column of the second grid
+ def heuristic_vertical_line column
+ bfs_scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height])
+ end
+
+ # Returns a horizontal line for a column of the second grid
+ def heuristic_horizontal_line row
+ bfs_scale_up([grid.width + 1, row, grid.width + grid.width + 1, row])
+ end
+
+ # Renders the star on the first grid
+ def render_bfs_star
+ outputs.sprites << [bfs_scale_up(grid.star), 'star.png']
+ end
+
+ # Renders the star on the second grid
+ def render_heuristic_star
+ outputs.sprites << [heuristic_scale_up(grid.star), 'star.png']
+ end
+
+ # Renders the target on the first grid
+ def render_bfs_target
+ outputs.sprites << [bfs_scale_up(grid.target), 'target.png']
+ end
+
+ # Renders the target on the second grid
+ def render_heuristic_target
+ outputs.sprites << [heuristic_scale_up(grid.target), 'target.png']
+ end
+
+ # Renders the walls on the first grid
+ def render_bfs_walls
+ grid.walls.each_key do | wall |
+ outputs.solids << [bfs_scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders the walls on the second grid
+ def render_heuristic_walls
+ grid.walls.each_key do | wall |
+ outputs.solids << [heuristic_scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders the visited cells on the first grid
+ def render_bfs_visited
+ bfs.came_from.each_key do | visited_cell |
+ outputs.solids << [bfs_scale_up(visited_cell), visited_color]
+ end
+ end
+
+ # Renders the visited cells on the second grid
+ def render_heuristic_visited
+ heuristic.came_from.each_key do | visited_cell |
+ outputs.solids << [heuristic_scale_up(visited_cell), visited_color]
+ end
+ end
+
+ # Renders the frontier cells on the first grid
+ def render_bfs_frontier
+ bfs.frontier.each do | frontier_cell |
+ outputs.solids << [bfs_scale_up(frontier_cell), frontier_color, 200]
+ end
+ end
+
+ # Renders the frontier cells on the second grid
+ def render_heuristic_frontier
+ heuristic.frontier.each do | frontier_cell |
+ outputs.solids << [heuristic_scale_up(frontier_cell), frontier_color, 200]
+ end
+ end
+
+ # Renders the path found by the breadth first search on the first grid
+ def render_bfs_path
+ bfs.path.each do | path |
+ outputs.solids << [bfs_scale_up(path), path_color]
+ end
+ end
+
+ # Renders the path found by the heuristic search on the second grid
+ def render_heuristic_path
+ heuristic.path.each do | path |
+ outputs.solids << [heuristic_scale_up(path), path_color]
+ end
+ end
+
+ # Returns the rect for the path between two cells based on their relative positions
+ def get_path_between(cell_one, cell_two)
+ path = nil
+
+ # If cell one is above cell two
+ if cell_one.x == cell_two.x and cell_one.y > cell_two.y
+ # Path starts from the center of cell two and moves upward to the center of cell one
+ path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4]
+ # If cell one is below cell two
+ elsif cell_one.x == cell_two.x and cell_one.y < cell_two.y
+ # Path starts from the center of cell one and moves upward to the center of cell two
+ path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4]
+ # If cell one is to the left of cell two
+ elsif cell_one.x > cell_two.x and cell_one.y == cell_two.y
+ # Path starts from the center of cell two and moves rightward to the center of cell one
+ path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4]
+ # If cell one is to the right of cell two
+ elsif cell_one.x < cell_two.x and cell_one.y == cell_two.y
+ # Path starts from the center of cell one and moves rightward to the center of cell two
+ path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4]
+ end
+
+ path
+ 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
+ # This method scales up cells for the first grid
+ def bfs_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
+
+ # Translates the given cell grid.width + 1 to the right and then scales up
+ # Used to draw cells for the second grid
+ # This method does not work for lines,
+ # so separate methods exist for the grid lines
+ def heuristic_scale_up(cell)
+ # Prevents the original value of cell from being edited
+ cell = cell.clone
+ # Translates the cell to the second grid equivalent
+ cell.x += grid.width + 1
+ # Proceeds as if scaling up for the first grid
+ bfs_scale_up(cell)
+ end
+
+ # Checks and handles input for the buttons
+ # Called when the mouse is lifted
+ def input_buttons
+ input_left_button
+ input_center_button
+ input_right_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.current_step -= 1
+ recalculate_searches
+ 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? || 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_right_button
+ if right_button_clicked?
+ state.play = false
+ state.current_step += 1
+ move_searches_one_step_forward
+ end
+ end
+
+ # These methods detect when the buttons are clicked
+ def left_button_clicked?
+ inputs.mouse.point.inside_rect?(buttons.left) && inputs.mouse.up
+ end
+
+ def center_button_clicked?
+ inputs.mouse.point.inside_rect?(buttons.center) && inputs.mouse.up
+ end
+
+ def right_button_clicked?
+ inputs.mouse.point.inside_rect?(buttons.right) && inputs.mouse.up
+ end
+
+
+ # Signal that the user is going to be moving the slider
+ # Is the mouse over the circle of the slider?
+ def mouse_over_slider?
+ circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing)
+ circle_y = (slider.y - slider.offset)
+ circle_rect = [circle_x, circle_y, 37, 37]
+ inputs.mouse.point.inside_rect?(circle_rect)
+ end
+
+ # Signal that the user is going to be moving the star from the first grid
+ def bfs_mouse_over_star?
+ inputs.mouse.point.inside_rect?(bfs_scale_up(grid.star))
+ end
+
+ # Signal that the user is going to be moving the star from the second grid
+ def heuristic_mouse_over_star?
+ inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.star))
+ end
+
+ # Signal that the user is going to be moving the target from the first grid
+ def bfs_mouse_over_target?
+ inputs.mouse.point.inside_rect?(bfs_scale_up(grid.target))
+ end
+
+ # Signal that the user is going to be moving the target from the second grid
+ def heuristic_mouse_over_target?
+ inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.target))
+ end
+
+ # Signal that the user is going to be removing walls from the first grid
+ def bfs_mouse_over_wall?
+ grid.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(bfs_scale_up(wall))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be removing walls from the second grid
+ def heuristic_mouse_over_wall?
+ grid.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(heuristic_scale_up(wall))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be adding walls from the first grid
+ def bfs_mouse_over_grid?
+ inputs.mouse.point.inside_rect?(bfs_scale_up(grid.rect))
+ end
+
+ # Signal that the user is going to be adding walls from the second grid
+ def heuristic_mouse_over_grid?
+ inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.rect))
+ 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 process_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.current_step = ((mouse_x - slider.x) / slider.spacing).to_i
+
+ recalculate_searches
+ end
+
+ # Moves the star to the cell closest to the mouse in the first grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def process_input_bfs_star
+ old_star = grid.star.clone
+ unless bfs_cell_closest_to_mouse == grid.target
+ grid.star = bfs_cell_closest_to_mouse
+ end
+ unless old_star == grid.star
+ recalculate_searches
+ end
+ end
+
+ # Moves the star to the cell closest to the mouse in the second grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def process_input_heuristic_star
+ old_star = grid.star.clone
+ unless heuristic_cell_closest_to_mouse == grid.target
+ grid.star = heuristic_cell_closest_to_mouse
+ end
+ unless old_star == grid.star
+ recalculate_searches
+ end
+ end
+
+ # Moves the target to the grid closest to the mouse in the first grid
+ # Only recalculate_searchess the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def process_input_bfs_target
+ old_target = grid.target.clone
+ unless bfs_cell_closest_to_mouse == grid.star
+ grid.target = bfs_cell_closest_to_mouse
+ end
+ unless old_target == grid.target
+ recalculate_searches
+ end
+ end
+
+ # Moves the target to the cell closest to the mouse in the second grid
+ # Only recalculate_searchess the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def process_input_heuristic_target
+ old_target = grid.target.clone
+ unless heuristic_cell_closest_to_mouse == grid.star
+ grid.target = heuristic_cell_closest_to_mouse
+ end
+ unless old_target == grid.target
+ recalculate_searches
+ end
+ end
+
+ # Removes walls in the first grid that are under the cursor
+ def process_input_bfs_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 bfs_mouse_over_grid?
+ if grid.walls.has_key?(bfs_cell_closest_to_mouse)
+ grid.walls.delete(bfs_cell_closest_to_mouse)
+ recalculate_searches
+ end
+ end
+ end
+
+ # Removes walls in the second grid that are under the cursor
+ def process_input_heuristic_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 heuristic_mouse_over_grid?
+ if grid.walls.has_key?(heuristic_cell_closest_to_mouse)
+ grid.walls.delete(heuristic_cell_closest_to_mouse)
+ recalculate_searches
+ end
+ end
+ end
+ # Adds a wall in the first grid in the cell the mouse is over
+ def process_input_bfs_add_wall
+ if bfs_mouse_over_grid?
+ unless grid.walls.has_key?(bfs_cell_closest_to_mouse)
+ grid.walls[bfs_cell_closest_to_mouse] = true
+ recalculate_searches
+ end
+ end
+ end
+
+ # Adds a wall in the second grid in the cell the mouse is over
+ def process_input_heuristic_add_wall
+ if heuristic_mouse_over_grid?
+ unless grid.walls.has_key?(heuristic_cell_closest_to_mouse)
+ grid.walls[heuristic_cell_closest_to_mouse] = true
+ recalculate_searches
+ end
+ end
+ end
+
+ # When the user grabs the star and puts their cursor to the far right
+ # and moves up and down, the star is supposed to move along the grid as well
+ # Finding the cell closest to the mouse helps with this
+ def bfs_cell_closest_to_mouse
+ # Closest cell to the mouse in the first grid
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Bound x and y to the grid
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ # 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 in the second grid helps with this
+ def heuristic_cell_closest_to_mouse
+ # Closest cell grid to the mouse in the second
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Translate the cell to the first grid
+ x -= grid.width + 1
+ # Bound x and y to the first grid
+ x = 0 if x < 0
+ y = 0 if y < 0
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ def recalculate_searches
+ # Reset the searches
+ bfs.came_from = {}
+ bfs.frontier = []
+ bfs.path = []
+ heuristic.came_from = {}
+ heuristic.frontier = []
+ heuristic.path = []
+
+ # Move the searches forward to the current step
+ state.current_step.times { move_searches_one_step_forward }
+ end
+
+ def move_searches_one_step_forward
+ bfs_one_step_forward
+ heuristic_one_step_forward
+ end
+
+ def bfs_one_step_forward
+ return if bfs.came_from.has_key?(grid.target)
+
+ # Only runs at the beginning of the search as setup.
+ if bfs.came_from.empty?
+ bfs.frontier << grid.star
+ bfs.came_from[grid.star] = nil
+ end
+
+ # A step in the search
+ unless bfs.frontier.empty?
+ # Takes the next frontier cell
+ new_frontier = bfs.frontier.shift
+ # For each of its neighbors
+ adjacent_neighbors(new_frontier).each do |neighbor|
+ # That have not been visited and are not walls
+ unless bfs.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor)
+ # Add them to the frontier and mark them as visited
+ bfs.frontier << neighbor
+ bfs.came_from[neighbor] = new_frontier
+ end
+ end
+ end
+
+ # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
+ # Comment this line and let a path generate to see the difference
+ bfs.frontier = bfs.frontier.sort_by {| cell | proximity_to_star(cell) }
+
+ # If the search found the target
+ if bfs.came_from.has_key?(grid.target)
+ # Calculate the path between the target and star
+ bfs_calc_path
+ end
+ end
+
+ # Calculates the path between the target and star for the breadth first search
+ # Only called when the breadth first search finds the target
+ def bfs_calc_path
+ # Start from the target
+ endpoint = grid.target
+ # And the cell it came from
+ next_endpoint = bfs.came_from[endpoint]
+ while endpoint and next_endpoint
+ # Draw a path between these two cells and store it
+ path = get_path_between(endpoint, next_endpoint)
+ bfs.path << path
+ # And get the next pair of cells
+ endpoint = next_endpoint
+ next_endpoint = bfs.came_from[endpoint]
+ # Continue till there are no more cells
+ end
+ end
+
+ # Moves the heuristic search forward one step
+ # Can be called from tick while the animation is playing
+ # Can also be called when recalculating the searches after the user edited the grid
+ def heuristic_one_step_forward
+ # Stop the search if the target has been found
+ return if heuristic.came_from.has_key?(grid.target)
+
+ # If the search has not begun
+ if heuristic.came_from.empty?
+ # Setup the search to begin from the star
+ heuristic.frontier << grid.star
+ heuristic.came_from[grid.star] = nil
+ end
+
+ # One step in the heuristic search
+
+ # Unless there are no more cells to explore from
+ unless heuristic.frontier.empty?
+ # Get the next cell to explore from
+ new_frontier = heuristic.frontier.shift
+ # For each of its neighbors
+ adjacent_neighbors(new_frontier).each do |neighbor|
+ # That have not been visited and are not walls
+ unless heuristic.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor)
+ # Add them to the frontier and mark them as visited
+ heuristic.frontier << neighbor
+ heuristic.came_from[neighbor] = new_frontier
+ end
+ end
+ end
+
+ # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
+ heuristic.frontier = heuristic.frontier.sort_by {| cell | proximity_to_star(cell) }
+ # Sort the frontier so cells that are close to the target are then prioritized
+ heuristic.frontier = heuristic.frontier.sort_by {| cell | heuristic_heuristic(cell) }
+
+ # If the search found the target
+ if heuristic.came_from.has_key?(grid.target)
+ # Calculate the path between the target and star
+ heuristic_calc_path
+ end
+ end
+
+ # Returns one-dimensional absolute distance between cell and target
+ # Returns a number to compare distances between cells and the target
+ def heuristic_heuristic(cell)
+ (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs
+ end
+
+ # Calculates the path between the target and star for the heuristic search
+ # Only called when the heuristic search finds the target
+ def heuristic_calc_path
+ # Start from the target
+ endpoint = grid.target
+ # And the cell it came from
+ next_endpoint = heuristic.came_from[endpoint]
+ while endpoint and next_endpoint
+ # Draw a path between these two cells and store it
+ path = get_path_between(endpoint, next_endpoint)
+ heuristic.path << path
+ # And get the next pair of cells
+ endpoint = next_endpoint
+ next_endpoint = heuristic.came_from[endpoint]
+ # Continue till there are no more cells
+ 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 = []
+
+ # Gets all the valid neighbors into the array
+ # From southern neighbor, clockwise
+ neighbors << [cell.x , cell.y - 1] unless cell.y == 0
+ neighbors << [cell.x - 1, cell.y ] unless cell.x == 0
+ neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1
+ neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1
+
+ neighbors
+ end
+
+ # Finds the vertical and horizontal distance of a cell from the star
+ # and returns the larger value
+ # This method is used to have a zigzag pattern in the rendered path
+ # A cell that is [5, 5] from the star,
+ # is explored before over a cell that is [0, 7] away.
+ # So, if possible, the search tries to go diagonal (zigzag) first
+ def proximity_to_star(cell)
+ distance_x = (grid.star.x - cell.x).abs
+ distance_y = (grid.star.y - cell.y).abs
+
+ if distance_x > distance_y
+ return distance_x
+ else
+ return distance_y
+ end
+ end
+
+ # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored.
+ def grid
+ state.grid
+ end
+
+ def buttons
+ state.buttons
+ end
+
+ def slider
+ state.slider
+ end
+
+ def bfs
+ state.bfs
+ end
+
+ def heuristic
+ state.heuristic
+ end
+
+ # Descriptive aliases for colors
+ def default_color
+ [221, 212, 213] # Light Brown
+ end
+
+ def wall_color
+ [134, 134, 120] # Camo Green
+ end
+
+ def visited_color
+ [204, 191, 179] # Dark Brown
+ end
+
+ def frontier_color
+ [103, 136, 204] # Blue
+ end
+
+ def path_color
+ [231, 230, 228] # Pastel White
+ end
+
+ def button_color
+ [190, 190, 190] # Gray
+ 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
+ $heuristic_with_walls ||= Heuristic_With_Walls.new(args)
+ $heuristic_with_walls.args = args
+ $heuristic_with_walls.tick
+end
+
+
+def reset
+ $heuristic_with_walls = nil
+end
diff --git a/samples/13_path_finding_algorithms/06_heuristic/circle-white.png b/samples/13_path_finding_algorithms/06_heuristic/circle-white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/13_path_finding_algorithms/06_heuristic/circle-white.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/06_heuristic/star.png b/samples/13_path_finding_algorithms/06_heuristic/star.png
new file mode 100644
index 0000000..b37bb04
--- /dev/null
+++ b/samples/13_path_finding_algorithms/06_heuristic/star.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/06_heuristic/target.png b/samples/13_path_finding_algorithms/06_heuristic/target.png
new file mode 100644
index 0000000..fb2223d
--- /dev/null
+++ b/samples/13_path_finding_algorithms/06_heuristic/target.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/07_heuristic_with_walls/app/main.rb b/samples/13_path_finding_algorithms/07_heuristic_with_walls/app/main.rb
new file mode 100644
index 0000000..5fc0804
--- /dev/null
+++ b/samples/13_path_finding_algorithms/07_heuristic_with_walls/app/main.rb
@@ -0,0 +1,1013 @@
+# This program is inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html
+# The effectiveness of the Heuristic search algorithm is shown through this demonstration.
+# Notice that both searches find the shortest path
+# The heuristic search, however, explores less of the grid, and is therefore faster.
+# The heuristic search prioritizes searching cells that are closer to the target.
+# Make sure to look at the Heuristic with walls program to see some of the downsides of the heuristic algorithm.
+
+class Heuristic
+ attr_gtk
+
+ def tick
+ defaults
+ render
+ input
+ # If animation is playing, and max steps have not been reached
+ # Move the search a step forward
+ if state.play && state.current_step < state.max_steps
+ # Variable that tells the program what step to recalculate up to
+ state.current_step += 1
+ move_searches_one_step_forward
+ end
+ end
+
+ def defaults
+ # Variables to edit the size and appearance of the grid
+ # Freely customizable to user's liking
+ grid.width ||= 15
+ grid.height ||= 15
+ grid.cell_size ||= 40
+ grid.rect ||= [0, 0, grid.width, grid.height]
+
+ grid.star ||= [0, 2]
+ grid.target ||= [14, 12]
+ grid.walls ||= {
+ [2, 2] => true,
+ [3, 2] => true,
+ [4, 2] => true,
+ [5, 2] => true,
+ [6, 2] => true,
+ [7, 2] => true,
+ [8, 2] => true,
+ [9, 2] => true,
+ [10, 2] => true,
+ [11, 2] => true,
+ [12, 2] => true,
+ [12, 3] => true,
+ [12, 4] => true,
+ [12, 5] => true,
+ [12, 6] => true,
+ [12, 7] => true,
+ [12, 8] => true,
+ [12, 9] => true,
+ [12, 10] => true,
+ [12, 11] => true,
+ [12, 12] => true,
+ [2, 12] => true,
+ [3, 12] => true,
+ [4, 12] => true,
+ [5, 12] => true,
+ [6, 12] => true,
+ [7, 12] => true,
+ [8, 12] => true,
+ [9, 12] => true,
+ [10, 12] => true,
+ [11, 12] => true,
+ [12, 12] => true
+ }
+ # There are no hills in the Heuristic Search Demo
+
+ # What the user is currently editing on the grid
+ # We store this value, because we want to remember the value even when
+ # the user's cursor is no longer over what they're interacting with, but
+ # they are still clicking down on the mouse.
+ state.user_input ||= :none
+
+ # These variables allow the breadth first search to take place
+ # 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.
+ # Used to prevent searching cells that have already been found
+ # and to trace a path from the target back to the starting point.
+ # Frontier is an array of cells to expand the search from.
+ # The search is over when there are no more cells to search from.
+ # Path stores the path from the target to the star, once the target has been found
+ # It prevents calculating the path every tick.
+ bfs.came_from ||= {}
+ bfs.frontier ||= []
+ bfs.path ||= []
+
+ heuristic.came_from ||= {}
+ heuristic.frontier ||= []
+ heuristic.path ||= []
+
+ # Stores which step of the animation is being rendered
+ # When the user moves the star or messes with the walls,
+ # the searches are recalculated up to this step
+
+ # Unless the current step has a value
+ unless state.current_step
+ # Set the current step to 10
+ state.current_step = 10
+ # And calculate the searches up to step 10
+ recalculate_searches
+ end
+
+ # 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
+ state.max_steps = grid.width * 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
+ # An if statement instead of the ||= operator is used for assigning a boolean value.
+ # The || operator does not differentiate between nil and false.
+ if state.play == nil
+ state.play = false
+ end
+
+ # 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
+ buttons.left = [470, 600, 50, 50]
+ buttons.center = [520, 600, 200, 50]
+ buttons.right = [720, 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
+ slider.x = 440
+ slider.y = 675
+ # This is the width of the line
+ 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
+ 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
+ slider.spacing = slider.w.to_f / state.max_steps.to_f
+ end
+
+ # All methods with render draw stuff on the screen
+ # UI has buttons, the slider, and labels
+ # The search specific rendering occurs in the respective methods
+ def render
+ render_ui
+ render_bfs
+ render_heuristic
+ end
+
+ def render_ui
+ render_buttons
+ render_slider
+ render_labels
+ end
+
+ def render_buttons
+ render_left_button
+ render_center_button
+ render_right_button
+ end
+
+ def render_bfs
+ render_bfs_grid
+ render_bfs_star
+ render_bfs_target
+ render_bfs_visited
+ render_bfs_walls
+ render_bfs_frontier
+ render_bfs_path
+ end
+
+ def render_heuristic
+ render_heuristic_grid
+ render_heuristic_star
+ render_heuristic_target
+ render_heuristic_visited
+ render_heuristic_walls
+ render_heuristic_frontier
+ render_heuristic_path
+ end
+
+ # This method handles user input every tick
+ def input
+ # Check and handle button input
+ input_buttons
+
+ # If the mouse was lifted this tick
+ if inputs.mouse.up
+ # Set current input to none
+ state.user_input = :none
+ end
+
+ # If the mouse was clicked this tick
+ if inputs.mouse.down
+ # Determine what the user is editing and appropriately edit the state.user_input variable
+ determine_input
+ end
+
+ # Process user input based on user_input variable and current mouse position
+ process_input
+ end
+
+ # Determines what the user is editing
+ # This method is called when the mouse is clicked down
+ def determine_input
+ if mouse_over_slider?
+ state.user_input = :slider
+ # If the mouse is over the star in the first grid
+ elsif bfs_mouse_over_star?
+ # The user is editing the star from the first grid
+ state.user_input = :bfs_star
+ # If the mouse is over the star in the second grid
+ elsif heuristic_mouse_over_star?
+ # The user is editing the star from the second grid
+ state.user_input = :heuristic_star
+ # If the mouse is over the target in the first grid
+ elsif bfs_mouse_over_target?
+ # The user is editing the target from the first grid
+ state.user_input = :bfs_target
+ # If the mouse is over the target in the second grid
+ elsif heuristic_mouse_over_target?
+ # The user is editing the target from the second grid
+ state.user_input = :heuristic_target
+ # If the mouse is over a wall in the first grid
+ elsif bfs_mouse_over_wall?
+ # The user is removing a wall from the first grid
+ state.user_input = :bfs_remove_wall
+ # If the mouse is over a wall in the second grid
+ elsif heuristic_mouse_over_wall?
+ # The user is removing a wall from the second grid
+ state.user_input = :heuristic_remove_wall
+ # If the mouse is over the first grid
+ elsif bfs_mouse_over_grid?
+ # The user is adding a wall from the first grid
+ state.user_input = :bfs_add_wall
+ # If the mouse is over the second grid
+ elsif heuristic_mouse_over_grid?
+ # The user is adding a wall from the second grid
+ state.user_input = :heuristic_add_wall
+ end
+ end
+
+ # Processes click and drag based on what the user is currently dragging
+ def process_input
+ if state.user_input == :slider
+ process_input_slider
+ elsif state.user_input == :bfs_star
+ process_input_bfs_star
+ elsif state.user_input == :heuristic_star
+ process_input_heuristic_star
+ elsif state.user_input == :bfs_target
+ process_input_bfs_target
+ elsif state.user_input == :heuristic_target
+ process_input_heuristic_target
+ elsif state.user_input == :bfs_remove_wall
+ process_input_bfs_remove_wall
+ elsif state.user_input == :heuristic_remove_wall
+ process_input_heuristic_remove_wall
+ elsif state.user_input == :bfs_add_wall
+ process_input_bfs_add_wall
+ elsif state.user_input == :heuristic_add_wall
+ process_input_heuristic_add_wall
+ end
+ end
+
+ 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.current_step * 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
+
+ def render_labels
+ outputs.labels << [205, 625, "Breadth First Search"]
+ outputs.labels << [820, 625, "Heuristic Best-First Search"]
+ end
+
+ def render_left_button
+ # Draws the button_color button, and a black border
+ # The border separates the buttons visually
+ outputs.solids << [buttons.left, button_color]
+ outputs.borders << [buttons.left]
+
+ # 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 button_color button, and a black border
+ # The border separates the buttons visually
+ outputs.solids << [buttons.center, button_color]
+ outputs.borders << [buttons.center]
+
+ # 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 button_color button, and a black border
+ # The border separates the buttons visually
+ outputs.solids << [buttons.right, button_color]
+ outputs.borders << [buttons.right]
+
+ # 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
+
+ def render_bfs_grid
+ # A large rect the size of the grid
+ outputs.solids << [bfs_scale_up(grid.rect), default_color]
+
+ # The vertical grid lines
+ for x in 0..grid.width
+ outputs.lines << bfs_vertical_line(x)
+ end
+
+ # The horizontal grid lines
+ for y in 0..grid.height
+ outputs.lines << bfs_horizontal_line(y)
+ end
+ end
+
+ def render_heuristic_grid
+ # A large rect the size of the grid
+ outputs.solids << [heuristic_scale_up(grid.rect), default_color]
+
+ # The vertical grid lines
+ for x in 0..grid.width
+ outputs.lines << heuristic_vertical_line(x)
+ end
+
+ # The horizontal grid lines
+ for y in 0..grid.height
+ outputs.lines << heuristic_horizontal_line(y)
+ end
+ end
+
+ # Returns a vertical line for a column of the first grid
+ def bfs_vertical_line column
+ bfs_scale_up([column, 0, column, grid.height])
+ end
+
+ # Returns a horizontal line for a column of the first grid
+ def bfs_horizontal_line row
+ bfs_scale_up([0, row, grid.width, row])
+ end
+
+ # Returns a vertical line for a column of the second grid
+ def heuristic_vertical_line column
+ bfs_scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height])
+ end
+
+ # Returns a horizontal line for a column of the second grid
+ def heuristic_horizontal_line row
+ bfs_scale_up([grid.width + 1, row, grid.width + grid.width + 1, row])
+ end
+
+ # Renders the star on the first grid
+ def render_bfs_star
+ outputs.sprites << [bfs_scale_up(grid.star), 'star.png']
+ end
+
+ # Renders the star on the second grid
+ def render_heuristic_star
+ outputs.sprites << [heuristic_scale_up(grid.star), 'star.png']
+ end
+
+ # Renders the target on the first grid
+ def render_bfs_target
+ outputs.sprites << [bfs_scale_up(grid.target), 'target.png']
+ end
+
+ # Renders the target on the second grid
+ def render_heuristic_target
+ outputs.sprites << [heuristic_scale_up(grid.target), 'target.png']
+ end
+
+ # Renders the walls on the first grid
+ def render_bfs_walls
+ grid.walls.each_key do | wall |
+ outputs.solids << [bfs_scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders the walls on the second grid
+ def render_heuristic_walls
+ grid.walls.each_key do | wall |
+ outputs.solids << [heuristic_scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders the visited cells on the first grid
+ def render_bfs_visited
+ bfs.came_from.each_key do | visited_cell |
+ outputs.solids << [bfs_scale_up(visited_cell), visited_color]
+ end
+ end
+
+ # Renders the visited cells on the second grid
+ def render_heuristic_visited
+ heuristic.came_from.each_key do | visited_cell |
+ outputs.solids << [heuristic_scale_up(visited_cell), visited_color]
+ end
+ end
+
+ # Renders the frontier cells on the first grid
+ def render_bfs_frontier
+ bfs.frontier.each do | frontier_cell |
+ outputs.solids << [bfs_scale_up(frontier_cell), frontier_color, 200]
+ end
+ end
+
+ # Renders the frontier cells on the second grid
+ def render_heuristic_frontier
+ heuristic.frontier.each do | frontier_cell |
+ outputs.solids << [heuristic_scale_up(frontier_cell), frontier_color, 200]
+ end
+ end
+
+ # Renders the path found by the breadth first search on the first grid
+ def render_bfs_path
+ bfs.path.each do | path |
+ outputs.solids << [bfs_scale_up(path), path_color]
+ end
+ end
+
+ # Renders the path found by the heuristic search on the second grid
+ def render_heuristic_path
+ heuristic.path.each do | path |
+ outputs.solids << [heuristic_scale_up(path), path_color]
+ end
+ end
+
+ # Returns the rect for the path between two cells based on their relative positions
+ def get_path_between(cell_one, cell_two)
+ path = []
+
+ # If cell one is above cell two
+ if cell_one.x == cell_two.x and cell_one.y > cell_two.y
+ # Path starts from the center of cell two and moves upward to the center of cell one
+ path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4]
+ # If cell one is below cell two
+ elsif cell_one.x == cell_two.x and cell_one.y < cell_two.y
+ # Path starts from the center of cell one and moves upward to the center of cell two
+ path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4]
+ # If cell one is to the left of cell two
+ elsif cell_one.x > cell_two.x and cell_one.y == cell_two.y
+ # Path starts from the center of cell two and moves rightward to the center of cell one
+ path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4]
+ # If cell one is to the right of cell two
+ elsif cell_one.x < cell_two.x and cell_one.y == cell_two.y
+ # Path starts from the center of cell one and moves rightward to the center of cell two
+ path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4]
+ end
+
+ path
+ 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
+ # This method scales up cells for the first grid
+ def bfs_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
+
+ # Translates the given cell grid.width + 1 to the right and then scales up
+ # Used to draw cells for the second grid
+ # This method does not work for lines,
+ # so separate methods exist for the grid lines
+ def heuristic_scale_up(cell)
+ # Prevents the original value of cell from being edited
+ cell = cell.clone
+ # Translates the cell to the second grid equivalent
+ cell.x += grid.width + 1
+ # Proceeds as if scaling up for the first grid
+ bfs_scale_up(cell)
+ end
+
+ # Checks and handles input for the buttons
+ # Called when the mouse is lifted
+ def input_buttons
+ input_left_button
+ input_center_button
+ input_right_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.current_step -= 1
+ recalculate_searches
+ 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? || 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_right_button
+ if right_button_clicked?
+ state.play = false
+ state.current_step += 1
+ move_searches_one_step_forward
+ end
+ end
+
+ # These methods detect when the buttons are clicked
+ def left_button_clicked?
+ inputs.mouse.point.inside_rect?(buttons.left) && inputs.mouse.up
+ end
+
+ def center_button_clicked?
+ inputs.mouse.point.inside_rect?(buttons.center) && inputs.mouse.up
+ end
+
+ def right_button_clicked?
+ inputs.mouse.point.inside_rect?(buttons.right) && inputs.mouse.up
+ end
+
+
+ # Signal that the user is going to be moving the slider
+ # Is the mouse over the circle of the slider?
+ def mouse_over_slider?
+ circle_x = (slider.x - slider.offset) + (state.current_step * slider.spacing)
+ circle_y = (slider.y - slider.offset)
+ circle_rect = [circle_x, circle_y, 37, 37]
+ inputs.mouse.point.inside_rect?(circle_rect)
+ end
+
+ # Signal that the user is going to be moving the star from the first grid
+ def bfs_mouse_over_star?
+ inputs.mouse.point.inside_rect?(bfs_scale_up(grid.star))
+ end
+
+ # Signal that the user is going to be moving the star from the second grid
+ def heuristic_mouse_over_star?
+ inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.star))
+ end
+
+ # Signal that the user is going to be moving the target from the first grid
+ def bfs_mouse_over_target?
+ inputs.mouse.point.inside_rect?(bfs_scale_up(grid.target))
+ end
+
+ # Signal that the user is going to be moving the target from the second grid
+ def heuristic_mouse_over_target?
+ inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.target))
+ end
+
+ # Signal that the user is going to be removing walls from the first grid
+ def bfs_mouse_over_wall?
+ grid.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(bfs_scale_up(wall))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be removing walls from the second grid
+ def heuristic_mouse_over_wall?
+ grid.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(heuristic_scale_up(wall))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be adding walls from the first grid
+ def bfs_mouse_over_grid?
+ inputs.mouse.point.inside_rect?(bfs_scale_up(grid.rect))
+ end
+
+ # Signal that the user is going to be adding walls from the second grid
+ def heuristic_mouse_over_grid?
+ inputs.mouse.point.inside_rect?(heuristic_scale_up(grid.rect))
+ 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 process_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.current_step = ((mouse_x - slider.x) / slider.spacing).to_i
+
+ recalculate_searches
+ end
+
+ # Moves the star to the cell closest to the mouse in the first grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def process_input_bfs_star
+ old_star = grid.star.clone
+ unless bfs_cell_closest_to_mouse == grid.target
+ grid.star = bfs_cell_closest_to_mouse
+ end
+ unless old_star == grid.star
+ recalculate_searches
+ end
+ end
+
+ # Moves the star to the cell closest to the mouse in the second grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def process_input_heuristic_star
+ old_star = grid.star.clone
+ unless heuristic_cell_closest_to_mouse == grid.target
+ grid.star = heuristic_cell_closest_to_mouse
+ end
+ unless old_star == grid.star
+ recalculate_searches
+ end
+ end
+
+ # Moves the target to the grid closest to the mouse in the first grid
+ # Only recalculate_searchess the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def process_input_bfs_target
+ old_target = grid.target.clone
+ unless bfs_cell_closest_to_mouse == grid.star
+ grid.target = bfs_cell_closest_to_mouse
+ end
+ unless old_target == grid.target
+ recalculate_searches
+ end
+ end
+
+ # Moves the target to the cell closest to the mouse in the second grid
+ # Only recalculate_searchess the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def process_input_heuristic_target
+ old_target = grid.target.clone
+ unless heuristic_cell_closest_to_mouse == grid.star
+ grid.target = heuristic_cell_closest_to_mouse
+ end
+ unless old_target == grid.target
+ recalculate_searches
+ end
+ end
+
+ # Removes walls in the first grid that are under the cursor
+ def process_input_bfs_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 bfs_mouse_over_grid?
+ if grid.walls.has_key?(bfs_cell_closest_to_mouse)
+ grid.walls.delete(bfs_cell_closest_to_mouse)
+ recalculate_searches
+ end
+ end
+ end
+
+ # Removes walls in the second grid that are under the cursor
+ def process_input_heuristic_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 heuristic_mouse_over_grid?
+ if grid.walls.has_key?(heuristic_cell_closest_to_mouse)
+ grid.walls.delete(heuristic_cell_closest_to_mouse)
+ recalculate_searches
+ end
+ end
+ end
+ # Adds a wall in the first grid in the cell the mouse is over
+ def process_input_bfs_add_wall
+ if bfs_mouse_over_grid?
+ unless grid.walls.has_key?(bfs_cell_closest_to_mouse)
+ grid.walls[bfs_cell_closest_to_mouse] = true
+ recalculate_searches
+ end
+ end
+ end
+
+ # Adds a wall in the second grid in the cell the mouse is over
+ def process_input_heuristic_add_wall
+ if heuristic_mouse_over_grid?
+ unless grid.walls.has_key?(heuristic_cell_closest_to_mouse)
+ grid.walls[heuristic_cell_closest_to_mouse] = true
+ recalculate_searches
+ end
+ end
+ end
+
+ # When the user grabs the star and puts their cursor to the far right
+ # and moves up and down, the star is supposed to move along the grid as well
+ # Finding the cell closest to the mouse helps with this
+ def bfs_cell_closest_to_mouse
+ # Closest cell to the mouse in the first grid
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Bound x and y to the grid
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ # 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 in the second grid helps with this
+ def heuristic_cell_closest_to_mouse
+ # Closest cell grid to the mouse in the second
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Translate the cell to the first grid
+ x -= grid.width + 1
+ # Bound x and y to the first grid
+ x = 0 if x < 0
+ y = 0 if y < 0
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ def recalculate_searches
+ # Reset the searches
+ bfs.came_from = {}
+ bfs.frontier = []
+ bfs.path = []
+ heuristic.came_from = {}
+ heuristic.frontier = []
+ heuristic.path = []
+
+ # Move the searches forward to the current step
+ state.current_step.times { move_searches_one_step_forward }
+ end
+
+ def move_searches_one_step_forward
+ bfs_one_step_forward
+ heuristic_one_step_forward
+ end
+
+ def bfs_one_step_forward
+ return if bfs.came_from.has_key?(grid.target)
+
+ # Only runs at the beginning of the search as setup.
+ if bfs.came_from.empty?
+ bfs.frontier << grid.star
+ bfs.came_from[grid.star] = nil
+ end
+
+ # A step in the search
+ unless bfs.frontier.empty?
+ # Takes the next frontier cell
+ new_frontier = bfs.frontier.shift
+ # For each of its neighbors
+ adjacent_neighbors(new_frontier).each do |neighbor|
+ # That have not been visited and are not walls
+ unless bfs.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor)
+ # Add them to the frontier and mark them as visited
+ bfs.frontier << neighbor
+ bfs.came_from[neighbor] = new_frontier
+ end
+ end
+ end
+
+ # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
+ # Comment this line and let a path generate to see the difference
+ bfs.frontier = bfs.frontier.sort_by {| cell | proximity_to_star(cell) }
+
+ # If the search found the target
+ if bfs.came_from.has_key?(grid.target)
+ # Calculate the path between the target and star
+ bfs_calc_path
+ end
+ end
+
+ # Calculates the path between the target and star for the breadth first search
+ # Only called when the breadth first search finds the target
+ def bfs_calc_path
+ # Start from the target
+ endpoint = grid.target
+ # And the cell it came from
+ next_endpoint = bfs.came_from[endpoint]
+ while endpoint and next_endpoint
+ # Draw a path between these two cells and store it
+ path = get_path_between(endpoint, next_endpoint)
+ bfs.path << path
+ # And get the next pair of cells
+ endpoint = next_endpoint
+ next_endpoint = bfs.came_from[endpoint]
+ # Continue till there are no more cells
+ end
+ end
+
+ # Moves the heuristic search forward one step
+ # Can be called from tick while the animation is playing
+ # Can also be called when recalculating the searches after the user edited the grid
+ def heuristic_one_step_forward
+ # Stop the search if the target has been found
+ return if heuristic.came_from.has_key?(grid.target)
+
+ # If the search has not begun
+ if heuristic.came_from.empty?
+ # Setup the search to begin from the star
+ heuristic.frontier << grid.star
+ heuristic.came_from[grid.star] = nil
+ end
+
+ # One step in the heuristic search
+
+ # Unless there are no more cells to explore from
+ unless heuristic.frontier.empty?
+ # Get the next cell to explore from
+ new_frontier = heuristic.frontier.shift
+ # For each of its neighbors
+ adjacent_neighbors(new_frontier).each do |neighbor|
+ # That have not been visited and are not walls
+ unless heuristic.came_from.has_key?(neighbor) || grid.walls.has_key?(neighbor)
+ # Add them to the frontier and mark them as visited
+ heuristic.frontier << neighbor
+ heuristic.came_from[neighbor] = new_frontier
+ end
+ end
+ end
+
+ # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
+ heuristic.frontier = heuristic.frontier.sort_by {| cell | proximity_to_star(cell) }
+ # Sort the frontier so cells that are close to the target are then prioritized
+ heuristic.frontier = heuristic.frontier.sort_by {| cell | heuristic_heuristic(cell) }
+
+ # If the search found the target
+ if heuristic.came_from.has_key?(grid.target)
+ # Calculate the path between the target and star
+ heuristic_calc_path
+ end
+ end
+
+ # Returns one-dimensional absolute distance between cell and target
+ # Returns a number to compare distances between cells and the target
+ def heuristic_heuristic(cell)
+ (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs
+ end
+
+ # Calculates the path between the target and star for the heuristic search
+ # Only called when the heuristic search finds the target
+ def heuristic_calc_path
+ # Start from the target
+ endpoint = grid.target
+ # And the cell it came from
+ next_endpoint = heuristic.came_from[endpoint]
+ while endpoint and next_endpoint
+ # Draw a path between these two cells and store it
+ path = get_path_between(endpoint, next_endpoint)
+ heuristic.path << path
+ # And get the next pair of cells
+ endpoint = next_endpoint
+ next_endpoint = heuristic.came_from[endpoint]
+ # Continue till there are no more cells
+ 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 = []
+
+ # Gets all the valid neighbors into the array
+ # From southern neighbor, clockwise
+ neighbors << [cell.x , cell.y - 1] unless cell.y == 0
+ neighbors << [cell.x - 1, cell.y ] unless cell.x == 0
+ neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1
+ neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1
+
+ neighbors
+ end
+
+ # Finds the vertical and horizontal distance of a cell from the star
+ # and returns the larger value
+ # This method is used to have a zigzag pattern in the rendered path
+ # A cell that is [5, 5] from the star,
+ # is explored before over a cell that is [0, 7] away.
+ # So, if possible, the search tries to go diagonal (zigzag) first
+ def proximity_to_star(cell)
+ distance_x = (grid.star.x - cell.x).abs
+ distance_y = (grid.star.y - cell.y).abs
+
+ if distance_x > distance_y
+ return distance_x
+ else
+ return distance_y
+ end
+ end
+
+ # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored.
+ def grid
+ state.grid
+ end
+
+ def buttons
+ state.buttons
+ end
+
+ def slider
+ state.slider
+ end
+
+ def bfs
+ state.bfs
+ end
+
+ def heuristic
+ state.heuristic
+ end
+
+ # Descriptive aliases for colors
+ def default_color
+ [221, 212, 213] # Light Brown
+ end
+
+ def wall_color
+ [134, 134, 120] # Camo Green
+ end
+
+ def visited_color
+ [204, 191, 179] # Dark Brown
+ end
+
+ def frontier_color
+ [103, 136, 204] # Blue
+ end
+
+ def path_color
+ [231, 230, 228] # Pastel White
+ end
+
+ def button_color
+ [190, 190, 190] # Gray
+ 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
+ $heuristic ||= Heuristic.new(args)
+ $heuristic.args = args
+ $heuristic.tick
+end
+
+
+def reset
+ $heuristic = nil
+end
diff --git a/samples/13_path_finding_algorithms/07_heuristic_with_walls/circle-white.png b/samples/13_path_finding_algorithms/07_heuristic_with_walls/circle-white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/13_path_finding_algorithms/07_heuristic_with_walls/circle-white.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/07_heuristic_with_walls/star.png b/samples/13_path_finding_algorithms/07_heuristic_with_walls/star.png
new file mode 100644
index 0000000..b37bb04
--- /dev/null
+++ b/samples/13_path_finding_algorithms/07_heuristic_with_walls/star.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/07_heuristic_with_walls/target.png b/samples/13_path_finding_algorithms/07_heuristic_with_walls/target.png
new file mode 100644
index 0000000..fb2223d
--- /dev/null
+++ b/samples/13_path_finding_algorithms/07_heuristic_with_walls/target.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/08_a_star/app/main.rb b/samples/13_path_finding_algorithms/08_a_star/app/main.rb
new file mode 100644
index 0000000..e9fcb8c
--- /dev/null
+++ b/samples/13_path_finding_algorithms/08_a_star/app/main.rb
@@ -0,0 +1,1029 @@
+# This program is inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html
+
+# The A* Search works by incorporating both the distance from the starting point
+# and the distance from the target in its heurisitic.
+
+# It tends to find the correct (shortest) path even when the Greedy Best-First Search does not,
+# and it explores less of the grid, and is therefore faster, than Dijkstra's Search.
+
+class A_Star_Algorithm
+ attr_gtk
+
+ def tick
+ defaults
+ render
+ input
+
+ if dijkstra.came_from.empty?
+ calc_searches
+ end
+ end
+
+ def defaults
+ # Variables to edit the size and appearance of the grid
+ # Freely customizable to user's liking
+ grid.width ||= 15
+ grid.height ||= 15
+ grid.cell_size ||= 27
+ grid.rect ||= [0, 0, grid.width, grid.height]
+
+ grid.star ||= [0, 2]
+ grid.target ||= [11, 13]
+ grid.walls ||= {
+ [2, 2] => true,
+ [3, 2] => true,
+ [4, 2] => true,
+ [5, 2] => true,
+ [6, 2] => true,
+ [7, 2] => true,
+ [8, 2] => true,
+ [9, 2] => true,
+ [10, 2] => true,
+ [11, 2] => true,
+ [12, 2] => true,
+ [12, 3] => true,
+ [12, 4] => true,
+ [12, 5] => true,
+ [12, 6] => true,
+ [12, 7] => true,
+ [12, 8] => true,
+ [12, 9] => true,
+ [12, 10] => true,
+ [12, 11] => true,
+ [12, 12] => true,
+ [5, 12] => true,
+ [6, 12] => true,
+ [7, 12] => true,
+ [8, 12] => true,
+ [9, 12] => true,
+ [10, 12] => true,
+ [11, 12] => true,
+ [12, 12] => true
+ }
+
+ # What the user is currently editing on the grid
+ # We store this value, because we want to remember the value even when
+ # the user's cursor is no longer over what they're interacting with, but
+ # they are still clicking down on the mouse.
+ state.user_input ||= :none
+
+ # These variables allow the breadth first search to take place
+ # 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.
+ # Used to prevent searching cells that have already been found
+ # and to trace a path from the target back to the starting point.
+ # Frontier is an array of cells to expand the search from.
+ # The search is over when there are no more cells to search from.
+ # Path stores the path from the target to the star, once the target has been found
+ # It prevents calculating the path every tick.
+ dijkstra.came_from ||= {}
+ dijkstra.cost_so_far ||= {}
+ dijkstra.frontier ||= []
+ dijkstra.path ||= []
+
+ greedy.came_from ||= {}
+ greedy.frontier ||= []
+ greedy.path ||= []
+
+ a_star.frontier ||= []
+ a_star.came_from ||= {}
+ a_star.path ||= []
+ end
+
+ # All methods with render draw stuff on the screen
+ # UI has buttons, the slider, and labels
+ # The search specific rendering occurs in the respective methods
+ def render
+ render_labels
+ render_dijkstra
+ render_greedy
+ render_a_star
+ end
+
+ def render_labels
+ outputs.labels << [150, 450, "Dijkstra's"]
+ outputs.labels << [550, 450, "Greedy Best-First"]
+ outputs.labels << [1025, 450, "A* Search"]
+ end
+
+ def render_dijkstra
+ render_dijkstra_grid
+ render_dijkstra_star
+ render_dijkstra_target
+ render_dijkstra_visited
+ render_dijkstra_walls
+ render_dijkstra_path
+ end
+
+ def render_greedy
+ render_greedy_grid
+ render_greedy_star
+ render_greedy_target
+ render_greedy_visited
+ render_greedy_walls
+ render_greedy_path
+ end
+
+ def render_a_star
+ render_a_star_grid
+ render_a_star_star
+ render_a_star_target
+ render_a_star_visited
+ render_a_star_walls
+ render_a_star_path
+ end
+
+ # This method handles user input every tick
+ def input
+ # If the mouse was lifted this tick
+ if inputs.mouse.up
+ # Set current input to none
+ state.user_input = :none
+ end
+
+ # If the mouse was clicked this tick
+ if inputs.mouse.down
+ # Determine what the user is editing and appropriately edit the state.user_input variable
+ determine_input
+ end
+
+ # Process user input based on user_input variable and current mouse position
+ process_input
+ end
+
+ # Determines what the user is editing
+ # This method is called when the mouse is clicked down
+ def determine_input
+ # If the mouse is over the star in the first grid
+ if dijkstra_mouse_over_star?
+ # The user is editing the star from the first grid
+ state.user_input = :dijkstra_star
+ # If the mouse is over the star in the second grid
+ elsif greedy_mouse_over_star?
+ # The user is editing the star from the second grid
+ state.user_input = :greedy_star
+ # If the mouse is over the star in the third grid
+ elsif a_star_mouse_over_star?
+ # The user is editing the star from the third grid
+ state.user_input = :a_star_star
+ # If the mouse is over the target in the first grid
+ elsif dijkstra_mouse_over_target?
+ # The user is editing the target from the first grid
+ state.user_input = :dijkstra_target
+ # If the mouse is over the target in the second grid
+ elsif greedy_mouse_over_target?
+ # The user is editing the target from the second grid
+ state.user_input = :greedy_target
+ # If the mouse is over the target in the third grid
+ elsif a_star_mouse_over_target?
+ # The user is editing the target from the third grid
+ state.user_input = :a_star_target
+ # If the mouse is over a wall in the first grid
+ elsif dijkstra_mouse_over_wall?
+ # The user is removing a wall from the first grid
+ state.user_input = :dijkstra_remove_wall
+ # If the mouse is over a wall in the second grid
+ elsif greedy_mouse_over_wall?
+ # The user is removing a wall from the second grid
+ state.user_input = :greedy_remove_wall
+ # If the mouse is over a wall in the third grid
+ elsif a_star_mouse_over_wall?
+ # The user is removing a wall from the third grid
+ state.user_input = :a_star_remove_wall
+ # If the mouse is over the first grid
+ elsif dijkstra_mouse_over_grid?
+ # The user is adding a wall from the first grid
+ state.user_input = :dijkstra_add_wall
+ # If the mouse is over the second grid
+ elsif greedy_mouse_over_grid?
+ # The user is adding a wall from the second grid
+ state.user_input = :greedy_add_wall
+ # If the mouse is over the third grid
+ elsif a_star_mouse_over_grid?
+ # The user is adding a wall from the third grid
+ state.user_input = :a_star_add_wall
+ end
+ end
+
+ # Processes click and drag based on what the user is currently dragging
+ def process_input
+ if state.user_input == :dijkstra_star
+ process_input_dijkstra_star
+ elsif state.user_input == :greedy_star
+ process_input_greedy_star
+ elsif state.user_input == :a_star_star
+ process_input_a_star_star
+ elsif state.user_input == :dijkstra_target
+ process_input_dijkstra_target
+ elsif state.user_input == :greedy_target
+ process_input_greedy_target
+ elsif state.user_input == :a_star_target
+ process_input_a_star_target
+ elsif state.user_input == :dijkstra_remove_wall
+ process_input_dijkstra_remove_wall
+ elsif state.user_input == :greedy_remove_wall
+ process_input_greedy_remove_wall
+ elsif state.user_input == :a_star_remove_wall
+ process_input_a_star_remove_wall
+ elsif state.user_input == :dijkstra_add_wall
+ process_input_dijkstra_add_wall
+ elsif state.user_input == :greedy_add_wall
+ process_input_greedy_add_wall
+ elsif state.user_input == :a_star_add_wall
+ process_input_a_star_add_wall
+ end
+ end
+
+ def render_dijkstra_grid
+ # A large rect the size of the grid
+ outputs.solids << [dijkstra_scale_up(grid.rect), default_color]
+
+ # The vertical grid lines
+ for x in 0..grid.width
+ outputs.lines << dijkstra_vertical_line(x)
+ end
+
+ # The horizontal grid lines
+ for y in 0..grid.height
+ outputs.lines << dijkstra_horizontal_line(y)
+ end
+ end
+
+ def render_greedy_grid
+ # A large rect the size of the grid
+ outputs.solids << [greedy_scale_up(grid.rect), default_color]
+
+ # The vertical grid lines
+ for x in 0..grid.width
+ outputs.lines << greedy_vertical_line(x)
+ end
+
+ # The horizontal grid lines
+ for y in 0..grid.height
+ outputs.lines << greedy_horizontal_line(y)
+ end
+ end
+
+ def render_a_star_grid
+ # A large rect the size of the grid
+ outputs.solids << [a_star_scale_up(grid.rect), default_color]
+
+ # The vertical grid lines
+ for x in 0..grid.width
+ outputs.lines << a_star_vertical_line(x)
+ end
+
+ # The horizontal grid lines
+ for y in 0..grid.height
+ outputs.lines << a_star_horizontal_line(y)
+ end
+ end
+
+ # Returns a vertical line for a column of the first grid
+ def dijkstra_vertical_line column
+ dijkstra_scale_up([column, 0, column, grid.height])
+ end
+
+ # Returns a horizontal line for a column of the first grid
+ def dijkstra_horizontal_line row
+ dijkstra_scale_up([0, row, grid.width, row])
+ end
+
+ # Returns a vertical line for a column of the second grid
+ def greedy_vertical_line column
+ dijkstra_scale_up([column + grid.width + 1, 0, column + grid.width + 1, grid.height])
+ end
+
+ # Returns a horizontal line for a column of the second grid
+ def greedy_horizontal_line row
+ dijkstra_scale_up([grid.width + 1, row, grid.width + grid.width + 1, row])
+ end
+
+ # Returns a vertical line for a column of the third grid
+ def a_star_vertical_line column
+ dijkstra_scale_up([column + (grid.width * 2) + 2, 0, column + (grid.width * 2) + 2, grid.height])
+ end
+
+ # Returns a horizontal line for a column of the third grid
+ def a_star_horizontal_line row
+ dijkstra_scale_up([(grid.width * 2) + 2, row, (grid.width * 2) + grid.width + 2, row])
+ end
+
+ # Renders the star on the first grid
+ def render_dijkstra_star
+ outputs.sprites << [dijkstra_scale_up(grid.star), 'star.png']
+ end
+
+ # Renders the star on the second grid
+ def render_greedy_star
+ outputs.sprites << [greedy_scale_up(grid.star), 'star.png']
+ end
+
+ # Renders the star on the third grid
+ def render_a_star_star
+ outputs.sprites << [a_star_scale_up(grid.star), 'star.png']
+ end
+
+ # Renders the target on the first grid
+ def render_dijkstra_target
+ outputs.sprites << [dijkstra_scale_up(grid.target), 'target.png']
+ end
+
+ # Renders the target on the second grid
+ def render_greedy_target
+ outputs.sprites << [greedy_scale_up(grid.target), 'target.png']
+ end
+
+ # Renders the target on the third grid
+ def render_a_star_target
+ outputs.sprites << [a_star_scale_up(grid.target), 'target.png']
+ end
+
+ # Renders the walls on the first grid
+ def render_dijkstra_walls
+ grid.walls.each_key do | wall |
+ outputs.solids << [dijkstra_scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders the walls on the second grid
+ def render_greedy_walls
+ grid.walls.each_key do | wall |
+ outputs.solids << [greedy_scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders the walls on the third grid
+ def render_a_star_walls
+ grid.walls.each_key do | wall |
+ outputs.solids << [a_star_scale_up(wall), wall_color]
+ end
+ end
+
+ # Renders the visited cells on the first grid
+ def render_dijkstra_visited
+ dijkstra.came_from.each_key do | visited_cell |
+ outputs.solids << [dijkstra_scale_up(visited_cell), visited_color]
+ end
+ end
+
+ # Renders the visited cells on the second grid
+ def render_greedy_visited
+ greedy.came_from.each_key do | visited_cell |
+ outputs.solids << [greedy_scale_up(visited_cell), visited_color]
+ end
+ end
+
+ # Renders the visited cells on the third grid
+ def render_a_star_visited
+ a_star.came_from.each_key do | visited_cell |
+ outputs.solids << [a_star_scale_up(visited_cell), visited_color]
+ end
+ end
+
+ # Renders the path found by the breadth first search on the first grid
+ def render_dijkstra_path
+ dijkstra.path.each do | path |
+ outputs.solids << [dijkstra_scale_up(path), path_color]
+ end
+ end
+
+ # Renders the path found by the greedy search on the second grid
+ def render_greedy_path
+ greedy.path.each do | path |
+ outputs.solids << [greedy_scale_up(path), path_color]
+ end
+ end
+
+ # Renders the path found by the a_star search on the third grid
+ def render_a_star_path
+ a_star.path.each do | path |
+ outputs.solids << [a_star_scale_up(path), path_color]
+ end
+ end
+
+ # Returns the rect for the path between two cells based on their relative positions
+ def get_path_between(cell_one, cell_two)
+ path = []
+
+ # If cell one is above cell two
+ if cell_one.x == cell_two.x and cell_one.y > cell_two.y
+ # Path starts from the center of cell two and moves upward to the center of cell one
+ path = [cell_two.x + 0.3, cell_two.y + 0.3, 0.4, 1.4]
+ # If cell one is below cell two
+ elsif cell_one.x == cell_two.x and cell_one.y < cell_two.y
+ # Path starts from the center of cell one and moves upward to the center of cell two
+ path = [cell_one.x + 0.3, cell_one.y + 0.3, 0.4, 1.4]
+ # If cell one is to the left of cell two
+ elsif cell_one.x > cell_two.x and cell_one.y == cell_two.y
+ # Path starts from the center of cell two and moves rightward to the center of cell one
+ path = [cell_two.x + 0.3, cell_two.y + 0.3, 1.4, 0.4]
+ # If cell one is to the right of cell two
+ elsif cell_one.x < cell_two.x and cell_one.y == cell_two.y
+ # Path starts from the center of cell one and moves rightward to the center of cell two
+ path = [cell_one.x + 0.3, cell_one.y + 0.3, 1.4, 0.4]
+ end
+
+ path
+ 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
+ # This method scales up cells for the first grid
+ def dijkstra_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
+
+ # Translates the given cell grid.width + 1 to the right and then scales up
+ # Used to draw cells for the second grid
+ # This method does not work for lines,
+ # so separate methods exist for the grid lines
+ def greedy_scale_up(cell)
+ # Prevents the original value of cell from being edited
+ cell = cell.clone
+ # Translates the cell to the second grid equivalent
+ cell.x += grid.width + 1
+ # Proceeds as if scaling up for the first grid
+ dijkstra_scale_up(cell)
+ end
+
+ # Translates the given cell (grid.width + 1) * 2 to the right and then scales up
+ # Used to draw cells for the third grid
+ # This method does not work for lines,
+ # so separate methods exist for the grid lines
+ def a_star_scale_up(cell)
+ # Prevents the original value of cell from being edited
+ cell = cell.clone
+ # Translates the cell to the second grid equivalent
+ cell.x += grid.width + 1
+ # Translates the cell to the third grid equivalent
+ cell.x += grid.width + 1
+ # Proceeds as if scaling up for the first grid
+ dijkstra_scale_up(cell)
+ end
+
+ # Signal that the user is going to be moving the star from the first grid
+ def dijkstra_mouse_over_star?
+ inputs.mouse.point.inside_rect?(dijkstra_scale_up(grid.star))
+ end
+
+ # Signal that the user is going to be moving the star from the second grid
+ def greedy_mouse_over_star?
+ inputs.mouse.point.inside_rect?(greedy_scale_up(grid.star))
+ end
+
+ # Signal that the user is going to be moving the star from the third grid
+ def a_star_mouse_over_star?
+ inputs.mouse.point.inside_rect?(a_star_scale_up(grid.star))
+ end
+
+ # Signal that the user is going to be moving the target from the first grid
+ def dijkstra_mouse_over_target?
+ inputs.mouse.point.inside_rect?(dijkstra_scale_up(grid.target))
+ end
+
+ # Signal that the user is going to be moving the target from the second grid
+ def greedy_mouse_over_target?
+ inputs.mouse.point.inside_rect?(greedy_scale_up(grid.target))
+ end
+
+ # Signal that the user is going to be moving the target from the third grid
+ def a_star_mouse_over_target?
+ inputs.mouse.point.inside_rect?(a_star_scale_up(grid.target))
+ end
+
+ # Signal that the user is going to be removing walls from the first grid
+ def dijkstra_mouse_over_wall?
+ grid.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(dijkstra_scale_up(wall))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be removing walls from the second grid
+ def greedy_mouse_over_wall?
+ grid.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(greedy_scale_up(wall))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be removing walls from the third grid
+ def a_star_mouse_over_wall?
+ grid.walls.each_key do | wall |
+ return true if inputs.mouse.point.inside_rect?(a_star_scale_up(wall))
+ end
+
+ false
+ end
+
+ # Signal that the user is going to be adding walls from the first grid
+ def dijkstra_mouse_over_grid?
+ inputs.mouse.point.inside_rect?(dijkstra_scale_up(grid.rect))
+ end
+
+ # Signal that the user is going to be adding walls from the second grid
+ def greedy_mouse_over_grid?
+ inputs.mouse.point.inside_rect?(greedy_scale_up(grid.rect))
+ end
+
+ # Signal that the user is going to be adding walls from the third grid
+ def a_star_mouse_over_grid?
+ inputs.mouse.point.inside_rect?(a_star_scale_up(grid.rect))
+ end
+
+ # Moves the star to the cell closest to the mouse in the first grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def process_input_dijkstra_star
+ old_star = grid.star.clone
+ unless dijkstra_cell_closest_to_mouse == grid.target
+ grid.star = dijkstra_cell_closest_to_mouse
+ end
+ unless old_star == grid.star
+ reset_searches
+ end
+ end
+
+ # Moves the star to the cell closest to the mouse in the second grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def process_input_greedy_star
+ old_star = grid.star.clone
+ unless greedy_cell_closest_to_mouse == grid.target
+ grid.star = greedy_cell_closest_to_mouse
+ end
+ unless old_star == grid.star
+ reset_searches
+ end
+ end
+
+ # Moves the star to the cell closest to the mouse in the third grid
+ # Only resets the search if the star changes position
+ # Called whenever the user is editing the star (puts mouse down on star)
+ def process_input_a_star_star
+ old_star = grid.star.clone
+ unless a_star_cell_closest_to_mouse == grid.target
+ grid.star = a_star_cell_closest_to_mouse
+ end
+ unless old_star == grid.star
+ reset_searches
+ end
+ end
+
+ # Moves the target to the grid closest to the mouse in the first grid
+ # Only reset_searchess the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def process_input_dijkstra_target
+ old_target = grid.target.clone
+ unless dijkstra_cell_closest_to_mouse == grid.star
+ grid.target = dijkstra_cell_closest_to_mouse
+ end
+ unless old_target == grid.target
+ reset_searches
+ end
+ end
+
+ # Moves the target to the cell closest to the mouse in the second grid
+ # Only reset_searchess the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def process_input_greedy_target
+ old_target = grid.target.clone
+ unless greedy_cell_closest_to_mouse == grid.star
+ grid.target = greedy_cell_closest_to_mouse
+ end
+ unless old_target == grid.target
+ reset_searches
+ end
+ end
+
+ # Moves the target to the cell closest to the mouse in the third grid
+ # Only reset_searchess the search if the target changes position
+ # Called whenever the user is editing the target (puts mouse down on target)
+ def process_input_a_star_target
+ old_target = grid.target.clone
+ unless a_star_cell_closest_to_mouse == grid.star
+ grid.target = a_star_cell_closest_to_mouse
+ end
+ unless old_target == grid.target
+ reset_searches
+ end
+ end
+
+ # Removes walls in the first grid that are under the cursor
+ def process_input_dijkstra_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 dijkstra_mouse_over_grid?
+ if grid.walls.has_key?(dijkstra_cell_closest_to_mouse)
+ grid.walls.delete(dijkstra_cell_closest_to_mouse)
+ reset_searches
+ end
+ end
+ end
+
+ # Removes walls in the second grid that are under the cursor
+ def process_input_greedy_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 greedy_mouse_over_grid?
+ if grid.walls.has_key?(greedy_cell_closest_to_mouse)
+ grid.walls.delete(greedy_cell_closest_to_mouse)
+ reset_searches
+ end
+ end
+ end
+
+ # Removes walls in the third grid that are under the cursor
+ def process_input_a_star_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 a_star_mouse_over_grid?
+ if grid.walls.has_key?(a_star_cell_closest_to_mouse)
+ grid.walls.delete(a_star_cell_closest_to_mouse)
+ reset_searches
+ end
+ end
+ end
+
+ # Adds a wall in the first grid in the cell the mouse is over
+ def process_input_dijkstra_add_wall
+ if dijkstra_mouse_over_grid?
+ unless grid.walls.has_key?(dijkstra_cell_closest_to_mouse)
+ grid.walls[dijkstra_cell_closest_to_mouse] = true
+ reset_searches
+ end
+ end
+ end
+
+ # Adds a wall in the second grid in the cell the mouse is over
+ def process_input_greedy_add_wall
+ if greedy_mouse_over_grid?
+ unless grid.walls.has_key?(greedy_cell_closest_to_mouse)
+ grid.walls[greedy_cell_closest_to_mouse] = true
+ reset_searches
+ end
+ end
+ end
+
+ # Adds a wall in the third grid in the cell the mouse is over
+ def process_input_a_star_add_wall
+ if a_star_mouse_over_grid?
+ unless grid.walls.has_key?(a_star_cell_closest_to_mouse)
+ grid.walls[a_star_cell_closest_to_mouse] = true
+ reset_searches
+ end
+ end
+ end
+
+ # When the user grabs the star and puts their cursor to the far right
+ # and moves up and down, the star is supposed to move along the grid as well
+ # Finding the cell closest to the mouse helps with this
+ def dijkstra_cell_closest_to_mouse
+ # Closest cell to the mouse in the first grid
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Bound x and y to the grid
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ # 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 in the second grid helps with this
+ def greedy_cell_closest_to_mouse
+ # Closest cell grid to the mouse in the second
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Translate the cell to the first grid
+ x -= grid.width + 1
+ # Bound x and y to the first grid
+ x = 0 if x < 0
+ y = 0 if y < 0
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ 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 in the third grid helps with this
+ def a_star_cell_closest_to_mouse
+ # Closest cell grid to the mouse in the second
+ x = (inputs.mouse.point.x / grid.cell_size).to_i
+ y = (inputs.mouse.point.y / grid.cell_size).to_i
+ # Translate the cell to the first grid
+ x -= (grid.width + 1) * 2
+ # Bound x and y to the first grid
+ x = 0 if x < 0
+ y = 0 if y < 0
+ x = grid.width - 1 if x > grid.width - 1
+ y = grid.height - 1 if y > grid.height - 1
+ # Return closest cell
+ [x, y]
+ end
+
+ def reset_searches
+ # Reset the searches
+ dijkstra.came_from = {}
+ dijkstra.cost_so_far = {}
+ dijkstra.frontier = []
+ dijkstra.path = []
+
+ greedy.came_from = {}
+ greedy.frontier = []
+ greedy.path = []
+ a_star.came_from = {}
+ a_star.frontier = []
+ a_star.path = []
+ end
+
+ def calc_searches
+ calc_dijkstra
+ calc_greedy
+ calc_a_star
+ # Move the searches forward to the current step
+ # state.current_step.times { move_searches_one_step_forward }
+ end
+
+ def calc_dijkstra
+ # Sets up the search to begin from the star
+ dijkstra.frontier << grid.star
+ dijkstra.came_from[grid.star] = nil
+ dijkstra.cost_so_far[grid.star] = 0
+
+ # Until the target is found or there are no more cells to explore from
+ until dijkstra.came_from.has_key?(grid.target) or dijkstra.frontier.empty?
+ # Take the next frontier cell. The first element is the cell, the second is the priority.
+ new_frontier = dijkstra.frontier.shift#[0]
+ # For each of its neighbors
+ adjacent_neighbors(new_frontier).each do | neighbor |
+ # That have not been visited and are not walls
+ unless dijkstra.came_from.has_key?(neighbor) or grid.walls.has_key?(neighbor)
+ # Add them to the frontier and mark them as visited
+ dijkstra.frontier << neighbor
+ dijkstra.came_from[neighbor] = new_frontier
+ dijkstra.cost_so_far[neighbor] = dijkstra.cost_so_far[new_frontier] + 1
+ end
+ end
+
+ # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
+ # Comment this line and let a path generate to see the difference
+ dijkstra.frontier = dijkstra.frontier.sort_by {| cell | proximity_to_star(cell) }
+ dijkstra.frontier = dijkstra.frontier.sort_by {| cell | dijkstra.cost_so_far[cell] }
+ end
+
+
+ # If the search found the target
+ if dijkstra.came_from.has_key?(grid.target)
+ # Calculate the path between the target and star
+ dijkstra_calc_path
+ end
+ end
+
+ def calc_greedy
+ # Sets up the search to begin from the star
+ greedy.frontier << grid.star
+ greedy.came_from[grid.star] = nil
+
+ # Until the target is found or there are no more cells to explore from
+ until greedy.came_from.has_key?(grid.target) or greedy.frontier.empty?
+ # Take the next frontier cell
+ new_frontier = greedy.frontier.shift
+ # For each of its neighbors
+ adjacent_neighbors(new_frontier).each do | neighbor |
+ # That have not been visited and are not walls
+ unless greedy.came_from.has_key?(neighbor) or grid.walls.has_key?(neighbor)
+ # Add them to the frontier and mark them as visited
+ greedy.frontier << neighbor
+ greedy.came_from[neighbor] = new_frontier
+ end
+ end
+ # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
+ # Comment this line and let a path generate to see the difference
+ greedy.frontier = greedy.frontier.sort_by {| cell | proximity_to_star(cell) }
+ # Sort the frontier so cells that are close to the target are then prioritized
+ greedy.frontier = greedy.frontier.sort_by {| cell | greedy_heuristic(cell) }
+ end
+
+
+ # If the search found the target
+ if greedy.came_from.has_key?(grid.target)
+ # Calculate the path between the target and star
+ greedy_calc_path
+ end
+ end
+
+ def calc_a_star
+ # Setup the search to start from the star
+ a_star.came_from[grid.star] = nil
+ a_star.cost_so_far[grid.star] = 0
+ a_star.frontier << grid.star
+
+ # Until there are no more cells to explore from or the search has found the target
+ until a_star.frontier.empty? or a_star.came_from.has_key?(grid.target)
+ # Get the next cell to expand from
+ current_frontier = a_star.frontier.shift
+
+ # For each of that cells neighbors
+ adjacent_neighbors(current_frontier).each do | neighbor |
+ # That have not been visited and are not walls
+ unless a_star.came_from.has_key?(neighbor) or grid.walls.has_key?(neighbor)
+ # Add them to the frontier and mark them as visited
+ a_star.frontier << neighbor
+ a_star.came_from[neighbor] = current_frontier
+ a_star.cost_so_far[neighbor] = a_star.cost_so_far[current_frontier] + 1
+ end
+ end
+
+ # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
+ # Comment this line and let a path generate to see the difference
+ a_star.frontier = a_star.frontier.sort_by {| cell | proximity_to_star(cell) }
+ a_star.frontier = a_star.frontier.sort_by {| cell | a_star.cost_so_far[cell] + greedy_heuristic(cell) }
+ end
+
+ # If the search found the target
+ if a_star.came_from.has_key?(grid.target)
+ # Calculate the path between the target and star
+ a_star_calc_path
+ end
+ end
+
+ # Calculates the path between the target and star for the breadth first search
+ # Only called when the breadth first search finds the target
+ def dijkstra_calc_path
+ # Start from the target
+ endpoint = grid.target
+ # And the cell it came from
+ next_endpoint = dijkstra.came_from[endpoint]
+ while endpoint and next_endpoint
+ # Draw a path between these two cells and store it
+ path = get_path_between(endpoint, next_endpoint)
+ dijkstra.path << path
+ # And get the next pair of cells
+ endpoint = next_endpoint
+ next_endpoint = dijkstra.came_from[endpoint]
+ # Continue till there are no more cells
+ end
+ end
+
+ # Returns one-dimensional absolute distance between cell and target
+ # Returns a number to compare distances between cells and the target
+ def greedy_heuristic(cell)
+ (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs
+ end
+
+ # Calculates the path between the target and star for the greedy search
+ # Only called when the greedy search finds the target
+ def greedy_calc_path
+ # Start from the target
+ endpoint = grid.target
+ # And the cell it came from
+ next_endpoint = greedy.came_from[endpoint]
+ while endpoint and next_endpoint
+ # Draw a path between these two cells and store it
+ path = get_path_between(endpoint, next_endpoint)
+ greedy.path << path
+ # And get the next pair of cells
+ endpoint = next_endpoint
+ next_endpoint = greedy.came_from[endpoint]
+ # Continue till there are no more cells
+ end
+ end
+
+ # Calculates the path between the target and star for the a_star search
+ # Only called when the a_star search finds the target
+ def a_star_calc_path
+ # Start from the target
+ endpoint = grid.target
+ # And the cell it came from
+ next_endpoint = a_star.came_from[endpoint]
+
+ while endpoint and next_endpoint
+ # Draw a path between these two cells and store it
+ path = get_path_between(endpoint, next_endpoint)
+ a_star.path << path
+ # And get the next pair of cells
+ endpoint = next_endpoint
+ next_endpoint = a_star.came_from[endpoint]
+ # Continue till there are no more cells
+ 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 = []
+
+ # Gets all the valid neighbors into the array
+ # From southern neighbor, clockwise
+ neighbors << [cell.x , cell.y - 1] unless cell.y == 0
+ neighbors << [cell.x - 1, cell.y ] unless cell.x == 0
+ neighbors << [cell.x , cell.y + 1] unless cell.y == grid.height - 1
+ neighbors << [cell.x + 1, cell.y ] unless cell.x == grid.width - 1
+
+ neighbors
+ end
+
+ # Finds the vertical and horizontal distance of a cell from the star
+ # and returns the larger value
+ # This method is used to have a zigzag pattern in the rendered path
+ # A cell that is [5, 5] from the star,
+ # is explored before over a cell that is [0, 7] away.
+ # So, if possible, the search tries to go diagonal (zigzag) first
+ def proximity_to_star(cell)
+ distance_x = (grid.star.x - cell.x).abs
+ distance_y = (grid.star.y - cell.y).abs
+
+ if distance_x > distance_y
+ return distance_x
+ else
+ return distance_y
+ end
+ end
+
+ # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored.
+ def grid
+ state.grid
+ end
+
+ def dijkstra
+ state.dijkstra
+ end
+
+ def greedy
+ state.greedy
+ end
+
+ def a_star
+ state.a_star
+ end
+
+ # Descriptive aliases for colors
+ def default_color
+ [221, 212, 213] # Light Brown
+ end
+
+ def wall_color
+ [134, 134, 120] # Camo Green
+ end
+
+ def visited_color
+ [204, 191, 179] # Dark Brown
+ end
+
+ def path_color
+ [231, 230, 228] # Pastel White
+ end
+
+ def button_color
+ [190, 190, 190] # Gray
+ 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
+ $a_star_algorithm ||= A_Star_Algorithm.new(args)
+ $a_star_algorithm.args = args
+ $a_star_algorithm.tick
+end
+
+
+def reset
+ $a_star_algorithm = nil
+end
diff --git a/samples/13_path_finding_algorithms/08_a_star/circle-white.png b/samples/13_path_finding_algorithms/08_a_star/circle-white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/13_path_finding_algorithms/08_a_star/circle-white.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/08_a_star/star.png b/samples/13_path_finding_algorithms/08_a_star/star.png
new file mode 100644
index 0000000..b37bb04
--- /dev/null
+++ b/samples/13_path_finding_algorithms/08_a_star/star.png
Binary files differ
diff --git a/samples/13_path_finding_algorithms/08_a_star/target.png b/samples/13_path_finding_algorithms/08_a_star/target.png
new file mode 100644
index 0000000..fb2223d
--- /dev/null
+++ b/samples/13_path_finding_algorithms/08_a_star/target.png
Binary files differ
diff --git a/samples/99_genre_3d/01_3d_cube/app/main.rb b/samples/99_genre_3d/01_3d_cube/app/main.rb
new file mode 100644
index 0000000..fc95291
--- /dev/null
+++ b/samples/99_genre_3d/01_3d_cube/app/main.rb
@@ -0,0 +1,50 @@
+STARTX = 0.0
+STARTY = 0.0
+ENDY = 20.0
+ENDX = 20.0
+SPINPOINT = 10
+SPINDURATION = 400
+POINTSIZE = 8
+BOXDEPTH = 40
+YAW = 1
+DISTANCE = 10
+
+def tick args
+ args.outputs.background_color = [0, 0, 0]
+ a = Math.sin(args.state.tick_count / SPINDURATION) * Math.tan(args.state.tick_count / SPINDURATION)
+ s = Math.sin(a)
+ c = Math.cos(a)
+ x = STARTX
+ y = STARTY
+ offset_x = (1280 - (ENDX - STARTX)) / 2
+ offset_y = (360 - (ENDY - STARTY)) / 2
+
+ srand(1)
+ while y < ENDY do
+ while x < ENDX do
+ if (y == STARTY ||
+ y == (ENDY / 0.5) * 2 ||
+ y == (ENDY / 0.5) * 2 + 0.5 ||
+ y == ENDY - 0.5 ||
+ x == STARTX ||
+ x == ENDX - 0.5)
+ z = rand(BOXDEPTH)
+ z *= Math.sin(a / 2)
+ x -= SPINPOINT
+ u = (x * c) - (z * s)
+ v = (x * s) + (z * c)
+ k = DISTANCE.fdiv(100) + (v / 500 * YAW)
+ u = u / k
+ v = y / k
+ w = POINTSIZE / 10 / k
+ args.outputs.sprites << { x: offset_x + u - w, y: offset_y + v - w, w: w, h: w, path: 'sprites/square-blue.png'}
+ x += SPINPOINT
+ end
+ x += 0.5
+ end
+ y += 0.5
+ x = STARTX
+ end
+end
+
+$gtk.reset
diff --git a/samples/99_genre_3d/01_3d_cube/sprites/square-blue.png b/samples/99_genre_3d/01_3d_cube/sprites/square-blue.png
new file mode 100644
index 0000000..b840849
--- /dev/null
+++ b/samples/99_genre_3d/01_3d_cube/sprites/square-blue.png
Binary files differ
diff --git a/samples/99_genre_3d/02_wireframe/app/main.rb b/samples/99_genre_3d/02_wireframe/app/main.rb
new file mode 100644
index 0000000..8407d47
--- /dev/null
+++ b/samples/99_genre_3d/02_wireframe/app/main.rb
@@ -0,0 +1,150 @@
+def tick args
+ args.state.model ||= Object3D.new('data/shuttle.off')
+ args.state.mtx ||= rotate3D(0, 0, 0)
+ args.state.inv_mtx ||= rotate3D(0, 0, 0)
+ delta_mtx = rotate3D(args.inputs.up_down * 0.01, input_roll(args) * 0.01, args.inputs.left_right * 0.01)
+ args.outputs.lines << args.state.model.edges
+ args.state.model.fast_3x3_transform! args.state.inv_mtx
+ args.state.inv_mtx = mtx_mul(delta_mtx.transpose, args.state.inv_mtx)
+ args.state.mtx = mtx_mul(args.state.mtx, delta_mtx)
+ args.state.model.fast_3x3_transform! args.state.mtx
+ args.outputs.background_color = [0, 0, 0]
+ args.outputs.debug << args.gtk.framerate_diagnostics_primitives
+end
+
+def input_roll args
+ roll = 0
+ roll += 1 if args.inputs.keyboard.e
+ roll -= 1 if args.inputs.keyboard.q
+ roll
+end
+
+def rotate3D(theta_x = 0.1, theta_y = 0.1, theta_z = 0.1)
+ c_x, s_x = Math.cos(theta_x), Math.sin(theta_x)
+ c_y, s_y = Math.cos(theta_y), Math.sin(theta_y)
+ c_z, s_z = Math.cos(theta_z), Math.sin(theta_z)
+ rot_x = [[1, 0, 0], [0, c_x, -s_x], [0, s_x, c_x]]
+ rot_y = [[c_y, 0, s_y], [0, 1, 0], [-s_y, 0, c_y]]
+ rot_z = [[c_z, -s_z, 0], [s_z, c_z, 0], [0, 0, 1]]
+ mtx_mul(mtx_mul(rot_x, rot_y), rot_z)
+end
+
+def mtx_mul(a, b)
+ is = (0...a.length)
+ js = (0...b[0].length)
+ ks = (0...b.length)
+ is.map do |i|
+ js.map do |j|
+ ks.map do |k|
+ a[i][k] * b[k][j]
+ end.reduce(&:plus)
+ end
+ end
+end
+
+class Object3D
+ attr_reader :vert_count, :face_count, :edge_count, :verts, :faces, :edges
+
+ def initialize(path)
+ @vert_count = 0
+ @face_count = 0
+ @edge_count = 0
+ @verts = []
+ @faces = []
+ @edges = []
+ _init_from_file path
+ end
+
+ def _init_from_file path
+ file_lines = $gtk.read_file(path).split("\n")
+ .reject { |line| line.start_with?('#') || line.split(' ').length == 0 } # Strip out simple comments and blank lines
+ .map { |line| line.split('#')[0] } # Strip out end of line comments
+ .map { |line| line.split(' ') } # Tokenize by splitting on whitespace
+ raise "OFF file did not start with OFF." if file_lines.shift != ["OFF"] # OFF meshes are supposed to begin with "OFF" as the first line.
+ raise "<NVertices NFaces NEdges> line malformed" if file_lines[0].length != 3 # The second line needs to have 3 numbers. Raise an error if it doesn't.
+ @vert_count, @face_count, @edge_count = file_lines.shift&.map(&:to_i) # Update the counts
+ # Only the vertex and face counts need to be accurate. Raise an error if they are inaccurate.
+ raise "Incorrect number of vertices and/or faces (Parsed VFE header: #{@vert_count} #{@face_count} #{@edge_count})" if file_lines.length != @vert_count + @face_count
+ # Grab all the lines describing vertices.
+ vert_lines = file_lines[0, @vert_count]
+ # Grab all the lines describing faces.
+ face_lines = file_lines[@vert_count, @face_count]
+ # Create all the vertices
+ @verts = vert_lines.map_with_index { |line, id| Vertex.new(line, id) }
+ # Create all the faces
+ @faces = face_lines.map { |line| Face.new(line, @verts) }
+ # Create all the edges
+ @edges = @faces.flat_map(&:edges).uniq do |edge|
+ sorted = edge.sorted
+ [sorted.point_a, sorted.point_b]
+ end
+ end
+
+ def fast_3x3_transform! mtx
+ @verts.each { |vert| vert.fast_3x3_transform! mtx }
+ end
+end
+
+class Face
+
+ attr_reader :verts, :edges
+
+ def initialize(data, verts)
+ vert_count = data[0].to_i
+ vert_ids = data[1, vert_count].map(&:to_i)
+ @verts = vert_ids.map { |i| verts[i] }
+ @edges = []
+ (0...vert_count).each { |i| @edges[i] = Edge.new(verts[vert_ids[i - 1]], verts[vert_ids[i]]) }
+ @edges.rotate! 1
+ end
+end
+
+class Edge
+ attr_reader :point_a, :point_b
+
+ def initialize(point_a, point_b)
+ @point_a = point_a
+ @point_b = point_b
+ end
+
+ def sorted
+ @point_a.id < @point_b.id ? self : Edge.new(@point_b, @point_a)
+ end
+
+ def draw_override ffi
+ ffi.draw_line(@point_a.render_x, @point_a.render_y, @point_b.render_x, @point_b.render_y, 255, 0, 0, 128)
+ ffi.draw_line(@point_a.render_x+1, @point_a.render_y, @point_b.render_x+1, @point_b.render_y, 255, 0, 0, 128)
+ ffi.draw_line(@point_a.render_x, @point_a.render_y+1, @point_b.render_x, @point_b.render_y+1, 255, 0, 0, 128)
+ ffi.draw_line(@point_a.render_x+1, @point_a.render_y+1, @point_b.render_x+1, @point_b.render_y+1, 255, 0, 0, 128)
+ end
+
+ def primitive_marker
+ :line
+ end
+end
+
+class Vertex
+ attr_accessor :x, :y, :z, :id
+
+ def initialize(data, id)
+ @x = data[0].to_f
+ @y = data[1].to_f
+ @z = data[2].to_f
+ @id = id
+ end
+
+ def fast_3x3_transform! mtx
+ _x, _y, _z = @x, @y, @z
+ @x = mtx[0][0] * _x + mtx[0][1] * _y + mtx[0][2] * _z
+ @y = mtx[1][0] * _x + mtx[1][1] * _y + mtx[1][2] * _z
+ @z = mtx[2][0] * _x + mtx[2][1] * _y + mtx[2][2] * _z
+ end
+
+ def render_x
+ @x * (10 / (5 - @y)) * 170 + 640
+ end
+
+ def render_y
+ @z * (10 / (5 - @y)) * 170 + 360
+ end
+end \ No newline at end of file
diff --git a/samples/99_genre_3d/02_wireframe/data/shuttle.off b/samples/99_genre_3d/02_wireframe/data/shuttle.off
new file mode 100644
index 0000000..4d0803c
--- /dev/null
+++ b/samples/99_genre_3d/02_wireframe/data/shuttle.off
@@ -0,0 +1,284 @@
+OFF
+167 115 0
+-0.0556651316583157 -0.5373616218566895 0.2210596650838852
+-0.1558777540922165 -0.5373616218566895 0.1656820923089981
+-0.1971056312322617 -0.5373616218566895 0.0667097717523575
+0.0555876418948174 -0.5373616218566895 0.2210596650838852
+0.1558002829551697 -0.5373616218566895 0.1656820923089981
+0.1970281600952148 -0.5373616218566895 0.0667097717523575
+0.2002865821123123 -0.5373616218566895 -0.0815882310271263
+0.1503289043903351 -0.5373616218566895 -0.1599303781986237
+-0.2003640681505203 -0.5373616218566895 -0.0815882310271263
+-0.1504063904285431 -0.5373616218566895 -0.1599303781986237
+-0.0556651316583157 0.7521815299987793 0.2210596650838852
+-0.1558777540922165 0.7521815299987793 0.1656820923089981
+-0.1971056312322617 0.7521815299987793 0.0667097717523575
+0.0555876418948174 0.7521815299987793 0.2210596650838852
+0.1558002829551697 0.7521815299987793 0.1656820923089981
+0.1970281600952148 0.7521815299987793 0.0667097717523575
+0.2002865821123123 0.7521815299987793 -0.0815882310271263
+0.1503289043903351 0.7521815299987793 -0.1599303781986237
+0.0555877126753330 0.7521815299987793 -0.1941412389278412
+-0.2003640681505203 0.7521815299987793 -0.0815882310271263
+-0.1504063904285431 0.7521815299987793 -0.1599303781986237
+-0.0556652024388313 0.7521815299987793 -0.1941412389278412
+-0.0375367477536201 -0.8870638012886047 0.0328058265149593
+-0.1050905436277390 -0.8870638012886047 0.0040050041861832
+-0.1328824460506439 -0.8870638012886047 -0.0474686101078987
+0.0374592579901218 -0.8870638012886047 0.0328058265149593
+0.1050130575895309 -0.8870638012886047 0.0040050041861832
+0.1328049600124359 -0.8870638012886047 -0.0474686101078987
+0.1350014954805374 -0.8870638012886047 -0.1245955601334572
+0.1013247817754745 -0.8870638012886047 -0.1653398126363754
+0.0374593064188957 -0.8870638012886047 -0.1831322312355042
+-0.1350789815187454 -0.8870638012886047 -0.1245955601334572
+-0.1014022752642632 -0.8870638012886047 -0.1653398126363754
+-0.0375367961823940 -0.8870638012886047 -0.1831322312355042
+-0.0172216147184372 -0.9763066768646240 -0.0525034293532372
+-0.0481770746409893 -0.9763066768646240 -0.0657009631395340
+-0.0609122700989246 -0.9763066768646240 -0.0892879292368889
+0.0171441230922937 -0.9763066768646240 -0.0525034293532372
+0.0480995811522007 -0.9763066768646240 -0.0657009631395340
+0.0608347803354263 -0.9763066768646240 -0.0892879292368889
+0.0618413127958775 -0.9763066768646240 -0.1246301382780075
+0.0464094914495945 -0.9763066768646240 -0.1433005332946777
+0.0171441435813904 -0.9763066768646240 -0.1514536291360855
+-0.0619188025593758 -0.9763066768646240 -0.1246301382780075
+-0.0464869812130928 -0.9763066768646240 -0.1433005332946777
+-0.0172216352075338 -0.9763066768646240 -0.1514536291360855
+-0.0000387450963899 -1.0021373033523560 -0.1109079420566559
+-0.0445398576557636 0.7521816492080688 0.1774666309356689
+-0.1247099563479424 0.7521816492080688 0.1331645548343658
+0.0444623678922653 0.7521816492080688 0.1774666309356689
+0.1246324777603149 0.7521816492080688 0.1331645548343658
+0.1576147824525833 0.7521816492080688 0.0539867058396339
+0.1602215319871902 0.7521816492080688 -0.0646517053246498
+0.1202553808689117 0.7521816492080688 -0.1273254156112671
+0.0444624200463295 0.7521816492080688 -0.1546941250562668
+-0.1576922535896301 0.7521816492080688 0.0539867058396339
+-0.1602990031242371 0.7521816492080688 -0.0646517053246498
+-0.1203328520059586 0.7521816492080688 -0.1273254156112671
+-0.0445399098098278 0.7521816492080688 -0.1546941250562668
+-0.0445398576557636 0.7211543321609497 0.1774666309356689
+-0.1247099563479424 0.7211543321609497 0.1331645548343658
+0.0444623678922653 0.7211543321609497 0.1774666309356689
+0.1246324777603149 0.7211543321609497 0.1331645548343658
+0.1576147824525833 0.7211543321609497 0.0539867058396339
+0.1602215319871902 0.7211543321609497 -0.0646517053246498
+0.1202553808689117 0.7211543321609497 -0.1273254156112671
+0.0444624200463295 0.7211543321609497 -0.1546941250562668
+-0.1576922535896301 0.7211543321609497 0.0539867058396339
+-0.1602990031242371 0.7211543321609497 -0.0646517053246498
+-0.1203328520059586 0.7211543321609497 -0.1273254156112671
+-0.0445399098098278 0.7211543321609497 -0.1546941250562668
+-0.0158861558884382 0.7211543321609497 -0.0512818209826946
+-0.0230847373604774 0.7211543321609497 -0.0244163442403078
+-0.0427516363561153 0.7211543321609497 -0.0047494410537183
+-0.0696171224117279 0.7211543321609497 0.0024491411168128
+-0.0964826047420502 0.7211543321609497 -0.0047494443133473
+-0.1161495000123978 0.7211543321609497 -0.0244163367897272
+-0.1233480796217918 0.7211543321609497 -0.0512818172574043
+-0.1161495000123978 0.7211543321609497 -0.0781472921371460
+-0.0964826121926308 0.7211543321609497 -0.0978141874074936
+-0.0696171447634697 0.7211543321609497 -0.1050127968192101
+-0.0427516624331474 0.7211543321609497 -0.0978142097592354
+-0.0230847634375095 0.7211543321609497 -0.0781473368406296
+0.0158086661249399 0.7211543321609497 -0.0512818209826946
+0.0230072475969791 0.7211543321609497 -0.0244163442403078
+0.0426741465926170 0.7211543321609497 -0.0047494410537183
+0.0695396289229393 0.7211543321609497 0.0024491411168128
+0.0964051112532616 0.7211543321609497 -0.0047494443133473
+0.1160720214247704 0.7211543321609497 -0.0244163367897272
+0.1232706010341644 0.7211543321609497 -0.0512818172574043
+0.1160720214247704 0.7211543321609497 -0.0781472921371460
+0.0964051261544228 0.7211543321609497 -0.0978141874074936
+0.0695396587252617 0.7211543321609497 -0.1050127968192101
+0.0426741726696491 0.7211543321609497 -0.0978142097592354
+0.0230072736740112 0.7211543321609497 -0.0781473368406296
+-0.0269042234867811 0.7211543321609497 0.1413877010345459
+-0.0465711168944836 0.7211543321609497 0.1217208057641983
+-0.0000387450963899 0.7211543321609497 0.1485862880945206
+0.0268267337232828 0.7211543321609497 0.1413877010345459
+0.0464936271309853 0.7211543321609497 0.1217208057641983
+0.0536922141909599 0.7211543321609497 0.0948553308844566
+0.0464936345815659 0.7211543321609497 0.0679898560047150
+0.0268267467617989 0.7211543321609497 0.0483229570090771
+-0.0000387450963899 0.7211543321609497 0.0411243513226509
+-0.0537697039544582 0.7211543321609497 0.0948553308844566
+-0.0465711243450642 0.7211543321609497 0.0679898560047150
+-0.0269042365252972 0.7211543321609497 0.0483229570090771
+-0.0105130579322577 0.7469451427459717 -0.0512818209826946
+-0.0184314977377653 0.7469451427459717 -0.0217297952622175
+-0.0400650873780251 0.7469451427459717 -0.0000962029516813
+-0.0696171224117279 0.7469451427459717 0.0078222369775176
+-0.0991691499948502 0.7469451427459717 -0.0000962029516813
+-0.1208027452230453 0.7469451427459717 -0.0217297878116369
+-0.1287211626768112 0.7469451427459717 -0.0512818172574043
+-0.1208027452230453 0.7469451427459717 -0.0808338448405266
+-0.0991691574454308 0.7469451427459717 -0.1024674326181412
+-0.0696171447634697 0.7469451427459717 -0.1103858947753906
+0.0104355672374368 0.7469451427459717 -0.0512818209826946
+0.0183540079742670 0.7469451427459717 -0.0217297952622175
+0.0399875976145267 0.7469451427459717 -0.0000962029516813
+0.0695396289229393 0.7469451427459717 0.0078222369775176
+0.0990916565060616 0.7469451427459717 -0.0000962029516813
+0.1207252666354179 0.7469451427459717 -0.0217297878116369
+0.1286436915397644 0.7469451427459717 -0.0512818172574043
+0.1207252666354179 0.7469451427459717 -0.0808338448405266
+0.0990916714072227 0.7469451427459717 -0.1024674326181412
+0.0695396587252617 0.7469451427459717 -0.1103858947753906
+0.0399876236915588 0.7469451427459717 -0.1024674549698830
+0.0183540340512991 0.7469451427459717 -0.0808338895440102
+-0.0400651134550571 0.7469451427459717 -0.1024674549698830
+-0.0184315238147974 0.7469451427459717 -0.0808338895440102
+-0.0000387450963899 0.7469451427459717 0.1539593935012817
+0.0295132827013731 0.7469451427459717 0.1460409313440323
+0.0511468648910522 0.7469451427459717 0.1244073510169983
+0.0590653121471405 0.7469451427459717 0.0948553308844566
+0.0511468723416328 0.7469451427459717 0.0653033033013344
+0.0295132957398891 0.7469451427459717 0.0436697266995907
+-0.0000387450963899 0.7469451427459717 0.0357512533664703
+-0.0295907724648714 0.7469451427459717 0.1460409313440323
+-0.0512243546545506 0.7469451427459717 0.1244073510169983
+0.1986573636531830 -0.5373616218566895 -0.0507182702422142
+0.1986573636531830 0.7521815299987793 -0.0507182702422142
+0.6378207802772522 0.7521815299987793 -0.0815882310271263
+0.6361915469169617 0.7521815299987793 -0.0507182702422142
+-0.0591428019106388 0.7469451427459717 0.0948553308844566
+0.0185033846646547 0.7521815299987793 0.2210596650838852
+-0.0512243621051311 0.7469451427459717 0.0653033033013344
+-0.0295907855033875 0.7469451427459717 0.0436697266995907
+0.0185033846646547 0.4187007546424866 0.2210596650838852
+0.0185033846646547 0.7521815299987793 0.4184791445732117
+-0.0000387450963899 0.7521815299987793 -0.1941412389278412
+-0.0000387450963899 0.7521816492080688 -0.1546941250562668
+-0.1987348496913910 -0.5373616218566895 -0.0507182702422142
+-0.1987348496913910 0.7521815299987793 -0.0507182702422142
+-0.6378982663154602 0.7521815299987793 -0.0815882310271263
+-0.6362690329551697 0.7521815299987793 -0.0507182702422142
+-0.0185808744281530 0.7521815299987793 0.2210596650838852
+-0.0185808744281530 0.4187007546424866 0.2210596650838852
+-0.0185808744281530 0.7521815299987793 0.4184791445732117
+-0.1220196112990379 -0.7704964280128479 0.0578973665833473
+-0.1389486789703369 -0.6539289951324463 0.1117897257208824
+-0.0496223382651806 -0.6539289951324463 0.1583083868026733
+-0.0435795448720455 -0.7704964280128479 0.0955571085214615
+0.1219421327114105 -0.7704964280128479 0.0578973665833473
+0.1388712078332901 -0.6539289951324463 0.1117897257208824
+0.0495448485016823 -0.6539289951324463 0.1583083868026733
+0.0435020551085472 -0.7704964280128479 0.0955571085214615
+4 5 4 14 15
+4 152 153 12 2
+3 152 155 153
+4 140 5 15 141
+5 18 30 29 7 17
+4 7 6 16 17
+4 4 3 13 14
+5 21 20 9 32 33
+4 2 12 11 1
+6 4 5 27 26 163 164
+4 8 31 32 9
+6 1 160 159 23 24 2
+5 5 140 6 28 27
+5 150 21 33 30 18
+4 6 7 29 28
+4 166 163 26 25
+4 162 22 23 159
+4 9 20 19 8
+4 27 28 40 39
+3 46 34 37
+3 43 46 44
+4 28 29 41 40
+4 25 26 38 37
+4 24 36 43 31
+4 29 30 42 41
+4 26 27 39 38
+3 44 46 45
+4 32 44 45 33
+3 39 40 46
+3 34 46 35
+3 40 41 46
+3 37 38 46
+4 48 60 59 47
+3 41 42 46
+3 38 39 46
+3 35 46 36
+4 23 35 36 24
+3 36 46 43
+8 0 10 156 157 148 145 13 3
+5 66 70 58 151 54
+4 50 49 61 62
+4 165 161 0 3
+4 58 70 69 57
+4 54 53 65 66
+4 51 50 62 63
+5 61 59 95 97 98
+4 52 51 63 64
+4 55 67 60 48
+4 57 69 68 56
+4 53 52 64 65
+4 72 108 107 71
+12 131 138 139 144 146 147 137 136 135 134 133 132
+4 71 107 130 82
+4 80 116 115 79
+4 84 83 117 118
+12 108 109 110 111 112 113 114 115 116 129 130 107
+4 84 85 119 118
+4 96 139 138 95
+4 98 97 131 132
+4 86 85 119 120
+4 75 111 110 74
+4 99 98 132 133
+4 87 86 120 121
+4 104 144 139 96
+4 100 99 133 134
+4 88 87 121 122
+4 76 112 111 75
+4 101 100 134 135
+4 89 88 122 123
+4 105 146 144 104
+4 102 101 135 136
+4 90 89 123 124
+4 77 113 112 76
+4 102 103 137 136
+4 91 90 124 125
+4 106 147 146 105
+4 92 91 125 126
+4 78 114 113 77
+3 19 154 8
+4 93 92 126 127
+4 106 147 137 103
+4 94 93 127 128
+4 79 115 114 78
+4 83 94 128 117
+4 81 129 116 80
+4 82 130 129 81
+12 118 117 128 127 126 125 124 123 122 121 120 119
+4 47 59 61 49
+4 74 110 109 73
+3 16 6 142
+4 6 140 143 142
+3 140 141 143
+46 69 70 66 65 64 63 62 61 98 99 100 101 102 85 86 87 88 89 90 91 92 93 94 83 84 72 71 82 81 80 79 78 77 76 75 74 73 106 105 104 96 95 59 60 67 68
+3 156 158 157
+3 145 148 149
+4 8 154 155 152
+4 95 138 131 97
+7 84 72 73 106 103 102 85
+4 72 108 109 73
+4 56 68 67 55
+4 157 158 149 148
+4 31 43 44 32
+3 45 46 42
+4 22 34 35 23
+8 145 149 158 156 10 47 49 13
+5 2 24 31 8 152
+4 1 11 10 0
+4 37 34 22 25
+4 33 45 42 30
+4 25 22 162 166
+8 165 164 163 166 162 159 160 161
+4 0 161 160 1
+4 3 4 164 165
+17 10 11 12 153 155 154 19 20 21 150 151 58 57 56 55 48 47
+17 13 49 50 51 52 53 54 151 150 18 17 16 142 143 141 15 14
diff --git a/samples/99_genre_3d/02_wireframe/data/what-is-this.txt b/samples/99_genre_3d/02_wireframe/data/what-is-this.txt
new file mode 100644
index 0000000..2b14e34
--- /dev/null
+++ b/samples/99_genre_3d/02_wireframe/data/what-is-this.txt
@@ -0,0 +1 @@
+https://en.wikipedia.org/wiki/OFF_(file_format) \ No newline at end of file
diff --git a/samples/99_genre_arcade/twinstick/app/main.rb b/samples/99_genre_arcade/twinstick/app/main.rb
new file mode 100644
index 0000000..4edef92
--- /dev/null
+++ b/samples/99_genre_arcade/twinstick/app/main.rb
@@ -0,0 +1,151 @@
+def tick args
+ args.state.player ||= {x: 600, y: 320, w: 80, h: 80, path: 'sprites/circle-white.png', vx: 0, vy: 0, health: 10, cooldown: 0, score: 0}
+ args.state.enemies ||= []
+ args.state.player_bullets ||= []
+ args.state.tick_count ||= -1
+ args.state.tick_count += 1
+ spawn_enemies args
+ kill_enemies args
+ move_enemies args
+ move_bullets args
+ move_player args
+ fire_player args
+ args.state.player[:r] = args.state.player[:g] = args.state.player[:b] = (args.state.player[:health] * 25.5).clamp(0, 255)
+ label_color = args.state.player[:health] <= 5 ? 255 : 0
+ args.outputs.labels << [
+ {
+ x: args.state.player.x + 40, y: args.state.player.y + 60, alignment_enum: 1, text: "#{args.state.player[:health]} HP",
+ r: label_color, g: label_color, b: label_color
+ }, {
+ x: args.state.player.x + 40, y: args.state.player.y + 40, alignment_enum: 1, text: "#{args.state.player[:score]} PTS",
+ r: label_color, g: label_color, b: label_color, size_enum: 2 - args.state.player[:score].to_s.length,
+ }
+ ]
+ args.outputs.sprites << [args.state.player, args.state.enemies, args.state.player_bullets]
+ args.state.clear! if args.state.player[:health] < 0 # Reset the game if the player's health drops below zero
+end
+
+def spawn_enemies args
+ # Spawn enemies more frequently as the player's score increases.
+ if rand < (100+args.state.player[:score])/(10000 + args.state.player[:score]) || args.state.tick_count.zero?
+ theta = rand * Math::PI * 2
+ args.state.enemies << {
+ x: 600 + Math.cos(theta) * 800, y: 320 + Math.sin(theta) * 800, w: 80, h: 80, path: 'sprites/circle-white.png',
+ r: (256 * rand).floor, g: (256 * rand).floor, b: (256 * rand).floor
+ }
+ end
+end
+
+def kill_enemies args
+ args.state.enemies.reject! do |enemy|
+ # Check if enemy and player are within 80 pixels of each other (i.e. overlapping)
+ if 6400 > (enemy.x - args.state.player.x) ** 2 + (enemy.y - args.state.player.y) ** 2
+ # Enemy is touching player. Kill enemy, and reduce player HP by 1.
+ args.state.player[:health] -= 1
+ else
+ args.state.player_bullets.any? do |bullet|
+ # Check if enemy and bullet are within 50 pixels of each other (i.e. overlapping)
+ if 2500 > (enemy.x - bullet.x + 30) ** 2 + (enemy.y - bullet.y + 30) ** 2
+ # Increase player health by one for each enemy killed by a bullet after the first enemy, up to a maximum of 10 HP
+ args.state.player[:health] += 1 if args.state.player[:health] < 10 && bullet[:kills] > 0
+ # Keep track of how many enemies have been killed by this particular bullet
+ bullet[:kills] += 1
+ # Earn more points by killing multiple enemies with one shot.
+ args.state.player[:score] += bullet[:kills]
+ end
+ end
+ end
+ end
+end
+
+def move_enemies args
+ args.state.enemies.each do |enemy|
+ # Get the angle from the enemy to the player
+ theta = Math.atan2(enemy.y - args.state.player.y, enemy.x - args.state.player.x)
+ # Convert the angle to a vector pointing at the player
+ dx, dy = theta.to_degrees.vector 5
+ # Move the enemy towards thr player
+ enemy.x -= dx
+ enemy.y -= dy
+ end
+end
+
+def move_bullets args
+ args.state.player_bullets.each do |bullet|
+ # Move the bullets according to the bullet's velocity
+ bullet.x += bullet[:vx]
+ bullet.y += bullet[:vy]
+ end
+ args.state.player_bullets.reject! do |bullet|
+ # Despawn bullets that are outside the screen area
+ bullet.x < -20 || bullet.y < -20 || bullet.x > 1300 || bullet.y > 740
+ end
+end
+
+def move_player args
+ # Get the currently held direction.
+ dx, dy = move_directional_vector args
+ # Take the weighted average of the old velocities and the desired velocities.
+ # Since move_directional_vector returns values between -1 and 1,
+ # and we want to limit the speed to 7.5, we multiply dx and dy by 7.5*0.1 to get 0.75
+ args.state.player[:vx] = args.state.player[:vx] * 0.9 + dx * 0.75
+ args.state.player[:vy] = args.state.player[:vy] * 0.9 + dy * 0.75
+ # Move the player
+ args.state.player.x += args.state.player[:vx]
+ args.state.player.y += args.state.player[:vy]
+ # If the player is about to go out of bounds, put them back in bounds.
+ args.state.player.x = args.state.player.x.clamp(0, 1201)
+ args.state.player.y = args.state.player.y.clamp(0, 640)
+end
+
+
+def fire_player args
+ # Reduce the firing cooldown each tick
+ args.state.player[:cooldown] -= 1
+ # If the player is allowed to fire
+ if args.state.player[:cooldown] <= 0
+ dx, dy = shoot_directional_vector args # Get the bullet velocity
+ return if dx == 0 && dy == 0 # If the velocity is zero, the player doesn't want to fire. Therefore, we just return early.
+ # Add a new bullet to the list of player bullets.
+ args.state.player_bullets << {
+ x: args.state.player.x + 30 + 40 * dx,
+ y: args.state.player.y + 30 + 40 * dy,
+ w: 20, h: 20,
+ path: 'sprites/circle-white.png',
+ r: 0, g: 0, b: 0,
+ vx: 10 * dx + args.state.player[:vx] / 7.5, vy: 10 * dy + args.state.player[:vy] / 7.5, # Factor in a bit of the player's velocity
+ kills: 0
+ }
+ args.state.player[:cooldown] = 30 # Reset the cooldown
+ end
+end
+
+# Custom function for getting a directional vector just for movement using WASD
+def move_directional_vector args
+ dx = 0
+ dx += 1 if args.inputs.keyboard.d
+ dx -= 1 if args.inputs.keyboard.a
+ dy = 0
+ dy += 1 if args.inputs.keyboard.w
+ dy -= 1 if args.inputs.keyboard.s
+ if dx != 0 && dy != 0
+ dx *= 0.7071
+ dy *= 0.7071
+ end
+ [dx, dy]
+end
+
+# Custom function for getting a directional vector just for shooting using the arrow keys
+def shoot_directional_vector args
+ dx = 0
+ dx += 1 if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_held.right
+ dx -= 1 if args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_held.left
+ dy = 0
+ dy += 1 if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_held.up
+ dy -= 1 if args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_held.down
+ if dx != 0 && dy != 0
+ dx *= 0.7071
+ dy *= 0.7071
+ end
+ [dx, dy]
+end \ No newline at end of file
diff --git a/samples/99_genre_arcade/twinstick/sprites/circle-white.png b/samples/99_genre_arcade/twinstick/sprites/circle-white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/99_genre_arcade/twinstick/sprites/circle-white.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/app/main.rb b/samples/99_genre_rpg_tactical/gameboard_movement/app/main.rb
new file mode 100644
index 0000000..8eb935e
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/app/main.rb
@@ -0,0 +1,529 @@
+# Size of board is always 1280x720
+
+def tick args
+ size = 64
+
+ # Draw a checkerboard as a placeholder game board
+ i = 0
+ j = 0
+ while i < 12 do
+ while j < 21 do
+ args.outputs.solids << [(j*size), (i*size), size, size, 255, 100, 0, ((i+j) % 2 == 0) ? 255 : 0]
+ j += 1
+ end
+ j = 0
+ i += 1
+ end
+ k = 0
+ ary = Array.new(220)
+ while k < 220
+ ary[k] = 1
+ if k > 20 and k < 36
+ ary[k] = 0
+ end
+ if k > 40 and k < 56
+ ary[k] = 0
+ end
+ if k > 60 and k < 76
+ ary[k] = 0
+ end
+ if k > 80 and k < 96
+ ary[k] = 0
+ end
+ if k > 100 and k < 116
+ ary[k] = 0
+ end
+ if k > 120 and k < 136
+ ary[k] = 0
+ end
+ if k == 161 and args.state.bot3.hp >= 1
+ ary[k] = 0
+ end
+ if k == 147 and args.state.bot1.hp >= 1
+ ary[k] = 0
+ end
+ if k == 215 and args.state.bot2.hp >= 1
+ ary[k] = 0
+ end
+ k += 1
+ end
+ ary2 = ary;
+ htarget = 161
+ target = 160
+ targetb = 181
+ targetc = 141
+ targetd = 162
+ target2 = 146
+ target2b = 167
+ target2c = 148
+ htarget2 = 147
+ target3 = 214
+ target3c = 216
+ target3b = 195
+ htarget3 = 215
+ # player attributes
+ args.state.player.x ||= 0
+ args.state.player.y ||= 0
+ args.state.player.w ||= 64
+ args.state.player.h ||= 64
+ args.state.player.direction ||= 1
+ args.state.player.hp ||= 100
+ args.state.player.strength ||= 100
+ args.state.player.pos ||= 0
+
+ # bot1 attributes
+ args.state.bot1.x ||= 448
+ args.state.bot1.y ||= 448
+ args.state.bot1.w ||= 64
+ args.state.bot1.h ||= 64
+ args.state.bot1.direction ||= 1
+ args.state.bot1.hp ||= 100
+ args.state.bot1.strength ||= 5
+
+ # bot2 attributes
+ args.state.bot2.x ||= 960
+ args.state.bot2.y ||= 640
+ args.state.bot2.w ||= 64
+ args.state.bot2.h ||= 64
+ args.state.bot2.direction ||= 1
+ args.state.bot2.hp ||= 100
+ args.state.bot2.strength ||= 8
+
+ # bot3 attributes
+ args.state.bot3.x ||= 64
+ args.state.bot3.y ||= 512
+ args.state.bot3.w ||= 64
+ args.state.bot3.h ||= 64
+ args.state.bot3.direction ||= 1
+ args.state.bot3.hp ||= 100
+ args.state.bot3.strength ||= 8
+
+ # obstacle attributes
+ args.state.obs1.x ||= 64
+ args.state.obs1.y ||= 64
+ args.state.obs1.w ||= 960
+ args.state.obs1.h ||= 384
+ args.state.obs1.direction ||= 1
+
+
+ @menu_shown ||= :hidden
+
+ # display menu
+ if @menu_shown == :hidden
+ args.state.menu_button ||= new_button :menu, 1081, 650, "Menu"
+ args.outputs.primitives << args.state.menu_button[:primitives]
+
+ if button_clicked? args, args.state.menu_button
+ @menu_shown = :visible
+ end
+
+ else
+ args.state.menu_overlay = [1080, 0, 200, 720, 100, 0, 0, 250]
+
+ # first overlay
+ if args.state.menu_overlay
+ args.outputs.solids << args.state.menu_overlay
+
+ # move button
+ args.state.move_button ||= new_button :move, 1081, 650, "Move"
+ args.outputs.primitives << args.state.move_button[:primitives]
+
+ if button_clicked? args, args.state.move_button
+ args.gtk.notify! "Move button was clicked!"
+ end
+
+ # attack button
+ args.state.attack_button ||= new_button :attack, 1081, 600, "Attack"
+ args.outputs.primitives << args.state.attack_button[:primitives]
+
+ if button_clicked? args, args.state.attack_button
+
+ if args.state.player.pos+1 == htarget or args.state.player.pos-1 == htarget or args.state.player.pos+20 == htarget or args.state.player.pos-20 == htarget
+ damage = rand(100)
+ dealt = "#{damage} Damage Dealt!"
+ args.gtk.notify! dealt
+ args.state.bot3.hp -= damage
+
+ end
+ if args.state.player.pos+1 == htarget2 or args.state.player.pos-1 == htarget2 or args.state.player.pos+20 == htarget2 or args.state.player.pos-20 == htarget2
+ damage = rand(100)
+ dealt = "#{damage} Damage Dealt!"
+ args.gtk.notify! dealt
+
+ args.state.bot1.hp -= damage
+
+ end
+ if args.state.player.pos+1 == htarget3 or args.state.player.pos-1 == htarget3 or args.state.player.pos+20 == htarget3 or args.state.player.pos-20 == htarget3
+ damage = rand(100)
+ dealt = "#{damage} Damage Dealt!"
+ args.gtk.notify! dealt
+ args.state.bot2.hp -= damage
+
+ end
+ end
+
+ # items button
+ args.state.items_button ||= new_button :items, 1081, 550, "Items"
+ args.outputs.primitives << args.state.items_button[:primitives]
+
+ if button_clicked? args, args.state.items_button
+ args.state.itemMenu_overlay = [880, 0, 200, 720, 150, 0, 0, 250]
+ args.gtk.notify! "Items button was clicked!"
+ end
+
+ # second overlay
+ if args.state.itemMenu_overlay
+ args.outputs.solids << args.state.itemMenu_overlay
+ args.outputs.labels << [960, 700, "Items"]
+
+ # create items
+ args.state.potion_button ||= new_button :potion, 881, 600, "Potion"
+ args.outputs.primitives << args.state.potion_button[:primitives]
+
+ if button_clicked? args, args.state.potion_button
+ args.gtk.notify! "Potion Used!"
+ end
+
+ args.state.elixer_button ||= new_button :potion, 881, 550, "Elixer"
+ args.outputs.primitives << args.state.elixer_button[:primitives]
+
+ if button_clicked? args, args.state.elixer_button
+ args.gtk.notify! "Elixer Used!"
+ end
+
+ end
+
+ # wait button
+ args.state.wait_button ||= new_button :wait, 1081, 500, "Wait"
+ args.outputs.primitives << args.state.wait_button[:primitives]
+
+ if button_clicked? args, args.state.wait_button
+ args.gtk.notify! "Wait button was clicked!"
+ end
+
+ # close button
+ args.state.close_button ||= new_button :close, 1081, 450, "Close"
+ args.outputs.primitives << args.state.close_button[:primitives]
+
+ # hide menu
+ if button_clicked? args, args.state.close_button
+ @menu_shown = :hidden
+ end
+
+ end
+ end
+
+ # left and right movement
+ if args.inputs.keyboard.key_down.right and ary[args.state.player.pos+1] == 1 and args.state.player.pos%20 < 19
+ args.state.player.direction = 1
+ args.state.player.started_running_at = args.state.tick_count
+ args.state.player.x += size
+ args.state.player.pos += 1
+ elsif args.inputs.keyboard.key_down.left and ary[args.state.player.pos-1] == 1 and args.state.player.pos%20 > 0
+ args.state.player.direction = -1
+ args.state.player.started_running_at = args.state.tick_count
+ args.state.player.x -= size
+ args.state.player.pos -= 1
+ end
+
+
+
+
+ # up and down movement
+ if args.inputs.keyboard.key_down.up and ary[args.state.player.pos+20] == 1 and args.state.player.pos < 200
+ args.state.player.direction = 1
+ args.state.player.started_running_at = args.state.tick_count
+ args.state.player.y += size
+ args.state.player.pos += 20
+ elsif args.inputs.keyboard.key_down.down and ary[args.state.player.pos-20] == 1 and args.state.player.pos >= 20
+ args.state.player.direction = -1
+ args.state.player.started_running_at = args.state.tick_count
+ args.state.player.y -= size
+ args.state.player.pos -= 20
+ end
+
+ pigga = args.state.player.intersect_rect? args.state.obs1
+ if pigga
+ args.gtk.notify! "sprites collide!"
+ args.state.player.y -= size
+ end
+ looping = true
+ cangoup = false
+ cangoright = false
+ cangodown = false
+ cangoleft = false
+ ftarget = 10000
+ hold = 9
+ testtarget = args.state.player.pos
+ if args.state.bot3.hp > 0
+ ftarget = (target-testtarget).abs
+ hold = target
+ end
+ if(ftarget > (targetb-testtarget).abs and args.state.bot3.hp > 0)
+ ftarget = (targetb-testtarget).abs
+ hold = targetb
+ end
+ if(ftarget > (targetc-testtarget).abs and args.state.bot3.hp > 0)
+ ftarget = (targetc-testtarget).abs
+ hold = targetc
+ end
+ if(ftarget > (targetd-testtarget).abs and args.state.bot3.hp > 0)
+ ftarget = (targetd-testtarget).abs
+ hold = targetd
+ end
+ if(ftarget > (target2-testtarget).abs and args.state.bot1.hp > 0)
+ ftarget = (target2-testtarget).abs
+ hold = target2
+ end
+ if(ftarget > (target2b-testtarget).abs and args.state.bot1.hp > 0)
+ ftarget = (target2b-testtarget).abs
+ hold = target2b
+ end
+ if(ftarget > (target2c-testtarget).abs and args.state.bot1.hp > 0)
+ ftarget = (target2c-testtarget).abs
+ hold = target2c
+ end
+ if(ftarget > (target3-testtarget).abs and args.state.bot2.hp > 0)
+ ftarget = (target3-testtarget).abs
+ hold = target3
+ end
+ if(ftarget > (target3b-testtarget).abs and args.state.bot2.hp > 0)
+ ftarget = (target3b-testtarget).abs
+ hold = target3b
+ end
+ if(ftarget > (target3c-testtarget).abs and args.state.bot2.hp > 0)
+ ftarget = (target3c-testtarget).abs
+ hold = target3c
+ end
+ if ftarget == 10000
+ looping = false
+ end
+ if args.inputs.keyboard.key_down.f
+ while looping and target != testtarget and targetb != testtarget and targetc != testtarget and targetd != testtarget and target2 != testtarget and target2b != testtarget and target2c != testtarget and target3 != testtarget and target3b != testtarget and target3c != testtarget
+ cangoup = false
+ cangoright = false
+ cangodown = false
+ cangoleft = false
+
+ if hold-testtarget >= 20 and ary2[args.state.player.pos+20] == 1 and args.state.player.pos < 200
+ cangoup = true
+ elsif testtarget-hold >= 20 and ary2[args.state.player.pos-20] == 1 and args.state.player.pos > 20
+ cangodown = true
+ cangoup = false
+ elsif hold%20-testtarget%20 > 0 and ary2[args.state.player.pos+1] == 1 and args.state.player.pos%20 < 19
+ cangodown = false
+ cangoup = false
+ cangoright = true
+ elsif testtarget%20-hold%20 > 0 and ary2[args.state.player.pos-1] == 1 and args.state.player.pos%20 > 0
+ cangodown = false
+ cangoup = false
+ cangoright = false
+ cangoleft = true
+ end
+ if cangodown == false and cangoup == false and cangoleft == false and cangoright == false
+ if ary2[args.state.player.pos+20] == 1 and args.state.player.pos < 200
+ cangoup = true
+ elsif ary2[args.state.player.pos-20] == 1 and args.state.player.pos > 20
+ cangodown = true
+ cangoup = false
+ elsif ary2[args.state.player.pos+1] == 1 and args.state.player.pos%20 < 19
+ cangodown = false
+ cangoup = false
+ cangoright = true
+ elsif ary2[args.state.player.pos-1] == 1 and args.state.player.pos%20 > 0
+ cangodown = false
+ cangoup = false
+ cangoright = false
+ cangoleft = true
+ end
+ end
+ if cangodown == true
+ args.state.player.direction = -1
+ args.state.player.started_running_at = args.state.tick_count
+ args.state.player.y -= size
+ ary2[args.state.player.pos] = 0
+ args.state.player.pos -= 20
+ testtarget -= 20
+
+ elsif cangoup == true
+ args.state.player.direction = 1
+ args.state.player.started_running_at = args.state.tick_count
+ args.state.player.y += size
+ ary2[args.state.player.pos] = 0
+ args.state.player.pos += 20
+ testtarget+=20
+
+ elsif cangoright == true
+ args.state.player.direction = 1
+ args.state.player.started_running_at = args.state.tick_count
+ args.state.player.x += size
+ ary2[args.state.player.pos] = 0
+ args.state.player.pos += 1
+ testtarget += 1
+
+ elsif cangoleft == true
+ args.state.player.direction = -1
+ args.state.player.started_running_at = args.state.tick_count
+ args.state.player.x -= size
+ ary2[args.state.player.pos] = 0
+ args.state.player.pos -= 1
+ testtarget -= 1
+ end
+
+ end
+ end
+
+
+ #Wrap player around the stage
+ if args.state.player.x > 1280
+ args.state.player.x = -64
+ args.state.player.started_running_at ||= args.state.tick_count
+ elsif args.state.player.x < -64
+ args.state.player.x = 1280
+ args.state.player.started_running_at ||= args.state.tick_count
+ end
+
+ if args.state.player.y > 720
+ args.state.player.y = -64
+ args.state.player.started_running_at ||= args.state.tick_count
+ elsif args.state.player.y < -64
+ args.state.player.y = 720
+ args.state.player.started_running_at ||= args.state.tick_count
+ end
+
+ # Display obstacles
+ args.outputs.sprites << display_obs1(args)
+
+ #Display the flying dragon and bots
+ args.outputs.sprites << display_dragon(args)
+ if args.state.bot1.hp >= 1
+ args.outputs.sprites << display_bot1(args)
+ end
+ if args.state.bot2.hp >= 1
+ args.outputs.sprites << display_bot2(args)
+ end
+ if args.state.bot3.hp >= 1
+ args.outputs.sprites << display_bot3(args)
+ end
+ if args.state.bot1.hp <= 0 and args.state.bot3.hp <= 0 and args.state.bot2.hp <= 0
+ args.gtk.notify! "You Win!"
+ end
+end
+
+# helper method to create a button
+def new_button id, x, y, text
+ # create a hash ("entity") that has some metadata
+ # about what it represents
+ entity =
+ {
+ id: id,
+ rect: { x: x, y: y, w: 200, h: 50 }
+ }
+
+ # for that entity, define the primitives
+ # that form it
+ entity[:primitives] =
+ [
+ { x: x, y: y, w: 200, h: 50 }.border,
+ { x: x + 75, y: y + 30, text: text }.label
+ ]
+
+ entity
+end
+
+# helper method for determining if a button was clicked
+def button_clicked? args, button
+ return false unless args.inputs.mouse.click
+ return args.inputs.mouse.point.inside_rect? button[:rect]
+end
+
+def display_dragon args
+ start_looping_at = 0
+ number_of_sprites = 2
+ number_of_frames_to_show_each_sprite = 8
+ does_sprite_loop = true
+ sprite_index = start_looping_at.frame_index number_of_sprites,
+ number_of_frames_to_show_each_sprite,
+ does_sprite_loop
+ {
+ pos: args.state.player.pos,
+ x: args.state.player.x,
+ y: args.state.player.y,
+ w: args.state.player.w,
+ h: args.state.player.h,
+ path: "sprites/roy-#{sprite_index}.png",
+ flip_horizontally: args.state.player.direction < 0
+ }
+end
+
+def display_bot1 args
+ start_looping_at = 0
+ number_of_sprites = 6
+ number_of_frames_to_show_each_sprite = 4
+ does_sprite_loop = true
+ sprite_index = start_looping_at.frame_index number_of_sprites,
+ number_of_frames_to_show_each_sprite,
+ does_sprite_loop
+ {
+ x: args.state.bot1.x,
+ y: args.state.bot1.y,
+ w: args.state.bot1.w,
+ h: args.state.bot1.h,
+ path: "sprites/dragon-#{sprite_index}.png",
+ flip_horizontally: args.state.bot1.direction < 0
+ }
+end
+
+def display_bot2 args
+ start_looping_at = 0
+ number_of_sprites = 6
+ number_of_frames_to_show_each_sprite = 4
+ does_sprite_loop = true
+ sprite_index = start_looping_at.frame_index number_of_sprites,
+ number_of_frames_to_show_each_sprite,
+ does_sprite_loop
+ {
+ x: args.state.bot2.x,
+ y: args.state.bot2.y,
+ w: args.state.bot2.w,
+ h: args.state.bot2.h,
+ path: "sprites/dragon-#{sprite_index}.png",
+ flip_horizontally: args.state.bot2.direction < 0
+ }
+end
+
+def display_bot3 args
+ start_looping_at = 0
+ number_of_sprites = 6
+ number_of_frames_to_show_each_sprite = 4
+ does_sprite_loop = true
+ sprite_index = start_looping_at.frame_index number_of_sprites,
+ number_of_frames_to_show_each_sprite,
+ does_sprite_loop
+ {
+ x: args.state.bot3.x,
+ y: args.state.bot3.y,
+ w: args.state.bot3.w,
+ h: args.state.bot3.h,
+ path: "sprites/dragon-#{sprite_index}.png",
+ flip_horizontally: args.state.bot3.direction < 0
+ }
+end
+
+def display_obs1 args
+ start_looping_at = 0
+ number_of_sprites = 1
+ number_of_frames_to_show_each_sprite = 8
+ does_sprite_loop = true
+ sprite_index = start_looping_at.frame_index number_of_sprites,
+ number_of_frames_to_show_each_sprite,
+ does_sprite_loop
+ {
+ x: args.state.obs1.x,
+ y: args.state.obs1.y,
+ w: args.state.obs1.w,
+ h: args.state.obs1.h,
+ path: "sprites/water-1.png",
+ flip_horizontally: args.state.obs1.direction < 0
+ }
+end
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/border-black.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/border-black.png
new file mode 100644
index 0000000..c9d0bad
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/border-black.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-black.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-black.png
new file mode 100644
index 0000000..c98e23d
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-black.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-blue.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-blue.png
new file mode 100644
index 0000000..1726d2a
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-blue.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-gray.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-gray.png
new file mode 100644
index 0000000..960f191
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-gray.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-green.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-green.png
new file mode 100644
index 0000000..43cf7ee
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-green.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-indigo.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-indigo.png
new file mode 100644
index 0000000..598e240
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-indigo.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-orange.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-orange.png
new file mode 100644
index 0000000..5604a42
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-orange.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-red.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-red.png
new file mode 100644
index 0000000..7f17ca6
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-red.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-violet.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-violet.png
new file mode 100644
index 0000000..681d210
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-violet.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-white.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-white.png
new file mode 100644
index 0000000..bd32155
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-white.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-yellow.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-yellow.png
new file mode 100644
index 0000000..94992eb
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/circle-yellow.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-0.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-0.png
new file mode 100644
index 0000000..fb179af
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-0.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-1.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-1.png
new file mode 100644
index 0000000..8cfe531
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-1.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-2.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-2.png
new file mode 100644
index 0000000..cb462e1
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-2.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-3.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-3.png
new file mode 100644
index 0000000..04c4977
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-3.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-4.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-4.png
new file mode 100644
index 0000000..b29fa3d
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-4.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-5.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-5.png
new file mode 100644
index 0000000..99f4e74
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/dragon-5.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-0.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-0.png
new file mode 100644
index 0000000..f48636f
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-0.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-1.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-1.png
new file mode 100644
index 0000000..b4018d9
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-1.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-2.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-2.png
new file mode 100644
index 0000000..3abaedd
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-2.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-3.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-3.png
new file mode 100644
index 0000000..fe94a5a
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-3.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-4.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-4.png
new file mode 100644
index 0000000..ed04237
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-4.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-5.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-5.png
new file mode 100644
index 0000000..2cd8f06
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-5.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-6.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-6.png
new file mode 100644
index 0000000..e55909c
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-6.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-sheet.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-sheet.png
new file mode 100644
index 0000000..8559a5c
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/explosion-sheet.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-black.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-black.png
new file mode 100644
index 0000000..f50c872
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-black.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-blue.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-blue.png
new file mode 100644
index 0000000..1696bae
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-blue.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-gray.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-gray.png
new file mode 100644
index 0000000..e8c4c5a
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-gray.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-green.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-green.png
new file mode 100644
index 0000000..a700602
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-green.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-indigo.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-indigo.png
new file mode 100644
index 0000000..15f6f4f
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-indigo.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-orange.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-orange.png
new file mode 100644
index 0000000..1587173
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-orange.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-red.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-red.png
new file mode 100644
index 0000000..d442f39
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-red.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-violet.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-violet.png
new file mode 100644
index 0000000..3be5731
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-violet.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-white.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-white.png
new file mode 100644
index 0000000..c1ad970
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-white.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-yellow.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-yellow.png
new file mode 100644
index 0000000..63f5f34
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/hexagon-yellow.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-black.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-black.png
new file mode 100644
index 0000000..fa9e463
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-black.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-blue.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-blue.png
new file mode 100644
index 0000000..a3d8524
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-blue.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-gray.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-gray.png
new file mode 100644
index 0000000..85dcc1d
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-gray.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-green.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-green.png
new file mode 100644
index 0000000..ec2773e
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-green.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-indigo.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-indigo.png
new file mode 100644
index 0000000..e6be50c
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-indigo.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-orange.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-orange.png
new file mode 100644
index 0000000..154d81c
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-orange.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-red.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-red.png
new file mode 100644
index 0000000..3448c4d
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-red.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-violet.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-violet.png
new file mode 100644
index 0000000..f09bf21
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-violet.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-white.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-white.png
new file mode 100644
index 0000000..a45793d
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-white.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-yellow.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-yellow.png
new file mode 100644
index 0000000..9be20c7
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/isometric-yellow.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/roy-0.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/roy-0.png
new file mode 100644
index 0000000..dd122e1
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/roy-0.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/roy-1.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/roy-1.png
new file mode 100644
index 0000000..d3ac548
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/roy-1.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-black.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-black.png
new file mode 100644
index 0000000..cea7bd7
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-black.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-blue.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-blue.png
new file mode 100644
index 0000000..b840849
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-blue.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-gray.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-gray.png
new file mode 100644
index 0000000..2142b30
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-gray.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-green.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-green.png
new file mode 100644
index 0000000..5ef7f75
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-green.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-indigo.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-indigo.png
new file mode 100644
index 0000000..2384108
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-indigo.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-orange.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-orange.png
new file mode 100644
index 0000000..bb1eee7
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-orange.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-red.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-red.png
new file mode 100644
index 0000000..3ed5f13
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-red.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-violet.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-violet.png
new file mode 100644
index 0000000..333540c
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-violet.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-white.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-white.png
new file mode 100644
index 0000000..378c565
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-white.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-yellow.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-yellow.png
new file mode 100644
index 0000000..0edeeec
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/square-yellow.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/star.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/star.png
new file mode 100644
index 0000000..e0ee0f9
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/star.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/water-1.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/water-1.png
new file mode 100644
index 0000000..527385a
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/water-1.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/gameboard_movement/sprites/water-2.png b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/water-2.png
new file mode 100644
index 0000000..7f73b1b
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/gameboard_movement/sprites/water-2.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/app/main.rb b/samples/99_genre_rpg_tactical/taking_turns/app/main.rb
new file mode 100644
index 0000000..aac6de7
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/app/main.rb
@@ -0,0 +1,189 @@
+def tick args
+ args.state.base_columns ||= 10.times.map { |n| 50 * n + 1280 / 2 - 5 * 50 + 5 }
+ args.state.base_rows ||= 5.times.map { |n| 50 * n + 720 - 5 * 50 }
+ args.state.offset_columns = 10.times.map { |n| (n - 4.5) * Math.sin(Kernel.tick_count.to_radians) * 12 }
+ args.state.offset_rows = 5.map { 0 }
+ args.state.columns = 10.times.map { |i| args.state.base_columns[i] + args.state.offset_columns[i] }
+ args.state.rows = 5.times.map { |i| args.state.base_rows[i] + args.state.offset_rows[i] }
+ args.state.explosions ||= []
+ args.state.enemies ||= []
+ args.state.score ||= 0
+ args.state.wave ||= 0
+ if args.state.enemies.empty?
+ args.state.wave += 1
+ args.state.wave_root = Math.sqrt(args.state.wave)
+ args.state.enemies = make_enemies
+ end
+ args.state.player ||= {x: 620, y: 80, w: 40, h: 40, path: 'sprites/circle-gray.png', angle: 90, cooldown: 0, alive: true}
+ args.state.enemy_bullets ||= []
+ args.state.player_bullets ||= []
+ args.state.lives ||= 3
+ args.state.missed_shots ||= 0
+ args.state.fired_shots ||= 0
+
+ update_explosions args
+ update_enemy_positions args
+
+ if args.inputs.left && args.state.player[:x] > (300 + 5)
+ args.state.player[:x] -= 5
+ end
+ if args.inputs.right && args.state.player[:x] < (1280 - args.state.player[:w] - 300 - 5)
+ args.state.player[:x] += 5
+ end
+
+ args.state.enemy_bullets.each do |bullet|
+ bullet[:x] += bullet[:dx]
+ bullet[:y] += bullet[:dy]
+ end
+ args.state.player_bullets.each do |bullet|
+ bullet[:x] += bullet[:dx]
+ bullet[:y] += bullet[:dy]
+ end
+
+ args.state.enemy_bullets = args.state.enemy_bullets.find_all { |bullet| bullet[:y].between?(-16, 736) }
+ args.state.player_bullets = args.state.player_bullets.find_all do |bullet|
+ if bullet[:y].between?(-16, 736)
+ true
+ else
+ args.state.missed_shots += 1
+ false
+ end
+ end
+
+ args.state.enemies = args.state.enemies.reject do |enemy|
+ if args.state.player[:alive] && 1500 > (args.state.player[:x] - enemy[:x]) ** 2 + (args.state.player[:y] - enemy[:y]) ** 2
+ args.state.explosions << {x: enemy[:x] + 4, y: enemy[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0}
+ args.state.explosions << {x: args.state.player[:x] + 4, y: args.state.player[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0}
+ args.state.player[:alive] = false
+ true
+ else
+ false
+ end
+ end
+ args.state.enemy_bullets.each do |bullet|
+ if args.state.player[:alive] && 400 > (args.state.player[:x] - bullet[:x] + 12) ** 2 + (args.state.player[:y] - bullet[:y] + 12) ** 2
+ args.state.explosions << {x: args.state.player[:x] + 4, y: args.state.player[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0}
+ args.state.player[:alive] = false
+ bullet[:despawn] = true
+ end
+ end
+ args.state.enemies = args.state.enemies.reject do |enemy|
+ args.state.player_bullets.any? do |bullet|
+ if 400 > (enemy[:x] - bullet[:x] + 12) ** 2 + (enemy[:y] - bullet[:y] + 12) ** 2
+ args.state.explosions << {x: enemy[:x] + 4, y: enemy[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0}
+ bullet[:despawn] = true
+ args.state.score += 1000 * args.state.wave
+ true
+ else
+ false
+ end
+ end
+ end
+
+ args.state.player_bullets = args.state.player_bullets.reject { |bullet| bullet[:despawn] }
+ args.state.enemy_bullets = args.state.enemy_bullets.reject { |bullet| bullet[:despawn] }
+
+ args.state.player[:cooldown] -= 1
+ if args.inputs.keyboard.key_held.space && args.state.player[:cooldown] <= 0 && args.state.player[:alive]
+ args.state.player_bullets << {x: args.state.player[:x] + 12, y: args.state.player[:y] + 28, w: 16, h: 16, path: 'sprites/star.png', dx: 0, dy: 8}.sprite
+ args.state.fired_shots += 1
+ args.state.player[:cooldown] = 10 + 20 / args.state.wave
+ end
+ args.state.enemies.each do |enemy|
+ if Math.rand < 0.0005 + 0.0005 * args.state.wave && args.state.player[:alive] && enemy[:move_state] == :normal
+ args.state.enemy_bullets << {x: enemy[:x] + 12, y: enemy[:y] - 8, w: 16, h: 16, path: 'sprites/star.png', dx: 0, dy: -3 - args.state.wave_root}.sprite
+ end
+ end
+
+ args.outputs.background_color = [0, 0, 0]
+ args.outputs.primitives << args.state.enemies.map do |enemy|
+ [enemy[:x], enemy[:y], 40, 40, enemy[:path], -90].sprite
+ end
+ args.outputs.primitives << args.state.player if args.state.player[:alive]
+ args.outputs.primitives << args.state.explosions
+ args.outputs.primitives << args.state.player_bullets
+ args.outputs.primitives << args.state.enemy_bullets
+ accuracy = args.state.fired_shots.zero? ? 1 : (args.state.fired_shots - args.state.missed_shots) / args.state.fired_shots
+ args.outputs.primitives << [
+ [0, 0, 300, 720, 96, 0, 0].solid,
+ [1280 - 300, 0, 300, 720, 96, 0, 0].solid,
+ [1280 - 290, 60, "Wave #{args.state.wave}", 255, 255, 255].label,
+ [1280 - 290, 40, "Accuracy #{(accuracy * 100).floor}%", 255, 255, 255].label,
+ [1280 - 290, 20, "Score #{(args.state.score * accuracy).floor}", 255, 255, 255].label,
+ ]
+ args.outputs.primitives << args.state.lives.times.map do |n|
+ [1280 - 290 + 50 * n, 80, 40, 40, 'sprites/circle-gray.png', 90].sprite
+ end
+ #args.outputs.debug << args.gtk.framerate_diagnostics_primitives
+
+ if (!args.state.player[:alive]) && args.state.enemy_bullets.empty? && args.state.explosions.empty? && args.state.enemies.all? { |enemy| enemy[:move_state] == :normal }
+ args.state.player[:alive] = true
+ args.state.player[:x] = 624
+ args.state.player[:y] = 80
+ args.state.lives -= 1
+ if args.state.lives == -1
+ args.state.clear!
+ end
+ end
+end
+
+def make_enemies
+ enemies = []
+ enemies += 10.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 0, col: n, path: 'sprites/circle-orange.png', move_state: :retreat} }
+ enemies += 10.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 1, col: n, path: 'sprites/circle-orange.png', move_state: :retreat} }
+ enemies += 8.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 2, col: n + 1, path: 'sprites/circle-blue.png', move_state: :retreat} }
+ enemies += 8.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 3, col: n + 1, path: 'sprites/circle-blue.png', move_state: :retreat} }
+ enemies += 4.times.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 4, col: n + 3, path: 'sprites/circle-green.png', move_state: :retreat} }
+ enemies
+end
+
+def update_explosions args
+ args.state.explosions.each do |explosion|
+ explosion[:age] += 0.5
+ explosion[:path] = "sprites/explosion-#{explosion[:age].floor}.png"
+ end
+ args.state.explosions = args.state.explosions.reject { |explosion| explosion[:age] >= 7 }
+end
+
+def update_enemy_positions args
+ args.state.enemies.each do |enemy|
+ if enemy[:move_state] == :normal
+ enemy[:x] = args.state.columns[enemy[:col]]
+ enemy[:y] = args.state.rows[enemy[:row]]
+ enemy[:move_state] = :dive if Math.rand < 0.0002 + 0.00005 * args.state.wave && args.state.player[:alive]
+ elsif enemy[:move_state] == :dive
+ enemy[:target_x] ||= args.state.player[:x]
+ enemy[:target_y] ||= args.state.player[:y]
+ dx = enemy[:target_x] - enemy[:x]
+ dy = enemy[:target_y] - enemy[:y]
+ vel = Math.sqrt(dx * dx + dy * dy)
+ speed_limit = 2 + args.state.wave_root
+ if vel > speed_limit
+ dx /= vel / speed_limit
+ dy /= vel / speed_limit
+ end
+ if vel < 1 || !args.state.player[:alive]
+ enemy[:move_state] = :retreat
+ end
+ enemy[:x] += dx
+ enemy[:y] += dy
+ elsif enemy[:move_state] == :retreat
+ enemy[:target_x] = args.state.columns[enemy[:col]]
+ enemy[:target_y] = args.state.rows[enemy[:row]]
+ dx = enemy[:target_x] - enemy[:x]
+ dy = enemy[:target_y] - enemy[:y]
+ vel = Math.sqrt(dx * dx + dy * dy)
+ speed_limit = 2 + args.state.wave_root
+ if vel > speed_limit
+ dx /= vel / speed_limit
+ dy /= vel / speed_limit
+ elsif vel < 1
+ enemy[:move_state] = :normal
+ enemy[:target_x] = nil
+ enemy[:target_y] = nil
+ end
+ enemy[:x] += dx
+ enemy[:y] += dy
+ end
+ end
+end
diff --git a/samples/99_genre_rpg_tactical/taking_turns/run.bat b/samples/99_genre_rpg_tactical/taking_turns/run.bat
new file mode 100644
index 0000000..36335e3
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/run.bat
@@ -0,0 +1,6 @@
+cd /d %~dp0
+
+cd ..
+cd ..
+cd ..
+dragonruby samples/99_genre_arcade/dragalaga
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-blue.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-blue.png
new file mode 100644
index 0000000..1726d2a
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-blue.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-gray.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-gray.png
new file mode 100644
index 0000000..960f191
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-gray.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-green.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-green.png
new file mode 100644
index 0000000..43cf7ee
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-green.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-orange.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-orange.png
new file mode 100644
index 0000000..5604a42
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/circle-orange.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-0.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-0.png
new file mode 100644
index 0000000..f48636f
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-0.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-1.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-1.png
new file mode 100644
index 0000000..b4018d9
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-1.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-2.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-2.png
new file mode 100644
index 0000000..3abaedd
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-2.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-3.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-3.png
new file mode 100644
index 0000000..fe94a5a
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-3.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-4.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-4.png
new file mode 100644
index 0000000..ed04237
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-4.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-5.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-5.png
new file mode 100644
index 0000000..2cd8f06
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-5.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-6.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-6.png
new file mode 100644
index 0000000..e55909c
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/explosion-6.png
Binary files differ
diff --git a/samples/99_genre_rpg_tactical/taking_turns/sprites/star.png b/samples/99_genre_rpg_tactical/taking_turns/sprites/star.png
new file mode 100644
index 0000000..e0ee0f9
--- /dev/null
+++ b/samples/99_genre_rpg_tactical/taking_turns/sprites/star.png
Binary files differ