• Docs
  • Samples
  • Table Of Contents

    Samples link

    Follows is a source code listing for all files that have been open sourced. This code can be found in the ./samples directory.

    Rendering Basics link

    Labels - main.rb link

    # ./samples/01_rendering_basics/01_labels/app/main.rb
    =begin
    
    APIs listing that haven't been encountered in a previous sample apps:
    
    - args.outputs.labels: An array. Values in this array generate labels the screen.
    
    =end
    
    # Labels are used to represent text elements in DragonRuby
    
    # An example of creating a label is:
    # args.outputs.labels << [320, 640, "Example", 3, 1, 255, 0, 0, 200, manaspace.ttf]
    
    # The code above does the following:
    # 1. GET the place where labels go: args.outputs.labels
    # 2. Request a new LABEL be ADDED: <<
    # 3. The DEFINITION of a LABEL is the ARRAY:
    #     [320, 640, "Example
    #     [ X ,  Y,    TEXT]
    # 4. It's recommended to use hashes so that you're not reliant on positional values:
    #    { x: 320,
    #      y: 640,
    #      text: "Text",
    #      font: "fonts/font.ttf",
    #      anchor_x: 0.5, # or alignment_enum: 0, 1, or 2
    #      anchor_y: 0.5, # or vertical_alignment_enum: 0, 1, or 2
    #      r: 0,
    #      g: 0,
    #      b: 0,
    #      a: 255,
    #      size_px: 20,   # or size_enum: -10 to 10 (0 means "ledgible on small devices" ie: 20px)
    #      blendmode_enum: 1 }
    
    
    # The tick method is called by DragonRuby every frame
    # args contains all the information regarding the game.
    def tick args
      # render the current frame to the screen using a simple array
      # this is useful for quick and dirty output and is recommended to use
      # a Hash to render long term.
      args.outputs.labels << [640, 650, "frame: #{Kernel.tick_count}"]
    
      # render the current frame to the screen centered vertically and horizontally at 640, 620
      args.outputs.labels << { x: 640, y: 620, anchor_x: 0.5, anchor_y: 0.5, text: "frame: #{Kernel.tick_count}" }
    
      # Here are some examples of simple labels, with the minimum number of parameters
      # Note that the default values for the other parameters are 0, except for Alpha which is 255 and Font Style which is the default font
      args.outputs.labels << { x: 5,          y: 720 - 5, text: "This is a label located at the top left." }
      args.outputs.labels << { x: 5,          y:      30, text: "This is a label located at the bottom left." }
      args.outputs.labels << { x: 1280 - 420, y: 720 - 5, text: "This is a label located at the top right." }
      args.outputs.labels << { x: 1280 - 440, y: 30,      text: "This is a label located at the bottom right." }
    
      # Demonstration of the Size Enum Parameter
    
      # size_enum of -2 is equivalent to using size_px: 18
      args.outputs.labels << { x: 175 + 150, y: 635 - 50, text: "Smaller label.",  size_enum: -2 }
      args.outputs.labels << { x: 175 + 150, y: 620 - 50, text: "Smaller label.",  size_px: 18 }
    
      # size_enum of -1 is equivalent to using size_px: 20
      args.outputs.labels << { x: 175 + 150, y: 595 - 50, text: "Small label.",    size_enum: -1 }
      args.outputs.labels << { x: 175 + 150, y: 580 - 50, text: "Small label.",    size_px: 20 }
    
      # size_enum of  0 is equivalent to using size_px: 22
      args.outputs.labels << { x: 175 + 150, y: 550 - 50, text: "Medium label.",   size_enum:  0 }
    
      # size_enum of  0 is equivalent to using size_px: 24
      args.outputs.labels << { x: 175 + 150, y: 520 - 50, text: "Large label.",    size_enum:  1 }
    
      # size_enum of  0 is equivalent to using size_px: 26
      args.outputs.labels << { x: 175 + 150, y: 490 - 50, text: "Larger label.",   size_enum:  2 }
    
      # Demonstration of the Align Parameter
      args.outputs.lines  << { x: 175 + 150, y: 0, h: 720 }
    
      # alignment_enum: 0 is equivalent to anchor_x: 0
      # vertical_alignment_enum: 1 is equivalent to anchor_y: 0.5
      args.outputs.labels << { x: 175 + 150, y: 360 - 50, text: "Left aligned.",   alignment_enum: 0, vertical_alignment_enum: 1 }
      args.outputs.labels << { x: 175 + 150, y: 342 - 50, text: "Left aligned.",   anchor_x: 0, anchor_y: 0.5 }
    
      # alignment_enum: 1 is equivalent to anchor_x: 0.5
      args.outputs.labels << { x: 175 + 150, y: 325 - 50, text: "Center aligned.", alignment_enum: 1, vertical_alignment_enum: 1  }
    
      # alignment_enum: 2 is equivalent to anchor_x: 1
      args.outputs.labels << { x: 175 + 150, y: 305 - 50, text: "Right aligned.",  alignment_enum: 2 }
    
      # Demonstration of the RGBA parameters
      args.outputs.labels << { x: 600  + 150, y: 590 - 50, text: "Red Label.",   r: 255, g:   0, b:   0 }
      args.outputs.labels << { x: 600  + 150, y: 570 - 50, text: "Green Label.", r:   0, g: 255, b:   0 }
      args.outputs.labels << { x: 600  + 150, y: 550 - 50, text: "Blue Label.",  r:   0, g:   0, b: 255 }
      args.outputs.labels << { x: 600  + 150, y: 530 - 50, text: "Faded Label.", r:   0, g:   0, b:   0, a: 128 }
    
      # providing a custom font
      args.outputs.labels << { x: 690 + 150,
                               y: 330 - 50,
                               text: "Custom font (Hash)",
                               size_enum: 0,                 # equivalent to size_px:  22
                               alignment_enum: 1,            # equivalent to anchor_x: 0.5
                               vertical_alignment_enum: 2,   # equivalent to anchor_y: 1
                               r: 125,
                               g: 0,
                               b: 200,
                               a: 255,
                               font: "manaspc.ttf" }
    
      # Primitives can hold anything, and can be given a label in the following forms
      args.outputs.primitives << { x: 690 + 150,
                                   y: 330 - 80,
                                   text: "Custom font (.primitives Hash)",
                                   size_enum: 0,
                                   alignment_enum: 1,
                                   r: 125,
                                   g: 0,
                                   b: 200,
                                   a: 255,
                                   font: "manaspc.ttf" }
    
      args.outputs.labels << { x: 640,
                               y: 100,
                               anchor_x: 0.5,
                               anchor_y: 0.5,
                               text: "Ніколи не здам тебе. Ніколи не підведу тебе. Ніколи не буду бігати навколо і залишати тебе." }
    end
    
    

    Labels Text Wrapping - main.rb link

    # ./samples/01_rendering_basics/01_labels_text_wrapping/app/main.rb
    def tick args
      # create a really long string
      really_long_string =  "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In vulputate viverra metus et vehicula. Aenean quis accumsan dolor. Nulla tempus, ex et lacinia elementum, nisi felis ullamcorper sapien, sed sagittis sem justo eu lectus. Etiam ut vehicula lorem, nec placerat ligula. Duis varius ultrices magna non sagittis. Aliquam et sem vel risus viverra hendrerit. Maecenas dapibus congue lorem, a blandit mauris feugiat sit amet."
      really_long_string += "\n\n"
      really_long_string += "Sed quis metus lacinia mi dapibus fermentum nec id nunc. Donec tincidunt ante a sem bibendum, eget ultricies ex mollis. Quisque venenatis erat quis pretium bibendum. Pellentesque vel laoreet nibh. Cras gravida nisi nec elit pulvinar, in feugiat leo blandit. Quisque sodales quam sed congue consequat. Vivamus placerat risus vitae ex feugiat viverra. In lectus arcu, pellentesque vel ipsum ac, dictum finibus enim. Quisque consequat leo in urna dignissim, eu tristique ipsum accumsan. In eros sem, iaculis ac rhoncus eu, laoreet vitae ipsum. In sodales, ante eu tempus vehicula, mi nulla luctus turpis, eu egestas leo sapien et mi."
    
      # length of characters on line
      max_character_length = 80
    
      # API: String.wrapped_lines(string, max_character_length)
      long_strings_split = String.wrapped_lines really_long_string,
                                                max_character_length
    
      # render a label for each line and offset by the index value
      # setting the anchor_y for a label will offset the text by its
      # height
      args.outputs.labels << long_strings_split.map_with_index do |s, i|
        {
          x: 60,
          y: 720 - 60,
          anchor_y: i,
          text: s
        }
      end
    end
    
    

    Lines - main.rb link

    # ./samples/01_rendering_basics/02_lines/app/main.rb
    =begin
    APIs listing that haven't been encountered in a previous sample apps:
    
    - args.outputs.lines: Provided an Array or a Hash, lines will be rendered to the screen.
    - Kernel.tick_count: This property contains an integer value that
      represents the current frame. DragonRuby renders at 60 FPS. A value of 0
      for Kernel.tick_count represents the initial load of the game.
    =end
    
    # The parameters required for lines are:
    # 1. The initial point (x, y)
    # 2. The end point (x2, y2)
    # 3. The rgba values for the color and transparency (r, g, b, a)
    #    Creating a line using an Array (quick and dirty):
    #    [x, y, x2, y2, r, g, b, a]
    #    args.outputs.lines << [100, 100, 300, 300, 255, 0, 255, 255]
    #    This would create a line from (100, 100) to (300, 300)
    #    The RGB code (255, 0, 255) would determine its color, a purple
    #    It would have an Alpha value of 255, making it completely opaque
    # 4. Using Hashes, the keys are :x, :y, :x2, :y2, :r, :g, :b, and :a
    def tick args
      args.outputs.labels << { x: 640,
                               y: 700,
                               text: "Sample app shows how to create lines.",
                               size_px: 22,
                               anchor_x: 0.5,
                               anchor_y: 0.5 }
    
      # Render lines using Arrays/Tuples
      # This is quick and dirty and it's recommended to use Hashes long term
      args.outputs.lines  << [380, 450, 675, 450]
      args.outputs.lines  << [380, 410, 875, 410]
    
      # These examples utilize Kernel.tick_count to change the length of the lines over time
      # Kernel.tick_count is the ticks that have occurred in the game
      # This is accomplished by making either the starting or ending point based on the Kernel.tick_count
      args.outputs.lines  << { x:  380,
                               y:  370,
                               x2: 875,
                               y2: 370,
                               r:  Kernel.tick_count % 255,
                               g:  0,
                               b:  0,
                               a:  255 }
    
      args.outputs.lines  << { x:  380,
                               y:  330 - Kernel.tick_count % 25,
                               x2: 875,
                               y2: 330,
                               r:  0,
                               g:  0,
                               b:  0,
                               a:  255 }
    
      args.outputs.lines  << { x:  380 + Kernel.tick_count % 400,
                               y:  290,
                               x2: 875,
                               y2: 290,
                               r:  0,
                               g:  0,
                               b:  0,
                               a:  255 }
    end
    
    

    Solids Borders - main.rb link

    # ./samples/01_rendering_basics/03_solids_borders/app/main.rb
    =begin
    APIs listing that haven't been encountered in a previous sample apps:
    
    - args.outputs.solids: Provided an Array or a Hash, solid squares will be
      rendered to the screen.
    - args.outputs.borders: Provided an Array or a Hash, borders
      will be rendered to the screen.
    - args.outputs.primitives: Provided an Hash with a :primitive_marker key,
      either a solid square or border will be rendered to the screen.
    =end
    
    # The parameters required for rects are:
    # 1. The bottom left corner (x, y)
    # 2. The width (w)
    # 3. The height (h)
    # 4. The rgba values for the color and transparency (r, g, b, a)
    # [100, 100, 400, 500, 0, 255, 0, 180]
    # Whether the rect would be filled or not depends on if
    # it is added to args.outputs.solids or args.outputs.borders
    # (or its :primitive_marker if Hash is sent to args.outputs.primitives)
    def tick args
      args.outputs.labels << { x: 640,
                               y: 700,
                               text: "Sample app shows how to create solid squares and borders.",
                               size_px: 22,
                               anchor_x: 0.5,
                               anchor_y: 0.5 }
    
      # Render solids/borders using Arrays/Tuples
      # Using arrays is quick and dirty and it's recommended to use Hashes long term
      args.outputs.solids << [470, 520, 50, 50]
      args.outputs.solids << [530, 520, 50, 50, 0, 0, 0]
      args.outputs.solids << [590, 520, 50, 50, 255, 0, 0]
      args.outputs.solids << [650, 520, 50, 50, 255, 0, 0, 128]
    
      # using Hashes
      args.outputs.solids << { x: 710,
                               y: 520,
                               w: 50,
                               h: 50,
                               r: 0,
                               g: 80,
                               b: 40,
                               a: Kernel.tick_count % 255 }
    
      # primitives outputs requires a primitive_marker to differentiate
      # between a solid or a border
      args.outputs.primitives << { x: 770,
                                   y: 520,
                                   w: 50,
                                   h: 50,
                                   r: 0,
                                   g: 80,
                                   b: 40,
                                   a: Kernel.tick_count % 255,
                                   primitive_marker: :solid }
    
      # using :solid sprite
      args.outputs.sprites << { x: 710,
                                y: 460,
                                w: 50,
                                h: 50,
                                path: :solid,
                                r: 0,
                                g: 80,
                                b: 40,
                                a: Kernel.tick_count % 255 }
    
      # using :solid sprite does not require a primitive marker
      args.outputs.primitives << { x: 770,
                                   y: 460,
                                   w: 50,
                                   h: 50,
                                   path: :solid,
                                   r: 0,
                                   g: 80,
                                   b: 40,
                                   a: Kernel.tick_count % 255 }
    
    
      # you can also render a border
      # Using arrays is quick and dirty and it's recommended to use Hashes long term
      args.outputs.borders << [470, 320, 50, 50]
      args.outputs.borders << [530, 320, 50, 50, 0, 0, 0]
      args.outputs.borders << [590, 320, 50, 50, 255, 0, 0]
      args.outputs.borders << [650, 320, 50, 50, 255, 0, 0, 128]
    
      args.outputs.borders << { x: 710,
                                y: 320,
                                w: 50,
                                h: 50,
                                r: 0,
                                g: 80,
                                b: 40,
                                a: Kernel.tick_count % 255 }
    
      # primitives outputs requires a primitive_marker to differentiate
      # between a solid or a border
      args.outputs.borders << { x: 770,
                                y: 320,
                                w: 50,
                                h: 50,
                                r: 0,
                                g: 80,
                                b: 40,
                                a: Kernel.tick_count % 255,
                                primitive_marker: :border }
    end
    
    

    Sprites - main.rb link

    # ./samples/01_rendering_basics/04_sprites/app/main.rb
    =begin
    APIs listing that haven't been encountered in a previous sample apps:
    - args.outputs.sprites: Provided an Array or a Hash, a sprite will be
      rendered to the screen.
    
    Properties of a sprite:
    {
      # common properties
      x: 0,
      y: 0,
      w: 100,
      h: 100,
      path: "sprites/square/blue.png",
      angle: 90,
      a: 255,
    
      # anchoring (float value representing a percentage to offset w and h)
      anchor_x: 0,
      anchor_y: 0,
      angle_anchor_x: 0,
      angle_anchor_y: 0,
    
      # color saturation
      r: 255,
      g: 255,
      b: 255,
    
      # flip rendering
      flip_horizontally: false,
      flip_vertically: false
    
      # sprite sheet properties/clipped rect (using the top-left as the origin)
      tile_x: 0,
      tile_y: 0,
      tile_w: 20,
      tile_h: 20
    
      # sprite sheet properties/clipped rect (using the bottom-left as the origin)
      source_x: 0,
      source_y: 0,
      source_w: 20,
      source_h: 20,
    }
    =end
    def tick args
      args.outputs.labels << { x: 640,
                               y: 700,
                               text: "Sample app shows how to render a sprite.",
                               size_px: 22,
                               anchor_x: 0.5,
                               anchor_y: 0.5 }
    
      # ==================
      # ROW 1 Simple Rendering
      # ==================
      args.outputs.labels << { x: 460,
                               y: 600,
                               text: "Simple rendering." }
    
      # using quick and dirty Array (use Hashes for long term maintainability)
      args.outputs.sprites << [460, 470, 128, 101, 'dragonruby.png']
    
      # using Hashes
      args.outputs.sprites << { x: 610,
                                y: 470,
                                w: 128,
                                h: 101,
                                path: 'dragonruby.png',
                                a: Kernel.tick_count % 255 }
    
      args.outputs.sprites << { x: 760 + 64,
                                y: 470 + 50,
                                w: 128,
                                h: 101,
                                anchor_x: 0.5,
                                anchor_y: 0.5,
                                path: 'dragonruby.png',
                                flip_horizontally: true,
                                flip_vertically: true,
                                a: Kernel.tick_count % 255 }
    
      # ==================
      # ROW 2 Angle/Angle Anchors
      # ==================
      args.outputs.labels << { x: 460,
                               y: 400,
                               text: "Angle/Angle Anchors." }
      # rotation using angle (in degrees)
      args.outputs.sprites << { x: 460,
                                y: 270,
                                w: 128,
                                h: 101,
                                path: 'dragonruby.png',
                                angle: Kernel.tick_count % 360 }
    
      # rotation anchor using angle_anchor_x
      args.outputs.sprites << { x: 760,
                                y: 270,
                                w: 128,
                                h: 101,
                                path: 'dragonruby.png',
                                angle: Kernel.tick_count % 360,
                                angle_anchor_x: 0,
                                angle_anchor_y: 0 }
    
      # ==================
      # ROW 3 Sprite Cropping
      # ==================
      args.outputs.labels << { x: 460,
                               y: 200,
                               text: "Cropping (tile sheets)." }
    
      # tiling using top left as the origin
      args.outputs.sprites << { x: 460,
                                y: 90,
                                w: 80,
                                h: 80,
                                path: 'dragonruby.png',
                                tile_x: 0,
                                tile_y: 0,
                                tile_w: 80,
                                tile_h: 80 }
    
      # overlay to see how tile_* crops
      args.outputs.sprites << { x: 460,
                                y: 70,
                                w: 128,
                                h: 101,
                                path: 'dragonruby.png',
                                a: 80 }
    
      # tiling using bottom left as the origin
      args.outputs.sprites << { x: 610,
                                y: 70,
                                w: 80,
                                h: 80,
                                path: 'dragonruby.png',
                                source_x: 0,
                                source_y: 0,
                                source_w: 80,
                                source_h: 80 }
    
      # overlay to see how source_* crops
      args.outputs.sprites << { x: 610,
                                y: 70,
                                w: 128,
                                h: 101,
                                path: 'dragonruby.png',
                                a: 80 }
    end
    
    

    Sounds - main.rb link

    # ./samples/01_rendering_basics/05_sounds/app/main.rb
    =begin
    
     APIs Listing that haven't been encountered in previous sample apps:
    
     - sample: Chooses random element from array.
       In this sample app, the target note is set by taking a sample from the collection
       of available notes.
    
     Reminders:
    
     - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated
       as Ruby code, and the placeholder is replaced with its corresponding value or result.
    
     - args.outputs.labels: An array. The values generate a label.
       The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE]
       For more information about labels, go to mygame/documentation/02-labels.md.
    =end
    
    # This sample app allows users to test their musical skills by matching the piano sound that plays in each
    # level to the correct note.
    
    # Runs all the methods necessary for the game to function properly.
    def tick args
      args.outputs.labels << [640, 360, "Click anywhere to play a random sound.", 0, 1]
      args.state.notes ||= [:C3, :D3, :E3, :F3, :G3, :A3, :B3, :C4]
    
      if args.inputs.mouse.click
        # Play a sound by adding a string to args.outputs.sounds
        args.outputs.sounds << "sounds/#{args.state.notes.sample}.wav" # sound of target note is output
      end
    end
    
    

    Input Basics link

    Keyboard - main.rb link

    # ./samples/02_input_basics/01_keyboard/app/main.rb
    =begin
    
    APIs listing that haven't been encountered in a previous sample apps:
    
    - args.inputs.keyboard.key_up.KEY: The value of the properties will be set
      to the frame  that the key_up event occurred (the frame correlates
      to Kernel.tick_count). Otherwise the value will be nil. For a
      full listing of keys, take a look at mygame/documentation/06-keyboard.md.
    - args.state.PROPERTY: The state property on args is a dynamic
      structure. You can define ANY property here with ANY type of
      arbitrary nesting. Properties defined on args.state will be retained
      across frames. If you attempt access a property that doesn't exist
      on args.state, it will simply return nil (no exception will be thrown).
    
    =end
    
    # Along with outputs, inputs are also an essential part of video game development
    # DragonRuby can take input from keyboards, mouse, and controllers.
    # This sample app will cover keyboard input.
    
    # args.inputs.keyboard.key_up.a will check to see if the a key has been pressed
    # This will work with the other keys as well
    
    
    def tick args
      tick_instructions args, "Sample app shows how keyboard events are registered and accessed.", 360
      args.outputs.labels << { x: 460, y: row_to_px(args, 0), text: "Current game time: #{Kernel.tick_count}", size_enum: -1 }
      args.outputs.labels << { x: 460, y: row_to_px(args, 2), text: "Keyboard input: args.inputs.keyboard.key_up.h", size_enum: -1 }
      args.outputs.labels << { x: 460, y: row_to_px(args, 3), text: "Press \"h\" on the keyboard.", size_enum: -1 }
    
      # Input on a specifc key can be found through args.inputs.keyboard.key_up followed by the key
      if args.inputs.keyboard.key_up.h
        args.state.h_pressed_at = Kernel.tick_count
      end
    
      # This code simplifies to if args.state.h_pressed_at has not been initialized, set it to false
      args.state.h_pressed_at ||= false
    
      if args.state.h_pressed_at
        args.outputs.labels << { x: 460, y: row_to_px(args, 4), text: "\"h\" was pressed at time: #{args.state.h_pressed_at}", size_enum: -1 }
      else
        args.outputs.labels << { x: 460, y: row_to_px(args, 4), text: "\"h\" has never been pressed.", size_enum: -1 }
      end
    
      tick_help_text args
    end
    
    def row_to_px args, row_number, y_offset = 20
      # This takes a row_number and converts it to pixels DragonRuby understands.
      # Row 0 starts 5 units below the top of the grid
      # Each row afterward is 20 units lower
      args.grid.top - 5 - (y_offset * row_number)
    end
    
    # Don't worry about understanding the code within this method just yet.
    # This method shows you the help text within the game.
    def tick_help_text args
      return unless args.state.h_pressed_at
    
      args.state.key_value_history      ||= {}
      args.state.key_down_value_history ||= {}
      args.state.key_held_value_history ||= {}
      args.state.key_up_value_history   ||= {}
    
      if (args.inputs.keyboard.key_down.truthy_keys.length > 0 ||
          args.inputs.keyboard.key_held.truthy_keys.length > 0 ||
          args.inputs.keyboard.key_up.truthy_keys.length > 0)
        args.state.help_available = true
        args.state.no_activity_debounce = nil
      else
        args.state.no_activity_debounce ||= 5.seconds
        args.state.no_activity_debounce -= 1
        if args.state.no_activity_debounce <= 0
          args.state.help_available = false
          args.state.key_value_history        = {}
          args.state.key_down_value_history   = {}
          args.state.key_held_value_history   = {}
          args.state.key_up_value_history     = {}
        end
      end
    
      args.outputs.labels << { x: 10, y: row_to_px(args, 6), text: "This is the api for the keys you've pressed:", size_enum: -1, r: 180 }
    
      if !args.state.help_available
        args.outputs.labels << [10, row_to_px(args, 7),  "Press a key and I'll show code to access the key and what value will be returned if you used the code."]
        return
      end
    
      args.outputs.labels << { x: 10 , y: row_to_px(args, 7), text: "args.inputs.keyboard",          size_enum: -2 }
      args.outputs.labels << { x: 330, y: row_to_px(args, 7), text: "args.inputs.keyboard.key_down", size_enum: -2 }
      args.outputs.labels << { x: 650, y: row_to_px(args, 7), text: "args.inputs.keyboard.key_held", size_enum: -2 }
      args.outputs.labels << { x: 990, y: row_to_px(args, 7), text: "args.inputs.keyboard.key_up",   size_enum: -2 }
    
      fill_history args, :key_value_history,      :down_or_held, nil
      fill_history args, :key_down_value_history, :down,        :key_down
      fill_history args, :key_held_value_history, :held,        :key_held
      fill_history args, :key_up_value_history,   :up,          :key_up
    
      render_help_labels args, :key_value_history,      :down_or_held, nil,      10
      render_help_labels args, :key_down_value_history, :down,        :key_down, 330
      render_help_labels args, :key_held_value_history, :held,        :key_held, 650
      render_help_labels args, :key_up_value_history,   :up,          :key_up,   990
    end
    
    def fill_history args, history_key, state_key, keyboard_method
      fill_single_history args, history_key, state_key, keyboard_method, :raw_key
      fill_single_history args, history_key, state_key, keyboard_method, :char
      args.inputs.keyboard.keys[state_key].each do |key_name|
        fill_single_history args, history_key, state_key, keyboard_method, key_name
      end
    end
    
    def fill_single_history args, history_key, state_key, keyboard_method, key_name
      current_value = args.inputs.keyboard.send(key_name)
      if keyboard_method
        current_value = args.inputs.keyboard.send(keyboard_method).send(key_name)
      end
      args.state.as_hash[history_key][key_name] ||= []
      args.state.as_hash[history_key][key_name] << current_value
      args.state.as_hash[history_key][key_name] = args.state.as_hash[history_key][key_name].reverse.uniq.take(3).reverse
    end
    
    def render_help_labels args, history_key, state_key, keyboard_method, x
      idx = 8
      args.outputs.labels << args.state
                               .as_hash[history_key]
                               .keys
                               .reverse
                               .map
                               .with_index do |k, i|
        v = args.state.as_hash[history_key][k]
        current_value = args.inputs.keyboard.send(k)
        if keyboard_method
          current_value = args.inputs.keyboard.send(keyboard_method).send(k)
        end
        idx += 2
        [
          { x: x, y: row_to_px(args, idx + 0, 16), text: "    .#{k} is #{current_value || "nil"}", size_enum: -2 },
          { x: x, y: row_to_px(args, idx + 1, 16), text: "       was #{v}", size_enum: -2 }
        ]
      end
    end
    
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << { x: 0,   y: y - 50, w: 1280, h: 60 }.solid!
      args.outputs.debug << { x: 640, y: y,      text: text,
                              size_enum: 1, alignment_enum: 1, r: 255, g: 255, b: 255 }.label!
      args.outputs.debug << { x: 640, y: y - 25, text: "(click to dismiss instructions)",
                              size_enum: -2, alignment_enum: 1, r: 255, g: 255, b: 255 }.label!
    end
    
    

    Moving A Sprite - main.rb link

    # ./samples/02_input_basics/01_moving_a_sprite/app/main.rb
    def tick args
      # Create a player and set default values
      # NOTE: args.state is a construct that lets you define properties on the fly
      args.state.player ||= { x: 100,
                              y: 100,
                              w: 50,
                              h: 50,
                              path: 'sprites/square/green.png' }
    
      # move the player around by consulting args.inputs
      # the top level args.inputs checks the keyboard's arrow keys, WASD,
      # and controller one
      if args.inputs.up
        args.state.player.y += 10
      elsif args.inputs.down
        args.state.player.y -= 10
      end
    
      if args.inputs.left
        args.state.player.x -= 10
      elsif args.inputs.right
        args.state.player.x += 10
      end
    
      # Render the player to the screen
      args.outputs.sprites << args.state.player
    end
    
    

    Mouse - main.rb link

    # ./samples/02_input_basics/02_mouse/app/main.rb
    =begin
    
    APIs that haven't been encountered in a previous sample apps:
    
    - args.inputs.mouse.click: This property will be set if the mouse was clicked.
    - args.inputs.mouse.click.point.(x|y): The x and y location of the mouse.
    - args.inputs.mouse.click.point.created_at: The frame the mouse click occurred in.
    - args.inputs.mouse.click.point.created_at_elapsed: How many frames have passed
      since the click event.
    
    Reminder:
    
    - args.state.PROPERTY: The state property on args is a dynamic
      structure. You can define ANY property here with ANY type of
      arbitrary nesting. Properties defined on args.state will be retained
      across frames. If you attempt access a property that doesn't exist
      on args.state, it will simply return nil (no exception will be thrown).
    
    =end
    
    # This code demonstrates DragonRuby mouse input
    
    # To see if the a mouse click occurred
    # Use args.inputs.mouse.click
    # Which returns a boolean
    
    # To see where a mouse click occurred
    # Use args.inputs.mouse.click.point.x AND
    # args.inputs.mouse.click.point.y
    
    # To see which frame the click occurred
    # Use args.inputs.mouse.click.created_at
    
    # To see how many frames its been since the click occurred
    # Use args.inputs.mouse.click.created_at_elapsed
    
    # Saving the click in args.state can be quite useful
    
    def tick args
      args.outputs.labels << { x: 640,
                               y: 700,
                               anchor_x: 0.5,
                               anchor_y: 0.5,
                               text: "Sample app shows how mouse events are registered and how to measure elapsed time." }
      x = 460
    
      args.outputs.labels << small_label(args, x, 11, "Mouse input: args.inputs.mouse")
    
      if args.inputs.mouse.click
        args.state.last_mouse_click = args.inputs.mouse.click
      end
    
      if args.state.last_mouse_click
        click = args.state.last_mouse_click
        args.outputs.labels << small_label(args, x, 12, "Mouse click happened at: #{click.created_at}")
        args.outputs.labels << small_label(args, x, 13, "Mouse clicked #{click.created_at_elapsed} ticks ago")
        args.outputs.labels << small_label(args, x, 14, "Mouse click location: #{click.point.x}, #{click.point.y}")
      else
        args.outputs.labels << small_label(args, x, 12, "Mouse click has not occurred yet.")
        args.outputs.labels << small_label(args, x, 13, "Please click mouse.")
      end
    end
    
    def small_label args, x, row, message
      { x: x,
        y: 720 - 5 - 20 * row,
        text: message }
    end
    
    

    Mouse Properties - main.rb link

    # ./samples/02_input_basics/02_mouse_properties/app/main.rb
    def tick args
      args.state.properties = [
        { name: "Top Level properties" },
        { name: "mouse.x", value: args.inputs.mouse.x },
        { name: "mouse.y", value: args.inputs.mouse.y },
        { name: "mouse.wheel", value: args.inputs.mouse.wheel },
        { name: "mouse.moved", value: args.inputs.mouse.moved },
        { name: "mouse.moved_at", value: args.inputs.mouse.moved_at },
        { name: "mouse.click", value: args.inputs.mouse.click },
        { name: "mouse.click_at", value: args.inputs.mouse.click_at },
        { name: "mouse.held", value: args.inputs.mouse.held },
        { name: "mouse.held_at", value: args.inputs.mouse.held_at },
        { name: "mouse.up", value: args.inputs.mouse.up },
        { name: "mouse.up_at", value: args.inputs.mouse.up_at },
        { name: "" },
        { name: "Keys" },
        { name: "mouse.key_down.left", value: args.inputs.mouse.key_down.left },
        { name: "mouse.key_held.left", value: args.inputs.mouse.key_held.left },
        { name: "mouse.key_up.left", value: args.inputs.mouse.key_up.left },
        { name: "mouse.key_down.right", value: args.inputs.mouse.key_down.right },
        { name: "mouse.key_held.right", value: args.inputs.mouse.key_held.right },
        { name: "mouse.key_up.right", value: args.inputs.mouse.key_up.right },
        { name: "mouse.button_bits.to_s(2)", value: args.inputs.mouse.button_bits.to_s(2) },
        { name: "mouse.button_left", value: args.inputs.mouse.button_left },
        { name: "mouse.button_right", value: args.inputs.mouse.button_right },
        { name: "" },
        { name: "Buttons" },
        { name: "mouse.button_bits", value: args.inputs.mouse.button_bits.to_s(2) },
        { name: "mouse.button_left", value: args.inputs.mouse.button_left },
        { name: "mouse.buttons.left.click", value: args.inputs.mouse.buttons.left.click },
        { name: "mouse.buttons.left.click_at", value: args.inputs.mouse.buttons.left.click_at },
        { name: "mouse.buttons.left.held", value: args.inputs.mouse.buttons.left.held },
        { name: "mouse.buttons.left.held_at", value: args.inputs.mouse.buttons.left.held_at },
        { name: "mouse.buttons.left.up", value: args.inputs.mouse.buttons.left.up },
        { name: "mouse.buttons.left.up_at", value: args.inputs.mouse.buttons.left.up_at },
        { name: "mouse.buttons.left.buffered_click", value: args.inputs.mouse.buttons.left.buffered_click },
        { name: "mouse.buttons.left.buffered_held", value: args.inputs.mouse.buttons.left.buffered_held },
        { name: "mouse.button_right", value: args.inputs.mouse.button_left },
        { name: "mouse.buttons.right.click", value: args.inputs.mouse.buttons.right.click },
        { name: "mouse.buttons.right.click_at", value: args.inputs.mouse.buttons.right.click_at },
        { name: "mouse.buttons.right.held", value: args.inputs.mouse.buttons.right.held },
        { name: "mouse.buttons.right.held_at", value: args.inputs.mouse.buttons.right.held_at },
        { name: "mouse.buttons.right.up", value: args.inputs.mouse.buttons.right.up },
        { name: "mouse.buttons.right.up_at", value: args.inputs.mouse.buttons.right.up_at },
        { name: "mouse.buttons.right.buffered_click", value: args.inputs.mouse.buttons.right.buffered_click },
        { name: "mouse.buttons.right.buffered_held", value: args.inputs.mouse.buttons.right.buffered_held },
      ]
    
      args.outputs.primitives << args.state.highlight_fx
    
      args.outputs.labels << args.state.properties.map_with_index do |property, i|
        text = if property.key?(:value)
                 "#{property.name}: #{property.value.inspect}"
               else
                 property.name
               end
        {
          x: 16,
          y: 720 - 8 - i * 16,
          text: text,
          size_px: 14
        }
      end
    end
    
    

    Mouse Point To Rect - main.rb link

    # ./samples/02_input_basics/03_mouse_point_to_rect/app/main.rb
    =begin
    - Example usage of Hash#inside_rect? to determine if a mouse click happened
      inside of a box.
      ```
      rect_1 = { x: 100, y: 100, w:   1, h:   1 }
      rect_2 = { x:   0, y:   0, w: 500, h: 500 }
      result = rect_1.inside_rect? rect_2
      ```
    =end
    def tick args
      # initialize the rectangle
      args.state.box ||= { x: 785, y: 370, w: 50, h: 50, r: 0, g: 0, b: 170 }
    
      # store the mouse click and the frame the click occurred
      # and whether it was inside or outside the box
      if args.inputs.mouse.click
        args.state.last_mouse_click = args.inputs.mouse.click
        args.state.last_mouse_click_at = Kernel.tick_count
        if args.state.last_mouse_click.inside_rect? args.state.box
          args.state.was_inside_rect = true
        else
          args.state.was_inside_rect = false
        end
      end
    
      # render
      args.outputs.labels << { x: 640, y: 700, anchor_x: 0.5, anchor_y: 0.5, text: "Sample app shows how to determine if a click happened inside a rectangle." }
      args.outputs.labels << { x: 340, y: 420, text:  "Click inside (or outside) the blue box ---->" }
    
      args.outputs.borders << args.state.box
    
      if args.state.last_mouse_click
        if args.state.was_inside_rect
          args.outputs.labels << { x: 810,
                                   y: 340,
                                   anchor_x: 0.5,
                                   anchor_y: 0.5,
                                   text: "Mouse click happened *inside* the box [frame #{args.state.last_mouse_click_at}]." }
        else
          args.outputs.labels << { x: 810,
                                   y: 340,
                                   anchor_x: 0.5,
                                   anchor_y: 0.5,
                                   text: "Mouse click happened *outside* the box [frame #{args.state.last_mouse_click_at}]." }
        end
      else
        args.outputs.labels << { x: 810,
                                 y: 340,
                                 anchor_x: 0.5,
                                 anchor_y: 0.5,
                                 text: "Waiting for mouse click..." }
      end
    end
    
    

    Mouse Drag And Drop - main.rb link

    # ./samples/02_input_basics/04_mouse_drag_and_drop/app/main.rb
    def tick args
      # create 10 random squares on the screen
      if !args.state.squares
        # the squares will be contained in lookup/Hash so that we can access via their id
        args.state.squares = {}
        10.times_with_index do |id|
          # for each square, store it in the hash with
          # the id (we're just using the index 0-9 as the index)
          args.state.squares[id] = {
            id: id,
            x: 100 + (rand * 1080),
            y: 100 + (520 * rand),
            w: 100,
            h: 100,
            path: "sprites/square/blue.png"
          }
        end
      end
    
      # two key variables are set here
      # - square_reference: this represents the square that is currently being dragged
      # - square_under_mouse: this represents the square that the mouse is currently being hovered over
      if args.state.currently_dragging_square_id
        # if the currently_dragging_square_id is set, then set the "square_under_mouse" to
        # the same square as square_reference
        square_reference = args.state.squares[args.state.currently_dragging_square_id]
        square_under_mouse = square_reference
      else
        # if currently_dragging_square_id isn't set, then see if there is a square that
        # the mouse is currently hovering over (the square reference will be nil since
        # we haven't selected a drag target yet)
        square_under_mouse = Geometry.find_intersect_rect args.inputs.mouse, args.state.squares.values
        square_reference = nil
      end
    
    
      # if a click occurs, and there is a square under the mouse
      if args.inputs.mouse.click && square_under_mouse
        # capture the id of the square that the mouse is hovering over
        args.state.currently_dragging_square_id = square_under_mouse.id
    
        # also capture where in the square the mouse was clicked so that
        # the movement of the square will smoothly transition with the mouse's
        # location
        args.state.mouse_point_inside_square = {
          x: args.inputs.mouse.x - square_under_mouse.x,
          y: args.inputs.mouse.y - square_under_mouse.y,
        }
      elsif args.inputs.mouse.held && args.state.currently_dragging_square_id
        # if the mouse is currently being held and the currently_dragging_square_id was set,
        # then update the x and y location of the referenced square (taking into consideration the
        # relative position of the mouse when the square was clicked)
        square_reference.x = args.inputs.mouse.x - args.state.mouse_point_inside_square.x
        square_reference.y = args.inputs.mouse.y - args.state.mouse_point_inside_square.y
      elsif args.inputs.mouse.up
        # if the mouse is released, then clear out the currently_dragging_square_id
        args.state.currently_dragging_square_id = nil
      end
    
      # render all the squares on the screen
      args.outputs.sprites << args.state.squares.values
    
      # if there was a square under the mouse, add an "overlay"
      if square_under_mouse
        args.outputs.sprites << square_under_mouse.merge(path: "sprites/square/red.png")
      end
    end
    
    GTK.recording.on_replay_completed_successfully do |args|
      raise "Square was not in the right place" if args.state.squares[2].x.floor != 746
    end
    
    

    Mouse Rect To Rect - main.rb link

    # ./samples/02_input_basics/04_mouse_rect_to_rect/app/main.rb
    =begin
    
    APIs that haven't been encountered in a previous sample apps:
    
    - args.outputs.borders: An array. Values in this array will be rendered as
      unfilled rectangles on the screen.
    - ARRAY#intersect_rect?: An array with at least four values is
      considered a rect. The intersect_rect? function returns true
      or false depending on if the two rectangles intersect.
    
      ```
      # Rect One: x: 100, y: 100, w: 100, h: 100
      # Rect Two: x: 0, y: 0, w: 500, h: 500
      # Result:   true
    
      [100, 100, 100, 100].intersect_rect? [0, 0, 500, 500]
      ```
    
      ```
      # Rect One: x: 100, y: 100, w: 10, h: 10
      # Rect Two: x: 500, y: 500, w: 10, h: 10
      # Result:   false
    
      [100, 100, 10, 10].intersect_rect? [500, 500, 10, 10]
      ```
    
    =end
    
    # Similarly, whether rects intersect can be found through
    # rect1.intersect_rect? rect2
    
    def tick args
      tick_instructions args, "Sample app shows how to determine if two rectangles intersect."
      x = 460
    
      args.outputs.labels << small_label(args, x, 3, "Click anywhere on the screen")
      # red_box = [460, 250, 355, 90, 170, 0, 0]
      # args.outputs.borders << red_box
    
      # args.state.box_collision_one and args.state.box_collision_two
      # Are given values of a solid when they should be rendered
      # They are stored in game so that they do not get reset every tick
      if args.inputs.mouse.click
        if !args.state.box_collision_one
          args.state.box_collision_one = { x: args.inputs.mouse.click.point.x - 25,
                                           y: args.inputs.mouse.click.point.y - 25,
                                           w: 125, h: 125,
                                           r: 180, g: 0, b: 0, a: 180 }
        elsif !args.state.box_collision_two
          args.state.box_collision_two = { x: args.inputs.mouse.click.point.x - 25,
                                           y: args.inputs.mouse.click.point.y - 25,
                                           w: 125, h: 125,
                                           r: 0, g: 0, b: 180, a: 180 }
        else
          args.state.box_collision_one = nil
          args.state.box_collision_two = nil
        end
      end
    
      if args.state.box_collision_one
        args.outputs.solids << args.state.box_collision_one
      end
    
      if args.state.box_collision_two
        args.outputs.solids << args.state.box_collision_two
      end
    
      if args.state.box_collision_one && args.state.box_collision_two
        if args.state.box_collision_one.intersect_rect? args.state.box_collision_two
          args.outputs.labels << small_label(args, x, 4, 'The boxes intersect.')
        else
          args.outputs.labels << small_label(args, x, 4, 'The boxes do not intersect.')
        end
      else
        args.outputs.labels << small_label(args, x, 4, '--')
      end
    end
    
    def small_label args, x, row, message
      { x: x, y: row_to_px(args, row), text: message, size_enum: -2 }
    end
    
    def row_to_px args, row_number
      args.grid.top - 5 - (20 * row_number)
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Controller - main.rb link

    # ./samples/02_input_basics/05_controller/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - args.current_controller.key_held.KEY: Will check to see if a specific key
       is being held down on the controller.
       If there is more than one controller being used, they can be differentiated by
       using names like controller_one and controller_two.
    
       For a full listing of buttons, take a look at mygame/documentation/08-controllers.md.
    
     Reminder:
    
     - args.state.PROPERTY: The state property on args is a dynamic
       structure. You can define ANY property here with ANY type of
       arbitrary nesting. Properties defined on args.state will be retained
       across frames. If you attempt to access a property that doesn't exist
       on args.state, it will simply return nil (no exception will be thrown).
    
       In this sample app, args.state.BUTTONS is an array that stores the buttons of the controller.
       The parameters of a button are:
       1. the position (x, y)
       2. the input key held on the controller
       3. the text or name of the button
    
    =end
    
    # This sample app provides a visual demonstration of a standard controller, including
    # the placement and function of all buttons.
    
    class ControllerDemo
      attr_accessor :inputs, :state, :outputs
    
      # Calls the methods necessary for the app to run successfully.
      def tick
        process_inputs
        render
      end
    
      # Starts with an empty collection of buttons.
      # Adds buttons that are on the controller to the collection.
      def process_inputs
        state.target  ||= :controller_one
        state.buttons = []
    
        if inputs.keyboard.key_down.tab
          if state.target == :controller_one
            state.target = :controller_two
          elsif state.target == :controller_two
            state.target = :controller_three
          elsif state.target == :controller_three
            state.target = :controller_four
          elsif state.target == :controller_four
            state.target = :controller_one
          end
        end
    
        state.buttons << { x: 100,  y: 500, active: current_controller.key_held.l1, text: "L1"}
        state.buttons << { x: 100,  y: 600, active: current_controller.key_held.l2, text: "L2"}
        state.buttons << { x: 1100, y: 500, active: current_controller.key_held.r1, text: "R1"}
        state.buttons << { x: 1100, y: 600, active: current_controller.key_held.r2, text: "R2"}
        state.buttons << { x: 540,  y: 450, active: current_controller.key_held.select, text: "Select"}
        state.buttons << { x: 660,  y: 450, active: current_controller.key_held.start, text: "Start"}
        state.buttons << { x: 200,  y: 300, active: current_controller.key_held.left, text: "Left"}
        state.buttons << { x: 300,  y: 400, active: current_controller.key_held.up, text: "Up"}
        state.buttons << { x: 400,  y: 300, active: current_controller.key_held.right, text: "Right"}
        state.buttons << { x: 300,  y: 200, active: current_controller.key_held.down, text: "Down"}
        state.buttons << { x: 800,  y: 300, active: current_controller.key_held.x, text: "X"}
        state.buttons << { x: 900,  y: 400, active: current_controller.key_held.y, text: "Y"}
        state.buttons << { x: 1000, y: 300, active: current_controller.key_held.a, text: "A"}
        state.buttons << { x: 900,  y: 200, active: current_controller.key_held.b, text: "B"}
        state.buttons << { x: 450 + current_controller.left_analog_x_perc * 100,
                           y: 100 + current_controller.left_analog_y_perc * 100,
                           active: current_controller.key_held.l3,
                           text: "L3" }
        state.buttons << { x: 750 + current_controller.right_analog_x_perc * 100,
                           y: 100 + current_controller.right_analog_y_perc * 100,
                           active: current_controller.key_held.r3,
                           text: "R3" }
      end
    
      # Gives each button a square shape.
      # If the button is being pressed or held (which means it is considered active),
      # the square is filled in. Otherwise, the button simply has a border.
      def render
        state.buttons.each do |b|
          rect = { x: b.x, y: b.y, w: 75, h: 75 }
    
          if b.active # if button is pressed
            outputs.solids << rect # rect is output as solid (filled in)
          else
            outputs.borders << rect # otherwise, output as border
          end
    
          # Outputs the text of each button using labels.
          outputs.labels << { x: b.x, y: b.y + 95, text: b.text } # add 95 to place label above button
        end
    
        outputs.labels << { x:  10, y: 60, text: "Left Analog x: #{current_controller.left_analog_x_raw} (#{current_controller.left_analog_x_perc * 100}%)" }
        outputs.labels << { x:  10, y: 30, text: "Left Analog y: #{current_controller.left_analog_y_raw} (#{current_controller.left_analog_y_perc * 100}%)" }
        outputs.labels << { x: 1270, y: 60, text: "Right Analog x: #{current_controller.right_analog_x_raw} (#{current_controller.right_analog_x_perc * 100}%)", alignment_enum: 2 }
        outputs.labels << { x: 1270, y: 30, text: "Right Analog y: #{current_controller.right_analog_y_raw} (#{current_controller.right_analog_y_perc * 100}%)" , alignment_enum: 2 }
    
        outputs.labels << { x: 640, y: 60, text: "Target: #{state.target} (press tab to go to next controller)", alignment_enum: 1 }
        outputs.labels << { x: 640, y: 30, text: "Connected: #{current_controller.connected}", alignment_enum: 1 }
      end
    
      def current_controller
        if state.target == :controller_one
          return inputs.controller_one
        elsif state.target == :controller_two
          return inputs.controller_two
        elsif state.target == :controller_three
          return inputs.controller_three
        elsif state.target == :controller_four
          return inputs.controller_four
        end
      end
    end
    
    $controller_demo = ControllerDemo.new
    
    def tick args
      tick_instructions args, "Sample app shows how controller input is handled. You'll need to connect a USB controller."
      $controller_demo.inputs = args.inputs
      $controller_demo.state = args.state
      $controller_demo.outputs = args.outputs
      $controller_demo.tick
    end
    
    # Resets the app.
    def r
      GTK.reset
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Touch - main.rb link

    # ./samples/02_input_basics/06_touch/app/main.rb
    def tick args
      args.outputs.background_color = [ 0, 0, 0 ]
      args.outputs.primitives << [640, 700, "Touch your screen.", 5, 1, 255, 255, 255].label
    
      # If you don't want to get fancy, you can just look for finger_one
      #  (and _two, if you like), which are assigned in the order new touches hit
      #  the screen. If not nil, they are touching right now, and are just
      #  references to specific items in the args.input.touch hash.
      # If finger_one lifts off, it will become nil, but finger_two, if it was
      #  touching, remains until it also lifts off. When all fingers lift off, the
      #  the next new touch will be finger_one again, but until then, new touches
      #  don't fill in earlier slots.
      if !args.inputs.finger_one.nil?
        args.outputs.primitives << { x: 640, y: 650, text: "Finger #1 is touching at (#{args.inputs.finger_one.x}, #{args.inputs.finger_one.y}).",
                                     size_enum: 5, alignment_enum: 1, r: 255, g: 255, b: 255 }.label!
      end
      if !args.inputs.finger_two.nil?
        args.outputs.primitives << { x: 640, y: 600, text: "Finger #2 is touching at (#{args.inputs.finger_two.x}, #{args.inputs.finger_two.y}).",
                                     size_enum: 5, alignment_enum: 1, r: 255, g: 255, b: 255 }.label!
      end
    
      # Here's the more flexible interface: this will report as many simultaneous
      #  touches as the system can handle, but it's a little more effort to track
      #  them. Each item in the args.input.touch hash has a unique key (an
      #  incrementing integer) that exists until the finger lifts off. You can
      #  tell which order the touches happened globally by the key value, or
      #  by the touch[id].touch_order field, which resets to zero each time all
      #  touches have lifted.
    
      args.state.colors ||= [
        0xFF0000, 0x00FF00, 0x1010FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, 0xFFFFFF
      ]
    
      size = 100
      args.inputs.touch.each { |k,v|
        color = args.state.colors[v.touch_order % 7]
        r = (color & 0xFF0000) >> 16
        g = (color & 0x00FF00) >> 8
        b = (color & 0x0000FF)
        args.outputs.primitives << { x: v.x - (size / 2), y: v.y + (size / 2), w: size, h: size, r: r, g: g, b: b, a: 255 }.solid!
        args.outputs.primitives << { x: v.x, y: v.y + size, text: k.to_s, alignment_enum: 1 }.label!
      }
    end
    
    

    Managing Scenes - main.rb link

    # ./samples/02_input_basics/07_managing_scenes/app/main.rb
    def tick args
      # initialize the scene to scene 1
      args.state.current_scene ||= :title_scene
      # capture the current scene to verify it didn't change through
      # the duration of tick
      current_scene = args.state.current_scene
    
      # tick whichever scene is current
      case current_scene
      when :title_scene
        tick_title_scene args
      when :game_scene
        tick_game_scene args
      when :game_over_scene
        tick_game_over_scene args
      end
    
      # make sure that the current_scene flag wasn't set mid tick
      if args.state.current_scene != current_scene
        raise "Scene was changed incorrectly. Set args.state.next_scene to change scenes."
      end
    
      # if next scene was set/requested, then transition the current scene to the next scene
      if args.state.next_scene
        args.state.current_scene = args.state.next_scene
        args.state.next_scene = nil
      end
    end
    
    def tick_title_scene args
      args.outputs.labels << { x: 640,
                               y: 360,
                               text: "Title Scene (click to go to game)",
                               alignment_enum: 1 }
    
      if args.inputs.mouse.click
        args.state.next_scene = :game_scene
      end
    end
    
    def tick_game_scene args
      args.outputs.labels << { x: 640,
                               y: 360,
                               text: "Game Scene (click to go to game over)",
                               alignment_enum: 1 }
    
      if args.inputs.mouse.click
        args.state.next_scene = :game_over_scene
      end
    end
    
    def tick_game_over_scene args
      args.outputs.labels << { x: 640,
                               y: 360,
                               text: "Game Over Scene (click to go to title)",
                               alignment_enum: 1 }
    
      if args.inputs.mouse.click
        args.state.next_scene = :title_scene
      end
    end
    
    

    Managing Scenes Advanced - main.rb link

    # ./samples/02_input_basics/07_managing_scenes_advanced/app/main.rb
    # representation of a game that has a healing mechanic
    class Game
      # game has access to args and hp
      attr :args, :hp
    
      # initialize game with 100 hp
      def initialize
        @hp = 100
      end
    
      # take damage function that reduces hp
      def take_damage
        @hp -= 10
      end
    
      # heal function that increases hp
      def heal
        @hp += 10
      end
    
      # game over if hp <= 0
      def dead?
        @hp <= 0
      end
    
      # resets the game from the start
      def restart
        @hp = 100
      end
    end
    
    # scene that represents game over
    class GameOverScene
      # property reference to game and args
      attr :game, :args
    
      # initialize scene with game reference
      def initialize game
        @game = game
      end
    
      # id for scene lookup
      def id
        :game_over_scene
      end
    
      # main tick function for scene
      def tick
        # click to restart game
        if args.inputs.mouse.click
          # mark the game as restarted
          @game.restart
    
          # set the scene to be the heal scene
          args.state.next_scene = :heal_scene
        end
    
        # render label with instructions
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: "Game Over. Click to restart.",
                                 anchor_x: 0.5,
                                 anchor_y: 0.5 }
      end
    end
    
    # scene that represents healing
    class HealScene
      # property reference to game and args
      attr :game, :args
    
      # initialize scene with game reference
      def initialize game
        @game = game
      end
    
      # id for scene lookup
      def id
        :heal_scene
      end
    
      # main tick function for scene
      def tick
        # if mouse is clicked, go to the damage scene
        if args.inputs.click
          args.state.next_scene = :damage_scene
        end
    
        # if enter is pressed, heal
        if args.inputs.keyboard.key_down.enter
          @game.heal
        end
    
        # render instructions and current hp
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: "I am Heal Scene. Click to go to Damage Scene. Press enter to Heal.",
                                 anchor_x: 0.5,
                                 anchor_y: 0.5 }
    
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: "Current HP: #{@game.hp}",
                                 anchor_x: 0.5,
                                 anchor_y: 1.5 }
      end
    end
    
    # scene that represents damage
    class DamageScene
      # property reference to game and args
      attr :game, :args
    
      # initialize scene with game reference
      def initialize game
        @game = game
      end
    
      # id for scene lookup
      def id
        :damage_scene
      end
    
      # main tick function for scene
      def tick
        # if mouse is clicked, go to heal scene
        if args.inputs.click
          args.state.next_scene = :heal_scene
        end
    
        # if enter is pressed, take damage
        if args.inputs.keyboard.key_down.enter
          @game.take_damage
        end
    
        # if the player is dead, go to the game over scene
        if @game.dead?
          args.state.next_scene = :game_over_scene
        end
    
        # render instructions and current hp
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: "I am Damage Scene. Click to go to Heal Scene. Press enter to Take Damage.",
                                 anchor_x: 0.5,
                                 anchor_y: 0.5 }
    
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: "Current HP: #{@game.hp}",
                                 anchor_x: 0.5,
                                 anchor_y: 1.5 }
      end
    end
    
    # root scene holds game and all other scenes
    class RootScene
      # property reference to game and args
      attr :args, :game
    
      # initialize the root scene with game and all scenes
      def initialize
        @game = Game.new
        @heal_scene = HealScene.new @game
        @damage_scene = DamageScene.new @game
        @game_over_scene = GameOverScene.new @game
        @scenes = [@heal_scene, @damage_scene, @game_over_scene]
      end
    
      # set the starting state to the heal
      def defaults
        args.state.scene ||= :heal_scene
      end
    
      # top level tick function
      def tick
        # initialize defaults
        defaults
    
        # we want to make sure that scene transitions happen at the end
        # (you never want to swap scenes mid-tick since it makes things hard to debug)
        scene_before_tick = args.state.scene
    
        # get the current scene that should be ticked
        scene = get_current_scene
        # set that scene's args reference
        scene.args = args
        # invoke tick on the scene
        scene.tick
    
        # check to make sure that the current scene wasn't changed within the tick
        if args.state.scene != scene_before_tick
          raise "Do not change the scene mid tick, set state.next_scene"
        end
    
        # check to see if next scene was set, and if so do the scene transition here
        if args.state.next_scene
          args.state.scene = args.state.next_scene
          args.state.next_scene = nil
        end
      end
    
      # function is used to find the current scene that should be ticked
      def get_current_scene
        # each scene has a scene id, we use args.state.scene to search for the
        # correct scene to call tick on
        scene = @scenes.find { |scene| scene.id == args.state.scene }
        # raise an error if no scene was found
        raise "Scene with id #{args.state.scene} does not exist." if !scene
    
        # return the scene that was found
        scene
      end
    end
    
    # entry point
    def tick args
      # set root scene if it isn't initialized, set args, and invoke tick
      $root_scene ||= RootScene.new
      $root_scene.args = args
      $root_scene.tick
    end
    
    # reset method that clears out root scene
    def reset args
      $root_scene = nil
    end
    
    GTK.reset
    
    

    Rendering Sprites link

    Animation Using Separate Pngs - main.rb link

    # ./samples/03_rendering_sprites/01_animation_using_separate_pngs/app/main.rb
    =begin
     Reminders:
    
     - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated
       as Ruby code, and the placeholder is replaced with its corresponding value or result.
    
       In this sample app, we're using string interpolation to iterate through images in the
       sprites folder using their image path names.
    
     - args.outputs.sprites: An array. Values in this array generate sprites on the screen.
       The parameters are [X, Y, WIDTH, HEIGHT, IMAGE PATH]
       For more information about sprites, go to mygame/documentation/05-sprites.md.
    
     - args.outputs.labels: An array. Values in the array generate labels on the screen.
       The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE]
       For more information about labels, go to mygame/documentation/02-labels.md.
    
     - args.inputs.keyboard.key_down.KEY: Determines if a key is in the down state, or pressed.
       Stores the frame that key was pressed on.
       For more information about the keyboard, go to mygame/documentation/06-keyboard.md.
    
    =end
    
    # This sample app demonstrates how sprite animations work.
    # There are two sprites that animate forever and one sprite
    # that *only* animates when you press the "f" key on the keyboard.
    
    # This is the entry point to your game. The `tick` method
    # executes at 60 frames per second. There are two methods
    # in this tick "entry point": `looping_animation`, and the
    # second method is `one_time_animation`.
    def tick args
      # uncomment the line below to see animation play out in slow motion
      # GTK.slowmo! 6
      looping_animation args
      one_time_animation args
    end
    
    # This function shows how to animate a sprite that loops forever.
    def looping_animation args
      # Here we define a few local variables that will be sent
      # into the magic function that gives us the correct sprite image
      # over time. There are four things we need in order to figure
      # out which sprite to show.
    
      # 1. When to start the animation.
      start_looping_at = 0
    
      # 2. The number of pngs that represent the full animation.
      number_of_sprites = 6
    
      # 3. How long to show each png.
      number_of_frames_to_show_each_sprite = 4
    
      # 4. Whether the animation should loop once, or forever.
      does_sprite_loop = true
    
      # With the variables defined above, we can get a number
      # which represents the sprite to show by calling the `frame_index` function.
      # In this case the number will be between 0, and 5 (you can see the sprites
      # in the ./sprites directory).
      sprite_index = start_looping_at.frame_index number_of_sprites,
                                                  number_of_frames_to_show_each_sprite,
                                                  does_sprite_loop
    
      # Now that we have `sprite_index, we can present the correct file.
      args.outputs.sprites << { x: 100,
                                y: 100,
                                w: 100,
                                h: 100,
                                path: "sprites/dragon_fly_#{sprite_index}.png" }
    
      # Try changing the numbers below to see how the animation changes:
      args.outputs.sprites << { x: 100,
                                y: 200,
                                w: 100,
                                h: 100,
                                path: "sprites/dragon_fly_#{0.frame_index 6, 4, true}.png" }
    end
    
    # This function shows how to animate a sprite that executes
    # only once when the "f" key is pressed.
    def one_time_animation args
      # This is just a label the shows instructions within the game.
      args.outputs.labels <<  { x: 220, y: 350, text: "(press f to animate)" }
    
      # If "f" is pressed on the keyboard...
      if args.inputs.keyboard.key_down.f
        # Print the frame that "f" was pressed on.
        puts "Hello from main.rb! The \"f\" key was in the down state on frame: #{Kernel.tick_count}"
    
        # And MOST IMPORTANTLY set the point it time to start the animation,
        # equal to "now" which is represented as Kernel.tick_count.
    
        # Also IMPORTANT, you'll notice that the value of when to start looping
        # is stored in `args.state`. This construct's values are retained across
        # executions of the `tick` method.
        args.state.start_looping_at = Kernel.tick_count
      end
    
      # These are the same local variables that were defined
      # for the `looping_animation` function.
      number_of_sprites = 6
      number_of_frames_to_show_each_sprite = 4
    
      # Except this sprite does not loop again. If the animation time has passed,
      # then the frame_index function returns nil.
      does_sprite_loop = false
    
      if args.state.start_looping_at
        sprite_index = args.state
                           .start_looping_at
                           .frame_index number_of_sprites,
                                        number_of_frames_to_show_each_sprite,
                                        does_sprite_loop
      end
    
      # This line sets the frame index to zero, if
      # the animation duration has passed (frame_index returned nil).
    
      # Remeber: we are not looping forever here.
      sprite_index ||= 0
    
      # Present the sprite.
      args.outputs.sprites << { x: 100,
                                y: 300,
                                w: 100,
                                h: 100,
                                path: "sprites/dragon_fly_#{sprite_index}.png" }
    
      args.outputs.labels << { x: 640,
                               y: 700,
                               text: "Sample app shows how to use Numeric#frame_index to animate a sprite over time.",
                               anchor_x: 0.5,
                               anchor_y: 0.5 }
    end
    
    

    Animation Using Sprite Sheet - main.rb link

    # ./samples/03_rendering_sprites/02_animation_using_sprite_sheet/app/main.rb
    def tick args
      args.state.player ||= { x: 100,
                              y: 100,
                              w: 64,
                              h: 64,
                              direction: 1,
                              is_moving: false }
    
      # get the keyboard input and set player properties
      if args.inputs.keyboard.right
        args.state.player.x += 3
        args.state.player.direction = 1
        args.state.player.started_running_at ||= Kernel.tick_count
      elsif args.inputs.keyboard.left
        args.state.player.x -= 3
        args.state.player.direction = -1
        args.state.player.started_running_at ||= Kernel.tick_count
      end
    
      if args.inputs.keyboard.up
        args.state.player.y += 1
        args.state.player.started_running_at ||= Kernel.tick_count
      elsif args.inputs.keyboard.down
        args.state.player.y -= 1
        args.state.player.started_running_at ||= Kernel.tick_count
      end
    
      # if no arrow keys are being pressed, set the player as not moving
      if !args.inputs.keyboard.directional_vector
        args.state.player.started_running_at = nil
      end
    
      # wrap player around the stage
      if args.state.player.x > 1280
        args.state.player.x = -64
        args.state.player.started_running_at ||= Kernel.tick_count
      elsif args.state.player.x < -64
        args.state.player.x = 1280
        args.state.player.started_running_at ||= Kernel.tick_count
      end
    
      if args.state.player.y > 720
        args.state.player.y = -64
        args.state.player.started_running_at ||= Kernel.tick_count
      elsif args.state.player.y < -64
        args.state.player.y = 720
        args.state.player.started_running_at ||= Kernel.tick_count
      end
    
      # render player as standing or running
      if args.state.player.started_running_at
        args.outputs.sprites << running_sprite(args)
      else
        args.outputs.sprites << standing_sprite(args)
      end
      args.outputs.labels << [30, 700, "Use arrow keys to move around."]
    end
    
    def standing_sprite args
      {
        x: args.state.player.x,
        y: args.state.player.y,
        w: args.state.player.w,
        h: args.state.player.h,
        path: "sprites/horizontal-stand.png",
        flip_horizontally: args.state.player.direction > 0
      }
    end
    
    def running_sprite args
      if !args.state.player.started_running_at
        tile_index = 0
      else
        how_many_frames_in_sprite_sheet = 6
        how_many_ticks_to_hold_each_frame = 3
        should_the_index_repeat = true
        tile_index = args.state
                         .player
                         .started_running_at
                         .frame_index(how_many_frames_in_sprite_sheet,
                                      how_many_ticks_to_hold_each_frame,
                                      should_the_index_repeat)
      end
    
      {
        x: args.state.player.x,
        y: args.state.player.y,
        w: args.state.player.w,
        h: args.state.player.h,
        path: 'sprites/horizontal-run.png',
        tile_x: 0 + (tile_index * args.state.player.w),
        tile_y: 0,
        tile_w: args.state.player.w,
        tile_h: args.state.player.h,
        flip_horizontally: args.state.player.direction > 0
      }
    end
    
    

    Animation States 1 - main.rb link

    # ./samples/03_rendering_sprites/03_animation_states_1/app/main.rb
    class Game
      attr_gtk
    
      def defaults
        state.show_debug_layer = true if Kernel.tick_count == 0
    
        state.player ||= {
          tile_size: 64,
          speed: 3,
          slash_frames: 15,
          x: 50,
          y: 400,
          dir_x: 1,
          dir_y: -1,
          is_moving: false
        }
    
        state.enemies ||= []
      end
    
      def add_enemy
        state.enemies << {
          x: 1200 * rand,
          y: 600 * rand,
          w: 64,
          h: 64,
          anchor_x: 0.5,
          anchor_y: 0.5,
          path: 'sprites/enemy.png'
        }
      end
    
      def sprite_horizontal_run
        tile_index = 0.frame_index(6, 3, true)
        tile_index = 0 if !player.is_moving
    
        {
          x: player.x,
          y: player.y,
          w: player.tile_size,
          h: player.tile_size,
          anchor_x: 0.5,
          anchor_y: 0.5,
          path: 'sprites/horizontal-run.png',
          tile_x: 0 + (tile_index * player.tile_size),
          tile_y: 0,
          tile_w: player.tile_size,
          tile_h: player.tile_size,
          flip_horizontally: player.dir_x > 0,
        }
      end
    
      def sprite_horizontal_stand
        {
          x: player.x,
          y: player.y,
          w: player.tile_size,
          h: player.tile_size,
          anchor_x: 0.5,
          anchor_y: 0.5,
          path: 'sprites/horizontal-stand.png',
          flip_horizontally: player.dir_x > 0,
        }
      end
    
      def sprite_horizontal_slash
        tile_index   = player.slash_at.frame_index(5, player.slash_frames.idiv(5), false) || 0
    
        {
          x: player.x + player.dir_x.sign * 9.25,
          y: player.y + 9.25,
          w: 165,
          h: 165,
          anchor_x: 0.5,
          anchor_y: 0.5,
          path: 'sprites/horizontal-slash.png',
          tile_x: 0 + (tile_index * 128),
          tile_y: 0,
          tile_w: 128,
          tile_h: 128,
          flip_horizontally: player.dir_x > 0
        }
      end
    
      def render_player
        if player.slash_at
          outputs.sprites << sprite_horizontal_slash
        elsif player.is_moving
          outputs.sprites << sprite_horizontal_run
        else
          outputs.sprites << sprite_horizontal_stand
        end
      end
    
      def render_enemies
        outputs.borders << state.enemies
      end
    
      def render_debug_layer
        return if !state.show_debug_layer
        outputs.borders << player.slash_collision_rect
      end
    
      def slash_initiate?
        inputs.controller_one.key_down.a || inputs.keyboard.key_down.j
      end
    
      def input
        # player movement
        if slash_complete? && (vector = inputs.directional_vector)
          player.x += vector.x * player.speed
          player.y += vector.y * player.speed
        end
        player.slash_at = slash_initiate? if slash_initiate?
      end
    
      def calc_movement
        # movement
        if vector = inputs.directional_vector
          state.debug_label = vector
          player.dir_x = vector.x if vector.x != 0
          player.dir_y = vector.y if vector.y != 0
          player.is_moving = true
        else
          state.debug_label = vector
          player.is_moving = false
        end
      end
    
      def calc_slash
        player.slash_collision_rect = {
          x: player.x + player.dir_x.sign * 52,
          y: player.y,
          w: 40,
          h: 20,
          anchor_x: 0.5,
          anchor_y: 0.5,
          path: "sprites/debug-slash.png"
        }
    
        # recalc sword's slash state
        player.slash_at = nil if slash_complete?
    
        # determine collision if the sword is at it's point of damaging
        return unless slash_can_damage?
    
        state.enemies.reject! { |e| e.intersect_rect? player.slash_collision_rect }
      end
    
      def slash_complete?
        !player.slash_at || player.slash_at.elapsed?(player.slash_frames)
      end
    
      def slash_can_damage?
        # damage occurs half way into the slash animation
        return false if slash_complete?
        return false if (player.slash_at + player.slash_frames.idiv(2)) != Kernel.tick_count
        return true
      end
    
      def calc
        # generate an enemy if there aren't any on the screen
        add_enemy if state.enemies.length == 0
        calc_movement
        calc_slash
      end
    
      # source is at http://github.com/amirrajan/dragonruby-link-to-the-past
      def tick
        defaults
        render_enemies
        render_player
        outputs.labels << [30, 30, "Gamepad: D-Pad to move. B button to attack."]
        outputs.labels << [30, 52, "Keyboard: WASD/Arrow keys to move. J to attack."]
        render_debug_layer
        input
        calc
      end
    
      def player
        state.player
      end
    end
    
    $game = Game.new
    
    def tick args
      $game.args = args
      $game.tick
    end
    
    GTK.reset
    
    

    Animation States 2 - main.rb link

    # ./samples/03_rendering_sprites/03_animation_states_2/app/main.rb
    def tick args
      defaults args
      input args
      calc args
      render args
    end
    
    def defaults args
      # uncomment the line below to slow the game down by a factor of 4 -> 15 fps (for debugging)
      # GTK.slowmo! 4
    
      args.state.player ||= {
        x: 144,                # render x of the player
        y: 32,                 # render y of the player
        w: 144 * 2,            # render width of the player
        h: 72 * 2,             # render height of the player
        dx: 0,                 # velocity x of the player
        action: :standing,     # current action/status of the player
        action_at: 0,          # frame that the action occurred
        previous_direction: 1, # direction the player was facing last frame
        direction: 1,          # direction the player is facing this frame
        launch_speed: 4,       # speed the player moves when they start running
        run_acceleration: 1,   # how much the player accelerates when running
        run_top_speed: 8,      # the top speed the player can run
        friction: 0.9,         # how much the player slows down when have stopped attempting to run
        anchor_x: 0.5,         # render anchor x of the player
        anchor_y: 0            # render anchor y of the player
      }
    end
    
    def input args
      # if the directional has been pressed on the input device
      if args.inputs.left_right != 0
        # determine if the player is currently running or not,
        # if they aren't, set their dx to their launch speed
        # otherwise, add the run acceleration to their dx
        if args.state.player.action != :running
          args.state.player.dx = args.state.player.launch_speed * args.inputs.left_right.sign
        else
          args.state.player.dx += args.inputs.left_right * args.state.player.run_acceleration
        end
    
        # capture the direction the player is facing and the previous direction
        args.state.player.previous_direction = args.state.player.direction
        args.state.player.direction = args.inputs.left_right.sign
      end
    end
    
    def calc args
      # clamp the player's dx to the top speed
      args.state.player.dx = args.state.player.dx.clamp(-args.state.player.run_top_speed, args.state.player.run_top_speed)
    
      # move the player by their dx
      args.state.player.x += args.state.player.dx
    
      # capture the player's hitbox
      player_hitbox = hitbox args.state.player
    
      # check boundary collisions and stop the player if they are colliding with the ednges of the screen
      if (player_hitbox.x - player_hitbox.w / 2) < 0
        args.state.player.x = player_hitbox.w / 2
        args.state.player.dx = 0
        # if the player is not standing, set them to standing and capture the frame
        if args.state.player.action != :standing
          args.state.player.action = :standing
          args.state.player.action_at = Kernel.tick_count
        end
      elsif (player_hitbox.x + player_hitbox.w / 2) > 1280
        args.state.player.x = 1280 - player_hitbox.w / 2
        args.state.player.dx = 0
    
        # if the player is not standing, set them to standing and capture the frame
        if args.state.player.action != :standing
          args.state.player.action = :standing
          args.state.player.action_at = Kernel.tick_count
        end
      end
    
      # if the player's dx is not 0, they are running. update their action and capture the frame if needed
      if args.state.player.dx.abs > 0
        if args.state.player.action != :running || args.state.player.direction != args.state.player.previous_direction
          args.state.player.action = :running
          args.state.player.action_at = Kernel.tick_count
        end
      elsif args.inputs.left_right == 0
        # if the player's dx is 0 and they are not currently trying to run (left_right == 0), set them to standing and capture the frame
        if args.state.player.action != :standing
          args.state.player.action = :standing
          args.state.player.action_at = Kernel.tick_count
        end
      end
    
      # if the player is not trying to run (left_right == 0), slow them down by the friction amount
      if args.inputs.left_right == 0
        args.state.player.dx *= args.state.player.friction
    
        # if the player's dx is less than 1, set it to 0
        if args.state.player.dx.abs < 1
          args.state.player.dx = 0
        end
      end
    end
    
    def render args
      # determine if the player should be flipped horizontally
      flip_horizontally = args.state.player.direction == -1
      # determine the path to the sprite to render, the idle sprite is used if action == :standing
      path = "sprites/link-idle.png"
    
      # if the player is running, determine the frame to render
      if args.state.player.action == :running
        # the sprite animation's first 3 frames represent the launch of the run, so we skip them on the animation loop
        # by setting the repeat_index to 3 (the 4th frame)
        frame_index = args.state.player.action_at.frame_index(count: 9, hold_for: 8, repeat: true, repeat_index: 3)
        path = "sprites/link-run-#{frame_index}.png"
    
        args.outputs.labels << { x: args.state.player.x - 144, y: args.state.player.y + 230, text: "action:      #{args.state.player.action}" }
        args.outputs.labels << { x: args.state.player.x - 144, y: args.state.player.y + 200, text: "action_at:   #{args.state.player.action_at}" }
        args.outputs.labels << { x: args.state.player.x - 144, y: args.state.player.y + 170, text: "frame_index: #{frame_index}" }
      else
        args.outputs.labels << { x: args.state.player.x - 144, y: args.state.player.y + 230, text: "action:      #{args.state.player.action}" }
        args.outputs.labels << { x: args.state.player.x - 144, y: args.state.player.y + 200, text: "action_at:   #{args.state.player.action_at}" }
        args.outputs.labels << { x: args.state.player.x - 144, y: args.state.player.y + 170, text: "frame_index: n/a" }
      end
    
    
      # render the player's hitbox and sprite (the hitbox is used to determine boundary collision)
      args.outputs.borders << hitbox(args.state.player)
      args.outputs.borders << args.state.player
    
      # render the player's sprite
      args.outputs.sprites << args.state.player.merge(path: path, flip_horizontally: flip_horizontally)
    end
    
    def hitbox entity
      {
        x: entity.x,
        y: entity.y + 5,
        w: 64,
        h: 96,
        anchor_x: 0.5,
        anchor_y: 0
      }
    end
    
    
    GTK.reset
    
    

    Animation States 3 - main.rb link

    # ./samples/03_rendering_sprites/03_animation_states_3/app/main.rb
    class Game
      attr_gtk
    
      def request_action name, at: nil
        at ||= Kernel.tick_count
        state.player.requested_action = name
        state.player.requested_action_at = at
      end
    
      def defaults
        state.player.x                  ||= 64
        state.player.y                  ||= 0
        state.player.dx                 ||= 0
        state.player.dy                 ||= 0
        state.player.action             ||= :standing
        state.player.action_at          ||= 0
        state.player.next_action_queue  ||= {}
        state.player.facing             ||= 1
        state.player.jump_at            ||= 0
        state.player.jump_count         ||= 0
        state.player.max_speed          ||= 1.0
        state.sabre.x                   ||= state.player.x
        state.sabre.y                   ||= state.player.y
        state.actions_lookup            ||= new_actions_lookup
      end
    
      def render
        outputs.background_color = [32, 32, 32]
        outputs[:scene].w = 128
        outputs[:scene].h = 128
        outputs[:scene].borders << { x: 0, y: 0, w: 128, h: 128, r: 255, g: 255, b: 255 }
        render_player
        render_sabre
        args.outputs.sprites << { x: 320, y: 0, w: 640, h: 640, path: :scene }
        args.outputs.labels << { x: 10, y: 100, text: "Controls:", r: 255, g: 255, b: 255, size_enum: -1 }
        args.outputs.labels << { x: 10, y: 80, text: "Move:   left/right", r: 255, g: 255, b: 255, size_enum: -1 }
        args.outputs.labels << { x: 10, y: 60, text: "Jump:   space | up | right click", r: 255, g: 255, b: 255, size_enum: -1 }
        args.outputs.labels << { x: 10, y: 40, text: "Attack: f     | j  | left click", r: 255, g: 255, b: 255, size_enum: -1 }
      end
    
      def render_sabre
        return if !state.sabre.is_active
        sabre_index = 0.frame_index count:    4,
                                    hold_for: 2,
                                    repeat:   true
        offset =  0
        offset = -8 if state.player.facing == -1
        outputs[:scene].sprites << { x: state.sabre.x + offset,
                            y: state.sabre.y, w: 16, h: 16, path: "sprites/sabre-throw/#{sabre_index}.png" }
      end
    
      def new_actions_lookup
        r = {
          slash_0: {
            frame_count: 6,
            interrupt_count: 4,
            path: "sprites/kenobi/slash-0/:index.png"
          },
          slash_1: {
            frame_count: 6,
            interrupt_count: 4,
            path: "sprites/kenobi/slash-1/:index.png"
          },
          throw_0: {
            frame_count: 8,
            throw_frame: 2,
            catch_frame: 6,
            path: "sprites/kenobi/slash-2/:index.png"
          },
          throw_1: {
            frame_count: 9,
            throw_frame: 2,
            catch_frame: 7,
            path: "sprites/kenobi/slash-3/:index.png"
          },
          throw_2: {
            frame_count: 9,
            throw_frame: 2,
            catch_frame: 7,
            path: "sprites/kenobi/slash-4/:index.png"
          },
          slash_5: {
            frame_count: 11,
            path: "sprites/kenobi/slash-5/:index.png"
          },
          slash_6: {
            frame_count: 8,
            interrupt_count: 6,
            path: "sprites/kenobi/slash-6/:index.png"
          }
        }
    
        r.each.with_index do |(k, v), i|
          v.name               ||= k
          v.index              ||= i
    
          v.hold_for           ||= 5
          v.duration           ||= v.frame_count * v.hold_for
          v.last_index         ||= v.frame_count - 1
    
          v.interrupt_count    ||= v.frame_count
          v.interrupt_duration ||= v.interrupt_count * v.hold_for
    
          v.repeat             ||= false
          v.next_action        ||= r[r.keys[i + 1]]
        end
    
        r
      end
    
      def render_player
        flip_horizontally = if state.player.facing == -1
                              true
                            else
                              false
                            end
    
        player_sprite = { x: state.player.x + 1 - 8,
                          y: state.player.y,
                          w: 16,
                          h: 16,
                          flip_horizontally: flip_horizontally }
    
        if state.player.action == :standing
          if state.player.y != 0
            if state.player.jump_count <= 1
              outputs[:scene].sprites << { **player_sprite, path: "sprites/kenobi/jumping.png" }
            else
              index = state.player.jump_at.frame_index count: 8, hold_for: 5, repeat: false
              index ||= 7
              outputs[:scene].sprites << { **player_sprite, path: "sprites/kenobi/second-jump/#{index}.png" }
            end
          elsif state.player.dx != 0
            index = state.player.action_at.frame_index count: 4, hold_for: 5, repeat: true
            outputs[:scene].sprites << { **player_sprite, path: "sprites/kenobi/run/#{index}.png" }
          else
            outputs[:scene].sprites << { **player_sprite, path: 'sprites/kenobi/standing.png'}
          end
        else
          v = state.actions_lookup[state.player.action]
          slash_frame_index = state.player.action_at.frame_index count:    v.frame_count,
                                                                 hold_for: v.hold_for,
                                                                 repeat:   v.repeat
          slash_frame_index ||= v.last_index
          slash_path          = v.path.sub ":index", slash_frame_index.to_s
          outputs[:scene].sprites << { **player_sprite, path: slash_path }
        end
      end
    
      def calc_input
        if state.player.next_action_queue.length > 2
          raise "Code in calc assums that key length of state.player.next_action_queue will never be greater than 2."
        end
    
        if inputs.controller_one.key_down.a ||
           inputs.mouse.button_left ||
           inputs.keyboard.key_down.j ||
           inputs.keyboard.key_down.f
          request_action :attack
        end
    
        should_update_facing = false
        if state.player.action == :standing
          should_update_facing = true
        else
          key_0 = state.player.next_action_queue.keys[0]
          key_1 = state.player.next_action_queue.keys[1]
          if Kernel.tick_count == key_0
            should_update_facing = true
          elsif Kernel.tick_count == key_1
            should_update_facing = true
          elsif key_0 && key_1 && Kernel.tick_count.between?(key_0, key_1)
            should_update_facing = true
          end
        end
    
        if should_update_facing && inputs.left_right.sign != state.player.facing.sign
          state.player.dx = 0
    
          if inputs.left
            state.player.facing = -1
          elsif inputs.right
            state.player.facing = 1
          end
    
          state.player.dx += 0.1 * inputs.left_right
        end
    
        if state.player.action == :standing
          state.player.dx += 0.1 * inputs.left_right
          if state.player.dx.abs > state.player.max_speed
            state.player.dx = state.player.max_speed * state.player.dx.sign
          end
        end
    
        was_jump_requested = inputs.keyboard.key_down.up ||
                             inputs.keyboard.key_down.w  ||
                             inputs.mouse.button_right  ||
                             inputs.controller_one.key_down.up ||
                             inputs.controller_one.key_down.b ||
                             inputs.keyboard.key_down.space
    
        can_jump = state.player.jump_at.elapsed_time > 20
        if state.player.jump_count <= 1
          can_jump = state.player.jump_at.elapsed_time > 10
        end
    
        if was_jump_requested && can_jump
          if state.player.action == :slash_6
            state.player.action = :standing
          end
          state.player.dy = 1
          state.player.jump_count += 1
          state.player.jump_at     = Kernel.tick_count
        end
      end
    
      def calc
        calc_input
        calc_requested_action
        calc_next_action
        calc_sabre
        calc_player_movement
    
        if state.player.y <= 0 && state.player.dy < 0
          state.player.y = 0
          state.player.dy = 0
          state.player.jump_at = 0
          state.player.jump_count = 0
        end
      end
    
      def calc_player_movement
        state.player.x += state.player.dx
        state.player.y += state.player.dy
        state.player.dy -= 0.05
        if state.player.y <= 0
          state.player.y = 0
          state.player.dy = 0
          state.player.jump_at = 0
          state.player.jump_count = 0
        end
    
        if state.player.dx.abs < 0.09
          state.player.dx = 0
        end
    
        state.player.x = 8  if state.player.x < 8
        state.player.x = 120 if state.player.x > 120
      end
    
      def calc_requested_action
        return if !state.player.requested_action
        return if state.player.requested_action_at > Kernel.tick_count
    
        player_action = state.player.action
        player_action_at = state.player.action_at
    
        # first attack
        if state.player.requested_action == :attack
          if player_action == :standing
            state.player.next_action_queue.clear
            state.player.next_action_queue[Kernel.tick_count] = :slash_0
            state.player.next_action_queue[Kernel.tick_count + state.actions_lookup.slash_0.duration] = :standing
          else
            current_action = state.actions_lookup[state.player.action]
            state.player.next_action_queue.clear
            queue_at = player_action_at + current_action.interrupt_duration
            queue_at = Kernel.tick_count if queue_at < Kernel.tick_count
            next_action = current_action.next_action
            next_action ||= { name: :standing,
                              duration: 4 }
            if next_action
            state.player.next_action_queue[queue_at] = next_action.name
            state.player.next_action_queue[player_action_at +
                                           current_action.interrupt_duration +
                                           next_action.duration] = :standing
            end
          end
        end
    
        state.player.requested_action = nil
        state.player.requested_action_at = nil
      end
    
      def calc_sabre
        can_throw_sabre = true
        sabre_throws = [:throw_0, :throw_1, :throw_2]
        if !sabre_throws.include? state.player.action
          state.sabre.facing = nil
          state.sabre.is_active = false
          return
        end
    
        current_action = state.actions_lookup[state.player.action]
        throw_at = state.player.action_at + (current_action.throw_frame) * 5
        catch_at = state.player.action_at + (current_action.catch_frame) * 5
        if !Kernel.tick_count.between? throw_at, catch_at
          state.sabre.facing = nil
          state.sabre.is_active = false
          return
        end
    
        state.sabre.facing ||= state.player.facing
    
        state.sabre.is_active = true
    
        spline_definition = [
          [  0, 0.25, 0.75, 1.0],
          [1.0, 0.75, 0.25,   0]
        ]
    
        throw_duration = catch_at - throw_at
    
        current_progress = Easing.spline throw_at,
                                         Kernel.tick_count,
                                         throw_duration,
                                         spline_definition
    
        farthest_sabre_x = 32
        state.sabre.y = state.player.y
        state.sabre.x = state.player.x + farthest_sabre_x * current_progress * state.sabre.facing
      end
    
      def calc_next_action
        return if !state.player.next_action_queue[Kernel.tick_count]
    
        state.player.previous_action = state.player.action
        state.player.previous_action_at = state.player.action_at
        state.player.previous_action_ended_at = Kernel.tick_count
        state.player.action = state.player.next_action_queue[Kernel.tick_count]
        state.player.action_at = Kernel.tick_count
    
        is_air_born = state.player.y != 0
    
        if state.player.action == :slash_0
          state.player.dy = 0 if state.player.dy > 0
          if is_air_born
            state.player.dy  = 0.5
          else
            state.player.dx += 0.25 * state.player.facing
          end
        elsif state.player.action == :slash_1
          state.player.dy = 0 if state.player.dy > 0
          if is_air_born
            state.player.dy  = 0.5
          else
            state.player.dx += 0.25 * state.player.facing
          end
        elsif state.player.action == :throw_0
          if is_air_born
            state.player.dy  = 1.0
          end
    
          state.player.dx += 0.5 * state.player.facing
        elsif state.player.action == :throw_1
          if is_air_born
            state.player.dy  = 1.0
          end
    
          state.player.dx += 0.5 * state.player.facing
        elsif state.player.action == :throw_2
          if is_air_born
            state.player.dy  = 1.0
          end
    
          state.player.dx += 0.5 * state.player.facing
        elsif state.player.action == :slash_5
          state.player.dy = 0 if state.player.dy < 0
          if is_air_born
            state.player.dy += 1.0
          else
            state.player.dy += 1.0
          end
    
          state.player.dx += 1.0 * state.player.facing
        elsif state.player.action == :slash_6
          state.player.dy = 0 if state.player.dy > 0
          if is_air_born
            state.player.dy  = -0.5
          end
    
          state.player.dx += 0.5 * state.player.facing
        end
      end
    
      def tick
        defaults
        calc
        render
      end
    end
    
    $game = Game.new
    
    def tick args
      $game.args = args
      $game.tick
    end
    
    GTK.reset
    
    

    Color And Rotation - main.rb link

    # ./samples/03_rendering_sprites/04_color_and_rotation/app/main.rb
    =begin
     APIs listing that haven't been encountered in previous sample apps:
    
     - merge: Returns a hash containing the contents of two original hashes.
       Merge does not allow duplicate keys, so the value of a repeated key
       will be overwritten.
    
       For example, if we had two hashes
       h1 = { "a" => 1, "b" => 2}
       h2 = { "b" => 3, "c" => 3}
       and we called the command
       h1.merge(h2)
       the result would the following hash
       { "a" => 1, "b" => 3, "c" => 3}.
    
     Reminders:
    
     - Hashes: Collection of unique keys and their corresponding values. The value can be found
       using their keys.
       In this sample app, we're using a hash to create a sprite.
    
     - args.outputs.sprites: An array. The values generate a sprite.
       The parameters are [X, Y, WIDTH, HEIGHT, PATH, ANGLE, ALPHA, RED, GREEN, BLUE]
       Before continuing with this sample app, it is HIGHLY recommended that you look
       at mygame/documentation/05-sprites.md.
    
     - args.inputs.keyboard.key_held.KEY: Determines if a key is being pressed.
       For more information about the keyboard, go to mygame/documentation/06-keyboard.md.
    
     - args.inputs.controller_one: Takes input from the controller based on what key is pressed.
       For more information about the controller, go to mygame/documentation/08-controllers.md.
    
     - num1.lesser(num2): Finds the lower value of the given options.
    
    =end
    
    # This sample app shows a car moving across the screen. It loops back around if it exceeds the dimensions of the screen,
    # and also can be moved in different directions through keyboard input from the user.
    
    # Calls the methods necessary for the game to run successfully.
    def tick args
      default args
      render args.grid, args.outputs, args.state
      calc args.state
      process_inputs args
    end
    
    # Sets default values for the car sprite
    # Initialization ||= only happens in the first frame
    def default args
      args.state.sprite.width    = 19
      args.state.sprite.height   = 10
      args.state.sprite.scale    = 4
      args.state.max_speed       = 5
      args.state.x             ||= 100
      args.state.y             ||= 100
      args.state.speed         ||= 1
      args.state.angle         ||= 0
    end
    
    # Outputs sprite onto screen
    def render grid, outputs, state
      outputs.background_color = [70, 70, 70]
      outputs.sprites <<  { **destination_rect(state), # sets first four parameters of car sprite
                            path: 'sprites/86.png',    # image path of car
                            angle: state.angle,
                            a: opacity,                # alpha
                            **saturation,
                            **source_rect(state),      # sprite sub division/tile (source x, y, w, h)
                            flip_horizontally: false,
                            flip_vertically: false,    # don't flip sprites
                            **rotation_anchor }
    end
    
    # Calls the calc_pos and calc_wrap methods.
    def calc state
      calc_pos state
      calc_wrap state
    end
    
    # Changes sprite's position on screen
    # Vectors have magnitude and direction, so the incremented x and y values give the car direction
    def calc_pos state
      state.x     += state.angle.vector_x * state.speed # increments x by product of angle's x vector and speed
      state.y     += state.angle.vector_y * state.speed # increments y by product of angle's y vector and speed
      state.speed *= 1.1 # scales speed up
      state.speed  = state.speed.lesser(state.max_speed) # speed is either current speed or max speed, whichever has a lesser value (ensures that the car doesn't go too fast or exceed the max speed)
    end
    
    # The screen's dimensions are 1280x720. If the car goes out of scope,
    # it loops back around on the screen.
    def calc_wrap state
    
      # car returns to left side of screen if it disappears on right side of screen
      # sprite.width refers to tile's size, which is multipled by scale (4) to make it bigger
      state.x = -state.sprite.width * state.sprite.scale if state.x - 20 > 1280
    
      # car wraps around to right side of screen if it disappears on the left side
      state.x = 1280 if state.x + state.sprite.width * state.sprite.scale + 20 < 0
    
      # car wraps around to bottom of screen if it disappears at the top of the screen
      # if you subtract 520 pixels instead of 20 pixels, the car takes longer to reappear (try it!)
      state.y = 0    if state.y - 20 > 720 # if 20 pixels less than car's y position is greater than vertical scope
    
      # car wraps around to top of screen if it disappears at the bottom of the screen
      state.y = 720  if state.y + state.sprite.height * state.sprite.scale + 20 < 0
    end
    
    # Changes angle of sprite based on user input from keyboard or controller
    def process_inputs args
    
      # NOTE: increasing the angle doesn't mean that the car will continue to go
      # in a specific direction. The angle is increasing, which means that if the
      # left key was kept in the "down" state, the change in the angle would cause
      # the car to go in a counter-clockwise direction and form a circle (360 degrees)
      if args.inputs.keyboard.key_held.left # if left key is pressed
        args.state.angle += 2 # car's angle is incremented by 2
    
      # The same applies to decreasing the angle. If the right key was kept in the
      # "down" state, the decreasing angle would cause the car to go in a clockwise
      # direction and form a circle (360 degrees)
      elsif args.inputs.keyboard.key_held.right # if right key is pressed
        args.state.angle -= 2 # car's angle is decremented by 2
    
      # Input from a controller can also change the angle of the car
      elsif args.inputs.controller_one.left_analog_x_perc != 0
        args.state.angle += 2 * args.inputs.controller_one.left_analog_x_perc * -1
      end
    end
    
    # A sprite's center of rotation can be altered
    # Increasing either of these numbers would dramatically increase the
    # car's drift when it turns!
    def rotation_anchor
      { angle_anchor_x: 0.7, angle_anchor_y: 0.5 }
    end
    
    # Sets opacity value of sprite to 255 so that it is not transparent at all
    # Change it to 0 and you won't be able to see the car sprite on the screen
    def opacity
      255
    end
    
    # Sets the color of the sprite to white.
    def saturation
      { r: 255, g: 255, b: 255 }
    end
    
    # Sets definition of destination_rect (used to define the car sprite)
    def destination_rect state
      { x: state.x,
        y: state.y,
        w: state.sprite.width  * state.sprite.scale, # multiplies by 4 to set size
        h: state.sprite.height * state.sprite.scale }
    end
    
    # Portion of a sprite (a tile)
    # Sub division of sprite is denoted as a rectangle directly related to original size of .png
    # Tile is located at bottom left corner within a 19x10 pixel rectangle (based on sprite.width, sprite.height)
    def source_rect state
      { source_x: 0,
        source_y: 0,
        source_w: state.sprite.width,
        source_h: state.sprite.height }
    end
    
    

    Particles - main.rb link

    # ./samples/03_rendering_sprites/05_particles/app/main.rb
    def tick args
      # Set the background color to black
      args.outputs.background_color = [0, 0, 0]
    
      # Initialize the particle queue if it doesn't exist
      args.state.particle_queue ||= []
    
      # Add a new particle to the queue if the mouse is clicked
      if args.inputs.mouse.click || args.inputs.mouse.held
        args.state.particle_queue << {
          x: args.inputs.mouse.x,    # Set the x position to the mouse's x position
          y: args.inputs.mouse.y,    # Set the y position to the mouse's y position
          emission_speed: 5,         # Set the emission speed to 5
          emission_angle: rand(360), # Set the emission angle to a random angle
          r: 128,                    # Set the red color to 128
          g: rand(128) + 128,        # Set the green color to a random value between 128 and 255
          b: rand(128) + 128,        # Set the blue color to a random value between 128 and 255
        }
      end
    
      # Update the particles
      args.state.particle_queue.each do |particle|
        # initialize default values for particle
        particle.a ||= 255
        particle.path ||= :solid
        particle.w ||= 5
        particle.h ||= 5
        particle.anchor_x ||= 0.5
        particle.anchor_y ||= 0.5
    
        # initialize dx and dy of particle based on the emission speed and angle
        particle.dx ||= particle.emission_speed * particle.emission_angle.vector_x
        particle.dy ||= particle.emission_speed * particle.emission_angle.vector_y
    
        # update the particle's position based on the dx and dy
        particle.x += particle.dx
        particle.y += particle.dy
    
        # decrease the speed of the particle
        particle.dx *= 0.95
        particle.dy *= 0.95
    
        # if the particle's speed is less than 1.0, decrease the alpha value
        if particle.dx.abs < 1.0 && particle.dy.abs < 1.0
          particle.a -= 5
        end
      end
    
      # Remove particles with an alpha value less than or equal to 0
      args.state.particle_queue.reject! do |particle|
        particle.a <= 0
      end
    
      args.outputs.labels << {
        x: 640,
        y: 720,
        text: "Click and hold the mouse to create particles.",
        r: 255,
        g: 255,
        b: 255,
        anchor_x: 0.5,
        anchor_y: 1.0,
      }
    
      # Render the particles
      args.outputs.primitives << args.state.particle_queue
    end
    
    GTK.reset
    
    

    Physics And Collisions link

    Simple - main.rb link

    # ./samples/04_physics_and_collisions/01_simple/app/main.rb
    =begin
    
     Reminders:
     - ARRAY#intersect_rect?: Returns true or false depending on if the two rectangles intersect.
    
     - args.outputs.solids: An array. The values generate a solid.
       The parameters are [X, Y, WIDTH, HEIGHT, RED, GREEN, BLUE]
    
    =end
    
    # This sample app shows collisions between two boxes.
    
    # Runs methods needed for game to run properly.
    def tick args
      tick_instructions args, "Sample app shows how to move a square over time and determine collision."
      defaults args
      render args
      calc args
    end
    
    # Sets default values.
    def defaults args
      # These values represent the moving box.
      args.state.moving_box_speed   = 10
      args.state.moving_box_size    = 100
      args.state.moving_box_dx    ||=  1
      args.state.moving_box_dy    ||=  1
      args.state.moving_box       ||= [0, 0, args.state.moving_box_size, args.state.moving_box_size] # moving_box_size is set as the width and height
    
      # These values represent the center box.
      args.state.center_box ||= [540, 260, 200, 200, 180]
      args.state.center_box_collision ||= false # initially no collision
    end
    
    def render args
      # If the game state denotes that a collision has occurred,
      # render a solid square, otherwise render a border instead.
      if args.state.center_box_collision
        args.outputs.solids << args.state.center_box
      else
        args.outputs.borders << args.state.center_box
      end
    
      # Then render the moving box.
      args.outputs.solids << args.state.moving_box
    end
    
    # Generally in a pipeline for a game engine, you have rendering,
    # game simulation (calculation), and input processing.
    # This fuction represents the game simulation.
    def calc args
      position_moving_box args
      determine_collision_center_box args
    end
    
    # Changes the position of the moving box on the screen by multiplying the change in x (dx) and change in y (dy) by the speed,
    # and adding it to the current position.
    # dx and dy are positive if the box is moving right and up, respectively
    # dx and dy are negative if the box is moving left and down, respectively
    def position_moving_box args
      args.state.moving_box.x += args.state.moving_box_dx * args.state.moving_box_speed
      args.state.moving_box.y += args.state.moving_box_dy * args.state.moving_box_speed
    
      # 1280x720 are the virtual pixels you work with (essentially 720p).
      screen_width  = 1280
      screen_height = 720
    
      # Position of the box is denoted by the bottom left hand corner, in
      # that case, we have to subtract the width of the box so that it stays
      # in the scene (you can try deleting the subtraction to see how it
      # impacts the box's movement).
      if args.state.moving_box.x > screen_width - args.state.moving_box_size
        args.state.moving_box_dx = -1 # moves left
      elsif args.state.moving_box.x < 0
        args.state.moving_box_dx =  1 # moves right
      end
    
      # Here, we're making sure the moving box remains within the vertical scope of the screen
      if args.state.moving_box.y > screen_height - args.state.moving_box_size # if the box moves too high
        args.state.moving_box_dy = -1 # moves down
      elsif args.state.moving_box.y < 0 # if the box moves too low
        args.state.moving_box_dy =  1 # moves up
      end
    end
    
    def determine_collision_center_box args
      # Collision is handled by the engine. You simply have to call the
      # `intersect_rect?` function.
      if args.state.moving_box.intersect_rect? args.state.center_box # if the two boxes intersect
        args.state.center_box_collision = true # then a collision happened
      else
        args.state.center_box_collision = false # otherwise, no collision happened
      end
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Simple Aabb Collision - main.rb link

    # ./samples/04_physics_and_collisions/01_simple_aabb_collision/app/main.rb
    def tick args
      # define terrain of 32x32 sized squares
      args.state.terrain ||= [
        { x: 640,          y: 360,          w: 32, h: 32, path: 'sprites/square/blue.png' },
        { x: 640,          y: 360 - 32,     w: 32, h: 32, path: 'sprites/square/blue.png' },
        { x: 640,          y: 360 - 32 * 2, w: 32, h: 32, path: 'sprites/square/blue.png' },
        { x: 640 + 32,     y: 360 - 32 * 2, w: 32, h: 32, path: 'sprites/square/blue.png' },
        { x: 640 + 32 * 2, y: 360 - 32 * 2, w: 32, h: 32, path: 'sprites/square/blue.png' },
      ]
    
      # define player
      args.state.player ||= {
        x: 600,
        y: 360,
        w: 32,
        h: 32,
        dx: 0,
        dy: 0,
        path: 'sprites/square/red.png'
      }
    
      # render terrain and player
      args.outputs.sprites << args.state.terrain
      args.outputs.sprites << args.state.player
    
      # set dx and dy based on inputs
      args.state.player.dx = args.inputs.left_right * 2
      args.state.player.dy = args.inputs.up_down * 2
    
      # check for collisions on the x and y axis independently
    
      # increment the player's position by dx
      args.state.player.x += args.state.player.dx
    
      # check for collision on the x axis first
      collision = args.state.terrain.find { |t| t.intersect_rect? args.state.player }
    
      # if there is a collision, move the player to the edge of the collision
      # based on the direction of the player's movement and set the player's
      # dx to 0
      if collision
        if args.state.player.dx > 0
          args.state.player.x = collision.x - args.state.player.w
        elsif args.state.player.dx < 0
          args.state.player.x = collision.x + collision.w
        end
        args.state.player.dx = 0
      end
    
      # increment the player's position by dy
      args.state.player.y += args.state.player.dy
    
      # check for collision on the y axis next
      collision = args.state.terrain.find { |t| t.intersect_rect? args.state.player }
    
      # if there is a collision, move the player to the edge of the collision
      # based on the direction of the player's movement and set the player's
      # dy to 0
      if collision
        if args.state.player.dy > 0
          args.state.player.y = collision.y - args.state.player.h
        elsif args.state.player.dy < 0
          args.state.player.y = collision.y + collision.h
        end
        args.state.player.dy = 0
      end
    end
    
    

    Simple Aabb Collision With Map Editor - main.rb link

    # ./samples/04_physics_and_collisions/01_simple_aabb_collision_with_map_editor/app/main.rb
    # the sample app is an expansion of ./01_simple_aabb_collision
    # but includes an in game map editor that saves map data to disk
    def tick args
      # if it's the first tick, read the terrain data from disk
      # and create the player
      if Kernel.tick_count == 0
        args.state.terrain = read_terrain_data args
    
        args.state.player = {
          x: 320,
          y: 320,
          w: 32,
          h: 32,
          dx: 0,
          dy: 0,
          path: 'sprites/square/red.png'
        }
      end
    
      # tick the game (where input and aabb collision is processed)
      tick_game args
    
      # tick the map editor
      tick_map_editor args
    end
    
    def tick_game args
      # render terrain and player
      args.outputs.sprites << args.state.terrain
      args.outputs.sprites << args.state.player
    
      # set dx and dy based on inputs
      args.state.player.dx = args.inputs.left_right * 2
      args.state.player.dy = args.inputs.up_down * 2
    
      # check for collisions on the x and y axis independently
    
      # increment the player's position by dx
      args.state.player.x += args.state.player.dx
    
      # check for collision on the x axis first
      collision = args.state.terrain.find { |t| t.intersect_rect? args.state.player }
    
      # if there is a collision, move the player to the edge of the collision
      # based on the direction of the player's movement and set the player's
      # dx to 0
      if collision
        if args.state.player.dx > 0
          args.state.player.x = collision.x - args.state.player.w
        elsif args.state.player.dx < 0
          args.state.player.x = collision.x + collision.w
        end
        args.state.player.dx = 0
      end
    
      # increment the player's position by dy
      args.state.player.y += args.state.player.dy
    
      # check for collision on the y axis next
      collision = args.state.terrain.find { |t| t.intersect_rect? args.state.player }
    
      # if there is a collision, move the player to the edge of the collision
      # based on the direction of the player's movement and set the player's
      # dy to 0
      if collision
        if args.state.player.dy > 0
          args.state.player.y = collision.y - args.state.player.h
        elsif args.state.player.dy < 0
          args.state.player.y = collision.y + collision.h
        end
        args.state.player.dy = 0
      end
    end
    
    def tick_map_editor args
      # determine the location of the mouse, but
      # aligned to the grid
      grid_aligned_mouse_rect = {
        x: args.inputs.mouse.x.idiv(32) * 32,
        y: args.inputs.mouse.y.idiv(32) * 32,
        w: 32,
        h: 32
      }
    
      # determine if there's a tile at the grid aligned mouse location
      existing_terrain = args.state.terrain.find { |t| t.intersect_rect? grid_aligned_mouse_rect }
    
      # if there is, then render a red square to denote that
      # the tile will be deleted
      if existing_terrain
        args.outputs.sprites << {
          x: args.inputs.mouse.x.idiv(32) * 32,
          y: args.inputs.mouse.y.idiv(32) * 32,
          w: 32,
          h: 32,
          path: "sprites/square/red.png",
          a: 128
        }
      else
        # otherwise, render a blue square to denote that
        # a tile will be added
        args.outputs.sprites << {
          x: args.inputs.mouse.x.idiv(32) * 32,
          y: args.inputs.mouse.y.idiv(32) * 32,
          w: 32,
          h: 32,
          path: "sprites/square/blue.png",
          a: 128
        }
      end
    
      # if the mouse is clicked, then add or remove a tile
      if args.inputs.mouse.click
        if existing_terrain
          args.state.terrain.delete existing_terrain
        else
          args.state.terrain << { **grid_aligned_mouse_rect, path: "sprites/square/blue.png" }
        end
    
        # once the terrain state has been updated
        # save the terrain data to disk
        write_terrain_data args
      end
    end
    
    def read_terrain_data args
      # create the terrain data file if it doesn't exist
      contents = GTK.read_file "data/terrain.txt"
      if !contents
        GTK.write_file "data/terrain.txt", ""
      end
    
      # read the terrain data from disk which is a csv
      GTK.read_file('data/terrain.txt').split("\n").map do |line|
        x, y, w, h = line.split(',').map(&:to_i)
        { x: x, y: y, w: w, h: h, path: 'sprites/square/blue.png' }
      end
    end
    
    def write_terrain_data args
      terrain_csv = args.state.terrain.map { |t| "#{t.x},#{t.y},#{t.w},#{t.h}" }.join "\n"
      GTK.write_file 'data/terrain.txt', terrain_csv
    end
    
    

    Moving Objects - main.rb link

    # ./samples/04_physics_and_collisions/02_moving_objects/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - Hashes: Collection of unique keys and their corresponding values. The value can be found
       using their keys.
    
       For example, if we have a "numbers" hash that stores numbers in English as the
       key and numbers in Spanish as the value, we'd have a hash that looks like this...
       numbers = { "one" => "uno", "two" => "dos", "three" => "tres" }
       and on it goes.
    
       Now if we wanted to find the corresponding value of the "one" key, we could say
       puts numbers["one"]
       which would print "uno" to the console.
    
     - num1.greater(num2): Returns the greater value.
       For example, if we have the command
       puts 4.greater(3)
       the number 4 would be printed to the console since it has a greater value than 3.
       Similar to lesser, which returns the lesser value.
    
     - num1.lesser(num2): Finds the lower value of the given options.
       For example, in the statement
       a = 4.lesser(3)
       3 has a lower value than 4, which means that the value of a would be set to 3,
       but if the statement had been
       a = 4.lesser(5)
       4 has a lower value than 5, which means that the value of a would be set to 4.
    
     - reject: Removes elements from a collection if they meet certain requirements.
       For example, you can derive an array of odd numbers from an original array of
       numbers 1 through 10 by rejecting all elements that are even (or divisible by 2).
    
     - find_all: Finds all values that satisfy specific requirements.
       For example, you can find all elements of a collection that are divisible by 2
       or find all objects that have intersected with another object.
    
     - abs: Returns the absolute value.
       For example, the command
       (-30).abs
       would return 30 as a result.
    
     - map: Ruby method used to transform data; used in arrays, hashes, and collections.
       Can be used to perform an action on every element of a collection, such as multiplying
       each element by 2 or declaring every element as a new entity.
    
     Reminders:
    
     - args.inputs.keyboard.KEY: Determines if a key has been pressed.
       For more information about the keyboard, take a look at mygame/documentation/06-keyboard.md.
    
     - ARRAY#intersect_rect?: Returns true or false depending on if the two rectangles intersect.
    
     - args.outputs.solids: An array. The values generate a solid.
       The parameters are [X, Y, WIDTH, HEIGHT, RED, GREEN, BLUE]
       For more information about solids, go to mygame/documentation/03-solids-and-borders.md.
    
    =end
    
    # Calls methods needed for game to run properly
    def tick args
      tick_instructions args, "Use LEFT and RIGHT arrow keys to move and SPACE to jump."
      defaults args
      render args
      calc args
      input args
    end
    
    # sets default values and creates empty collections
    # initialization only happens in the first frame
    def defaults args
      fiddle args
      args.state.enemy.hammers ||= []
      args.state.enemy.hammer_queue ||= []
      Kernel.tick_count = Kernel.tick_count
      args.state.bridge_top = 128
      args.state.player.x  ||= 0                        # initializes player's properties
      args.state.player.y  ||= args.state.bridge_top
      args.state.player.w  ||= 64
      args.state.player.h  ||= 64
      args.state.player.dy ||= 0
      args.state.player.dx ||= 0
      args.state.enemy.x   ||= 800                      # initializes enemy's properties
      args.state.enemy.y   ||= 0
      args.state.enemy.w   ||= 128
      args.state.enemy.h   ||= 128
      args.state.enemy.dy  ||= 0
      args.state.enemy.dx  ||= 0
      args.state.game_over_at ||= 0
    end
    
    # sets enemy, player, hammer values
    def fiddle args
      args.state.gravity                     = -0.3
      args.state.enemy_jump_power            = 10       # sets enemy values
      args.state.enemy_jump_interval         = 60
      args.state.hammer_throw_interval       = 40       # sets hammer values
      args.state.hammer_launch_power_default = 5
      args.state.hammer_launch_power_near    = 2
      args.state.hammer_launch_power_far     = 7
      args.state.hammer_upward_launch_power  = 15
      args.state.max_hammers_per_volley      = 10
      args.state.gap_between_hammers         = 10
      args.state.player_jump_power           = 10       # sets player values
      args.state.player_jump_power_duration  = 10
      args.state.player_max_run_speed        = 10
      args.state.player_speed_slowdown_rate  = 0.9
      args.state.player_acceleration         = 1
      args.state.hammer_size                 = 32
    end
    
    # outputs objects onto the screen
    def render args
      args.outputs.solids << 20.map_with_index do |i| # uses 20 squares to form bridge
        # sets x by multiplying 64 to index to find pixel value (places all squares side by side)
        # subtracts 64 from bridge_top because position is denoted by bottom left corner
        [i * 64, args.state.bridge_top - 64, 64, 64]
      end
    
      args.outputs.solids << [args.state.x, args.state.y, args.state.w, args.state.h, 255, 0, 0]
      args.outputs.solids << [args.state.player.x, args.state.player.y, args.state.player.w, args.state.player.h, 255, 0, 0] # outputs player onto screen (red box)
      args.outputs.solids << [args.state.enemy.x, args.state.enemy.y, args.state.enemy.w, args.state.enemy.h, 0, 255, 0] # outputs enemy onto screen (green box)
      args.outputs.solids << args.state.enemy.hammers # outputs enemy's hammers onto screen
    end
    
    # Performs calculations to move objects on the screen
    def calc args
    
      # Since velocity is the change in position, the change in x increases by dx. Same with y and dy.
      args.state.player.x  += args.state.player.dx
      args.state.player.y  += args.state.player.dy
    
      # Since acceleration is the change in velocity, the change in y (dy) increases every frame
      args.state.player.dy += args.state.gravity
    
      # player's y position is either current y position or y position of top of
      # bridge, whichever has a greater value
      # ensures that the player never goes below the bridge
      args.state.player.y  = args.state.player.y.greater(args.state.bridge_top)
    
      # player's x position is either the current x position or 0, whichever has a greater value
      # ensures that the player doesn't go too far left (out of the screen's scope)
      args.state.player.x  = args.state.player.x.greater(0)
    
      # player is not falling if it is located on the top of the bridge
      args.state.player.falling = false if args.state.player.y == args.state.bridge_top
      args.state.player.rect = [args.state.player.x, args.state.player.y, args.state.player.h, args.state.player.w] # sets definition for player
    
      args.state.enemy.x += args.state.enemy.dx # velocity; change in x increases by dx
      args.state.enemy.y += args.state.enemy.dy # same with y and dy
    
      # ensures that the enemy never goes below the bridge
      args.state.enemy.y  = args.state.enemy.y.greater(args.state.bridge_top)
    
      # ensures that the enemy never goes too far left (outside the screen's scope)
      args.state.enemy.x  = args.state.enemy.x.greater(0)
    
      # objects that go up must come down because of gravity
      args.state.enemy.dy += args.state.gravity
    
      args.state.enemy.y  = args.state.enemy.y.greater(args.state.bridge_top)
    
      #sets definition of enemy
      args.state.enemy.rect = [args.state.enemy.x, args.state.enemy.y, args.state.enemy.h, args.state.enemy.w]
    
      if args.state.enemy.y == args.state.bridge_top # if enemy is located on the top of the bridge
        args.state.enemy.dy = 0 # there is no change in y
      end
    
      # if 60 frames have passed and the enemy is not moving vertically
      if Kernel.tick_count.mod_zero?(args.state.enemy_jump_interval) && args.state.enemy.dy == 0
        args.state.enemy.dy = args.state.enemy_jump_power # the enemy jumps up
      end
    
      # if 40 frames have passed or 5 frames have passed since the game ended
      if Kernel.tick_count.mod_zero?(args.state.hammer_throw_interval) || args.state.game_over_at.elapsed_time == 5
        # rand will return a number greater than or equal to 0 and less than given variable's value (since max is excluded)
        # that is why we're adding 1, to include the max possibility
        volley_dx   = (rand(args.state.hammer_launch_power_default) + 1) * -1 # horizontal movement (follow order of operations)
    
        # if the horizontal distance between the player and enemy is less than 128 pixels
        if (args.state.player.x - args.state.enemy.x).abs < 128
          # the change in x won't be that great since the enemy and player are closer to each other
          volley_dx = (rand(args.state.hammer_launch_power_near) + 1) * -1
        end
    
        # if the horizontal distance between the player and enemy is greater than 300 pixels
        if (args.state.player.x - args.state.enemy.x).abs > 300
          # change in x will be more drastic since player and enemy are so far apart
          volley_dx = (rand(args.state.hammer_launch_power_far) + 1) * -1 # more drastic change
        end
    
        (rand(args.state.max_hammers_per_volley) + 1).map_with_index do |i|
          args.state.enemy.hammer_queue << { # stores hammer values in a hash
            x: args.state.enemy.x,
            w: args.state.hammer_size,
            h: args.state.hammer_size,
            dx: volley_dx, # change in horizontal position
            # multiplication operator takes precedence over addition operator
            throw_at: Kernel.tick_count + i * args.state.gap_between_hammers
          }
        end
      end
    
      # add elements from hammer_queue collection to the hammers collection by
      # finding all hammers that were thrown before the current frame (have already been thrown)
      args.state.enemy.hammers += args.state.enemy.hammer_queue.find_all do |h|
        h[:throw_at] < Kernel.tick_count
      end
    
      args.state.enemy.hammers.each do |h| # sets values for all hammers in collection
        h[:y]  ||= args.state.enemy.y + 130
        h[:dy] ||= args.state.hammer_upward_launch_power
        h[:dy]  += args.state.gravity # acceleration is change in gravity
        h[:x]   += h[:dx] # incremented by change in position
        h[:y]   += h[:dy]
        h[:rect] = [h[:x], h[:y], h[:w], h[:h]] # sets definition of hammer's rect
      end
    
      # reject hammers that have been thrown before current frame (have already been thrown)
      args.state.enemy.hammer_queue = args.state.enemy.hammer_queue.reject do |h|
        h[:throw_at] < Kernel.tick_count
      end
    
      # any hammers with a y position less than 0 are rejected from the hammers collection
      # since they have gone too far down (outside the scope's screen)
      args.state.enemy.hammers = args.state.enemy.hammers.reject { |h| h[:y] < 0 }
    
      # if there are any hammers that intersect with (or hit) the player,
      # the reset_player method is called (so the game can start over)
      if args.state.enemy.hammers.any? { |h| h[:rect].intersect_rect?(args.state.player.rect) }
        reset_player args
      end
    
      # if the enemy's rect intersects with (or hits) the player,
      # the reset_player method is called (so the game can start over)
      if args.state.enemy.rect.intersect_rect? args.state.player.rect
        reset_player args
      end
    end
    
    # Resets the player by changing its properties back to the values they had at initialization
    def reset_player args
      args.state.player.x = 0
      args.state.player.y = args.state.bridge_top
      args.state.player.dy = 0
      args.state.player.dx = 0
      args.state.enemy.hammers.clear # empties hammer collection
      args.state.enemy.hammer_queue.clear # empties hammer_queue
      args.state.game_over_at = Kernel.tick_count # game_over_at set to current frame (or passage of time)
    end
    
    # Processes input from the user to move the player
    def input args
      if args.inputs.keyboard.space # if the user presses the space bar
        args.state.player.jumped_at ||= Kernel.tick_count # jumped_at is set to current frame
    
        # if the time that has passed since the jump is less than the player's jump duration and
        # the player is not falling
        if args.state.player.jumped_at.elapsed_time < args.state.player_jump_power_duration && !args.state.player.falling
          args.state.player.dy = args.state.player_jump_power # change in y is set to power of player's jump
        end
      end
    
      # if the space bar is in the "up" state (or not being pressed down)
      if args.inputs.keyboard.key_up.space
        args.state.player.jumped_at = nil # jumped_at is empty
        args.state.player.falling = true # the player is falling
      end
    
      if args.inputs.keyboard.left # if left key is pressed
        args.state.player.dx -= args.state.player_acceleration # dx decreases by acceleration (player goes left)
        # dx is either set to current dx or the negative max run speed (which would be -10),
        # whichever has a greater value
        args.state.player.dx = args.state.player.dx.greater(-args.state.player_max_run_speed)
      elsif args.inputs.keyboard.right # if right key is pressed
        args.state.player.dx += args.state.player_acceleration # dx increases by acceleration (player goes right)
        # dx is either set to current dx or max run speed (which would be 10),
        # whichever has a lesser value
        args.state.player.dx = args.state.player.dx.lesser(args.state.player_max_run_speed)
      else
        args.state.player.dx *= args.state.player_speed_slowdown_rate # dx is scaled down
      end
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.space ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Entities - main.rb link

    # ./samples/04_physics_and_collisions/03_entities/app/main.rb
    =begin
    
     Reminders:
    
     - map: Ruby method used to transform data; used in arrays, hashes, and collections.
       Can be used to perform an action on every element of a collection, such as multiplying
       each element by 2 or declaring every element as a new entity.
    
     - reject: Removes elements from a collection if they meet certain requirements.
       For example, you can derive an array of odd numbers from an original array of
       numbers 1 through 10 by rejecting all elements that are even (or divisible by 2).
    
     - args.state.new_entity: Used when we want to create a new object, like a sprite or button.
       In this sample app, new_entity is used to define the properties of enemies and bullets.
       (Remember, you can use state to define ANY property and it will be retained across frames.)
    
     - args.outputs.labels: An array. The values generate a label on the screen.
       The parameters are [X, Y, TEXT, SIZE, ALIGN, RED, GREEN, BLUE, ALPHA, FONT STYLE]
    
     - ARRAY#intersect_rect?: Returns true or false depending on if the two rectangles intersect.
    
     - args.inputs.mouse.click.point.(x|y): The x and y location of the mouse.
    
    =end
    
    # This sample app shows enemies that contain an id value and the time they were created.
    # These enemies can be removed by shooting at them with bullets.
    
    # Calls all methods necessary for the game to function properly.
    def tick args
      tick_instructions args, "Sample app shows how to use args.state.new_entity along with collisions. CLICK to shoot a bullet."
      defaults args
      render args
      calc args
      process_inputs args
    end
    
    # Sets default values
    # Enemies and bullets start off as empty collections
    def defaults args
      args.state.enemies ||= []
      args.state.bullets ||= []
    end
    
    # Provides each enemy in enemies collection with rectangular border,
    # as well as a label showing id and when they were created
    def render args
      # When you're calling a method that takes no arguments, you can use this & syntax on map.
      # Numbers are being added to x and y in order to keep the text within the enemy's borders.
      args.outputs.borders << args.state.enemies.map(&:rect)
      args.outputs.labels  << args.state.enemies.flat_map do |enemy|
        [
          [enemy.x + 4, enemy.y + 29, "id: #{enemy.entity_id}", -3, 0],
          [enemy.x + 4, enemy.y + 17, "created_at: #{enemy.created_at}", -3, 0] # frame enemy was created
        ]
      end
    
      # Outputs bullets in bullets collection as rectangular solids
      args.outputs.solids << args.state.bullets.map(&:rect)
    end
    
    # Calls all methods necessary for performing calculations
    def calc args
      add_new_enemies_if_needed args
      move_bullets args
      calculate_collisions args
      remove_bullets_of_screen args
    end
    
    # Adds enemies to the enemies collection and sets their values
    def add_new_enemies_if_needed args
      return if args.state.enemies.length >= 10 # if 10 or more enemies, enemies are not added
      return unless args.state.bullets.length == 0 # if user has not yet shot bullet, no enemies are added
    
      args.state.enemies += (10 - args.state.enemies.length).map do # adds enemies so there are 10 total
        args.state.new_entity(:enemy) do |e| # each enemy is declared as a new entity
          e.x = 640 + 500 * rand # each enemy is given random position on screen
          e.y = 600 * rand + 50
          e.rect = [e.x, e.y, 130, 30] # sets definition for enemy's rect
        end
      end
    end
    
    # Moves bullets across screen
    # Sets definition of the bullets
    def move_bullets args
      args.state.bullets.each do |bullet| # perform action on each bullet in collection
        bullet.x += bullet.speed # increment x by speed (bullets fly horizontally across screen)
    
        # By randomizing the value that increments bullet.y, the bullet does not fly straight up and out
        # of the scope of the screen. Try removing what follows bullet.speed, or changing 0.25 to 1.25 to
        # see what happens to the bullet's movement.
        bullet.y += bullet.speed.*(0.25).randomize(:ratio, :sign)
        bullet.rect = [bullet.x, bullet.y, bullet.size, bullet.size] # sets definition of bullet's rect
      end
    end
    
    # Determines if a bullet hits an enemy
    def calculate_collisions args
      args.state.bullets.each do |bullet| # perform action on every bullet and enemy in collections
        args.state.enemies.each do |enemy|
          # if bullet has not exploded yet and the bullet hits an enemy
          if !bullet.exploded && bullet.rect.intersect_rect?(enemy.rect)
            bullet.exploded = true # bullet explodes
            enemy.dead = true # enemy is killed
          end
        end
      end
    
      # All exploded bullets are rejected or removed from the bullets collection
      # and any dead enemy is rejected from the enemies collection.
      args.state.bullets = args.state.bullets.reject(&:exploded)
      args.state.enemies = args.state.enemies.reject(&:dead)
    end
    
    # Bullets are rejected from bullets collection once their position exceeds the width of screen
    def remove_bullets_of_screen args
      args.state.bullets = args.state.bullets.reject { |bullet| bullet.x > 1280 } # screen width is 1280
    end
    
    # Calls fire_bullet method
    def process_inputs args
      fire_bullet args
    end
    
    # Once mouse is clicked by the user to fire a bullet, a new bullet is added to bullets collection
    def fire_bullet args
      return unless args.inputs.mouse.click # return unless mouse is clicked
      args.state.bullets << args.state.new_entity(:bullet) do |bullet| # new bullet is declared a new entity
        bullet.y = args.inputs.mouse.click.point.y # set to the y value of where the mouse was clicked
        bullet.x = 0 # starts on the left side of the screen
        bullet.size = 10
        bullet.speed = 10 * rand + 2 # speed of a bullet is randomized
        bullet.rect = [bullet.x, bullet.y, bullet.size, bullet.size] # definition is set
      end
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.space ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Box Collision - main.rb link

    # ./samples/04_physics_and_collisions/04_box_collision/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - first: Returns the first element of the array.
       For example, if we have an array
       numbers = [1, 2, 3, 4, 5]
       and we call first by saying
       numbers.first
       the number 1 will be returned because it is the first element of the numbers array.
    
     - num1.idiv(num2): Divides two numbers and returns an integer.
       For example,
       16.idiv(3) = 5, because 16 / 3 is 5.33333 returned as an integer.
       16.idiv(4) = 4, because 16 / 4 is 4 and already has no decimal.
    
     Reminders:
    
     - find_all: Finds all values that satisfy specific requirements.
    
     - ARRAY#intersect_rect?: An array with at least four values is
       considered a rect. The intersect_rect? function returns true
       or false depending on if the two rectangles intersect.
    
     - reject: Removes elements from a collection if they meet certain requirements.
    
    =end
    
    # This sample app allows users to create tiles and place them anywhere on the screen as obstacles.
    # The player can then move and maneuver around them.
    
    class PoorManPlatformerPhysics
      attr_accessor :grid, :inputs, :state, :outputs
    
      # Calls all methods necessary for the app to run successfully.
      def tick
        defaults
        render
        calc
        process_inputs
      end
    
      # Sets default values for variables.
      # The ||= sign means that the variable will only be set to the value following the = sign if the value has
      # not already been set before. Intialization happens only in the first frame.
      def defaults
        state.tile_size               = 64
        state.gravity                 = -0.2
        state.previous_tile_size    ||= state.tile_size
        state.x                     ||= 0
        state.y                     ||= 800
        state.dy                    ||= 0
        state.dx                    ||= 0
        state.world                 ||= []
        state.world_lookup          ||= {}
        state.world_collision_rects ||= []
      end
    
      # Outputs solids and borders of different colors for the world and collision_rects collections.
      def render
    
        # Sets a black background on the screen (Comment this line out and the background will become white.)
        # Also note that black is the default color for when no color is assigned.
        outputs.solids << grid.rect
    
        # The position, size, and color (white) are set for borders given to the world collection.
        # Try changing the color by assigning different numbers (between 0 and 255) to the last three parameters.
        outputs.borders << state.world.map do |x, y|
          [x * state.tile_size,
           y * state.tile_size,
           state.tile_size,
           state.tile_size, 255, 255, 255]
        end
    
        # The top, bottom, and sides of the borders for collision_rects are different colors.
        outputs.borders << state.world_collision_rects.map do |e|
          [
            [e[:top],                             0, 170,   0], # top is a shade of green
            [e[:bottom],                          0, 100, 170], # bottom is a shade of greenish-blue
            [e[:left_right],                    170,   0,   0], # left and right are a shade of red
          ]
        end
    
        # Sets the position, size, and color (a shade of green) of the borders of only the player's
        # box and outputs it. If you change the 180 to 0, the player's box will be black and you
        # won't be able to see it (because it will match the black background).
        outputs.borders << [state.x,
                            state.y,
                            state.tile_size,
                            state.tile_size,  0, 180, 0]
      end
    
      # Calls methods needed to perform calculations.
      def calc
        calc_world_lookup
        calc_player
      end
    
      # Performs calculations on world_lookup and sets values.
      def calc_world_lookup
    
        # If the tile size isn't equal to the previous tile size,
        # the previous tile size is set to the tile size,
        # and world_lookup hash is set to empty.
        if state.tile_size != state.previous_tile_size
          state.previous_tile_size = state.tile_size
          state.world_lookup = {} # empty hash
        end
    
        # return if the world_lookup hash has keys (or, in other words, is not empty)
        # return unless the world collection has values inside of it (or is not empty)
        return if state.world_lookup.keys.length > 0
        return unless state.world.length > 0
    
        # Starts with an empty hash for world_lookup.
        # Searches through the world and finds the coordinates that exist.
        state.world_lookup = {}
        state.world.each { |x, y| state.world_lookup[[x, y]] = true }
    
        # Assigns world_collision_rects for every sprite drawn.
        state.world_collision_rects =
          state.world_lookup
              .keys
              .map do |coord_x, coord_y|
                s = state.tile_size
                # multiply by tile size so the grid coordinates; sets pixel value
                # don't forget that position is denoted by bottom left corner
                # set x = coord_x or y = coord_y and see what happens!
                x = s * coord_x
                y = s * coord_y
                {
                  # The values added to x, y, and s position the world_collision_rects so they all appear
                  # stacked (on top of world rects) but don't directly overlap.
                  # Remove these added values and mess around with the rect placement!
                  args:       [coord_x, coord_y],
                  left_right: [x,     y + 4, s,     s - 6], # hash keys and values
                  top:        [x + 4, y + 6, s - 8, s - 6],
                  bottom:     [x + 1, y - 1, s - 2, s - 8],
                }
              end
      end
    
      # Performs calculations to change the x and y values of the player's box.
      def calc_player
    
        # Since acceleration is the change in velocity, the change in y (dy) increases every frame.
        # What goes up must come down because of gravity.
        state.dy += state.gravity
    
        # Calls the calc_box_collision and calc_edge_collision methods.
        calc_box_collision
        calc_edge_collision
    
        # Since velocity is the change in position, the change in y increases by dy. Same with x and dx.
        state.y += state.dy
        state.x += state.dx
    
        # Scales dx down.
        state.dx *= 0.8
      end
    
      # Calls methods needed to determine collisions between player and world_collision rects.
      def calc_box_collision
        return unless state.world_lookup.keys.length > 0 # return unless hash has atleast 1 key
        collision_floor!
        collision_left!
        collision_right!
        collision_ceiling!
      end
    
      # Finds collisions between the bottom of the player's rect and the top of a world_collision_rect.
      def collision_floor!
        return unless state.dy <= 0 # return unless player is going down or is as far down as possible
        player_rect = [state.x, state.y - 0.1, state.tile_size, state.tile_size] # definition of player
    
        # Goes through world_collision_rects to find all intersections between the bottom of player's rect and
        # the top of a world_collision_rect (hence the "-0.1" above)
        floor_collisions = state.world_collision_rects
                               .find_all { |r| r[:top].intersect_rect?(player_rect, collision_tollerance) }
                               .first
    
        return unless floor_collisions # return unless collision occurred
        state.y = floor_collisions[:top].top # player's y is set to the y of the top of the collided rect
        state.dy = 0 # if a collision occurred, the player's rect isn't moving because its path is blocked
      end
    
      # Finds collisions between the player's left side and the right side of a world_collision_rect.
      def collision_left!
        return unless state.dx < 0 # return unless player is moving left
        player_rect = [state.x - 0.1, state.y, state.tile_size, state.tile_size]
    
        # Goes through world_collision_rects to find all intersections beween the player's left side and the
        # right side of a world_collision_rect.
        left_side_collisions = state.world_collision_rects
                                   .find_all { |r| r[:left_right].intersect_rect?(player_rect, collision_tollerance) }
                                   .first
    
        return unless left_side_collisions # return unless collision occurred
    
        # player's x is set to the value of the x of the collided rect's right side
        state.x = left_side_collisions[:left_right].right
        state.dx = 0 # player isn't moving left because its path is blocked
      end
    
      # Finds collisions between the right side of the player and the left side of a world_collision_rect.
      def collision_right!
        return unless state.dx > 0 # return unless player is moving right
        player_rect = [state.x + 0.1, state.y, state.tile_size, state.tile_size]
    
        # Goes through world_collision_rects to find all intersections between the player's right side
        # and the left side of a world_collision_rect (hence the "+0.1" above)
        right_side_collisions = state.world_collision_rects
                                    .find_all { |r| r[:left_right].intersect_rect?(player_rect, collision_tollerance) }
                                    .first
    
        return unless right_side_collisions # return unless collision occurred
    
        # player's x is set to the value of the collided rect's left, minus the size of a rect
        # tile size is subtracted because player's position is denoted by bottom left corner
        state.x = right_side_collisions[:left_right].left - state.tile_size
        state.dx = 0 # player isn't moving right because its path is blocked
      end
    
      # Finds collisions between the top of the player's rect and the bottom of a world_collision_rect.
      def collision_ceiling!
        return unless state.dy > 0 # return unless player is moving up
        player_rect = [state.x, state.y + 0.1, state.tile_size, state.tile_size]
    
        # Goes through world_collision_rects to find intersections between the bottom of a
        # world_collision_rect and the top of the player's rect (hence the "+0.1" above)
        ceil_collisions = state.world_collision_rects
                              .find_all { |r| r[:bottom].intersect_rect?(player_rect, collision_tollerance) }
                              .first
    
        return unless ceil_collisions # return unless collision occurred
    
        # player's y is set to the bottom y of the rect it collided with, minus the size of a rect
        state.y = ceil_collisions[:bottom].y - state.tile_size
        state.dy = 0 # if a collision occurred, the player isn't moving up because its path is blocked
      end
    
      # Makes sure the player remains within the screen's dimensions.
      def calc_edge_collision
    
        #Ensures that the player doesn't fall below the map.
        if state.y < 0
          state.y = 0
          state.dy = 0
    
        #Ensures that the player doesn't go too high.
        # Position of player is denoted by bottom left hand corner, which is why we have to subtract the
        # size of the player's box (so it remains visible on the screen)
        elsif state.y > 720 - state.tile_size # if the player's y position exceeds the height of screen
          state.y = 720 - state.tile_size # the player will remain as high as possible while staying on screen
          state.dy = 0
        end
    
        # Ensures that the player remains in the horizontal range that it is supposed to.
        if state.x >= 1280 - state.tile_size && state.dx > 0 # if player moves too far right
          state.x = 1280 - state.tile_size # player will remain as right as possible while staying on screen
          state.dx = 0
        elsif state.x <= 0 && state.dx < 0 # if player moves too far left
          state.x = 0 # player will remain as left as possible while remaining on screen
          state.dx = 0
        end
      end
    
      # Processes input from the user on the keyboard.
      def process_inputs
        if inputs.mouse.down
          state.world_lookup = {}
          x, y = to_coord inputs.mouse.down.point  # gets x, y coordinates for the grid
    
          if state.world.any? { |loc| loc == [x, y] }  # checks if coordinates duplicate
            state.world = state.world.reject { |loc| loc == [x, y] }  # erases tile space
          else
            state.world << [x, y] # If no duplicates, adds to world collection
          end
        end
    
        # Sets dx to 0 if the player lets go of arrow keys.
        if inputs.keyboard.key_up.right
          state.dx = 0
        elsif inputs.keyboard.key_up.left
          state.dx = 0
        end
    
        # Sets dx to 3 in whatever direction the player chooses.
        if inputs.keyboard.key_held.right # if right key is pressed
          state.dx =  3
        elsif inputs.keyboard.key_held.left # if left key is pressed
          state.dx = -3
        end
    
        #Sets dy to 5 to make the player ~fly~ when they press the space bar
        if inputs.keyboard.key_held.space
          state.dy = 5
        end
      end
    
      def to_coord point
    
        # Integer divides (idiv) point.x to turn into grid
        # Then, you can just multiply each integer by state.tile_size later so the grid coordinates.
        [point.x.idiv(state.tile_size), point.y.idiv(state.tile_size)]
      end
    
      # Represents the tolerance for a collision between the player's rect and another rect.
      def collision_tollerance
        0.0
      end
    end
    
    $platformer_physics = PoorManPlatformerPhysics.new
    
    def tick args
      $platformer_physics.grid    = args.grid
      $platformer_physics.inputs  = args.inputs
      $platformer_physics.state    = args.state
      $platformer_physics.outputs = args.outputs
      $platformer_physics.tick
      tick_instructions args, "Sample app shows platformer collisions. CLICK to place box. ARROW keys to move around. SPACE to jump."
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Box Collision 2 - main.rb link

    # ./samples/04_physics_and_collisions/05_box_collision_2/app/main.rb
    =begin
     APIs listing that haven't been encountered in previous sample apps:
    
     - times: Performs an action a specific number of times.
       For example, if we said
       5.times puts "Hello DragonRuby",
       then we'd see the words "Hello DragonRuby" printed on the console 5 times.
    
     - split: Divides a string into substrings based on a delimiter.
       For example, if we had a command
       "DragonRuby is awesome".split(" ")
       then the result would be
       ["DragonRuby", "is", "awesome"] because the words are separated by a space delimiter.
    
     - join: Opposite of split; converts each element of array to a string separated by delimiter.
       For example, if we had a command
       ["DragonRuby","is","awesome"].join(" ")
       then the result would be
       "DragonRuby is awesome".
    
     Reminders:
    
     - to_s: Returns a string representation of an object.
       For example, if we had
       500.to_s
       the string "500" would be returned.
       Similar to to_i, which returns an integer representation of an object.
    
     - elapsed_time: How many frames have passed since the click event.
    
     - args.outputs.labels: An array. Values in the array generate labels on the screen.
       The parameters are: [X, Y, TEXT, SIZE, ALIGN, RED, GREEN, BLUE, ALPHA, FONT STYLE]
       For more information about labels, go to mygame/documentation/02-labels.md.
    
     - inputs.mouse.down: Determines whether or not the mouse is being pressed down.
       The position of the mouse when it is pressed down can be found using inputs.mouse.down.point.(x|y).
    
     - first: Returns the first element of the array.
    
     - num1.idiv(num2): Divides two numbers and returns an integer.
    
     - find_all: Finds all values that satisfy specific requirements.
    
     - ARRAY#intersect_rect?: Returns true or false depending on if two rectangles intersect.
    
     - reject: Removes elements from a collection if they meet certain requirements.
    
     - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated
       as Ruby code, and the placeholder is replaced with its corresponding value or result.
    
    =end
    
    MAP_FILE_PATH = 'app/map.txt' # the map.txt file in the app folder contains exported map
    
    class MetroidvaniaStarter
      attr_accessor :grid, :inputs, :state, :outputs, :gtk
    
      # Calls methods needed to run the game properly.
      def tick
        defaults
        render
        calc
        process_inputs
      end
    
      # Sets all the default variables.
      # '||' states that initialization occurs only in the first frame.
      def defaults
        state.tile_size                = 64
        state.gravity                  = -0.2
        state.player_width             = 60
        state.player_height            = 64
        state.collision_tolerance      = 0.0
        state.previous_tile_size     ||= state.tile_size
        state.x                      ||= 0
        state.y                      ||= 800
        state.dy                     ||= 0
        state.dx                     ||= 0
        attempt_load_world_from_file
        state.world_lookup           ||= { }
        state.world_collision_rects  ||= []
        state.mode                   ||= :creating # alternates between :creating and :selecting for sprite selection
        state.select_menu            ||= [0, 720, 1280, 720]
        #=======================================IMPORTANT=======================================#
        # When adding sprites, please label them "image1.png", "image2.png", image3".png", etc.
        # Once you have done that, adjust "state.sprite_quantity" to how many sprites you have.
        #=======================================================================================#
        state.sprite_quantity        ||= 20 # IMPORTANT TO ALTER IF SPRITES ADDED IF YOU ADD MORE SPRITES
        state.sprite_coords          ||= []
        state.banner_coords          ||= [640, 680 + 720]
        state.sprite_selected        ||= 1
        state.map_saved_at           ||= 0
    
        # Sets all the cordinate values for the sprite selection screen into a grid
        # Displayed when 's' is pressed by player to access sprites
        if state.sprite_coords == [] # if sprite_coords is an empty array
          count = 1
          temp_x = 165 # sets a starting x and y position for display
          temp_y = 500 + 720
          state.sprite_quantity.times do # for the number of sprites you have
            state.sprite_coords += [[temp_x, temp_y, count]] # add element to sprite_coords array
            temp_x += 100 # increment temp_x
            count += 1 # increment count
            if temp_x > 1280 - (165 + 50) # if exceeding specific horizontal width on screen
              temp_x = 165 # a new row of sprites starts
              temp_y -= 75 # new row of sprites starts 75 units lower than the previous row
            end
          end
        end
      end
    
      # Places sprites
      def render
    
        # Sets the x, y, width, height, and image path for each sprite in the world collection.
        outputs.sprites << state.world.map do |x, y, sprite|
          [x * state.tile_size, # multiply by size so grid coordinates; pixel value of location
           y * state.tile_size,
           state.tile_size,
           state.tile_size,
           'sprites/image' + sprite.to_s + '.png'] # uses concatenation to create unique image path
        end
    
        # Outputs sprite for the player by setting x, y, width, height, and image path
        outputs.sprites << [state.x,
                            state.y,
                            state.player_width,
                            state.player_height,'sprites/player.png']
    
        # Outputs labels as primitives in top right of the screen
        outputs.primitives << [920, 700, 'Press \'s\' to access sprites.', 1, 0].label
        outputs.primitives << [920, 675, 'Click existing sprite to delete.', 1, 0].label
    
        outputs.primitives << [920, 640, '<- and -> to move.', 1, 0].label
        outputs.primitives << [920, 615, 'Press and hold space to jump.', 1, 0].label
    
        outputs.primitives << [920, 580, 'Press \'e\' to export current map.', 1, 0].label
    
        # if the map is saved and less than 120 frames have passed, the label is displayed
        if state.map_saved_at > 0 && state.map_saved_at.elapsed_time < 120
          outputs.primitives << [920, 555, 'Map has been exported!', 1, 0, 50, 100, 50].label
        end
    
        # If player hits 's', following appears
        if state.mode == :selecting
          # White background for sprite selection
          outputs.primitives << [state.select_menu, 255, 255, 255].solid
    
          # Select tile label at the top of the screen
          outputs.primitives << [state.banner_coords.x, state.banner_coords.y, "Select Sprite (sprites located in \"sprites\" folder)", 10, 1, 0, 0, 0, 255].label
    
          # Places sprites in locations calculated in the defaults function
          outputs.primitives << state.sprite_coords.map do |x, y, order|
            [x, y, 50, 50, 'sprites/image' + order.to_s + ".png"].sprite
          end
        end
    
        # Creates sprite following mouse to help indicate which sprite you have selected
        # 10 is subtracted from the mouse's x position so that the sprite is not covered by the mouse icon
        outputs.primitives << [inputs.mouse.position.x - 10, inputs.mouse.position.y,
                               10, 10, 'sprites/image' + state.sprite_selected.to_s + ".png"].sprite
      end
    
      # Calls methods that perform calculations
      def calc
        calc_in_game
        calc_sprite_selection
      end
    
      # Calls methods that perform calculations (if in creating mode)
      def calc_in_game
        return unless state.mode == :creating
        calc_world_lookup
        calc_player
      end
    
      def calc_world_lookup
        # If the tile size isn't equal to the previous tile size,
        # the previous tile size is set to the tile size,
        # and world_lookup hash is set to empty.
        if state.tile_size != state.previous_tile_size
          state.previous_tile_size = state.tile_size
          state.world_lookup = {}
        end
    
        # return if world_lookup is not empty or if world is empty
        return if state.world_lookup.keys.length > 0
        return unless state.world.length > 0
    
        # Searches through the world and finds the coordinates that exist
        state.world_lookup = {}
        state.world.each { |x, y| state.world_lookup[[x, y]] = true }
    
        # Assigns collision rects for every sprite drawn
        state.world_collision_rects =
          state.world_lookup
               .keys
               .map do |coord_x, coord_y|
                 s = state.tile_size
                 # Multiplying by s (the size of a tile) ensures that the rect is
                 # placed exactly where you want it to be placed (causes grid to coordinate)
                 # How many pixels horizontally across and vertically up and down
                 x = s * coord_x
                 y = s * coord_y
                 {
                   args:       [coord_x, coord_y],
                   left_right: [x,     y + 4, s,     s - 6], # hash keys and values
                   top:        [x + 4, y + 6, s - 8, s - 6],
                   bottom:     [x + 1, y - 1, s - 2, s - 8],
                 }
               end
      end
    
      # Calculates movement of player and calls methods that perform collision calculations
      def calc_player
        state.dy += state.gravity  # what goes up must come down because of gravity
        calc_box_collision
        calc_edge_collision
        state.y  += state.dy       # Since velocity is the change in position, the change in y increases by dy
        state.x  += state.dx       # Ditto line above but dx and x
        state.dx *= 0.8            # Scales dx down
      end
    
      # Calls methods that determine whether the player collides with any world_collision_rects.
      def calc_box_collision
        return unless state.world_lookup.keys.length > 0 # return unless hash has atleast 1 key
        collision_floor
        collision_left
        collision_right
        collision_ceiling
      end
    
      # Finds collisions between the bottom of the player's rect and the top of a world_collision_rect.
      def collision_floor
        return unless state.dy <= 0 # return unless player is going down or is as far down as possible
        player_rect = [state.x, next_y, state.tile_size, state.tile_size] # definition of player
    
        # Runs through all the sprites on the field and finds all intersections between player's
        # bottom and the top of a rect.
        floor_collisions = state.world_collision_rects
                             .find_all { |r| r[:top].intersect_rect?(player_rect, state.collision_tolerance) }
                             .first
    
        return unless floor_collisions # performs following changes if a collision has occurred
        state.y = floor_collisions[:top].top # y of player is set to the y of the colliding rect's top
        state.dy = 0 # no change in y because the player's path is blocked
      end
    
      # Finds collisions between the player's left side and the right side of a world_collision_rect.
      def collision_left
        return unless state.dx < 0 # return unless player is moving left
        player_rect = [next_x, state.y, state.tile_size, state.tile_size]
    
        # Runs through all the sprites on the field and finds all intersections between the player's left side
        # and the right side of a rect.
        left_side_collisions = state.world_collision_rects
                                 .find_all { |r| r[:left_right].intersect_rect?(player_rect, state.collision_tolerance) }
                                 .first
    
        return unless left_side_collisions # return unless collision occurred
        state.x = left_side_collisions[:left_right].right # sets player's x to the x of the colliding rect's right side
        state.dx = 0 # no change in x because the player's path is blocked
      end
    
      # Finds collisions between the right side of the player and the left side of a world_collision_rect.
      def collision_right
        return unless state.dx > 0 # return unless player is moving right
        player_rect = [next_x, state.y, state.tile_size, state.tile_size]
    
        # Runs through all the sprites on the field and finds all intersections between the  player's
        # right side and the left side of a rect.
        right_side_collisions = state.world_collision_rects
                                  .find_all { |r| r[:left_right].intersect_rect?(player_rect, state.collision_tolerance) }
                                  .first
    
        return unless right_side_collisions # return unless collision occurred
        state.x = right_side_collisions[:left_right].left - state.tile_size # player's x is set to the x of colliding rect's left side (minus tile size since x is the player's bottom left corner)
        state.dx = 0 # no change in x because the player's path is blocked
      end
    
      # Finds collisions between the top of the player's rect and the bottom of a world_collision_rect.
      def collision_ceiling
        return unless state.dy > 0 # return unless player is moving up
        player_rect = [state.x, next_y, state.player_width, state.player_height]
    
        # Runs through all the sprites on the field and finds all intersections between the player's top
        # and the bottom of a rect.
        ceil_collisions = state.world_collision_rects
                            .find_all { |r| r[:bottom].intersect_rect?(player_rect, state.collision_tolerance) }
                            .first
    
        return unless ceil_collisions # return unless collision occurred
        state.y = ceil_collisions[:bottom].y - state.tile_size # player's y is set to the y of the colliding rect's bottom (minus tile size)
        state.dy = 0 # no change in y because the player's path is blocked
      end
    
      # Makes sure the player remains within the screen's dimensions.
      def calc_edge_collision
        # Ensures that player doesn't fall below the map
        if next_y < 0 && state.dy < 0 # if player is moving down and is about to fall (next_y) below the map's scope
          state.y = 0 # 0 is the lowest the player can be while staying on the screen
          state.dy = 0
        # Ensures player doesn't go insanely high
        elsif next_y > 720 - state.tile_size && state.dy > 0 # if player is moving up, about to exceed map's scope
          state.y = 720 - state.tile_size # if we don't subtract tile_size, we won't be able to see the player on the screen
          state.dy = 0
        end
    
        # Ensures that player remains in the horizontal range its supposed to
        if state.x >= 1280 - state.tile_size && state.dx > 0 # if the player is moving too far right
          state.x = 1280 - state.tile_size # farthest right the player can be while remaining in the screen's scope
          state.dx = 0
        elsif state.x <= 0 && state.dx < 0 # if the player is moving too far left
          state.x = 0 # farthest left the player can be while remaining in the screen's scope
          state.dx = 0
        end
      end
    
      def calc_sprite_selection
        # Does the transition to bring down the select sprite screen
        if state.mode == :selecting && state.select_menu.y != 0
          state.select_menu.y = 0  # sets y position of select menu (shown when 's' is pressed)
          state.banner_coords.y = 680 # sets y position of Select Sprite banner
          state.sprite_coords = state.sprite_coords.map do |x, y, w, h|
            [x, y - 720, w, h] # sets definition of sprites (change '-' to '+' and the sprites can't be seen)
          end
        end
    
        # Does the transition to leave the select sprite screen
        if state.mode == :creating  && state.select_menu.y != 720
          state.select_menu.y = 720 # sets y position of select menu (menu is retreated back up)
          state.banner_coords.y = 1000 # sets y position of Select Sprite banner
          state.sprite_coords = state.sprite_coords.map do |x, y, w, h|
            [x, y + 720, w, h] # sets definition of all elements in collection
          end
        end
      end
    
      def process_inputs
        # If the state.mode is back and if the menu has retreated back up
        # call methods that process user inputs
        if state.mode == :creating
          process_inputs_player_movement
          process_inputs_place_tile
        end
    
        # For each sprite_coordinate added, check what sprite was selected
        if state.mode == :selecting
          state.sprite_coords.map do |x, y, order| # goes through all sprites in collection
            # checks that a specific sprite was pressed based on x, y position
            if inputs.mouse.down && # the && (and) sign means ALL statements must be true for the evaluation to be true
               inputs.mouse.down.point.x >= x      && # x is greater than or equal to sprite's x and
               inputs.mouse.down.point.x <= x + 50 && # x is less than or equal to 50 pixels to the right
               inputs.mouse.down.point.y >= y      && # y is greater than or equal to sprite's y
               inputs.mouse.down.point.y <= y + 50 # y is less than or equal to 50 pixels up
              state.sprite_selected = order # sprite is chosen
            end
          end
        end
    
        inputs_export_stage
        process_inputs_show_available_sprites
      end
    
      # Moves the player based on the keys they press on their keyboard
      def process_inputs_player_movement
        # Sets dx to 0 if the player lets go of arrow keys (player won't move left or right)
        if inputs.keyboard.key_up.right
          state.dx = 0
        elsif inputs.keyboard.key_up.left
          state.dx = 0
        end
    
        # Sets dx to 3 in whatever direction the player chooses when they hold down (or press) the left or right keys
        if inputs.keyboard.key_held.right
          state.dx =  3
        elsif inputs.keyboard.key_held.left
          state.dx = -3
        end
    
        # Sets dy to 5 to make the player ~fly~ when they press the space bar on their keyboard
        if inputs.keyboard.key_held.space
          state.dy = 5
        end
      end
    
      # Adds tile in the place the user holds down the mouse
      def process_inputs_place_tile
        if inputs.mouse.down # if mouse is pressed
          state.world_lookup = {}
          x, y = to_coord inputs.mouse.down.point # gets x, y coordinates for the grid
    
          # Checks if any coordinates duplicate (already exist in world)
          if state.world.any? { |existing_x, existing_y, n| existing_x == x && existing_y == y }
            #erases existing tile space by rejecting them from world
            state.world = state.world.reject do |existing_x, existing_y, n|
              existing_x == x && existing_y == y
            end
          else
            state.world << [x, y, state.sprite_selected] # If no duplicates, add the sprite
          end
        end
      end
    
      # Stores/exports world collection's info (coordinates, sprite number) into a file
      def inputs_export_stage
        if inputs.keyboard.key_down.e # if "e" is pressed
          export_string = state.world.map do |x, y, sprite_number| # stores world info in a string
            "#{x},#{y},#{sprite_number}"                           # using string interpolation
          end
          gtk.write_file(MAP_FILE_PATH, export_string.join("\n")) # writes string into a file
          state.map_saved_at = Kernel.tick_count # frame number (passage of time) when the map was saved
        end
      end
    
      def process_inputs_show_available_sprites
        # Based on keyboard input, the entity (:creating and :selecting) switch
        if inputs.keyboard.key_held.s && state.mode == :creating # if "s" is pressed and currently creating
          state.mode = :selecting # will change to selecting
          inputs.keyboard.clear # VERY IMPORTANT! If not present, it'll flicker between on and off
        elsif inputs.keyboard.key_held.s && state.mode == :selecting # if "s" is pressed and currently selecting
          state.mode = :creating # will change to creating
          inputs.keyboard.clear # VERY IMPORTANT! If not present, it'll flicker between on and off
        end
      end
    
      # Loads the world collection by reading from the map.txt file in the app folder
      def attempt_load_world_from_file
        return if state.world # return if the world collection is already populated
        state.world ||= [] # initialized as an empty collection
        exported_world = gtk.read_file(MAP_FILE_PATH) # reads the file using the path mentioned at top of code
        return unless exported_world # return unless the file read was successful
        state.world = exported_world.each_line.map do |l| # perform action on each line of exported_world
            l.split(',').map(&:to_i) # calls split using ',' as a delimiter, and invokes .map on the collection,
                                     # calling to_i (converts to integers) on each element
        end
      end
    
      # Adds the change in y to y to determine the next y position of the player.
      def next_y
        state.y + state.dy
      end
    
      # Determines next x position of player
      def next_x
        if state.dx < 0 # if the player moves left
          return state.x - (state.tile_size - state.player_width) # subtracts since the change in x is negative (player is moving left)
        else
          return state.x + (state.tile_size - state.player_width) # adds since the change in x is positive (player is moving right)
        end
      end
    
      def to_coord point
        # Integer divides (idiv) point.x to turn into grid
        # Then, you can just multiply each integer by state.tile_size
        # later and huzzah. Grid coordinates
        [point.x.idiv(state.tile_size), point.y.idiv(state.tile_size)]
      end
    end
    
    $metroidvania_starter = MetroidvaniaStarter.new
    
    def tick args
        $metroidvania_starter.grid    = args.grid
        $metroidvania_starter.inputs  = args.inputs
        $metroidvania_starter.state   = args.state
        $metroidvania_starter.outputs = args.outputs
        $metroidvania_starter.gtk     = GTK
        $metroidvania_starter.tick
    end
    
    

    Box Collision 3 - main.rb link

    # ./samples/04_physics_and_collisions/06_box_collision_3/app/main.rb
    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 = Kernel.tick_count
        elsif inputs.keyboard.key_down.space
          player.dy             = player.jump_power
          player.jumped_at      = Kernel.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.prev_rect.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.prev_rect.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.dx  = player.dx.clamp(-5,  5)
        player.dx *= 0.9
        player.x  += player.dx
      end
    
      def calc_player_dy
        player.y  += player.dy
        player.dy += state.gravity
        player.dy += player.dy * state.drag ** 2 * -1
      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
    
    

    Jump Physics - main.rb link

    # ./samples/04_physics_and_collisions/07_jump_physics/app/main.rb
    =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
    
      def init_game
        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  = Kernel.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
    
      # Sets default values
      def defaults
        init_game
      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
          # game over
          s.as_hash.clear # otherwise clear the hash (no new platform is necessary)
          init_game
        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
    
    

    Bouncing On Collision - ball.rb link

    # ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/ball.rb
    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 = 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
    
    

    Bouncing On Collision - block.rb link

    # ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/block.rb
    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
    
    

    Bouncing On Collision - cannon.rb link

    # ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/cannon.rb
    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
    
    

    Bouncing On Collision - main.rb link

    # ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/main.rb
    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
    
    

    Bouncing On Collision - peg.rb link

    # ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/peg.rb
    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
    
    

    Bouncing On Collision - vector2d.rb link

    # ./samples/04_physics_and_collisions/08_bouncing_on_collision/app/vector2d.rb
    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
    
    

    Arbitrary Collision - ball.rb link

    # ./samples/04_physics_and_collisions/09_arbitrary_collision/app/ball.rb
    
    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
    
    

    Arbitrary Collision - blocks.rb link

    # ./samples/04_physics_and_collisions/09_arbitrary_collision/app/blocks.rb
    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, Kernel.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] = Kernel.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, Kernel.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
    
    

    Arbitrary Collision - linear_collider.rb link

    # ./samples/04_physics_and_collisions/09_arbitrary_collision/app/linear_collider.rb
    
    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 occurred
          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
    
    

    Arbitrary Collision - main.rb link

    # ./samples/04_physics_and_collisions/09_arbitrary_collision/app/main.rb
    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 ||= Kernel.tick_count
      args.state.ball_hypotenuse = (10**2 + 10**2)**0.5
      args.state.ballParents ||= []
    
      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 = Kernel.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, Grid.right, 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 << [Grid.right-(args.state.board_width + Grid.w / 8), 0, Grid.right, Grid.top, "sprites/square-white-2.png", 0, 255, bgClr[:r], bgClr[:g], bgClr[:b]]
      args.outputs.sprites << [0, 0, (args.state.board_width + Grid.w / 8), 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, 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, 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
    
    

    Arbitrary Collision - paddle.rb link

    # ./samples/04_physics_and_collisions/09_arbitrary_collision/app/paddle.rb
    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
    
    

    Arbitrary Collision - rectangle.rb link

    # ./samples/04_physics_and_collisions/09_arbitrary_collision/app/rectangle.rb
    class Rectangle
      def initialize args
    
        @image = "sprites/roundSquare_white.png"
        @width  = 160.0
        @height = 80.0
        @x=Grid.right/2.0 - @width/2.0
        @y=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
    
    

    Arbitrary Collision - square_collider.rb link

    # ./samples/04_physics_and_collisions/09_arbitrary_collision/app/square_collider.rb
    
    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
    
    

    Arbitrary Collision - vector2d.rb link

    # ./samples/04_physics_and_collisions/09_arbitrary_collision/app/vector2d.rb
    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
    

    Collision With Object Removal - ball.rb link

    # ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb
    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
        @[email protected]
        @[email protected]
      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
    
    

    Collision With Object Removal - linear_collider.rb link

    # ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb
    #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
    
    

    Collision With Object Removal - main.rb link

    # ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb
    # 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 = Kernel.tick_count
        end
    
        if args.state.game_over_at.elapsed_time < 60 && Kernel.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 = Kernel.tick_count
        end
    
        if args.state.game_over_at.elapsed_time < 60 && Kernel.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
    
    

    Collision With Object Removal - paddle.rb link

    # ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb
    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
    
    

    Collision With Object Removal - tests.rb link

    # ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb
    # 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
    
    

    Collision With Object Removal - vector2d.rb link

    # ./samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb
    
    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
    
    

    Bouncing Ball With Gravity - main.rb link

    # ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/app/main.rb
    class Game
      attr_gtk
    
      def tick
        outputs.labels << { x: 30, y: 30.from_top,
                            text: "left/right arrow keys to spin, up arrow to jump, ctrl+r to reset, click two points to place terrain" }
        defaults
        calc
        render
      end
    
      def defaults
        state.terrain ||= []
    
        state.player ||= { x: 100,
                           y: 640,
                           dx: 0,
                           dy: 0,
                           radius: 12,
                           drag: 0.05477,
                           gravity: 0.03,
                           entropy: 0.9,
                           angle: 0,
                           facing: 1,
                           angle_velocity: 0,
                           elasticity: 0.5 }
    
        state.grid_points ||= (1280.idiv(40) + 1).flat_map do |x|
          (720.idiv(40) + 1).map do |y|
            { x: x * 40,
              y: y * 40,
              w: 40,
              h: 40,
              anchor_x: 0.5,
              anchor_y: 0.5 }
          end
        end
      end
    
      def calc
        player.y = 720  if player.y < 0
        player.x = 1280 if player.x < 0
        player.x = 0    if player.x > 1280
        player.angle_velocity = player.angle_velocity.clamp(-30, 30)
        calc_edit_mode
        calc_play_mode
      end
    
      def calc_edit_mode
        state.current_grid_point = Geometry.find_intersect_rect(inputs.mouse, state.grid_points)
        calc_edit_mode_click
      end
    
      def calc_edit_mode_click
        return if !state.current_grid_point
        return if !inputs.mouse.click
    
        if !state.start_point
          state.start_point = state.current_grid_point
        else
          state.terrain << { x: state.start_point.x,
                             y: state.start_point.y,
                             x2: state.current_grid_point.x,
                             y2: state.current_grid_point.y }
          state.start_point = nil
        end
      end
    
      def calc_play_mode
        player.x += player.dx
        player.dy -= player.gravity
        player.y += player.dy
        player.angle += player.angle_velocity
        player.dy += player.dy * player.drag ** 2 * -1
        player.dx += player.dx * player.drag ** 2 * -1
        player.colliding = false
        player.colliding_with = nil
    
        if inputs.keyboard.key_down.up
          player.dy += 5 * player.angle.vector_y
          player.dx += 5 * player.angle.vector_x
        end
        player.angle_velocity += inputs.left_right * -1
        player.facing = if inputs.left_right == -1
                          -1
                        elsif inputs.left_right == 1
                          1
                        else
                          player.facing
                        end
    
        collisions = player_terrain_collisions
        collisions.each do |collision|
          collide! player, collision
        end
    
        if player.colliding_with
          roll! player, player.colliding_with
        end
      end
    
      def reflect_velocity! circle, line
        slope = Geometry.line_slope line, replace_infinity: 1000
        slope_angle = Geometry.line_angle line
        if slope_angle == 90 || slope_angle == 270
          circle.dx *= -circle.elasticity
        else
          circle.angle_velocity += slope * (circle.dx.abs + circle.dy.abs)
          vec = line.x2 - line.x, line.y2 - line.y
          len = Math.sqrt(vec.x**2 + vec.y**2)
    
          vec.x /= len
          vec.y /= len
    
          n = Geometry.vec2_normal vec
    
          v_dot_n = Geometry.vec2_dot_product({ x: circle.dx, y: circle.dy }, n)
    
          circle.dx = circle.dx - n.x * (2 * v_dot_n)
          circle.dy = circle.dy - n.y * (2 * v_dot_n)
          circle.dx *= circle.elasticity
          circle.dy *= circle.elasticity
          half_terminal_velocity = 10
          impact_intensity = (circle.dy.abs) / half_terminal_velocity
          impact_intensity = 1 if impact_intensity > 1
    
          final = (0.9 - 0.8 * impact_intensity)
          next_angular_velocity = circle.angle_velocity * final
          circle.angle_velocity *= final
    
          if (circle.dx.abs + circle.dy.abs) <= 0.2
            circle.dx = 0
            circle.dy = 0
            circle.angle_velocity *= 0.99
          end
    
          if circle.angle_velocity.abs <= 0.1
            circle.angle_velocity = 0
          end
        end
      end
    
      def position_on_line! circle, line
        circle.colliding = true
        point = Geometry.line_normal line, circle
        if point.y > circle.y
          circle.colliding_from_above = true
        else
          circle.colliding_from_above = false
        end
    
        circle.colliding_with = line
    
        if !Geometry.point_on_line? point, line
          distance_from_start_of_line = Geometry.distance_squared({ x: line.x, y: line.y }, point)
          distance_from_end_of_line = Geometry.distance_squared({ x: line.x2, y: line.y2 }, point)
          if distance_from_start_of_line < distance_from_end_of_line
            point = { x: line.x, y: line.y }
          else
            point = { x: line.x2, y: line.y2 }
          end
        end
        angle = Geometry.angle_to point, circle
        circle.y = point.y + angle.vector_y * (circle.radius)
        circle.x = point.x + angle.vector_x * (circle.radius)
      end
    
      def collide! circle, line
        return if !line
        position_on_line! circle, line
        reflect_velocity! circle, line
        next_player = { x: player.x + player.dx,
                        y: player.y + player.dy,
                        radius: player.radius }
      end
    
      def roll! circle, line
        slope_angle = Geometry.line_angle line
        return if slope_angle == 90 || slope_angle == 270
    
        ax = -circle.gravity * slope_angle.vector_y
        ay = -circle.gravity * slope_angle.vector_x
    
        if ax.abs < 0.05 && ay.abs < 0.05
          ax = 0
          ay = 0
        end
    
        friction_coefficient = 0.0001
        friction_force = friction_coefficient * circle.gravity * slope_angle.vector_x
    
        circle.dy += ay
        circle.dx += ax
    
        if circle.colliding_from_above
          circle.dx += circle.angle_velocity * slope_angle.vector_x * 0.1
          circle.dy += circle.angle_velocity * slope_angle.vector_y * 0.1
        else
          circle.dx += circle.angle_velocity * slope_angle.vector_x * -0.1
          circle.dy += circle.angle_velocity * slope_angle.vector_y * -0.1
        end
    
        if circle.dx != 0
          circle.dx -= friction_force * (circle.dx / circle.dx.abs)
        end
    
        if circle.dy != 0
          circle.dy -= friction_force * (circle.dy / circle.dy.abs)
        end
      end
    
      def player_terrain_collisions
        terrain.find_all do |terrain|
                 Geometry.circle_intersect_line? player, terrain
               end
               .sort_by do |terrain|
                 if player.facing == -1
                   -terrain.x
                 else
                   terrain.x
                 end
               end
      end
    
      def render
        render_current_grid_point
        render_preview_line
        render_grid_points
        render_terrain
        render_player
        render_player_terrain_collisions
      end
    
      def render_player_terrain_collisions
        collisions = player_terrain_collisions
        outputs.lines << collisions.map do |collision|
                           { x: collision.x,
                             y: collision.y,
                             x2: collision.x2,
                             y2: collision.y2,
                             r: 255,
                             g: 0,
                             b: 0 }
                         end
      end
    
      def render_current_grid_point
        return if state.game_mode == :play
        return if !state.current_grid_point
        outputs.sprites << state.current_grid_point
                                .merge(w: 8,
                                       h: 8,
                                       anchor_x: 0.5,
                                       anchor_y: 0.5,
                                       path: :solid,
                                       g: 0,
                                       r: 0,
                                       b: 0,
                                       a: 128)
      end
    
      def render_preview_line
        return if state.game_mode == :play
        return if !state.start_point
        return if !state.current_grid_point
    
        outputs.lines << { x: state.start_point.x,
                           y: state.start_point.y,
                           x2: state.current_grid_point.x,
                           y2: state.current_grid_point.y }
      end
    
      def render_grid_points
        outputs
          .sprites << state
                        .grid_points
                        .map do |point|
          point.merge w: 8,
                      h: 8,
                      anchor_x: 0.5,
                      anchor_y: 0.5,
                      path: :solid,
                      g: 255,
                      r: 255,
                      b: 255,
                      a: 128
        end
      end
    
      def render_terrain
        outputs.lines << state.terrain
      end
    
      def render_player
        outputs.sprites << player_prefab
      end
    
      def player_prefab
        flip_horizontally = player.facing == -1
        { x: player.x,
          y: player.y,
          w: player.radius * 2,
          h: player.radius * 2,
          angle: player.angle,
          anchor_x: 0.5,
          anchor_y: 0.5,
          path: "sprites/circle/blue.png" }
      end
    
      def player
        state.player
      end
    
      def terrain
        state.terrain
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    def reset args
      $terrain = args.state.terrain
      $game = nil
    end
    
    

    Ramp Collision - main.rb link

    # ./samples/04_physics_and_collisions/12_ramp_collision/app/main.rb
    # sample app shows how to do ramp collision
    # based off of the writeup here:
    # http://higherorderfun.com/blog/2012/05/20/the-guide-to-implementing-2d-platformers/
    
    # NOTE: at the bottom of the file you'll find GTK.reset_and_replay "replay.txt"
    #       whenever you make changes to this file, a replay will automatically run so you can
    #       see how your changes affected the game. Comment out the line at the bottom if you
    #       don't want the replay to autmatically run.
    def tick args
      tick_toolbar args
      tick_game args
    end
    
    def tick_game args
      game_defaults args
      game_input args
      game_calc args
      game_render args
    end
    
    def game_input args
      # if space is pressed or held (signifying a jump)
      if args.inputs.keyboard.space
        # change the player's dy to the jump power if the
        # player is not currently touching a ceiling
        if !args.state.player.on_ceiling
          args.state.player.dy = args.state.player.jump_power
          args.state.player.on_floor = false
          args.state.player.jumping = true
        end
      else
        # if the space key is released, then jumping is false
        # and the player will no longer be on the ceiling
        args.state.player.jumping = false
        args.state.player.on_ceiling = false
      end
    
      # set the player's dx value to the left/right input
      # NOTE: that the speed of the player's dx movement has
      #       a sensitive relation ship with collision detection.
      #       If you increase the speed of the player, you may
      #       need to tweak the collision code to compensate for
      #       the extra horizontal speed.
      args.state.player.dx = args.inputs.left_right * 2
    end
    
    def game_render args
      # for each terrain entry, render the line that represents the connection
      # from the tile's left_height to the tile's right_height
      args.outputs.primitives << args.state.terrain.map { |t| t.line }
    
      # determine if the player sprite needs to be flipped hoizontally
      flip_horizontally = args.state.player.facing == -1
    
      # render the player
      args.outputs.sprites << args.state.player.merge(flip_horizontally: flip_horizontally)
    
      args.outputs.labels << {
        x: 640,
        y: 100,
        alignment_enum: 1,
        text: "Left and Right to move player. Space to jump. Use the toolbar at the top to add more terrain."
      }
    
      args.outputs.labels << {
        x: 640,
        y: 60,
        alignment_enum: 1,
        text: "Click any existing terrain on the map to delete it."
      }
    end
    
    def game_calc args
      # set the direction the player is facing based on the
      # the dx value of the player
      if args.state.player.dx > 0
        args.state.player.facing = 1
      elsif args.state.player.dx < 0
        args.state.player.facing = -1
      end
    
      # preform the calcuation of ramp collision
      calc_collision args
    
      # reset the player if the go off screen
      calc_off_screen args
    end
    
    def game_defaults args
      # how much gravity is in the game
      args.state.gravity ||= 0.1
    
      # initialized the player to the center of the screen
      args.state.player ||= {
        x: 640,
        y: 360,
        w: 16,
        h: 16,
        dx: 0,
        dy: 0,
        jump_power: 3,
        path: 'sprites/square/blue.png',
        on_floor: false,
        on_ceiling: false,
        facing: 1
      }
    end
    
    def calc_collision args
      # increment the players x position by the dx value
      args.state.player.x += args.state.player.dx
    
      # if the player is not on the floor
      if !args.state.player.on_floor
        # then apply gravity
        args.state.player.dy -= args.state.gravity
        # clamp the max dy value to -12 to 12
        args.state.player.dy = args.state.player.dy.clamp(-12, 12)
    
        # update the player's y position by the dy value
        args.state.player.y += args.state.player.dy
      end
    
      # get all colisions between the player and the terrain
      collisions = Geometry.find_all_intersect_rect args.state.player, args.state.terrain
    
      # if there are no collisions, then the player is not on the floor or ceiling
      # return from the method since there is nothing more to process
      if collisions.length == 0
        args.state.player.on_floor = false
        args.state.player.on_ceiling = false
        return
      end
    
      # set a local variable to the player since
      # we'll be accessing it a lot
      player = args.state.player
    
      # sort the collisions by the distance from the collision's center to the player's center
      sorted_collisions = collisions.sort_by do |collision|
        player_center = player.x + player.w / 2
        collision_center = collision.x + collision.w / 2
        (player_center - collision_center).abs
      end
    
      # define a one pixel wide rectangle that represents the center of the player
      # we'll use this value to determine the location of the player's feet on
      # a ramp
      player_center_rect = {
        x: player.x + player.w / 2 - 0.5,
        y: player.y,
        w: 1,
        h: player.h
      }
    
      # for each collision...
      sorted_collisions.each do |collision|
        # if the player doesn't intersect with the collision,
        # then set the player's on_floor and on_ceiling values to false
        # and continue to the next collision
        if !collision.intersect_rect? player_center_rect
          player.on_floor = false
          player.on_ceiling = false
          next
        end
    
        if player.dy < 0
          # if the player is falling
          # the percentage of the player's center relative to the collision
          # is a difference from the collision to the player (as opposed to the player to the collision)
          perc = (collision.x - player_center_rect.x) / player.w
          height_of_slope = collision.tile.left_height - collision.tile.right_height
    
          new_y = (collision.y + collision.tile.left_height + height_of_slope * perc)
          diff = new_y - player.y
    
          if diff < 0
            # if the current fall rate of the player is less than the difference
            # of the player's new y position and the player's current y position
            # then don't set the player's y position to the new y position
            # and wait for another application of gravity to bring the player a little
            # closer
            if player.dy.abs >= diff.abs
              # if the player's current fall speed can cover the distance to the
              # new y position, then set the player's y position to the new y position
              # and mark them as being on the floor so that gravity no longer get's processed
              player.y = new_y
              player.on_floor = true
    
              # given the player's speed, set the player's dy to a value that will
              # keep them from bouncing off the floor when the ramp is steep
              # NOTE: if you change the player's speed, then this value will need to be adjusted
              #       to keep the player from bouncing off the floor
              player.dy = -1
            end
          elsif diff > 0 && diff < 8
            # there's a small edge case where collision may be processed from
            # below the terrain (eg when the player is jumping up and hitting the
            # ramp from below). The moment when jump is released, the player's dy
            # value could result in the player tunneling through the terrain,
            # and get popped on to the top side.
    
            # testing to make sure the distance that will be displaced is less than
            # 8 pixels will keep this tunneling from happening
            player.y = new_y
            player.on_floor = true
    
            # given the player's speed, set the player's dy to a value that will
            # keep them from bouncing off the floor when the ramp is steep
            # NOTE: if you change the player's speed, then this value will need to be adjusted
            #       to keep the player from bouncing off the floor
            player.dy = -1
          end
        elsif player.dy > 0
          # if the player is jumping
          # the percentage of the player's center relative to the collision
          # is a difference is reversed from the player to the collision (as opposed to the player to the collision)
          perc = (player_center_rect.x - collision.x) / player.w
    
          # the height of the slope is also reversed when approaching the collision from the bottom
          height_of_slope = collision.tile.right_height - collision.tile.left_height
    
          new_y = collision.y + collision.tile.left_height + height_of_slope * perc
    
          # since this collision is being processed from below, the difference
          # between the current players position and the new y position is
          # based off of the player's top position (their head)
          player_top = player.y + player.h
    
          diff = new_y - player_top
    
          # we also need to calculate the difference between the player's bottom
          # and the new position. This will be used to determine if the player
          # can jump from the new_y position
          diff_bottom = new_y - player.y
    
    
          # if the player's current rising speed can cover the distance to the
          # new y position, then set the player's y position to the new y position
          # an mark them as being on the floor so that gravity no longer get's processed
          can_cover_distance_to_new_y = player.dy >= diff.abs && player.dy.sign == diff.sign
    
          # another scenario that needs to be covered is if the player's top is already passed
          # the new_y position (their rising speed made them partially clip through the collision)
          player_top_above_new_y = player_top > new_y
    
          # if either of the conditions above is true then we want to set the player's y position
          if can_cover_distance_to_new_y || player_top_above_new_y
            # only set the player's y position to the new y position if the player's
            # cannot escape the collision by jumping up from the new_y position
            if diff_bottom >= player.jump_power
              player.y = new_y.floor - player.h
    
              # after setting the new_y position, we need to determine if the player
              # if the player is touching the ceiling or not
              # touching the ceiling disables the ability for the player to jump/increase
              # their dy value any more than it already is
              if player.jumping
                # disable jumping if the player is currently moving upwards
                player.on_ceiling = true
    
                # NOTE: if you change the player's speed, then this value will need to be adjusted
                #       to keep the player from bouncing off the ceiling as they move right and left
                player.dy = 1
              else
                # if the player is not currently jumping, then set their dy to 0
                # so they can immediately start falling after the collision
                # this also means that they are no longer on the ceiling and can jump again
                player.dy = 0
                player.on_ceiling = false
              end
            end
          end
        end
      end
    end
    
    def calc_off_screen args
      below_screen = args.state.player.y + args.state.player.h < 0
      above_screen = args.state.player.y > 720 + args.state.player.h
      off_screen_left = args.state.player.x + args.state.player.w < 0
      off_screen_right = args.state.player.x > 1280
    
      # if the player is off the screen, then reset them to the top of the screen
      if below_screen || above_screen || off_screen_left || off_screen_right
        args.state.player.x = 640
        args.state.player.y = 720
        args.state.player.dy = 0
        args.state.player.on_floor = false
      end
    end
    
    def tick_toolbar args
      # ================================================
      # tollbar defaults
      # ================================================
      if !args.state.toolbar
        # these are the tiles you can select from
        tile_definitions = [
          { name: "16-12", left_height: 16, right_height: 12  },
          { name: "12-8",  left_height: 12, right_height: 8   },
          { name: "8-4",   left_height: 8,  right_height: 4   },
          { name: "4-0",   left_height: 4,  right_height: 0   },
          { name: "0-4",   left_height: 0,  right_height: 4   },
          { name: "4-8",   left_height: 4,  right_height: 8   },
          { name: "8-12",  left_height: 8,  right_height: 12  },
          { name: "12-16", left_height: 12, right_height: 16  },
    
          { name: "16-8",  left_height: 16, right_height: 8   },
          { name: "8-0",   left_height: 8,  right_height: 0   },
          { name: "0-8",   left_height: 0,  right_height: 8   },
          { name: "8-16",  left_height: 8,  right_height: 16  },
    
          { name: "0-0",   left_height: 0,  right_height: 0   },
          { name: "8-8",   left_height: 8,  right_height: 8   },
          { name: "16-16", left_height: 16, right_height: 16  },
        ]
    
        # toolbar data representation which will be used to render the toolbar.
        # the buttons array will be used to render the buttons
        # the toolbar_rect will be used to restrict the creation of tiles
        # within the toolbar area
        args.state.toolbar = {
          toolbar_rect: nil,
          buttons: []
        }
    
        # for each tile definition, create a button
        args.state.toolbar.buttons = tile_definitions.map_with_index do |spec, index|
          left_height  = spec.left_height
          right_height = spec.right_height
          button_size  = 48
          column_size  = 15
          column_padding = 2
          column = index % column_size
          column_padding = column * column_padding
          margin = 10
          row = index.idiv(column_size)
          row_padding = row * 2
          x = margin + column_padding + (column * button_size)
          y = (margin + button_size + row_padding + (row * button_size)).from_top
    
          # when a tile is added, the data of this button will be used
          # to construct the terrain
    
          # each tile has an x, y, w, h which represents the bounding box
          # of the button.
          # the button also contains the left_height and right_height which is
          # important when determining collision of the ramps
          {
            name: spec.name,
            left_height: left_height,
            right_height: right_height,
            button_rect: {
              x: x,
              y: y,
              w: 48,
              h: 48
            }
          }
        end
    
        # with the buttons populated, compute the bounding box of the entire
        # toolbar (again this will be used to restrict the creation of tiles)
        min_x = args.state.toolbar.buttons.map { |t| t.button_rect.x }.min
        min_y = args.state.toolbar.buttons.map { |t| t.button_rect.y }.min
    
        max_x = args.state.toolbar.buttons.map { |t| t.button_rect.x }.max
        max_y = args.state.toolbar.buttons.map { |t| t.button_rect.y }.max
    
        args.state.toolbar.rect = {
          x: min_x - 10,
          y: min_y - 10,
          w: max_x - min_x + 10 + 64,
          h: max_y - min_y + 10 + 64
        }
      end
    
      # set the selected tile to the last button in the toolbar
      args.state.selected_tile ||= args.state.toolbar.buttons.last
    
      # ================================================
      # starting terrain generation
      # ================================================
      if !args.state.terrain
        world = [
          { row: 14, col: 25, name: "0-8"   },
          { row: 14, col: 26, name: "8-16"  },
          { row: 15, col: 27, name: "0-8"   },
          { row: 15, col: 28, name: "8-16"  },
          { row: 16, col: 29, name: "0-8"   },
          { row: 16, col: 30, name: "8-16"  },
          { row: 17, col: 31, name: "0-8"   },
          { row: 17, col: 32, name: "8-16"  },
          { row: 18, col: 33, name: "0-8"   },
          { row: 18, col: 34, name: "8-16"  },
          { row: 18, col: 35, name: "16-12" },
          { row: 18, col: 36, name: "12-8"  },
          { row: 18, col: 37, name: "8-4"   },
          { row: 18, col: 38, name: "4-0"   },
          { row: 18, col: 39, name: "0-0"   },
          { row: 18, col: 40, name: "0-0"   },
          { row: 18, col: 41, name: "0-0"   },
          { row: 18, col: 42, name: "0-4"   },
          { row: 18, col: 43, name: "4-8"   },
          { row: 18, col: 44, name: "8-12"  },
          { row: 18, col: 45, name: "12-16" },
        ]
    
        args.state.terrain = world.map do |tile|
          template = tile_by_name(args, tile.name)
          next if !template
          grid_rect = grid_rect_for(tile.row, tile.col)
          new_terrain_definition(grid_rect, template)
        end
      end
    
      # ================================================
      # toolbar input and rendering
      # ================================================
      # store the mouse position alligned to the tile grid
      mouse_grid_aligned_rect = grid_aligned_rect args.inputs.mouse, 16
    
      # determine if the mouse intersects the toolbar
      mouse_intersects_toolbar = args.state.toolbar.rect.intersect_rect? args.inputs.mouse
    
      # determine if the mouse intersects a toolbar button
      toolbar_button = args.state.toolbar.buttons.find { |t| t.button_rect.intersect_rect? args.inputs.mouse }
    
      # determine if the mouse click occurred over a tile in the terrain
      terrain_tile = Geometry.find_intersect_rect mouse_grid_aligned_rect, args.state.terrain
    
    
      # if a mouse click occurs....
      if args.inputs.mouse.click
        if toolbar_button
          # if a toolbar button was clicked, set the currently selected tile to the toolbar tile
          args.state.selected_tile = toolbar_button
        elsif terrain_tile
          # if a tile was clicked, delete it from the terrain
          args.state.terrain.delete terrain_tile
        elsif !args.state.toolbar.rect.intersect_rect? args.inputs.mouse
          # if the mouse was not clicked in the toolbar area
          # add a new terrain based off of the information in the selected tile
          args.state.terrain << new_terrain_definition(mouse_grid_aligned_rect, args.state.selected_tile)
        end
      end
    
      # render a light blue background for the toolbar button that is currently
      # being hovered over (if any)
      if toolbar_button
        args.outputs.primitives << toolbar_button.button_rect.merge(primitive_marker: :solid, a: 64, b: 255)
      end
    
      # put a blue background around the currently selected tile
      args.outputs.primitives << args.state.selected_tile.button_rect.merge(primitive_marker: :solid, b: 255, r: 128, a: 64)
    
      if !mouse_intersects_toolbar
        if terrain_tile
          # if the mouse is hoving over an existing terrain tile, render a red border around the
          # tile to signify that it will be deleted if the mouse is clicked
          args.outputs.borders << terrain_tile.merge(a: 255, r: 255)
        else
          # if the mouse is not hovering over an existing terrain tile, render the currently
          # selected tile at the mouse position
          grid_aligned_rect = grid_aligned_rect args.inputs.mouse, 16
    
          args.outputs.solids << {
            **grid_aligned_rect,
            a: 30,
            g: 128
          }
    
          args.outputs.lines << {
            x:  grid_aligned_rect.x,
            y:  grid_aligned_rect.y + args.state.selected_tile.left_height,
            x2: grid_aligned_rect.x + grid_aligned_rect.w,
            y2: grid_aligned_rect.y + args.state.selected_tile.right_height,
          }
        end
      end
    
      # render each toolbar button using two primitives, a border to denote
      # the click area of the button, and a line to denote the terrain that
      # will be created when the button is clicked
      args.outputs.primitives << args.state.toolbar.buttons.map do |toolbar_tile|
        primitives = []
        scale = toolbar_tile.button_rect.w / 16
    
        primitive_type = :border
    
        [
          {
            **toolbar_tile.button_rect,
            primitive_marker: primitive_type,
            a: 64,
            g: 128
          },
          {
            x:  toolbar_tile.button_rect.x,
            y:  toolbar_tile.button_rect.y + toolbar_tile.left_height * scale,
            x2: toolbar_tile.button_rect.x + toolbar_tile.button_rect.w,
            y2: toolbar_tile.button_rect.y + toolbar_tile.right_height * scale
          }
        ]
      end
    end
    
    # ================================================
    # helper methods
    #=================================================
    
    # converts a row and column on the grid to
    # a rect
    def grid_rect_for row, col
      { x: col * 16, y: row * 16, w: 16, h: 16 }
    end
    
    # find a tile by name
    def tile_by_name args, name
      args.state.toolbar.buttons.find { |b| b.name == name }
    end
    
    # data structure containing terrain information
    # specifcially tile.left_height and tile.right_height
    def new_terrain_definition grid_rect, tile
      grid_rect.merge(
        tile: tile,
        line: {
          x:  grid_rect.x,
          y:  grid_rect.y + tile.left_height,
          x2: grid_rect.x + grid_rect.w,
          y2: grid_rect.y + tile.right_height
        }
      )
    end
    
    # helper method that returns a grid aligned rect given
    # an arbitrary rect and a grid size
    def grid_aligned_rect point, size
      grid_aligned_x = point.x - (point.x % size)
      grid_aligned_y = point.y - (point.y % size)
      { x: grid_aligned_x.to_i, y: grid_aligned_y.to_i, w: size.to_i, h: size.to_i }
    end
    
    GTK.reset_and_replay "replay.txt", speed: 2
    
    

    Ramp Collision Simple - main.rb link

    # ./samples/04_physics_and_collisions/12_ramp_collision_simple/app/main.rb
    class Game
      attr :args
    
      def defaults
        state.terrain ||= [
          { x:   0,  y:  0, w: 128, h: 128, left_perc: 0,   right_perc: 0.5 },
          { x: 128,  y: 64, w: 128, h: 128, left_perc: 0,   right_perc: 1.0 },
          { x: 256,  y: 64, w: 128, h: 128, left_perc: 1.0, right_perc: 0 },
          { x: 384,  y: 64, w: 128, h: 128, left_perc: 0,   right_perc: 0 },
          { x: 512,  y: 64, w: 128, h: 128, left_perc: 0,   right_perc: 0 },
          { x: 640,  y:  0, w: 128, h: 128, left_perc: 0.5, right_perc: 0 },
          { x: 768,  y:  0, w: 128, h: 128, left_perc: 0,   right_perc: 1.0 },
        ]
    
        state.player ||= {
          x: 100,
          y: 720,
          w: 32,
          h: 32,
          dx: 0,
          dy: 0,
          on_ground: false
        }
      end
    
      def tick
        defaults
        calc
        render
      end
    
      def calc
        if inputs.keyboard.right
          player.dx = 2
        elsif inputs.keyboard.left
          player.dx = -2
        end
    
        if inputs.keyboard.key_down.space && player.on_ground
          player.dy = 8
          player.on_ground = false
        end
    
        if player.y + player.h < 0
          player.x = 100
          player.y = 720
        end
    
        player.prev_y = player.y
        player.x += player.dx
        player.dx *= 0.9
        player.dy -= 0.2
        player.dy = player.dy.clamp(-8, 8)
        player.y += player.dy
        collisions = Geometry.find_all_intersect_rect(player_feet_box, state.terrain)
    
        collision = collisions.map do |c|
          r = { rect: c, ramp_y: ramp_y_for_x(player.x, c) }
          r.delta_y = (player.y - (c.y + r.ramp_y))
          r
        end.sort_by { |c| c.delta_y.abs }.first # sort by the smallest ramp delta
    
        if collision
          if clipping_ramp?(player.y, collision)
            player.y = collision.rect.y + collision.ramp_y
            player.on_ground = true
          elsif player.on_ground
            player.dy = 0
            player.y = player.prev_y
            player.on_ground = false
          end
        elsif player.on_ground
          player.dy = 0
          player.y = player.prev_y
          player.on_ground = false
        end
      end
    
      def render
        outputs.background_color = [0, 0, 0]
        outputs.primitives << state.terrain.map { |t| ramp_prefab(t) }
        outputs.primitives << state.player.merge(path: :solid, r: 255, g: 255, b: 255, anchor_x: 0.5, anchor_y: 0)
        outputs.primitives << player_feet_box.merge(path: :solid, r: 255, g: 0, b: 0, anchor_x: 0.5, anchor_y: 0)
      end
    
      def clipping_ramp? y, ramp
        clip_height = 16
        y < ramp.rect.y + ramp.ramp_y && y + clip_height > ramp.rect.y + ramp.ramp_y
      end
    
      def ramp_y_for_x x, ramp
        rel_x = (x - ramp.x).fdiv ramp.w
        ((ramp.right_perc - ramp.left_perc) * rel_x + ramp.left_perc) * ramp.h
      end
    
      def outputs
        @args.outputs
      end
    
      def state
        @args.state
      end
    
      def inputs
        @args.inputs
      end
    
      def player
        state.player
      end
    
      def player_feet_box
        { x: player.x, y: player.y, w: 2, h: 16, anchor_x: 0.5, anchor_y: 0 }
      end
    
      def ramp_prefab ramp
        { x:  ramp.x,
          y:  ramp.y + ramp.h * ramp.left_perc,
          x2: ramp.x + ramp.w,
          y2: ramp.y + ramp.h * ramp.right_perc,
          r: 255,
          g: 0,
          b: 0 }
      end
    end
    
    def boot args
      args.state = {}
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    def reset args
      $game = nil
    end
    
    

    Verlet Integration - main.rb link

    # ./samples/04_physics_and_collisions/13_verlet_integration/app/main.rb
    # https://www.youtube.com/watch?v=D2M8jTtKi44
    
    class Game
      attr_gtk
    
      def initialize
      end
    
      def defaults
        state.objects ||= []
      end
    
      def render
        outputs.watch "#{GTK.current_framerate} FPS"
        outputs.watch "#{state.objects.length}"
        outputs.primitives << state.objects
      end
    
      def calc_dt dt
        state.objects.each do |object|
          object.prev_x ||= object.x
          object.prev_y ||= object.y
          if !object.acceleration_x
            object.acceleration_x = object.start_acceleration_x
          else
            object.acceleration_x = 0
          end
    
          if !object.acceleration_y
            object.acceleration_y = object.start_acceleration_y
          else
            object.acceleration_y = -0.25 * dt
          end
    
          if object.y < 0
            object.y = 0
          end
    
          if object.x < 0
            object.x = 0
          elsif (object.x + object.w) > 1280
            object.x = 1280 - object.w
          end
    
          dx = object.x - object.prev_x
          dy = object.y - object.prev_y
          dx += object.acceleration_x * dt
          dy += object.acceleration_y * dt
          dx *= object.drag_x ** dt
          dy *= object.drag_y ** dt
    
          object.prev_x = object.x
          object.prev_y = object.y
          object.x += dx
          object.y += dy
        end
    
        Geometry.each_intersect_rect(state.objects, state.objects) do |o_1, o_2|
          o_1_center_x = o_1.x + o_1.radius
          o_1_center_y = o_1.y + o_1.radius
          o_2_center_x = o_2.x + o_2.radius
          o_2_center_y = o_2.y + o_2.radius
    
          distance_x = o_1_center_x - o_2_center_x
          distance_y = o_1_center_y - o_2_center_y
          distance = Math.sqrt(distance_x * distance_x + distance_y * distance_y)
    
          if distance < o_1.radius + o_2.radius
            v_x = (o_2_center_x - o_1_center_x) / distance
            v_y = (o_2_center_y - o_1_center_y) / distance
            delta = o_1.radius + o_2.radius - distance
    
            o_1_dx = -0.75 * dt * delta * v_x * 0.5
            o_1_dy = -0.75 * dt * delta * v_y * 0.5
            o_1.x += o_1_dx
            o_1.y += o_1_dy
    
            o_2_dx = 0.75 * dt * delta * v_x * 0.5
            o_2_dy = 0.75 * dt * delta * v_y * 0.5
            o_2.x += o_2_dx
            o_2.y += o_2_dy
          end
        end
      end
    
      def calc
        if inputs.mouse.held || state.objects.length < 100
          angle = rand(360)
          acc_x = angle.vector_x * 20
          acc_y = angle.vector_y * 20
          mouse_x = if inputs.mouse.click || inputs.mouse.held
                      inputs.mouse.x
                    else
                      640
                    end
    
          mouse_y = if inputs.mouse.click || inputs.mouse.held
                      inputs.mouse.y
                    else
                      540
                    end
    
          color = [:red, :blue].sample
    
          state.objects << {
            x: mouse_x - 8,
            y: mouse_y - 8,
            w: 16,
            h: 16,
            radius: 8,
            path: "sprites/square/#{color}.png",
            start_acceleration_x: acc_x,
            start_acceleration_y: acc_y,
            acceleration_x: nil,
            acceleration_y: nil,
            drag_x: 0.95,
            drag_y: 0.99
          }
        end
    
        calc_dt 0.5
        calc_dt 0.5
      end
    
      def tick
        defaults
        calc
        render
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    def sub_tick args
    end
    
    def reset args
      $game = nil
    end
    
    GTK.reset
    
    

    Mouse link

    Mouse Click - main.rb link

    # ./samples/05_mouse/01_mouse_click/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - product: Returns an array of all combinations of elements from all arrays.
    
       For example, [1,2].product([1,2]) would return the following array...
       [[1,1], [1,2], [2,1], [2,2]]
       More than two arrays can be given to product and it will still work,
       such as [1,2].product([1,2],[3,4]). What would product return in this case?
    
       Answer:
       [[1,1,3],[1,1,4],[1,2,3],[1,2,4],[2,1,3],[2,1,4],[2,2,3],[2,2,4]]
    
     - num1.fdiv(num2): Returns the float division (will have a decimal) of the two given numbers.
       For example, 5.fdiv(2) = 2.5 and 5.fdiv(5) = 1.0
    
     - yield: Allows you to call a method with a code block and yield to that block.
    
     Reminders:
    
     - Hash#inside_rect?: Returns true or false depending on if the point is inside the rect.
    
     - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated
       as Ruby code, and the placeholder is replaced with its corresponding value or result.
    
     - args.inputs.mouse.click: This property will be set if the mouse was clicked.
    
     - Ternary operator (?): Will evaluate a statement (just like an if statement)
       and perform an action if the result is true or another action if it is false.
    
     - reject: Removes elements from a collection if they meet certain requirements.
    
     - args.outputs.borders: An array. The values generate a border.
       The parameters are [X, Y, WIDTH, HEIGHT, RED, GREEN, BLUE]
       For more information about borders, go to mygame/documentation/03-solids-and-borders.md.
    
     - args.outputs.labels: An array. The values generate a label.
       The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE]
       For more information about labels, go to mygame/documentation/02-labels.
    
    =end
    
    # This sample app is a classic game of Tic Tac Toe.
    class TicTacToe
      attr_gtk # class macro that adds outputs, inputs, state, etc to class
    
      def tick
        init_new_game
        render_board
        input_board
      end
    
      def init_new_game
        state.current_turn       ||= :x
        state.space_combinations ||= [-1, 0, 1].product([-1, 0, 1]).to_a
        if !state.spaces
          state.square_size ||= 80
          state.board_left  ||= grid.w_half - state.square_size * 1.5
          state.board_top   ||= grid.h_half - state.square_size * 1.5
          state.spaces = {}
          state.space_combinations.each do |x, y|
            state.spaces[x]    ||= {}
            state.spaces[x][y] ||= {}
            state.spaces[x][y].hitbox ||= {
              x: state.board_left + (x + 1) * state.square_size,
              y: state.board_top  + (y + 1) * state.square_size,
              w: state.square_size,
              h: state.square_size
            }
          end
        end
      end
    
      # Uses borders to create grid squares for the game's board. Also outputs the game pieces using labels.
      def render_board
        # At first glance, the add(1) looks pretty trivial. But if you remove it,
        # you'll see that the positioning of the board would be skewed without it!
        # Or if you put 2 in the parenthesis, the pieces will be placed in the wrong squares
        # due to the change in board placement.
        outputs.borders << all_spaces.map do |space| # outputs borders for all board spaces
                             space.hitbox
                           end
    
        hovered_box = all_spaces.find do |space|
          inputs.mouse.inside_rect?(space.hitbox) && !space.piece
        end
    
        if hovered_box && !state.game_over
          args.outputs.solids << { x: hovered_box.hitbox.x,
                                   y: hovered_box.hitbox.y,
                                   w: hovered_box.hitbox.w,
                                   h: hovered_box.hitbox.h,
                                   r: 0,
                                   g: 100,
                                   b: 200,
                                   a: 80 }
        end
    
        # put label in each filled space of board
        outputs.labels << filled_spaces.map do |space|
          { x: space.hitbox.x + space.hitbox.w / 2,
            y: space.hitbox.y + space.hitbox.h / 2,
            anchor_x: 0.5,
            anchor_y: 0.5,
            size_px: 40,
            text: space.piece }
        end
    
        # Uses a label to output whether x or o won, or if a draw occurred.
        # If the game is ongoing, a label shows whose turn it currently is.
        outputs.labels << if state.x_won
                            { x: 640, y: 600, text: "x won", size_px: 40, anchor_x: 0.5, anchor_y: 0.5 }
                          elsif state.o_won
                            { x: 640, y: 600, text: "o won", size_px: 40, anchor_x: 0.5, anchor_y: 0.5 }
                          elsif state.draw
                            { x: 640, y: 600, text: "draw", size_px: 40, anchor_x: 0.5, anchor_y: 0.5 }
                          else
                            { x: 640, y: 600, text: "turn: #{state.current_turn}", size_px: 40, anchor_x: 0.5, anchor_y: 0.5 }
                          end
      end
    
      # Calls the methods responsible for handling user input and determining the winner.
      # Does nothing unless the mouse is clicked.
      def input_board
        return unless inputs.mouse.click
        input_place_piece
        input_restart_game
        determine_winner
      end
    
      # Handles user input for placing pieces on the board.
      def input_place_piece
        return if state.game_over
    
        # Checks to find the space that the mouse was clicked inside of, and makes sure the space does not already
        # have a piece in it.
        space = all_spaces.find do |space|
          inputs.mouse.click.point.inside_rect?(space.hitbox) && !space.piece
        end
    
        # The piece that goes into the space belongs to the player whose turn it currently is.
        return unless space
    
        space.piece = state.current_turn
    
        # This ternary operator statement allows us to change the current player's turn.
        # If it is currently x's turn, it becomes o's turn. If it is not x's turn, it become's x's turn.
        state.current_turn = state.current_turn == :x ? :o : :x
      end
    
      # Resets the game.
      def input_restart_game
        return unless state.game_over
        gtk.reset
        init_new_game
      end
    
      # Checks if x or o won the game.
      # If neither player wins and all nine squares are filled, a draw happens.
      # Once a player is chosen as the winner or a draw happens, the game is over.
      def determine_winner
        state.x_won = won? :x # evaluates to either true or false (boolean values)
        state.o_won = won? :o
        state.draw = true if filled_spaces.length == 9 && !state.x_won && !state.o_won
        state.game_over = state.x_won || state.o_won || state.draw
      end
    
      # Determines if a player won by checking if there is a horizontal match or vertical match.
      # Horizontal_match and vertical_match have boolean values. If either is true, the game has been won.
      def won? piece
        # performs action on all space combinations
        won = [[-1, 0, 1]].product([-1, 0, 1]).map do |xs, y|
          # Checks if the 3 grid spaces with the same y value (or same row) and
          # x values that are next to each other have pieces that belong to the same player.
          # Remember, the value of piece is equal to the current turn (which is the player).
          horizontal_match = state.spaces[xs[0]][y].piece == piece &&
                             state.spaces[xs[1]][y].piece == piece &&
                             state.spaces[xs[2]][y].piece == piece
    
          # Checks if the 3 grid spaces with the same x value (or same column) and
          # y values that are next to each other have pieces that belong to the same player.
          # The && represents an "and" statement: if even one part of the statement is false,
          # the entire statement evaluates to false.
          vertical_match = state.spaces[y][xs[0]].piece == piece &&
                           state.spaces[y][xs[1]].piece == piece &&
                           state.spaces[y][xs[2]].piece == piece
    
          horizontal_match || vertical_match # if either is true, true is returned
        end
    
        # Sees if there is a diagonal match, starting from the bottom left and ending at the top right.
        # Is added to won regardless of whether the statement is true or false.
        won << (state.spaces[-1][-1].piece == piece && # bottom left
                state.spaces[ 0][ 0].piece == piece && # center
                state.spaces[ 1][ 1].piece == piece)   # top right
    
        # Sees if there is a diagonal match, starting at the bottom right and ending at the top left
        # and is added to won.
        won << (state.spaces[ 1][-1].piece == piece && # bottom right
                state.spaces[ 0][ 0].piece == piece && # center
                state.spaces[-1][ 1].piece == piece)   # top left
    
        # Any false statements (meaning false diagonal matches) are rejected from won
        won.reject_false.any?
      end
    
      # Defines filled spaces on the board by rejecting all spaces that do not have game pieces in them.
      # The ! before a statement means "not". For example, we are rejecting any space combinations that do
      # NOT have pieces in them.
      def filled_spaces
        all_spaces.reject { |space| !space.piece } # reject spaces with no pieces in them
      end
    
      # Defines all spaces on the board.
      def all_spaces
        state.space_combinations.map do |x, y|
          state.spaces[x][y] # yield if a block is given
        end
      end
    end
    
    $tic_tac_toe = nil
    
    def tick args
      args.outputs.labels << { x: 640,
                               y: 700,
                               anchor_x: 0.5,
                               anchor_y: 0.5,
                               text: "Sample app shows how to work with mouse clicks and hitboxes." }
      $tic_tac_toe ||= TicTacToe.new
      $tic_tac_toe.args = args
      $tic_tac_toe.tick
    end
    
    

    Mouse Move - main.rb link

    # ./samples/05_mouse/02_mouse_move/app/main.rb
    =begin
    
     Reminders:
    
     - find_all: Finds all elements of a collection that meet certain requirements.
       For example, in this sample app, we're using find_all to find all zombies that have intersected
       or hit the player's sprite since these zombies have been killed.
    
     - args.inputs.keyboard.key_down.KEY: Determines if a key is being held or pressed.
       Stores the frame the "down" event occurred.
       For more information about the keyboard, go to mygame/documentation/06-keyboard.md.
    
     - args.outputs.sprites: An array. The values generate a sprite.
       The parameters are [X, Y, WIDTH, HEIGHT, PATH, ANGLE, ALPHA, RED, GREEN, BLUE]
       For more information about sprites, go to mygame/documentation/05-sprites.md.
    
     - args.state.new_entity: Used when we want to create a new object, like a sprite or button.
       When we want to create a new object, we can 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.)
    
     - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated
       as Ruby code, and the placeholder is replaced with its corresponding value or result.
    
     - map: Ruby method used to transform data; used in arrays, hashes, and collections.
       Can be used to perform an action on every element of a collection, such as multiplying
       each element by 2 or declaring every element as a new entity.
    
     - sample: Chooses a random element from the array.
    
     - reject: Removes elements that meet certain requirements.
       In this sample app, we're removing/rejecting zombies that reach the center of the screen. We're also
       rejecting zombies that were killed more than 30 frames ago.
    
    =end
    
    # This sample app allows users to move around the screen in order to kill zombies. Zombies appear from every direction so the goal
    # is to kill the zombies as fast as possible!
    
    class ProtectThePuppiesFromTheZombies
      attr_accessor :grid, :inputs, :state, :outputs
    
      # Calls the methods necessary for the game to run properly.
      def tick
        defaults
        render
        calc
        input
      end
    
      # Sets default values for the zombies and for the player.
      # Initialization happens only in the first frame.
      def defaults
        state.flash_at               ||= 0
        state.zombie_min_spawn_rate  ||= 60
        state.zombie_spawn_countdown ||= random_spawn_countdown state.zombie_min_spawn_rate
        state.zombies                ||= []
        state.killed_zombies         ||= []
    
        # Declares player as a new entity and sets its properties.
        # The player begins the game in the center of the screen, not moving in any direction.
        state.player ||= { x: 640,
                           y: 360,
                           w: 4 * 3,
                           h: 8 * 3,
                           attack_angle: 0,
                           dx: 0,
                           dy: 0,
                           created_at: Kernel.tick_count }
      end
    
      # Outputs a gray background.
      # Calls the methods needed to output the player, zombies, etc onto the screen.
      def render
        outputs.background_color = [100, 100, 100]
        render_zombies
        render_killed_zombies
        render_player
        render_flash
      end
    
      # Outputs the zombies on the screen and sets values for the sprites, such as the position, width, height, and animation.
      def render_zombies
        outputs.sprites << state.zombies.map do |z| # performs action on all zombies in the collection
          z.merge path: animation_sprite(z)  # sets definition for sprite, calls animation_sprite method
        end
      end
    
      # Outputs sprites of killed zombies, and displays a slash image to show that a zombie has been killed.
      def render_killed_zombies
        outputs.sprites << state.killed_zombies.map do |z| # performs action on all killed zombies in collection
          zombie = { x: z.x,
                     y: z.y,
                     w: 4 * 3,
                     h: 8 * 3,
                     path: animation_sprite(z, z.death_at), # calls animation_sprite method
                     a: 255 * z.death_at.ease(30, :flip) }  # transparency of a zombie changes when they die
    
          # Sets values to output the slash over the zombie's sprite when a zombie is killed.
          # The slash is tilted 45 degrees from the angle of the player's attack.
          # Change the 3 inside scale_rect to 30 and the slash will be HUGE! Scale_rect positions
          # the slash over the killed zombie's sprite.
          [zombie,
           zombie.merge(path: 'sprites/slash.png',
                        angle: 45 + (state.player.attack_angle_on_click || 0)).scale_rect(3, 0.5, 0.5)]
        end
      end
    
      # Outputs the player sprite using the images in the sprites folder.
      def render_player
        # Outputs a small red square that previews the angles that the player can attack in.
        # It can be moved in a perfect circle around the player to show possible movements.
        # Change the 60 in the parenthesis and see what happens to the movement of the red square.
        outputs.sprites << { x: state.player.x + state.player.attack_angle.vector_x(60),
                             y: state.player.y + state.player.attack_angle.vector_y(60),
                             w: 3,
                             h: 3,
                             r: 255,
                             g: 0,
                             b: 0,
                             path: :solid }
    
        outputs.sprites << { x: state.player.x,
                             y: state.player.y,
                             w: 4 * 3,
                             h: 8 * 3,
                             path: "sprites/player-#{animation_index(state.player.created_at.elapsed_time)}.png" } # string interpolation
      end
    
      # Renders flash as a solid. The screen turns white for 10 frames when a zombie is killed.
      def render_flash
        return if state.flash_at.elapsed_time > 10 # return if more than 10 frames have passed since flash.
        # Transparency gradually changes (or eases) during the 10 frames of flash.
        outputs.primitives << { **grid.rect, r: 255, g: 255, b: 255, a: 255 * state.flash_at.ease(10, :flip), path: :solid }
      end
    
      # Calls all methods necessary for performing calculations.
      def calc
        calc_spawn_zombie
        calc_move_zombies
        calc_player
        calc_kill_zombie
      end
    
      # Decreases the zombie spawn countdown by 1 if it has a value greater than 0.
      def calc_spawn_zombie
        if state.zombie_spawn_countdown > 0
          state.zombie_spawn_countdown -= 1
          return
        end
    
        # New zombies are created, positioned on the screen, and added to the zombies collection.
        state.zombies << (if rand > 0.5
                           {
                             x: grid.rect.w.randomize(:ratio), # random x position on screen (within grid scope)
                             y: [-10, 730].sample, # y position is set to either -10 or 730 (randomly chosen)
                             w: 4 * 3, h: 8 * 3,
                             created_at: Kernel.tick_count
                           }
                          else
                           {
                             x: [-10, 1290].sample, # x position is set to either -10 or 1290 (randomly chosen)
                             y: grid.rect.w.randomize(:ratio), # random y position on screen
                             w: 4 * 3, h: 8 * 3,
                             created_at: Kernel.tick_count
                           }
                          end)
    
        # Calls random_spawn_countdown method (determines how fast new zombies appear)
        state.zombie_spawn_countdown = random_spawn_countdown state.zombie_min_spawn_rate
        state.zombie_min_spawn_rate -= 1
        # set to either the current zombie_min_spawn_rate or 0, depending on which value is greater
        state.zombie_min_spawn_rate  = state.zombie_min_spawn_rate.clamp(0)
      end
    
      # Moves all zombies towards the center of the screen.
      # All zombies that reach the center (640, 360) are rejected from the zombies collection and disappear.
      def calc_move_zombies
        state.zombies.each do |z| # for each zombie in the collection
          z.y = z.y.towards(360, 0.1) # move the zombie towards the center (640, 360) at a rate of 0.1
          z.x = z.x.towards(640, 0.1) # change 0.1 to 1.1 and see how much faster the zombies move to the center
        end
        state.zombies = state.zombies.reject { |z| z.y == 360 && z.x == 640 } # remove zombies that are in center
      end
    
      # Calculates the position and movement of the player on the screen.
      def calc_player
        state.player.x += state.player.dx # changes x based on dx (change in x)
        state.player.y += state.player.dy # changes y based on dy (change in y)
    
        state.player.dx *= 0.9 # scales dx down
        state.player.dy *= 0.9 # scales dy down
    
        # Compares player's x to 1280 to find lesser value, then compares result to 0 to find greater value.
        # This ensures that the player remains within the screen's scope.
        state.player.x = state.player.x.clamp(0, 1280)
        state.player.y = state.player.y.clamp(0, 720) # same with player's y
      end
    
      # Finds all zombies that intersect with the player's sprite. These zombies are removed from the zombies collection
      # and added to the killed_zombies collection since any zombie that intersects with the player is killed.
      def calc_kill_zombie
    
        # Find all zombies that intersect with the player. They are considered killed.
        killed_this_frame = state.zombies.find_all { |z| (z.intersect_rect? state.player) }
        state.zombies = state.zombies - killed_this_frame # remove newly killed zombies from zombies collection
        state.killed_zombies += killed_this_frame # add newly killed zombies to killed zombies
    
        if killed_this_frame.length > 0 # if atleast one zombie was killed in the frame
          state.flash_at = Kernel.tick_count # flash_at set to the frame when the zombie was killed
        # Don't forget, the rendered flash lasts for 10 frames after the zombie is killed (look at render_flash method)
        end
    
        # Sets the tick_count (passage of time) as the value of the death_at variable for each killed zombie.
        # Death_at stores the frame a zombie was killed.
        killed_this_frame.each do |z|
          z.death_at = Kernel.tick_count
        end
    
        # Zombies are rejected from the killed_zombies collection depending on when they were killed.
        # They are rejected if more than 30 frames have passed since their death.
        state.killed_zombies = state.killed_zombies.reject { |z| Kernel.tick_count - z.death_at > 30 }
      end
    
      # Uses input from the user to move the player around the screen.
      def input
    
        # If the "a" key or left key is pressed, the x position of the player decreases.
        # Otherwise, if the "d" key or right key is pressed, the x position of the player increases.
        if inputs.keyboard.key_held.a || inputs.keyboard.key_held.left
          state.player.x -= 5
        elsif inputs.keyboard.key_held.d || inputs.keyboard.key_held.right
          state.player.x += 5
        end
    
        # If the "w" or up key is pressed, the y position of the player increases.
        # Otherwise, if the "s" or down key is pressed, the y position of the player decreases.
        if inputs.keyboard.key_held.w || inputs.keyboard.key_held.up
          state.player.y += 5
        elsif inputs.keyboard.key_held.s || inputs.keyboard.key_held.down
          state.player.y -= 5
        end
    
        # Sets the attack angle so the player can move and attack in the precise direction it wants to go.
        # If the mouse is moved, the attack angle is changed (based on the player's position and mouse position).
        # Attack angle also contributes to the position of red square.
        if inputs.mouse.moved
          state.player.attack_angle = inputs.mouse.position.angle_from [state.player.x, state.player.y]
        end
    
        if inputs.mouse.click && state.player.dx < 0.5 && state.player.dy < 0.5
          state.player.attack_angle_on_click = inputs.mouse.position.angle_from [state.player.x, state.player.y]
          state.player.attack_angle = state.player.attack_angle_on_click # player's attack angle is set
          state.player.dx = state.player.attack_angle.vector_x(25) # change in player's position
          state.player.dy = state.player.attack_angle.vector_y(25)
        end
      end
    
      # Sets the zombie spawn's countdown to a random number.
      # How fast zombies appear (change the 60 to 6 and too many zombies will appear at once!)
      def random_spawn_countdown minimum
        10.randomize(:ratio, :sign).to_i + 60
      end
    
      # Helps to iterate through the images in the sprites folder by setting the animation index.
      # 3 frames is how long to show an image, and 6 is how many images to flip through.
      def animation_index at
        at.idiv(3).mod(6)
      end
    
      # Animates the zombies by using the animation index to go through the images in the sprites folder.
      def animation_sprite zombie, at = nil
        at ||= zombie.created_at.elapsed_time # how long it is has been since a zombie was created
        index = animation_index at
        "sprites/zombie-#{index}.png" # string interpolation to iterate through images
      end
    end
    
    $protect_the_puppies_from_the_zombies = ProtectThePuppiesFromTheZombies.new
    
    def tick args
      $protect_the_puppies_from_the_zombies.grid    = args.grid
      $protect_the_puppies_from_the_zombies.inputs  = args.inputs
      $protect_the_puppies_from_the_zombies.state    = args.state
      $protect_the_puppies_from_the_zombies.outputs = args.outputs
      $protect_the_puppies_from_the_zombies.tick
      tick_instructions args, "How to get the mouse position and translate it to an x, y position using .vector_x and .vector_y. CLICK to play."
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Mouse Move Paint App - main.rb link

    # ./samples/05_mouse/03_mouse_move_paint_app/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - Floor: Method that returns an integer number smaller than or equal to the original with no decimal.
    
       For example, if we have a variable, a = 13.7, and we called floor on it, it would look like this...
       puts a.floor()
       which would print out 13.
       (There is also a ceil method, which returns an integer number greater than or equal to the original
       with no decimal. If we had called ceil on the variable a, the result would have been 14.)
    
     Reminders:
    
     - Hashes: Collection of unique keys and their corresponding values. The value can be found
       using their keys.
    
       For example, if we have a "numbers" hash that stores numbers in English as the
       key and numbers in Spanish as the value, we'd have a hash that looks like this...
       numbers = { "one" => "uno", "two" => "dos", "three" => "tres" }
       and on it goes.
    
       Now if we wanted to find the corresponding value of the "one" key, we could say
       puts numbers["one"]
       which would print "uno" to the console.
    
     - args.state.new_entity: Used when we want to create a new object, like a sprite or button.
       In this sample app, new_entity is used to create a new button that clears the grid.
       (Remember, you can use state to define ANY property and it will be retained across frames.)
    
     - args.inputs.mouse.click.point.(x|y): The x and y location of the mouse.
    
     - args.inputs.mouse.click.point.created_at: The frame the mouse click occurred in.
    
     - args.outputs.labels: An array. The values in the array generate a label.
       The parameters are [X, Y, TEXT, SIZE, ALIGN, RED, GREEN, BLUE, ALPHA, FONT STYLE]
       For more information about labels, go to mygame/documentation/02-labels.md.
    
     - ARRAY#inside_rect?: Returns true or false depending on if the point is inside the rect.
    
    =end
    
    # This sample app shows an empty grid that the user can paint on.
    # To paint, the user must keep their mouse presssed and drag it around the grid.
    # The "clear" button allows users to clear the grid so they can start over.
    
    class PaintApp
      attr_accessor :inputs, :state, :outputs, :grid, :args
    
      # Runs methods necessary for the game to function properly.
      def tick
        print_title
        add_grid
        check_click
        draw_buttons
      end
    
      # Prints the title onto the screen by using a label.
      # Also separates the title from the grid with a line as a horizontal separator.
      def print_title
        args.outputs.labels << [ 640, 700, 'Paint!', 0, 1 ]
        outputs.lines << horizontal_separator(660, 0, 1280)
      end
    
      # Sets the starting position, ending position, and color for the horizontal separator.
      # The starting and ending positions have the same y values.
      def horizontal_separator y, x, x2
        [x, y, x2, y, 150, 150, 150]
      end
    
      # Sets the starting position, ending position, and color for the vertical separator.
      # The starting and ending positions have the same x values.
      def vertical_separator x, y, y2
        [x, y, x, y2, 150, 150, 150]
      end
    
      # Outputs a border and a grid containing empty squares onto the screen.
      def add_grid
    
        # Sets the x, y, height, and width of the grid.
        # There are 31 horizontal lines and 31 vertical lines in the grid.
        # Feel free to count them yourself before continuing!
        x, y, h, w = 640 - 500/2, 640 - 500, 500, 500 # calculations done so the grid appears in screen's center
        lines_h = 31
        lines_v = 31
    
        # Sets values for the grid's border, grid lines, and filled squares.
        # The filled_squares variable is initially set to an empty array.
        state.grid_border ||= [ x, y, h, w ] # definition of grid's outer border
        state.grid_lines ||= draw_grid(x, y, h, w, lines_h, lines_v) # calls draw_grid method
        state.filled_squares ||= [] # there are no filled squares until the user fills them in
    
        # Outputs the grid lines, border, and filled squares onto the screen.
        outputs.lines.concat state.grid_lines
        outputs.borders << state.grid_border
        outputs.solids << state.filled_squares
      end
    
      # Draws the grid by adding in vertical and horizontal separators.
      def draw_grid x, y, h, w, lines_h, lines_v
    
        # The grid starts off empty.
        grid = []
    
        # Calculates the placement and adds horizontal lines or separators into the grid.
        curr_y = y # start at the bottom of the box
        dist_y = h / (lines_h + 1) # finds distance to place horizontal lines evenly throughout 500 height of grid
        lines_h.times do
          curr_y += dist_y # increment curr_y by the distance between the horizontal lines
          grid << horizontal_separator(curr_y, x, x + w - 1) # add a separator into the grid
        end
    
        # Calculates the placement and adds vertical lines or separators into the grid.
        curr_x = x # now start at the left of the box
        dist_x = w / (lines_v + 1) # finds distance to place vertical lines evenly throughout 500 width of grid
        lines_v.times do
          curr_x += dist_x # increment curr_x by the distance between the vertical lines
          grid << vertical_separator(curr_x, y + 1, y  + h) # add separator
        end
    
        # paint_grid uses a hash to assign values to keys.
        state.paint_grid ||= {"x" => x, "y" => y, "h" => h, "w" => w, "lines_h" => lines_h,
                              "lines_v" => lines_v, "dist_x" => dist_x,
                              "dist_y" => dist_y }
    
        return grid
      end
    
      # Checks if the user is keeping the mouse pressed down and sets the mouse_hold variable accordingly using boolean values.
      # If the mouse is up, the user cannot drag the mouse.
      def check_click
        if inputs.mouse.down #is mouse up or down?
          state.mouse_held = true # mouse is being held down
        elsif inputs.mouse.up # if mouse is up
        state.mouse_held = false # mouse is not being held down or dragged
          state.mouse_dragging = false
        end
    
        if state.mouse_held &&    # mouse needs to be down
          !inputs.mouse.click &&     # must not be first click
          ((inputs.mouse.previous_click.point.x - inputs.mouse.position.x).abs > 15) # Need to move 15 pixels before "drag"
          state.mouse_dragging = true
        end
    
        # If the user clicks their mouse inside the grid, the search_lines method is called with a click input type.
        if ((inputs.mouse.click) && (inputs.mouse.click.point.inside_rect? state.grid_border))
          search_lines(inputs.mouse.click.point, :click)
    
        # If the user drags their mouse inside the grid, the search_lines method is called with a drag input type.
        elsif ((state.mouse_dragging) && (inputs.mouse.position.inside_rect? state.grid_border))
          search_lines(inputs.mouse.position, :drag)
        end
      end
    
      # Sets the definition of a grid box and handles user input to fill in or clear grid boxes.
      def search_lines (point, input_type)
        point.x -= state.paint_grid["x"] # subtracts the value assigned to the "x" key in the paint_grid hash
        point.y -= state.paint_grid["y"] # subtracts the value assigned to the "y" key in the paint_grid hash
    
        # Remove code following the .floor and see what happens when you try to fill in grid squares
        point.x = (point.x / state.paint_grid["dist_x"]).floor * state.paint_grid["dist_x"]
        point.y = (point.y / state.paint_grid["dist_y"]).floor * state.paint_grid["dist_y"]
    
        point.x += state.paint_grid["x"]
        point.y += state.paint_grid["y"]
    
        # Sets definition of a grid box, meaning its x, y, width, and height.
        # Floor is called on the point.x and point.y variables.
        # Ceil method is called on values of the distance hash keys, setting the width and height of a box.
        grid_box = [ point.x.floor, point.y.floor, state.paint_grid["dist_x"].ceil, state.paint_grid["dist_y"].ceil ]
    
        if input_type == :click # if user clicks their mouse
          if state.filled_squares.include? grid_box # if grid box is already filled in
            state.filled_squares.delete grid_box # box is cleared and removed from filled_squares
          else
            state.filled_squares << grid_box # otherwise, box is filled in and added to filled_squares
          end
        elsif input_type == :drag # if user drags mouse
          unless state.filled_squares.include? grid_box # unless the grid box dragged over is already filled in
            state.filled_squares << grid_box # the box is filled in and added to filled_squares
          end
        end
      end
    
      # Creates and outputs a "Clear" button on the screen using a label and a border.
      # If the button is clicked, the filled squares are cleared, making the filled_squares collection empty.
      def draw_buttons
        x, y, w, h = 390, 50, 240, 50
        state.clear_button        ||= state.new_entity(:button_with_fade)
    
        # The x and y positions are set to display the label in the center of the button.
        # Try changing the first two parameters to simply x, y and see what happens to the text placement!
        state.clear_button.label  ||= [x + w.half, y + h.half + 10, "Clear", 0, 1] # placed in center of border
        state.clear_button.border ||= [x, y, w, h]
    
        # If the mouse is clicked inside the borders of the clear button,
        # the filled_squares collection is emptied and the squares are cleared.
        if inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.clear_button.border)
          state.clear_button.clicked_at = inputs.mouse.click.created_at # time (frame) the click occurred
          state.filled_squares.clear
          inputs.mouse.previous_click = nil
        end
    
        outputs.labels << state.clear_button.label
        outputs.borders << state.clear_button.border
    
        # When the clear button is clicked, the color of the button changes
        # and the transparency changes, as well. If you change the time from
        # 0.25.seconds to 1.25.seconds or more, the change will last longer.
        if state.clear_button.clicked_at
          outputs.solids << [x, y, w, h, 0, 180, 80, 255 * state.clear_button.clicked_at.ease(0.25.seconds, :flip)]
        end
      end
    end
    
    $paint_app = PaintApp.new
    
    def tick args
      $paint_app.inputs = args.inputs
      $paint_app.state = args.state
      $paint_app.grid = args.grid
      $paint_app.args = args
      $paint_app.outputs = args.outputs
      $paint_app.tick
      tick_instructions args, "How to create a simple paint app. CLICK and HOLD to draw."
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Coordinate Systems - main.rb link

    # ./samples/05_mouse/04_coordinate_systems/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - args.inputs.mouse.click.position: Coordinates of the mouse's position on the screen.
       Unlike args.inputs.mouse.click.point, the mouse does not need to be pressed down for
       position to know the mouse's coordinates.
       For more information about the mouse, go to mygame/documentation/07-mouse.md.
    
     Reminders:
    
     - args.inputs.mouse.click: This property will be set if the mouse was clicked.
    
     - args.inputs.mouse.click.point.(x|y): The x and y location of the mouse.
    
     - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated
       as Ruby code, and the placeholder is replaced with its corresponding value or result.
    
       In this sample app, string interpolation is used to show the current position of the mouse
       in a label.
    
     - args.outputs.labels: An array that generates a label.
       The parameters are [X, Y, TEXT, SIZE, ALIGN, RED, GREEN, BLUE, ALPHA, FONT STYLE]
       For more information about labels, go to mygame/documentation/02-labels.md.
    
     - args.outputs.solids: An array that generates a solid.
       The parameters are [X, Y, WIDTH, HEIGHT, RED, GREEN, BLUE, ALPHA]
       For more information about solids, go to mygame/documentation/03-solids-and-borders.md.
    
     - args.outputs.lines: An array that generates a line.
       The parameters are [X, Y, X2, Y2, RED, GREEN, BLUE, ALPHA]
       For more information about lines, go to mygame/documentation/04-lines.md.
    
    =end
    
    # This sample app shows a coordinate system or grid. The user can move their mouse around the screen and the
    # coordinates of their position on the screen will be displayed. Users can choose to view one quadrant or
    # four quadrants by pressing the button.
    
    def tick args
    
      # The addition and subtraction in the first two parameters of the label and solid
      # ensure that the outputs don't overlap each other. Try removing them and see what happens.
      pos = args.inputs.mouse.position # stores coordinates of mouse's position
      args.outputs.labels << [pos.x + 10, pos.y + 10, "#{pos}"] # outputs label of coordinates
      args.outputs.solids << [pos.x -  2, pos.y - 2, 5, 5] # outputs small blackk box placed where mouse is hovering
    
      button = [0, 0, 370, 50] # sets definition of toggle button
      args.outputs.borders << button # outputs button as border (not filled in)
      args.outputs.labels << [10, 35, "click here toggle coordinate system"] # label of button
      args.outputs.lines << [    0, -720,    0, 720] # vertical line dividing quadrants
      args.outputs.lines << [-1280,    0, 1280,   0] # horizontal line dividing quadrants
    
      if args.inputs.mouse.click # if the user clicks the mouse
        pos = args.inputs.mouse.click.point # pos's value is point where user clicked (coordinates)
        if pos.inside_rect? button # if the click occurred inside the button
          if args.grid.name == :bottom_left # if the grid shows bottom left as origin
            args.grid.origin_center! # origin will be shown in center
          else
            args.grid.origin_bottom_left! # otherwise, the view will change to show bottom left as origin
          end
        end
      end
    
      tick_instructions args, "Sample app shows the two supported coordinate systems in Game Toolkit."
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Clicking Buttons - main.rb link

    # ./samples/05_mouse/05_clicking_buttons/app/main.rb
    def tick args
      # create buttons
      args.state.buttons ||= [
        create_button(args, id: :button_1, row: 0, col: 0, text: "button 1"),
        create_button(args, id: :button_2, row: 1, col: 0, text: "button 2"),
        create_button(args, id: :clear,    row: 2, col: 0, text: "clear")
      ]
    
      # render button's border and label
      args.outputs.primitives << args.state.buttons.map do |b|
        b.primitives
      end
    
      # render center label if the text is set
      if args.state.center_label_text
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: args.state.center_label_text,
                                 alignment_enum: 1,
                                 vertical_alignment_enum: 1 }
      end
    
      # if the mouse is clicked, see if the mouse click intersected
      # with a button
      if args.inputs.mouse.click
        button = args.state.buttons.find do |b|
          args.inputs.mouse.intersect_rect? b
        end
    
        # update the center label text based on button clicked
        case button.id
        when :button_1
          args.state.center_label_text = "button 1 was clicked"
        when :button_2
          args.state.center_label_text = "button 2 was clicked"
        when :clear
          args.state.center_label_text = nil
        end
      end
    end
    
    def create_button args, id:, row:, col:, text:;
      # Layout.rect(row:, col:, w:, h:) is method that will
      # return a rectangle inside of a grid with 12 rows and 24 columns
      rect = Layout.rect row: row, col: col, w: 3, h: 1
    
      # get senter of rect for label
      center = Geometry.rect_center_point rect
    
      {
        id: id,
        x: rect.x,
        y: rect.y,
        w: rect.w,
        h: rect.h,
        primitives: [
          {
            x: rect.x,
            y: rect.y,
            w: rect.w,
            h: rect.h,
            primitive_marker: :border
          },
          {
            x: center.x,
            y: center.y,
            text: text,
            size_enum: -1,
            alignment_enum: 1,
            vertical_alignment_enum: 1,
            primitive_marker: :label
          }
        ]
      }
    end
    
    GTK.reset
    
    

    Save Load link

    Save Load Game - main.rb link

    # ./samples/06_save_load/01_save_load_game/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - Symbol (:): Ruby object with a name and an internal ID. Symbols are useful
       because with a given symbol name, you can refer to the same object throughout
       a Ruby program.
    
       In this sample app, we're using symbols for our buttons. We have buttons that
       light fires, save, load, etc. Each of these buttons has a distinct symbol like
       :light_fire, :save_game, :load_game, etc.
    
     - to_sym: Returns the symbol corresponding to the given string; creates the symbol
       if it does not already exist.
       For example,
       'car'.to_sym
       would return the symbol :car.
    
     - last: Returns the last element of an array.
    
     Reminders:
    
     - num1.lesser(num2): finds the lower value of the given options.
       For example, in the statement
       a = 4.lesser(3)
       3 has a lower value than 4, which means that the value of a would be set to 3,
       but if the statement had been
       a = 4.lesser(5)
       4 has a lower value than 5, which means that the value of a would be set to 4.
    
     - num1.fdiv(num2): returns the float division (will have a decimal) of the two given numbers.
       For example, 5.fdiv(2) = 2.5 and 5.fdiv(5) = 1.0
    
     - String interpolation: uses #{} syntax; everything between the #{ and the } is evaluated
       as Ruby code, and the placeholder is replaced with its corresponding value or result.
    
     - args.outputs.labels: An array. Values generate a label.
       Parameters are [X, Y, TEXT, SIZE, ALIGN, RED, GREEN, BLUE, ALPHA, FONT STYLE]
       For more information, go to mygame/documentation/02-labels.md.
    
     - ARRAY#inside_rect?: An array with at least two values is considered a point. An array
       with at least four values is considered a rect. The inside_rect? function returns true
       or false depending on if the point is inside the rect.
    
    =end
    
    # This code allows users to perform different tasks, such as saving and loading the game.
    # Users also have options to reset the game and light a fire.
    
    class TextedBasedGame
    
      # Contains methods needed for game to run properly.
      # Increments tick count by 1 each time it runs (60 times in a single second)
      def tick
        default
        show_intro
        state.engine_tick_count += 1
        tick_fire
      end
    
      # Sets default values.
      # The ||= ensures that a variable's value is only set to the value following the = sign
      # if the value has not already been set before. Intialization happens only in the first frame.
      def default
        state.engine_tick_count ||= 0
        state.active_module     ||= :room
        state.fire_progress     ||= 0
        state.fire_ready_in     ||= 10
        state.previous_fire     ||= :dead
        state.fire              ||= :dead
      end
    
      def show_intro
        return unless state.engine_tick_count == 0 # return unless the game just started
        set_story_line "awake." # calls set_story_line method, sets to "awake"
      end
    
      # Sets story line.
      def set_story_line story_line
        state.story_line    = story_line # story line set to value of parameter
        state.active_module = :alert # active module set to alert
      end
    
      # Clears story line.
      def clear_storyline
        state.active_module = :none # active module set to none
        state.story_line = nil # story line is cleared, set to nil (or empty)
      end
    
      # Determines fire progress (how close the fire is to being ready to light).
      def tick_fire
        return if state.active_module == :alert # return if active module is alert
        state.fire_progress += 1 # increment fire progress
        # fire_ready_in is 10. The fire_progress is either the current value or 10, whichever has a lower value.
        state.fire_progress = state.fire_progress.lesser(state.fire_ready_in)
      end
    
      # Sets the value of fire (whether it is dead or roaring), and the story line
      def light_fire
        return unless fire_ready? # returns unless the fire is ready to be lit
        state.fire = :roaring # fire is lit, set to roaring
        state.fire_progress = 0 # the fire progress returns to 0, since the fire has been lit
        if state.fire != state.previous_fire
          set_story_line "the fire is #{state.fire}." # the story line is set using string interpolation
          state.previous_fire = state.fire
        end
      end
    
      # Checks if the fire is ready to be lit. Returns a boolean value.
      def fire_ready?
        # If fire_progress (value between 0 and 10) is equal to fire_ready_in (value of 10),
        # the fire is ready to be lit.
        state.fire_progress == state.fire_ready_in
      end
    
      # Divides the value of the fire_progress variable by 10 to determine how close the user is to
      # being able to light a fire.
      def light_fire_progress
        state.fire_progress.fdiv(10) # float division
      end
    
      # Defines fire as the state.fire variable.
      def fire
        state.fire
      end
    
      # Sets the title of the room.
      def room_title
        return "a room that is dark" if state.fire == :dead # room is dark if the fire is dead
        return "a room that is lit" # room is lit if the fire is not dead
      end
    
      # Sets the active_module to room.
      def go_to_room
        state.active_module = :room
      end
    
      # Defines active_module as the state.active_module variable.
      def active_module
        state.active_module
      end
    
      # Defines story_line as the state.story_line variable.
      def story_line
        state.story_line
      end
    
      # Update every 60 frames (or every second)
      def should_tick?
        Kernel.tick_count.zmod?(60)
      end
    
      # Sets the value of the game state provider.
      def initialize game_state_provider
        @game_state_provider = game_state_provider
      end
    
      # Defines the game state.
      # Any variable prefixed with an @ symbol is an instance variable.
      def state
        @game_state_provider.state
      end
    
      # Saves the state of the game in a text file called game_state.txt.
      def save
        GTK.serialize_state('game_state.txt', state)
      end
    
      # Loads the game state from the game_state.txt text file.
      # If the load is unsuccessful, the user is informed since the story line indicates the failure.
      def load
        parsed_state = GTK.deserialize_state('game_state.txt')
        if !parsed_state
          set_story_line "no game to load. press save first."
        else
          GTK.args.state = parsed_state
        end
      end
    
      # Resets the game.
      def reset
        GTK.reset
      end
    end
    
    class TextedBasedGamePresenter
      attr_accessor :state, :outputs, :inputs
    
      # Creates empty collection called highlights.
      # Calls methods necessary to run the game.
      def tick
        state.layout.highlights ||= []
        game.tick if game.should_tick?
        render
        process_input
      end
    
      # Outputs a label of the tick count (passage of time) and calls all render methods.
      def render
        outputs.labels << [10, 30, Kernel.tick_count]
        render_alert
        render_room
        render_highlights
      end
    
      # Outputs a label onto the screen that shows the story line, and also outputs a "close" button.
      def render_alert
        return unless game.active_module == :alert
    
        outputs.labels << [640, 480, game.story_line, 5, 1]  # outputs story line label
        outputs.primitives << button(:alert_dismiss, 490, 380, "close")  # positions "close" button under story line
      end
    
      def render_room
        return unless game.active_module == :room
        outputs.labels << [640, 700, game.room_title, 4, 1] # outputs room title label at top of screen
    
        # The parameters for these outputs are (symbol, x, y, text, value/percentage) and each has a y value
        # that positions it 60 pixels lower than the previous output.
    
        # outputs the light_fire_progress bar, uses light_fire_progress for its percentage (which changes bar's appearance)
        outputs.primitives << progress_bar(:light_fire, 490, 600, "light fire", game.light_fire_progress)
        outputs.primitives << button(       :save_game, 490, 540, "save") # outputs save button
        outputs.primitives << button(       :load_game, 490, 480, "load") # outputs load button
        outputs.primitives << button(      :reset_game, 490, 420, "reset") # outputs reset button
        outputs.labels << [640, 30, "the fire is #{game.fire}", 0, 1] # outputs fire label at bottom of screen
      end
    
      # Outputs a collection of highlights using an array to set their values, and also rejects certain values from the collection.
      def render_highlights
        state.layout.highlights.each do |h| # for each highlight in the collection
            h.lifetime -= 1 # decrease the value of its lifetime
          end
    
          outputs.solids << state.layout.highlights.map do |h| # outputs highlights collection
            [h.x, h.y, h.w, h.h, h.color, 255 * h.lifetime / h.max_lifetime] # sets definition for each highlight
            # transparency changes; divide lifetime by max_lifetime, multiply result by 255
          end
    
          # reject highlights from collection that have no remaining lifetime
          state.layout.highlights = state.layout.highlights.reject { |h| h.lifetime <= 0 }
      end
    
      # Checks whether or not a button was clicked.
      # Returns a boolean value.
      def process_input
        button = button_clicked? # calls button_clicked? method
      end
    
      # Returns a boolean value.
      # Finds the button that was clicked from the button list and determines what method to call.
      # Adds a highlight to the highlights collection.
      def button_clicked?
        return nil unless click_pos # return nil unless click_pos holds coordinates of mouse click
          button = @button_list.find do |k, v| # goes through button_list to find button clicked
            click_pos.inside_rect? v[:primitives].last.rect # was the mouse clicked inside the rect of button?
          end
          return unless button # return unless a button was clicked
          method_to_call = "#{button[0]}_clicked".to_sym # sets method_to_call to symbol (like :save_game or :load_game)
          if self.respond_to? method_to_call # returns true if self responds to the given method (method actually exists)
            border = button[1][:primitives].last # sets border definition using value of last key in button list hash
    
            # declares each highlight as a new entity, sets properties
            state.layout.highlights << state.new_entity(:highlight) do |h|
                h.x = border.x
                h.y = border.y
                h.w = border.w
                h.h = border.h
                h.max_lifetime = 10
                h.lifetime = h.max_lifetime
                h.color = [120, 120, 180] # sets color to shade of purple
              end
    
              self.send method_to_call # invoke method identified by symbol
            else # otherwise, if self doesn't respond to given method
              border = button[1][:primitives].last # sets border definition using value of last key in hash
    
              # declares each highlight as a new entity, sets properties
              state.layout.highlights << state.new_entity(:highlight) do |h|
                h.x = border.x
                h.y = border.y
                h.w = border.w
                h.h = border.h
                h.max_lifetime = 4 # different max_lifetime than the one set if respond_to? had been true
                h.lifetime = h.max_lifetime
                h.color = [120, 80, 80] # sets color to dark color
              end
    
              # instructions for users on how to add the missing method_to_call to the code
              puts "It looks like #{method_to_call} doesn't exists on TextedBasedGamePresenter. Please add this method:"
              puts "Just copy the code below and put it in the #{TextedBasedGamePresenter} class definition."
              puts ""
              puts "```"
              puts "class TextedBasedGamePresenter <--- find this class and put the method below in it"
              puts ""
              puts "  def #{method_to_call}"
              puts "    puts 'Yay that worked!'"
              puts "  end"
              puts ""
              puts "end <-- make sure to put the #{method_to_call} method in between the `class` word and the final `end` statement."
              puts "```"
              puts ""
          end
      end
    
      # Returns the position of the mouse when it is clicked.
      def click_pos
        return nil unless inputs.mouse.click # returns nil unless the mouse was clicked
        return inputs.mouse.click.point # returns location of mouse click (coordinates)
      end
    
      # Creates buttons for the button_list and sets their values using a hash (uses symbols as keys)
      def button id, x, y, text
        @button_list[id] ||= { # assigns values to hash keys
          id: id,
          text: text,
          primitives: [
            [x + 10, y + 30, text, 2, 0].label, # positions label inside border
            [x, y, 300, 50].border,             # sets definition of border
          ]
        }
    
        @button_list[id][:primitives] # returns label and border for buttons
      end
    
      # Creates a progress bar (used for lighting the fire) and sets its values.
      def progress_bar id, x, y, text, percentage
        @button_list[id] = { # assigns values to hash keys
          id: id,
          text: text,
          primitives: [
            [x, y, 300, 50, 100, 100, 100].solid, # sets definition for solid (which fills the bar with gray)
            [x + 10, y + 30, text, 2, 0].label, # sets definition for label, positions inside border
            [x, y, 300, 50].border, # sets definition of border
          ]
        }
    
        # Fills progress bar based on percentage. If the fire was ready to be lit (100%) and we multiplied by
        # 100, only 1/3 of the bar would only be filled in. 200 would cause only 2/3 to be filled in.
        @button_list[id][:primitives][0][2] = 300 * percentage
        @button_list[id][:primitives]
      end
    
      # Defines the game.
      def game
        @game
      end
    
      # Initalizes the game and creates an empty list of buttons.
      def initialize
        @game = TextedBasedGame.new self
        @button_list ||= {}
      end
    
      # Clears the storyline and takes the user to the room.
      def alert_dismiss_clicked
        game.clear_storyline
        game.go_to_room
      end
    
      # Lights the fire when the user clicks the "light fire" option.
      def light_fire_clicked
        game.light_fire
      end
    
      # Saves the game when the user clicks the "save" option.
      def save_game_clicked
        game.save
      end
    
      # Resets the game when the user clicks the "reset" option.
      def reset_game_clicked
        game.reset
      end
    
      # Loads the game when the user clicks the "load" option.
      def load_game_clicked
        game.load
      end
    end
    
    $text_based_rpg = TextedBasedGamePresenter.new
    
    def tick args
      $text_based_rpg.state = args.state
      $text_based_rpg.outputs = args.outputs
      $text_based_rpg.inputs = args.inputs
      $text_based_rpg.tick
    end
    
    

    Advanced Audio link

    Audio Mixer - main.rb link

    # ./samples/07_advanced_audio/01_audio_mixer/app/main.rb
    # these are the properties that you can sent on args.audio
    def spawn_new_sound args, name, path
      # Spawn randomly in an area that won't be covered by UI.
      screenx = (rand * 600.0) + 200.0
      screeny = (rand * 400.0) + 100.0
    
      id = new_sound_id! args
      # you can hang anything on the audio hashes you want, so we store the
      #  actual screen position in here for convenience.
      args.audio[id] = {
        name: name,
        input: path,
        screenx: screenx,
        screeny: screeny,
        x: ((screenx / 1279.0) * 2.0) - 1.0,  # scale to -1.0 - 1.0 range
        y: ((screeny / 719.0) * 2.0) - 1.0,   # scale to -1.0 - 1.0 range
        z: 0.0,
        gain: 1.0,
        pitch: 1.0,
        looping: true,
        paused: false
      }
    
      args.state.selected = id
    end
    
    # these are values you can change on the ~args.audio~ data structure
    def input_panel args
      return unless args.state.panel
      return if args.state.dragging
    
      audio_entry = args.audio[args.state.selected]
      results = args.state.panel
    
      if args.state.mouse_state == :held && args.inputs.mouse.position.inside_rect?(results.pitch_slider_rect.rect)
        audio_entry.pitch = 2.0 * ((args.inputs.mouse.x - results.pitch_slider_rect.rect.x).to_f / (results.pitch_slider_rect.rect.w - 1.0))
      elsif args.state.mouse_state == :held && args.inputs.mouse.position.inside_rect?(results.playtime_slider_rect.rect)
        audio_entry.playtime = audio_entry.length_ * ((args.inputs.mouse.x - results.playtime_slider_rect.rect.x).to_f / (results.playtime_slider_rect.rect.w - 1.0))
      elsif args.state.mouse_state == :held && args.inputs.mouse.position.inside_rect?(results.gain_slider_rect.rect)
        audio_entry.gain = (args.inputs.mouse.x - results.gain_slider_rect.rect.x).to_f / (results.gain_slider_rect.rect.w - 1.0)
      elsif args.inputs.mouse.click && args.inputs.mouse.position.inside_rect?(results.looping_checkbox_rect.rect)
        audio_entry.looping = !audio_entry.looping
      elsif args.inputs.mouse.click && args.inputs.mouse.position.inside_rect?(results.paused_checkbox_rect.rect)
        audio_entry.paused = !audio_entry.paused
      elsif args.inputs.mouse.click && args.inputs.mouse.position.inside_rect?(results.delete_button_rect.rect)
        args.audio.delete args.state.selected
      end
    end
    
    def render_sources args
      args.outputs.primitives << args.audio.keys.map do |k|
        s = args.audio[k]
    
        isselected = (k == args.state.selected)
    
        color = isselected ? [ 0, 255, 0, 255 ] : [ 0, 0, 255, 255 ]
        [
          [s.screenx, s.screeny, args.state.boxsize, args.state.boxsize, *color].solid,
    
          {
            x: s.screenx + args.state.boxsize.half,
            y: s.screeny,
            text: s.name,
            r: 255,
            g: 255,
            b: 255,
            alignment_enum: 1
          }.label!
        ]
      end
    end
    
    def playtime_str t
      return "" unless t
      minutes = (t / 60.0).floor
      seconds = t - (minutes * 60.0).to_f
      return minutes.to_s + ':' + seconds.floor.to_s + ((seconds - seconds.floor).to_s + "000")[1..3]
    end
    
    def label_with_drop_shadow x, y, text
      [
        { x: x + 1, y: y + 1, text: text, vertical_alignment_enum: 1, alignment_enum: 1, r:   0, g:   0, b:   0 }.label!,
        { x: x + 2, y: y + 0, text: text, vertical_alignment_enum: 1, alignment_enum: 1, r:   0, g:   0, b:   0 }.label!,
        { x: x + 0, y: y + 1, text: text, vertical_alignment_enum: 1, alignment_enum: 1, r: 200, g: 200, b: 200 }.label!
      ]
    end
    
    def check_box opts = {}
      checkbox_template = Layout.rect(w: 0.5, h: 0.5, col: 2)
      final_rect = checkbox_template.center_inside_rect_y(Layout.rect(row: opts.row, col: opts.col))
      color = { r:   0, g:   0, b:   0 }
      color = { r: 255, g: 255, b: 255 } if opts.checked
    
      {
        rect: final_rect,
        primitives: [
          (final_rect.to_solid color)
        ]
      }
    end
    
    def progress_bar opts = {}
      outer_rect  = Layout.rect(row: opts.row, col: opts.col, w: 5, h: 1)
      color = opts.percentage * 255
      baseline_progress_bar = opts.args
                                  .layout
                                  .rect(w: 5, h: 0.5)
    
      final_rect = baseline_progress_bar.center_inside_rect(outer_rect)
      center = Geometry.rect_center_point(final_rect)
    
      {
        rect: final_rect,
        primitives: [
          final_rect.merge(r: color, g: color, b: color, a: 128).solid!,
          label_with_drop_shadow(center.x, center.y, opts.text)
        ]
      }
    end
    
    def panel_primitives args, audio_entry
      results = { primitives: [] }
    
      return results unless audio_entry
    
      # this uses DRGTK's layout apis to layout the controls
      # imagine the screen is split into equal cells (24 cells across, 12 cells up and down)
      # Layout.rect returns a hash which we merge values with to create primitives
      # using Layout.rect removes the need for pixel pushing
    
      # args.outputs.debug << Layout.debug_primitives(r: 255, g: 255, b: 255)
    
      white_color = { r: 255, g: 255, b: 255 }
      label_style = white_color.merge(vertical_alignment_enum: 1)
    
      # panel background
      results.primitives << Layout.rect(row: 0, col: 0, w: 7, h: 6, include_col_gutter: true, include_row_gutter: true)
                                       .border!(r: 255, g: 255, b: 255)
    
      # title
      results.primitives << Layout.point(row: 0, col: 3.5, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text:           "Source #{args.state.selected} (#{args.audio[args.state.selected].name})",
                                              size_enum:      3,
                                              alignment_enum: 1)
    
      # seperator line
      results.primitives << Layout.rect(row: 1, col: 0, w: 7, h: 0)
                                       .line!(white_color)
    
      # screen location
      results.primitives << Layout.point(row: 1.0, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "screen:")
    
      results.primitives << Layout.point(row: 1.0, col: 2, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "(#{audio_entry.screenx.to_i}, #{audio_entry.screeny.to_i})")
    
      # position
      results.primitives << Layout.point(row: 1.5, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "position:")
    
      results.primitives << Layout.point(row: 1.5, col: 2, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "(#{audio_entry[:x].round(5).to_s[0..6]}, #{audio_entry[:y].round(5).to_s[0..6]})")
    
      results.primitives << Layout.point(row: 2.0, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "pitch:")
    
      results.pitch_slider_rect = progress_bar(row: 2.0, col: 2,
                                               percentage: audio_entry.pitch / 2.0,
                                               text: "#{audio_entry.pitch.to_sf}",
                                               args: args)
    
      results.primitives << results.pitch_slider_rect.primitives
    
      results.primitives << Layout.point(row: 2.5, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "playtime:")
    
      results.playtime_slider_rect = progress_bar(args: args,
                                                  row:  2.5,
                                                  col:  2,
                                                  percentage: (audio_entry.playtime || 1) / (audio_entry.length_ || 1),
                                                  text: "#{playtime_str(audio_entry.playtime)} / #{playtime_str(audio_entry.length_)}")
    
      results.primitives << results.playtime_slider_rect.primitives
    
      results.primitives << Layout.point(row: 3.0, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "gain:")
    
      results.gain_slider_rect = progress_bar(args: args,
                                              row:  3.0,
                                              col:  2,
                                              percentage: audio_entry.gain,
                                              text: "#{audio_entry.gain.to_sf}")
    
      results.primitives << results.gain_slider_rect.primitives
    
    
      results.primitives << Layout.point(row: 3.5, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "looping:")
    
      checkbox_template = Layout.rect(w: 0.5, h: 0.5, col: 2)
    
      results.looping_checkbox_rect = check_box(args: args, row: 3.5, col: 2, checked: audio_entry.looping)
      results.primitives << results.looping_checkbox_rect.primitives
    
      results.primitives << Layout.point(row: 4.0, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "paused:")
    
      checkbox_template = Layout.rect(w: 0.5, h: 0.5, col: 2)
    
      results.paused_checkbox_rect = check_box(args: args, row: 4.0, col: 2, checked: !audio_entry.paused)
      results.primitives << results.paused_checkbox_rect.primitives
    
      results.delete_button_rect = { rect: Layout.rect(row: 5, col: 0, w: 7, h: 1) }
    
      results.primitives << results.delete_button_rect.rect.to_solid(r: 180)
    
      results.primitives << Layout.point(row: 5, col: 3.5, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "DELETE", alignment_enum: 1)
    
      return results
    end
    
    def render_panel args
      args.state.panel = nil
      audio_entry = args.audio[args.state.selected]
      return unless audio_entry
    
      mouse_down = (args.state.mouse_held >= 0)
      args.state.panel = panel_primitives args, audio_entry
      args.outputs.primitives << args.state.panel.primitives
    end
    
    def new_sound_id! args
      args.state.sound_id ||= 0
      args.state.sound_id  += 1
      args.state.sound_id
    end
    
    def render_launcher args
      args.outputs.primitives << args.state.spawn_sound_buttons.map(&:primitives)
    end
    
    def render_ui args
      render_launcher args
      render_panel args
    end
    
    def tick args
      defaults args
      render args
      input args
    end
    
    def input args
      if !args.audio[args.state.selected]
        args.state.selected = nil
        args.state.dragging = nil
      end
    
      # spawn button and node interaction
      if args.inputs.mouse.click
        spawn_sound_button = args.state.spawn_sound_buttons.find { |b| args.inputs.mouse.inside_rect? b.rect }
    
        audio_click_key, audio_click_value = args.audio.find do |k, v|
          args.inputs.mouse.inside_rect? [v.screenx, v.screeny, args.state.boxsize, args.state.boxsize]
        end
    
        if spawn_sound_button
          args.state.selected = nil
          spawn_new_sound args, spawn_sound_button.name, spawn_sound_button.path
        elsif audio_click_key
          args.state.selected = audio_click_key
        end
      end
    
      if args.state.mouse_state == :held && args.state.selected
        v = args.audio[args.state.selected]
        if args.inputs.mouse.inside_rect? [v.screenx, v.screeny, args.state.boxsize, args.state.boxsize]
          args.state.dragging = args.state.selected
        end
    
        if args.state.dragging
          s = args.audio[args.state.selected]
          # you can hang anything on the audio hashes you want, so we store the
          #  actual screen position so it doesn't scale weirdly vs your mouse.
          s.screenx = args.inputs.mouse.x - (args.state.boxsize / 2)
          s.screeny = args.inputs.mouse.y - (args.state.boxsize / 2)
    
          s.screeny = 50 if s.screeny < 50
          s.screeny = (719 - args.state.boxsize) if s.screeny > (719 - args.state.boxsize)
          s.screenx = 0 if s.screenx < 0
          s.screenx = (1279 - args.state.boxsize) if s.screenx > (1279 - args.state.boxsize)
    
          s.x = ((s.screenx / 1279.0) * 2.0) - 1.0  # scale to -1.0 - 1.0 range
          s.y = ((s.screeny / 719.0) * 2.0) - 1.0   # scale to -1.0 - 1.0 range
        end
      elsif args.state.mouse_state == :released
        args.state.dragging = nil
      end
    
      input_panel args
    end
    
    def defaults args
      args.state.mouse_state      ||= :released
      args.state.dragging_source  ||= false
      args.state.selected         ||= 0
      args.state.next_sound_index ||= 0
      args.state.boxsize          ||= 30
      args.state.sound_files      ||= [
        { name: :tada,   path: "sounds/tada.wav"   },
        { name: :splash, path: "sounds/splash.wav" },
        { name: :drum,   path: "sounds/drum.mp3"   },
        { name: :spring, path: "sounds/spring.wav" },
        { name: :music,  path: "sounds/music.ogg"  }
      ]
    
      # generate buttons based off the sound collection above
      args.state.spawn_sound_buttons ||= begin
        # create a group of buttons
        # column centered (using col_offset to calculate the column offset)
        # where each item is 2 columns apart
        rects = Layout.rect_group row:   11,
                                       col_offset: {
                                         count: args.state.sound_files.length,
                                         w:     2
                                       },
                                       dcol:  2,
                                       w:     2,
                                       h:     1,
                                       group: args.state.sound_files
    
        # now that you have the rects
        # construct the metadata for the buttons
        rects.map do |rect|
          {
            rect: rect,
            name: rect.name,
            path: rect.path,
            primitives: [
              rect.to_border(r: 255, g: 255, b: 255),
              rect.to_label(x: rect.center_x,
                            y: rect.center_y,
                            text: "#{rect.name}",
                            alignment_enum: 1,
                            vertical_alignment_enum: 1,
                            r: 255, g: 255, b: 255)
            ]
          }
        end
      end
    
      if args.inputs.mouse.up
        args.state.mouse_state = :released
        args.state.dragging_source = false
      elsif args.inputs.mouse.down
        args.state.mouse_state = :held
      end
    
      args.outputs.background_color = [ 0, 0, 0, 255 ]
    end
    
    def render args
      render_ui args
      render_sources args
    end
    
    

    Sound Synthesis - main.rb link

    # ./samples/07_advanced_audio/02_sound_synthesis/app/main.rb
    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.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 render args
        args.outputs.borders << args.state.buttons.map { |b| b[:border] }
        args.outputs.labels  << args.state.buttons.map { |b| b[:label]  }
      end
    
      def input args
        args.state.buttons.each do |b|
          if args.inputs.mouse.click && (args.inputs.mouse.click.inside_rect? b[:rect])
            parameter_string = (b.slice :frequency, :note, :octave).map { |k, v| "#{k}: #{v}" }.join ", "
            GTK.notify! "#{b[:method_to_call]} #{parameter_string}"
            send b[:method_to_call], args, b
          end
        end
    
        if args.inputs.mouse.click && (args.inputs.mouse.click.inside_rect? (Layout.rect(row: 0).yield_self { |r| r.merge y: r.y + r.h.half, h: r.h.half }))
          GTK.openurl 'https://www.youtube.com/watch?v=zEzovM5jT-k&ab_channel=AmirRajan'
        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
        to_queue.each { |a| args.audio[a[:id]] = a }
    
        args.audio.find_all { |k, v| v[:decay_rate] }
          .each     { |k, v| v[:gain] -= v[:decay_rate] }
    
        sounds_to_stop = args.audio
                           .find_all { |k, v| v[:stop_at] && Kernel.tick_count >= v[:stop_at] }
                           .map { |k, v| k }
    
        sounds_to_stop.each { |k| args.audio.delete k }
      end
    end
    
    begin # region: button definitions, ui layout, callback functions
      def button args, opts
        button_def = opts.merge rect: (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
    
        button_def
      end
    
      def play_sine_wave args, sender
        queue_sine_wave args,
                        frequency: sender[:frequency],
                        duration: 1.seconds,
                        fade_out: true
      end
    
      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
    
      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 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
    
      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
    
      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
    
      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
    
    begin # region: wave generation
      begin # sine wave
        def defaults_sine_wave_for
          { frequency: 440, sample_rate: 48000 }
        end
    
        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
    
      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
    
      begin # region: saw tooth wave
        def defaults_saw_tooth_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 defaults_queue_saw_tooth_wave
          { frequency: 440, duration: 60, gain: 0.3, fade_out: false, queue_in: 0 }
        end
    
        def queue_saw_tooth_wave args, opts = {}
          opts        = defaults_queue_saw_tooth_wave.merge opts
          frequency   = opts[:frequency]
          sample_rate = 48000
    
          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
    
          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
    
      begin # region: triangle wave
        def defaults_triangle_wave_for
          { frequency: 440, sample_rate: 48000 }
        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 defaults_queue_triangle_wave
          { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 }
        end
    
        def queue_triangle_wave args, opts = {}
          opts        = defaults_queue_triangle_wave.merge opts
          frequency   = opts[:frequency]
          sample_rate = 48000
    
          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
    
      begin # region: bell
        def defaults_queue_bell
          { frequency: 440, duration: 1.seconds, queue_in: 0 }
        end
    
        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
    
      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 defaults_new_audio_state
          { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 }
        end
    
        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
    
          {
            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:         Kernel.tick_count + opts[:queue_in],
            decay_rate:       decay_rate,
            pitch:            1.0,
            looping:          true,
            paused:           false
          }
        end
    
        def queue_audio args, opts = {}
          graph_wave args, opts[:wave], opts[:audio_state][:frequency]
          args.state.audio_queue << opts[:audio_state]
        end
    
        def new_id! args
          args.state.audio_id ||= 0
          args.state.audio_id  += 1
        end
    
        def graph_wave args, wave, frequency
          if Kernel.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 = 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 = Kernel.tick_count
        end
      end
    
      begin # region: musical note mapping
        def defaults_frequency_for
          { note: :a, octave: 5, sharp:  false, flat:   false }
        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
          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
    
    

    Rhythm Game Calibration - main.rb link

    # ./samples/07_advanced_audio/03_rhythm_game_calibration/app/main.rb
    def tick args
      defaults args
      tick_audio args
      tick_calibration args
    
      if Kernel.tick_count > args.state.start_playing_on_tick
        args.state.beat_accumulator += args.state.beats_per_tick
        args.state.quarter_beat = args.state.beat_accumulator.to_i
        args.state.previous_quarter_beat ||= args.state.quarter_beat
      end
    
      if args.state.previous_quarter_beat != args.state.quarter_beat
        args.state.previous_quarter_beat_at = args.state.quarter_beat
        args.state.quarter_beat_occurred_at = Kernel.tick_count
      end
    
      if (Kernel.tick_count - args.state.quarter_beat_occurred_at + args.state.calibration_ticks).abs == 0
        args.state.fx_queue << { x: 640,
                                 y: 360,
                                 w: 100,
                                 h: 100,
                                 r: 255,
                                 anchor_x: 0.5,
                                 anchor_y: 0.5,
                                 g: 0,
                                 b: 0,
                                 a: 255,
                                 path: :solid }
    
        args.state.fx_queue << { x: 640,
                                 y: 360,
                                 w: 100,
                                 h: 100,
                                 r: 255,
                                 anchor_x: 0.5,
                                 anchor_y: 0.5,
                                 g: 0,
                                 b: 0,
                                 a: 255,
                                 d_size: 20,
                                 path: :solid }
      end
    
      if args.inputs.keyboard.key_down.space || args.inputs.controller_one.key_down.a
        input_diff = (Kernel.tick_count - args.state.quarter_beat_occurred_at + args.state.calibration_ticks)
        if input_diff.abs <= 1
          args.state.label_fx_queue << { x: 640,
                                         y: 360,
                                         anchor_x: 0.5,
                                         anchor_y: 0.5,
                                         text: "perfect! (#{input_diff})" }
        elsif input_diff.abs <= 3
          args.state.label_fx_queue << { x: 640,
                                         y: 360,
                                         anchor_x: 0.5,
                                         anchor_y: 0.5,
                                         text: "great! (#{input_diff})" }
        elsif input_diff.abs <= 5
          args.state.label_fx_queue << { x: 640,
                                         y: 360,
                                         anchor_x: 0.5,
                                         anchor_y: 0.5,
                                         text: "okay... (#{input_diff})" }
        else
          args.state.label_fx_queue << { x: 640,
                                         y: 360,
                                         anchor_x: 0.5,
                                         anchor_y: 0.5,
                                         text: "bad :-( (#{input_diff})" }
        end
      end
    
      calc_fx_queues args
      render args
    end
    
    def defaults args
      args.state.track_length_in_ticks     ||= 2057
      args.state.main_track                ||= :track_1
      args.state.other_track               ||= :track_2
      args.state.fx_queue                  ||= []
      args.state.label_fx_queue            ||= []
      args.state.play_head                 ||= 0
      args.state.start_playing_on_tick     ||= 180
      args.state.beats_per_minute          ||= 140
      args.state.beats_per_second          ||= args.state.beats_per_minute / 60.0
      args.state.beats_per_tick            ||= args.state.beats_per_second / 60.0
      args.state.beat_accumulator          ||= 0
      args.state.quarter_beat              ||= 0
      args.state.calibration_ticks         ||= 0
      args.state.quarter_beat_interval     ||= 1.fdiv(args.state.beats_per_tick).to_i
      args.state.quarter_beat_inputs       ||= 0
      args.state.quarter_beat_diff_history ||= []
    end
    
    def tick_audio args
      return if Kernel.tick_count < args.state.start_playing_on_tick
    
      # start up audio
      args.audio[:track_1] ||= {
        input: "sounds/music.ogg",
        gain: 1.0,
        looping: false
      }
    
      args.audio[:track_2] ||= {
        input: "sounds/music.ogg",
        looping: false,
        gain: 0.0
      }
    
      # play head increment every tick
      args.state.play_head += 1
      args.state.play_head = args.state.play_head % args.state.track_length_in_ticks
    
      # every 10 seconds, cross fade
      if args.state.play_head.zmod?(600) && Kernel.tick_count > args.state.start_playing_on_tick
        if args.state.main_track == :track_1
          args.state.main_track = :track_2
          args.state.other_track = :track_1
        else
          args.state.main_track = :track_1
          args.state.other_track = :track_2
        end
    
        if args.audio[args.state.main_track]
          args.audio[args.state.main_track].playtime = args.state.play_head.idiv(60)
        end
      end
    
      # perform cross fade
      if args.audio[args.state.main_track]
        args.audio[args.state.main_track].gain += 0.1
        args.audio[args.state.main_track].gain = 1.0 if args.audio[args.state.main_track].gain > 1.0
      end
    
      if args.audio[args.state.other_track]
        args.audio[args.state.other_track].gain -= 0.1
        args.audio[args.state.other_track].gain = 0.0 if args.audio[args.state.other_track].gain < 0.0
      end
    end
    
    def tick_calibration args
      if args.inputs.keyboard.key_down.up || args.inputs.controller_one.key_down.up
        args.state.calibration_ticks += 1
      elsif args.inputs.keyboard.key_down.down || args.inputs.controller_one.key_down.down
        args.state.calibration_ticks -= 1
      end
    
      if args.inputs.keyboard.key_down.m || args.inputs.controller_one.key_down.b
        args.state.player_beat_at = Kernel.tick_count
      else
        args.state.player_beat_at = nil
      end
    
      if args.state.player_beat_at && args.state.quarter_beat_occurred_at
        diff = args.state.player_beat_at - args.state.quarter_beat_occurred_at
        description = if (diff + args.state.calibration_ticks) < 0
                        "early: increase calibration value"
                      elsif (diff + args.state.calibration_ticks) > 0
                        "late:  decrease calibration value"
                      else
                        "perfect"
                      end
    
        quarter_beat_diff = { diff: (diff + args.state.calibration_ticks), description: description }
        args.state.quarter_beat_diff_history.unshift quarter_beat_diff.copy
        if args.state.quarter_beat_diff_history.length > 20
          args.state.quarter_beat_diff_history = args.state.quarter_beat_diff_history.take 20
        end
      end
    end
    
    def calc_fx_queues args
      args.state.fx_queue.each do |fx|
        fx.at ||= Kernel.tick_count
        fx.d_size ||= 0
        fx.w += fx.d_size
        fx.h += fx.d_size
      end
    
      args.state.fx_queue.reject! { |fx| fx.at.elapsed_time > 5 }
    
      args.state.label_fx_queue.each do |fx|
        fx.at ||= Kernel.tick_count
        fx.a  ||= 255
        fx.y    = fx.y.lerp(540, 0.1)
        fx.a   -= 5
      end
    
      args.state.label_fx_queue.reject! { |fx| fx.a <= 0 }
    end
    
    def render args
      if Kernel.tick_count < args.state.start_playing_on_tick
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: "Count down: #{(args.state.start_playing_on_tick - Kernel.tick_count).idiv(60) + 1}",
                                 anchor_x: 0.5,
                                 anchor_y: 0.5 }
      end
    
      args.outputs.borders << { x: 640, y: 360, w: 100, h: 100,
                                anchor_x: 0.5, anchor_y: 0.5,
                                r: 255, g: 0, b: 0, a: 255 }
    
      args.outputs.primitives << args.state.fx_queue
      args.outputs.primitives << args.state.label_fx_queue
      args.state.previous_quarter_beat = args.state.quarter_beat
    
      args.outputs.debug.watch "Instructions: Close your eyes and listen to the beat and press 'M' (or 'B' on your controller) when you hear a quarter beat."
      args.outputs.debug.watch "              Press 'UP' or 'DOWN' to adjust calibration_ticks."
      args.outputs.debug.watch "              Press 'SPACE' (or 'A' on your controller) on quarter beats to test calibration."
    
      if args.audio[:track_1] && args.audio[:track_2]
        args.outputs.debug.watch "track_1 gain: #{args.audio[:track_1].gain.to_sf}"
        args.outputs.debug.watch "track_2 gain: #{args.audio[:track_2].gain.to_sf}"
      end
    
      args.outputs.debug.watch "beat accumulator: #{args.state.beat_accumulator.to_sf}"
      args.outputs.debug.watch "quarter beat: #{args.state.quarter_beat}"
      args.outputs.debug.watch "calibration_ticks: #{args.state.calibration_ticks.to_i}"
      args.state.quarter_beat_diff_history.each do |item|
        if item.diff >= 0
          args.outputs.debug.watch "+#{item.diff.to_sf} #{item.description}"
        elsif item.diff < 0
          args.outputs.debug.watch "#{item.diff.to_sf} #{item.description}"
        end
      end
    end
    
    

    Advanced Rendering link

    Render Targets Clip Area - main.rb link

    # ./samples/07_advanced_rendering/01_render_targets_clip_area/app/main.rb
    def tick args
      # define your state
      args.state.player ||= { x: 0, y: 0, w: 300, h: 300, path: "sprites/square/blue.png" }
    
      # controller input for player
      args.state.player.x += args.inputs.left_right * 5
      args.state.player.y += args.inputs.up_down * 5
    
      # create a render target that holds the
      # full view that you want to render
    
      # make the background transparent
      args.outputs[:clipped_area].background_color = [0, 0, 0, 0]
    
      # set the w/h to match the screen
      args.outputs[:clipped_area].w = 1280
      args.outputs[:clipped_area].h = 720
    
      # render the player in the render target
      args.outputs[:clipped_area].sprites << args.state.player
    
      # render the player and clip area as borders to
      # keep track of where everything is at regardless of clip mode
      args.outputs.borders << args.state.player
      args.outputs.borders << { x: 540, y: 460, w: 200, h: 200 }
    
      # render the render target, but only the clipped area
      args.outputs.sprites << {
        # where to render the render target
        x: 540,
        y: 460,
        w: 200,
        h: 200,
        # what part of the render target to render
        source_x: 540,
        source_y: 460,
        source_w: 200,
        source_h: 200,
        # path of render target to render
        path: :clipped_area
      }
    
      # mini map
      args.outputs.borders << { x: 1280 - 160, y: 0, w: 160, h: 90 }
      args.outputs.sprites << { x: 1280 - 160, y: 0, w: 160, h: 90, path: :clipped_area }
    end
    
    GTK.reset
    
    

    Render Targets Combining Sprites - main.rb link

    # ./samples/07_advanced_rendering/01_render_targets_combining_sprites/app/main.rb
    # sample app shows how to use a render target to
    # create a combined sprite
    def tick args
      create_combined_sprite args
    
      # render the combined sprite
      # using its name :two_squares
      # have it move across the screen and rotate
      args.outputs.sprites << { x: Kernel.tick_count % 1280,
                                y: 0,
                                w: 80,
                                h: 80,
                                angle: Kernel.tick_count,
                                path: :two_squares }
    end
    
    def create_combined_sprite args
      # NOTE: you can have the construction of the combined
      #       sprite to happen every tick or only once (if the
      #       combined sprite never changes).
      #
      # if the combined sprite never changes, comment out the line
      # below to only construct it on the first frame and then
      # use the cached texture
      # return if Kernel.tick_count != 0 # <---- guard clause to only construct on first frame and cache
    
      # define the dimensions of the combined sprite
      # the name of the combined sprite is :two_squares
      args.outputs[:two_squares].w = 80
      args.outputs[:two_squares].h = 80
    
      # put a blue sprite within the combined sprite
      # who's width is "thin"
      args.outputs[:two_squares].sprites << {
        x: 40 - 10,
        y: 0,
        w: 20,
        h: 80,
        path: 'sprites/square/blue.png'
      }
    
      # put a red sprite within the combined sprite
      # who's height is "thin"
      args.outputs[:two_squares].sprites << {
        x: 0,
        y: 40 - 10,
        w: 80,
        h: 20,
        path: 'sprites/square/red.png'
      }
    end
    
    

    Simple Render Targets - main.rb link

    # ./samples/07_advanced_rendering/01_simple_render_targets/app/main.rb
    def tick args
      # args.outputs.render_targets are really really powerful.
      # They essentially allow you to create a sprite programmatically and cache the result.
    
      # Create a render_target of a :block and a :gradient on tick zero.
      if Kernel.tick_count == 0
        args.render_target(:block).solids << [0, 0, 1280, 100]
    
        # The gradient is actually just a collection of black solids with increasing
        # opacities.
        args.render_target(:gradient).solids << 90.map_with_index do |x|
          50.map_with_index do |y|
            [x * 15, y * 15, 15, 15, 0, 0, 0, (x * 3).fdiv(255) * 255]
          end
        end
      end
    
      # Take the :block render_target and present it horizontally centered.
      # Use a subsection of the render_targetd specified by source_x,
      # source_y, source_w, source_h.
      args.outputs.sprites << { x: 0,
                                y: 310,
                                w: 1280,
                                h: 100,
                                path: :block,
                                source_x: 0,
                                source_y: 0,
                                source_w: 1280,
                                source_h: 100 }
    
      # After rendering :block, render gradient on top of :block.
      args.outputs.sprites << [0, 0, 1280, 720, :gradient]
    
      args.outputs.labels  << [1270, 710, GTK.current_framerate, 0, 2, 255, 255, 255]
      tick_instructions args, "Sample app shows how to use render_targets (programmatically create cached sprites)."
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    GTK.reset
    
    

    Coordinate Systems And Render Targets - main.rb link

    # ./samples/07_advanced_rendering/02_coordinate_systems_and_render_targets/app/main.rb
    def tick args
      # every 4.5 seconds, swap between origin_bottom_left and origin_center
      args.state.origin_state ||= :bottom_left
    
      if Kernel.tick_count.zmod? 270
        args.state.origin_state = if args.state.origin_state == :bottom_left
                                    :center
                                  else
                                    :bottom_left
                                  end
      end
    
      if args.state.origin_state == :bottom_left
        tick_origin_bottom_left args
      else
        tick_origin_center args
      end
    end
    
    def tick_origin_center args
      # set the coordinate system to origin_center
      args.grid.origin_center!
      args.outputs.labels <<  { x: 0, y: 100, text: "args.grid.origin_center! with sprite inside of a render target, centered at 0, 0", vertical_alignment_enum: 1, alignment_enum: 1 }
    
      # create a render target with a sprint in the center assuming the origin is center screen
      args.outputs[:scene].sprites << { x: -50, y: -50, w: 100, h: 100, path: 'sprites/square/blue.png' }
      args.outputs.sprites << { x: -640, y: -360, w: 1280, h: 720, path: :scene }
    end
    
    def tick_origin_bottom_left args
      args.grid.origin_bottom_left!
      args.outputs.labels <<  { x: 640, y: 360 + 100, text: "args.grid.origin_bottom_left! with sprite inside of a render target, centered at 640, 360", vertical_alignment_enum: 1, alignment_enum: 1 }
    
      # create a render target with a sprint in the center assuming the origin is bottom left
      args.outputs[:scene].sprites << { x: 640 - 50, y: 360 - 50, w: 100, h: 100, path: 'sprites/square/blue.png' }
      args.outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: :scene }
    end
    
    

    Render Targets Repeating Texture - main.rb link

    # ./samples/07_advanced_rendering/02_render_targets_repeating_texture/app/main.rb
    # Sample app shows how to leverage render targets to create a repeating
    # texture given a source sprite.
    def tick args
      args.outputs.sprites << repeating_texture(args,
                                                x: 640,
                                                y: 360,
                                                w: 1280,
                                                h: 720,
                                                anchor_x: 0.5,
                                                anchor_y: 0.5,
                                                path: 'sprites/square/blue.png')
    end
    
    def repeating_texture args, x:, y:, w:, h:, path:, anchor_x: 0, anchor_y: 0
      # create an area to store state for function
      args.state.repeating_texture_lookup ||= {}
    
      # create a unique name for the repeating texture
      rt_name = "#{path.hash}-#{w}-#{h}"
    
      # if the repeating texture has not been created yet, create it
      if args.state.repeating_texture_lookup[rt_name]
        return { x: x,
                 y: y,
                 w: w,
                 h: h,
                 anchor_x: anchor_x,
                 anchor_y: anchor_y,
                 path: rt_name }
      end
    
      # create a render target to store the repeating texture
      args.outputs[rt_name].w = w
      args.outputs[rt_name].h = h
    
      # calculate the sprite box for the repeating texture
      sprite_w, sprite_h = GTK.calcspritebox path
    
      # calculate the number of rows and columns needed to fill the repeating texture
      rows = h.idiv(sprite_h) + 1
      cols = w.idiv(sprite_w) + 1
    
      # generate the repeating texture using a render target
      # this only needs to be done once and will be cached
      args.outputs[rt_name].sprites << rows.map do |r|
                                         cols.map do |c|
                                           { x: sprite_w * c,
                                             y:  h - sprite_h * (r + 1),
                                             w: sprite_w,
                                             h: sprite_h,
                                             path: path }
                                         end
                                       end
    
      # store a flag in state denoting that the repeating
      # texture has been generated
      args.state.repeating_texture_lookup[rt_name] = true
    
      # return the repeating texture
      repeating_texture args, x: x, y: y, w: w, h: h, path: path
    end
    
    GTK.reset
    
    

    Render Targets Thick Lines - main.rb link

    # ./samples/07_advanced_rendering/02_render_targets_thick_lines/app/main.rb
    # Sample app shows how you can use render targets to create arbitrary shapes like a thicker line
    def tick args
      args.state.line_cache ||= {}
      args.outputs.primitives << thick_line(args,
                                            args.state.line_cache,
                                            x: 0, y: 0, x2: 640, y2: 360, thickness: 3).merge(r: 0, g: 0, b: 0)
    end
    
    def thick_line args, cache, line
      line_length = Math.sqrt((line.x2 - line.x)**2 + (line.y2 - line.y)**2)
      name = "line-sprite-#{line_length}-#{line.thickness}"
      cached_line = cache[name]
      line_angle = Math.atan2(line.y2 - line.y, line.x2 - line.x) * 180 / Math::PI
      if cached_line
        perpendicular_angle = (line_angle + 90) % 360
        return cached_line.sprite.merge(x: line.x - perpendicular_angle.vector_x * (line.thickness / 2),
                                        y: line.y - perpendicular_angle.vector_y * (line.thickness / 2),
                                        angle: line_angle)
      end
    
      cache[name] = {
        line: line,
        thickness: line.thickness,
        sprite: {
          w: line_length,
          h: line.thickness,
          path: name,
          angle_anchor_x: 0,
          angle_anchor_y: 0
        }
      }
    
      args.outputs[name].w = line_length
      args.outputs[name].h = line.thickness
      args.outputs[name].solids << { x: 0, y: 0, w: line_length, h: line.thickness, r: 255, g: 255, b: 255 }
      return thick_line args, cache, line
    end
    
    

    Render Targets With Tile Manipulation - main.rb link

    # ./samples/07_advanced_rendering/02_render_targets_with_tile_manipulation/app/main.rb
    # This sample is meant to show you how to do that dripping transition thing
    #  at the start of the original Doom. Most of this file is here to animate
    #  a scene to wipe away; the actual wipe effect is in the last 20 lines or
    #  so.
    
    GTK.reset   # reset all game state if reloaded.
    
    def circle_of_blocks pass, xoffset, yoffset, angleoffset, blocksize, distance
      numblocks = 10
    
      for i in 1..numblocks do
        angle = ((360 / numblocks) * i) + angleoffset
        radians = angle * (Math::PI / 180)
        x = (xoffset + (distance * Math.cos(radians))).round
        y = (yoffset + (distance * Math.sin(radians))).round
        pass.solids << [ x, y, blocksize, blocksize, 255, 255, 0 ]
      end
    end
    
    def draw_scene args, pass
      pass.solids << [0, 360, 1280, 360, 0, 0, 200]
      pass.solids << [0, 0, 1280, 360, 0, 127, 0]
    
      blocksize = 100
      angleoffset = Kernel.tick_count * 2.5
      centerx = (1280 - blocksize) / 2
      centery = (720 - blocksize) / 2
    
      circle_of_blocks pass, centerx, centery, angleoffset, blocksize * 2, 500
      circle_of_blocks pass, centerx, centery, angleoffset, blocksize, 325
      circle_of_blocks pass, centerx, centery, angleoffset, blocksize / 2, 200
      circle_of_blocks pass, centerx, centery, angleoffset, blocksize / 4, 100
    end
    
    def tick args
      segments = 160
    
      # On the first tick, initialize some stuff.
      if !args.state.yoffsets
        args.state.baseyoff = 0
        args.state.yoffsets = []
        for i in 0..segments do
          args.state.yoffsets << rand * 100
        end
      end
    
      # Just draw some random stuff for a few seconds.
      args.state.static_debounce ||= 60 * 2.5
      if args.state.static_debounce > 0
        last_frame = args.state.static_debounce == 1
        target = last_frame ? args.render_target(:last_frame) : args.outputs
        draw_scene args, target
        args.state.static_debounce -= 1
        return unless last_frame
      end
    
      # build up the wipe...
    
      # this is the thing we're wiping to.
      args.outputs.sprites << [ 0, 0, 1280, 720, 'dragonruby.png' ]
    
      return if (args.state.baseyoff > (1280 + 100))  # stop when done sliding
    
      segmentw = 1280 / segments
    
      x = 0
      for i in 0..segments do
        yoffset = 0
        if args.state.yoffsets[i] < args.state.baseyoff
          yoffset = args.state.baseyoff - args.state.yoffsets[i]
        end
    
        # (720 - yoffset) flips the coordinate system, (- 720) adjusts for the height of the segment.
        args.outputs.sprites << [ x, (720 - yoffset) - 720, segmentw, 720, 'last_frame', 0, 255, 255, 255, 255, x, 0, segmentw, 720 ]
        x += segmentw
      end
    
      args.state.baseyoff += 4
    
      tick_instructions args, "Sample app shows an advanced usage of render_target."
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Render Target Viewports - main.rb link

    # ./samples/07_advanced_rendering/03_render_target_viewports/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - 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.)
    
       If you have a solar system and you're creating args.state.sun and setting its image path to an
       image in the sprites folder, you would do the following:
       (See samples/99_sample_nddnug_workshop for more details.)
    
       args.state.sun ||= args.state.new_entity(:sun) do |s|
       s.path = 'sprites/sun.png'
       end
    
     - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated
       as Ruby code, and the placeholder is replaced with its corresponding value or result.
    
       For example, if we have a variable
       name = "Ruby"
       then the line
       puts "How are you, #{name}?"
       would print "How are you, Ruby?" to the console.
       (Remember, string interpolation only works with double quotes!)
    
     - Ternary operator (?): Similar to if statement; first evalulates whether a statement is
       true or false, and then executes a command depending on that result.
       For example, if we had a variable
       grade = 75
       and used the ternary operator in the command
       pass_or_fail = grade > 65 ? "pass" : "fail"
       then the value of pass_or_fail would be "pass" since grade's value was greater than 65.
    
     Reminders:
    
     - args.grid.(left|right|top|bottom): Pixel value for the boundaries of the virtual
       720 p screen (Dragon Ruby Game Toolkits's virtual resolution is always 1280x720).
    
     - Numeric#shift_(left|right|up|down): Shifts the Numeric in the correct direction
       by adding or subracting.
    
     - ARRAY#inside_rect?: An array with at least two values is considered a point. An array
       with at least four values is considered a rect. The inside_rect? function returns true
       or false depending on if the point is inside the rect.
    
     - ARRAY#intersect_rect?: Returns true or false depending on if the two rectangles intersect.
    
     - args.inputs.mouse.click: This property will be set if the mouse was clicked.
       For more information about the mouse, go to mygame/documentation/07-mouse.md.
    
     - args.inputs.keyboard.key_up.KEY: The value of the properties will be set
       to the frame  that the key_up event occurred (the frame correlates
       to Kernel.tick_count).
       For more information about the keyboard, go to mygame/documentation/06-keyboard.md.
    
     - args.state.labels:
       The parameters for a label are
       1. the position (x, y)
       2. the text
       3. the size
       4. the alignment
       5. the color (red, green, and blue saturations)
       6. the alpha (or transparency)
       For more information about labels, go to mygame/documentation/02-labels.md.
    
     - args.state.lines:
       The parameters for a line are
       1. the starting position (x, y)
       2. the ending position (x2, y2)
       3. the color (red, green, and blue saturations)
       4. the alpha (or transparency)
       For more information about lines, go to mygame/documentation/04-lines.md.
    
     - args.state.solids (and args.state.borders):
       The parameters for a solid (or border) are
       1. the position (x, y)
       2. the width (w)
       3. the height (h)
       4. the color (r, g, b)
       5. the alpha (or transparency)
       For more information about solids and borders, go to mygame/documentation/03-solids-and-borders.md.
    
     - args.state.sprites:
       The parameters for a sprite are
       1. the position (x, y)
       2. the width (w)
       3. the height (h)
       4. the image path
       5. the angle
       6. the alpha (or transparency)
       For more information about sprites, go to mygame/documentation/05-sprites.md.
    =end
    
    # This sample app shows different objects that can be used when making games, such as labels,
    # lines, sprites, solids, buttons, etc. Each demo section shows how these objects can be used.
    
    # Also note that Kernel.tick_count refers to the passage of time, or current frame.
    
    class TechDemo
      attr_accessor :inputs, :state, :outputs, :grid, :args
    
      # Calls all methods necessary for the app to run properly.
      def tick
        labels_tech_demo
        lines_tech_demo
        solids_tech_demo
        borders_tech_demo
        sprites_tech_demo
        keyboards_tech_demo
        controller_tech_demo
        mouse_tech_demo
        point_to_rect_tech_demo
        rect_to_rect_tech_demo
        button_tech_demo
        export_game_state_demo
        window_state_demo
        render_seperators
      end
    
      # Shows output of different kinds of labels on the screen
      def labels_tech_demo
        outputs.labels << [grid.left.shift_right(5), grid.top.shift_down(5), "This is a label located at the top left."]
        outputs.labels << [grid.left.shift_right(5), grid.bottom.shift_up(30), "This is a label located at the bottom left."]
        outputs.labels << [ 5, 690, "Labels (x, y, text, size, align, r, g, b, a)"]
        outputs.labels << [ 5, 660, "Smaller label.",  -2]
        outputs.labels << [ 5, 630, "Small label.",    -1]
        outputs.labels << [ 5, 600, "Medium label.",    0]
        outputs.labels << [ 5, 570, "Large label.",     1]
        outputs.labels << [ 5, 540, "Larger label.",    2]
        outputs.labels << [300, 660, "Left aligned.",    0, 2]
        outputs.labels << [300, 640, "Center aligned.",  0, 1]
        outputs.labels << [300, 620, "Right aligned.",   0, 0]
        outputs.labels << [175, 595, "Red Label.",       0, 0, 255,   0,   0]
        outputs.labels << [175, 575, "Green Label.",     0, 0,   0, 255,   0]
        outputs.labels << [175, 555, "Blue Label.",      0, 0,   0,   0, 255]
        outputs.labels << [175, 535, "Faded Label.",     0, 0,   0,   0,   0, 128]
      end
    
      # Shows output of lines on the screen
      def lines_tech_demo
        outputs.labels << [5, 500, "Lines (x, y, x2, y2, r, g, b, a)"]
        outputs.lines  << [5, 450, 100, 450]
        outputs.lines  << [5, 430, 300, 430]
        outputs.lines  << [5, 410, 300, 410, Kernel.tick_count % 255, 0, 0, 255] # red saturation changes
        outputs.lines  << [5, 390 - Kernel.tick_count % 25, 300, 390, 0, 0, 0, 255] # y position changes
        outputs.lines  << [5 + Kernel.tick_count % 200, 360, 300, 360, 0, 0, 0, 255] # x position changes
      end
    
      # Shows output of different kinds of solids on the screen
      def solids_tech_demo
        outputs.labels << [  5, 350, "Solids (x, y, w, h, r, g, b, a)"]
        outputs.solids << [ 10, 270, 50, 50]
        outputs.solids << [ 70, 270, 50, 50, 0, 0, 0]
        outputs.solids << [130, 270, 50, 50, 255, 0, 0]
        outputs.solids << [190, 270, 50, 50, 255, 0, 0, 128]
        outputs.solids << [250, 270, 50, 50, 0, 0, 0, 128 + Kernel.tick_count % 128] # transparency changes
      end
    
      # Shows output of different kinds of borders on the screen
      # The parameters for a border are the same as the parameters for a solid
      def borders_tech_demo
        outputs.labels <<  [  5, 260, "Borders (x, y, w, h, r, g, b, a)"]
        outputs.borders << [ 10, 180, 50, 50]
        outputs.borders << [ 70, 180, 50, 50, 0, 0, 0]
        outputs.borders << [130, 180, 50, 50, 255, 0, 0]
        outputs.borders << [190, 180, 50, 50, 255, 0, 0, 128]
        outputs.borders << [250, 180, 50, 50, 0, 0, 0, 128 + Kernel.tick_count % 128] # transparency changes
      end
    
      # Shows output of different kinds of sprites on the screen
      def sprites_tech_demo
        outputs.labels <<  [   5, 170, "Sprites (x, y, w, h, path, angle, a)"]
        outputs.sprites << [  10, 40, 128, 101, 'dragonruby.png']
        outputs.sprites << [ 150, 40, 128, 101, 'dragonruby.png', Kernel.tick_count % 360] # angle changes
        outputs.sprites << [ 300, 40, 128, 101, 'dragonruby.png', 0, Kernel.tick_count % 255] # transparency changes
      end
    
      # Holds size, alignment, color (black), and alpha (transparency) parameters
      # Using small_font as a parameter accounts for all remaining parameters
      # so they don't have to be repeatedly typed
      def small_font
        [-2, 0, 0, 0, 0, 255]
      end
    
      # Sets position of each row
      # Converts given row value to pixels that DragonRuby understands
      def row_to_px row_number
    
        # Row 0 starts 5 units below the top of the grid.
        # Each row afterward is 20 units lower.
        grid.top.shift_down(5).shift_down(20 * row_number)
      end
    
      # Uses labels to output current game time (passage of time), and whether or not "h" was pressed
      # If "h" is pressed, the frame is output when the key_up event occurred
      def keyboards_tech_demo
        outputs.labels << [460, row_to_px(0), "Current game time: #{Kernel.tick_count}", small_font]
        outputs.labels << [460, row_to_px(2), "Keyboard input: inputs.keyboard.key_up.h", small_font]
        outputs.labels << [460, row_to_px(3), "Press \"h\" on the keyboard.", small_font]
    
        if inputs.keyboard.key_up.h # if "h" key_up event occurs
          state.h_pressed_at = Kernel.tick_count # frame it occurred is stored
        end
    
        # h_pressed_at is initially set to false, and changes once the user presses the "h" key.
        state.h_pressed_at ||= false
    
        if state.h_pressed_at # if h is pressed (pressed_at has a frame number and is no longer false)
          outputs.labels << [460, row_to_px(4), "\"h\" was pressed at time: #{state.h_pressed_at}", small_font]
        else # otherwise, label says "h" was never pressed
          outputs.labels << [460, row_to_px(4), "\"h\" has never been pressed.", small_font]
        end
    
        # border around keyboard input demo section
        outputs.borders << [455, row_to_px(5), 360, row_to_px(2).shift_up(5) - row_to_px(5)]
      end
    
      # Sets definition for a small label
      # Makes it easier to position labels in respect to the position of other labels
      def small_label x, row, message
        [x, row_to_px(row), message, small_font]
      end
    
      # Uses small labels to show whether the "a" button on the controller is down, held, or up.
      # y value of each small label is set by calling the row_to_px method
      def controller_tech_demo
        x = 460
        outputs.labels << small_label(x, 6, "Controller one input: inputs.controller_one")
        outputs.labels << small_label(x, 7, "Current state of the \"a\" button.")
        outputs.labels << small_label(x, 8, "Check console window for more info.")
    
        if inputs.controller_one.key_down.a # if "a" is in "down" state
          outputs.labels << small_label(x, 9, "\"a\" button down: #{inputs.controller_one.key_down.a}")
          puts "\"a\" button down at #{inputs.controller_one.key_down.a}" # prints frame the event occurred
        elsif inputs.controller_one.key_held.a # if "a" is held down
          outputs.labels << small_label(x, 9, "\"a\" button held: #{inputs.controller_one.key_held.a}")
        elsif inputs.controller_one.key_up.a # if "a" is in up state
          outputs.labels << small_label(x, 9, "\"a\" button up: #{inputs.controller_one.key_up.a}")
          puts "\"a\" key up at #{inputs.controller_one.key_up.a}"
        else # if no event has occurred
          outputs.labels << small_label(x, 9, "\"a\" button state is nil.")
        end
    
        # border around controller input demo section
        outputs.borders << [455, row_to_px(10), 360, row_to_px(6).shift_up(5) - row_to_px(10)]
      end
    
      # Outputs when the mouse was clicked, as well as the coordinates on the screen
      # of where the click occurred
      def mouse_tech_demo
        x = 460
    
        outputs.labels << small_label(x, 11, "Mouse input: inputs.mouse")
    
        if inputs.mouse.click # if click has a value and is not nil
          state.last_mouse_click = inputs.mouse.click # coordinates of click are stored
        end
    
        if state.last_mouse_click # if mouse is clicked (has coordinates as value)
          # outputs the time (frame) the click occurred, as well as how many frames have passed since the event
          outputs.labels << small_label(x, 12, "Mouse click happened at: #{state.last_mouse_click.created_at}, #{state.last_mouse_click.created_at_elapsed}")
          # outputs coordinates of click
          outputs.labels << small_label(x, 13, "Mouse click location: #{state.last_mouse_click.point.x}, #{state.last_mouse_click.point.y}")
        else # otherwise if the mouse has not been clicked
          outputs.labels << small_label(x, 12, "Mouse click has not occurred yet.")
          outputs.labels << small_label(x, 13, "Please click mouse.")
        end
      end
    
      # Outputs whether a mouse click occurred inside or outside of a box
      def point_to_rect_tech_demo
        x = 460
    
        outputs.labels << small_label(x, 15, "Click inside the blue box maybe ---->")
    
        box = [765, 370, 50, 50, 0, 0, 170] # blue box
        outputs.borders << box
    
        if state.last_mouse_click # if the mouse was clicked
          if state.last_mouse_click.point.inside_rect? box # if mouse clicked inside box
            outputs.labels << small_label(x, 16, "Mouse click happened inside the box.")
          else # otherwise, if mouse was clicked outside the box
            outputs.labels << small_label(x, 16, "Mouse click happened outside the box.")
          end
        else # otherwise, if was not clicked at all
          outputs.labels << small_label(x, 16, "Mouse click has not occurred yet.") # output if the mouse was not clicked
        end
    
        # border around mouse input demo section
        outputs.borders << [455, row_to_px(14), 360, row_to_px(11).shift_up(5) - row_to_px(14)]
      end
    
      # Outputs a red box onto the screen. A mouse click from the user inside of the red box will output
      # a smaller box. If two small boxes are inside of the red box, it will be determined whether or not
      # they intersect.
      def rect_to_rect_tech_demo
        x = 460
    
        outputs.labels << small_label(x, 17.5, "Click inside the red box below.") # label with instructions
        red_box = [460, 250, 355, 90, 170, 0, 0] # definition of the red box
        outputs.borders << red_box # output as a border (not filled in)
    
        # If the mouse is clicked inside the red box, two collision boxes are created.
        if inputs.mouse.click
          if inputs.mouse.click.point.inside_rect? red_box
            if !state.box_collision_one # if the collision_one box does not yet have a definition
              # Subtracts 25 from the x and y positions of the click point in order to make the click point the center of the box.
              # You can try deleting the subtraction to see how it impacts the box placement.
              state.box_collision_one = [inputs.mouse.click.point.x - 25, inputs.mouse.click.point.y - 25, 50, 50, 180, 0,   0, 180]  # sets definition
            elsif !state.box_collision_two # if collision_two does not yet have a definition
              state.box_collision_two = [inputs.mouse.click.point.x - 25, inputs.mouse.click.point.y - 25, 50, 50,   0, 0, 180, 180] # sets definition
            else
              state.box_collision_one = nil # both boxes are empty
              state.box_collision_two = nil
            end
          end
        end
    
        # If collision boxes exist, they are output onto screen inside the red box as solids
        if state.box_collision_one
          outputs.solids << state.box_collision_one
        end
    
        if state.box_collision_two
          outputs.solids << state.box_collision_two
        end
    
        # Outputs whether or not the two collision boxes intersect.
        if state.box_collision_one && state.box_collision_two # if both collision_boxes are defined (and not nil or empty)
          if state.box_collision_one.intersect_rect? state.box_collision_two # if the two boxes intersect
            outputs.labels << small_label(x, 23.5, 'The boxes intersect.')
          else # otherwise, if the two boxes do not intersect
            outputs.labels << small_label(x, 23.5, 'The boxes do not intersect.')
          end
        else
          outputs.labels << small_label(x, 23.5, '--') # if the two boxes are not defined (are nil or empty), this label is output
        end
      end
    
      # Creates a button and outputs it onto the screen using labels and borders.
      # If the button is clicked, the color changes to make it look faded.
      def button_tech_demo
        x, y, w, h = 460, 160, 300, 50
        state.button        ||= state.new_entity(:button_with_fade)
    
        # Adds w.half to x and h.half + 10 to y in order to display the text inside the button's borders.
        state.button.label  ||= [x + w.half, y + h.half + 10, "click me and watch me fade", 0, 1]
        state.button.border ||= [x, y, w, h]
    
        if inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.button.border) # if mouse is clicked, and clicked inside button's border
          state.button.clicked_at = inputs.mouse.click.created_at # stores the time the click occurred
        end
    
        outputs.labels << state.button.label
        outputs.borders << state.button.border
    
        if state.button.clicked_at # if button was clicked (variable has a value and is not nil)
    
          # The appearance of the button changes for 0.25 seconds after the time the button is clicked at.
          # The color changes (rgb is set to 0, 180, 80) and the transparency gradually changes.
          # Change 0.25 to 1.25 and notice that the transparency takes longer to return to normal.
          outputs.solids << [x, y, w, h, 0, 180, 80, 255 * state.button.clicked_at.ease(0.25.seconds, :flip)]
        end
      end
    
      # Creates a new button by declaring it as a new entity, and sets values.
      def new_button_prefab x, y, message
        w, h = 300, 50
        button        = state.new_entity(:button_with_fade)
        button.label  = [x + w.half, y + h.half + 10, message, 0, 1] # '+ 10' keeps label's text within button's borders
        button.border = [x, y, w, h] # sets border definition
        button
      end
    
      # If the mouse has been clicked and the click's location is inside of the button's border, that means
      # that the button has been clicked. This method returns a boolean value.
      def button_clicked? button
        inputs.mouse.click && inputs.mouse.click.point.inside_rect?(button.border)
      end
    
      # Determines if button was clicked, and changes its appearance if it is clicked
      def tick_button_prefab button
        outputs.labels << button.label # outputs button's label and border
        outputs.borders << button.border
    
        if button_clicked? button # if button is clicked
          button.clicked_at = inputs.mouse.click.created_at # stores the time that the button was clicked
        end
    
        if button.clicked_at # if clicked_at has a frame value and is not nil
          # button is output; color changes and transparency changes for 0.25 seconds after click occurs
          outputs.solids << [button.border.x, button.border.y, button.border.w, button.border.h,
                             0, 180, 80, 255 * button.clicked_at.ease(0.25.seconds, :flip)] # transparency changes for 0.25 seconds
        end
      end
    
      # Exports the app's game state if the export button is clicked.
      def export_game_state_demo
        state.export_game_state_button ||= new_button_prefab(460, 100, "click to export app state")
        tick_button_prefab(state.export_game_state_button) # calls method to output button
        if button_clicked? state.export_game_state_button # if the export button is clicked
          GTK.export! "Exported from clicking the export button in the tech demo." # the export occurs
        end
      end
    
      # The mouse and keyboard focus are set to "yes" when the Dragonruby window is the active window.
      def window_state_demo
        m = GTK.args.inputs.mouse.has_focus ? 'Y' : 'N' # ternary operator (similar to if statement)
        k = GTK.args.inputs.keyboard.has_focus ? 'Y' : 'N'
        outputs.labels << [460, 20, "mouse focus: #{m}   keyboard focus: #{k}", small_font]
      end
    
      #Sets values for the horizontal separator (divides demo sections)
      def horizontal_seperator y, x, x2
        [x, y, x2, y, 150, 150, 150]
      end
    
      #Sets the values for the vertical separator (divides demo sections)
      def vertical_seperator x, y, y2
        [x, y, x, y2, 150, 150, 150]
      end
    
      # Outputs vertical and horizontal separators onto the screen to separate each demo section.
      def render_seperators
        outputs.lines << horizontal_seperator(505, grid.left, 445)
        outputs.lines << horizontal_seperator(353, grid.left, 445)
        outputs.lines << horizontal_seperator(264, grid.left, 445)
        outputs.lines << horizontal_seperator(174, grid.left, 445)
    
        outputs.lines << vertical_seperator(445, grid.top, grid.bottom)
    
        outputs.lines << horizontal_seperator(690, 445, 820)
        outputs.lines << horizontal_seperator(426, 445, 820)
    
        outputs.lines << vertical_seperator(820, grid.top, grid.bottom)
      end
    end
    
    $tech_demo = TechDemo.new
    
    def tick args
      $tech_demo.inputs = args.inputs
      $tech_demo.state = args.state
      $tech_demo.grid = args.grid
      $tech_demo.args = args
      $tech_demo.outputs = args.render_target(:mini_map)
      $tech_demo.tick
      args.outputs.labels  << [830, 715, "Render target:", [-2, 0, 0, 0, 0, 255]]
      args.outputs.sprites << [0, 0, 1280, 720, :mini_map]
      args.outputs.sprites << [830, 300, 675, 379, :mini_map]
      tick_instructions args, "Sample app shows all the rendering apis available."
    end
    
    def tick_instructions args, text, y = 715
      return if args.state.key_event_occurred
      if args.inputs.mouse.click ||
         args.inputs.keyboard.directional_vector ||
         args.inputs.keyboard.key_down.enter ||
         args.inputs.keyboard.key_down.escape
        args.state.key_event_occurred = true
      end
    
      args.outputs.debug << [0, y - 50, 1280, 60].solid
      args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
      args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
    end
    
    

    Render Primitive Hierarchies - main.rb link

    # ./samples/07_advanced_rendering/04_render_primitive_hierarchies/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - Nested array: An array whose individual elements are also arrays; useful for
       storing groups of similar data.  Also called multidimensional arrays.
    
       In this sample app, we see nested arrays being used in object definitions.
       Notice the parameters for solids, listed below. Parameters 1-3 set the
       definition for the rect, and parameter 4 sets the definition of the color.
    
       Instead of having a solid definition that looks like this,
       [X, Y, W, H, R, G, B]
       we can separate it into two separate array definitions in one, like this
       [[X, Y, W, H], [R, G, B]]
       and both options work fine in defining our solid (or any object).
    
     - Collections: Lists of data; useful for organizing large amounts of data.
       One element of a collection could be an array (which itself contains many elements).
       For example, a collection that stores two solid objects would look like this:
       [
        [100, 100, 50, 50, 0, 0, 0],
        [100, 150, 50, 50, 255, 255, 255]
       ]
       If this collection was added to args.outputs.solids, two solids would be output
       next to each other, one black and one white.
       Nested arrays can be used in collections, as you will see in this sample app.
    
     Reminders:
    
     - args.outputs.solids: An array. The values generate a solid.
       The parameters for a solid are
       1. The position on the screen (x, y)
       2. The width (w)
       3. The height (h)
       4. The color (r, g, b) (if a color is not assigned, the object's default color will be black)
       NOTE: THE PARAMETERS ARE THE SAME FOR BORDERS!
    
       Here is an example of a (red) border or solid definition:
       [100, 100, 400, 500, 255, 0, 0]
       It will be a solid or border depending on if it is added to args.outputs.solids or args.outputs.borders.
       For more information about solids and borders, go to mygame/documentation/03-solids-and-borders.md.
    
     - args.outputs.sprites: An array. The values generate a sprite.
       The parameters for sprites are
       1. The position on the screen (x, y)
       2. The width (w)
       3. The height (h)
       4. The image path (p)
    
       Here is an example of a sprite definition:
       [100, 100, 400, 500, 'sprites/dragonruby.png']
       For more information about sprites, go to mygame/documentation/05-sprites.md.
    
    =end
    
    # This code demonstrates the creation and output of objects like sprites, borders, and solids
    # If filled in, they are solids
    # If hollow, they are borders
    # If images, they are sprites
    
    # Solids are added to args.outputs.solids
    # Borders are added to args.outputs.borders
    # Sprites are added to args.outputs.sprites
    
    # The tick method runs 60 frames every second.
    # Your game is going to happen under this one function.
    def tick args
      border_as_solid_and_solid_as_border args
      sprite_as_border_or_solids args
      collection_of_borders_and_solids args
      collection_of_sprites args
    end
    
    # Shows a border being output onto the screen as a border and a solid
    # Also shows how colors can be set
    def border_as_solid_and_solid_as_border args
      border = [0, 0, 50, 50]
      args.outputs.borders << border
      args.outputs.solids  << border
    
      # Red, green, blue saturations (last three parameters) can be any number between 0 and 255
      border_with_color = [0, 100, 50, 50, 255, 0, 0]
      args.outputs.borders << border_with_color
      args.outputs.solids  << border_with_color
    
      border_with_nested_color = [0, 200, 50, 50, [0, 255, 0]] # nested color
      args.outputs.borders << border_with_nested_color
      args.outputs.solids  << border_with_nested_color
    
      border_with_nested_rect = [[0, 300, 50, 50], 0, 0, 255] # nested rect
      args.outputs.borders << border_with_nested_rect
      args.outputs.solids  << border_with_nested_rect
    
      border_with_nested_color_and_rect = [[0, 400, 50, 50], [255, 0, 255]] # nested rect and color
      args.outputs.borders << border_with_nested_color_and_rect
      args.outputs.solids  << border_with_nested_color_and_rect
    end
    
    # Shows a sprite output onto the screen as a sprite, border, and solid
    # Demonstrates that all three outputs appear differently on screen
    def sprite_as_border_or_solids args
      sprite = [100, 0, 50, 50, 'sprites/ship.png']
      args.outputs.sprites << sprite
    
      # Sprite_as_border variable has same parameters (excluding position) as above object,
      # but will appear differently on screen because it is added to args.outputs.borders
      sprite_as_border = [100, 100, 50, 50, 'sprites/ship.png']
      args.outputs.borders << sprite_as_border
    
      # Sprite_as_solid variable has same parameters (excluding position) as above object,
      # but will appear differently on screen because it is added to args.outputs.solids
      sprite_as_solid = [100, 200, 50, 50, 'sprites/ship.png']
      args.outputs.solids << sprite_as_solid
    end
    
    # Holds and outputs a collection of borders and a collection of solids
    # Collections are created by using arrays to hold parameters of each individual object
    def collection_of_borders_and_solids args
      collection_borders = [
        [
          [200,  0, 50, 50],                    # black border
          [200,  100, 50, 50, 255, 0, 0],       # red border
          [200,  200, 50, 50, [0, 255, 0]],     # nested color
        ],
        [[200, 300, 50, 50], 0, 0, 255],        # nested rect
        [[200, 400, 50, 50], [255, 0, 255]]     # nested rect and nested color
      ]
    
      args.outputs.borders << collection_borders
    
      collection_solids = [
        [
          [[300, 300, 50, 50], 0, 0, 255],      # nested rect
          [[300, 400, 50, 50], [255, 0, 255]]   # nested rect and nested color
        ],
        [300,  0, 50, 50],
        [300,  100, 50, 50, 255, 0, 0],
        [300,  200, 50, 50, [0, 255, 0]],       # nested color
      ]
    
      args.outputs.solids << collection_solids
    end
    
    # Holds and outputs a collection of sprites by adding it to args.outputs.sprites
    # Also outputs a collection with same parameters (excluding position) by adding
    # it to args.outputs.solids and another to args.outputs.borders
    def collection_of_sprites args
      sprites_collection = [
        [
          [400, 0, 50, 50, 'sprites/ship.png'],
          [400, 100, 50, 50, 'sprites/ship.png'],
        ],
        [400, 200, 50, 50, 'sprites/ship.png']
      ]
    
      args.outputs.sprites << sprites_collection
    
      args.outputs.solids << [
        [500, 0, 50, 50, 'sprites/ship.png'],
        [500, 100, 50, 50, 'sprites/ship.png'],
        [[[500, 200, 50, 50, 'sprites/ship.png']]]
      ]
    
      args.outputs.borders << [
        [
          [600, 0, 50, 50, 'sprites/ship.png'],
          [600, 100, 50, 50, 'sprites/ship.png'],
        ],
        [600, 200, 50, 50, 'sprites/ship.png']
      ]
    end
    
    

    Render Primitives As Hash - main.rb link

    # ./samples/07_advanced_rendering/05_render_primitives_as_hash/app/main.rb
    =begin
    
     Reminders:
    
     - Hashes: Collection of unique keys and their corresponding values. The value can be found
       using their keys.
    
       For example, if we have a "numbers" hash that stores numbers in English as the
       key and numbers in Spanish as the value, we'd have a hash that looks like this...
       numbers = { "one" => "uno", "two" => "dos", "three" => "tres" }
       and on it goes.
    
       Now if we wanted to find the corresponding value of the "one" key, we could say
       puts numbers["one"]
       which would print "uno" to the console.
    
     - args.outputs.sprites: An array. The values generate a sprite.
       The parameters are [X, Y, WIDTH, HEIGHT, PATH, ANGLE, ALPHA, RED, GREEN, BLUE]
       For more information about sprites, go to mygame/documentation/05-sprites.md.
    
     - args.outputs.labels: An array. The values generate a label.
       The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE]
       For more information about labels, go to mygame/documentation/02-labels.md.
    
     - args.outputs.solids: An array. The values generate a solid.
       The parameters are [X, Y, WIDTH, HEIGHT, RED, GREEN, BLUE, ALPHA]
       For more information about solids, go to mygame/documentation/03-solids-and-borders.md.
    
     - args.outputs.borders: An array. The values generate a border.
       The parameters are the same as a solid.
       For more information about borders, go to mygame/documentation/03-solids-and-borders.md.
    
     - args.outputs.lines: An array. The values generate a line.
       The parameters are [X, Y, X2, Y2, RED, GREEN, BLUE]
       For more information about labels, go to mygame/documentation/02-labels.md.
    
    =end
    
    # This sample app demonstrates how hashes can be used to output different kinds of objects.
    
    def tick args
      args.state.angle ||= 0 # initializes angle to 0
      args.state.angle  += 1 # increments angle by 1 every frame (60 times a second)
    
      # Outputs sprite using a hash
      args.outputs.sprites << {
        x: 30,                          # sprite position
        y: 550,
        w: 128,                         # sprite size
        h: 101,
        path: "dragonruby.png",         # image path
        angle: args.state.angle,        # angle
        a: 255,                         # alpha (transparency)
        r: 255,                         # color saturation
        g: 255,
        b: 255,
        tile_x:  0,                     # sprite sub division/tile
        tile_y:  0,
        tile_w: -1,
        tile_h: -1,
        flip_vertically: false,         # don't flip sprite
        flip_horizontally: false,
        angle_anchor_x: 0.5,            # rotation center set to middle
        angle_anchor_y: 0.5
      }
    
      # Outputs label using a hash
      args.outputs.labels << {
        x:              200,                 # label position
        y:              550,
        text:           "dragonruby",        # label text
        size_enum:      2,
        alignment_enum: 1,
        r:              155,                 # color saturation
        g:              50,
        b:              50,
        a:              255,                 # transparency
        font:           "fonts/manaspc.ttf"  # font style; without mentioned file, label won't output correctly
      }
    
      # Outputs solid using a hash
      # [X, Y, WIDTH, HEIGHT, RED, GREEN, BLUE, ALPHA]
      args.outputs.solids << {
        x: 400,                         # position
        y: 550,
        w: 160,                         # size
        h:  90,
        r: 120,                         # color saturation
        g:  50,
        b:  50,
        a: 255                          # transparency
      }
    
      # Outputs border using a hash
      # Same parameters as a solid
      args.outputs.borders << {
        x: 600,
        y: 550,
        w: 160,
        h:  90,
        r: 120,
        g:  50,
        b:  50,
        a: 255
      }
    
      # Outputs line using a hash
      args.outputs.lines << {
        x:  900,                        # starting position
        y:  550,
        x2: 1200,                       # ending position
        y2: 550,
        r:  120,                        # color saturation
        g:   50,
        b:   50,
        a:  255                         # transparency
      }
    
      # Outputs sprite as a primitive using a hash
      args.outputs.primitives << {
        x: 30,                          # position
        y: 200,
        w: 128,                         # size
        h: 101,
        path: "dragonruby.png",         # image path
        angle: args.state.angle,        # angle
        a: 255,                         # transparency
        r: 255,                         # color saturation
        g: 255,
        b: 255,
        tile_x:  0,                     # sprite sub division/tile
        tile_y:  0,
        tile_w: -1,
        tile_h: -1,
        flip_vertically: false,         # don't flip
        flip_horizontally: false,
        angle_anchor_x: 0.5,            # rotation center set to middle
        angle_anchor_y: 0.5
      }.sprite!
    
      # Outputs label as primitive using a hash
      args.outputs.primitives << {
        x:         200,                 # position
        y:         200,
        text:      "dragonruby",        # text
        size:      2,
        alignment: 1,
        r:         155,                 # color saturation
        g:         50,
        b:         50,
        a:         255,                 # transparency
        font:      "fonts/manaspc.ttf"  # font style
      }.label!
    
      # Outputs solid as primitive using a hash
      args.outputs.primitives << {
        x: 400,                         # position
        y: 200,
        w: 160,                         # size
        h:  90,
        r: 120,                         # color saturation
        g:  50,
        b:  50,
        a: 255                          # transparency
      }.solid!
    
      # Outputs border as primitive using a hash
      # Same parameters as solid
      args.outputs.primitives << {
        x: 600,                         # position
        y: 200,
        w: 160,                         # size
        h:  90,
        r: 120,                         # color saturation
        g:  50,
        b:  50,
        a: 255                          # transparency
      }.border!
    
      # Outputs line as primitive using a hash
      args.outputs.primitives << {
        x:  900,                        # starting position
        y:  200,
        x2: 1200,                       # ending position
        y2: 200,
        r:  120,                        # color saturation
        g:   50,
        b:   50,
        a:  255                         # transparency
      }.line!
    end
    
    

    Buttons As Render Targets - main.rb link

    # ./samples/07_advanced_rendering/06_buttons_as_render_targets/app/main.rb
    def tick args
      # create a texture/render_target that's composed of a border and a label
      create_button args, :hello_world_button, "Hello World", 500, 50
    
      # two button primitives using the hello_world_button render_target
      args.state.buttons ||= [
        # one button at the top
        { id: :top_button, x: 640 - 250, y: 80.from_top, w: 500, h: 50, path: :hello_world_button },
    
        # another button at the buttom, upside down, and flipped horizontally
        { id: :bottom_button, x: 640 - 250, y: 30, w: 500, h: 50, path: :hello_world_button, angle: 180, flip_horizontally: true },
      ]
    
      # check if a mouse click occurred
      if args.inputs.mouse.click
        # check to see if any of the buttons were intersected
        # and set the selected button if so
        args.state.selected_button = args.state.buttons.find { |b| b.intersect_rect? args.inputs.mouse }
      end
    
      # render the buttons
      args.outputs.sprites << args.state.buttons
    
      # if there was a selected button, print it's id
      if args.state.selected_button
        args.outputs.labels << { x: 30, y: 30.from_top, text: "#{args.state.selected_button.id} was clicked." }
      end
    end
    
    def create_button args, id, text, w, h
      # render_targets only need to be created once, we use the the id to determine if the texture
      # has already been created
      args.state.created_buttons ||= {}
      return if args.state.created_buttons[id]
    
      # if the render_target hasn't been created, then generate it and store it in the created_buttons cache
      args.state.created_buttons[id] = { created_at: Kernel.tick_count, id: id, w: w, h: h, text: text }
    
      # define the w/h of the texture
      args.outputs[id].w = w
      args.outputs[id].h = h
    
      # create a border
      args.outputs[id].borders << { x: 0, y: 0, w: w, h: h }
    
      # create a label centered vertically and horizontally within the texture
      args.outputs[id].labels << { x: w / 2, y: h / 2, text: text, vertical_alignment_enum: 1, alignment_enum: 1 }
    end
    
    

    Pixel Arrays - main.rb link

    # ./samples/07_advanced_rendering/06_pixel_arrays/app/main.rb
    def tick args
      args.state.posinc ||= 1
      args.state.pos ||= 0
      args.state.rotation ||= 0
    
      dimension = 10  # keep it small and let the GPU scale it when rendering the sprite.
    
      # Set up our "scanner" pixel array and fill it with black pixels.
      args.pixel_array(:scanner).width = dimension
      args.pixel_array(:scanner).height = dimension
      args.pixel_array(:scanner).pixels.fill(0xFF000000, 0, dimension * dimension)  # black, full alpha
    
      # Draw a green line that bounces up and down the sprite.
      args.pixel_array(:scanner).pixels.fill(0xFF00FF00, dimension * args.state.pos, dimension)  # green, full alpha
    
      # Adjust position for next frame.
      args.state.pos += args.state.posinc
      if args.state.posinc > 0 && args.state.pos >= dimension
        args.state.posinc = -1
        args.state.pos = dimension - 1
      elsif args.state.posinc < 0 && args.state.pos < 0
        args.state.posinc = 1
        args.state.pos = 1
      end
    
      # New/changed pixel arrays get uploaded to the GPU before we render
      #  anything. At that point, they can be scaled, rotated, and otherwise
      #  used like any other sprite.
      w = 100
      h = 100
      x = (1280 - w) / 2
      y = (720 - h) / 2
      args.outputs.background_color = [64, 0, 128]
      args.outputs.primitives << [x, y, w, h, :scanner, args.state.rotation].sprite
      args.state.rotation += 1
    
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    
    GTK.reset
    
    

    Pixel Arrays From File - main.rb link

    # ./samples/07_advanced_rendering/06_pixel_arrays_from_file/app/main.rb
    def tick args
      args.state.rotation ||= 0
    
      # on load, get pixels from png and load it into a pixel array
      if Kernel.tick_count == 0
        pixel_array = GTK.get_pixels 'sprites/square/blue.png'
        args.pixel_array(:square).w = pixel_array.w
        args.pixel_array(:square).h = pixel_array.h
        pixel_array.pixels.each_with_index do |p, i|
          args.pixel_array(:square).pixels[i] = p
        end
      end
    
      w = 100
      h = 100
      x = (1280 - w) / 2
      y = (720 - h) / 2
      args.outputs.background_color = [64, 0, 128]
      # render the pixel array by name
      args.outputs.primitives << { x: x, y: y, w: w, h: h, path: :square, angle: args.state.rotation }
      args.state.rotation += 1
    
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    GTK.reset
    
    

    Shake Camera - main.rb link

    # ./samples/07_advanced_rendering/07_shake_camera/app/main.rb
    # Demo of camera shake
    # Hold space to shake and release to stop
    
    class ScreenShake
      attr_gtk
    
      def tick
        defaults
        calc_camera
    
        outputs.labels << { x: 600, y: 400, text: "Hold Space!" }
    
        # Add outputs to :scene
        outputs[:scene].sprites << { x: 100, y: 100,          w: 80, h: 80, path: 'sprites/square/blue.png' }
        outputs[:scene].sprites << { x: 200, y: 300.from_top, w: 80, h: 80, path: 'sprites/square/blue.png' }
        outputs[:scene].sprites << { x: 900, y: 200,          w: 80, h: 80, path: 'sprites/square/blue.png' }
    
        # Describe how to render :scene
        outputs.sprites << { x: 0 - state.camera.x_offset,
                             y: 0 - state.camera.y_offset,
                             w: 1280,
                             h: 720,
                             angle: state.camera.angle,
                             path: :scene }
      end
    
      def defaults
        state.camera.trauma ||= 0
        state.camera.angle ||= 0
        state.camera.x_offset ||= 0
        state.camera.y_offset ||= 0
      end
    
      def calc_camera
        if inputs.keyboard.key_held.space
          state.camera.trauma += 0.02
        end
    
        next_camera_angle = 180.0 / 20.0 * state.camera.trauma**2
        next_offset       = 100.0 * state.camera.trauma**2
    
        # Ensure that the camera angle always switches from
        # positive to negative and vice versa
        # which gives the effect of shaking back and forth
        state.camera.angle = state.camera.angle > 0 ?
                               next_camera_angle * -1 :
                               next_camera_angle
    
        state.camera.x_offset = next_offset.randomize(:sign, :ratio)
        state.camera.y_offset = next_offset.randomize(:sign, :ratio)
    
        # Gracefully degrade trauma
        state.camera.trauma *= 0.95
      end
    end
    
    def tick args
      $screen_shake ||= ScreenShake.new
      $screen_shake.args = args
      $screen_shake.tick
    end
    
    

    Simple Camera - main.rb link

    # ./samples/07_advanced_rendering/07_simple_camera/app/main.rb
    def tick args
      # variables you can play around with
      args.state.world.w      ||= 1280
      args.state.world.h      ||= 720
    
      args.state.player.x     ||= 0
      args.state.player.y     ||= 0
      args.state.player.size  ||= 32
    
      args.state.enemy.x      ||= 700
      args.state.enemy.y      ||= 700
      args.state.enemy.size   ||= 16
    
      args.state.camera.x                ||= 640
      args.state.camera.y                ||= 300
      args.state.camera.scale            ||= 1.0
      args.state.camera.show_empty_space ||= :yes
    
      # instructions
      args.outputs.primitives << { x: 0, y:  80.from_top, w: 360, h: 80, r: 0, g: 0, b: 0, a: 128 }.solid!
      args.outputs.primitives << { x: 10, y: 10.from_top, text: "arrow keys to move around", r: 255, g: 255, b: 255}.label!
      args.outputs.primitives << { x: 10, y: 30.from_top, text: "+/- to change zoom of camera", r: 255, g: 255, b: 255}.label!
      args.outputs.primitives << { x: 10, y: 50.from_top, text: "tab to change camera edge behavior", r: 255, g: 255, b: 255}.label!
    
      # render scene
      args.outputs[:scene].w = args.state.world.w
      args.outputs[:scene].h = args.state.world.h
    
      args.outputs[:scene].solids << { x: 0, y: 0, w: args.state.world.w, h: args.state.world.h, r: 20, g: 60, b: 80 }
      args.outputs[:scene].solids << { x: args.state.player.x, y: args.state.player.y,
                                       w: args.state.player.size, h: args.state.player.size, r: 80, g: 155, b: 80 }
      args.outputs[:scene].solids << { x: args.state.enemy.x, y: args.state.enemy.y,
                                       w: args.state.enemy.size, h: args.state.enemy.size, r: 155, g: 80, b: 80 }
    
      # render camera
      scene_position = calc_scene_position args
      args.outputs.sprites << { x: scene_position.x,
                                y: scene_position.y,
                                w: scene_position.w,
                                h: scene_position.h,
                                path: :scene }
    
      # move player
      if args.inputs.directional_angle
        args.state.player.x += args.inputs.directional_angle.vector_x * 5
        args.state.player.y += args.inputs.directional_angle.vector_y * 5
        args.state.player.x  = args.state.player.x.clamp(0, args.state.world.w - args.state.player.size)
        args.state.player.y  = args.state.player.y.clamp(0, args.state.world.h - args.state.player.size)
      end
    
      # +/- to zoom in and out
      if args.inputs.keyboard.plus && Kernel.tick_count.zmod?(3)
        args.state.camera.scale += 0.05
      elsif args.inputs.keyboard.hyphen && Kernel.tick_count.zmod?(3)
        args.state.camera.scale -= 0.05
      elsif args.inputs.keyboard.key_down.tab
        if args.state.camera.show_empty_space == :yes
          args.state.camera.show_empty_space = :no
        else
          args.state.camera.show_empty_space = :yes
        end
      end
    
      args.state.camera.scale = args.state.camera.scale.greater(0.1)
    end
    
    def calc_scene_position args
      result = { x: args.state.camera.x - (args.state.player.x * args.state.camera.scale),
                 y: args.state.camera.y - (args.state.player.y * args.state.camera.scale),
                 w: args.state.world.w * args.state.camera.scale,
                 h: args.state.world.h * args.state.camera.scale,
                 scale: args.state.camera.scale }
    
      return result if args.state.camera.show_empty_space == :yes
    
      if result.w < args.grid.w
        result.merge!(x: (args.grid.w - result.w).half)
      elsif (args.state.player.x * result.scale) < args.grid.w.half
        result.merge!(x: 10)
      elsif (result.x + result.w) < args.grid.w
        result.merge!(x: - result.w + (args.grid.w - 10))
      end
    
      if result.h < args.grid.h
        result.merge!(y: (args.grid.h - result.h).half)
      elsif (result.y) > 10
        result.merge!(y: 10)
      elsif (result.y + result.h) < args.grid.h
        result.merge!(y: - result.h + (args.grid.h - 10))
      end
    
      result
    end
    
    

    Simple Camera Multiple Targets - main.rb link

    # ./samples/07_advanced_rendering/07_simple_camera_multiple_targets/app/main.rb
    def tick args
      args.outputs.background_color = [0, 0, 0]
    
      # variables you can play around with
      args.state.world.w                ||= 1280
      args.state.world.h                ||= 720
      args.state.target_hero            ||= :hero_1
      args.state.target_hero_changed_at ||= -30
      args.state.hero_size              ||= 32
    
      # initial state of heros and camera
      args.state.hero_1 ||= { x: 100, y: 100 }
      args.state.hero_2 ||= { x: 100, y: 600 }
      args.state.camera ||= { x: 640, y: 360, scale: 1.0 }
    
      # render instructions
      args.outputs.primitives << { x: 0,  y: 80.from_top, w: 360, h: 80, r: 0, g: 0, b: 0, a: 128 }.solid!
      args.outputs.primitives << { x: 10, y: 10.from_top, text: "+/- to change zoom of camera", r: 255, g: 255, b: 255}.label!
      args.outputs.primitives << { x: 10, y: 30.from_top, text: "arrow keys to move target hero", r: 255, g: 255, b: 255}.label!
      args.outputs.primitives << { x: 10, y: 50.from_top, text: "space to cycle target hero", r: 255, g: 255, b: 255}.label!
    
      # render scene
      args.outputs[:scene].w = args.state.world.w
      args.outputs[:scene].h = args.state.world.h
    
      # render world
      args.outputs[:scene].solids << { x: 0, y: 0, w: args.state.world.w, h: args.state.world.h, r: 20, g: 60, b: 80 }
    
      # render hero_1
      args.outputs[:scene].solids << { x: args.state.hero_1.x, y: args.state.hero_1.y,
                                       w: args.state.hero_size, h: args.state.hero_size, r: 255, g: 155, b: 80 }
    
      # render hero_2
      args.outputs[:scene].solids << { x: args.state.hero_2.x, y: args.state.hero_2.y,
                                       w: args.state.hero_size, h: args.state.hero_size, r: 155, g: 255, b: 155 }
    
      # render scene relative to camera
      scene_position = calc_scene_position args
    
      args.outputs.sprites << { x: scene_position.x,
                                y: scene_position.y,
                                w: scene_position.w,
                                h: scene_position.h,
                                path: :scene }
    
      # mini map
      args.outputs.borders << { x: 10,
                                y: 10,
                                w: args.state.world.w.idiv(8),
                                h: args.state.world.h.idiv(8),
                                r: 255,
                                g: 255,
                                b: 255 }
      args.outputs.sprites << { x: 10,
                                y: 10,
                                w: args.state.world.w.idiv(8),
                                h: args.state.world.h.idiv(8),
                                path: :scene }
    
      # cycle target hero
      if args.inputs.keyboard.key_down.space
        if args.state.target_hero == :hero_1
          args.state.target_hero = :hero_2
        else
          args.state.target_hero = :hero_1
        end
        args.state.target_hero_changed_at = Kernel.tick_count
      end
    
      # move target hero
      hero_to_move = if args.state.target_hero == :hero_1
                       args.state.hero_1
                     else
                       args.state.hero_2
                     end
    
      if args.inputs.directional_angle
        hero_to_move.x += args.inputs.directional_angle.vector_x * 5
        hero_to_move.y += args.inputs.directional_angle.vector_y * 5
        hero_to_move.x  = hero_to_move.x.clamp(0, args.state.world.w - hero_to_move.size)
        hero_to_move.y  = hero_to_move.y.clamp(0, args.state.world.h - hero_to_move.size)
      end
    
      # +/- to zoom in and out
      if args.inputs.keyboard.plus && Kernel.tick_count.zmod?(3)
        args.state.camera.scale += 0.05
      elsif args.inputs.keyboard.hyphen && Kernel.tick_count.zmod?(3)
        args.state.camera.scale -= 0.05
      end
    
      args.state.camera.scale = 0.1 if args.state.camera.scale < 0.1
    end
    
    def other_hero args
      if args.state.target_hero == :hero_1
        return args.state.hero_2
      else
        return args.state.hero_1
      end
    end
    
    def calc_scene_position args
      target_hero = if args.state.target_hero == :hero_1
                      args.state.hero_1
                    else
                      args.state.hero_2
                    end
    
      other_hero = if args.state.target_hero == :hero_1
                     args.state.hero_2
                   else
                     args.state.hero_1
                   end
    
      # calculate the lerp percentage based on the time since the target hero changed
      lerp_percentage = Easing.ease args.state.target_hero_changed_at,
                                    Kernel.tick_count,
                                    30,
                                    :smooth_stop_quint,
                                    :flip
    
      # calculate the angle and distance between the target hero and the other hero
      angle_to_other_hero = Geometry.angle_to target_hero, other_hero
    
      # calculate the distance between the target hero and the other hero
      distance_to_other_hero = Geometry.distance target_hero, other_hero
    
      # the camera position is the target hero position plus the angle and distance to the other hero (lerped)
      { x: args.state.camera.x - (target_hero.x + (angle_to_other_hero.vector_x * distance_to_other_hero * lerp_percentage)) * args.state.camera.scale,
        y: args.state.camera.y - (target_hero.y + (angle_to_other_hero.vector_y * distance_to_other_hero * lerp_percentage)) * args.state.camera.scale,
        w: args.state.world.w * args.state.camera.scale,
        h: args.state.world.h * args.state.camera.scale }
    end
    
    

    Splitscreen Camera - main.rb link

    # ./samples/07_advanced_rendering/08_splitscreen_camera/app/main.rb
    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
    
    

    Z Targeting Camera - main.rb link

    # ./samples/07_advanced_rendering/09_z_targeting_camera/app/main.rb
    class Game
      attr_gtk
    
      def tick
        defaults
        render
        input
        calc
      end
    
      def defaults
        outputs.background_color = [219, 208, 191]
        player.x        ||= 634
        player.y        ||= 153
        player.angle    ||= 90
        player.distance ||= arena_radius
        target.x        ||= 634
        target.y        ||= 359
      end
    
      def render
        outputs[:scene].sprites << ({ x: 0, y: 0, w: 933, h: 700, path: 'sprites/arena.png' }.center_inside_rect grid.rect)
        outputs[:scene].sprites << target_sprite
        outputs[:scene].sprites << player_sprite
        outputs.sprites << scene
      end
    
      def target_sprite
        {
          x: target.x, y: target.y,
          w: 10, h: 10,
          path: 'sprites/square/black.png'
        }.anchor_rect 0.5, 0.5
      end
    
      def input
        if inputs.up && player.distance > 30
          player.distance -= 2
        elsif inputs.down && player.distance < 200
          player.distance += 2
        end
    
        player.angle += inputs.left_right * -1
      end
    
      def calc
        player.x = target.x + ((player.angle *  1).vector_x player.distance)
        player.y = target.y + ((player.angle * -1).vector_y player.distance)
      end
    
      def player_sprite
        {
          x: player.x,
          y: player.y,
          w: 50,
          h: 100,
          path: 'sprites/player.png',
          angle: (player.angle * -1) + 90
        }.anchor_rect 0.5, 0
      end
    
      def center_map
        { x: 634, y: 359 }
      end
    
      def zoom_factor_single
        2 - ((Geometry.distance player, center_map).fdiv arena_radius)
      end
    
      def zoom_factor
        zoom_factor_single ** 2
      end
    
      def arena_radius
        206
      end
    
      def scene
        {
          x:    (640 - player.x) + (640 - (640 * zoom_factor)),
          y:    (360 - player.y - (75 * zoom_factor)) + (320 - (320 * zoom_factor)),
          w:    1280 * zoom_factor,
          h:     720 * zoom_factor,
          path: :scene,
          angle: player.angle - 90,
          angle_anchor_x: (player.x.fdiv 1280),
          angle_anchor_y: (player.y.fdiv 720)
        }
      end
    
      def player
        state.player
      end
    
      def target
        state.target
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    GTK.reset
    
    

    Camera And Large Map - main.rb link

    # ./samples/07_advanced_rendering/10_camera_and_large_map/app/main.rb
    def tick args
      # you want to make sure all of your pngs are a maximum size of 1280x1280
      # low-end android devices and machines with underpowered GPUs are unable to
      # load very large textures.
    
      # this sample app creates 640x640 tiles of a 6400x6400 pixel png and displays them
      # on the screen relative to the player's position
    
      # tile creation process
      create_tiles_if_needed args
    
      # if tiles are already present the show map
      display_tiles args
    end
    
    def display_tiles args
      # set the player's starting location
      args.state.player ||= {
        x:  0,
        y:  0,
        w: 40,
        h: 40,
        path: "sprites/square/blue.png"
      }
    
      # if all tiles have been created, then we are
      # in "displaying_tiles" mode
      if args.state.displaying_tiles
        # create a render target that can hold 9 640x640 tiles
        args.outputs[:scene].background_color = [0, 0, 0, 0]
        args.outputs[:scene].w = 1920
        args.outputs[:scene].h = 1920
    
        # allow player to be moved with arrow keys
        args.state.player.x += args.inputs.left_right * 10
        args.state.player.y += args.inputs.up_down * 10
    
        # given the player's location, return a collection of primitives
        # to render that are within the 1920x1920 viewport
        args.outputs[:scene].primitives << tiles_in_viewport(args)
    
        # place the player in the center of the render_target
        args.outputs[:scene].primitives << {
          x: 960 - 20,
          y: 960 - 20,
          w: 40,
          h: 40,
          path: "sprites/square/blue.png"
        }
    
        # center the 1920x1920 render target within the 1280x720 window
        args.outputs.sprites << {
          x: -320,
          y: -600,
          w: 1920,
          h: 1920,
          path: :scene
        }
      end
    end
    
    def tiles_in_viewport args
      state = args.state
      # define the size of each tile
      tile_size = 640
    
      # determine what tile the player is on
      tile_player_is_on = { x: state.player.x.idiv(tile_size), y: state.player.y.idiv(tile_size) }
    
      # calculate the x and y offset of the player so that tiles are positioned correctly
      offset_x = 960 - (state.player.x - (tile_player_is_on.x * tile_size))
      offset_y = 960 - (state.player.y - (tile_player_is_on.y * tile_size))
    
      primitives = []
    
      # get 9 tiles in total (the tile the player is on and the 8 surrounding tiles)
    
      # center tile
      primitives << (tile_in_viewport size:       tile_size,
                                      from_row:   tile_player_is_on.y,
                                      from_col:   tile_player_is_on.x,
                                      offset_row: 0,
                                      offset_col: 0,
                                      dy:         offset_y,
                                      dx:         offset_x)
    
      # tile to the right
      primitives << (tile_in_viewport size:       tile_size,
                                      from_row:   tile_player_is_on.y,
                                      from_col:   tile_player_is_on.x,
                                      offset_row: 0,
                                      offset_col: 1,
                                      dy:         offset_y,
                                      dx:         offset_x)
      # tile to the left
      primitives << (tile_in_viewport size:        tile_size,
                                      from_row:    tile_player_is_on.y,
                                      from_col:    tile_player_is_on.x,
                                      offset_row:  0,
                                      offset_col: -1,
                                      dy:          offset_y,
                                      dx:          offset_x)
    
      # tile directly above
      primitives << (tile_in_viewport size:       tile_size,
                                      from_row:   tile_player_is_on.y,
                                      from_col:   tile_player_is_on.x,
                                      offset_row: 1,
                                      offset_col: 0,
                                      dy:         offset_y,
                                      dx:         offset_x)
      # tile directly below
      primitives << (tile_in_viewport size:         tile_size,
                                      from_row:     tile_player_is_on.y,
                                      from_col:     tile_player_is_on.x,
                                      offset_row:  -1,
                                      offset_col:   0,
                                      dy:           offset_y,
                                      dx:           offset_x)
      # tile up and to the left
      primitives << (tile_in_viewport size:        tile_size,
                                      from_row:    tile_player_is_on.y,
                                      from_col:    tile_player_is_on.x,
                                      offset_row:  1,
                                      offset_col: -1,
                                      dy:          offset_y,
                                      dx:          offset_x)
    
      # tile up and to the right
      primitives << (tile_in_viewport size:       tile_size,
                                      from_row:   tile_player_is_on.y,
                                      from_col:   tile_player_is_on.x,
                                      offset_row: 1,
                                      offset_col: 1,
                                      dy:         offset_y,
                                      dx:         offset_x)
    
      # tile down and to the left
      primitives << (tile_in_viewport size:        tile_size,
                                      from_row:    tile_player_is_on.y,
                                      from_col:    tile_player_is_on.x,
                                      offset_row: -1,
                                      offset_col: -1,
                                      dy:          offset_y,
                                      dx:          offset_x)
    
      # tile down and to the right
      primitives << (tile_in_viewport size:        tile_size,
                                      from_row:    tile_player_is_on.y,
                                      from_col:    tile_player_is_on.x,
                                      offset_row: -1,
                                      offset_col:  1,
                                      dy:          offset_y,
                                      dx:          offset_x)
    
      primitives
    end
    
    def tile_in_viewport size:, from_row:, from_col:, offset_row:, offset_col:, dy:, dx:;
      x = size * offset_col + dx
      y = size * offset_row + dy
    
      return nil if (from_row + offset_row) < 0
      return nil if (from_row + offset_row) > 9
    
      return nil if (from_col + offset_col) < 0
      return nil if (from_col + offset_col) > 9
    
      # return the tile sprite, a border demarcation, and label of which tile x and y
      [
        {
          x: x,
          y: y,
          w: size,
          h: size,
          path: "sprites/tile-#{from_col + offset_col}-#{from_row + offset_row}.png",
        },
        {
          x: x,
          y: y,
          w: size,
          h: size,
          r: 255,
          primitive_marker: :border,
        },
        {
          x: x + size / 2 - 150,
          y: y + size / 2 - 25,
          w: 300,
          h: 50,
          primitive_marker: :solid,
          r: 0,
          g: 0,
          b: 0,
          a: 128
        },
        {
          x: x + size / 2,
          y: y + size / 2,
          text: "tile #{from_col + offset_col}, #{from_row + offset_row}",
          alignment_enum: 1,
          vertical_alignment_enum: 1,
          size_enum: 2,
          r: 255,
          g: 255,
          b: 255
        },
      ]
    end
    
    def create_tiles_if_needed args
      # We are going to use args.outputs.screenshots to generate tiles of a
      # png of size 6400x6400 called sprites/large.png.
      if !GTK.stat_file("sprites/tile-9-9.png") && !args.state.creating_tiles
        args.state.displaying_tiles = false
        args.outputs.labels << {
          x: 960,
          y: 360,
          text: "Press enter to generate tiles of sprites/large.png.",
          alignment_enum: 1,
          vertical_alignment_enum: 1
        }
      elsif !args.state.creating_tiles
        args.state.displaying_tiles = true
      end
    
      # pressing enter will start the tile creation process
      if args.inputs.keyboard.key_down.enter && !args.state.creating_tiles
        args.state.displaying_tiles = false
        args.state.creating_tiles = true
        args.state.tile_clock = 0
      end
    
      # the tile creation process renders an area of sprites/large.png
      # to the screen and takes a screenshot of it every half second
      # until all tiles are generated.
      # once all tiles are generated a map viewport will be rendered that
      # stitches tiles together.
      if args.state.creating_tiles
        args.state.tile_x ||= 0
        args.state.tile_y ||= 0
    
        # render a sub-square of the large png.
        args.outputs.sprites << {
          x: 0,
          y: 0,
          w: 640,
          h: 640,
          source_x: args.state.tile_x * 640,
          source_y: args.state.tile_y * 640,
          source_w: 640,
          source_h: 640,
          path: "sprites/large.png"
        }
    
        # determine tile file name
        tile_path = "sprites/tile-#{args.state.tile_x}-#{args.state.tile_y}.png"
    
        args.outputs.labels << {
          x: 960,
          y: 320,
          text: "Generating #{tile_path}",
          alignment_enum: 1,
          vertical_alignment_enum: 1
        }
    
        # take a screenshot on frames divisible by 29
        if args.state.tile_clock.zmod?(29)
          args.outputs.screenshots << {
            x: 0,
            y: 0,
            w: 640,
            h: 640,
            path: tile_path,
            a: 255
          }
        end
    
        # increment tile to render on frames divisible by 30 (half a second)
        # (one frame is allotted to take screenshot)
        if args.state.tile_clock.zmod?(30)
          args.state.tile_x += 1
          if args.state.tile_x >= 10
            args.state.tile_x  = 0
            args.state.tile_y += 1
          end
    
          # once all of tile tiles are created, begin displaying map
          if args.state.tile_y >= 10
            args.state.creating_tiles = false
            args.state.displaying_tiles = true
          end
        end
    
        args.state.tile_clock += 1
      end
    end
    
    GTK.reset
    
    

    Camera And Large Sprites - main.rb link

    # ./samples/07_advanced_rendering/10_camera_and_large_sprites/app/main.rb
    # When using a render target as a camera, sprite rendered
    # within the camera can become very large and tax the GPU. This
    # example shows how to calculate the crop rectangle for the sprite and only
    # render the portion of the sprite that is visible within the camera.
    class Game
      attr :args
    
      def tick
        defaults
        calc
        render
      end
    
      def defaults
        @args.state.orbit ||= {
          x: 640,
          y: 640,
          w: 1280,
          h: 1280,
          anchor_x: 0.5,
          anchor_y: 0.5
        }
    
        @args.state.viewport ||= {
          x: 0,
          y: 0,
          w: 720,
          h: 720
        }
    
        @args.state.camera ||= {
          x: 0,
          y: 0,
          scale: 0.25,
          w: 720,
          h: 720
        }
    
        if [email protected]_sprite_size
          w, h = GTK.calcspritebox("sprites/ring-1280.png")
          @args.state.orbit_sprite_size = {
            w: w,
            h: h
          }
        end
      end
    
      def calc
        if inputs.keyboard.i
          state.camera.scale += 0.005 * state.camera.scale
        elsif inputs.keyboard.o
          state.camera.scale -= 0.005 * state.camera.scale
        end
    
        if inputs.keyboard.d
          state.camera.x += 10 / state.camera.scale
        elsif inputs.keyboard.a
          state.camera.x -= 10 / state.camera.scale
        end
    
        if inputs.keyboard.s
          state.camera.y -= 10 / state.camera.scale
        elsif inputs.keyboard.w
          state.camera.y += 10 / state.camera.scale
        end
    
        state.camera.scale = state.camera.scale.clamp(0.25, 10)
        state.camera.x = state.camera.x.round(2)
        state.camera.y = state.camera.y.round(2)
    
        state.orbit_in_camera = {
          x: (state.orbit.x - state.camera.x) * state.camera.scale,
          y: (state.orbit.y - state.camera.y) * state.camera.scale,
          w: state.orbit.w * state.camera.scale,
          h: state.orbit.h * state.camera.scale,
          anchor_x: state.orbit.anchor_x,
          anchor_y: state.orbit.anchor_y
        }
      end
    
      def render
        outputs.background_color = [32, 32, 32]
    
        outputs[:scene].w = 720
        outputs[:scene].h = 720
        outputs[:scene].background_color = [0, 0, 0, 0]
    
        orbit_sprite_rect = sprite_rect state.viewport, state.orbit_in_camera, state.orbit_sprite_size
        outputs[:scene].sprites << {
          **orbit_sprite_rect,
          path: "sprites/ring-1280.png"
        }
    
        outputs.borders << {
          x: Grid.w / 2,
          y: Grid.h / 2,
          w: 720,
          h: 720,
          path: :scene,
          anchor_x: 0.5,
          anchor_y: 0.5,
          r: 255,
          g: 255,
          b: 255
        }
    
        outputs.sprites << {
          x: Grid.w / 2,
          y: Grid.h / 2,
          w: 720,
          h: 720,
          path: :scene,
          anchor_x: 0.5,
          anchor_y: 0.5
        }
    
        outputs.watch("Instructions WASD: move camera, I: zoom in, O: zoom out")
        outputs.watch("state.camera:    #{state.camera.to_sf}")
        outputs.watch("orbit_in_camera: #{state.orbit_in_camera.to_sf}")
        outputs.watch("sprite_rect:     #{orbit_sprite_rect.to_sf}")
      end
    
      def sprite_rect viewport_rect, destination_rect, sprite_size
        ratio = destination_rect.w / sprite_size.w
    
        # if the destination rect is not within the viewport, return an empty rect
        if !Geometry.intersect_rect? viewport_rect, destination_rect
          return { x: 0, y: 0, w: 0, h: 0 }
        end
    
        # Geometry.rect_props returns a hash with x, y, w, h (removes/recomputes anchor_x, anchor_y)
        destination_rect = Geometry.rect_props destination_rect
        viewport_rect = Geometry.rect_props viewport_rect
    
        # calculate the x, w, source_x, source_w of the sprite
        destination_left = destination_rect.x
        viewport_left = viewport_rect.x
        destination_right = destination_rect.x + destination_rect.w
        viewport_right = viewport_rect.x + viewport_rect.w
        left_diff = viewport_left - destination_left
        right_diff = destination_right - viewport_right
    
        if destination_left <= viewport_left && destination_right >= viewport_right
          # destination rect's x, w is larger than the viewport
          x = viewport_left
          w = destination_rect.w - (viewport_left - destination_left) - right_diff
          source_x = 0 + left_diff / ratio
          source_w = sprite_size.w - left_diff / ratio - right_diff / ratio
        elsif destination_left <= viewport_left && destination_right <= viewport_right
          # destination rect's x, w is partially within the viewport
          x = viewport_left
          w = destination_rect.w - (viewport_left - destination_left)
          source_x = 0 + left_diff / ratio
          source_w = sprite_size.w - left_diff / ratio
        elsif destination_right >= viewport_right && destination_left >= viewport_left
          # destination rect's x, w is partially within the viewport
          x = destination_left
          w = destination_rect.w - right_diff
          source_x = 0
          source_w = sprite_size.w - right_diff / ratio
        else
          # destination rect's x, w is completely within the viewport
          x = destination_left
          w = destination_rect.w
          source_x = 0
          source_w = sprite_size.w
        end
    
        # calculate the y, h, source_y, source_h of the sprite
        destination_top = destination_rect.y + destination_rect.h
        viewport_top = viewport_rect.y + viewport_rect.h
        destination_bottom = destination_rect.y
        viewport_bottom = viewport_rect.y
        bottom_diff = viewport_bottom - destination_bottom
        top_diff = destination_top - viewport_top
    
        if destination_top >= viewport_top && destination_bottom <= viewport_bottom
          # destination rect's y, h is larger than the viewport
          y = viewport_bottom
          h = destination_rect.h - (viewport_bottom - destination_bottom) - top_diff
          source_y = 0 + (viewport_bottom - destination_bottom) / ratio
          source_h = sprite_size.h - (viewport_bottom - destination_bottom) / ratio - top_diff / ratio
        elsif destination_top >= viewport_top && destination_bottom >= viewport_bottom
          # destination rect's y, h is partially within the viewport
          y = destination_bottom
          h = destination_rect.h - top_diff
          source_y = 0
          source_h = sprite_size.h - top_diff / ratio
        elsif destination_bottom <= viewport_bottom && destination_top <= viewport_top
          # destination rect's y, h is partially within the viewport
          source_y = 0 + bottom_diff / ratio
          source_h = sprite_size.h - bottom_diff / ratio
          y = viewport_bottom
          h = destination_rect.h - bottom_diff
        else
          # destination rect's y, h is completely within the viewport
          y = destination_bottom
          h = destination_rect.h
          source_y = 0
          source_h = sprite_size.h
        end
    
        # return the calculated values
        {
          x: x,
          y: y,
          w: w,
          h: h,
          source_x: source_x,
          source_y: source_y,
          source_w: source_w,
          source_h: source_h
        }
      end
    
      def state
        @args.state
      end
    
      def outputs
        @args.outputs
      end
    
      def inputs
        @args.inputs
      end
    end
    
    def boot args
      args.state = {}
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    def reset args
      $game = nil
    end
    
    GTK.reset
    
    

    Blend Modes - main.rb link

    # ./samples/07_advanced_rendering/11_blend_modes/app/main.rb
    def draw_blendmode args, mode
      w = 160
      h = w
      args.state.x += (1280-w) / (args.state.blendmodes.length + 1)
      x = args.state.x
      y = (720 - h) / 2
      s = 'sprites/blue-feathered.png'
      args.outputs.sprites << { blendmode_enum: mode.value, x: x, y: y, w: w, h: h, path: s }
      args.outputs.labels << [x + (w/2), y, mode.name.to_s, 1, 1, 255, 255, 255]
    end
    
    def tick args
    
      # Different blend modes do different things, depending on what they
      # blend against (in this case, the pixels of the background color).
      args.state.bg_element ||= 1
      args.state.bg_color ||= 255
      args.state.bg_color_direction ||= 1
      bg_r = (args.state.bg_element == 1) ? args.state.bg_color : 0
      bg_g = (args.state.bg_element == 2) ? args.state.bg_color : 0
      bg_b = (args.state.bg_element == 3) ? args.state.bg_color : 0
      args.state.bg_color += args.state.bg_color_direction
      if (args.state.bg_color_direction > 0) && (args.state.bg_color >= 255)
        args.state.bg_color_direction = -1
        args.state.bg_color = 255
      elsif (args.state.bg_color_direction < 0) && (args.state.bg_color <= 0)
        args.state.bg_color_direction = 1
        args.state.bg_color = 0
        args.state.bg_element += 1
        if args.state.bg_element >= 4
          args.state.bg_element = 1
        end
      end
    
      args.outputs.background_color = [ bg_r, bg_g, bg_b, 255 ]
    
      args.state.blendmodes ||= [
        { name: :none,  value: 0 },
        { name: :blend, value: 1 },
        { name: :add,   value: 2 },
        { name: :mod,   value: 3 },
        { name: :mul,   value: 4 }
      ]
    
      args.state.x = 0  # reset this, draw_blendmode will increment it.
      args.state.blendmodes.each { |blendmode| draw_blendmode args, blendmode }
    end
    
    GTK.reset
    
    

    Blend Modes Additive Modulo - main.rb link

    # ./samples/07_advanced_rendering/12_blend_modes_additive_modulo/app/main.rb
    # Sample app shows how to use blend modes to create a masking layer
    # Special thanks to akzidenz@discord (https://akzidenz.itch.io/) for providing this sample app
    #
    # blendmode_enum reference:
    #  0: no blend
    #  1: alpha blending (default)
    #  2: additive blending
    #  3: modulo blending
    #  4: multiply blending
    def tick args
      # create a render target to represent the masking layer
      args.outputs[:mask].w = 1280
      args.outputs[:mask].h = 720
    
      # don't erase the texture when new items are added
      args.outputs[:mask].clear_before_render = false
    
      # the "cover" only goes in once
      if Kernel.tick_count == 0
        # place a black background in the render target
        args.outputs[:mask].sprites << {
          x: 0, y: 0, w: 1280, h: 720,
          path: :solid,
          r: 0, g: 0, b: 0 # <-- important (black color)
        }
      end
    
      # the "reveal" sprite is added to the render target
      # when the left mouse button is clicked or held
      # NOTE: setting `clear_before_render = false` keeps the RT from resetting
      #       when a new primitive is drawn to it
      if args.inputs.mouse.key_down.left || args.inputs.mouse.key_held.left
        args.outputs[:mask].sprites << {
          x: args.inputs.mouse.x,
          y: args.inputs.mouse.y,
          w: 240, h: 240,
          anchor_x: 0.5,
          anchor_y: 0.5,
          path: 'sprites/mask.png', # <-- sprite representing the "reveal shape"
          blendmode_enum: 2,        # <-- important (2 means additive blending)
          r: 255, g: 255, b: 255    # <-- important (white color)
        }
      end
    
      # render background to reveal
      args.outputs.sprites << { x: 0,
                                y: 0,
                                w: 1280,
                                h: 720,
                                path: 'sprites/bg.png' }
    
      # render masking layer over the bg to reveal
      args.outputs.sprites << {
        x: 0, y: 0, w: 1280, h: 720,
        path: :mask,
        blendmode_enum: 3 # <-- important (3 means modulo blending)
      }
    
      # render mouse overlay
      args.outputs.sprites << {
        x: args.inputs.mouse.x,
        y: args.inputs.mouse.y,
        w: 180, h: 180,
        anchor_x: 0.5, anchor_y: 0.5,
        a: 32
      }
    
      # render instructions
      args.outputs.labels << { x: 8,
                               y: 720 - 8,
                               text: "click/drag move to uncover bg image",
                               r: 255,
                               g: 255,
                               b: 255 }
    end
    
    

    Render Target Noclear - main.rb link

    # ./samples/07_advanced_rendering/12_render_target_noclear/app/main.rb
    def tick args
      args.state.x ||= 500
      args.state.y ||= 350
      args.state.xinc ||= 7
      args.state.yinc ||= 7
      args.state.bgcolor ||= 1
      args.state.bginc ||= 1
    
      # clear the render target on the first tick, and then never again. Draw
      #  another box to it every tick, accumulating over time.
      clear_target = (Kernel.tick_count == 0) || (args.inputs.keyboard.key_down.space)
      args.render_target(:accumulation).background_color = [ 0, 0, 0, 0 ];
      args.render_target(:accumulation).clear_before_render = clear_target
      args.render_target(:accumulation).solids << [args.state.x, args.state.y, 25, 25, 255, 0, 0, 255];
      args.state.x += args.state.xinc
      args.state.y += args.state.yinc
      args.state.bgcolor += args.state.bginc
    
      # animation upkeep...change where we draw the next box and what color the
      #  window background will be.
      if args.state.xinc > 0 && args.state.x >= 1280
        args.state.xinc = -7
      elsif args.state.xinc < 0 && args.state.x < 0
        args.state.xinc = 7
      end
    
      if args.state.yinc > 0 && args.state.y >= 720
        args.state.yinc = -7
      elsif args.state.yinc < 0 && args.state.y < 0
        args.state.yinc = 7
      end
    
      if args.state.bginc > 0 && args.state.bgcolor >= 255
        args.state.bginc = -1
      elsif args.state.bginc < 0 && args.state.bgcolor <= 0
        args.state.bginc = 1
      end
    
      # clear the screen to a shade of blue and draw the render target, which
      #  is not clearing every frame, on top of it. Note that you can NOT opt to
      #  skip clearing the screen, only render targets. The screen clears every
      #  frame; double-buffering would prevent correct updates between frames.
      args.outputs.background_color = [ 0, 0, args.state.bgcolor, 255 ]
      args.outputs.sprites << [ 0, 0, 1280, 720, :accumulation ]
    end
    
    GTK.reset
    
    

    Lighting - main.rb link

    # ./samples/07_advanced_rendering/13_lighting/app/main.rb
    def calc args
      args.state.swinging_light_sign     ||= 1
      args.state.swinging_light_start_at ||= 0
      args.state.swinging_light_duration ||= 300
      swinging_light_perc = Easing.spline(args.state.swinging_light_start_at,
                                          Kernel.tick_count,
                                          args.state.swinging_light_duration,
                                          [
                                            [0.0, 1.0, 1.0, 1.0],
                                            [1.0, 1.0, 1.0, 0.0]
                                          ])
      args.state.max_swing_angle ||= 45
    
      if args.state.swinging_light_start_at.elapsed_time > args.state.swinging_light_duration
        args.state.swinging_light_start_at = Kernel.tick_count
        args.state.swinging_light_sign *= -1
      end
    
      args.state.swinging_light_angle = 360 + ((args.state.max_swing_angle * swinging_light_perc) * args.state.swinging_light_sign)
    end
    
    def render args
      args.outputs.background_color = [0, 0, 0]
    
      # render scene
      args.outputs[:scene].sprites << { x:        0, y:   0, w: 1280, h: 720, path: :pixel }
      args.outputs[:scene].sprites << { x: 640 - 40, y: 100, w:   80, h:  80, path: 'sprites/square/blue.png' }
      args.outputs[:scene].sprites << { x: 640 - 40, y: 200, w:   80, h:  80, path: 'sprites/square/blue.png' }
      args.outputs[:scene].sprites << { x: 640 - 40, y: 300, w:   80, h:  80, path: 'sprites/square/blue.png' }
      args.outputs[:scene].sprites << { x: 640 - 40, y: 400, w:   80, h:  80, path: 'sprites/square/blue.png' }
      args.outputs[:scene].sprites << { x: 640 - 40, y: 500, w:   80, h:  80, path: 'sprites/square/blue.png' }
    
      # render light
      swinging_light_w = 1100
      args.outputs[:lights].background_color = [0, 0, 0, 0]
      args.outputs[:lights].sprites << { x: 640 - swinging_light_w.half,
                                         y: -1300,
                                         w: swinging_light_w,
                                         h: 3000,
                                         angle_anchor_x: 0.5,
                                         angle_anchor_y: 1.0,
                                         path: "sprites/lights/mask.png",
                                         angle: args.state.swinging_light_angle }
    
      args.outputs[:lights].sprites << { x: args.inputs.mouse.x - 400,
                                         y: args.inputs.mouse.y - 400,
                                         w: 800,
                                         h: 800,
                                         path: "sprites/lights/mask.png" }
    
      # merge unlighted scene with lights
      args.outputs[:lighted_scene].sprites << { x: 0, y: 0, w: 1280, h: 720, path: :lights, blendmode_enum: 0 }
      args.outputs[:lighted_scene].sprites << { blendmode_enum: 2, x: 0, y: 0, w: 1280, h: 720, path: :scene }
    
      # output lighted scene to main canvas
      args.outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: :lighted_scene }
    
      # render lights and scene render_targets as a mini map
      args.outputs.debug  << { x: 16,      y: (16 + 90).from_top, w: 160, h: 90, r: 255, g: 255, b: 255 }.solid!
      args.outputs.debug  << { x: 16,      y: (16 + 90).from_top, w: 160, h: 90, path: :lights }
      args.outputs.debug  << { x: 16 + 80, y: (16 + 90 + 8).from_top, text: ":lights render_target", r: 255, g: 255, b: 255, size_enum: -3, alignment_enum: 1 }
    
      args.outputs.debug  << { x: 16 + 160 + 16,      y: (16 + 90).from_top, w: 160, h: 90, r: 255, g: 255, b: 255 }.solid!
      args.outputs.debug  << { x: 16 + 160 + 16,      y: (16 + 90).from_top, w: 160, h: 90, path: :scene }
      args.outputs.debug  << { x: 16 + 160 + 16 + 80, y: (16 + 90 + 8).from_top, text: ":scene render_target", r: 255, g: 255, b: 255, size_enum: -3, alignment_enum: 1 }
    end
    
    def tick args
      render args
      calc args
    end
    
    GTK.reset
    
    

    Triangles - main.rb link

    # ./samples/07_advanced_rendering/14_triangles/app/main.rb
    def tick args
      dragonruby_logo_width  = 128
      dragonruby_logo_height = 101
    
      row_0 = 400
      row_1 = 250
    
      col_0 = 384 - dragonruby_logo_width.half + dragonruby_logo_width * 0
      col_1 = 384 - dragonruby_logo_width.half + dragonruby_logo_width * 1
      col_2 = 384 - dragonruby_logo_width.half + dragonruby_logo_width * 2
      col_3 = 384 - dragonruby_logo_width.half + dragonruby_logo_width * 3
      col_4 = 384 - dragonruby_logo_width.half + dragonruby_logo_width * 4
    
      # row 0
      args.outputs.solids << make_triangle(
        col_0,
        row_0,
        col_0 + dragonruby_logo_width.half,
        row_0 + dragonruby_logo_height,
        col_0 + dragonruby_logo_width.half + dragonruby_logo_width.half,
        row_0,
        0, 128, 128,
        128
      )
    
      args.outputs.solids << {
        x:  col_1,
        y:  row_0,
        x2: col_1 + dragonruby_logo_width.half,
        y2: row_0 + dragonruby_logo_height,
        x3: col_1 + dragonruby_logo_width,
        y3: row_0,
      }
    
      args.outputs.sprites << {
        x:  col_2,
        y:  row_0,
        w:  dragonruby_logo_width,
        h:  dragonruby_logo_height,
        path: 'dragonruby.png'
      }
    
      args.outputs.sprites << {
        x:  col_3,
        y:  row_0,
        x2: col_3 + dragonruby_logo_width.half,
        y2: row_0 + dragonruby_logo_height,
        x3: col_3 + dragonruby_logo_width,
        y3: row_0,
        path: 'dragonruby.png',
        source_x:  0,
        source_y:  0,
        source_x2: dragonruby_logo_width.half,
        source_y2: dragonruby_logo_height,
        source_x3: dragonruby_logo_width,
        source_y3: 0
      }
    
      args.outputs.sprites << TriangleLogo.new(x:  col_4,
                                               y:  row_0,
                                               x2: col_4 + dragonruby_logo_width.half,
                                               y2: row_0 + dragonruby_logo_height,
                                               x3: col_4 + dragonruby_logo_width,
                                               y3: row_0,
                                               path: 'dragonruby.png',
                                               source_x:  0,
                                               source_y:  0,
                                               source_x2: dragonruby_logo_width.half,
                                               source_y2: dragonruby_logo_height,
                                               source_x3: dragonruby_logo_width,
                                               source_y3: 0)
    
      # row 1
      args.outputs.primitives << make_triangle(
        col_0,
        row_1,
        col_0 + dragonruby_logo_width.half,
        row_1 + dragonruby_logo_height,
        col_0 + dragonruby_logo_width,
        row_1,
        0, 128, 128,
        Kernel.tick_count.to_radians.sin_r.abs * 255
      )
    
      args.outputs.primitives << {
        x:  col_1,
        y:  row_1,
        x2: col_1 + dragonruby_logo_width.half,
        y2: row_1 + dragonruby_logo_height,
        x3: col_1 + dragonruby_logo_width,
        y3: row_1,
        r:  0, g: 0, b: 0, a: Kernel.tick_count.to_radians.sin_r.abs * 255
      }
    
      args.outputs.sprites << {
        x:  col_2,
        y:  row_1,
        w:  dragonruby_logo_width,
        h:  dragonruby_logo_height,
        path: 'dragonruby.png',
        source_x:  0,
        source_y:  0,
        source_w:  dragonruby_logo_width,
        source_h:  dragonruby_logo_height.half +
                   dragonruby_logo_height.half * Math.sin(Kernel.tick_count.to_radians).abs,
      }
    
      args.outputs.primitives << {
        x:  col_3,
        y:  row_1,
        x2: col_3 + dragonruby_logo_width.half,
        y2: row_1 + dragonruby_logo_height,
        x3: col_3 + dragonruby_logo_width,
        y3: row_1,
        path: 'dragonruby.png',
        source_x:  0,
        source_y:  0,
        source_x2: dragonruby_logo_width.half,
        source_y2: dragonruby_logo_height.half +
                   dragonruby_logo_height.half * Math.sin(Kernel.tick_count.to_radians).abs,
        source_x3: dragonruby_logo_width,
        source_y3: 0
      }
    
      args.outputs.primitives << TriangleLogo.new(x:  col_4,
                                                  y:  row_1,
                                                  x2: col_4 + dragonruby_logo_width.half,
                                                  y2: row_1 + dragonruby_logo_height,
                                                  x3: col_4 + dragonruby_logo_width,
                                                  y3: row_1,
                                                  path: 'dragonruby.png',
                                                  source_x:  0,
                                                  source_y:  0,
                                                  source_x2: dragonruby_logo_width.half,
                                                  source_y2: dragonruby_logo_height.half +
                                                             dragonruby_logo_height.half * Math.sin(Kernel.tick_count.to_radians).abs,
                                                  source_x3: dragonruby_logo_width,
                                                  source_y3: 0)
    end
    
    def make_triangle *opts
      x, y, x2, y2, x3, y3, r, g, b, a = opts
      {
        x: x, y: y, x2: x2, y2: y2, x3: x3, y3: y3,
        r: r || 0,
        g: g || 0,
        b: b || 0,
        a: a || 255
      }
    end
    
    class TriangleLogo
      attr_sprite
    
      def initialize x:, y:, x2:, y2:, x3:, y3:, path:, source_x:, source_y:, source_x2:, source_y2:, source_x3:, source_y3:;
        @x         = x
        @y         = y
        @x2        = x2
        @y2        = y2
        @x3        = x3
        @y3        = y3
        @path      = path
        @source_x  = source_x
        @source_y  = source_y
        @source_x2 = source_x2
        @source_y2 = source_y2
        @source_x3 = source_x3
        @source_y3 = source_y3
      end
    end
    
    

    Triangles Trapezoid - main.rb link

    # ./samples/07_advanced_rendering/15_triangles_trapezoid/app/main.rb
    def tick args
      transform_scale = ((Kernel.tick_count / 3).sin.abs ** 5).half
      args.outputs.sprites << [
        { x:         600,
          y:         320,
          x2:        600,
          y2:        400,
          x3:        640,
          y3:        360,
          path:      "sprites/square/blue.png",
          source_x:  0,
          source_y:  0,
          source_x2: 0,
          source_y2: 80,
          source_x3: 40,
          source_y3: 40 },
        { x:         600,
          y:         400,
          x2:        680,
          y2:        (400 - 80 * transform_scale).round,
          x3:        640,
          y3:        360,
          path:      "sprites/square/blue.png",
          source_x:  0,
          source_y:  80,
          source_x2: 80,
          source_y2: 80,
          source_x3: 40,
          source_y3: 40 },
        { x:         640,
          y:         360,
          x2:        680,
          y2:        (400 - 80 * transform_scale).round,
          x3:        680,
          y3:        (320 + 80 * transform_scale).round,
          path:      "sprites/square/blue.png",
          source_x:  40,
          source_y:  40,
          source_x2: 80,
          source_y2: 80,
          source_x3: 80,
          source_y3: 0 },
        { x:         600,
          y:         320,
          x2:        640,
          y2:        360,
          x3:        680,
          y3:        (320 + 80 * transform_scale).round,
          path:      "sprites/square/blue.png",
          source_x:  0,
          source_y:  0,
          source_x2: 40,
          source_y2: 40,
          source_x3: 80,
          source_y3: 0 }
      ]
    end
    
    

    Camera Space World Space Simple - main.rb link

    # ./samples/07_advanced_rendering/16_camera_space_world_space_simple/app/main.rb
    def tick args
      # camera must have the following properties (x, y, and scale)
      args.state.camera ||= {
        x: 0,
        y: 0,
        scale: 1
      }
    
      args.state.camera.x += args.inputs.left_right * 10 * args.state.camera.scale
      args.state.camera.y += args.inputs.up_down * 10 * args.state.camera.scale
    
      # generate 500 shapes with random positions
      args.state.objects ||= 500.map do
        {
          x: -2000 + rand(4000),
          y: -2000 + rand(4000),
          w: 16,
          h: 16,
          path: 'sprites/square/blue.png'
        }
      end
    
      # "i" to zoom in, "o" to zoom out
      if args.inputs.keyboard.key_down.i || args.inputs.keyboard.key_down.equal_sign || args.inputs.keyboard.key_down.plus
        args.state.camera.scale += 0.1
      elsif args.inputs.keyboard.key_down.o || args.inputs.keyboard.key_down.minus
        args.state.camera.scale -= 0.1
        args.state.camera.scale = 0.1 if args.state.camera.scale < 0.1
      end
    
      # "zero" to reset zoom and camera
      if args.inputs.keyboard.key_down.zero
        args.state.camera.scale = 1
        args.state.camera.x = 0
        args.state.camera.y = 0
      end
    
      # if mouse is clicked
      if args.inputs.mouse.click
        # convert the mouse to world space and delete any objects that intersect with the mouse
        rect = Camera.to_world_space args.state.camera, args.inputs.mouse
        args.state.objects.reject! { |o| rect.intersect_rect? o }
      end
    
      # "r" to reset
      if args.inputs.keyboard.key_down.r
        GTK.reset_next_tick
      end
    
      # define scene
      args.outputs[:scene].w = Camera::WORLD_SIZE
      args.outputs[:scene].h = Camera::WORLD_SIZE
    
      # render diagonals and background of scene
      args.outputs[:scene].lines << { x: 0, y: 0, x2: 1500, y2: 1500, r: 0, g: 0, b: 0, a: 255 }
      args.outputs[:scene].lines << { x: 0, y: 1500, x2: 1500, y2: 0, r: 0, g: 0, b: 0, a: 255 }
      args.outputs[:scene].solids << { x: 0, y: 0, w: 1500, h: 1500, a: 128 }
    
      # find all objects to render
      objects_to_render = Camera.find_all_intersect_viewport args.state.camera, args.state.objects
    
      # for objects that were found, convert the rect to screen coordinates and place them in scene
      args.outputs[:scene].sprites << objects_to_render.map { |o| Camera.to_screen_space args.state.camera, o }
    
      # render scene to screen
      args.outputs.sprites << { **Camera.viewport, path: :scene }
    
      # render instructions
      args.outputs.sprites << { x: 0, y: 110.from_top, w: 1280, h: 110, path: :pixel, r: 0, g: 0, b: 0, a: 128 }
      label_style = { r: 255, g: 255, b: 255, anchor_y: 0.5 }
      args.outputs.labels << { x: 30, y: 30.from_top, text: "Arrow keys to move around. I and O Keys to zoom in and zoom out (0 to reset camera, R to reset everything).", **label_style }
      args.outputs.labels << { x: 30, y: 60.from_top, text: "Click square to remove from world.", **label_style }
      args.outputs.labels << { x: 30, y: 90.from_top, text: "Mouse locationin world: #{(Camera.to_world_space args.state.camera, args.inputs.mouse).to_sf}", **label_style }
    end
    
    # helper methods to create a camera and go to and from screen space and world space
    class Camera
      SCREEN_WIDTH = 1280
      SCREEN_HEIGHT = 720
      WORLD_SIZE = 1500
      WORLD_SIZE_HALF = WORLD_SIZE / 2
      OFFSET_X = (SCREEN_WIDTH - WORLD_SIZE) / 2
      OFFSET_Y = (SCREEN_HEIGHT - WORLD_SIZE) / 2
    
      class << self
        # given a rect in screen space, converts the rect to world space
        def to_world_space camera, rect
          rect_x = rect.x
          rect_y = rect.y
          rect_w = rect.w || 0
          rect_h = rect.h || 0
          x = (rect_x - WORLD_SIZE_HALF + camera.x * camera.scale - OFFSET_X) / camera.scale
          y = (rect_y - WORLD_SIZE_HALF + camera.y * camera.scale - OFFSET_Y) / camera.scale
          w = rect_w / camera.scale
          h = rect_h / camera.scale
          rect.merge x: x, y: y, w: w, h: h
        end
    
        # given a rect in world space, converts the rect to screen space
        def to_screen_space camera, rect
          rect_x = rect.x
          rect_y = rect.y
          rect_w = rect.w || 0
          rect_h = rect.h || 0
          x = rect_x * camera.scale - camera.x * camera.scale + WORLD_SIZE_HALF
          y = rect_y * camera.scale - camera.y * camera.scale + WORLD_SIZE_HALF
          w = rect_w * camera.scale
          h = rect_h * camera.scale
          rect.merge x: x, y: y, w: w, h: h
        end
    
        # viewport of the scene
        def viewport
          {
            x: OFFSET_X,
            y: OFFSET_Y,
            w: 1500,
            h: 1500
          }
        end
    
        # viewport in the context of the world
        def viewport_world camera
          to_world_space camera, viewport
        end
    
        # helper method to find objects within viewport
        def find_all_intersect_viewport camera, os
          Geometry.find_all_intersect_rect viewport_world(camera), os
        end
      end
    end
    
    GTK.reset
    
    

    Camera Space World Space Simple Grid Map - main.rb link

    # ./samples/07_advanced_rendering/16_camera_space_world_space_simple_grid_map/app/main.rb
    def tick args
      defaults args
      calc args
      render args
    end
    
    def defaults args
      tile_size = 100
      tiles_per_row = 32
      number_of_rows = 32
      number_of_tiles = tiles_per_row * number_of_rows
    
      # generate map tiles
      args.state.tiles ||= number_of_tiles.map_with_index do |i|
        row = i.idiv(tiles_per_row)
        col = i.mod(tiles_per_row)
        {
          x: row * tile_size,
          y: col * tile_size,
          w: tile_size,
          h: tile_size,
          path: 'sprites/square/blue.png'
        }
      end
    
      center_map = {
        x: tiles_per_row.idiv(2) * tile_size,
        y: number_of_rows.idiv(2) * tile_size,
        w: 1,
        h: 1
      }
    
      args.state.center_tile ||= args.state.tiles.find { |o| o.intersect_rect? center_map }
      args.state.selected_tile ||= args.state.center_tile
    
      # camera must have the following properties (x, y, and scale)
      if !args.state.camera
        args.state.camera = {
          x: 0,
          y: 0,
          scale: 1,
          target_x: 0,
          target_y: 0,
          target_scale: 1
        }
    
        args.state.camera.target_x = args.state.selected_tile.x + args.state.selected_tile.w.half
        args.state.camera.target_y = args.state.selected_tile.y + args.state.selected_tile.h.half
        args.state.camera.x = args.state.camera.target_x
        args.state.camera.y = args.state.camera.target_y
      end
    end
    
    def calc args
      calc_inputs args
      calc_camera args
    end
    
    def calc_inputs args
      # "i" to zoom in, "o" to zoom out
      if args.inputs.keyboard.key_down.i || args.inputs.keyboard.key_down.equal_sign || args.inputs.keyboard.key_down.plus
        args.state.camera.target_scale += 0.1 * args.state.camera.scale
      elsif args.inputs.keyboard.key_down.o || args.inputs.keyboard.key_down.minus
        args.state.camera.target_scale -= 0.1 * args.state.camera.scale
        args.state.camera.target_scale = 0.1 if args.state.camera.scale < 0.1
      end
    
      # "zero" to reset zoom and camera
      if args.inputs.keyboard.key_down.zero
        args.state.camera.target_scale = 1
        args.state.selected_tile = args.state.center_tile
      end
    
      # if mouse is clicked
      if args.inputs.mouse.click
        # convert the mouse to world space and delete any tiles that intersect with the mouse
        rect = Camera.to_world_space args.state.camera, args.inputs.mouse
        selected_tile = args.state.tiles.find { |o| rect.intersect_rect? o }
        if selected_tile
          args.state.selected_tile = selected_tile
          args.state.camera.target_scale = 1
        end
      end
    
      # "r" to reset
      if args.inputs.keyboard.key_down.r
        GTK.reset_next_tick
      end
    end
    
    def calc_camera args
      args.state.camera.target_x = args.state.selected_tile.x + args.state.selected_tile.w.half
      args.state.camera.target_y = args.state.selected_tile.y + args.state.selected_tile.h.half
      dx = args.state.camera.target_x - args.state.camera.x
      dy = args.state.camera.target_y - args.state.camera.y
      ds = args.state.camera.target_scale - args.state.camera.scale
      args.state.camera.x += dx * 0.1 * args.state.camera.scale
      args.state.camera.y += dy * 0.1 * args.state.camera.scale
      args.state.camera.scale += ds * 0.1
    end
    
    def render args
      args.outputs.background_color = [0, 0, 0]
    
      # define scene
      args.outputs[:scene].w = Camera::WORLD_SIZE
      args.outputs[:scene].h = Camera::WORLD_SIZE
      args.outputs[:scene].background_color = [0, 0, 0, 0]
    
      # render diagonals and background of scene
      args.outputs[:scene].lines << { x: 0, y: 0, x2: 1500, y2: 1500, r: 0, g: 0, b: 0, a: 255 }
      args.outputs[:scene].lines << { x: 0, y: 1500, x2: 1500, y2: 0, r: 0, g: 0, b: 0, a: 255 }
      args.outputs[:scene].solids << { x: 0, y: 0, w: 1500, h: 1500, a: 128 }
    
      # find all tiles to render
      objects_to_render = Camera.find_all_intersect_viewport args.state.camera, args.state.tiles
    
      # convert mouse to world space to see if it intersects with any tiles (hover color)
      mouse_in_world = Camera.to_world_space args.state.camera, args.inputs.mouse
    
      # for tiles that were found, convert the rect to screen coordinates and place them in scene
      args.outputs[:scene].sprites << objects_to_render.map do |o|
        if o == args.state.selected_tile
          tile_to_render = o.merge path: 'sprites/square/green.png'
        elsif o.intersect_rect? mouse_in_world
          tile_to_render = o.merge path: 'sprites/square/orange.png'
        else
          tile_to_render = o.merge path: 'sprites/square/blue.png'
        end
    
        Camera.to_screen_space args.state.camera, tile_to_render
      end
    
      # render scene to screen
      args.outputs.sprites << { **Camera.viewport, path: :scene }
    
      # render instructions
      args.outputs.sprites << { x: 0, y: 110.from_top, w: 1280, h: 110, path: :pixel, r: 0, g: 0, b: 0, a: 200 }
      label_style = { r: 255, g: 255, b: 255, anchor_y: 0.5 }
      args.outputs.labels << { x: 30, y: 30.from_top, text: "I/O or +/- keys to zoom in and zoom out (0 to reset camera, R to reset everything).", **label_style }
      args.outputs.labels << { x: 30, y: 60.from_top, text: "Click to center on square.", **label_style }
      args.outputs.labels << { x: 30, y: 90.from_top, text: "Mouse location in world: #{(Camera.to_world_space args.state.camera, args.inputs.mouse).to_sf}", **label_style }
    end
    
    # helper methods to create a camera and go to and from screen space and world space
    class Camera
      SCREEN_WIDTH = 1280
      SCREEN_HEIGHT = 720
      WORLD_SIZE = 1500
      WORLD_SIZE_HALF = WORLD_SIZE / 2
      OFFSET_X = (SCREEN_WIDTH - WORLD_SIZE) / 2
      OFFSET_Y = (SCREEN_HEIGHT - WORLD_SIZE) / 2
    
      class << self
        # given a rect in screen space, converts the rect to world space
        def to_world_space camera, rect
          rect_x = rect.x
          rect_y = rect.y
          rect_w = rect.w || 0
          rect_h = rect.h || 0
          x = (rect_x - WORLD_SIZE_HALF + camera.x * camera.scale - OFFSET_X) / camera.scale
          y = (rect_y - WORLD_SIZE_HALF + camera.y * camera.scale - OFFSET_Y) / camera.scale
          w = rect_w / camera.scale
          h = rect_h / camera.scale
          rect.merge x: x, y: y, w: w, h: h
        end
    
        # given a rect in world space, converts the rect to screen space
        def to_screen_space camera, rect
          rect_x = rect.x
          rect_y = rect.y
          rect_w = rect.w || 0
          rect_h = rect.h || 0
          x = rect_x * camera.scale - camera.x * camera.scale + WORLD_SIZE_HALF
          y = rect_y * camera.scale - camera.y * camera.scale + WORLD_SIZE_HALF
          w = rect_w * camera.scale
          h = rect_h * camera.scale
          rect.merge x: x, y: y, w: w, h: h
        end
    
        # viewport of the scene
        def viewport
          {
            x: OFFSET_X,
            y: OFFSET_Y,
            w: WORLD_SIZE,
            h: WORLD_SIZE
          }
        end
    
        # viewport in the context of the world
        def viewport_world camera
          to_world_space camera, viewport
        end
    
        # helper method to find objects within viewport
        def find_all_intersect_viewport camera, os
          Geometry.find_all_intersect_rect viewport_world(camera), os
        end
      end
    end
    
    GTK.reset
    
    

    Matrix And Triangles 2d - main.rb link

    # ./samples/07_advanced_rendering/16_matrix_and_triangles_2d/app/main.rb
    include MatrixFunctions
    
    def tick args
      args.state.square_one_sprite = { x:        0,
                                       y:        0,
                                       w:        100,
                                       h:        100,
                                       path:     "sprites/square/blue.png",
                                       source_x: 0,
                                       source_y: 0,
                                       source_w: 80,
                                       source_h: 80 }
    
      args.state.square_two_sprite = { x:        0,
                                       y:        0,
                                       w:        100,
                                       h:        100,
                                       path:     "sprites/square/red.png",
                                       source_x: 0,
                                       source_y: 0,
                                       source_w: 80,
                                       source_h: 80 }
    
      args.state.square_one        = sprite_to_triangles args.state.square_one_sprite
      args.state.square_two        = sprite_to_triangles args.state.square_two_sprite
      args.state.camera.x        ||= 0
      args.state.camera.y        ||= 0
      args.state.camera.zoom     ||= 1
      args.state.camera.rotation ||= 0
    
      zmod = 1
      move_multiplier = 1
      dzoom = 0.01
    
      if Kernel.tick_count.zmod? zmod
        args.state.camera.x += args.inputs.left_right * -1 * move_multiplier
        args.state.camera.y += args.inputs.up_down * -1 * move_multiplier
      end
    
      if args.inputs.keyboard.i
        args.state.camera.zoom += dzoom
      elsif args.inputs.keyboard.o
        args.state.camera.zoom -= dzoom
      end
    
      args.state.camera.zoom = args.state.camera.zoom.clamp(0.25, 10)
    
      args.outputs.sprites << triangles_mat3_mul(args.state.square_one,
                                                 mat3_translate(-50, -50),
                                                 mat3_rotate(Kernel.tick_count),
                                                 mat3_translate(0, 0),
                                                 mat3_translate(args.state.camera.x, args.state.camera.y),
                                                 mat3_scale(args.state.camera.zoom),
                                                 mat3_translate(640, 360))
    
      args.outputs.sprites << triangles_mat3_mul(args.state.square_two,
                                                 mat3_translate(-50, -50),
                                                 mat3_rotate(Kernel.tick_count),
                                                 mat3_translate(100, 100),
                                                 mat3_translate(args.state.camera.x, args.state.camera.y),
                                                 mat3_scale(args.state.camera.zoom),
                                                 mat3_translate(640, 360))
    
      mouse_coord = vec3 args.inputs.mouse.x,
                         args.inputs.mouse.y,
                         1
    
      mouse_coord = mul mouse_coord,
                        mat3_translate(-640, -360),
                        mat3_scale(args.state.camera.zoom),
                        mat3_translate(-args.state.camera.x, -args.state.camera.y)
    
      args.outputs.lines  << { x: 640, y:   0, h:  720 }
      args.outputs.lines  << { x:   0, y: 360, w: 1280 }
      args.outputs.labels << { x: 30, y: 60.from_top, text: "x: #{args.state.camera.x.to_sf} y: #{args.state.camera.y.to_sf} z: #{args.state.camera.zoom.to_sf}" }
      args.outputs.labels << { x: 30, y: 90.from_top, text: "Mouse: #{mouse_coord.x.to_sf} #{mouse_coord.y.to_sf}" }
      args.outputs.labels << { x: 30,
                               y: 30.from_top,
                               text: "W,A,S,D to move. I, O to zoom." }
    end
    
    def sprite_to_triangles sprite
      [
        {
          x:         sprite.x,                          y:  sprite.y,
          x2:        sprite.x,                          y2: sprite.y + sprite.h,
          x3:        sprite.x + sprite.w,               y3: sprite.y + sprite.h,
          source_x:  sprite.source_x,                   source_y:  sprite.source_y,
          source_x2: sprite.source_x,                   source_y2: sprite.source_y + sprite.source_h,
          source_x3: sprite.source_x + sprite.source_w, source_y3: sprite.source_y + sprite.source_h,
          path:      sprite.path
        },
        {
          x:  sprite.x,                                 y:  sprite.y,
          x2: sprite.x + sprite.w,                      y2: sprite.y + sprite.h,
          x3: sprite.x + sprite.w,                      y3: sprite.y,
          source_x:  sprite.source_x,                   source_y:  sprite.source_y,
          source_x2: sprite.source_x + sprite.source_w, source_y2: sprite.source_y + sprite.source_h,
          source_x3: sprite.source_x + sprite.source_w, source_y3: sprite.source_y,
          path:      sprite.path
        }
      ]
    end
    
    def mat3_translate dx, dy
      mat3 1, 0, dx,
           0, 1, dy,
           0, 0,  1
    end
    
    def mat3_rotate angle_d
      angle_r = angle_d.to_radians
      mat3 Math.cos(angle_r), -Math.sin(angle_r), 0,
           Math.sin(angle_r),  Math.cos(angle_r), 0,
                           0,                  0, 1
    end
    
    def mat3_scale scale
      mat3 scale,     0, 0,
               0, scale, 0,
               0,     0, 1
    end
    
    def triangles_mat3_mul triangles, *matrices
      triangles.map { |triangle| triangle_mat3_mul triangle, *matrices }
    end
    
    def triangle_mat3_mul triangle, *matrices
      result = [
        (vec3 triangle.x,  triangle.y,  1),
        (vec3 triangle.x2, triangle.y2, 1),
        (vec3 triangle.x3, triangle.y3, 1)
      ].map do |coord|
        mul coord, *matrices
      end
    
      {
        **triangle,
        x:  result[0].x,
        y:  result[0].y,
        x2: result[1].x,
        y2: result[1].y,
        x3: result[2].x,
        y3: result[2].y,
      }
    rescue Exception => e
      pretty_print triangle
      pretty_print result
      pretty_print matrices
      puts "#{matrices}"
      raise e
    end
    
    

    Matrix And Triangles 3d - main.rb link

    # ./samples/07_advanced_rendering/16_matrix_and_triangles_3d/app/main.rb
    include MatrixFunctions
    
    def tick args
      args.outputs.labels << { x: 0,
                               y: 30.from_top,
                               text: "W,A,S,D to move. Q,E,U,O to turn, I,K for elevation.",
                               alignment_enum: 1 }
    
      args.grid.origin_center!
    
      args.state.cam_x ||= 0.00
      if args.inputs.keyboard.left
        args.state.cam_x += 0.01
      elsif args.inputs.keyboard.right
        args.state.cam_x -= 0.01
      end
    
      args.state.cam_y ||= 0.00
      if args.inputs.keyboard.i
        args.state.cam_y += 0.01
      elsif args.inputs.keyboard.k
        args.state.cam_y -= 0.01
      end
    
      args.state.cam_z ||= 6.5
      if args.inputs.keyboard.s
        args.state.cam_z += 0.1
      elsif args.inputs.keyboard.w
        args.state.cam_z -= 0.1
      end
    
      args.state.cam_angle_y ||= 0
      if args.inputs.keyboard.q
        args.state.cam_angle_y += 0.25
      elsif args.inputs.keyboard.e
        args.state.cam_angle_y -= 0.25
      end
    
      args.state.cam_angle_x ||= 0
      if args.inputs.keyboard.u
        args.state.cam_angle_x += 0.1
      elsif args.inputs.keyboard.o
        args.state.cam_angle_x -= 0.1
      end
    
      # model A
      args.state.a = [
        [vec4(0, 0, 0, 1),   vec4(0.5, 0, 0, 1),   vec4(0, 0.5, 0, 1)],
        [vec4(0.5, 0, 0, 1), vec4(0.5, 0.5, 0, 1), vec4(0, 0.5, 0, 1)]
      ]
    
      # model to world
      args.state.a_world = mul_world args,
                                     args.state.a,
                                     (translate -0.25, -0.25, 0),
                                     (translate  0, 0, 0.25),
                                     (rotate_x Kernel.tick_count)
    
      args.state.a_camera = mul_cam args, args.state.a_world
      args.state.a_projected = mul_perspective args, args.state.a_camera
      render_projection args, args.state.a_projected
    
      # model B
      args.state.b = [
        [vec4(0, 0, 0, 1),   vec4(0.5, 0, 0, 1),   vec4(0, 0.5, 0, 1)],
        [vec4(0.5, 0, 0, 1), vec4(0.5, 0.5, 0, 1), vec4(0, 0.5, 0, 1)]
      ]
    
      # model to world
      args.state.b_world = mul_world args,
                                     args.state.b,
                                     (translate -0.25, -0.25, 0),
                                     (translate  0, 0, -0.25),
                                     (rotate_x Kernel.tick_count)
    
      args.state.b_camera = mul_cam args, args.state.b_world
      args.state.b_projected = mul_perspective args, args.state.b_camera
      render_projection args, args.state.b_projected
    
      # model C
      args.state.c = [
        [vec4(0, 0, 0, 1),   vec4(0.5, 0, 0, 1),   vec4(0, 0.5, 0, 1)],
        [vec4(0.5, 0, 0, 1), vec4(0.5, 0.5, 0, 1), vec4(0, 0.5, 0, 1)]
      ]
    
      # model to world
      args.state.c_world = mul_world args,
                                     args.state.c,
                                     (translate -0.25, -0.25, 0),
                                     (rotate_y 90),
                                     (translate -0.25,  0, 0),
                                     (rotate_x Kernel.tick_count)
    
      args.state.c_camera = mul_cam args, args.state.c_world
      args.state.c_projected = mul_perspective args, args.state.c_camera
      render_projection args, args.state.c_projected
    
      # model D
      args.state.d = [
        [vec4(0, 0, 0, 1),   vec4(0.5, 0, 0, 1),   vec4(0, 0.5, 0, 1)],
        [vec4(0.5, 0, 0, 1), vec4(0.5, 0.5, 0, 1), vec4(0, 0.5, 0, 1)]
      ]
    
      # model to world
      args.state.d_world = mul_world args,
                                     args.state.d,
                                     (translate -0.25, -0.25, 0),
                                     (rotate_y 90),
                                     (translate  0.25,  0, 0),
                                     (rotate_x Kernel.tick_count)
    
      args.state.d_camera = mul_cam args, args.state.d_world
      args.state.d_projected = mul_perspective args, args.state.d_camera
      render_projection args, args.state.d_projected
    
      # model E
      args.state.e = [
        [vec4(0, 0, 0, 1),   vec4(0.5, 0, 0, 1),   vec4(0, 0.5, 0, 1)],
        [vec4(0.5, 0, 0, 1), vec4(0.5, 0.5, 0, 1), vec4(0, 0.5, 0, 1)]
      ]
    
      # model to world
      args.state.e_world = mul_world args,
                                     args.state.e,
                                     (translate -0.25, -0.25, 0),
                                     (rotate_x 90),
                                     (translate  0,  0.25, 0),
                                     (rotate_x Kernel.tick_count)
    
      args.state.e_camera = mul_cam args, args.state.e_world
      args.state.e_projected = mul_perspective args, args.state.e_camera
      render_projection args, args.state.e_projected
    
      # model E
      args.state.f = [
        [vec4(0, 0, 0, 1),   vec4(0.5, 0, 0, 1),   vec4(0, 0.5, 0, 1)],
        [vec4(0.5, 0, 0, 1), vec4(0.5, 0.5, 0, 1), vec4(0, 0.5, 0, 1)]
      ]
    
      # model to world
      args.state.f_world = mul_world args,
                                     args.state.f,
                                     (translate -0.25, -0.25, 0),
                                     (rotate_x 90),
                                     (translate  0,  -0.25, 0),
                                     (rotate_x Kernel.tick_count)
    
      args.state.f_camera = mul_cam args, args.state.f_world
      args.state.f_projected = mul_perspective args, args.state.f_camera
      render_projection args, args.state.f_projected
    
      # render_debug args, args.state.a, args.state.a_transform, args.state.a_projected
      # args.outputs.labels << { x: -630, y:  10.from_top,  text: "x:         #{args.state.cam_x.to_sf} -> #{( args.state.cam_x * 1000 ).to_sf}" }
      # args.outputs.labels << { x: -630, y:  30.from_top,  text: "y:         #{args.state.cam_y.to_sf} -> #{( args.state.cam_y * 1000 ).to_sf}" }
      # args.outputs.labels << { x: -630, y:  50.from_top,  text: "z:         #{args.state.cam_z.fdiv(10).to_sf} -> #{( args.state.cam_z * 100 ).to_sf}" }
    end
    
    def mul_world args, model, *mul_def
      model.map do |vecs|
        vecs.map do |vec|
          mul vec,
              *mul_def
        end
      end
    end
    
    def mul_cam args, world_vecs
      world_vecs.map do |vecs|
        vecs.map do |vec|
          mul vec,
              (translate -args.state.cam_x, args.state.cam_y, -args.state.cam_z),
              (rotate_y args.state.cam_angle_y),
              (rotate_x args.state.cam_angle_x)
        end
      end
    end
    
    def mul_perspective args, camera_vecs
      camera_vecs.map do |vecs|
        vecs.map do |vec|
          perspective vec
        end
      end
    end
    
    def render_debug args, model, transform, projected
      args.outputs.labels << { x: -630, y:  10.from_top,  text: "model:     #{vecs_to_s model[0]}" }
      args.outputs.labels << { x: -630, y:  30.from_top,  text: "           #{vecs_to_s model[1]}" }
      args.outputs.labels << { x: -630, y:  50.from_top,  text: "transform: #{vecs_to_s transform[0]}" }
      args.outputs.labels << { x: -630, y:  70.from_top,  text: "           #{vecs_to_s transform[1]}" }
      args.outputs.labels << { x: -630, y:  90.from_top,  text: "projected: #{vecs_to_s projected[0]}" }
      args.outputs.labels << { x: -630, y: 110.from_top,  text: "           #{vecs_to_s projected[1]}" }
    end
    
    def render_projection args, projection
      p0 = projection[0]
      args.outputs.sprites << {
        x:  p0[0].x,   y: p0[0].y,
        x2: p0[1].x,  y2: p0[1].y,
        x3: p0[2].x,  y3: p0[2].y,
        source_x:   0, source_y:   0,
        source_x2: 80, source_y2:  0,
        source_x3:  0, source_y3: 80,
        a: 40,
        # r: 128, g: 128, b: 128,
        path: 'sprites/square/blue.png'
      }
    
      p1 = projection[1]
      args.outputs.sprites << {
        x:  p1[0].x,   y: p1[0].y,
        x2: p1[1].x,  y2: p1[1].y,
        x3: p1[2].x,  y3: p1[2].y,
        source_x:  80, source_y:   0,
        source_x2: 80, source_y2: 80,
        source_x3:  0, source_y3: 80,
        a: 40,
        # r: 128, g: 128, b: 128,
        path: 'sprites/square/blue.png'
      }
    end
    
    def perspective vec
      left   = -1.0
      right  =  1.0
      bottom = -1.0
      top    =  1.0
      near   =  300.0
      far    =  1000.0
      sx = 2 * near / (right - left)
      sy = 2 * near / (top - bottom)
      c2 = - (far + near) / (far - near)
      c1 = 2 * near * far / (near - far)
      tx = -near * (left + right) / (right - left)
      ty = -near * (bottom + top) / (top - bottom)
    
      p = mat4 sx, 0, 0, tx,
               0, sy, 0, ty,
               0, 0, c2, c1,
               0, 0, -1, 0
    
      r = mul vec, p
      r.x *= r.z / r.w / 100
      r.y *= r.z / r.w / 100
      r
    end
    
    def mat_scale scale
      mat4 scale,     0,     0,   0,
               0, scale,     0,   0,
               0,     0, scale,   0,
               0,     0,     0,   1
    end
    
    def rotate_y angle_d
      cos_t = Math.cos angle_d.to_radians
      sin_t = Math.sin angle_d.to_radians
      (mat4  cos_t,  0, sin_t, 0,
             0,      1, 0,     0,
             -sin_t, 0, cos_t, 0,
             0,      0, 0,     1)
    end
    
    def rotate_z angle_d
      cos_t = Math.cos angle_d.to_radians
      sin_t = Math.sin angle_d.to_radians
      (mat4 cos_t, -sin_t, 0, 0,
            sin_t,  cos_t, 0, 0,
            0,      0,     1, 0,
            0,      0,     0, 1)
    end
    
    def translate dx, dy, dz
      mat4 1, 0, 0, dx,
           0, 1, 0, dy,
           0, 0, 1, dz,
           0, 0, 0,  1
    end
    
    
    def rotate_x angle_d
      cos_t = Math.cos angle_d.to_radians
      sin_t = Math.sin angle_d.to_radians
      (mat4  1,     0,      0, 0,
             0, cos_t, -sin_t, 0,
             0, sin_t,  cos_t, 0,
             0,     0,      0, 1)
    end
    
    def vecs_to_s vecs
      vecs.map do |vec|
        "[#{vec.x.to_sf} #{vec.y.to_sf} #{vec.z.to_sf}]"
      end.join " "
    end
    
    

    Matrix Camera Space World Space - main.rb link

    # ./samples/07_advanced_rendering/16_matrix_camera_space_world_space/app/main.rb
    # sample app shows how to translate between screen and world coordinates using matrix multiplication
    class Game
      attr_gtk
    
      def tick
        defaults
        input
        calc
        render
      end
    
      def defaults
        return if Kernel.tick_count != 0
    
        # define the size of the world
        state.world_size = 1280
    
        # initialize the camera
        state.camera = {
          x: 0,
          y: 0,
          zoom: 1
        }
    
        # initialize entities: place entities randomly in the world
        state.entities = 200.map do
          {
            x: (rand * state.world_size - 100).to_i * (rand > 0.5 ? 1 : -1),
            y: (rand * state.world_size - 100).to_i * (rand > 0.5 ? 1 : -1),
            w: 32,
            h: 32,
            angle: 0,
            path: "sprites/square/blue.png",
            rotation_speed: rand * 5
          }
        end
    
        # backdrop for the world
        state.backdrop = { x: -state.world_size,
                           y: -state.world_size,
                           w: state.world_size * 2,
                           h: state.world_size * 2,
                           r: 200,
                           g: 100,
                           b: 0,
                           a: 128,
                           path: :pixel }
    
        # rect representing the screen
        state.screen_rect = { x: 0, y: 0, w: 1280, h: 720 }
    
        # update the camera matricies (initial state)
        update_matricies!
      end
    
      # if the camera is ever changed, recompute the matricies that are used
      # to translate between screen and world coordinates. we want to cache
      # the resolved matrix for speed
      def update_matricies!
        # camera space is defined with three matricies
        # every entity is:
        # - offset by the location of the camera
        # - scaled
        # - then centered on the screen
        state.to_camera_space_matrix = MatrixFunctions.mul(mat3_translate(state.camera.x, state.camera.y),
                                                           mat3_scale(state.camera.zoom),
                                                           mat3_translate(640, 360))
    
        # world space is defined based off the camera matricies but inverted:
        # every entity is:
        # - uncentered from the screen
        # - unscaled
        # - offset by the location of the camera in the opposite direction
        state.to_world_space_matrix = MatrixFunctions.mul(mat3_translate(-640, -360),
                                                          mat3_scale(1.0 / state.camera.zoom),
                                                          mat3_translate(-state.camera.x, -state.camera.y))
    
        # the viewport is computed by taking the screen rect and moving it into world space.
        # what entities get rendered is based off of the viewport
        state.viewport = rect_mul_matrix(state.screen_rect, state.to_world_space_matrix)
      end
    
      def input
        # if the camera is changed, invalidate/recompute the translation matricies
        should_update_matricies = false
    
        # + and - keys zoom in and out
        if inputs.keyboard.equal_sign || inputs.keyboard.plus || inputs.mouse.wheel && inputs.mouse.wheel.y > 0
          state.camera.zoom += 0.01 * state.camera.zoom
          should_update_matricies = true
        elsif inputs.keyboard.minus || inputs.mouse.wheel && inputs.mouse.wheel.y < 0
          state.camera.zoom -= 0.01 * state.camera.zoom
          should_update_matricies = true
        end
    
        # clamp the zoom to a minimum of 0.25
        if state.camera.zoom < 0.25
          state.camera.zoom = 0.25
          should_update_matricies = true
        end
    
        # left and right keys move the camera left and right
        if inputs.left_right != 0
          state.camera.x += -1 * (inputs.left_right * 10) * state.camera.zoom
          should_update_matricies = true
        end
    
        # up and down keys move the camera up and down
        if inputs.up_down != 0
          state.camera.y += -1 * (inputs.up_down * 10) * state.camera.zoom
          should_update_matricies = true
        end
    
        # reset the camera to the default position
        if inputs.keyboard.key_down.zero
          state.camera.x = 0
          state.camera.y = 0
          state.camera.zoom = 1
          should_update_matricies = true
        end
    
        # if the update matricies flag is set, recompute the matricies
        update_matricies! if should_update_matricies
      end
    
      def calc
        # rotate all the entities by their rotation speed
        # and reset their hovered state
        state.entities.each do |entity|
          entity.hovered = false
          entity.angle += entity.rotation_speed
        end
    
        # find all the entities that are hovered by the mouse and update their state back to hovered
        mouse_in_world = rect_to_world_coordinates inputs.mouse.rect
        hovered_entities = Geometry.find_all_intersect_rect mouse_in_world, state.entities
        hovered_entities.each { |entity| entity.hovered = true }
      end
    
      def render
        # create a render target to represent the camera's viewport
        outputs[:scene].w = state.world_size
        outputs[:scene].h = state.world_size
    
        # render the backdrop
        outputs[:scene].primitives << rect_to_screen_coordinates(state.backdrop)
    
        # get all entities that are within the camera's viewport
        entities_to_render = Geometry.find_all_intersect_rect state.viewport, state.entities
    
        # render all the entities within the viewport
        outputs[:scene].primitives << entities_to_render.map do |entity|
          r = rect_to_screen_coordinates entity
    
          # change the color of the entity if it's hovered
          r.merge!(path: "sprites/square/red.png") if entity.hovered
    
          r
        end
    
        # render the camera's viewport
        outputs.sprites << {
          x: 0,
          y: 0,
          w: state.world_size,
          h: state.world_size,
          path: :scene
        }
    
        # show a label that shows the mouse's screen and world coordinates
        outputs.labels << { x: 30, y: 30.from_top, text: "#{gtk.current_framerate.to_sf}" }
    
        mouse_in_world = rect_to_world_coordinates inputs.mouse.rect
    
        outputs.labels << {
          x: 30,
          y: 55.from_top,
          text: "Screen Coordinates: #{inputs.mouse.x}, #{inputs.mouse.y}",
        }
    
        outputs.labels << {
          x: 30,
          y: 80.from_top,
          text: "World Coordinates: #{mouse_in_world.x.to_sf}, #{mouse_in_world.y.to_sf}",
        }
      end
    
      def rect_to_screen_coordinates rect
        rect_mul_matrix rect, state.to_camera_space_matrix
      end
    
      def rect_to_world_coordinates rect
        rect_mul_matrix rect, state.to_world_space_matrix
      end
    
      def rect_mul_matrix rect, matrix
        # the bottom left and top right corners of the rect
        # are multiplied by the matrix to get the new coordinates
        bottom_left = MatrixFunctions.mul (MatrixFunctions.vec3 rect.x, rect.y, 1), matrix
        top_right   = MatrixFunctions.mul (MatrixFunctions.vec3 rect.x + rect.w, rect.y + rect.h, 1), matrix
    
        # with the points of the rect recomputed, reconstruct the rect
        rect.merge x: bottom_left.x,
                   y: bottom_left.y,
                   w: top_right.x - bottom_left.x,
                   h: top_right.y - bottom_left.y
      end
    
      # this is the definition of how to move a point in 2d space using a matrix
      def mat3_translate x, y
        MatrixFunctions.mat3 1, 0, x,
                             0, 1, y,
                             0, 0, 1
      end
    
      # this is the definition of how to scale a point in 2d space using a matrix
      def mat3_scale scale
        MatrixFunctions.mat3 scale, 0, 0,
                             0, scale, 0,
                             0,     0, 1
      end
    end
    
    $game = Game.new
    
    def tick args
      $game.args = args
      $game.tick
    end
    
    GTK.reset
    
    

    Matrix Cubeworld - main.rb link

    # ./samples/07_advanced_rendering/16_matrix_cubeworld/app/main.rb
    require 'app/modeling-api.rb'
    
    include MatrixFunctions
    
    def tick args
      args.outputs.labels << { x: 0,
                               y: 30.from_top,
                               text: "W,A,S,D to move. Mouse to look.",
                               alignment_enum: 1 }
    
      args.grid.origin_center!
    
      args.state.cam_y ||= 0.00
      if args.inputs.keyboard.i
        args.state.cam_y += 0.01
      elsif args.inputs.keyboard.k
        args.state.cam_y -= 0.01
      end
    
      args.state.cam_angle_y ||= 0
      if args.inputs.keyboard.q
        args.state.cam_angle_y += 0.25
      elsif args.inputs.keyboard.e
        args.state.cam_angle_y -= 0.25
      end
    
      args.state.cam_angle_x ||= 0
      if args.inputs.keyboard.u
        args.state.cam_angle_x += 0.1
      elsif args.inputs.keyboard.o
        args.state.cam_angle_x -= 0.1
      end
    
      if args.inputs.mouse.has_focus
        y_change_rate = (args.inputs.mouse.x / 640) ** 2
        if args.inputs.mouse.x < 0
          args.state.cam_angle_y -= 0.8 * y_change_rate
        else
          args.state.cam_angle_y += 0.8 * y_change_rate
        end
    
        x_change_rate = (args.inputs.mouse.y / 360) ** 2
        if args.inputs.mouse.y < 0
          args.state.cam_angle_x += 0.8 * x_change_rate
        else
          args.state.cam_angle_x -= 0.8 * x_change_rate
        end
      end
    
      args.state.cam_z ||= 6.4
      if args.inputs.keyboard.up
        point_1 = { x: 0, y: 0.02 }
        point_r = Geometry.rotate_point point_1, args.state.cam_angle_y
        args.state.cam_x -= point_r.x
        args.state.cam_z -= point_r.y
      elsif args.inputs.keyboard.down
        point_1 = { x: 0, y: -0.02 }
        point_r = Geometry.rotate_point point_1, args.state.cam_angle_y
        args.state.cam_x -= point_r.x
        args.state.cam_z -= point_r.y
      end
    
      args.state.cam_x ||= 0.00
      if args.inputs.keyboard.right
        point_1 = { x: -0.02, y: 0 }
        point_r = Geometry.rotate_point point_1, args.state.cam_angle_y
        args.state.cam_x -= point_r.x
        args.state.cam_z -= point_r.y
      elsif args.inputs.keyboard.left
        point_1 = { x:  0.02, y: 0 }
        point_r = Geometry.rotate_point point_1, args.state.cam_angle_y
        args.state.cam_x -= point_r.x
        args.state.cam_z -= point_r.y
      end
    
    
      if args.inputs.keyboard.key_down.r || args.inputs.keyboard.key_down.zero
        args.state.cam_x = 0.00
        args.state.cam_y = 0.00
        args.state.cam_z = 1.00
        args.state.cam_angle_y = 0
        args.state.cam_angle_x = 0
      end
    
      if !args.state.models
        args.state.models = []
        25.times do
          args.state.models.concat new_random_cube
        end
      end
    
      args.state.models.each do |m|
        render_triangles args, m
      end
    
      args.outputs.lines << { x:   0, y: -50, h: 100, a: 80 }
      args.outputs.lines << { x: -50, y:   0, w: 100, a: 80 }
    end
    
    def mul_triangles model, *mul_def
      combined = mul mul_def
      model.map do |vecs|
        vecs.map do |vec|
          mul vec, *combined
        end
      end
    end
    
    def mul_cam args, world_vecs
      mul_triangles world_vecs,
                    (translate -args.state.cam_x, -args.state.cam_y, -args.state.cam_z),
                    (rotate_y args.state.cam_angle_y),
                    (rotate_x args.state.cam_angle_x)
    end
    
    def mul_perspective camera_vecs
      camera_vecs.map do |vecs|
        r = vecs.map do |vec|
          perspective vec
        end
    
        r if r[0] && r[1] && r[2]
      end.reject_nil
    end
    
    def render_debug args, model, transform, projected
      args.outputs.labels << { x: -630, y:  10.from_top,  text: "model:     #{vecs_to_s model[0]}" }
      args.outputs.labels << { x: -630, y:  30.from_top,  text: "           #{vecs_to_s model[1]}" }
      args.outputs.labels << { x: -630, y:  50.from_top,  text: "transform: #{vecs_to_s transform[0]}" }
      args.outputs.labels << { x: -630, y:  70.from_top,  text: "           #{vecs_to_s transform[1]}" }
      args.outputs.labels << { x: -630, y:  90.from_top,  text: "projected: #{vecs_to_s projected[0]}" }
      args.outputs.labels << { x: -630, y: 110.from_top,  text: "           #{vecs_to_s projected[1]}" }
    end
    
    def render_triangles args, triangles
      camera_space = mul_cam args, triangles
      projection = mul_perspective camera_space
    
      args.outputs.sprites << projection.map_with_index do |i, index|
        if i
          {
            x:  i[0].x,   y: i[0].y,
            x2: i[1].x,  y2: i[1].y,
            x3: i[2].x,  y3: i[2].y,
            source_x:   0, source_y:   0,
            source_x2: 80, source_y2:  0,
            source_x3:  0, source_y3: 80,
            r: 128, g: 128, b: 128,
            a: 80 + 128 * 1 / (index + 1),
            path: :pixel
          }
        end
      end
    end
    
    def perspective vec
      left   =  100.0
      right  = -100.0
      bottom =  100.0
      top    = -100.0
      near   =  3000.0
      far    =  8000.0
      sx = 2 * near / (right - left)
      sy = 2 * near / (top - bottom)
      c2 = - (far + near) / (far - near)
      c1 = 2 * near * far / (near - far)
      tx = -near * (left + right) / (right - left)
      ty = -near * (bottom + top) / (top - bottom)
    
      p = mat4 sx, 0, 0, tx,
               0, sy, 0, ty,
               0, 0, c2, c1,
               0, 0, -1, 0
    
      r = mul vec, p
      return nil if r.w < 0
      r.x *= r.z / r.w / 100
      r.y *= r.z / r.w / 100
      r
    end
    
    def mat_scale scale
      mat4 scale,     0,     0,   0,
               0, scale,     0,   0,
               0,     0, scale,   0,
               0,     0,     0,   1
    end
    
    def rotate_y angle_d
      cos_t = Math.cos angle_d.to_radians
      sin_t = Math.sin angle_d.to_radians
      (mat4  cos_t,  0, sin_t, 0,
             0,      1, 0,     0,
             -sin_t, 0, cos_t, 0,
             0,      0, 0,     1)
    end
    
    def rotate_z angle_d
      cos_t = Math.cos angle_d.to_radians
      sin_t = Math.sin angle_d.to_radians
      (mat4 cos_t, -sin_t, 0, 0,
            sin_t,  cos_t, 0, 0,
            0,      0,     1, 0,
            0,      0,     0, 1)
    end
    
    def translate dx, dy, dz
      mat4 1, 0, 0, dx,
           0, 1, 0, dy,
           0, 0, 1, dz,
           0, 0, 0,  1
    end
    
    
    def rotate_x angle_d
      cos_t = Math.cos angle_d.to_radians
      sin_t = Math.sin angle_d.to_radians
      (mat4  1,     0,      0, 0,
             0, cos_t, -sin_t, 0,
             0, sin_t,  cos_t, 0,
             0,     0,      0, 1)
    end
    
    def vecs_to_s vecs
      vecs.map do |vec|
        "[#{vec.x.to_sf} #{vec.y.to_sf} #{vec.z.to_sf}]"
      end.join " "
    end
    
    def new_random_cube
      cube_w = rand * 0.2 + 0.1
      cube_h = rand * 0.2 + 0.1
      randx = rand * 2.0 * [1, -1].sample
      randy = rand * 2.0
      randz = rand * 5   * [1, -1].sample
    
      cube = [
        square do
          scale x: cube_w, y: cube_h
          translate x: -cube_w / 2, y: -cube_h / 2
          rotate_x 90
          translate y: -cube_h / 2
          translate x: randx, y: randy, z: randz
        end,
        square do
          scale x: cube_w, y: cube_h
          translate x: -cube_w / 2, y: -cube_h / 2
          rotate_x 90
          translate y:  cube_h / 2
          translate x: randx, y: randy, z: randz
        end,
        square do
          scale x: cube_h, y: cube_h
          translate x: -cube_h / 2, y: -cube_h / 2
          rotate_y 90
          translate x: -cube_w / 2
          translate x: randx, y: randy, z: randz
        end,
        square do
          scale x: cube_h, y: cube_h
          translate x: -cube_h / 2, y: -cube_h / 2
          rotate_y 90
          translate x:  cube_w / 2
          translate x: randx, y: randy, z: randz
        end,
        square do
          scale x: cube_w, y: cube_h
          translate x: -cube_w / 2, y: -cube_h / 2
          translate z: -cube_h / 2
          translate x: randx, y: randy, z: randz
        end,
        square do
          scale x: cube_w, y: cube_h
          translate x: -cube_w / 2, y: -cube_h / 2
          translate z:  cube_h / 2
          translate x: randx, y: randy, z: randz
        end
      ]
    
      cube
    end
    
    GTK.reset
    
    

    Matrix Cubeworld - modeling-api.rb link

    # ./samples/07_advanced_rendering/16_matrix_cubeworld/app/modeling-api.rb
    class ModelingApi
      attr :matricies
    
      def initialize
        @matricies = []
      end
    
      def scale x: 1, y: 1, z: 1
        @matricies << scale_matrix(x: x, y: y, z: z)
        if block_given?
          yield
          @matricies << scale_matrix(x: -x, y: -y, z: -z)
        end
      end
    
      def translate x: 0, y: 0, z: 0
        @matricies << translate_matrix(x: x, y: y, z: z)
        if block_given?
          yield
          @matricies << translate_matrix(x: -x, y: -y, z: -z)
        end
      end
    
      def rotate_x x
        @matricies << rotate_x_matrix(x)
        if block_given?
          yield
          @matricies << rotate_x_matrix(-x)
        end
      end
    
      def rotate_y y
        @matricies << rotate_y_matrix(y)
        if block_given?
          yield
          @matricies << rotate_y_matrix(-y)
        end
      end
    
      def rotate_z z
        @matricies << rotate_z_matrix(z)
        if block_given?
          yield
          @matricies << rotate_z_matrix(-z)
        end
      end
    
      def scale_matrix x:, y:, z:;
        mat4 x, 0, 0, 0,
             0, y, 0, 0,
             0, 0, z, 0,
             0, 0, 0, 1
      end
    
      def translate_matrix x:, y:, z:;
        mat4 1, 0, 0, x,
             0, 1, 0, y,
             0, 0, 1, z,
             0, 0, 0, 1
      end
    
      def rotate_y_matrix angle_d
        cos_t = Math.cos angle_d.to_radians
        sin_t = Math.sin angle_d.to_radians
        (mat4  cos_t,  0, sin_t, 0,
               0,      1, 0,     0,
               -sin_t, 0, cos_t, 0,
               0,      0, 0,     1)
      end
    
      def rotate_z_matrix angle_d
        cos_t = Math.cos angle_d.to_radians
        sin_t = Math.sin angle_d.to_radians
        (mat4 cos_t, -sin_t, 0, 0,
              sin_t,  cos_t, 0, 0,
              0,      0,     1, 0,
              0,      0,     0, 1)
      end
    
      def rotate_x_matrix angle_d
        cos_t = Math.cos angle_d.to_radians
        sin_t = Math.sin angle_d.to_radians
        (mat4  1,     0,      0, 0,
               0, cos_t, -sin_t, 0,
               0, sin_t,  cos_t, 0,
               0,     0,      0, 1)
      end
    
      def __mul_triangles__ model, *mul_def
        model.map do |vecs|
          vecs.map do |vec|
            mul vec,
                *mul_def
          end
        end
      end
    end
    
    def square &block
      square_verticies = [
        [vec4(0, 0, 0, 1),   vec4(1.0, 0, 0, 1),   vec4(0, 1.0, 0, 1)],
        [vec4(1.0, 0, 0, 1), vec4(1.0, 1.0, 0, 1), vec4(0, 1.0, 0, 1)]
      ]
    
      m = ModelingApi.new
      m.instance_eval &block if block
      m.__mul_triangles__ square_verticies, *m.matricies
    end
    
    

    Override Core Rendering - main.rb link

    # ./samples/07_advanced_rendering/17_override_core_rendering/app/main.rb
    class GTK::Runtime
      # You can completely override how DR renders by defining this method
      # It is strongly recommend that you do not do this unless you know what you're doing.
      def primitives pass
        # pass.solids.each { |o| draw_solid o }
        # pass.static_solids.each { |o| draw_solid o }
        # pass.sprites.each { |o| draw_sprite o }
        # pass.static_sprites.each { |o| draw_sprite o }
        # pass.primitives.each { |o| draw_primitive o }
        # pass.static_primitives.each { |o| draw_primitive o }
        pass.labels.each { |o| draw_label o }
        pass.static_labels.each { |o| draw_label o }
        # pass.lines.each { |o| draw_line o }
        # pass.static_lines.each { |o| draw_line o }
        # pass.borders.each { |o| draw_border o }
        # pass.static_borders.each { |o| draw_border o }
    
        # if !self.production
        #   pass.debug.each { |o| draw_primitive o }
        #   pass.static_debug.each { |o| draw_primitive o }
        # end
    
        # pass.reserved.each { |o| draw_primitive o }
        # pass.static_reserved.each { |o| draw_primitive o }
      end
    end
    
    def tick args
      args.outputs.labels << { x: 30, y: 30, text: "primitives function defined, only labels rendered" }
      args.outputs.sprites << { x: 100, y: 100, w: 100, h: 100, path: "dragonruby.png" }
    end
    
    

    Layouts - main.rb link

    # ./samples/07_advanced_rendering/18_layouts/app/main.rb
    def tick args
      args.outputs.solids << Layout.rect(row: 0,
                                              col: 0,
                                              w: 24,
                                              h: 12,
                                              include_row_gutter: true,
                                              include_col_gutter: true).merge(b: 255, a: 80)
      render_row_examples args
      render_column_examples args
      render_max_width_max_height_examples args
      render_points_with_anchored_label_examples args
      render_centered_rect_examples args
      render_rect_group_examples args
    end
    
    def render_row_examples args
      # rows (light blue)
      args.outputs.labels << Layout.rect(row: 1, col: 6 + 3).merge(text: "row examples", anchor_x: 0.5, anchor_y: 0.5)
      4.map_with_index do |row|
        args.outputs.solids << Layout.rect(row: row, col: 6, w: 1, h: 1).merge(**light_blue)
      end
    
      2.map_with_index do |row|
        args.outputs.solids << Layout.rect(row: row * 2, col: 6 + 1, w: 1, h: 2).merge(**light_blue)
      end
    
      4.map_with_index do |row|
        args.outputs.solids << Layout.rect(row: row, col: 6 + 2, w: 2, h: 1).merge(**light_blue)
      end
    
      2.map_with_index do |row|
        args.outputs.solids << Layout.rect(row: row * 2, col: 6 + 4, w: 2, h: 2).merge(**light_blue)
      end
    end
    
    def render_column_examples args
      # columns (yellow)
      yellow = { r: 255, g: 255, b: 128 }
      args.outputs.labels << Layout.rect(row: 1, col: 12 + 3).merge(text: "column examples", anchor_x: 0.5, anchor_y: 0.5)
      6.times do |col|
        args.outputs.solids << Layout.rect(row: 0, col: 12 + col, w: 1, h: 1).merge(**yellow)
      end
    
      3.times do |col|
        args.outputs.solids << Layout.rect(row: 1, col: 12 + col * 2, w: 2, h: 1).merge(**yellow)
      end
    
      6.times do |col|
        args.outputs.solids << Layout.rect(row: 2, col: 12 + col, w: 1, h: 2).merge(**yellow)
      end
    end
    
    def render_max_width_max_height_examples args
      # max width/height baseline (transparent green)
      args.outputs.labels << Layout.rect(row: 4, col: 12).merge(text: "max width/height examples", anchor_x: 0.5, anchor_y: 0.5)
      args.outputs.solids << Layout.rect(row: 4, col: 0, w: 24, h: 2).merge(a: 64, **green)
    
      # max height
      args.outputs.solids << Layout.rect(row: 4, col: 0, w: 24, h: 2, max_height: 1).merge(a: 64, **green)
    
      # max width
      args.outputs.solids << Layout.rect(row: 4, col: 0, w: 24, h: 2, max_width: 12).merge(a: 64, **green)
    end
    
    def render_points_with_anchored_label_examples args
      # labels relative to rects
      label_color = { r: 0, g: 0, b: 0 }
    
      # labels realtive to point, achored at 0.0, 0.0
      args.outputs.borders << Layout.rect(row: 6, col: 3, w: 6, h: 5)
      args.outputs.labels << Layout.rect(row: 6, col: 3, w: 6, h: 1).center.merge(text: "layout.point anchored to 0.0, 0.0", anchor_x: 0.5, anchor_y: 0.5, size_px: 15)
      grey = { r: 128, g: 128, b: 128 }
      args.outputs.solids << Layout.rect(row: 7, col: 4.5).merge(**grey)
      args.outputs.labels << Layout.point(row: 7, col: 4.5, row_anchor: 1.0, col_anchor: 0.0).merge(text: "[x]", anchor_x: 0.5, anchor_y: 0.5, **label_color)
    
      args.outputs.solids << Layout.rect(row: 7, col: 5.5).merge(**grey)
      args.outputs.labels << Layout.point(row: 7, col: 5.5, row_anchor: 1.0, col_anchor: 0.5).merge(text: "[x]", anchor_x: 0.5, anchor_y: 0.5, **label_color)
    
      args.outputs.solids << Layout.rect(row: 7, col: 6.5).merge(**grey)
      args.outputs.labels << Layout.point(row: 7, col: 6.5, row_anchor: 1.0, col_anchor: 1.0).merge(text: "[x]", anchor_x: 0.5, anchor_y: 0.5, **label_color)
    
      args.outputs.solids << Layout.rect(row: 8, col: 4.5).merge(**grey)
      args.outputs.labels << Layout.point(row: 8, col: 4.5, row_anchor: 0.5, col_anchor: 0.0).merge(text: "[x]", anchor_x: 0.5, anchor_y: 0.5, **label_color)
    
      args.outputs.solids << Layout.rect(row: 8, col: 5.5).merge(**grey)
      args.outputs.labels << Layout.point(row: 8, col: 5.5, row_anchor: 0.5, col_anchor: 0.5).merge(text: "[x]", anchor_x: 0.5, anchor_y: 0.5, **label_color)
    
      args.outputs.solids << Layout.rect(row: 8, col: 6.5).merge(**grey)
      args.outputs.labels << Layout.point(row: 8, col: 6.5, row_anchor: 0.5, col_anchor: 1.0).merge(text: "[x]", anchor_x: 0.5, anchor_y: 0.5, **label_color)
    
      args.outputs.solids << Layout.rect(row: 9, col: 4.5).merge(**grey)
      args.outputs.labels << Layout.point(row: 9, col: 4.5, row_anchor: 0.0, col_anchor: 0.0).merge(text: "[x]", anchor_x: 0.5, anchor_y: 0.5, **label_color)
    
      args.outputs.solids << Layout.rect(row: 9, col: 5.5).merge(**grey)
      args.outputs.labels << Layout.point(row: 9, col: 5.5, row_anchor: 0.0, col_anchor: 0.5).merge(text: "[x]", anchor_x: 0.5, anchor_y: 0.5, **label_color)
    
      args.outputs.solids << Layout.rect(row: 9, col: 6.5).merge(**grey)
      args.outputs.labels << Layout.point(row: 9, col: 6.5, row_anchor: 0.0, col_anchor: 1.0).merge(text: "[x]", anchor_x: 0.5, anchor_y: 0.5, **label_color)
    end
    
    def render_centered_rect_examples args
      # centering rects
      args.outputs.borders << Layout.rect(row: 6, col: 9, w: 6, h: 5)
      args.outputs.labels << Layout.rect(row: 6, col: 9, w: 6, h: 1).center.merge(text: "layout.rect centered inside another rect", anchor_x: 0.5, anchor_y: 0.5, size_px: 15)
      outer_rect = Layout.rect(row: 7, col: 10.5, w: 3, h: 3)
    
      # render outer rect
      args.outputs.solids << outer_rect.merge(**light_blue)
    
      # # center a yellow rect with w and h of two
      args.outputs.solids << Layout.rect_center(
        Layout.rect(w: 1, h: 5), # inner rect
        outer_rect, # outer rect
      ).merge(**yellow)
    
      # # center a black rect with w three h of one
      args.outputs.solids << Layout.rect_center(
        Layout.rect(w: 5, h: 1), # inner rect
        outer_rect, # outer rect
      )
    end
    
    def render_rect_group_examples args
      args.outputs.labels << Layout.rect(row: 6, col: 15, w: 6, h: 1).center.merge(text: "layout.rect_group usage", anchor_x: 0.5, anchor_y: 0.5, size_px: 15)
      args.outputs.borders << Layout.rect(row: 6, col: 15, w: 6, h: 5)
    
      horizontal_markers = [
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
      ]
    
      args.outputs.solids << Layout.rect_group(row: 7,
                                                    col: 15,
                                                    dcol: 1,
                                                    w: 1,
                                                    h: 1,
                                                    group: horizontal_markers)
    
      vertical_markers = [
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 }
      ]
    
      args.outputs.solids << Layout.rect_group(row: 7,
                                                    col: 15,
                                                    drow: 1,
                                                    w: 1,
                                                    h: 1,
                                                    group: vertical_markers)
    
      colors = [
        { r:   0, g:   0, b:   0 },
        { r:  50, g:  50, b:  50 },
        { r: 100, g: 100, b: 100 },
        { r: 150, g: 150, b: 150 },
        { r: 200, g: 200, b: 200 },
        { r: 250, g: 250, b: 250 },
      ]
    
      args.outputs.solids << Layout.rect_group(row: 8,
                                                    col: 15,
                                                    dcol: 1,
                                                    w: 1,
                                                    h: 1,
                                                    group: colors)
    end
    
    def light_blue
      { r: 128, g: 255, b: 255 }
    end
    
    def yellow
      { r: 255, g: 255, b: 128 }
    end
    
    def green
      { r: 0, g: 128, b: 80 }
    end
    
    def white
      { r: 255, g: 255, b: 255 }
    end
    
    def label_color
      { r: 0, g: 0, b: 0 }
    end
    
    GTK.reset
    
    

    19 Layouts Extended Parameters - main.rb link

    # ./samples/07_advanced_rendering/19_layouts_extended_parameters/app/main.rb
    def tick args
      args.state.origin ||= :top_left
      args.state.safe_area ||= :yes
    
      args.outputs.watch "Instructions:"
      args.outputs.watch "Use tab to change origin, use space to toggle safe area."
      args.outputs.watch "origin: #{args.state.origin}"
      args.outputs.watch "safe_area: #{args.state.safe_area}"
    
      if args.inputs.keyboard.key_down.tab
        if args.state.origin == :top_left
          args.state.origin = :top_right
        elsif args.state.origin == :top_right
          args.state.origin = :bottom_right
        elsif args.state.origin == :bottom_right
          args.state.origin = :bottom_left
        elsif args.state.origin == :bottom_left
          args.state.origin = :top_left
        end
      end
    
      if args.inputs.keyboard.key_down.space
        if args.state.safe_area == :yes
          args.state.safe_area = :no
        elsif args.state.safe_area == :no
          args.state.safe_area = :yes
        end
      end
    
      origin = args.state.origin
      safe_area = args.state.safe_area == :yes
    
      sub_grid = Layout.rect(row: 0,
                             col: 0,
                             w: 4,
                             h: 5,
                             include_row_gutter: true,
                             include_col_gutter: true,
                             origin: origin,
                             safe_area: safe_area)
    
      slots ||= {}
      20.times do |i|
        row = i.idiv(4)
        col = i % 4
        slots[i] = Layout.rect(row: i.idiv(4),
                               col: i % 4,
                               w: 1,
                               h: 1,
                               safe_area: safe_area,
                               origin: origin)
                         .merge(row: row, col: col)
      end
    
      args.outputs.primitives << Layout.debug_primitives
      args.outputs.primitives << sub_grid.merge(path: :solid, r: 255, g: 0, b: 0, a: 255)
      args.outputs.primitives << slots.values.map { |r| r.merge(path: :solid, r: 0, g: 0, b: 0, a: 255) }
      args.outputs.primitives << slots.values.map { |r| r.center.merge(text: "#{r.row},#{r.col}", r: 255, g: 255, b: 255, anchor_x: 0.5, anchor_y: 0.5) }
    end
    
    

    Advanced Rendering Hd link

    Hd Labels - main.rb link

    # ./samples/07_advanced_rendering_hd/01_hd_labels/app/main.rb
    def tick args
      args.state.output_cycle ||= :top_level
    
      args.outputs.background_color = [0, 0, 0]
      args.outputs.solids << [0, 0, 1280, 720, 255, 255, 255]
      if args.state.output_cycle == :top_level
        render_main args
      else
        render_scene args
      end
    
      # cycle between labels in top level args.outputs
      # and labels inside of render target
      if Kernel.tick_count.zmod? 300
        if args.state.output_cycle == :top_level
          args.state.output_cycle = :render_target
        else
          args.state.output_cycle = :top_level
        end
      end
    
      args.state.window_scale ||= 1
      if args.inputs.keyboard.key_down.space
        if args.state.window_scale == 1
          args.state.window_scale = 2
          GTK.set_window_scale 2
        else
          args.state.window_scale = 1
          GTK.set_window_scale 1
        end
      end
    end
    
    def render_main args
      # center line
      args.outputs.lines   << { x:   0, y: 360, x2: 1280, y2: 360 }
      args.outputs.lines   << { x: 640, y:   0, x2:  640, y2: 720 }
    
      # horizontal ruler
      args.outputs.lines   << { x:   0, y: 370, x2: 1280, y2: 370 }
      args.outputs.lines   << { x:   0, y: 351, x2: 1280, y2: 351 }
    
      # vertical ruler
      args.outputs.lines   << { x:  575, y: 0, x2: 575, y2: 720 }
      args.outputs.lines   << { x:  701, y: 0, x2: 701, y2: 720 }
    
      args.outputs.sprites << { x: 640 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square/blue.png", a: 128 }
      args.outputs.labels  << { x:  640, y:   0, text: "(bottom)",  alignment_enum: 1, vertical_alignment_enum: 0 }
      args.outputs.labels  << { x:  640, y: 425, text: "top_level", alignment_enum: 1, vertical_alignment_enum: 1 }
      args.outputs.labels  << { x:  640, y: 720, text: "(top)",     alignment_enum: 1, vertical_alignment_enum: 2 }
      args.outputs.labels  << { x:    0, y: 360, text: "(left)",    alignment_enum: 0, vertical_alignment_enum: 1 }
      args.outputs.labels  << { x: 1280, y: 360, text: "(right)",   alignment_enum: 2, vertical_alignment_enum: 1 }
    end
    
    def render_scene args
      args.outputs[:scene].background_color = [255, 255, 255, 0]
    
      # center line
      args.outputs[:scene].lines   << { x:   0, y: 360, x2: 1280, y2: 360 }
      args.outputs[:scene].lines   << { x: 640, y:   0, x2:  640, y2: 720 }
    
      # horizontal ruler
      args.outputs[:scene].lines   << { x:   0, y: 370, x2: 1280, y2: 370 }
      args.outputs[:scene].lines   << { x:   0, y: 351, x2: 1280, y2: 351 }
    
      # vertical ruler
      args.outputs[:scene].lines   << { x:  575, y: 0, x2: 575, y2: 720 }
      args.outputs[:scene].lines   << { x:  701, y: 0, x2: 701, y2: 720 }
    
      args.outputs[:scene].sprites << { x: 640 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square/blue.png", a: 128, blendmode_enum: 0 }
      args.outputs[:scene].labels  << { x:  640, y:   0, text: "(bottom)",      alignment_enum: 1, vertical_alignment_enum: 0, blendmode_enum: 0 }
      args.outputs[:scene].labels  << { x:  640, y: 425, text: "render target", alignment_enum: 1, vertical_alignment_enum: 1, blendmode_enum: 0 }
      args.outputs[:scene].labels  << { x:  640, y: 720, text: "(top)",         alignment_enum: 1, vertical_alignment_enum: 2, blendmode_enum: 0 }
      args.outputs[:scene].labels  << { x:    0, y: 360, text: "(left)",        alignment_enum: 0, vertical_alignment_enum: 1, blendmode_enum: 0 }
      args.outputs[:scene].labels  << { x: 1280, y: 360, text: "(right)",       alignment_enum: 2, vertical_alignment_enum: 1, blendmode_enum: 0 }
    
      args.outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: :scene }
    end
    
    

    Texture Atlases - main.rb link

    # ./samples/07_advanced_rendering_hd/02_texture_atlases/app/main.rb
    # With HD mode enabled. DragonRuby will automatically use HD sprites given the following
    # naming convention (assume we are using a sprite called =player.png=):
    #
    # | Name  | Resolution | File Naming Convention        |
    # |-------+------------+-------------------------------|
    # | 720p  |   1280x720 | =player.png=                  |
    # | HD+   |   1600x900 | [email protected]=              |
    # | 1080p |  1920x1080 | [email protected]=              |
    # | 1440p |  2560x1440 | [email protected]=              |
    # | 1800p |  3200x1800 | [email protected]=              |
    # | 4k    |  3200x2160 | [email protected]=              |
    # | 5k    |  6400x2880 | [email protected]=              |
    
    # Note: Review the sample app's game_metadata.txt file for what configurations are enabled.
    
    def tick args
      args.outputs.background_color = [0, 0, 0]
      args.outputs.borders << { x: 0, y: 0, w: 1280, h: 720, r: 255, g: 255, b: 255 }
    
      args.outputs.labels << { x: 30, y: 30.from_top, text: "render scale: #{args.grid.native_scale}", r: 255, g: 255, b: 255 }
      args.outputs.labels << { x: 30, y: 60.from_top, text: "render scale: #{args.grid.texture_scale_enum}", r: 255, g: 255, b: 255 }
    
      args.outputs.sprites << { x: -640 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x: -320 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square.png" }
    
      args.outputs.sprites << { x:    0 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x:  320 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x:  640 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x:  960 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x: 1280 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square.png" }
    
      args.outputs.sprites << { x: 1600 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x: 1920 - 50, y: 360 - 50, w: 100, h: 100, path: "sprites/square.png" }
    
      args.outputs.sprites << { x:  640 - 50, y:          720, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x:  640 - 50, y: 100.from_top, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x:  640 - 50, y:     360 - 50, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x:  640 - 50, y:            0, w: 100, h: 100, path: "sprites/square.png" }
      args.outputs.sprites << { x:  640 - 50, y:         -100, w: 100, h: 100, path: "sprites/square.png" }
    
      if args.inputs.keyboard.key_down.right_arrow
        GTK.set_window_scale 1, 9, 16
      elsif args.inputs.keyboard.key_down.left_arrow
        GTK.set_window_scale 1, 32, 9
      elsif args.inputs.keyboard.key_down.up_arrow
        GTK.toggle_window_fullscreen
      end
    end
    
    

    Allscreen Properties - main.rb link

    # ./samples/07_advanced_rendering_hd/03_allscreen_properties/app/main.rb
    def tick args
      label_style = { r: 255, g: 255, b: 255, size_enum: 4 }
      args.outputs.background_color = [0, 0, 0]
      args.outputs.borders << { x: 0, y: 0, w: 1280, h: 720, r: 255, g: 255, b: 255 }
    
      args.outputs.labels << { x: 10, y:  10.from_top, text: "texture_scale:       #{args.grid.texture_scale}", **label_style }
      args.outputs.labels << { x: 10, y:  40.from_top, text: "texture_scale_enum:  #{args.grid.texture_scale_enum}",  **label_style }
      args.outputs.labels << { x: 10, y:  70.from_top, text: "allscreen_offset_x:  #{args.grid.allscreen_offset_x}", **label_style }
      args.outputs.labels << { x: 10, y: 100.from_top, text: "allscreen_offset_y:  #{args.grid.allscreen_offset_y}", **label_style }
    
      if (Kernel.tick_count % 500) < 250
        args.outputs.labels << { x: 10, y: 130.from_top, text: "cropped to:          grid", **label_style }
    
        args.outputs.sprites << { x:        args.grid.left,
                                  y:        args.grid.bottom,
                                  w:        args.grid.w,
                                  h:        args.grid.h,
                                  # world.png has a 720p baseline size of 2000x2000 pixels
                                  # we want to crop the center of the sprite
                                  # wrt the bounds of the safe area.
                                  source_x: 2000 - args.grid.w / 2,
                                  source_y: 2000 - args.grid.h / 2,
                                  source_w: 1280,
                                  source_h: 720,
                                  path: "sprites/world.png" } # world.png has a 720p baseline size of 2000x2000 pixels
      else
        args.outputs.labels << { x: 10, y: 130.from_top, text: "cropped to:          allscreen", **label_style }
    
        args.outputs.sprites << { x:        args.grid.allscreen_left,
                                  y:        args.grid.allscreen_bottom,
                                  w:        args.grid.allscreen_w,
                                  h:        args.grid.allscreen_h,
                                  # world.png has a 720p baseline size of 2000x2000 pixels
                                  # we want to crop the center of the sprite to the bounds
                                  # wrt to the bounds of the entire renderable area.
                                  source_x: 2000 - args.grid.allscreen_w / 2,
                                  source_y: 2000 - args.grid.allscreen_h / 2,
                                  source_w: args.grid.allscreen_w,
                                  source_h: args.grid.allscreen_h,
                                  path:     "sprites/world.png" }
      end
    
      args.outputs.sprites << { x: 0, y: 0.from_top - 165, w: 410, h: 165, r: 0, g: 0, b: 0, a: 200, path: :pixel }
    
      if args.inputs.keyboard.key_down.right_arrow
        GTK.set_window_scale 1, 9, 16
      elsif args.inputs.keyboard.key_down.left_arrow
        GTK.set_window_scale 1, 32, 9
      elsif args.inputs.keyboard.key_down.up_arrow
        GTK.toggle_window_fullscreen
      end
    end
    
    

    Layouts And Portrait Mode - main.rb link

    # ./samples/07_advanced_rendering_hd/04_layouts_and_portrait_mode/app/main.rb
    def tick args
      args.outputs.solids << Layout.rect(row: 0, col: 0, w: 12, h: 24, include_row_gutter: true, include_col_gutter: true).merge(b: 255, a: 80)
    
      # rows (light blue)
      light_blue = { r: 128, g: 255, b: 255 }
      args.outputs.labels << Layout.rect(row: 1, col: 3).merge(text: "row examples", vertical_alignment_enum: 1, alignment_enum: 1)
      4.map_with_index do |row|
        args.outputs.solids << Layout.rect(row: row, col: 0, w: 1, h: 1).merge(**light_blue)
      end
    
      2.map_with_index do |row|
        args.outputs.solids << Layout.rect(row: row * 2, col: 1, w: 1, h: 2).merge(**light_blue)
      end
    
      4.map_with_index do |row|
        args.outputs.solids << Layout.rect(row: row, col: 2, w: 2, h: 1).merge(**light_blue)
      end
    
      2.map_with_index do |row|
        args.outputs.solids << Layout.rect(row: row * 2, col: 4, w: 2, h: 2).merge(**light_blue)
      end
    
      # columns (yellow)
      yellow = { r: 255, g: 255, b: 128 }
      args.outputs.labels << Layout.rect(row: 1, col: 9).merge(text: "column examples", vertical_alignment_enum: 1, alignment_enum: 1)
      6.times do |col|
        args.outputs.solids << Layout.rect(row: 0, col: 6 + col, w: 1, h: 1).merge(**yellow)
      end
    
      3.times do |col|
        args.outputs.solids << Layout.rect(row: 1, col: 6 + col * 2, w: 2, h: 1).merge(**yellow)
      end
    
      6.times do |col|
        args.outputs.solids << Layout.rect(row: 2, col: 6 + col, w: 1, h: 2).merge(**yellow)
      end
    
      # max width/height baseline (transparent green)
      green = { r: 0, g: 128, b: 80 }
      args.outputs.labels << Layout.rect(row: 4, col: 6).merge(text: "max width/height examples", vertical_alignment_enum: 1, alignment_enum: 1)
      args.outputs.solids << Layout.rect(row: 4, col: 0, w: 12, h: 2).merge(a: 64, **green)
    
      # max height
      args.outputs.solids << Layout.rect(row: 4, col: 0, w: 12, h: 2, max_height: 1).merge(a: 64, **green)
    
      # max width
      args.outputs.solids << Layout.rect(row: 4, col: 0, w: 12, h: 2, max_width: 6).merge(a: 64, **green)
    
      # labels relative to rects
      label_color = { r: 0, g: 0, b: 0 }
      white = { r: 232, g: 232, b: 232 }
    
      # labels realtive to point, achored at 0.0, 0.0
      args.outputs.labels << Layout.rect(row: 5.5, col: 6).merge(text: "labels using Layout.point anchored to 0.0, 0.0", vertical_alignment_enum: 1, alignment_enum: 1)
      grey = { r: 128, g: 128, b: 128 }
      args.outputs.solids << Layout.rect(row: 7, col: 4).merge(**grey)
      args.outputs.labels << Layout.point(row: 7, col: 4, row_anchor: 1.0, col_anchor: 0.0).merge(text: "[x]", alignment_enum: 1, vertical_alignment_enum: 1, **label_color)
    
      args.outputs.solids << Layout.rect(row: 7, col: 5).merge(**grey)
      args.outputs.labels << Layout.point(row: 7, col: 5, row_anchor: 1.0, col_anchor: 0.5).merge(text: "[x]", alignment_enum: 1, vertical_alignment_enum: 1, **label_color)
    
      args.outputs.solids << Layout.rect(row: 7, col: 6).merge(**grey)
      args.outputs.labels << Layout.point(row: 7, col: 6, row_anchor: 1.0, col_anchor: 1.0).merge(text: "[x]", alignment_enum: 1, vertical_alignment_enum: 1, **label_color)
    
      args.outputs.solids << Layout.rect(row: 8, col: 4).merge(**grey)
      args.outputs.labels << Layout.point(row: 8, col: 4, row_anchor: 0.5, col_anchor: 0.0).merge(text: "[x]", alignment_enum: 1, vertical_alignment_enum: 1, **label_color)
    
      args.outputs.solids << Layout.rect(row: 8, col: 5).merge(**grey)
      args.outputs.labels << Layout.point(row: 8, col: 5, row_anchor: 0.5, col_anchor: 0.5).merge(text: "[x]", alignment_enum: 1, vertical_alignment_enum: 1, **label_color)
    
      args.outputs.solids << Layout.rect(row: 8, col: 6).merge(**grey)
      args.outputs.labels << Layout.point(row: 8, col: 6, row_anchor: 0.5, col_anchor: 1.0).merge(text: "[x]", alignment_enum: 1, vertical_alignment_enum: 1, **label_color)
    
      args.outputs.solids << Layout.rect(row: 9, col: 4).merge(**grey)
      args.outputs.labels << Layout.point(row: 9, col: 4, row_anchor: 0.0, col_anchor: 0.0).merge(text: "[x]", alignment_enum: 1, vertical_alignment_enum: 1, **label_color)
    
      args.outputs.solids << Layout.rect(row: 9, col: 5).merge(**grey)
      args.outputs.labels << Layout.point(row: 9, col: 5, row_anchor: 0.0, col_anchor: 0.5).merge(text: "[x]", alignment_enum: 1, vertical_alignment_enum: 1, **label_color)
    
      args.outputs.solids << Layout.rect(row: 9, col: 6).merge(**grey)
      args.outputs.labels << Layout.point(row: 9, col: 6, row_anchor: 0.0, col_anchor: 1.0).merge(text: "[x]", alignment_enum: 1, vertical_alignment_enum: 1, **label_color)
    
      # centering rects
      args.outputs.labels << Layout.rect(row: 10.5, col: 6).merge(text: "layout.rect centered inside another layout.rect", vertical_alignment_enum: 1, alignment_enum: 1)
      outer_rect = Layout.rect(row: 12, col: 4, w: 3, h: 3)
    
      # render outer rect
      args.outputs.solids << outer_rect.merge(**light_blue)
    
      # center a yellow rect with w and h of two
      args.outputs.solids << Layout.rect_center(
        Layout.rect(w: 1, h: 5), # inner rect
        outer_rect, # outer rect
      ).merge(**yellow)
    
      # center a black rect with w three h of one
      args.outputs.solids << Layout.rect_center(
        Layout.rect(w: 5, h: 1), # inner rect
        outer_rect, # outer rect
      )
    
      args.outputs.labels << Layout.rect(row: 16.5, col: 6).merge(text: "layout.rect_group usage", vertical_alignment_enum: 1, alignment_enum: 1)
    
      horizontal_markers = [
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 }
      ]
    
      args.outputs.solids << Layout.rect_group(row: 18,
                                                    dcol: 1,
                                                    w: 1,
                                                    h: 1,
                                                    group: horizontal_markers)
    
      vertical_markers = [
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 },
        { r: 0, g: 0, b: 0 }
      ]
    
      args.outputs.solids << Layout.rect_group(row: 18,
                                                    drow: 1,
                                                    w: 1,
                                                    h: 1,
                                                    group: vertical_markers)
    
      colors = [
        { r:   0, g:   0, b:   0 },
        { r:  50, g:  50, b:  50 },
        { r: 100, g: 100, b: 100 },
        { r: 150, g: 150, b: 150 },
        { r: 200, g: 200, b: 200 },
      ]
    
      args.outputs.solids << Layout.rect_group(row: 19,
                                                    col: 1,
                                                    dcol: 2,
                                                    w: 2,
                                                    h: 1,
                                                    group: colors)
    
      args.outputs.solids << Layout.rect_group(row: 19,
                                                    col: 1,
                                                    drow: 1,
                                                    w: 2,
                                                    h: 1,
                                                    group: colors)
    end
    
    GTK.reset
    
    

    Tweening Lerping Easing Functions link

    Easing Functions - main.rb link

    # ./samples/08_tweening_lerping_easing_functions/01_easing_functions/app/main.rb
    def tick args
      # STOP! Watch the following presentation first!!!!
      # Math for Game Programmers: Fast and Funky 1D Nonlinear Transformations
      # https://www.youtube.com/watch?v=mr5xkf6zSzk
    
      # You've watched the talk, yes? YES???
    
      # define starting and ending points of properties to animate
      args.state.target_x = 1180
      args.state.target_y = 620
      args.state.target_w = 100
      args.state.target_h = 100
      args.state.starting_x = 0
      args.state.starting_y = 0
      args.state.starting_w = 300
      args.state.starting_h = 300
    
      # define start time and duration of animation
      args.state.start_animate_at = 3.seconds # this is the same as writing 60 * 5 (or 300)
      args.state.duration = 2.seconds # this is the same as writing 60 * 2 (or 120)
    
      # define type of animations
      # Here are all the options you have for values you can put in the array:
      # :identity, :quad, :cube, :quart, :quint, :flip
    
      # Linear is defined as:
      # [:identity]
      #
      # Smooth start variations are:
      # [:quad]
      # [:cube]
      # [:quart]
      # [:quint]
    
      # Linear reversed, and smooth stop are the same as the animations defined above, but reversed:
      # [:flip, :identity, :flip]
      # [:flip, :quad, :flip]
      # [:flip, :cube, :flip]
      # [:flip, :quart, :flip]
      # [:flip, :quint, :flip]
    
      # You can also do custom definitions. See the bottom of the file details
      # on how to do that. I've defined a couple for you:
      # [:smoothest_start]
      # [:smoothest_stop]
    
      # CHANGE THIS LINE TO ONE OF THE LINES ABOVE TO SEE VARIATIONS
      args.state.animation_type = [:identity]
      # args.state.animation_type = [:quad]
      # args.state.animation_type = [:cube]
      # args.state.animation_type = [:quart]
      # args.state.animation_type = [:quint]
      # args.state.animation_type = [:flip, :identity, :flip]
      # args.state.animation_type = [:flip, :quad, :flip]
      # args.state.animation_type = [:flip, :cube, :flip]
      # args.state.animation_type = [:flip, :quart, :flip]
      # args.state.animation_type = [:flip, :quint, :flip]
      # args.state.animation_type = [:smoothest_start]
      # args.state.animation_type = [:smoothest_stop]
    
      # THIS IS WHERE THE MAGIC HAPPENS!
      # Numeric#ease
      progress = args.state.start_animate_at.ease(args.state.duration, args.state.animation_type)
    
      # Numeric#ease needs to called:
      # 1. On the number that represents the point in time you want to start, and takes two parameters:
      #   a. The first parameter is how long the animation should take.
      #   b. The second parameter represents the functions that need to be called.
      #
      # For example, if I wanted an animate to start 3 seconds in, and last for 10 seconds,
      # and I want to animation to start fast and end slow, I would do:
      # (60 * 3).ease(60 * 10, :flip, :quint, :flip)
    
      #        initial value           delta to the final value
      calc_x = args.state.starting_x + (args.state.target_x - args.state.starting_x) * progress
      calc_y = args.state.starting_y + (args.state.target_y - args.state.starting_y) * progress
      calc_w = args.state.starting_w + (args.state.target_w - args.state.starting_w) * progress
      calc_h = args.state.starting_h + (args.state.target_h - args.state.starting_h) * progress
    
      args.outputs.solids << [calc_x, calc_y, calc_w, calc_h, 0, 0, 0]
    
      # count down
      count_down = args.state.start_animate_at - Kernel.tick_count
      if count_down > 0
        args.outputs.labels << [640, 375, "Running: #{args.state.animation_type} in...", 3, 1]
        args.outputs.labels << [640, 345, "%.2f" % count_down.fdiv(60), 3, 1]
      elsif progress >= 1
        args.outputs.labels << [640, 360, "Click screen to reset.", 3, 1]
        if args.inputs.click
          GTK.reset
        end
      end
    end
    
    # GTK.reset
    
    # you can make own variations of animations using this
    module Easing
      # you have access to all the built in functions: identity, flip, quad, cube, quart, quint
      def self.smoothest_start x
        quad(quint(x))
      end
    
      def self.smoothest_stop x
        flip(quad(quint(flip(x))))
      end
    
      # this is the source for the existing easing functions
      def self.identity x
        x
      end
    
      def self.flip x
        1 - x
      end
    
      def self.quad x
        x * x
      end
    
      def self.cube x
        x * x * x
      end
    
      def self.quart x
        x * x * x * x * x
      end
    
      def self.quint x
        x * x * x * x * x * x
      end
    end
    
    

    Cubic Bezier - main.rb link

    # ./samples/08_tweening_lerping_easing_functions/02_cubic_bezier/app/main.rb
    def tick args
      args.outputs.background_color = [33, 33, 33]
      args.outputs.lines << bezier(100, 100,
                                   100, 620,
                                   1180, 620,
                                   1180, 100,
                                   0)
    
      args.outputs.lines << bezier(100, 100,
                                   100, 620,
                                   1180, 620,
                                   1180, 100,
                                   20)
    end
    
    
    def bezier x, y, x2, y2, x3, y3, x4, y4, step
      step ||= 0
      color = [200, 200, 200]
      points = points_for_bezier [x, y], [x2, y2], [x3, y3], [x4, y4], step
    
      points.each_cons(2).map do |p1, p2|
        [p1, p2, color]
      end
    end
    
    def points_for_bezier p1, p2, p3, p4, step
      points = []
      if step == 0
        [p1, p2, p3, p4]
      else
        t_step = 1.fdiv(step + 1)
        t = 0
        t += t_step
        points = []
        while t < 1
          points << [
            b_for_t(p1.x, p2.x, p3.x, p4.x, t),
            b_for_t(p1.y, p2.y, p3.y, p4.y, t),
          ]
          t += t_step
        end
    
        [
          p1,
          *points,
          p4
        ]
      end
    end
    
    def b_for_t v0, v1, v2, v3, t
      pow(1 - t, 3) * v0 +
      3 * pow(1 - t, 2) * t * v1 +
      3 * (1 - t) * pow(t, 2) * v2 +
      pow(t, 3) * v3
    end
    
    def pow n, to
      n ** to
    end
    
    

    Easing Using Spline - main.rb link

    # ./samples/08_tweening_lerping_easing_functions/03_easing_using_spline/app/main.rb
    def tick args
      args.state.duration = 10.seconds
      args.state.spline_definition = [
        [0.0, 0.33, 0.66, 1.0],
        [1.0, 1.0,  1.0,  1.0],
        [1.0, 0.66, 0.33, 0.0],
      ]
    
      args.state.simulation_tick = Kernel.tick_count % args.state.duration
      progress = Easing.spline 0, args.state.simulation_tick, args.state.duration, args.state.spline_definition
      args.outputs.borders << args.grid.rect
      args.outputs.solids << [20 + 1240 * progress,
                              20 +  680 * progress,
                              20, 20].anchor_rect(0.5, 0.5)
      args.outputs.labels << [10,
                              710,
                              "perc: #{"%.2f" % (args.state.simulation_tick / args.state.duration)} t: #{args.state.simulation_tick}"]
    end
    
    

    Pulsing Button - main.rb link

    # ./samples/08_tweening_lerping_easing_functions/04_pulsing_button/app/main.rb
    # game concept from: https://youtu.be/Tz-AinJGDIM
    
    # This class encapsulates the logic of a button that pulses when clicked.
    # It is used in the StartScene and GameOverScene classes.
    class PulseButton
      # a block is passed into the constructor and is called when the button is clicked,
      # and after the pulse animation is complete
      def initialize rect, text, &on_click
        @rect = rect
        @text = text
        @on_click = on_click
        @pulse_animation_spline = [[0.0, 0.90, 1.0, 1.0], [1.0, 0.10, 0.0, 0.0]]
        @duration = 10
      end
    
      # the button is ticked every frame and check to see if the mouse
      # intersects the button's bounding box.
      # if it does, then pertinent information is stored in the @clicked_at variable
      # which is used to calculate the pulse animation
      def tick tick_count, mouse
        @tick_count = tick_count
    
        if @clicked_at && @clicked_at.elapsed_time > @duration
          @clicked_at = nil
          @on_click.call
        end
    
        return if !mouse.click
        return if !mouse.inside_rect? @rect
        @clicked_at = tick_count
      end
    
      # this function returns an array of primitives that can be rendered
      def prefab easing
        # calculate the percentage of the pulse animation that has completed
        # and use the percentage to compute the size and position of the button
        perc = if @clicked_at
                 Easing.spline @clicked_at, @tick_count, @duration, @pulse_animation_spline
               else
                 0
               end
    
        rect = { x: @rect.x - 50 * perc / 2,
                 y: @rect.y - 50 * perc / 2,
                 w: @rect.w + 50 * perc,
                 h: @rect.h + 50 * perc }
    
        point = { x: @rect.x + @rect.w / 2, y: @rect.y + @rect.h / 2 }
        [
          { **rect, path: :pixel },
          { **point, text: @text, size_px: 32, anchor_x: 0.5, anchor_y: 0.5 }
        ]
      end
    end
    
    class Game
      attr_gtk
    
      def initialize args
        self.args = args
        @pulse_button ||= PulseButton.new({ x: 640 - 100, y: 360 - 50, w: 200, h: 100 }, 'Click Me!') do
          GTK.notify! "Animation complete and block invoked!"
        end
      end
    
      def tick
        @pulse_button.tick Kernel.tick_count, inputs.mouse
        outputs.primitives << @pulse_button.prefab(easing)
      end
    end
    
    def tick args
      $game ||= Game.new args
      $game.args = args
      $game.tick
    end
    
    

    Scene Transitions - main.rb link

    # ./samples/08_tweening_lerping_easing_functions/05_scene_transitions/app/main.rb
    # This sample app shows a more advanced implementation of scenes:
    # 1. "Scene 1" has a label on it that says "I am scene ONE. Press enter to go to scene TWO."
    # 2. "Scene 2" has a label on it that says "I am scene TWO. Press enter to go to scene ONE."
    # 3. When the game starts, Scene 1 is presented.
    # 4. When the player presses enter, the scene transitions to Scene 2 (fades out Scene 1 over half a second, then fades in Scene 2 over half a second).
    # 5. When the player presses enter again, the scene transitions to Scene 1 (fades out Scene 2 over half a second, then fades in Scene 1 over half a second).
    # 6. During the fade transitions, spamming the enter key is ignored (scenes don't accept a transition/respond to the enter key until the current transition is completed).
    class SceneOne
      attr_gtk
    
      def tick
        outputs[:scene].labels << { x: 640,
                                    y: 360,
                                    text: "I am scene ONE. Press enter to go to scene TWO.",
                                    alignment_enum: 1,
                                    vertical_alignment_enum: 1 }
    
        state.next_scene = :scene_two if inputs.keyboard.key_down.enter
      end
    end
    
    class SceneTwo
      attr_gtk
    
      def tick
        outputs[:scene].labels << { x: 640,
                                    y: 360,
                                    text: "I am scene TWO. Press enter to go to scene ONE.",
                                    alignment_enum: 1,
                                    vertical_alignment_enum: 1 }
    
        state.next_scene = :scene_one if inputs.keyboard.key_down.enter
      end
    end
    
    class RootScene
      attr_gtk
    
      def initialize
        @scene_one = SceneOne.new
        @scene_two = SceneTwo.new
      end
    
      def tick
        defaults
        render
        tick_scene
      end
    
      def defaults
        set_current_scene! :scene_one if Kernel.tick_count == 0
        state.scene_transition_duration ||= 30
      end
    
      def render
        a = if state.transition_scene_at
              255 * state.transition_scene_at.ease(state.scene_transition_duration, :flip)
            elsif state.current_scene_at
              255 * state.current_scene_at.ease(state.scene_transition_duration)
            else
              255
            end
    
        outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: :scene, a: a }
      end
    
      def tick_scene
        current_scene = state.current_scene
    
        @current_scene.args = args
        @current_scene.tick
    
        if current_scene != state.current_scene
          raise "state.current_scene changed mid tick from #{current_scene} to #{state.current_scene}. To change scenes, set state.next_scene."
        end
    
        if state.next_scene && state.next_scene != state.transition_scene && state.next_scene != state.current_scene
          state.transition_scene_at = Kernel.tick_count
          state.transition_scene = state.next_scene
        end
    
        if state.transition_scene_at && state.transition_scene_at.elapsed_time >= state.scene_transition_duration
          set_current_scene! state.transition_scene
        end
    
        state.next_scene = nil
      end
    
      def set_current_scene! id
        return if state.current_scene == id
        state.current_scene = id
        state.current_scene_at = Kernel.tick_count
        state.transition_scene = nil
        state.transition_scene_at = nil
    
        if state.current_scene == :scene_one
          @current_scene = @scene_one
        elsif state.current_scene == :scene_two
          @current_scene = @scene_two
        end
      end
    end
    
    def tick args
      $game ||= RootScene.new
      $game.args = args
      $game.tick
    end
    
    

    Animation Queues - main.rb link

    # ./samples/08_tweening_lerping_easing_functions/06_animation_queues/app/main.rb
    # here's how to create a "fire and forget" sprite animation queue
    def tick args
      args.outputs.labels << { x: 640,
                               y: 360,
                               text: "Click anywhere on the screen.",
                               alignment_enum: 1,
                               vertical_alignment_enum: 1 }
    
      # initialize the queue to an empty array
      args.state.fade_out_queue ||=[]
    
      # if the mouse is click, add a sprite to the fire and forget
      # queue to be processed
      if args.inputs.mouse.click
        args.state.fade_out_queue << {
          x: args.inputs.mouse.x - 20,
          y: args.inputs.mouse.y - 20,
          w: 40,
          h: 40,
          path: "sprites/square/blue.png"
        }
      end
    
      # process the queue
      args.state.fade_out_queue.each do |item|
        # default the alpha value if it isn't specified
        item.a ||= 255
    
        # decrement the alpha by 5 each frame
        item.a -= 5
      end
    
      # remove the item if it's completely faded out
      args.state.fade_out_queue.reject! { |item| item.a <= 0 }
    
      # render the sprites in the queue
      args.outputs.sprites << args.state.fade_out_queue
    end
    
    

    Animation Queues Advanced - main.rb link

    # ./samples/08_tweening_lerping_easing_functions/07_animation_queues_advanced/app/main.rb
    # sample app shows how to perform a fire and forget animation when a collision occurs
    def tick args
      defaults args
      spawn_bullets args
      calc_bullets args
      render args
    end
    
    def defaults args
      # place a player on the far left with sprite and hp information
      args.state.player ||= { x: 100, y: 360 - 50, w: 100, h: 100, path: "sprites/square/blue.png", hp: 30 }
      # create an array of bullets
      args.state.bullets ||= []
      # create a queue for handling bullet explosions
      args.state.explosion_queue ||= []
    end
    
    def spawn_bullets args
      # span a bullet in a random location on the far right every half second
      return if !Kernel.tick_count.zmod? 30
      args.state.bullets << {
        x: 1280 - 100,
        y: rand(720 - 100),
        w: 100,
        h: 100,
        path: "sprites/square/red.png"
      }
    end
    
    def calc_bullets args
      # for each bullet
      args.state.bullets.each do |b|
        # move it to the left by 20 pixels
        b.x -= 20
    
        # determine if the bullet collides with the player
        if b.intersect_rect? args.state.player
          # decrement the player's health if it does
          args.state.player.hp -= 1
          # mark the bullet as exploded
          b.exploded = true
    
          # queue the explosion by adding it to the explosion queue
          args.state.explosion_queue << b.merge(exploded_at: Kernel.tick_count)
        end
      end
    
      # remove bullets that have exploded so they wont be rendered
      args.state.bullets.reject! { |b| b.exploded }
    
      # remove animations from the animation queue that have completed
      # frame index will return nil once the animation has completed
      args.state.explosion_queue.reject! { |e| !e.exploded_at.frame_index(7, 4, false) }
    end
    
    def render args
      # render the player's hp above the sprite
      args.outputs.labels << {
        x: args.state.player.x + 50,
        y: args.state.player.y + 110,
        text: "#{args.state.player.hp}",
        alignment_enum: 1,
        vertical_alignment_enum: 0
      }
    
      # render the player
      args.outputs.sprites << args.state.player
    
      # render the bullets
      args.outputs.sprites << args.state.bullets
    
      # process the animation queue
      args.outputs.sprites << args.state.explosion_queue.map do |e|
        number_of_frames = 7
        hold_each_frame_for = 4
        repeat_animation = false
        # use the exploded_at property and the frame_index function to determine when the animation should start
        frame_index = e.exploded_at.frame_index(number_of_frames, hold_each_frame_for, repeat_animation)
        # take the explosion primitive and set the path variariable
        e.merge path: "sprites/misc/explosion-#{frame_index}.png"
      end
    end
    
    

    Cutscenes - main.rb link

    # ./samples/08_tweening_lerping_easing_functions/08_cutscenes/app/main.rb
    # sample app shows how you can user a queue/callback mechanism to create cutscenes
    class Game
      attr_gtk
    
      def initialize
        # this class controls the cutscene orchestration
        @tick_queue = TickQueue.new
      end
    
      def tick
        @tick_queue.args = args
        state.player ||= { x: 0, y: 0, w: 100, h: 100, path: :pixel, r: 0, g: 255, b: 0 }
        state.fade_to_black ||= 0
        state.back_and_forth_count ||= 0
    
        # if the mouse is clicked, start the cutscene
        if inputs.mouse.click && !state.cutscene_started
          start_cutscene
        end
    
        outputs.primitives << state.player
        outputs.primitives << { x: 0, y: 0, w: 1280, h: 720, path: :pixel, r: 0, g: 0, b: 0, a: state.fade_to_black }
        @tick_queue.tick
      end
    
      def start_cutscene
        # don't start the cutscene if it's already started
        return if state.cutscene_started
        state.cutscene_started = true
    
        # start the cutscene by moving right
        queue_move_to_right_side
      end
    
      def queue_move_to_right_side
        # use the tick queue mechanism to kick off the player moving right
        @tick_queue.queue_tick Kernel.tick_count do |args, entry|
          state.player.x += 30
          # once the player is done moving right, stage the next step of the cutscene (moving left)
          if state.player.x + state.player.w > 1280
            state.player.x = 1280 - state.player.w
            queue_move_to_left_side
    
            # marke the queued tick entry as complete so it doesn't get run again
            entry.complete!
          end
        end
      end
    
      def queue_move_to_left_side
        # use the tick queue mechanism to kick off the player moving right
        @tick_queue.queue_tick Kernel.tick_count do |args, entry|
          args.state.player.x -= 30
          # once the player id done moving left, decide on whether they should move right again or fade to black
          # the decision point is based on the number of times the player has moved left and right
          if args.state.player.x < 0
            state.player.x = 0
            args.state.back_and_forth_count += 1
            if args.state.back_and_forth_count < 3
              # if they haven't moved left and right 3 times, move them right again
              queue_move_to_right_side
            else
              # if they have moved left and right 3 times, fade to black
              queue_fade_to_black
            end
    
            # marke the queued tick entry as complete so it doesn't get run again
            entry.complete!
          end
        end
      end
    
      def queue_fade_to_black
        # we know the cutscene will end in 255 tickes, so we can queue a notification that will kick off in the future notifying that the cutscene is done
        @tick_queue.queue_one_time_tick Kernel.tick_count + 255 do |args, entry|
          GTK.notify "Cutscene complete!"
        end
    
        # start the fade to black
        @tick_queue.queue_tick Kernel.tick_count do |args, entry|
          args.state.fade_to_black += 1
          entry.complete! if state.fade_to_black > 255
        end
      end
    end
    
    # this construct handles the execution of animations/cutscenes
    # the key methods that are used are queue_tick and queue_one_time_tick
    class TickQueue
      attr_gtk
    
      attr :queued_ticks
      attr :queued_ticks_currently_running
    
      def initialize
        @queued_ticks ||= {}
        @queued_ticks_currently_running ||= []
      end
    
      # adds a callback that will be processed
      def queue_tick at, &block
        @queued_ticks[at] ||= []
        @queued_ticks[at] << QueuedTick.new(at, &block)
      end
    
      # adds a callback that will be processed and immediately marked as complete
      def queue_one_time_tick at, **metadata, &block
        @queued_ticks ||= {}
        @queued_ticks[at] ||= []
        @queued_ticks[at] << QueuedOneTimeTick.new(at, &block)
      end
    
      def tick
        # get all queued callbacs that need to start running on the current frame
        entries_this_tick = @queued_ticks.delete Kernel.tick_count
    
        # if there are values, then add them to the list of currently running callbacks
        if entries_this_tick
          @queued_ticks_currently_running.concat entries_this_tick
        end
    
        # run tick on each entry
        @queued_ticks_currently_running.each do |queued_tick|
          queued_tick.tick args
        end
    
        # remove all entries that are complete
        @queued_ticks_currently_running.reject!(&:complete?)
    
        # there is a chance that a queued tick will queue another tick, so we need to check
        # if there are any queued ticks for the current frame. if so, then recursively call tick again
        if @queued_ticks[Kernel.tick_count] && @queued_ticks[Kernel.tick_count].length > 0
          tick
        end
      end
    end
    
    # small data structure that holds the callback and status
    # queue_tick constructs an instance of this class to faciltate
    # the execution of the block and it's completion
    class QueuedTick
      attr :queued_at, :block
    
      def initialize queued_at, &block
        @queued_at = queued_at
        @is_complete = false
        @block = block
      end
    
      def complete!
        @is_complete = true
      end
    
      def complete?
        @is_complete
      end
    
      def tick args
        @block.call args, self
      end
    end
    
    # small data structure that holds the callback and status
    # queue_one_time_tick constructs an instance of this class to faciltate
    # the execution of the block and it's completion
    class QueuedOneTimeTick < QueuedTick
      def tick args
        @block.call args, self
        @is_complete = true
      end
    end
    
    
    $game = Game.new
    def tick args
      $game.args = args
      $game.tick
    end
    
    GTK.reset
    
    

    Tower Of Hanoi - main.rb link

    # ./samples/08_tweening_lerping_easing_functions/09_tower_of_hanoi/app/main.rb
    class Game
      attr_gtk
    
      # get solution for hanoi tower
      # https://youtu.be/rf6uf3jNjbo
      def solve count, from, to, other
        solve_recur(count, from, to, other).flatten
      end
    
      # recursive function for getting solution
      def solve_recur count, from, to, other
        if count == 1
          [{ from: from, to: to }]
        else
          [
            solve(count - 1, from, other, to),
            { from: from, to: to },
            solve(count - 1, other, to, from)
          ]
        end
      end
    
      def post_message message
        return if state.message_at && state.message == message && state.message_at.elapsed_time < 180
        state.message = message
        state.message_at = Kernel.tick_count
      end
    
      # initialize default values
      def defaults
        # number of discs for tower
        state.disc_count ||= 4
        # queue for peg selection (items in queue are processed after animations complete)
        state.select_peg_queue ||= []
    
        # precompute button locations based off of a 24x12 grid
        state.undo_button_rect ||= Layout.rect(row: 11, col: 8, w: 4, h: 1)
        state.auto_solve_button_rect ||= Layout.rect(row: 11, col: 12, w: 4, h: 1)
        state.select_peg_1_button_rect ||= Layout.rect(row: 10, col: 1.5, w: 5, h: 1)
        state.select_peg_2_button_rect ||= Layout.rect(row: 10, col: 9.5, w: 5, h: 1)
        state.select_peg_3_button_rect ||= Layout.rect(row: 10, col: 17.5, w: 5, h: 1)
    
        # default duration for disc animations
        state.animation_duration ||= 15
    
        # history of moves (used for undoing and resetting game)
        state.move_history ||= []
    
        if !state.tower
          # generate discs
          discs = state.disc_count.map do |i|
            { sz: i + 1 }
          end
    
          # create pegs
          state.tower = {
            pegs: [
              { index: 0, discs: discs.reverse },
              { index: 1, discs: [] },
              { index: 2, discs: [] },
            ]
          }
    
          # calculate peg render and click locations
          state.tower.pegs.each do |peg|
            x = Layout.rect(row: 0, col: peg.index * 8, w: 8, h: 1).center.x
            y, h = Layout.rect(row: 2, col: 0, w: 1, h: 8).slice(:y, :h).values
            peg.render_box = {
              x: x,
              y: y,
              w: 32,
              h: h,
              anchor_x: 0.5,
            }
    
            peg.hit_box = {
              x: x,
              y: y,
              w: 256,
              h: h,
              anchor_x: 0.5,
            }
          end
    
          # associate buttons to pegs
          state.tower.pegs[0].button_rect = state.select_peg_1_button_rect
          state.tower.pegs[1].button_rect = state.select_peg_2_button_rect
          state.tower.pegs[2].button_rect = state.select_peg_3_button_rect
        end
    
        # compute hanoi solution
        state.solution ||= solve(state.disc_count, 0, 2, 1)
      end
    
      # queue peg selection
      def queue_select_peg(peg, add_history:, animation_duration:)
        state.select_peg_queue.push_back peg: peg,
                                         add_history: add_history,
                                         animation_duration: animation_duration
      end
    
      # select peg action
      def select_peg(peg, add_history:, animation_duration:)
        # return if peg is nil
        return if !peg
    
        if !state.from_peg && peg.discs.any?
          # if from_peg is not set and the peg that is being selected has discs
          # set the from_peg
          state.from_peg = peg
          # generate a disc event (used for animations)
          state.disc_event = {
            type: :take,
            from_peg: peg,
            to_peg: peg,
            at: Kernel.tick_count,
            disc: peg.discs.last,
            duration: animation_duration
          }
    
          # reset the destination peg
          state.to_peg = nil
    
          # record move history if option is true
          # (when undoing moves, we don't want to record history)
          state.move_history << peg.index if add_history
        elsif state.from_peg == peg
          # if the destination peg is the same as the start peg
          # create an animation event that is half way done so
          # that only the drop disc part of the animation is performed
          state.to_peg = peg
          state.disc_event = {
            type: :drop,
            from_peg: peg,
            to_peg: peg,
            disc: state.from_peg.discs.last,
            at: Kernel.tick_count - animation_duration,
            duration: animation_duration * 2
          }
          # set from peg to nil
          state.from_peg = nil
          # record move history
          state.move_history << peg.index if add_history
        elsif state.from_peg
          # if the start and destination pegs are different
          # check to see if the destination location is valid
          # (top disc must be larger than disc being placed)
          state.to_peg = peg
          disc = state.from_peg.discs.pop_back
          valid_move = !state.to_peg.discs.last || (state.to_peg.discs.last.sz > disc.sz)
    
          if valid_move
            # if it's valid, then pop the disc from the source
            # and place it at the destination
            state.to_peg.discs.push_back disc
            # create a drop event to animate disc
            state.disc_event = {
              type: :drop,
              from_peg: state.from_peg,
              to_peg: state.to_peg,
              disc: disc,
              at: Kernel.tick_count,
              duration: animation_duration * 2
            }
            # record move history
            state.move_history << peg.index if add_history
          else
            post_message "Invalid Move..."
            # if it's invalid, place the disc back onto its source peg
            state.from_peg.discs.push_back disc
            # create drop event to animate disc
            state.disc_event = {
              type: :drop,
              from_peg: state.from_peg,
              to_peg: state.from_peg,
              disc: disc,
              at: Kernel.tick_count,
              duration: animation_duration * 2
            }
    
            # remove the entry in history
            state.move_history.pop_back
          end
    
          # clear the origination peg
          state.from_peg = nil
        end
      end
    
      def calc_disc_positions
        # every frame, calculate the render location of discs
        state.tower.pegs.each do |peg|
          # for each peg
          peg.discs.each_with_index do |disc, i|
            # for each disc calculate the default x and y position for rendering
            default_x = peg.render_box.x
            default_y = peg.render_box.y + i * 32
            removed_from_peg_y = Layout.rect(row: 1, col: 0, w: 1, h: 1).center.y - 16
    
            if state.disc_event && state.disc_event.disc == disc && state.disc_event.type == :take
              # if there is a "take" disc event and the target is the disc currently being processed
              # compute the easing function and update x, y accordingly
              from_peg_x = state.disc_event.from_peg.render_box.x
              to_peg_x = state.disc_event.to_peg.render_box.x
    
              perc = Easing.smooth_start(start_at: state.disc_event.at,
                                         end_at: state.disc_event.at + state.disc_event.duration,
                                         tick_count: Kernel.tick_count,
                                         power: 2)
    
              x = from_peg_x.lerp(to_peg_x, perc)
              y = default_y.lerp(removed_from_peg_y, perc)
            elsif state.disc_event && state.disc_event.disc == disc && state.disc_event.type == :drop
              # if there is a "drop" disc event and the target is the disc currently being processed
              # compute the easing function and update x, y accordingly
              from_peg_x = state.disc_event.from_peg.render_box.x
              to_peg_x = state.disc_event.to_peg.render_box.x
    
              # first part of the animation is the movement to the new peg
              perc = Easing.smooth_start(start_at: state.disc_event.at,
                                         end_at: state.disc_event.at + state.disc_event.duration / 2,
                                         tick_count: Kernel.tick_count,
                                         power: 2)
    
              x = from_peg_x.lerp(to_peg_x, perc)
    
              # second part of the animation is the drop of the peg at the new location
              perc = Easing.smooth_start(start_at: state.disc_event.at + state.disc_event.duration / 2,
                                         end_at: state.disc_event.at + state.disc_event.duration,
                                         tick_count: Kernel.tick_count,
                                         power: 2)
    
              y = removed_from_peg_y.lerp(default_y, perc)
            else
              # if there is no disc event, then set the x and y value to the defaults
              # for the disc
              x = default_x
              y = default_y
            end
    
            # width of the disc is the width of the peg multiplied by its size
            w = peg.render_box.w + disc.sz * 32
    
            # set the disc's render box
            disc.render_box = {
              x: x,
              y: y,
              w: w,
              h: 32,
              anchor_x: 0.5
            }
          end
        end
      end
    
      def rollback_all_moves
        # based on the number of moves in the move history
        # slowly increase the animation speed during rollback
        move_count = state.move_history.length
        state.move_history.reverse.each_with_index do |entry, index|
          percentage_complete = (index + 1).fdiv move_count
          animation_duration = (state.animation_duration - state.animation_duration * percentage_complete).clamp(4, state.animation_duration)
          peg_index = state.move_history.pop_back
          peg = state.tower.pegs[peg_index]
          queue_select_peg peg, add_history: false, animation_duration: animation_duration.to_i
        end
      end
    
      def calc_auto_solve
        # return if already auto solving or if the game is completed
        return if state.auto_solving
        return if state.completed_at
    
        auto_solve_requested   = inputs.mouse.up && inputs.mouse.intersect_rect?(state.auto_solve_button_rect)
        auto_solve_requested ||= inputs.keyboard.key_down.space
    
        # if space is pressed, do an auto solve of the game
        if auto_solve_requested
          post_message "Auto Solving..."
          state.auto_solving = true
          # rollback all moves before starting the auto solve
          rollback_all_moves
          # based on the number of moves to complete the tower
          # slowly increase the animation speed
          move_count = 2**state.disc_count - 1
          state.solution.each_with_index do |move, index|
            percentage_complete = (index + 1).fdiv move_count
            animation_duration = (state.animation_duration - state.animation_duration * percentage_complete).clamp(4, state.animation_duration)
            queue_select_peg state.tower.pegs[move[:from]], add_history: true, animation_duration: animation_duration.to_i
            queue_select_peg state.tower.pegs[move[:to]], add_history: true, animation_duration: animation_duration.to_i
          end
        end
      end
    
      def calc_game_ended
        # game is completed if all discs are on the last peg
        all_discs_on_last_peg = state.tower.pegs[0].discs.length == 0 && state.tower.pegs[1].discs.length == 0
        if all_discs_on_last_peg
          state.completed_at ||= Kernel.tick_count
          state.started_at = nil
        end
    
        if state.completed_at == Kernel.tick_count
          post_message "Complete..."
        end
    
        # if the game is completed roll back everything so they can play again
        if state.completed_at && state.completed_at.elapsed_time > 60
          rollback_all_moves
        end
    
        # game is at the start if all discs are on the first peg
        all_discs_on_first_peg = state.tower.pegs[1].discs.length == 0 && state.tower.pegs[2].discs.length == 0
        if all_discs_on_first_peg
          state.completed_at = nil
          state.started_at ||= Kernel.tick_count
        end
    
        if state.started_at == Kernel.tick_count
          post_message "Ready..."
        end
    
        # if the game is at the start and there are no moves in
        # the move history or in the select peg queue,
        # then set auto solving to false
        if all_discs_on_first_peg && state.move_history.length == 0 && state.select_peg_queue.length == 0
          state.auto_solving = false
        end
      end
    
      def calc_input
        return if state.auto_solving
        return if state.completed_at
    
        # process user input either mouse or keyboard
        state.hovered_peg = state.tower.pegs.find { |peg| inputs.mouse.intersect_rect?(peg.hit_box) || inputs.mouse.intersect_rect?(peg.button_rect) }
    
        undo_requested   = inputs.mouse.up && inputs.mouse.intersect_rect?(state.undo_button_rect)
        undo_requested ||= inputs.keyboard.key_down.u
        undo_requested   = false if state.move_history.length == 0
    
        # keyboard j, k, l to select pegs, u to undo
        if inputs.keyboard.key_down.j
          queue_select_peg state.tower.pegs[0], add_history: true, animation_duration: state.animation_duration
        elsif inputs.keyboard.key_down.k
          queue_select_peg state.tower.pegs[1], add_history: true, animation_duration: state.animation_duration
        elsif inputs.keyboard.key_down.l
          queue_select_peg state.tower.pegs[2], add_history: true, animation_duration: state.animation_duration
        elsif undo_requested
          post_message "Undo..."
          if state.move_history.length.even?
            peg_index = state.move_history.pop_back
            peg = state.tower.pegs[peg_index]
            queue_select_peg peg, add_history: false, animation_duration: state.animation_duration
    
            peg_index = state.move_history.pop_back
            peg = state.tower.pegs[peg_index]
            queue_select_peg peg, add_history: false, animation_duration: state.animation_duration
          else
            peg_index = state.move_history.pop_back
            peg = state.tower.pegs[peg_index]
            queue_select_peg peg, add_history: false, animation_duration: state.animation_duration
          end
        end
    
        # peg selection using mouse
        if state.hovered_peg && inputs.mouse.up
          queue_select_peg state.hovered_peg, add_history: true, animation_duration: state.animation_duration
        end
      end
    
      def calc_peg_queue
        # don't process selection queue if there are animation events pending
        disc_event_elapsed = if !state.disc_event
                               true
                             else
                               state.disc_event.at.elapsed_time > state.disc_event.duration
                             end
    
    
        # if there are no animation events then process the first item from the queue
        if disc_event_elapsed && state.select_peg_queue.length > 0
          entry = state.select_peg_queue.pop_front
          select_peg entry.peg, add_history: entry.add_history, animation_duration: entry.animation_duration
        end
      end
    
      def calc
        calc_disc_positions
        calc_auto_solve
        calc_game_ended
        calc_input
        calc_peg_queue
      end
    
      def render
        # render background
        outputs.background_color = [30, 30, 30]
    
        # render message
        if state.message && state.message_at
          duration = 180
          # spline represents an easing function for fading in and out
          # of the message
          spline_definition = [
            [0.00, 0.00, 0.66, 1.00],
            [1.00, 1.00, 1.00, 1.00],
            [1.00, 0.66, 0.00, 0.00]
          ]
    
          perc = Easing.spline state.message_at,
                               Kernel.tick_count,
                               duration,
                               spline_definition
    
          outputs.primitives << Layout.rect(row: 0, col: 0, w: 24, h: 1)
                                      .center
                                      .merge(text: state.message,
                                             anchor_x: 0.5,
                                             anchor_y: 0.5,
                                             r: 255, g: 255, b: 255,
                                             anchor_x: 0.5,
                                             anchor_y: 0.5,
                                             size_px: 32,
                                             a: 255 * perc)
        end
    
        # render pegs
        outputs.primitives << state.tower.pegs.map do |peg|
          peg.render_box.merge(path: :solid, r: 128, g: 128, b: 128)
        end
    
        # render visual indicators for currently hovered peg
        if state.hovered_peg && inputs.last_active == :mouse
          outputs.primitives << state.hovered_peg.render_box.merge(path: :solid, r: 80, g: 128, b: 80)
        end
    
        # render visual indicator for selected peg
        if state.from_peg
          outputs.primitives << state.from_peg.render_box.merge(path: :solid, r: 80, g: 80, b: 128)
        end
    
        # render visual indicator for destination peg
        if state.to_peg
          outputs.primitives << state.to_peg.render_box.merge(path: :solid, r: 0, g: 80, b: 80)
        end
    
        # render disks
        outputs.primitives << state.tower.pegs.map do |peg|
          peg.discs.map do |disc|
            disc.render_box.merge(path: :solid, r: 200, g: 200, b: 200).scale_rect(0.95)
          end
        end
    
        # render platform/intput specific controls
        if inputs.last_active == :keyboard
          outputs.primitives << button_prefab(state.select_peg_1_button_rect, "J: Select Peg 1")
          outputs.primitives << button_prefab(state.select_peg_2_button_rect, "K: Select Peg 2")
          outputs.primitives << button_prefab(state.select_peg_3_button_rect, "L: Select Peg 3")
          outputs.primitives << button_prefab(state.undo_button_rect, "U: Undo")
          outputs.primitives << button_prefab(state.auto_solve_button_rect, "Space: Auto Solve")
        else
          action_text = if GTK.platform?(:touch)
                          "Tap"
                        else
                          "Click"
                        end
    
          outputs.primitives << button_prefab(state.select_peg_1_button_rect, "#{action_text}: Select Peg 1")
          outputs.primitives << button_prefab(state.select_peg_2_button_rect, "#{action_text}: Select Peg 2")
          outputs.primitives << button_prefab(state.select_peg_3_button_rect, "#{action_text}: Select Peg 3")
          outputs.primitives << button_prefab(state.undo_button_rect, "Undo")
          outputs.primitives << button_prefab(state.auto_solve_button_rect, "Auto Solve")
        end
      end
    
      def button_prefab rect, text
        color = if inputs.mouse.intersect_rect?(rect)
                  { r: 255, g: 255, b: 255 }
                else
                  { r: 128, g: 128, b: 128 }
                end
        [
          rect.merge(primitive_marker: :border, **color),
          rect.center.merge(text: text, r: 255, g: 255, b: 255, anchor_x: 0.5, anchor_y: 0.5)
        ]
      end
    
      def tick
        # execution pipeline
        # initialize game defaults, calculate game, render game
        defaults
        calc
        render
      end
    end
    
    def boot args
      args.state = { }
    end
    
    def tick args
      # entry point
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    def reset args
      $game = nil
    end
    
    GTK.reset
    
    

    Performance link

    Sprites As Hash - main.rb link

    # ./samples/09_performance/01_sprites_as_hash/app/main.rb
    
    # Sprites represented as Hashes using the queue ~args.outputs.sprites~
    # code up, but are the "slowest" to render.
    # The reason for this is the access of the key in the Hash and also
    # because the data args.outputs.sprites is cleared every tick.
    def random_x args
      (args.grid.w.randomize :ratio) * -1
    end
    
    def random_y args
      (args.grid.h.randomize :ratio) * -1
    end
    
    def random_speed
      1 + (4.randomize :ratio)
    end
    
    def new_star args
      {
        x: (random_x args),
        y: (random_y args),
        w: 4, h: 4, path: 'sprites/tiny-star.png',
        s: random_speed
      }
    end
    
    def move_star args, star
      star.x += star[:s]
      star.y += star[:s]
      if star.x > args.grid.w || star.y > args.grid.h
        star.x = (random_x args)
        star.y = (random_y args)
        star[:s] = random_speed
      end
    end
    
    def tick args
      args.state.star_count ||= 0
    
      # sets console command when sample app initially opens
      if Kernel.global_tick_count == 0
        puts ""
        puts ""
        puts "========================================================="
        puts "* INFO: Sprites, Hashes"
        puts "* INFO: Please specify the number of sprites to render."
        GTK.console.set_command "reset_with count: 100"
      end
    
      if args.inputs.keyboard.key_down.space
        reset_with count: 5000
      end
    
      # init
      if Kernel.tick_count == 0
        args.state.stars = args.state.star_count.map { |i| new_star args }
      end
    
      # update
      args.state.stars.each { |s| move_star args, s }
    
      # render
      args.outputs.sprites << args.state.stars
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    # resets game, and assigns star count given by user
    def reset_with count: count
      GTK.reset
      GTK.args.state.star_count = count
    end
    
    

    Sprites As Entities - main.rb link

    # ./samples/09_performance/02_sprites_as_entities/app/main.rb
    # Sprites represented as Entities using the queue ~args.outputs.sprites~
    # yields nicer access apis over Hashes, but require a bit more code upfront.
    # The hash sample has to use star[:s] to get the speed of the star, but
    # an entity can use .s instead.
    def random_x args
      (args.grid.w.randomize :ratio) * -1
    end
    
    def random_y args
      (args.grid.h.randomize :ratio) * -1
    end
    
    def random_speed
      1 + (4.randomize :ratio)
    end
    
    def new_star args
      args.state.new_entity :star, {
        x: (random_x args),
        y: (random_y args),
        w: 4, h: 4,
        path: 'sprites/tiny-star.png',
        s: random_speed
      }
    end
    
    def move_star args, star
      star.x += star.s
      star.y += star.s
      if star.x > args.grid.w || star.y > args.grid.h
        star.x = (random_x args)
        star.y = (random_y args)
        star.s = random_speed
      end
    end
    
    def tick args
      args.state.star_count ||= 0
    
      # sets console command when sample app initially opens
      if Kernel.global_tick_count == 0
        puts ""
        puts ""
        puts "========================================================="
        puts "* INFO: Sprites, Open Entities"
        puts "* INFO: Please specify the number of sprites to render."
        GTK.console.set_command "reset_with count: 100"
      end
    
      if args.inputs.keyboard.key_down.space
        reset_with count: 5000
      end
    
      # init
      if Kernel.tick_count == 0
        args.state.stars = args.state.star_count.map { |i| new_star args }
      end
    
      # update
      args.state.stars.each { |s| move_star args, s }
    
      # render
      args.outputs.sprites << args.state.stars
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    # resets game, and assigns star count given by user
    def reset_with count: count
      GTK.reset
      GTK.args.state.star_count = count
    end
    
    

    Sprites As Strict Entities - main.rb link

    # ./samples/09_performance/04_sprites_as_strict_entities/app/main.rb
    # Sprites represented as StrictEntities using the queue ~args.outputs.sprites~
    # yields apis access similar to Entities, but all properties that can be set on the
    # entity must be predefined with a default value. Strict entities do not support the
    # addition of new properties after the fact. They are more performant than OpenEntities
    # because of this constraint.
    def random_x args
      (args.grid.w.randomize :ratio) * -1
    end
    
    def random_y args
      (args.grid.h.randomize :ratio) * -1
    end
    
    def random_speed
      1 + (4.randomize :ratio)
    end
    
    def new_star args
      args.state.new_entity_strict(:star,
                                   x: (random_x args),
                                   y: (random_y args),
                                   w: 4, h: 4,
                                   path: 'sprites/tiny-star.png',
                                   s: random_speed) do |entity|
        # invoke attr_sprite so that it responds to
        # all properties that are required to render a sprite
        entity.attr_sprite
      end
    end
    
    def move_star args, star
      star.x += star.s
      star.y += star.s
      if star.x > args.grid.w || star.y > args.grid.h
        star.x = (random_x args)
        star.y = (random_y args)
        star.s = random_speed
      end
    end
    
    def tick args
      args.state.star_count ||= 0
    
      # sets console command when sample app initially opens
      if Kernel.global_tick_count == 0
        puts ""
        puts ""
        puts "========================================================="
        puts "* INFO: Sprites, Strict Entities"
        puts "* INFO: Please specify the number of sprites to render."
        GTK.console.set_command "reset_with count: 100"
      end
    
      if args.inputs.keyboard.key_down.space
        reset_with count: 5000
      end
    
      # init
      if Kernel.tick_count == 0
        args.state.stars = args.state.star_count.map { |i| new_star args }
      end
    
      # update
      args.state.stars.each { |s| move_star args, s }
    
      # render
      args.outputs.sprites << args.state.stars
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    # resets game, and assigns star count given by user
    def reset_with count: count
      GTK.reset
      GTK.args.state.star_count = count
    end
    
    

    Sprites As Classes - main.rb link

    # ./samples/09_performance/05_sprites_as_classes/app/main.rb
    # Sprites represented as Classes using the queue ~args.outputs.sprites~.
    # gives you full control of property declaration and method invocation.
    # They are more performant than OpenEntities and StrictEntities, but more code upfront.
    class Star
      attr_sprite
    
      def initialize grid
        @grid = grid
        @x = (rand @grid.w) * -1
        @y = (rand @grid.h) * -1
        @w    = 4
        @h    = 4
        @s    = 1 + (4.randomize :ratio)
        @path = 'sprites/tiny-star.png'
      end
    
      def move
        @x += @s
        @y += @s
        @x = (rand @grid.w) * -1 if @x > @grid.right
        @y = (rand @grid.h) * -1 if @y > @grid.top
      end
    end
    
    # calls methods needed for game to run properly
    def tick args
      # sets console command when sample app initially opens
      if Kernel.global_tick_count == 0
        puts ""
        puts ""
        puts "========================================================="
        puts "* INFO: Sprites, Classes"
        puts "* INFO: Please specify the number of sprites to render."
        GTK.console.set_command "reset_with count: 100"
      end
    
      args.state.star_count ||= 0
    
      # init
      if Kernel.tick_count == 0
        args.state.stars = args.state.star_count.map { |i| Star.new args.grid }
      end
    
      if args.inputs.keyboard.key_down.space
        reset_with count: 5000
      end
    
      # update
      args.state.stars.each(&:move)
    
      # render
      args.outputs.sprites << args.state.stars
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    # resets game, and assigns star count given by user
    def reset_with count: count
      GTK.reset
      GTK.args.state.star_count = count
    end
    
    

    Static Sprites As Classes - main.rb link

    # ./samples/09_performance/06_static_sprites_as_classes/app/main.rb
    # Sprites represented as Classes using the queue ~args.outputs.static_sprites~.
    # bypasses the queue behavior of ~args.outputs.sprites~. All instances are held
    # by reference. You get better performance, but you are mutating state of held objects
    # which is less functional/data oriented.
    class Star
      attr_sprite
    
      def initialize grid
        @grid = grid
        @x = (rand @grid.w) * -1
        @y = (rand @grid.h) * -1
        @w    = 4
        @h    = 4
        @s    = 1 + (4.randomize :ratio)
        @path = 'sprites/tiny-star.png'
      end
    
      def move
        @x += @s
        @y += @s
        @x = (rand @grid.w) * -1 if @x > @grid.right
        @y = (rand @grid.h) * -1 if @y > @grid.top
      end
    end
    
    # calls methods needed for game to run properly
    def tick args
      # sets console command when sample app initially opens
      if Kernel.global_tick_count == 0
        puts ""
        puts ""
        puts "========================================================="
        puts "* INFO: Static Sprites, Classes"
        puts "* INFO: Please specify the number of sprites to render."
        GTK.console.set_command "reset_with count: 100"
      end
    
      if args.inputs.keyboard.key_down.space
        reset_with count: 5000
      end
    
      args.state.star_count ||= 0
    
      # init
      if Kernel.tick_count == 0
        args.state.stars = args.state.star_count.map { |i| Star.new args.grid }
        args.outputs.static_sprites << args.state.stars
      end
    
      # update
      args.state.stars.each(&:move)
    
      # render
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    # resets game, and assigns star count given by user
    def reset_with count: count
      GTK.reset
      GTK.args.state.star_count = count
    end
    
    

    Static Sprites As Classes With Custom Drawing - main.rb link

    # ./samples/09_performance/07_static_sprites_as_classes_with_custom_drawing/app/main.rb
    # Sprites represented as Classes, with a draw_override method, and using the queue ~args.outputs.static_sprites~.
    # is the fastest approach. This is comparable to what other game engines set as the default behavior.
    # There are tradeoffs for all this speed if the creation of a full blown class, and bypassing
    # functional/data-oriented practices.
    class Star
      def initialize grid
        @grid = grid
        @x = (rand @grid.w) * -1
        @y = (rand @grid.h) * -1
        @w    = 4
        @h    = 4
        @s    = 1 + (4.randomize :ratio)
        @path = 'sprites/tiny-star.png'
      end
    
      def move
        @x += @s
        @y += @s
        @x = (rand @grid.w) * -1 if @x > @grid.right
        @y = (rand @grid.h) * -1 if @y > @grid.top
      end
    
      # if the object that is in args.outputs.sprites (or static_sprites)
      # respond_to? :draw_override, then the method is invoked giving you
      # access to the class used to draw to the canvas.
      def draw_override ffi_draw
        # first move then draw
        move
    
        # The argument order for ffi.draw_sprite is:
        # x, y, w, h, path
        ffi_draw.draw_sprite @x, @y, @w, @h, @path
    
        # The argument order for ffi_draw.draw_sprite_2 is (pass in nil for default value):
        # x, y, w, h, path,
        # angle, alpha
    
        # The argument order for ffi_draw.draw_sprite_3 is:
        # x, y, w, h,
        # path,
        # angle,
        # alpha, red_saturation, green_saturation, blue_saturation
        # tile_x, tile_y, tile_w, tile_h,
        # flip_horizontally, flip_vertically,
        # angle_anchor_x, angle_anchor_y,
        # source_x, source_y, source_w, source_h
    
        # The argument order for ffi_draw.draw_sprite_4 is:
        # x, y, w, h,
        # path,
        # angle,
        # alpha, red_saturation, green_saturation, blue_saturation
        # tile_x, tile_y, tile_w, tile_h,
        # flip_horizontally, flip_vertically,
        # angle_anchor_x, angle_anchor_y,
        # source_x, source_y, source_w, source_h,
        # blendmode_enum
    
        # The argument order for ffi_draw.draw_sprite_5 is:
        # x, y, w, h,
        # path,
        # angle,
        # alpha, red_saturation, green_saturation, blue_saturation
        # tile_x, tile_y, tile_w, tile_h,
        # flip_horizontally, flip_vertically,
        # angle_anchor_x, angle_anchor_y,
        # source_x, source_y, source_w, source_h,
        # blendmode_enum
        # anchor_x
        # anchor_y
    
        # The argument order for ffi_draw.draw_sprite_6 is:
        # x, y, w, h,
        # path,
        # angle,
        # alpha, red_saturation, green_saturation, blue_saturation
        # tile_x, tile_y, tile_w, tile_h,
        # flip_horizontally, flip_vertically,
        # angle_anchor_x, angle_anchor_y,
        # source_x, source_y, source_w, source_h,
        # blendmode_enum
        # anchor_x
        # anchor_y
        # scale_quality_enum
      end
    end
    
    # calls methods needed for game to run properly
    def tick args
      # sets console command when sample app initially opens
      if Kernel.global_tick_count == 0
        puts ""
        puts ""
        puts "========================================================="
        puts "* INFO: Static Sprites, Classes, Draw Override"
        puts "* INFO: Please specify the number of sprites to render."
        GTK.console.set_command "reset_with count: 100"
      end
    
      if args.inputs.keyboard.key_down.space
        reset_with count: 40000
      end
    
      args.state.star_count ||= 0
    
      # init
      if Kernel.tick_count == 0
        args.state.stars = args.state.star_count.map { |i| Star.new args.grid }
        args.outputs.static_sprites << args.state.stars
      end
    
      # render framerate
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    # resets game, and assigns star count given by user
    def reset_with count: count
      GTK.reset
      GTK.args.state.star_count = count
    end
    
    

    Collision Limits - main.rb link

    # ./samples/09_performance/08_collision_limits/app/main.rb
    =begin
    
     Reminders:
     - find_all: Finds all elements of a collection that meet certain requirements.
       In this sample app, we're finding all bodies that intersect with the center body.
    
     - args.outputs.solids: An array. The values generate a solid.
       The parameters are [X, Y, WIDTH, HEIGHT, RED, GREEN, BLUE]
       For more information about solids, go to mygame/documentation/03-solids-and-borders.md.
    
     - args.outputs.labels: An array. The values generate a label.
       The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE]
       For more information about labels, go to mygame/documentation/02-labels.md.
    
     - ARRAY#intersect_rect?: Returns true or false depending on if two rectangles intersect.
    
    =end
    
    # This code demonstrates moving objects that loop around once they exceed the scope of the screen,
    # which has dimensions of 1280 by 720, and also detects collisions between objects called "bodies".
    
    def body_count num
      GTK.args.state.other_bodies = num.map { [1280 * rand, 720 * rand, 10, 10] } # other_bodies set using num collection
    end
    
    def tick args
    
      # Center body's values are set using an array
      # Map is used to set values of 5000 other bodies
      # All bodies that intersect with center body are stored in collisions collection
      args.state.center_body  ||= { x: 640 - 100, y: 360 - 100, w: 200, h: 200 } # calculations done to place body in center
      args.state.other_bodies ||= 5000.map do
        { x: 1280 * rand,
          y: 720 * rand,
          w: 2,
          h: 2,
          path: :pixel,
          r: 0,
          g: 0,
          b: 0 }
      end # 2000 bodies given random position on screen
    
      # finds all bodies that intersect with center body, stores them in collisions
      collisions = args.state.other_bodies.find_all { |b| b.intersect_rect? args.state.center_body }
    
      args.borders << args.state.center_body # outputs center body as a black border
    
      # transparency changes based on number of collisions; the more collisions, the redder (more transparent) the box becomes
      args.sprites  << { x: args.state.center_body.x,
                         y: args.state.center_body.y,
                         w: args.state.center_body.w,
                         h: args.state.center_body.h,
                         path: :pixel,
                         a: collisions.length.idiv(2), # alpha value represents the number of collisions that occurred
                         r: 255,
                         g: 0,
                         b: 0 } # center body is red solid
      args.sprites  << args.state.other_bodies # other bodies are output as (black) solids, as well
    
      args.labels  << [10, 30, GTK.current_framerate.to_sf] # outputs frame rate in bottom left corner
    
      # Bodies are returned to bottom left corner if positions exceed scope of screen
      args.state.other_bodies.each do |b| # for each body in the other_bodies collection
        b.x += 5 # x and y are both incremented by 5
        b.y += 5
        b.x = 0 if b.x > 1280 # x becomes 0 if star exceeds scope of screen (goes too far right)
        b.y = 0 if b.y > 720 # y becomes 0 if star exceeds scope of screen (goes too far up)
      end
    end
    
    # Resets the game.
    GTK.reset
    
    

    Collision Limits Aabb - main.rb link

    # ./samples/09_performance/09_collision_limits_aabb/app/main.rb
    def tick args
      args.state.id_seed    ||= 1
      args.state.boxes      ||= []
      args.state.terrain    ||= [
        {
          x: 40, y: 0, w: 1200, h: 40, path: :pixel, r: 0, g: 0, b: 0
        },
        {
          x: 1240, y: 0, w: 40, h: 720, path: :pixel, r: 0, g: 0, b: 0
        },
        {
          x: 0, y: 0, w: 40, h: 720, path: :pixel, r: 0, g: 0, b: 0
        },
        {
          x: 40, y: 680, w: 1200, h: 40, path: :pixel, r: 0, g: 0, b: 0
        },
    
        {
          x: 760, y: 420, w: 180, h: 40, path: :pixel, r: 0, g: 0, b: 0
        },
        {
          x: 720, y: 420, w: 40, h: 100, path: :pixel, r: 0, g: 0, b: 0
        },
        {
          x: 940, y: 420, w: 40, h: 100, path: :pixel, r: 0, g: 0, b: 0
        },
    
        {
          x: 660, y: 220, w: 280, h: 40, path: :pixel, r: 0, g: 0, b: 0
        },
        {
          x: 620, y: 220, w: 40, h: 100, path: :pixel, r: 0, g: 0, b: 0
        },
        {
          x: 940, y: 220, w: 40, h: 100, path: :pixel, r: 0, g: 0, b: 0
        },
    
        {
          x: 460, y: 40, w: 280, h: 40, path: :pixel, r: 0, g: 0, b: 0
        },
        {
          x: 420, y: 40, w: 40, h: 100, path: :pixel, r: 0, g: 0, b: 0
        },
        {
          x: 740, y: 40, w: 40, h: 100, path: :pixel, r: 0, g: 0, b: 0
        },
      ]
    
      if args.inputs.keyboard.space
        args.state.boxes << {
          id: args.state.id_seed,
          x: 60,
          y: 60,
          w: 10,
          h: 10,
          dy: Numeric.rand(10..30),
          dx: Numeric.rand(10..30),
          path: :solid,
          r: Numeric.rand(200),
          g: Numeric.rand(200),
          b: Numeric.rand(200)
        }
    
        args.state.id_seed += 1
      end
    
      if args.inputs.keyboard.backspace
        args.state.boxes.pop_back
      end
    
      terrain = args.state.terrain
    
      args.state.boxes.each do |b|
        if b.still
          b.dy = Numeric.rand(20)
          b.dx = Numeric.rand(-20..20)
          b.still = false
          b.on_floor = false
        end
    
        if b.on_floor
          b.dx *= 0.9
        end
    
        b.x += b.dx
    
        collision_x = Geometry.find_intersect_rect(b, terrain)
    
        if collision_x
          if b.dx > 0
            b.x = collision_x.x - b.w
          elsif b.dx < 0
            b.x = collision_x.x + collision_x.w
          end
          b.dx *= -0.8
        end
    
        b.dy -= 0.25
        b.y += b.dy
    
        collision_y = Geometry.find_intersect_rect(b, terrain)
    
        if collision_y
          if b.dy > 0
            b.y = collision_y.y - b.h
          elsif b.dy < 0
            b.y = collision_y.y + collision_y.h
          end
    
          if b.dy < 0 && b.dy.abs < 1
            b.on_floor = true
          end
    
          b.dy *= -0.8
        end
    
        if b.on_floor && (b.dy.abs + b.dx.abs) < 0.1
          b.still = true
        end
      end
    
      args.outputs.labels << { x: 60, y: 60.from_top, text: "Hold SPACEBAR to add boxes. Hold BACKSPACE to remove boxes." }
      args.outputs.labels << { x: 60, y: 90.from_top, text: "FPS: #{GTK.current_framerate.to_sf}" }
      args.outputs.labels << { x: 60, y: 120.from_top, text: "Count: #{args.state.boxes.length}" }
      args.outputs.borders << args.state.terrain
      args.outputs.sprites << args.state.boxes
    end
    
    # GTK.reset
    
    

    Collision Limits Find Single - main.rb link

    # ./samples/09_performance/09_collision_limits_find_single/app/main.rb
    def tick args
      if args.state.should_reset_framerate_calculation
        GTK.reset_framerate_calculation
        args.state.should_reset_framerate_calculation = nil
      end
    
      if !args.state.rects
        args.state.rects = []
        add_10_000_random_rects args
      end
    
      args.state.player_rect ||= { x: 640 - 20, y: 360 - 20, w: 40, h: 40 }
      args.state.collision_type ||= :using_lambda
    
      if Kernel.tick_count == 0
        generate_scene args, args.state.quad_tree
      end
    
      # inputs
      # have a rectangle that can be moved around using arrow keys
      args.state.player_rect.x += args.inputs.left_right * 4
      args.state.player_rect.y += args.inputs.up_down * 4
    
      if args.inputs.mouse.click
        add_10_000_random_rects args
        args.state.should_reset_framerate_calculation = true
      end
    
      if args.inputs.keyboard.key_down.tab
        if args.state.collision_type == :using_lambda
          args.state.collision_type = :using_while_loop
        elsif args.state.collision_type == :using_while_loop
          args.state.collision_type = :using_find_intersect_rect
        elsif args.state.collision_type == :using_find_intersect_rect
          args.state.collision_type = :using_lambda
        end
        args.state.should_reset_framerate_calculation = true
      end
    
      # calc
      if args.state.collision_type == :using_lambda
        args.state.current_collision = args.state.rects.find { |r| r.intersect_rect? args.state.player_rect }
      elsif args.state.collision_type == :using_while_loop
        args.state.current_collision = nil
        idx = 0
        l = args.state.rects.length
        rects = args.state.rects
        player = args.state.player_rect
        while idx < l
          if rects[idx].intersect_rect? player
            args.state.current_collision = rects[idx]
            break
          end
          idx += 1
        end
      else
        args.state.current_collision = Geometry.find_intersect_rect args.state.player_rect, args.state.rects
      end
    
      # render
      render_instructions args
      args.outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: :scene }
    
      if args.state.current_collision
        args.outputs.sprites << args.state.current_collision.merge(path: :pixel, r: 255, g: 0, b: 0)
      end
    
      args.outputs.sprites << args.state.player_rect.merge(path: :pixel, a: 80, r: 0, g: 255, b: 0)
      args.outputs.labels  << {
        x: args.state.player_rect.x + args.state.player_rect.w / 2,
        y: args.state.player_rect.y + args.state.player_rect.h / 2,
        text: "player",
        alignment_enum: 1,
        vertical_alignment_enum: 1,
        size_enum: -4
      }
    
    end
    
    def add_10_000_random_rects args
      add_rects args, 10_000.map { { x: rand(1080) + 100, y: rand(520) + 100 } }
    end
    
    def add_rects args, points
      args.state.rects.concat(points.map { |point| { x: point.x, y: point.y, w: 5, h: 5 } })
      # args.state.quad_tree = Geometry.quad_tree_create args.state.rects
      generate_scene args, args.state.quad_tree
    end
    
    def add_rect args, x, y
      args.state.rects << { x: x, y: y, w: 5, h: 5 }
      # args.state.quad_tree = Geometry.quad_tree_create args.state.rects
      generate_scene args, args.state.quad_tree
    end
    
    def generate_scene args, quad_tree
      args.outputs[:scene].w = 1280
      args.outputs[:scene].h = 720
      args.outputs[:scene].solids << { x: 0, y: 0, w: 1280, h: 720, r: 255, g: 255, b: 255 }
      args.outputs[:scene].sprites << args.state.rects.map { |r| r.merge(path: :pixel, r: 0, g: 0, b: 255) }
    end
    
    def render_instructions args
      args.outputs.primitives << { x:  0, y: 90.from_top, w: 1280, h: 100, r: 0, g: 0, b: 0, a: 200 }.solid!
      args.outputs.labels << { x: 10, y: 10.from_top, r: 255, g: 255, b: 255, size_enum: -2, text: "Click to add 10,000 random rects. Tab to change collision algorithm." }
      args.outputs.labels << { x: 10, y: 40.from_top, r: 255, g: 255, b: 255, size_enum: -2, text: "Algorithm: #{args.state.collision_type}" }
      args.outputs.labels << { x: 10, y: 55.from_top, r: 255, g: 255, b: 255, size_enum: -2, text: "Rect Count: #{args.state.rects.length}" }
      args.outputs.labels << { x: 10, y: 70.from_top, r: 255, g: 255, b: 255, size_enum: -2, text: "FPS: #{GTK.current_framerate.to_sf}" }
    end
    
    

    Collision Limits Many To Many - main.rb link

    # ./samples/09_performance/09_collision_limits_many_to_many/app/main.rb
    class Square
      attr_sprite
    
      def initialize x, y
        @x    = x
        @y    = y
        @w    = 8
        @h    = 8
        @path = 'sprites/square/blue.png'
        @dir = if x < 640
                 -1.0
               else
                 1.0
               end
      end
    
      def reset_collision
        @path = "sprites/square/blue.png"
      end
    
      def mark_collision
        @path = 'sprites/square/red.png'
      end
    
      def move
        @dir  = -1.0 if (@x + @w >= 1280) && @dir ==  1.0
        @dir  =  1.0 if (@x      <=    0) && @dir == -1.0
        @x   += @dir
      end
    end
    
    def generate_random_squares args, center_x, center_y
      100.times do
        angle = rand 360
        distance = rand(200) + 20
        x = center_x + angle.vector_x * distance
        y = center_y + angle.vector_y * distance
        if x > 0 && x < 1280 && y < 720 && y > 0
          args.state.squares << Square.new(x, y)
        end
      end
    
      args.outputs.static_sprites.clear
      args.outputs.static_sprites << args.state.squares
      args.state.square_count = args.state.squares.length
    end
    
    def tick args
      args.state.squares ||= []
    
      if Kernel.tick_count == 0
        generate_random_squares args, 640, 360
      end
    
      if args.inputs.mouse.click
        generate_random_squares args, args.inputs.mouse.x, args.inputs.mouse.y
      end
    
      Array.each(args.state.squares) do |s|
        s.reset_collision
        s.move
      end
    
      Geometry.each_intersect_rect(args.state.squares, args.state.squares) do |a, b|
        a.mark_collision
        b.mark_collision
      end
    
      args.outputs.background_color = [0, 0, 0]
      args.outputs.watch "FPS: #{GTK.current_framerate.to_sf}"
      args.outputs.watch "Square Count: #{args.state.square_count.to_i}"
      args.outputs.watch "Instructions: click to add squares."
    end
    
    

    Ui Controls link

    Checkboxes - main.rb link

    # ./samples/09_ui_controls/01_checkboxes/app/main.rb
    def tick args
      # use layout apis to position check boxes
      args.state.checkboxes ||= [
        Layout.rect(row: 0, col: 0, w: 1, h: 1).merge(id: :option1, text: "Option 1", checked: false, changed_at: -120),
        Layout.rect(row: 1, col: 0, w: 1, h: 1).merge(id: :option1, text: "Option 2", checked: false, changed_at: -120),
        Layout.rect(row: 2, col: 0, w: 1, h: 1).merge(id: :option1, text: "Option 3", checked: false, changed_at: -120),
        Layout.rect(row: 3, col: 0, w: 1, h: 1).merge(id: :option1, text: "Option 4", checked: false, changed_at: -120),
      ]
    
      # check for click of checkboxes
      if args.inputs.mouse.click
        args.state.checkboxes.find_all do |checkbox|
          args.inputs.mouse.inside_rect? checkbox
        end.each do |checkbox|
          # mark checkbox value
          checkbox.checked = !checkbox.checked
          # set the time the checkbox was changed
          checkbox.changed_at = Kernel.tick_count
        end
      end
    
      # render checkboxes
      args.outputs.primitives << args.state.checkboxes.map do |checkbox|
        # baseline prefab for checkbox
        prefab = {
          x: checkbox.x,
          y: checkbox.y,
          w: checkbox.w,
          h: checkbox.h
        }
    
        # label for checkbox centered vertically
        label = {
          x: checkbox.x + checkbox.w + 10,
          y: checkbox.y + checkbox.h / 2,
          text: checkbox.text,
          alignment_enum: 0,
          vertical_alignment_enum: 1
        }
    
        # rendering if checked or not
        if checkbox.checked
          # fade in
          a = 255 * Easing.ease(checkbox.changed_at, Kernel.tick_count, 30, :smooth_stop_quint)
    
          [
            label,
            prefab.merge(primitive_marker: :solid, a: a),
            prefab.merge(primitive_marker: :border)
          ]
        else
          # fade out
          a = 255 * Easing.ease(checkbox.changed_at, Kernel.tick_count, 30, :smooth_stop_quint, :flip)
    
          [
            label,
            prefab.merge(primitive_marker: :solid, a: a),
            prefab.merge(primitive_marker: :border)
          ]
        end
      end
    end
    
    

    Menu Navigation - main.rb link

    # ./samples/09_ui_controls/02_menu_navigation/app/main.rb
    class Game
      attr_gtk
    
      def tick
        defaults
        calc
        render
      end
    
      def render
        outputs.primitives << state.selection_point.merge(w: state.menu.button_w + 8,
                                                          h: state.menu.button_h + 8,
                                                          a: 128,
                                                          r: 0,
                                                          g: 200,
                                                          b: 100,
                                                          path: :solid,
                                                          anchor_x: 0.5,
                                                          anchor_y: 0.5)
    
        outputs.primitives << state.menu.buttons.map(&:primitives)
      end
    
      def calc_directional_input
        return if state.input_debounce.elapsed_time < 10
        return if !inputs.directional_vector
        state.input_debounce = Kernel.tick_count
    
        state.selected_button = Geometry::rect_navigate(
          rect: state.selected_button,
          rects: state.menu.buttons,
          left_right: inputs.left_right,
          up_down: inputs.up_down,
          wrap_x: true,
          wrap_y: true,
          using: lambda { |e| e.rect }
        )
      end
    
      def calc_mouse_input
        return if !inputs.mouse.moved
        hovered_button = state.menu.buttons.find { |b| Geometry::intersect_rect? inputs.mouse, b.rect }
        if hovered_button
          state.selected_button = hovered_button
        end
      end
    
      def calc
        target_point = state.selected_button.rect.center
        state.selection_point.x = state.selection_point.x.lerp(target_point.x, 0.25)
        state.selection_point.y = state.selection_point.y.lerp(target_point.y, 0.25)
        calc_directional_input
        calc_mouse_input
      end
    
      def defaults
        if !state.menu
          state.menu = {
            button_cell_w: 2,
            button_cell_h: 1,
          }
          state.menu.button_w = Layout::rect(w: 2).w
          state.menu.button_h = Layout::rect(h: 1).h
          state.menu.buttons = [
            menu_prefab(id: :item_1, text: "Item 1", row: 0, col: 0, w: state.menu.button_cell_w, h: state.menu.button_cell_h),
            menu_prefab(id: :item_2, text: "Item 2", row: 0, col: 2, w: state.menu.button_cell_w, h: state.menu.button_cell_h),
            menu_prefab(id: :item_3, text: "Item 3", row: 0, col: 4, w: state.menu.button_cell_w, h: state.menu.button_cell_h),
            menu_prefab(id: :item_4, text: "Item 4", row: 1, col: 0, w: state.menu.button_cell_w, h: state.menu.button_cell_h),
            menu_prefab(id: :item_5, text: "Item 5", row: 1, col: 2, w: state.menu.button_cell_w, h: state.menu.button_cell_h),
            menu_prefab(id: :item_6, text: "Item 6", row: 1, col: 4, w: state.menu.button_cell_w, h: state.menu.button_cell_h),
            menu_prefab(id: :item_7, text: "Item 7", row: 2, col: 0, w: state.menu.button_cell_w, h: state.menu.button_cell_h),
            menu_prefab(id: :item_8, text: "Item 8", row: 2, col: 2, w: state.menu.button_cell_w, h: state.menu.button_cell_h),
            menu_prefab(id: :item_9, text: "Item 9", row: 2, col: 4, w: state.menu.button_cell_w, h: state.menu.button_cell_h),
          ]
        end
    
        state.selected_button ||= state.menu.buttons.first
        state.selection_point ||= { x: state.selected_button.rect.center.x,
                                    y: state.selected_button.rect.center.y }
        state.input_debounce  ||= 0
      end
    
      def menu_prefab id:, text:, row:, col:, w:, h:;
        rect = Layout::rect(row: row, col: col, w: w, h: h)
        {
          id: id,
          row: row,
          col: col,
          text: text,
          rect: rect,
          primitives: [
            rect.merge(primitive_marker: :border),
            rect.center.merge(text: text, anchor_x: 0.5, anchor_y: 0.5)
          ]
        }
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    def reset args
      $game = nil
    end
    
    GTK.reset
    
    

    Radial Menu - main.rb link

    # ./samples/09_ui_controls/03_radial_menu/app/main.rb
    class Game
      attr_gtk
    
      def tick
        defaults
        calc
        render
      end
    
      def defaults
        state.menu_items = [
          { id: :item_1, text: "Item 1" },
          { id: :item_2, text: "Item 2" },
          { id: :item_3, text: "Item 3" },
          { id: :item_4, text: "Item 4" },
          { id: :item_5, text: "Item 5" },
          { id: :item_6, text: "Item 6" },
          { id: :item_7, text: "Item 7" },
          { id: :item_8, text: "Item 8" },
          { id: :item_9, text: "Item 9" },
        ]
    
        state.menu_status     ||= :hidden
        state.menu_radius     ||= 200
        state.menu_status_at  ||= -1000
      end
    
      def calc
        state.menu_items.each_with_index do |item, i|
          item.menu_angle = 90 + (360 / state.menu_items.length) * i
          item.menu_angle_range = 360 / state.menu_items.length - 10
        end
    
        state.menu_items.each do |item|
          item.rect = Geometry.rect_props x: 640 + item.menu_angle.vector_x * state.menu_radius - 50,
                                          y: 360 + item.menu_angle.vector_y * state.menu_radius - 25,
                                          w: 100,
                                          h: 50
    
          item.circle = { x: item.rect.x + item.rect.w / 2, y: item.rect.y + item.rect.h / 2, radius: item.rect.w / 2 }
        end
    
        show_menu_requested = false
        if state.menu_status == :hidden
          show_menu_requested = true if inputs.controller_one.key_down.a
          show_menu_requested = true if inputs.mouse.click
        end
    
        hide_menu_requested = false
        if state.menu_status == :shown
          hide_menu_requested = true if inputs.controller_one.key_down.b
          hide_menu_requested = true if inputs.mouse.click && !state.hovered_menu_item
        end
    
        if state.menu_status == :shown && state.hovered_menu_item && (inputs.mouse.click || inputs.controller_one.key_down.a)
          GTK.notify! "You selected #{state.hovered_menu_item[:text]}"
        elsif show_menu_requested
          state.menu_status = :shown
          state.menu_status_at = Kernel.tick_count
        elsif hide_menu_requested
          state.menu_status = :hidden
          state.menu_status_at = Kernel.tick_count
        end
    
        state.hovered_menu_item = state.menu_items.find { |item| Geometry.point_inside_circle? inputs.mouse, item.circle }
    
        if inputs.controller_one.active && inputs.controller_one.left_analog_active?(threshold_perc: 0.5)
          state.hovered_menu_item = state.menu_items.find do |item|
            Geometry.angle_within_range? inputs.controller_one.left_analog_angle, item.menu_angle, item.menu_angle_range
          end
        end
      end
    
      def menu_prefab item, perc
        dx = item.rect.center.x - 640
        x = 640 + dx * perc
        dy = item.rect.center.y - 360
        y = 360 + dy * perc
        Geometry.rect_props item.rect.merge x: x - item.rect.w / 2, y: y - item.rect.h / 2
      end
    
      def ring_prefab x_center, y_center, radius, precision:, color: nil
        color ||= { r: 0, g: 0, b: 0, a: 255 }
        pi = Math::PI
        lines = []
    
        precision.map do |i|
          theta = 2.0 * pi * i / precision
          next_theta = 2.0 * pi * (i + 1) / precision
    
          {
            x: x_center + radius * theta.cos_r,
            y: y_center + radius * theta.sin_r,
            x2: x_center + radius * next_theta.cos_r,
            y2: y_center + radius * next_theta.sin_r,
            **color
          }
        end
      end
    
      def circle_prefab x_center, y_center, radius, precision:, color: nil
        color ||= { r: 0, g: 0, b: 0, a: 255 }
        pi = Math::PI
        lines = []
    
        # Indie/Pro Only (uses triangles)
        precision.map do |i|
          theta = 2.0 * pi * i / precision
          next_theta = 2.0 * pi * (i + 1) / precision
    
          {
            x:  x_center + radius * theta.cos_r,
            y:  y_center + radius * theta.sin_r,
            x2: x_center + radius * next_theta.cos_r,
            y2: y_center + radius * next_theta.sin_r,
            y3: y_center,
            x3: x_center,
            source_x:  0,
            source_y:  0,
            source_x2: 0,
            source_y2: radius,
            source_x3: radius,
            source_y3: 0,
            path:      :solid,
            **color,
          }
        end
      end
    
      def render
        outputs.debug.watch "Controller"
        outputs.debug.watch pretty_format(inputs.controller_one.to_h)
    
        outputs.debug.watch "Mouse"
        outputs.debug.watch pretty_format(inputs.mouse.to_h)
    
        # outputs.debug.watch "Mouse"
        # outputs.debug.watch pretty_format(inputs.mouse)
        outputs.primitives << { x: 640, y: 360, w: 10, h: 10, path: :solid, r: 128, g: 0, b: 0, a: 128, anchor_x: 0.5, anchor_y: 0.5 }
    
        if state.menu_status == :shown
          perc = Easing.ease(state.menu_status_at, Kernel.tick_count, 30, :smooth_stop_quart)
        else
          perc = Easing.ease(state.menu_status_at, Kernel.tick_count, 30, :smooth_stop_quart, :flip)
        end
    
        outputs.primitives << state.menu_items.map do |item|
          a = 255 * perc
          color = { r: 128, g: 128, b: 128, a: a }
          if state.hovered_menu_item == item
            color = { r: 80, g: 128, b: 80, a: a }
          end
    
          menu = menu_prefab(item, perc)
    
          if state.menu_status == :shown
            ring = ring_prefab(menu.center.x, menu.center.y, item.circle.radius, precision: 30, color: color.merge(a: 128))
            circle = circle_prefab(menu.center.x, menu.center.y, item.circle.radius, precision: 30, color: color.merge(a: 128))
          end
    
          [
            ring,
            circle,
            menu.merge(path: :solid, **color),
            menu.center.merge(text: item.text, a: a, anchor_x: 0.5, anchor_y: 0.5)
          ]
        end
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    def reset
      $game = nil
    end
    
    GTK.reset
    
    

    Scroll View - main.rb link

    # ./samples/09_ui_controls/03_scroll_view/app/main.rb
    class ScrollView
      attr_gtk
    
      attr :y_offset, :rect, :clicked_items, :target_y_offset
    
      def initialize row:, col:, w:, h:;
        @items = []
        @clicked_items = []
        @y_offset = 0
        @scroll_view_dy = 0
        @rect = Layout.rect row: row,
                            col: col,
                            w: w,
                            h: h,
                            include_row_gutter: true,
                            include_col_gutter: true
        @primitives = []
      end
    
      def add_item prefab
        raise "prefab must be a Hash" unless prefab.is_a? Hash
        @items << prefab
      end
    
      def content_height
        lowest_item = @items.min_by { |primitive| primitive.y } || { x: 0, y: 0 }
        h = @rect.h
    
        if lowest_item
          h -= lowest_item.y - Layout.gutter
        end
    
        h
      end
    
      def y_offset_bottom_limit
        -80
      end
    
      def y_offset_top_limit
        content_height - @rect.h + @rect.y + 80
      end
    
      def tick_inputs
        @clicked_items = []
    
        if inputs.mouse.down
          @last_mouse_held_y = inputs.mouse.y
          @last_mouse_held_y_diff = 0
        elsif inputs.mouse.held
          @last_mouse_held_y ||= inputs.mouse.y
          @last_mouse_held_y_diff ||= 0
          @last_mouse_held_y_diff = inputs.mouse.y - @last_mouse_held_y
          @last_mouse_held_y = inputs.mouse.y
        end
    
        if inputs.mouse.down
          @mouse_down_at = Kernel.tick_count
          @mouse_down_y = inputs.mouse.y
          if @scroll_view_dy.abs < 7
            @maybe_click = true
          else
            @maybe_click = false
          end
    
          @scroll_view_dy = 0
        elsif inputs.mouse.held
          @target_y_offset = @y_offset + (inputs.mouse.y - @mouse_down_y) * 2
          @mouse_down_y = inputs.mouse.y
        elsif inputs.mouse.up
          @target_y_offset = nil
          @mouse_up_at = Kernel.tick_count
          @mouse_up_y = inputs.mouse.y
    
          if @maybe_click && (@last_mouse_held_y_diff).abs <= 1 && (@mouse_down_at - @mouse_up_at).abs < 12
            if inputs.mouse.y - 20 > @rect.y && inputs.mouse.y < (@rect.y + @rect.h - 20)
              @clicked_items = offset_items.reject { |primitive| !primitive.w || !primitive.h }
                                           .find_all { |primitive| inputs.mouse.inside_rect? primitive }
            end
          else
            @scroll_view_dy += @last_mouse_held_y_diff
          end
          @mouse_down_at = nil
          @mouse_up_at = nil
        end
    
        if inputs.keyboard.key_down.page_down
          if @scroll_view_dy >= 0
            @scroll_view_dy += 5
          else
            @scroll_view_dy = @scroll_view_dy.lerp(0, 1)
          end
        elsif inputs.keyboard.key_down.page_up
          if @scroll_view_dy <= 0
            @scroll_view_dy -= 5
          else
            @scroll_view_dy = @scroll_view_dy.lerp(0, 1)
          end
        end
    
        if inputs.mouse.wheel
          if inputs.mouse.wheel.inverted
            @scroll_view_dy -= inputs.mouse.wheel.y
          else
            @scroll_view_dy += inputs.mouse.wheel.y
          end
        end
    
      end
    
      def tick
        if @target_y_offset
          if @target_y_offset < y_offset_bottom_limit
            @y_offset = @y_offset.lerp @target_y_offset, 0.05
          elsif @target_y_offset > y_offset_top_limit
            @y_offset = @y_offset.lerp @target_y_offset, 0.05
          else
            @y_offset = @y_offset.lerp @target_y_offset, 0.5
          end
          @target_y_offset = nil if @y_offset.round == @target_y_offset.round
          @scroll_view_dy = 0
        end
    
        tick_inputs
    
        @y_offset += @scroll_view_dy
    
        if @y_offset < 0
          if inputs.mouse.held
            # if @y_offset < -80
            #   @y_offset = -80
            # end
          else
            @y_offset = @y_offset.lerp(0, 0.2)
          end
        end
    
        if content_height <= (@rect.h - @rect.y)
          @y_offset = 0
          @scroll_view_dy = 0
        elsif @y_offset > content_height - @rect.h + @rect.y
          if inputs.mouse.held
            # if @y_offset > (content_height - @rect.h + @rect.y) + 80
            #   @y_offset = (content_height - @rect.h + @rect.y) + 80
            # end
          else
            @y_offset = @y_offset.lerp(content_height - @rect.h + @rect.y, 0.2)
          end
        end
        @scroll_view_dy *= 0.95
        @scroll_view_dy = @scroll_view_dy.round(2)
      end
    
      def items
        @items
      end
    
      def offset_items
        @items.map { |primitive| primitive.merge(y: primitive.y + @y_offset) }
      end
    
      def prefab
        outputs[:scroll_view].w = Grid.w
        outputs[:scroll_view].h = Grid.h
        outputs[:scroll_view].background_color = [0, 0, 0, 0]
    
        outputs[:scroll_view_content].w = Grid.w
        outputs[:scroll_view_content].h = Grid.h
        outputs[:scroll_view_content].background_color = [0, 0, 0, 0]
    
        outputs[:scroll_view_content].primitives << offset_items
    
        outputs[:scroll_view].primitives << {
          x: @rect.x,
          y: @rect.y,
          w: @rect.w,
          h: @rect.h,
          source_x: @rect.x,
          source_y: @rect.y,
          source_w: @rect.w,
          source_h: @rect.h,
          path: :scroll_view_content
        }
    
        outputs[:scroll_view].primitives << [
          { x: @rect.x,
            y: @rect.y,
            w: @rect.w,
            h: @rect.h,
            primitive_marker: :border,
            r: 128,
            g: 128,
            b: 128 },
        ]
    
        { x: 0,
          y: 0,
          w: Grid.w,
          h: Grid.h,
          path: :scroll_view }
      end
    end
    
    class Game
      attr_gtk
    
      attr :scroll_view
    
      def initialize
        @scroll_view = ScrollView.new row: 2, col: 0, w: 12, h: 20
      end
    
      def defaults
        state.scroll_view_dy             ||= 0
        state.scroll_view_offset_y       ||= 0
      end
    
      def calc
        if Kernel.tick_count == 0
          80.times do |i|
            @scroll_view.add_item Layout.rect(row: 2 + i * 2, col: 0, w: 2, h: 2).merge(id: "item_#{i}_square_1".to_sym, path: :solid, r: 32 + i * 2, g: 32, b: 32)
            @scroll_view.add_item Layout.rect(row: 2 + i * 2, col: 0, w: 2, h: 2).center.merge(text: "item #{i}", anchor_x: 0.5, anchor_y: 0.5, r: 255, g: 255, b: 255)
            @scroll_view.add_item Layout.rect(row: 2 + i * 2, col: 2, w: 2, h: 2).merge(id: "item_#{i}_square_2".to_sym, path: :solid, r: 64 + i * 2, g: 64, b: 64)
          end
        end
    
        @scroll_view.args = args
        @scroll_view.tick
    
        if @scroll_view.clicked_items.length > 0
          puts @scroll_view.clicked_items
        end
      end
    
      def render
        outputs.primitives << @scroll_view.prefab
      end
    
      def tick
        defaults
        calc
        render
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    def reset args
      $game = nil
    end
    
    GTK.reset
    
    

    Accessiblity For The Blind - main.rb link

    # ./samples/09_ui_controls/04_accessiblity_for_the_blind/app/main.rb
    def tick args
      # create three buttons
      args.state.button_1 ||= { x: 0, y: 640, w: 100, h: 50 }
      args.state.button_1_label ||= { x: 50,
                                      y: 665,
                                      text: "button 1",
                                      anchor_x: 0.5,
                                      anchor_y: 0.5 }
    
      args.state.button_2 ||= { x: 104, y: 640, w: 100, h: 50 }
      args.state.button_2_label ||= { x: 154,
                                      y: 665,
                                      text: "button 2",
                                      anchor_x: 0.5,
                                      anchor_y: 0.5 }
    
      args.state.button_3 ||= { x: 208, y: 640, w: 100, h: 50 }
      args.state.button_3_label ||= { x: 258,
                                      y: 665,
                                      text: "button 3",
                                      anchor_x: 0.5,
                                      anchor_y: 0.5 }
    
      # create a label
      args.state.label_hello_world ||= { x: 640,
                                         y: 360,
                                         text: "hello world",
                                         anchor_x: 0.5,
                                         anchor_y: 0.5 }
    
      args.outputs.borders << args.state.button_1
      args.outputs.labels  << args.state.button_1_label
    
      args.outputs.borders << args.state.button_2
      args.outputs.labels  << args.state.button_2_label
    
      args.outputs.borders << args.state.button_3
      args.outputs.labels  << args.state.button_3_label
    
      args.outputs.labels  << args.state.label_hello_world
    
      # args.outputs.a11y is cleared every tick, internally the key
      # of the dictionary value is used to reference the interactable element.
      # the key can be a symbol or a string (everything get's converted to strings
      # beind the scenes)
    
      # =======================================
      # from the Console run GTK.a11y_enable!
      # ctrl+r will disable a11y (or you can run GTK.a11y_disable! in the console)
      # =======================================
    
      # with the a11y emulation enabled, you can only use left arrow, right arrow, and enter
      # when you press enter, DR converts the location to a mouse click
      args.outputs.a11y[:button_1] = {
        a11y_text: "button 1",
        a11y_trait: :button,
        x: args.state.button_1.x,
        y: args.state.button_1.y,
        w: args.state.button_1.w,
        h: args.state.button_1.h
      }
    
      args.outputs.a11y[:button_2] = {
        a11y_text: "button 2",
        a11y_trait: :button,
        x: args.state.button_2.x,
        y: args.state.button_2.y,
        w: args.state.button_2.w,
        h: args.state.button_2.h
      }
    
      args.outputs.a11y[:button_3] = {
        a11y_text: "button 3",
        a11y_trait: :button,
        x: args.state.button_3.x,
        y: args.state.button_3.y,
        w: args.state.button_3.w,
        h: args.state.button_3.h
      }
    
      args.outputs.a11y[:label_hello] = {
        a11y_text: "hello world",
        a11y_trait: :label,
        x: args.state.label_hello_world.x,
        y: args.state.label_hello_world.y,
        anchor_x: 0.5,
        anchor_y: 0.5,
      }
    
      # flash a notification for each respective button
      if args.inputs.mouse.click && args.inputs.mouse.inside_rect?(args.state.button_1)
        GTK.notify_extended! message: "Button 1 clicked", a: 255
        # you can use a11y to speak information
        args.outputs.a11y["notify button clicked"] = {
          a11y_text: "button 1 clicked",
          a11y_trait: :notification
        }
      end
    
      if args.inputs.mouse.click && args.inputs.mouse.inside_rect?(args.state.button_2)
        GTK.notify_extended! message: "Button 2 clicked", a: 255
      end
    
      if args.inputs.mouse.click && args.inputs.mouse.inside_rect?(args.state.button_3)
        GTK.notify_extended! message: "Button 3 clicked", a: 255
        # you can also use a11y to redirect focus to another control
        args.outputs.a11y["notify button clicked"] = {
          a11y_trait: :notification,
          a11y_notification_target: :label_hello
        }
      end
    end
    
    GTK.reset
    
    

    Advanced Debugging link

    Unit Tests - main.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/app/main.rb
    
    

    Unit Tests - benchmark_api_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/benchmark_api_tests.rb
    def test_benchmark_api args, assert
      result = GTK.benchmark iterations: 100,
                             only_one: -> () {
                               r = 0
                               (1..100).each do |i|
                                 r += 1
                               end
                             }
    
      assert.equal! result.first_place.name, :only_one
    
      result = GTK.benchmark iterations: 100,
                             iterations_100: -> () {
                               r = 0
                               (1..100).each do |i|
                                 r += 1
                               end
                             },
                             iterations_50: -> () {
                               r = 0
                               (1..50).each do |i|
                                 r += 1
                               end
                             }
    
      assert.equal! result.first_place.name, :iterations_50
    
      result = GTK.benchmark iterations: 1,
                             iterations_100: -> () {
                               r = 0
                               (1..100).each do |i|
                                 r += 1
                               end
                             },
                             iterations_50: -> () {
                               r = 0
                               (1..50).each do |i|
                                 r += 1
                               end
                             }
    
      assert.equal! result.too_small_to_measure, true
    end
    
    

    Unit Tests - enumerable_class_function_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/enumerable_class_function_tests.rb
    def test_hash_find_all args, assert
      h = {
        x: 100,
        y: 200,
        w: 10,
        h: 10
      }
    
      result_expected = h.find_all { |k, v| v == 100 }
      result_actual = Hash.find_all(h) { |k, v| v == 100 }
      assert.equal! result_expected, result_actual
    end
    
    def test_hash_merge args, assert
      a = {
        x: 100,
        y: 200,
        w: 10,
        h: 10
      }
    
      b = {
        r: 255,
        g: 255,
        b: 255
      }
    
      result_expected = a.merge b
      result_actual = Hash.merge a, b
      assert.equal! result_actual, result_expected, "class implementation, matches instance implemenation"
      assert.not_equal! a.object_id, result_actual.object_id, "new hash created for merge"
    end
    
    def test_hash_merge_bang args, assert
      a = {
        x: 100,
        y: 200,
        w: 10,
        h: 10
      }
    
      b = {
        r: 255,
        g: 255,
        b: 255
      }
    
      a_2 = {
        x: 100,
        y: 200,
        w: 10,
        h: 10
      }
    
      b_2 = {
        r: 255,
        g: 255,
        b: 255
      }
    
      result_expected = a.merge! b
      result_actual = Hash.merge! a_2, b_2
      assert.equal! result_actual, result_expected, "class implementation, matches instance implemenation"
      assert.equal! a_2.object_id, result_actual.object_id, "hash updated for merge!"
    end
    
    def test_hash_merge_with_block args, assert
      a = {
        x: 100,
        y: 200,
        w: 10,
        h: 10
      }
    
      b = {
        x: 500,
      }
    
      result_expected = a.merge(b) do |k, current_value, new_value|
        current_value + new_value
      end
    
      result_actual = Hash.merge(a, b) do
        |k, current_value, new_value|
        current_value + new_value
      end
    
      assert.equal! result_expected[:x], result_actual[:x]
    end
    
    def test_array_map args, assert
      a = [1, 2, 3]
    
      result_expected = a.map do |i| i**2 end
      result_actual = Array.map a do |i| i**2 end
      assert.equal! result_expected, result_actual
      assert.not_equal! a.object_id, result_actual.object_id
    end
    
    def test_array_map_with_destructoring args, assert
      a = [[1, 2], [3, 4]]
      result_expected = a.map do |x, y| x + y end
      result_actual = Array.map a do |x, y| x + y end
      assert.equal! result_expected, result_actual
      assert.not_equal! a.object_id, result_actual.object_id
    
      a = [[1, 2], [3, 4]]
      result_expected = a.map.with_index do |(x, y), i| x + y + i end
      result_actual = Array.map_with_index a do |(x, y), i| x + y + i end
      assert.equal! result_expected, result_actual
      assert.not_equal! a.object_id, result_actual.object_id
    end
    
    def test_array_map_bang args, assert
      a = [1, 2, 3]
      result_expected = a.map do |i| i**2 end
      result_actual = Array.map! a do |i| i**2 end
      assert.equal! result_expected, result_actual
      assert.equal! a.object_id, result_actual.object_id
    end
    
    def test_array_reject args, assert
      a = [1, 2, 3, 4, 5, 6]
      result_expected = a.reject do |i| i.even? end
      result_actual = Array.reject a do |i| i.even? end
      assert.equal! result_expected, result_actual
      assert.not_equal! a.object_id, result_actual.object_id
    end
    
    def test_array_reject_bang args, assert
      a = [1, 2, 3, 4, 5, 6]
      result_expected = a.reject do |i| i.even? end
      result_actual = Array.reject! a do |i| i.even? end
      assert.equal! result_expected, result_actual
      assert.equal! a.object_id, result_actual.object_id
    end
    
    def test_array_select args, assert
      a = [1, 2, 3, 4, 5, 6]
      result_expected = a.select do |i| i.even? end
      result_actual = Array.select a do |i| i.even? end
      assert.equal! result_expected, result_actual
      assert.not_equal! a.object_id, result_actual.object_id
    end
    
    def test_array_select_bang args, assert
      a = [1, 2, 3, 4, 5, 6]
      result_expected = a.select do |i| i.even? end
      result_actual = Array.select! a do |i| i.even? end
      assert.equal! result_expected, result_actual
      assert.equal! a.object_id, result_actual.object_id
    end
    
    def test_array_find_all args, assert
      a = [1, 2, 3, 4, 5, 6]
      result_expected = a.find_all do |i| i.even? end
      result_actual = Array.find_all a do |i| i.even? end
      assert.equal! result_expected, result_actual
      assert.not_equal! a.object_id, result_actual.object_id
    end
    
    def test_array_compact args, assert
      a = [1, nil, 3, false, 5, 6]
      result_expected = a.compact do |i| i.even? end
      result_actual = Array.compact a do |i| i.even? end
      assert.equal! result_expected, result_actual
      assert.not_equal! a.object_id, result_actual.object_id
    end
    
    def test_array_compact_bang args, assert
      a = 100.map { |i| i }.map { |i| i.even? ? i : nil }
      result_expected = a.compact do |i| i.even? end
      result_actual = Array.compact! a do |i| i.even? end
      assert.equal! result_expected, result_actual
      assert.equal! a.object_id, result_actual.object_id
    end
    
    def test_filter_map args, assert
      a = [1, 2, 3, 4, 5, 6]
      result_expected = a.filter_map do |i| i.even? ? i * 2 : nil end
      result_actual = Array.filter_map a do |i| i.even? ? i * 2 : nil end
      assert.equal! result_expected, result_actual
      assert.not_equal! a.object_id, result_actual.object_id
    end
    
    def test_flat_map args, assert
      a = 100.map.each_slice(2).to_a
      result_expected = a.flat_map do |i| i end
      result_actual = Array.flat_map a do |i| i end
      assert.equal! result_expected, result_actual
      assert.not_equal! a.object_id, result_actual.object_id
    end
    
    def test_array_each args, assert
      a = [1, 2, 3, 4, 5, 6]
      result_expected = []
      a.each do |i| result_expected << i end
      result_actual = []
      Array.each a do |i| result_actual << i end
      assert.equal! result_expected, result_actual
    
      a = [[1, 2], [3, 4], [5, 6]]
      result_expected = []
      a.each do |x, y| result_expected << x + y end
      result_actual = []
      Array.each a do |x, y| result_actual << x + y end
      assert.equal! result_expected, result_actual
    
      a = [1, 2, 3, 4, 5, 6]
      result_expected = []
      a.each_with_index do |n, i| result_expected << n - i end
      result_actual = []
      Array.each_with_index a do |n, i| result_actual << n - i end
      assert.equal! result_expected, result_actual
    
      a = [[1, 2], [3, 4], [5, 6]]
      result_expected = []
      a.each_with_index do |(x, y), i| result_expected << x + y + i end
      result_actual = []
      Array.each_with_index a do |(x, y), i| result_actual << x + y + i end
      assert.equal! result_expected, result_actual
    end
    
    def test_bench args, assert
      ary_numbers = 100.map { |i| i }.reverse.to_a
      ary_compact = 100.map { |i| i }.map { |i| i.even? ? i : nil }
      ary_flat_map = 100.map.each_slice(2).to_a
    
      functions = [
        { name: :map,        ary: ary_numbers, m: proc { |i| i / 2 } },
        { name: :map!,       ary: ary_numbers, m: proc { |i| i / 2 } },
        { name: :reject,     ary: ary_numbers, m: proc { |i| i.even? } },
        { name: :reject!,    ary: ary_numbers, m: proc { |i| i.even? } },
        { name: :find_all,   ary: ary_numbers, m: proc { |i| i.even? } },
        { name: :select,     ary: ary_numbers, m: proc { |i| i.even? } },
        { name: :select!,    ary: ary_numbers, m: proc { |i| i.even? } },
        { name: :filter_map, ary: ary_numbers, m: proc { |i| i.even? ? i * 2 : nil } },
        { name: :compact,    ary: ary_compact },
        { name: :compact!,   ary: ary_compact },
      ]
    
      functions.each do |fh|
        h = {
          iterations: 5000,
        }
    
        self_numbers = fh.ary.dup
        class_numbers = fh.ary.dup
    
        h["self_#{fh[:name]}".to_sym] = -> () {
          self_numbers.send(fh[:name], &fh[:m])
        }
    
        h["class_#{fh[:name]}".to_sym] = -> () {
          Array.send(fh[:name], class_numbers, &fh[:m])
        }
    
        results = GTK.benchmark(**h)
        assert.true! results.first_place.name.to_s.start_with?("class_"), "Class method #{fh[:name]} is faster"
      end
    
      self_numbers = ary_numbers.dup
      class_numbers = ary_numbers.dup
      results = GTK.benchmark iterations: 5000,
                              self_each: -> () { self_numbers.each { |i| i } },
                              class_each: -> () { Array.each(class_numbers) { |i| i } }
    
      self_flat_map = ary_flat_map.dup
      class_flat_map = ary_flat_map.dup
      results = GTK.benchmark iterations: 5000,
                              self_flat_map: -> () { self_flat_map.flat_map { |i| i } },
                              class_flat_map: -> () { Array.flat_map(class_flat_map) { |i| i } }
    
      assert.true! results.first_place.name.to_s.start_with?("class_"), "Class method each is faster"
    end
    
    

    Unit Tests - eval_hash_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/eval_hash_tests.rb
    def assert_hash assert, hash_or_string, expected
      h_to_s = if hash_or_string.is_a? String
                 hash_or_string
               else
                 hash_or_string.to_s
               end
    
      begin
        result = GTK::Codegen.eval_hash h_to_s
      rescue Exception => e
        result = e
      end
      if expected.is_a? Proc
        expected.call(result, assert)
      else
        assert.equal! result, expected
      end
    end
    
    def test_empty_hash args, assert
      assert_hash(assert, {}, {})
    end
    
    def test_allowed_node_types args, assert
      assert_hash(assert,
                  {
                    node_hash: { },
                    node_nil: nil,
                    node_int: 1,
                    node_float: 10.5,
                    node_str: "string",
                    node_sym: :symbol,
                    node_true: true,
                    node_false: false,
                    node_array: [1, 2, 3],
                  },
                  {
                    node_hash: { },
                    node_nil: nil,
                    node_int: 1,
                    node_float: 10.5,
                    node_str: "string",
                    node_sym: :symbol,
                    node_true: true,
                    node_false: false,
                    node_array: [1, 2, 3],
                  })
    end
    
    def test_args_state args, assert
      args.state.player.x ||= 100
      args.state.player.y ||= 200
      args.state.enemies ||= [
        { id: :a, x: 100, y: 100, w: 2, h: 3.0 },
        { id: :b, x: 100, y: 100, w: 2, h: 3.0 },
        { id: :c, x: 100, y: 100, w: 2, h: 3.0 },
        { id: :d, x: 100, y: 100, w: 2, h: 3.0 }
      ]
    
      assert_hash assert, args.state, ->(result, assert) {
        assert.true! args.state.as_hash.to_s, result.to_s
      }
    end
    
    def test_malicious_hash args, assert
      s = "{}; def malicious(args); end;"
      assert_hash assert, s, ->(result, assert) {
        assert.true! result.message.include?("NODE_DEF")
      }
    end
    
    def test_malicious_lvar_hash args, assert
      s = "a = 12; {};"
      assert_hash assert, s, ->(result, assert) {
        assert.true! result.message.include?("NODE_ASGN")
      }
    end
    
    

    Unit Tests - exception_raising_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/exception_raising_tests.rb
    begin :shared
      class ExceptionalClass
        def initialize exception_to_throw = nil
          raise exception_to_throw if exception_to_throw
        end
      end
    end
    
    def test_exception_in_newing_object args, assert
      begin
        ExceptionalClass.new TypeError
        raise "Exception wasn't thrown!"
      rescue Exception => e
        assert.equal! e.class, TypeError, "Exceptions within constructor should be retained."
      end
    end
    
    

    Unit Tests - fn_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/fn_tests.rb
    def infinity
      1 / 0
    end
    
    def neg_infinity
      -1 / 0
    end
    
    def nan
      0.0 / 0
    end
    
    def test_add args, assert
      assert.equal! (args.fn.add), 0
      assert.equal! (args.fn.+), 0
      assert.equal! (args.fn.+ 1, 2, 3), 6
      assert.equal! (args.fn.+ 0), 0
      assert.equal! (args.fn.+ 0, nil), 0
      assert.equal! (args.fn.+ 0, nan), nil
      assert.equal! (args.fn.+ 0, nil, infinity), nil
      assert.equal! (args.fn.+ [1, 2, 3, [4, 5, 6]]), 21
      assert.equal! (args.fn.+ [nil, [4, 5, 6]]), 15
    end
    
    def test_sub args, assert
      neg_infinity = infinity * -1
      assert.equal! (args.fn.+), 0
      assert.equal! (args.fn.- 1, 2, 3), -4
      assert.equal! (args.fn.- 4), -4
      assert.equal! (args.fn.- 4, nan), nil
      assert.equal! (args.fn.- 0, nil), 0
      assert.equal! (args.fn.- 0, nil, infinity), nil
      assert.equal! (args.fn.- [0, 1, 2, 3, [4, 5, 6]]), -21
      assert.equal! (args.fn.- [nil, 0, [4, 5, 6]]), -15
    end
    
    def test_div args, assert
      assert.equal! (args.fn.div), 1
      assert.equal! (args.fn./), 1
      assert.equal! (args.fn./ 6, 3), 2
      assert.equal! (args.fn./ 6, infinity), nil
      assert.equal! (args.fn./ 6, nan), nil
      assert.equal! (args.fn./ infinity), nil
      assert.equal! (args.fn./ 0), nil
      assert.equal! (args.fn./ 6, [3]), 2
    end
    
    def test_idiv args, assert
      assert.equal! (args.fn.idiv), 1
      assert.equal! (args.fn.idiv 7, 3), 2
      assert.equal! (args.fn.idiv 6, infinity), nil
      assert.equal! (args.fn.idiv 6, nan), nil
      assert.equal! (args.fn.idiv infinity), nil
      assert.equal! (args.fn.idiv 0), nil
      assert.equal! (args.fn.idiv 7, [3]), 2
    end
    
    def test_mul args, assert
      assert.equal! (args.fn.mul), 1
      assert.equal! (args.fn.*), 1
      assert.equal! (args.fn.* 7, 3), 21
      assert.equal! (args.fn.* 6, nan), nil
      assert.equal! (args.fn.* 6, infinity), nil
      assert.equal! (args.fn.* infinity), nil
      assert.equal! (args.fn.* 0), 0
      assert.equal! (args.fn.* 7, [3]), 21
    end
    
    def test_acopy args, assert
      orig  = [1, 2, 3]
      clone = args.fn.acopy orig
      assert.equal! clone, [1, 2, 3]
      assert.equal! clone, orig
      assert.not_equal! clone.object_id, orig.object_id
    end
    
    def test_aget args, assert
      assert.equal! (args.fn.aget [:a, :b, :c], 1), :b
      assert.equal! (args.fn.aget [:a, :b, :c], nil), nil
      assert.equal! (args.fn.aget nil, 1), nil
    end
    
    def test_alength args, assert
      assert.equal! (args.fn.alength [:a, :b, :c]), 3
      assert.equal! (args.fn.alength nil), nil
    end
    
    def test_amap args, assert
      inc = lambda { |i| i + 1 }
      ary = [1, 2, 3]
      assert.equal! (args.fn.amap ary, inc), [2, 3, 4]
      assert.equal! (args.fn.amap nil, inc), nil
      assert.equal! (args.fn.amap ary, nil), nil
      assert.equal! (args.fn.amap ary, inc).class, Array
    end
    
    def test_and args, assert
      assert.equal! (args.fn.and 1, 2, 3, 4), 4
      assert.equal! (args.fn.and 1, 2, nil, 4), nil
      assert.equal! (args.fn.and), true
    end
    
    def test_or args, assert
      assert.equal! (args.fn.or 1, 2, 3, 4), 1
      assert.equal! (args.fn.or 1, 2, nil, 4), 1
      assert.equal! (args.fn.or), nil
      assert.equal! (args.fn.or nil, nil, false, 5, 10), 5
    end
    
    def test_eq_eq args, assert
      assert.equal! (args.fn.eq?), true
      assert.equal! (args.fn.eq? 1, 0), false
      assert.equal! (args.fn.eq? 1, 1, 1), true
      assert.equal! (args.fn.== 1, 1, 1), true
      assert.equal! (args.fn.== nil, nil), true
    end
    
    def test_apply args, assert
      assert.equal! (args.fn.and [nil, nil, nil]), [nil, nil, nil]
      assert.equal! (args.fn.apply [nil, nil, nil], args.fn.method(:and)), nil
      and_lambda = lambda {|*xs| args.fn.and(*xs)}
      assert.equal! (args.fn.apply [nil, nil, nil], and_lambda), nil
    end
    
    def test_areduce args, assert
      assert.equal! (args.fn.areduce [1, 2, 3], 0, lambda { |i, a| i + a }), 6
    end
    
    def test_array_hash args, assert
      assert.equal! (args.fn.array_hash :a, 1, :b, 2), { a: 1, b: 2 }
      assert.equal! (args.fn.array_hash), { }
    end
    
    

    Unit Tests - gen_docs.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/gen_docs.rb
    # ./dragonruby . --eval samples/10_advanced_debugging/03_unit_tests/gen_docs.rb --no-tick
    # OR
    # ./dragonruby ./samples/10_advanced_debugging/03_unit_tests --test gen_docs.rb
    Kernel.export_docs!
    
    

    Unit Tests - geometry_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/geometry_tests.rb
    begin :shared
      def primitive_representations x, y, w, h
        [
          [x, y, w, h],
          { x: x, y: y, w: w, h: h },
          RectForTest.new(x, y, w, h)
        ]
      end
    
      class RectForTest
        attr_sprite
    
        def initialize x, y, w, h
          @x = x
          @y = y
          @w = w
          @h = h
        end
    
        def to_s
          "RectForTest: #{[x, y, w, h]}"
        end
      end
    end
    
    begin :intersect_rect?
      def test_intersect_rect_point args, assert
        assert.true! [16, 13].intersect_rect?([13, 12, 4, 4]), "point intersects with rect."
      end
    
      def test_intersect_rect args, assert
        intersecting = primitive_representations(0, 0, 100, 100) +
                       primitive_representations(20, 20, 20, 20)
    
        intersecting.product(intersecting).each do |rect_one, rect_two|
          assert.true! rect_one.intersect_rect?(rect_two),
                       "intersect_rect? assertion failed for #{rect_one}, #{rect_two} (expected true)."
        end
    
        not_intersecting = [
          [ 0, 0, 5, 5],
          { x: 10, y: 10, w: 5, h: 5 },
          RectForTest.new(20, 20, 5, 5)
        ]
    
        not_intersecting.product(not_intersecting)
          .reject { |rect_one, rect_two| rect_one == rect_two }
          .each do |rect_one, rect_two|
          assert.false! rect_one.intersect_rect?(rect_two),
                        "intersect_rect? assertion failed for #{rect_one}, #{rect_two} (expected false)."
        end
      end
    end
    
    begin :inside_rect?
      def assert_inside_rect outer: nil, inner: nil, expected: nil, assert: nil
        assert.true! inner.inside_rect?(outer) == expected,
                     "inside_rect? assertion failed for outer: #{outer} inner: #{inner} (expected #{expected})."
      end
    
      def test_inside_rect args, assert
        outer_rects = primitive_representations(0, 0, 10, 10)
        inner_rects = primitive_representations(1, 1, 5, 5)
        primitive_representations(0, 0, 10, 10).product(primitive_representations(1, 1, 5, 5))
          .each do |outer, inner|
          assert_inside_rect outer: outer, inner: inner,
                             expected: true, assert: assert
        end
      end
    end
    
    begin :angle_to
      def test_angle_to args, assert
        origins = primitive_representations(0, 0, 0, 0)
        rights = primitive_representations(1, 0, 0, 0)
        aboves = primitive_representations(0, 1, 0, 0)
    
        origins.product(aboves).each do |origin, above|
          assert.equal! origin.angle_to(above), 90,
                        "A point directly above should be 90 degrees."
    
          assert.equal! above.angle_from(origin), 90,
                        "A point coming from above should be 90 degrees."
        end
    
        origins.product(rights).each do |origin, right|
          assert.equal! origin.angle_to(right) % 360, 0,
                        "A point directly to the right should be 0 degrees."
    
          assert.equal! right.angle_from(origin) % 360, 0,
                        "A point coming from the right should be 0 degrees."
    
        end
      end
    end
    
    begin :scale_rect
      def test_scale_rect args, assert
        assert.equal! [0, 0, 100, 100].scale_rect(0.5, 0.5),
                      [25.0, 25.0, 50.0, 50.0]
    
        assert.equal! [0, 0, 100, 100].scale_rect(0.5),
                      [0.0, 0.0, 50.0, 50.0]
    
        assert.equal! [0, 0, 100, 100].scale_rect_extended(percentage_x: 0.5, percentage_y: 0.5, anchor_x: 0.5, anchor_y: 0.5),
                      [25.0, 25.0, 50.0, 50.0]
    
        assert.equal! [0, 0, 100, 100].scale_rect_extended(percentage_x: 0.5, percentage_y: 0.5, anchor_x: 0, anchor_y: 0),
                      [0.0, 0.0, 50.0, 50.0]
      end
    end
    
    
    

    Unit Tests - http_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/http_tests.rb
    def try_assert_or_schedule args, assert
      if $result[:complete]
        log_info "Request completed! Verifying."
        if $result[:http_response_code] != 200
          log_info "The request yielded a result of #{$result[:http_response_code]} instead of 200."
          exit
        end
        log_info ":try_assert_or_schedule succeeded!"
      else
        GTK.schedule_callback Kernel.tick_count + 10 do
          try_assert_or_schedule args, assert
        end
      end
    end
    
    def test_http args, assert
      $result = GTK.http_get 'http://dragonruby.org'
      try_assert_or_schedule args, assert
    end
    
    

    Unit Tests - input_emulation_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/input_emulation_tests.rb
    def test_keyboard args, assert
      args.inputs.keyboard.key_down.i = true
      assert.true! args.inputs.keyboard.truthy_keys.include?(:i)
    end
    
    

    Unit Tests - nil_coercion_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/nil_coercion_tests.rb
    # numbers
    def test_open_entity_add_number args, assert
      assert.nil! args.state.i_value
      args.state.i_value += 5
      assert.equal! args.state.i_value, 5
    
      assert.nil! args.state.f_value
      args.state.f_value += 5.5
      assert.equal! args.state.f_value, 5.5
    end
    
    def test_open_entity_subtract_number args, assert
      assert.nil! args.state.i_value
      args.state.i_value -= 5
      assert.equal! args.state.i_value, -5
    
      assert.nil! args.state.f_value
      args.state.f_value -= 5.5
      assert.equal! args.state.f_value, -5.5
    end
    
    def test_open_entity_multiply_number args, assert
      assert.nil! args.state.i_value
      args.state.i_value *= 5
      assert.equal! args.state.i_value, 0
    
      assert.nil! args.state.f_value
      args.state.f_value *= 5.5
      assert.equal! args.state.f_value, 0
    end
    
    def test_open_entity_divide_number args, assert
      assert.nil! args.state.i_value
      args.state.i_value /= 5
      assert.equal! args.state.i_value, 0
    
      assert.nil! args.state.f_value
      args.state.f_value /= 5.5
      assert.equal! args.state.f_value, 0
    end
    
    # array
    def test_open_entity_add_array args, assert
      assert.nil! args.state.values
      args.state.values += [:a, :b, :c]
      assert.equal! args.state.values, [:a, :b, :c]
    end
    
    def test_open_entity_subtract_array args, assert
      assert.nil! args.state.values
      args.state.values -= [:a, :b, :c]
      assert.equal! args.state.values, []
    end
    
    def test_open_entity_shovel_array args, assert
      assert.nil! args.state.values
      args.state.values << :a
      assert.equal! args.state.values, [:a]
    end
    
    def test_open_entity_enumerate args, assert
      assert.nil! args.state.values
      args.state.values = args.state.values.map_with_index { |i| i }
      assert.equal! args.state.values, []
    
      assert.nil! args.state.values_2
      args.state.values_2 = args.state.values_2.map { |i| i }
      assert.equal! args.state.values_2, []
    
      assert.nil! args.state.values_3
      args.state.values_3 = args.state.values_3.flat_map { |i| i }
      assert.equal! args.state.values_3, []
    end
    
    # hashes
    def test_open_entity_indexer args, assert
      GTK::Entity.__reset_id__!
      assert.nil! args.state.values
      args.state.values[:test] = :value
      assert.equal! args.state.values.to_s, { entity_id: 1, entity_name: :values, entity_keys_by_ref: {}, test: :value }.to_s
    end
    
    # bug
    def test_open_entity_nil_bug args, assert
      GTK::Entity.__reset_id__!
      args.state.foo.a
      args.state.foo.b
      @hello[:foobar]
      assert.nil! args.state.foo.a, "a was not nil."
      # the line below fails
      # assert.nil! args.state.foo.b, "b was not nil."
    end
    
    

    Unit Tests - numeric_rand_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/numeric_rand_tests.rb
    def test_randomize_int args, assert
      srand(100)
      assert.equal!(10.randomize(:ratio).round(5),         5.43405)
      assert.equal!(10.randomize(:ratio).round(5),         6.71156)
      assert.equal!(10.randomize(:ratio).round(5),         2.78369)
      assert.equal!(10.randomize(:ratio).round(5),         4.12046)
      assert.equal!(10.randomize(:sign),                   10)
      assert.equal!(10.randomize(:sign),                  -10)
      assert.equal!(10.randomize(:sign),                  -10)
      assert.equal!(10.randomize(:sign),                   10)
      assert.equal!(10.randomize(:ratio, :sign).round(5),  1.56711)
      assert.equal!(10.randomize(:ratio, :sign).round(5),  1.86467)
      assert.equal!(10.randomize(:ratio, :sign).round(5), -2.10108)
      assert.equal!(10.randomize(:ratio, :sign).round(5), -4.52740)
      assert.equal!(10.randomize(:int, :sign),             0)
      assert.equal!(10.randomize(:int, :sign),            -3)
      assert.equal!(10.randomize(:int, :sign),            -7)
      assert.equal!(10.randomize(:int, :sign),             6)
      assert.equal!(10.randomize(:int),                    0)
      assert.equal!(10.randomize(:int),                    1)
      assert.equal!(10.randomize(:int),                    9)
      assert.equal!(10.randomize(:int),                    9)
    end
    
    def test_randomize_float args, assert
      srand(100)
      assert.equal!(10.0.randomize(:ratio).round(5),         5.43405)
      assert.equal!(10.0.randomize(:ratio).round(5),         6.71156)
      assert.equal!(10.0.randomize(:ratio).round(5),         2.78369)
      assert.equal!(10.0.randomize(:ratio).round(5),         4.12046)
      assert.equal!(10.4.randomize(:sign),                   10.4)
      assert.equal!(10.4.randomize(:sign),                  -10.4)
      assert.equal!(10.4.randomize(:sign),                  -10.4)
      assert.equal!(10.4.randomize(:sign),                   10.4)
      assert.equal!(10.0.randomize(:ratio, :sign).round(5),  1.56711)
      assert.equal!(10.0.randomize(:ratio, :sign).round(5),  1.86467)
      assert.equal!(10.0.randomize(:ratio, :sign).round(5), -2.10108)
      assert.equal!(10.0.randomize(:ratio, :sign).round(5), -4.52740)
      assert.equal!(10.4.randomize(:int, :sign),             0)
      assert.equal!(10.4.randomize(:int, :sign),            -3)
      assert.equal!(10.4.randomize(:int, :sign),            -7)
      assert.equal!(10.4.randomize(:int, :sign),             6)
      assert.equal!(10.4.randomize(:int),                    0)
      assert.equal!(10.4.randomize(:int),                    1)
      assert.equal!(10.4.randomize(:int),                    9)
      assert.equal!(10.4.randomize(:int),                    9)
    end
    
    def test_ratio_float_alias args, assert
      srand(100)
      assert.equal!(10.0.randomize(:float).round(5),         5.43405)
      assert.equal!(10.0.randomize(:float).round(5),         6.71156)
      assert.equal!(10.0.randomize(:float).round(5),         2.78369)
      assert.equal!(10.0.randomize(:float).round(5),         4.12046)
      assert.equal!(10.4.randomize(:sign),                   10.4)
      assert.equal!(10.4.randomize(:sign),                  -10.4)
      assert.equal!(10.4.randomize(:sign),                  -10.4)
      assert.equal!(10.4.randomize(:sign),                   10.4)
      assert.equal!(10.0.randomize(:float, :sign).round(5),  1.56711)
      assert.equal!(10.0.randomize(:float, :sign).round(5),  1.86467)
      assert.equal!(10.0.randomize(:float, :sign).round(5), -2.10108)
      assert.equal!(10.0.randomize(:float, :sign).round(5), -4.52740)
      assert.equal!(10.4.randomize(:int, :sign),             0)
      assert.equal!(10.4.randomize(:int, :sign),            -3)
      assert.equal!(10.4.randomize(:int, :sign),            -7)
      assert.equal!(10.4.randomize(:int, :sign),             6)
      assert.equal!(10.4.randomize(:int),                    0)
      assert.equal!(10.4.randomize(:int),                    1)
      assert.equal!(10.4.randomize(:int),                    9)
      assert.equal!(10.4.randomize(:int),                    9)
    
      srand(100)
      assert.equal!(10.randomize(:float).round(5),         5.43405)
      assert.equal!(10.randomize(:float).round(5),         6.71156)
      assert.equal!(10.randomize(:float).round(5),         2.78369)
      assert.equal!(10.randomize(:float).round(5),         4.12046)
      assert.equal!(10.randomize(:sign),                   10)
      assert.equal!(10.randomize(:sign),                  -10)
      assert.equal!(10.randomize(:sign),                  -10)
      assert.equal!(10.randomize(:sign),                   10)
      assert.equal!(10.randomize(:float, :sign).round(5),  1.56711)
      assert.equal!(10.randomize(:float, :sign).round(5),  1.86467)
      assert.equal!(10.randomize(:float, :sign).round(5), -2.10108)
      assert.equal!(10.randomize(:float, :sign).round(5), -4.52740)
      assert.equal!(10.randomize(:int, :sign),             0)
      assert.equal!(10.randomize(:int, :sign),            -3)
      assert.equal!(10.randomize(:int, :sign),            -7)
      assert.equal!(10.randomize(:int, :sign),             6)
      assert.equal!(10.randomize(:int),                    0)
      assert.equal!(10.randomize(:int),                    1)
      assert.equal!(10.randomize(:int),                    9)
      assert.equal!(10.randomize(:int),                    9)
    end
    
    def test_numeric_instance_rand_sign args, assert
      srand(100)
      assert.equal!(10.rand(:sign), -10)
      assert.equal!(10.rand(:sign), -10)
      assert.equal!(10.rand(:sign),  10)
      assert.equal!(10.rand(:sign),  10)
      assert.equal!(10.rand(:sign),  10)
      assert.equal!(10.4.rand(:sign), -10.4)
      assert.equal!(10.4.rand(:sign), -10.4)
      assert.equal!(10.4.rand(:sign), 10.4)
      assert.equal!(10.4.rand(:sign), 10.4)
      assert.equal!(10.4.rand(:sign), 10.4)
    end
    
    def test_numeric_self_rand_vs_instance_rand args, assert
      value_comparison = [
        {
          name: "rand for integer",
          klass:    -> { Numeric.rand(10) },
          instance: -> { 10.rand }
        },
        {
          name: "rand for integer from float",
          klass:    -> { Numeric.rand(10) },
          instance: -> { 10.0.rand(:int) }
        },
        {
          name: "rand for float",
          klass:    -> { Numeric.rand(10.0).round(5) },
          instance: -> { 10.0.rand.round(5) }
        },
        {
          name: "rand for float from int",
          klass:    -> { Numeric.rand(10.0).round(5) },
          instance: -> { 10.rand(:ratio).round(5) }
        },
        {
          name: "rand for float from int",
          klass:    -> { Numeric.rand(10.0).round(5) },
          instance: -> { 10.rand(:float).round(5) }
        },
        {
          name: "rand int range (sign)",
          klass:    -> { Numeric.rand(-10..10) },
          instance: -> { 10.rand(:int, :sign) }
        },
        {
          name: "rand int range from float (sign)",
          klass:    -> { Numeric.rand(-10..10) },
          instance: -> { 10.0.rand(:int, :sign) }
        },
        {
          name: "rand ratio range (sign)",
          klass:    -> { Numeric.rand(-10.0..10.0) },
          instance: -> { 10.0.rand(:float, :sign) }
        },
        {
          name: "rand ratio range from int (sign)",
          klass:    -> { Numeric.rand(-10.0..10.0) },
          instance: -> { 10.rand(:float, :sign) }
        },
        {
          name: "rand ratio range (sign)",
          klass:    -> { Numeric.rand(-10.0..10.0) },
          instance: -> { 10.0.rand(:ratio, :sign) }
        },
        {
          name: "rand ratio range from int (sign)",
          klass:    -> { Numeric.rand(-10.0..10.0) },
          instance: -> { 10.rand(:ratio, :sign) }
        },
      ]
    
      value_comparison.each do |h|
        srand(100)
        klass_value = h.klass.call
        srand(100)
        instance_value = h.instance.call
        assert.equal!(klass_value, instance_value, "comparison label: #{h.name}")
      end
    end
    
    

    Unit Tests - object_to_primitive_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/object_to_primitive_tests.rb
    class PlayerSpriteForTest
    end
    
    def test_array_to_sprite args, assert
      array = [[0, 0, 100, 100, "test.png"]].sprites
      puts "No exception was thrown. Sweet!"
    end
    
    def test_class_to_sprite args, assert
      array = [PlayerSprite.new].sprites
      assert.true! array.first.is_a?(PlayerSprite)
      puts "No exception was thrown. Sweet!"
    end
    
    

    Unit Tests - parsing_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/parsing_tests.rb
    def test_parse_json args, assert
      result = GTK.parse_json '{ "name": "John Doe", "aliases": ["JD"] }'
      assert.equal! result, { "name"=>"John Doe", "aliases"=>["JD"] }, "Parsing JSON failed."
    end
    
    def test_parse_xml args, assert
      result = GTK.parse_xml <<-S
    <Person id="100">
      <Name>John Doe</Name>
    </Person>
    S
    
     expected = {:type=>:element,
                 :name=>nil,
                 :children=>[{:type=>:element,
                              :name=>"Person",
                              :children=>[{:type=>:element,
                                           :name=>"Name",
                                           :children=>[{:type=>:content,
                                                        :data=>"John Doe"}]}],
                              :attributes=>{"id"=>"100"}}]}
    
     assert.equal! result, expected, "Parsing xml failed."
    end
    
    

    Unit Tests - pretty_format_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/pretty_format_tests.rb
    def H opts
      opts
    end
    
    def A *opts
      opts
    end
    
    def assert_format args, assert, hash, expected
      actual = args.fn.pretty_format hash
      assert.are_equal! actual, expected
    end
    
    def test_pretty_print args, assert
      # =============================
      # hash with single value
      # =============================
      input = (H first_name: "John")
      expected = <<-S
    {:first_name "John"}
    S
      (assert_format args, assert, input, expected)
    
      # =============================
      # hash with two values
      # =============================
      input = (H first_name: "John", last_name: "Smith")
      expected = <<-S
    {:first_name "John"
     :last_name "Smith"}
    S
    
      (assert_format args, assert, input, expected)
    
      # =============================
      # hash with inner hash
      # =============================
      input = (H first_name: "John",
                 last_name: "Smith",
                 middle_initial: "I",
                 so: (H first_name: "Pocahontas",
                        last_name: "Tsenacommacah"),
                 friends: (A (H first_name: "Side", last_name: "Kick"),
                             (H first_name: "Tim", last_name: "Wizard")))
      expected = <<-S
    {:first_name "John"
     :last_name "Smith"
     :middle_initial "I"
     :so {:first_name "Pocahontas"
          :last_name "Tsenacommacah"}
     :friends [{:first_name "Side"
                :last_name "Kick"}
               {:first_name "Tim"
                :last_name "Wizard"}]}
    S
    
      (assert_format args, assert, input, expected)
    
      # =============================
      # array with one value
      # =============================
      input = (A 1)
      expected = <<-S
    [1]
    S
      (assert_format args, assert, input, expected)
    
      # =============================
      # array with multiple values
      # =============================
      input = (A 1, 2, 3)
      expected = <<-S
    [1
     2
     3]
    S
      (assert_format args, assert, input, expected)
    
      # =============================
      # array with multiple values hashes
      # =============================
      input = (A (H first_name: "Side", last_name: "Kick"),
                 (H first_name: "Tim", last_name: "Wizard"))
      expected = <<-S
    [{:first_name "Side"
      :last_name "Kick"}
     {:first_name "Tim"
      :last_name "Wizard"}]
    S
    
      (assert_format args, assert, input, expected)
    end
    
    def test_nested_nested args, assert
      # =============================
      # nested array in nested hash
      # =============================
      input = (H type: :root,
                 text: "Root",
                 children: (A (H level: 1,
                                 text: "Level 1",
                                 children: (A (H level: 2,
                                                 text: "Level 2",
                                                 children: [])))))
    
      expected = <<-S
    {:type :root
     :text "Root"
     :children [{:level 1
                 :text "Level 1"
                 :children [{:level 2
                             :text "Level 2"
                             :children []}]}]}
    
    S
    
      (assert_format args, assert, input, expected)
    end
    
    def test_scene args, assert
      script = <<-S
    * Scene 1
    ** Narrator
    They say happy endings don't exist.
    ** Narrator
    They say true love is a lie.
    S
      input = parse_org args, script
      puts (args.fn.pretty_format input)
    end
    
    

    Unit Tests - require_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/require_tests.rb
    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
    
    

    Unit Tests - serialize_deserialize_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/serialize_deserialize_tests.rb
    def assert_hash_strings! assert, string_1, string_2
      Kernel.eval("$assert_hash_string_1 = #{string_1}")
      Kernel.eval("$assert_hash_string_2 = #{string_2}")
      assert.equal! $assert_hash_string_1, $assert_hash_string_2
    end
    
    def test_serialize args, assert
      args.state.player_one = "test"
      result = GTK.serialize_state args.state
      assert_hash_strings! assert, result, "{:entity_id=>1, :entity_keys_by_ref=>{}, :tick_count=>-1, :player_one=>\"test\"}"
    
      GTK.write_file 'state.txt', ''
      result = GTK.serialize_state 'state.txt', args.state
      assert_hash_strings! assert, result, "{:entity_id=>1, :entity_keys_by_ref=>{}, :tick_count=>-1, :player_one=>\"test\"}"
    end
    
    def test_deserialize args, assert
      result = GTK.deserialize_state '{:entity_id=>3, :tick_count=>-1, :player_one=>"test"}'
      assert.equal! result.player_one, "test"
    
      GTK.write_file 'state.txt',  '{:entity_id=>3, :tick_count=>-1, :player_one=>"test"}'
      result = GTK.deserialize_state 'state.txt'
      assert.equal! result.player_one, "test"
    end
    
    def test_very_large_serialization args, assert
      GTK.write_file("logs/log.txt", "")
      size = 3000
      size.map_with_index do |i|
        args.state.send("k#{i}=".to_sym, i)
      end
    
      result = GTK.serialize_state args.state
      assert.true! $serialize_state_serialization_too_large
    end
    
    def test_strict_entity_serialization args, assert
      args.state.player_one = args.state.new_entity(:player, name: "Ryu")
      args.state.player_two = args.state.new_entity_strict(:player_strict, name: "Ken")
    
      serialized_state = GTK.serialize_state args.state
      assert_hash_strings! assert, serialized_state, '{:entity_id=>1, :entity_keys_by_ref=>{}, :tick_count=>-1, :player_one=>{:entity_id=>3, :entity_name=>:player, :entity_keys_by_ref=>{}, :entity_type=>:player, :created_at=>-1, :global_created_at=>-1, :name=>"Ryu"}, :player_two=>{:entity_id=>5, :entity_name=>:player_strict, :entity_type=>:player_strict, :created_at=>-1, :global_created_at_elapsed=>-1, :entity_strict=>true, :entity_keys_by_ref=>{}, :name=>"Ken"}}'
    
      deserialize_state = GTK.deserialize_state serialized_state
    
      assert.equal! args.state.player_one.name, deserialize_state.player_one.name
      assert.true! args.state.player_one.is_a? GTK::OpenEntity
    
      assert.equal! args.state.player_two.name, deserialize_state.player_two.name
      assert.true! args.state.player_two.is_a? GTK::StrictEntity
    end
    
    def test_strict_entity_serialization_with_nil args, assert
      args.state.player_one = args.state.new_entity(:player, name: "Ryu")
      args.state.player_two = args.state.new_entity_strict(:player_strict, name: "Ken", blood_type: nil)
    
      serialized_state = GTK.serialize_state args.state
      assert_hash_strings! assert, serialized_state, '{:entity_id=>1, :entity_keys_by_ref=>{}, :tick_count=>-1, :player_one=>{:entity_id=>3, :entity_name=>:player, :entity_keys_by_ref=>{}, :entity_type=>:player, :created_at=>-1, :global_created_at=>-1, :name=>"Ryu"}, :player_two=>{:entity_name=>:player_strict, :global_created_at_elapsed=>-1, :created_at=>-1, :blood_type=>nil, :name=>"Ken", :entity_type=>:player_strict, :entity_strict=>true, :entity_keys_by_ref=>{}, :entity_id=>4}}'
    
      deserialized_state = GTK.deserialize_state serialized_state
    
      assert.equal! args.state.player_one.name, deserialized_state.player_one.name
      assert.true! args.state.player_one.is_a? GTK::OpenEntity
    
      assert.equal! args.state.player_two.name, deserialized_state.player_two.name
      assert.equal! args.state.player_two.blood_type, deserialized_state.player_two.blood_type
      assert.equal! deserialized_state.player_two.blood_type, nil
      assert.true! args.state.player_two.is_a? GTK::StrictEntity
    
      deserialized_state.player_two.blood_type = :O
      assert.equal! deserialized_state.player_two.blood_type, :O
    end
    
    def test_multiple_strict_entities args, assert
      args.state.player = args.state.new_entity_strict(:player_one, name: "Ryu")
      args.state.enemy = args.state.new_entity_strict(:enemy, name: "Bison", other_property: 'extra mean')
    
      serialized_state = GTK.serialize_state args.state
    
      deserialized_state = GTK.deserialize_state serialized_state
    
      assert.equal! deserialized_state.player.name, "Ryu"
      assert.equal! deserialized_state.enemy.other_property, "extra mean"
    end
    
    def test_by_reference_state args, assert
      # TODO
      # args.state.a = args.state.new_entity(:person, name: "Jane Doe")
      # args.state.b = args.state.a
      # assert.equal! args.state.a.object_id, args.state.b.object_id
      # serialized_state = GTK.serialize_state args.state
    
      # deserialized_state = GTK.deserialize_state serialized_state
      # assert.equal! deserialized_state.a.object_id, deserialized_state.b.object_id
    end
    
    def test_by_reference_state_strict_entities args, assert
      args.state.strict_entity = args.state.new_entity_strict(:couple) do |e|
        e.one = args.state.new_entity_strict(:person, name: "Jane")
        e.two = e.one
      end
      assert.equal! args.state.strict_entity.one, args.state.strict_entity.two
      serialized_state = GTK.serialize_state args.state
    
      deserialized_state = GTK.deserialize_state serialized_state
      assert.equal! deserialized_state.strict_entity.one, deserialized_state.strict_entity.two
    end
    
    def test_serialization_does_not_mix_up_zero_and_true args, assert
      args.state.enemy.evil = true
      args.state.enemy.hp = 0
      serialized = GTK.serialize_state args.state.enemy
    
      deserialized = GTK.deserialize_state serialized
    
      assert.equal! deserialized.hp, 0,
                    "Value should have been deserialized as 0, but was #{deserialized.hp}"
      assert.equal! deserialized.evil, true,
                    "Value should have been deserialized as true, but was #{deserialized.evil}"
    end
    
    

    Unit Tests - state_serialization_experimental_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/state_serialization_experimental_tests.rb
    MAX_CODE_GEN_LENGTH = 50
    
    # NOTE: This is experimental/advanced stuff.
    def needs_partitioning? target
      target[:value].to_s.length > MAX_CODE_GEN_LENGTH
    end
    
    def partition target
      return [] unless needs_partitioning? target
      if target[:value].is_a? GTK::OpenEntity
        target[:value] = target[:value].hash
      end
    
      results = []
      idx = 0
      left, right = target[:value].partition do
        idx += 1
        idx.even?
      end
      left, right = Hash[left], Hash[right]
      left = { value: left }
      right = { value: right}
      [left, right]
    end
    
    def add_partition target, path, aggregate, final_result
      partitions = partition target
      partitions.each do |part|
        if needs_partitioning? part
          if part[:value].keys.length == 1
            first_key = part[:value].keys[0]
            new_part = { value: part[:value][first_key] }
            path.push first_key
            add_partition new_part, path, aggregate, final_result
            path.pop
          else
            add_partition part, path, aggregate, final_result
          end
        else
          final_result << { value: { __path__: [*path] } }
          final_result << { value: part[:value] }
        end
      end
    end
    
    def state_to_string state
      parts_queue = []
      final_queue = []
      add_partition({ value: state.hash },
                    [],
                    parts_queue,
                    final_queue)
      final_queue.reject {|i| i[:value].keys.length == 0}.map do |i|
        i[:value].to_s
      end.join("\n#==================================================#\n")
    end
    
    def state_from_string string
      Kernel.eval("$load_data = {}")
      lines = string.split("\n#==================================================#\n")
      lines.each do |l|
        puts "todo: #{l}"
      end
    
      GTK::OpenEntity.parse_from_hash $load_data
    end
    
    def test_save_and_load args, assert
      args.state.item_1.name = "Jane"
      string = state_to_string args.state
      state = state_from_string string
      assert.equal! args.state.item_1.name, state.item_1.name
    end
    
    def test_save_and_load_big args, assert
      size = 1000
      size.map_with_index do |i|
        args.state.send("k#{i}=".to_sym, i)
      end
    
      string = state_to_string args.state
      state = state_from_string string
      size.map_with_index do |i|
        assert.equal! args.state.send("k#{i}".to_sym), state.send("k#{i}".to_sym)
        assert.equal! args.state.send("k#{i}".to_sym), i
        assert.equal! state.send("k#{i}".to_sym), i
      end
    end
    
    def test_save_and_load_big_nested args, assert
      args.state.player_one.friend.nested_hash.k0 = 0
      args.state.player_one.friend.nested_hash.k1 = 1
      args.state.player_one.friend.nested_hash.k2 = 2
      args.state.player_one.friend.nested_hash.k3 = 3
      args.state.player_one.friend.nested_hash.k4 = 4
      args.state.player_one.friend.nested_hash.k5 = 5
      args.state.player_one.friend.nested_hash.k6 = 6
      args.state.player_one.friend.nested_hash.k7 = 7
      args.state.player_one.friend.nested_hash.k8 = 8
      args.state.player_one.friend.nested_hash.k9 = 9
      string = state_to_string args.state
      state = state_from_string string
    end
    
    

    Unit Tests - string_split_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/string_split_tests.rb
    def assert_string_split args, assert, entry
      desc = []
      desc << ""
      desc << "* Desc"
      desc << "  contents: #{entry.contents.inspect}"
    
      if entry.key?(:sep)
        desc << "  sep: #{entry.sep.inspect}"
      else
        desc << "  sep: not given"
      end
    
      if entry.key?(:limit)
        desc << "  limit: #{entry.limit.inspect}"
      else
        desc << "  limit: not given"
      end
    
      desc << "* Results"
      if entry.key?(:sep) && entry.key?(:limit)
        expected = entry.contents.__original_split__(entry.sep, entry.limit)
        actual = String.split(entry.contents, entry.sep, entry.limit)
        desc << "  String#split:  #{expected}"
        desc << "  String::split: #{actual}"
        assert.equal! expected, actual, desc.join("\n")
      elsif entry.key?(:sep)
        expected = entry.contents.__original_split__(entry.sep)
        actual = String.split(entry.contents, entry.sep)
        desc << "  String#split:  #{expected}"
        desc << "  String::split: #{actual}"
        assert.equal! expected, actual, desc.join("\n")
      else
        expected = entry.contents.__original_split__
        actual = String.split(entry.contents)
        desc << "  String#split:  #{expected}"
        desc << "  String::split: #{actual}"
        assert.equal! expected, actual, desc.join("\n")
      end
    end
    
    def test_string_split_empty_entries args, assert
      [
        {
          contents: ",",
          sep: ","
        },
        {
          contents: ",,,,,a",
          sep: ","
        },
        {
          contents: ",a",
          sep: ","
        },
        {
          contents: ",aaaa",
          sep: ","
        },
        {
          contents: ",a",
          sep: ",",
          limit: 2
        },
        {
          contents: "aaa,",
          sep: ",",
          limit: 1
        },
        {
          contents: ",,,,",
          sep: ","
        },
        {
          contents: "a,,b",
          sep: ",",
          limit: 2
        },
        {
          contents: ",a",
          sep: ",",
          limit: 2
        },
        {
          contents: "a,",
          sep: ",",
          limit: 1
        },
        {
          contents: "a,,,",
          sep: ",",
          limit: 2
        },
        {
          contents: ",,,,",
          sep: ",",
          limit: 2
        },
      ].each do |h|
        assert_string_split args, assert, h
      end
    end
    
    def test_string_split args, assert
      [
        {
          contents: "Hello Beautiful World",
        },
        {
          contents: "one,two,three",
          sep: ","
        },
        {
          contents: "Hello Beautiful World",
          sep: " ",
          limit: 2
        },
        {
          contents: "one,,three",
          sep: ","
        },
        {
          contents: "hello",
          sep: ""
        },
        {
          contents: "1,2,3,4,5",
          sep: ",",
          limit: 3
        },
        {
          contents: "1,2,3,4,5",
          sep: ",",
          limit: 1
        },
        {
          contents: "1,2,3,4,5",
          sep: ",",
          limit: 0
        },
        {
          contents: "1,2,3,4,5",
          sep: ",",
          limit: -1
        },
        {
          contents: "",
          sep: ","
        },
        {
          contents: "846,360,25,50,orange,1,sprites/bricks/orange_brick.png,v",
          sep: ","
        },
        {
          contents: "hello",
        },
        {
          contents: "hello",
          sep: ","
        },
        {
          contents: "hello",
          sep: ""
        },
        {
          contents: "a,,b",
          sep: ","
        },
        {
          contents: "a,",
          sep: ","
        },
        {
          contents: "aaa,",
          sep: ",",
          limit: 2
        },
        {
          contents: "aaa,",
          sep: ",",
          limit: 0
        },
        {
          contents: "",
          sep: ",",
          limit: 2
        },
        {
          contents: "846,360,25,50,orange,1,sprites/bricks/orange_brick.png,v",
          sep: ",",
          limit: 2
        },
        {
          contents: "hello",
          sep: ",",
          limit: 2
        },
        {
          contents: "hello",
          sep: "",
          limit: 2
        },
        {
          contents: "aa",
          sep: "",
          limit: 2
        },
        {
          contents: ",",
          sep: ",",
          limit: 0
        },
        {
          contents: ",",
          sep: ",",
          limit: 1
        },
        {
          contents: ",",
          sep: ",",
          limit: 2
        },
        {
          contents: ",",
          sep: ",",
          limit: 3
        },
      ].each do |h|
        assert_string_split args, assert, h
      end
    end
    
    def test_string_split_benchmark args, assert
      contents = "846,360,25,50,orange,1,sprites/bricks/orange_brick.png,v"
    
      GTK.benchmark seconds: 1, # number of seconds to run each experiment
                                  # label for experiment
                                  split: -> () {
                      # experiment body
                      contents.split ","
                    },
                                  # label for experiment
                                  split_new: -> () {
                      # experiment body
                      String.split(contents, ",")
                    }
    
      assert.ok!
    end
    
    

    Unit Tests - suggest_autocompletion_tests.rb link

    # ./samples/10_advanced_debugging/03_unit_tests/suggest_autocompletion_tests.rb
    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
      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
    
    

    Http link

    Retrieve Images - main.rb link

    # ./samples/11_http/01_retrieve_images/app/main.rb
    GTK.register_cvar 'app.warn_seconds', "seconds to wait before starting", :uint, 6
    
    def tick args
      args.outputs.background_color = [0, 0, 0]
    
      args.state.download_debounce ||= 0   # start immediately, reset to non zero later.
      args.state.photos ||= []
      if args.state.photos.length > 300
        args.state.photos.pop_front
      end
    
      # Show a warning at the start.
      args.state.warning_debounce ||= args.cvars['app.warn_seconds'].value * 60
      if args.state.warning_debounce > 0
        args.state.warning_debounce -= 1
        args.outputs.labels << { x: 640, y: 600, text: "This app shows random images from the Internet.", size_enum: 10, alignment_enum: 1, r: 255, g: 255, b: 255 }
        args.outputs.labels << { x: 640, y: 500, text: "Quit in the next few seconds if this is a problem.", size_enum: 10, alignment_enum: 1, r: 255, g: 255, b: 255 }
        args.outputs.labels << { x: 640, y: 350, text: "#{(args.state.warning_debounce / 60.0).to_i}", size_enum: 10, alignment_enum: 1, r: 255, g: 255, b: 255 }
        return
      end
    
      # Put a little pause between each download.
      if args.state.download.nil?
        if args.state.download_debounce > 0
          args.state.download_debounce -= 1
        else
          args.state.download = GTK.http_get 'https://picsum.photos/200/300.jpg'
        end
      end
    
      if !args.state.download.nil?
        if args.state.download[:complete]
          if args.state.download[:http_response_code] == 200
            fname = "sprites/#{args.state.photos.length}.jpg"
            GTK.write_file fname, args.state.download[:response_data]
            args.state.photos << { x: Numeric.rand(100..1180),
                                   y: Numeric.rand(150..570),
                                   path: fname,
                                   angle: Numeric.rand(-40..40) }
          end
          args.state.download = nil
          args.state.download_debounce = Numeric.rand(30..90)
        end
      end
    
      # draw any downloaded photos...
      args.state.photos.each { |i|
        args.outputs.primitives << { x: i.x, y: i.y, w: 200, h: 300, path: i.path, angle: i.angle, anchor_x: 0.5, anchor_y: 0.5 }
      }
    
      # Draw a download progress bar...
      args.outputs.primitives << { x: 0, y: 0, w: 1280, h: 30, r: 0, g: 0, b: 0, a: 255, path: :solid }
      if !args.state.download.nil?
        br = args.state.download[:response_read]
        total = args.state.download[:response_total]
        if total != 0
          pct = br.to_f / total.to_f
          args.outputs.primitives << { x: 0, y: 0, w: 1280 * pct, h: 30, r: 0, g: 0, b: 255, a: 255, path: :solid }
        end
      end
    end
    
    

    In Game Web Server Http Get - main.rb link

    # ./samples/11_http/02_in_game_web_server_http_get/app/main.rb
    def tick args
      args.state.reqnum ||= 0
      # by default the embedded webserver is disabled in a production build
      # to enable the http server in a production build you need to:
      # - update metadata/cvars.txt
      # - manually start the server up with enable_in_prod set to true:
      GTK.start_server! port: 3000, enable_in_prod: true
      args.outputs.background_color = [0, 0, 0]
      args.outputs.labels << { x: 640,
                               y: 360,
                               text: "Point your web browser at http://localhost:#{args.state.port}/",
                               size_px: 30,
                               anchor_x: 0.5,
                               anchor_y: 0.5 }
    
      args.outputs.labels << { x: 640,
                               y: 360,
                               text: "See metadata/cvars.txt for webserer configuration requirements.",
                               size_px: 30,
                               anchor_x: 0.5,
                               anchor_y: 1.5 }
    
      if Kernel.tick_count == 1
        GTK.openurl "http://localhost:3000"
      end
    
      args.inputs.http_requests.each { |req|
        puts("METHOD: #{req.method}");
        puts("URI: #{req.uri}");
        puts("HEADERS:");
        req.headers.each { |k,v| puts("  #{k}: #{v}") }
    
        if (req.uri == '/')
          # headers and body can be nil if you don't care about them.
          # If you don't set the Content-Type, it will default to
          #  "text/html; charset=utf-8".
          # Don't set Content-Length; we'll ignore it and calculate it for you
          args.state.reqnum += 1
          req.respond 200, "<html><head><title>hello</title></head><body><h1>This #{req.method} was request number #{args.state.reqnum}!</h1></body></html>\n", { 'X-DRGTK-header' => 'Powered by DragonRuby!' }
        else
          req.reject
        end
      }
    end
    
    

    In Game Web Server Http Post - main.rb link

    # ./samples/11_http/03_in_game_web_server_http_post/app/main.rb
    def tick args
      # by default the embedded webserver is disabled in a production build
      # to enable the http server in a production build you need to:
      # - update metadata/cvars.txt
      # - manually start the server up with enable_in_prod set to true:
      GTK.start_server! port: $cvars["webserver.port"].value, enable_in_prod: true
    
      # defaults
      args.state.post_button      = Layout.rect(row: 0, col: 0, w: 5, h: 1).merge(text: "execute http_post")
      args.state.post_body_button = Layout.rect(row: 1, col: 0, w: 5, h: 1).merge(text: "execute http_post_body")
      args.state.request_to_s ||= ""
      args.state.request_body ||= ""
    
      # render
      args.state.post_button.yield_self do |b|
        args.outputs.borders << b
        args.outputs.labels  << b.merge(text: b.text,
                                        y:    b.y + 30,
                                        x:    b.x + 10)
      end
    
      args.state.post_body_button.yield_self do |b|
        args.outputs.borders << b
        args.outputs.labels  << b.merge(text: b.text,
                                        y:    b.y + 30,
                                        x:    b.x + 10)
      end
    
      draw_label args, 0,  6, "Request:", args.state.request_to_s
      draw_label args, 0, 14, "Request Body Unaltered:", args.state.request_body
    
      # input
      if args.inputs.mouse.click
        # ============= HTTP_POST =============
        if (args.inputs.mouse.inside_rect? args.state.post_button)
          # ========= DATA TO SEND ===========
          form_fields = { "userId" => "#{Time.now.to_i}" }
          # ==================================
    
          GTK.http_post "http://localhost:9001/testing",
                             form_fields,
                             ["Content-Type: application/x-www-form-urlencoded"]
    
          GTK.notify! "http_post"
        end
    
        # ============= HTTP_POST_BODY =============
        if (args.inputs.mouse.inside_rect? args.state.post_body_button)
          # =========== DATA TO SEND ==============
          json = "{ \"userId\": \"#{Time.now.to_i}\"}"
          # ==================================
    
          GTK.http_post_body "http://localhost:9001/testing",
                                  json,
                                  ["Content-Type: application/json", "Content-Length: #{json.length}"]
    
          GTK.notify! "http_post_body"
        end
      end
    
      # calc
      args.inputs.http_requests.each do |r|
        puts "#{r}"
        if r.uri == "/testing"
          puts r
          args.state.request_to_s = "#{r}"
          args.state.request_body = r.raw_body
          r.respond 200, "ok"
        end
      end
    end
    
    def draw_label args, row, col, header, text
      label_pos = Layout.rect(row: row, col: col, w: 0, h: 0)
      args.outputs.labels << "#{header}\n\n#{text}".wrapped_lines(80).map_with_index do |l, i|
        { x: label_pos.x, y: label_pos.y - (i * 15), text: l, size_enum: -2 }
      end
    end
    
    

    C Extensions link

    Basics - main.rb link

    # ./samples/12_c_extensions/01_basics/app/main.rb
    GTK.ffi_misc.gtk_dlopen("ext")
    include FFI::CExt
    
    def tick args
      args.outputs.labels  << [640, 500, "mouse.x = #{args.mouse.x.to_i}", 5, 1]
      args.outputs.labels  << [640, 460, "square(mouse.x) = #{square(args.mouse.x.to_i)}", 5, 1]
      args.outputs.labels  << [640, 420, "mouse.y = #{args.mouse.y.to_i}", 5, 1]
      args.outputs.labels  << [640, 380, "square(mouse.y) = #{square(args.mouse.y.to_i)}", 5, 1]
    end
    
    
    

    Intermediate - main.rb link

    # ./samples/12_c_extensions/02_intermediate/app/main.rb
    GTK.ffi_misc.gtk_dlopen("ext")
    include FFI::RE
    
    def split_words(input)
      words = []
      last = IntPointer.new
      re = re_compile("\\w+")
      first = re_matchp(re, input, last)
      while first != -1
        words << input.slice(first, last.value)
        input = input.slice(last.value + first, input.length)
        first = re_matchp(re, input, last)
      end
      words
    end
    
    def tick args
      args.outputs.labels  << [640, 500, split_words("hello, dragonriders!").join(' '), 5, 1]
    end
    
    

    Native Pixel Arrays - main.rb link

    # ./samples/12_c_extensions/03_native_pixel_arrays/app/main.rb
    GTK.ffi_misc.gtk_dlopen("ext")
    include FFI::CExt
    
    def tick args
      args.state.rotation ||= 0
    
      update_scanner_texture   # this calls into a C extension!
    
      # New/changed pixel arrays get uploaded to the GPU before we render
      #  anything. At that point, they can be scaled, rotated, and otherwise
      #  used like any other sprite.
      w = 100
      h = 100
      x = (1280 - w) / 2
      y = (720 - h) / 2
      args.outputs.background_color = [64, 0, 128]
      args.outputs.primitives << [x, y, w, h, :scanner, args.state.rotation].sprite
      args.state.rotation += 1
    
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    
    

    Handcrafted Extension - main.rb link

    # ./samples/12_c_extensions/04_handcrafted_extension/app/main.rb
    GTK.ffi_misc.gtk_dlopen("ext")
    include FFI::CExt
    
    puts Adder.new.add_all(1, 2, 3, [4, 5, 6.0])
    
    def tick args
    end
    
    

    Handcrafted Extension Advanced - main.rb link

    # ./samples/12_c_extensions/04_handcrafted_extension_advanced/app/main.rb
    def build_c_extension
      v = Time.now.to_i
      GTK.exec("cd ./mygame && (env SUFFIX=#{v} sh ./pre.sh 2>&1 | tee ./build-results.txt)")
      build_output = GTK.read_file("build-results.txt")
      {
        dll_name: "ext_#{v}",
        build_output: build_output
      }
    end
    
    def tick args
      # sets console command when sample app initially opens
      if Kernel.global_tick_count == 0
        results = build_c_extension
        dll = results.dll_name
        GTK.dlopen(dll)
        puts ""
        puts ""
        puts "========================================================="
        puts "* INFO: Static Sprites, Classes, Draw Override"
        puts "* INFO: Please specify the number of sprites to render."
        GTK.console.set_command "reset_with count: 100"
      end
    
      args.state.star_count ||= 0
    
      # init
      if Kernel.tick_count == 0
        args.state.stars = args.state.star_count.map { |i| Star.new }
        args.outputs.static_sprites << args.state.stars
      end
    
      # render framerate
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << GTK.current_framerate_primitives
    end
    
    # resets game, and assigns star count given by user
    def reset_with count: count
      GTK.reset
      GTK.args.state.star_count = count
    end
    
    GTK.reset
    
    

    Ios main.rb link

    # ./samples/12_c_extensions/05_ios_c_extensions/app/main.rb
    # NOTE: This is assumed to be executed with mygame as the root directory
    #       you'll need to copy this code over there to try it out.
    
    # Steps:
    # 1. Create ext.h and ext.m
    # 2. Create Info.plist file
    # 3. Add before_create_payload to IOSWizard (which does the following):
    #    a. run ./dragonruby-bind against C Extension and update implementation file
    #    b. create output location for iOS Framework
    #    c. compile C extension into Framework
    #    d. copy framework to Payload directory and Sign
    # 4. Run $wizards.ios.start env: (:prod|:dev|:hotload) to create ipa
    # 5. Invoke GTK.dlopen giving the name of the C Extensions (~1s to load).
    # 6. Invoke methods as needed.
    
    # ===================================================
    # before_create_payload iOS Wizard
    # ===================================================
    class IOSWizard < Wizard
      def before_create_payload
        puts "* INFO - before_create_payload"
    
        # invoke ./dragonruby-bind
        sh "./dragonruby-bind --output=mygame/ext-bind.m mygame/ext.h"
    
        # update generated implementation file
        contents = GTK.read_file "ext-bind.m"
        contents = contents.gsub("#include \"mygame/ext.h\"", "#include \"mygame/ext.h\"\n#include \"mygame/ext.m\"")
        puts contents
    
        GTK.write_file "ext-bind.m", contents
    
        # create output location
        sh "rm -rf ./mygame/native/ios-device/ext.framework"
        sh "mkdir -p ./mygame/native/ios-device/ext.framework"
    
        # compile C extension into framework
        sh <<-S
    clang -I. -I./mruby/include -I./include -o "./mygame/native/ios-device/ext.framework/ext" \\
          -arch arm64 -dynamiclib -isysroot "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk" \\
          -install_name @rpath/ext.framework/ext \\
          -fembed-bitcode -Xlinker -rpath -Xlinker @loader_path/Frameworks -dead_strip -Xlinker -rpath -fobjc-arc -fobjc-link-runtime \\
          -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks \\
          -miphoneos-version-min=10.3 -Wl,-no_pie -licucore -stdlib=libc++ \\
          -framework CFNetwork -framework UIKit -framework Foundation \\
          ./mygame/ext-bind.m
    S
    
        # stage extension
        sh "cp ./mygame/native/ios-device/Info.plist ./mygame/native/ios-device/ext.framework/Info.plist"
        sh "mkdir -p \"#{app_path}/Frameworks/ext.framework/\""
        sh "cp -r \"#{root_folder}/native/ios-device/ext.framework/\" \"#{app_path}/Frameworks/ext.framework/\""
    
        # sign
        sh <<-S
    CODESIGN_ALLOCATE=#{codesign_allocate_path} #{codesign_path} \\
                                                -f -s \"#{certificate_name}\" \\
                                                \"#{app_path}/Frameworks/ext.framework/ext\"
    S
      end
    end
    
    def tick args
      if Kernel.tick_count == 60 && GTK.platform?(:ios)
        GTK.dlopen 'ext'
        include FFI::CExt
        puts "the results of hello world are:"
        puts hello_world()
        GTK.console.show
      end
    end
    
    

    Handcrafted Mac Extension - main.rb link

    # ./samples/12_c_extensions/06_handcrafted_mac_extension/app/main.rb
    def boot args
      GTK.dlopen 'ext'
    end
    
    def tick args
      if Kernel.tick_count == 0
        hello = Hello.new
        puts hello.get_message("John Doe")
        bye = Bye.new
        puts bye.get_message("John Doe")
      end
    end
    
    

    Handcrafted Steam Extensions - main.rb link

    # ./samples/12_c_extensions/07_handcrafted_steam_extensions/app/main.rb
    def boot args
      GTK.dlopen 'ext'
      $steam = Steam.new
      $steam.init_api
    end
    
    def tick args
      if Kernel.tick_count == 0
        puts "Retrieving user name."
        puts $steam.get_user_name
      end
    end
    
    

    Handcrafted Android Extension - main.rb link

    # ./samples/12_c_extensions/08_handcrafted_android_extension/app/main.rb
    def boot args
    end
    
    def tick args
      if args.inputs.mouse.click && !@dl_opened
        GTK.dlopen("ext")
        @dl_opened = true
      elsif args.inputs.mouse.click
        h = UserDefaults.new
        args.state.user_defaults_exist = true
      end
    
      if !args.state.user_defaults_exist
        args.outputs.labels << { x: 640, y: 360, text: "click to verify C extension", anchor_x: 0.5, anchor_y: 0.5 }
      else
        args.outputs.labels << { x: 640, y: 360, text: "C extension successfully created", anchor_x: 0.5, anchor_y: 0.5 }
      end
    end
    
    

    Handcrafted Threads - main.rb link

    # ./samples/12_c_extensions/09_handcrafted_threads/app/main.rb
    def boot args
      GTK.dlopen "ext"
    end
    
    def tick args
      args.state.mode ||= :stopped
    
      if args.inputs.keyboard.key_down.enter
        if args.state.mode == :stopped
          args.state.mode = :running
          Worker.start_printing
        else
          args.state.mode = :stopped
          Worker.stop_printing
        end
      end
    
      args.outputs.labels << {
        x: 640,
        y: 680,
        text: "Press Enter to start/stop printing",
        anchor_x: 0.5,
        anchor_y: 0.5,
      }
    
      args.outputs.labels << {
        x: 640,
        y: 360,
        text: "Printing is #{args.state.mode}",
        anchor_x: 0.5,
        anchor_y: 0.5,
      }
    end
    
    

    Path Finding Algorithms link

    Breadth First Search - main.rb link

    # ./samples/13_path_finding_algorithms/01_breadth_first_search/app/main.rb
    # Contributors outside of DragonRuby who also hold Copyright:
    # - Sujay Vadlakonda: https://github.com/sujayvadlakonda
    
    # 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   = { x: 450, y: 600, w: 50,  h: 50 }
        args.state.buttons.center = { x: 500, y: 600, w: 200, h: 50 }
        args.state.buttons.right  = { x: 700, y: 600, w: 50,  h: 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.merge(gray)
        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 << { x: label_x, y: label_y, text: '<' }
      end
    
      def render_center_button
        # Draws the gray button, and a black border
        # The border separates the buttons visually
        outputs.solids  << buttons.center.merge(gray)
        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 << { x: label_x, y: label_y, text: 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.merge(gray)
        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 << { x: label_x, y: label_y, text: ">" }
      end
    
      # Draws the slider so the user can move it and see the progress of the search
      def render_slider
        # Using a solid instead of a line, hides the line under the circle of the slider
        # Draws the line
        outputs.solids << {
          x: slider.x,
          y: slider.y,
          w: slider.w,
          h: 2
        }
        # 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)
        outputs.sprites << {
          x: circle_x,
          y: circle_y,
          w: 37,
          h: 37,
          path: 'circle-white.png'
        }
      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
        rect = { x: 0, y: 0, w: grid.width, h: grid.height }
        rect = rect.transform_values { |v| v * grid.cell_size }
        outputs.solids << rect.merge(unvisited_color)
      end
    
      # Draws grid lines to show the division of the grid into cells
      def render_grid_lines
        outputs.lines << (0..grid.width).map { |x| vertical_line(x) }
        outputs.lines << (0..grid.height).map { |y| horizontal_line(y) }
      end
    
      # Easy way to draw vertical lines given an index
      def vertical_line x
        line = { x: x, y: 0, w: 0, h: grid.height }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Easy way to draw horizontal lines given an index
      def horizontal_line y
        line = { x: 0, y: y, w: grid.width, h: 0 }
        line.transform_values { |v| v * grid.cell_size }
      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|
          render_cell cell, frontier_color
        end
      end
    
      # Draws the walls
      def render_walls
        outputs.solids << state.walls.map do |wall|
          render_cell 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|
          render_cell cell, visited_color
        end
      end
    
      # Renders the star
      def render_star
        outputs.sprites << render_cell(state.star, { path: 'star.png' })
      end
    
      def render_cell cell, attrs
        {
          x: cell.x * grid.cell_size,
          y: cell.y * grid.cell_size,
          w: grid.cell_size,
          h: grid.cell_size
        }.merge attrs
      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.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.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.key?(neighbor) || state.walls.key?(neighbor)
              # Add them to the frontier and mark them as visited
              state.frontier << neighbor
              state.visited[neighbor] = true
            end
          end
        end
      end
    
    
      # Returns a list of adjacent cells
      # Used to determine what the next cells to be added to the frontier are
      def adjacent_neighbors(cell)
        neighbors = []
    
        neighbors << [cell.x, cell.y + 1] unless cell.y == grid.height - 1
        neighbors << [cell.x + 1, cell.y] unless cell.x == grid.width - 1
        neighbors << [cell.x, cell.y - 1] unless cell.y == 0
        neighbors << [cell.x - 1, cell.y] unless cell.x == 0
    
        neighbors
      end
    
      # When the user grabs the star and puts their cursor to the far right
      # and moves up and down, the star is supposed to move along the grid as well
      # Finding the cell closest to the mouse helps with this
      def cell_closest_to_mouse
        # Closest cell to the mouse
        x = (inputs.mouse.point.x / grid.cell_size).to_i
        y = (inputs.mouse.point.y / grid.cell_size).to_i
        # Bound x and y to the grid
        x = grid.width - 1 if x > grid.width - 1
        y = grid.height - 1 if y > grid.height - 1
        # Return closest cell
        [x, y]
      end
    
      # These methods detect when the buttons are clicked
      def left_button_clicked?
        inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.left)
      end
    
      def center_button_clicked?
        inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.center)
      end
    
      def right_button_clicked?
        inputs.mouse.up && inputs.mouse.point.inside_rect?(buttons.right)
      end
    
      # Signal that the user is going to be moving the slider
      # Is the mouse down on the circle of the slider?
      def slider_clicked?
        circle_x = (slider.x - slider.offset) + (state.anim_steps * slider.spacing)
        circle_y = (slider.y - slider.offset)
        circle_rect = [circle_x, circle_y, 37, 37]
        inputs.mouse.down && inputs.mouse.point.inside_rect?(circle_rect)
      end
    
      # Signal that the user is going to be moving the star
      def star_clicked?
        inputs.mouse.down && inputs.mouse.point.inside_rect?(scale_up(state.star))
      end
    
      # Signal that the user is going to be removing walls
      def wall_clicked?
        inputs.mouse.down && mouse_inside_a_wall?
      end
    
      # Signal that the user is going to be adding walls
      def grid_clicked?
        inputs.mouse.down && mouse_inside_grid?
      end
    
      # Returns whether the mouse is inside of a wall
      # Part of the condition that checks whether the user is removing a wall
      def mouse_inside_a_wall?
        state.walls.each_key do | wall |
          return true if inputs.mouse.point.inside_rect?(scale_up([wall.x, wall.y]))
        end
    
        false
      end
    
      # Returns whether the mouse is inside of a grid
      # Part of the condition that checks whether the user is adding a wall
      def mouse_inside_grid?
        inputs.mouse.point.inside_rect?(scale_up([0, 0, grid.width, grid.height]))
      end
    
      # Light brown
      def unvisited_color
        { r: 221, g: 212, b: 213 }
      end
    
      # Dark Brown
      def visited_color
        { r: 204, g: 191, b: 179 }
      end
    
      # Blue
      def frontier_color
        { r: 103, g: 136, b: 204 }
      end
    
      # Camo Green
      def wall_color
        { r: 134, g: 134, b: 120 }
      end
    
      # Button Background
      def gray
        { r: 190, g: 190, b: 190 }
      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
        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
    
    

    Detailed Breadth First Search - main.rb link

    # ./samples/13_path_finding_algorithms/02_detailed_breadth_first_search/app/main.rb
    # Contributors outside of DragonRuby who also hold Copyright:
    # - Sujay Vadlakonda: https://github.com/sujayvadlakonda
    
    # 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]
    
        # 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]
    
        # 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).merge(unvisited_color)
      end
    
      # Draws grid lines to show the division of the grid into cells
      def render_grid_lines
        outputs.lines << (0..grid.width).map do |x|
          scale_up(vertical_line(x)).merge(grid_line_color)
        end
        outputs.lines << (0..grid.height).map do |y|
          scale_up(horizontal_line(y)).merge(grid_line_color)
        end
      end
    
      # Easy way to get a vertical line given an index
      def vertical_line column
        [column, 0, 0, grid.height]
      end
    
      # Easy way to get a horizontal line given an index
      def horizontal_line row
        [0, row, grid.width, 0]
      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).merge(frontier_color)
        end
      end
    
      # Draws the walls
      def render_walls
        state.walls.each_key do |wall|
          outputs.solids << scale_up(wall).merge(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).merge(visited_color)
        end
      end
    
      # Renders the star
      def render_star
        outputs.sprites << scale_up(state.star).merge({ path: '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).merge(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.key?(neighbor)
            outputs.solids << scale_up(neighbor).merge(highlighter_green)
          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)
        if cell.size == 2
          return {
            x: cell.x * grid.cell_size,
            y: cell.y * grid.cell_size,
            w: grid.cell_size,
            h: grid.cell_size
          }
        else
          return {
            x: cell.x * grid.cell_size,
            y: cell.y * grid.cell_size,
            w: cell.w * grid.cell_size,
            h: cell.h * grid.cell_size
          }
        end
      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.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.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.key?(neighbor) || state.walls.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
        { r: 221, g: 212, b: 213 }
      end
    
      # Black
      def grid_line_color
        { r: 255, g: 255, b: 255 }
      end
    
      # Dark Brown
      def visited_color
        { r: 204, g: 191, b: 179 }
      end
    
      # Blue
      def frontier_color
        { r: 103, g: 136, b: 204 }
      end
    
      # Camo Green
      def wall_color
        { r: 134, g: 134, b: 120 }
      end
    
      # Next frontier to be expanded
      def highlighter_yellow
        { r: 214, g: 231, b: 125 }
      end
    
      # The neighbors of the next frontier to be expanded
      def highlighter_green
        { r: 65, g: 191, b: 127, a: 70 }
      end
    
      # Button background
      def gray
        [190, 190, 190]
      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
        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
    
    

    Breadcrumbs - main.rb link

    # ./samples/13_path_finding_algorithms/03_breadcrumbs/app/main.rb
    # Contributors outside of DragonRuby who also hold Copyright:
    # - Sujay Vadlakonda: https://github.com/sujayvadlakonda
    
    # This program is inspired by https://www.redblobgames.com/pathfinding/a-star/introduction.html
    
    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
              # Point arrow right
              outputs.sprites << scale_up(arrow_cell).merge({ path: 'arrow.png', angle: 0})
            elsif parent.x < child.x # If the parent cell is to the right of the child cell
              outputs.sprites << scale_up(arrow_cell).merge({ path: 'arrow.png', angle: 180})
            elsif parent.y > child.y # If the parent cell is to the right of the child cell
              outputs.sprites << scale_up(arrow_cell).merge({ path: 'arrow.png', angle: 90})
            elsif parent.y < child.y # If the parent cell is to the right of the child cell
              outputs.sprites << scale_up(arrow_cell).merge({ path: 'arrow.png', angle: 270})
            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).merge(unvisited_color)
      end
    
      # Draws grid lines to show the division of the grid into cells
      def render_grid_lines
        outputs.lines << (0..grid.width).map { |x| vertical_line(x) }
        outputs.lines << (0..grid.height).map { |y| horizontal_line(y) }
      end
    
      # Easy way to draw vertical lines given an index
      def vertical_line x
        line = { x: x, y: 0, w: 0, h: grid.height }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Easy way to draw horizontal lines given an index
      def horizontal_line y
        line = { x: 0, y: y, w: grid.width, h: 0 }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Draws the walls on both grids
      def render_walls
        outputs.solids << grid.walls.map do |key, value|
          scale_up(key).merge(wall_color)
        end
      end
    
      # Renders the star on both grids
      def render_star
        outputs.sprites << scale_up(grid.star).merge({ path: 'star.png' })
      end
    
      # Renders the target on both grids
      def render_target
        outputs.sprites << scale_up(grid.target).merge({ path: '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)
        x = cell.x * grid.cell_size
        y = cell.y * grid.cell_size
        w = cell.w.zero? ? grid.cell_size : cell.w * grid.cell_size
        h = cell.h.zero? ? grid.cell_size : cell.h * grid.cell_size
        { x: x, y: y, w: w, h: h }
      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.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.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
        { r: 221, g: 212, b: 213 }
      end
    
      # Camo Green
      def wall_color
        { r: 134, g: 134, b: 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
        GTK.reset
        reset
        return
      end
    
      # Every tick, new args are passed, and the Breadth First Search tick is called
      $breadcrumbs ||= Breadcrumbs.new
      $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
    
    

    Early Exit - main.rb link

    # ./samples/13_path_finding_algorithms/04_early_exit/app/main.rb
    # Contributors outside of DragonRuby who also hold Copyright:
    # - Sujay Vadlakonda: https://github.com/sujayvadlakonda
    
    # 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).merge(unvisited_color)
        outputs.solids << early_exit_scale_up(grid.rect).merge(unvisited_color)
      end
    
      # Draws grid lines to show the division of the grid into cells
      def render_grid_lines
        outputs.lines << (0..grid.width).map { |x| vertical_line(x) }
        outputs.lines << (0..grid.width).map { |x| early_exit_vertical_line(x) }
        outputs.lines << (0..grid.height).map { |y| horizontal_line(y) }
        outputs.lines << (0..grid.height).map { |y| early_exit_horizontal_line(y) }
      end
    
      # Easy way to draw vertical lines given an index
      def vertical_line x
        line = { x: x, y: 0, w: 0, h: grid.height }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Easy way to draw horizontal lines given an index
      def horizontal_line y
        line = { x: 0, y: y, w: grid.width, h: 0 }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Easy way to draw vertical lines given an index
      def early_exit_vertical_line x
        vertical_line(x + grid.width + 1)
      end
    
      # Easy way to draw horizontal lines given an index
      def early_exit_horizontal_line y
        line = { x: grid.width + 1, y: y, w: grid.width, h: 0 }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Draws the walls on both grids
      def render_walls
        state.walls.each_key do |wall|
          outputs.solids << scale_up(wall).merge(wall_color)
          outputs.solids << early_exit_scale_up(wall).merge(wall_color)
        end
      end
    
      # Renders the star on both grids
      def render_star
        outputs.sprites << scale_up(state.star).merge({path: 'star.png'})
        outputs.sprites << early_exit_scale_up(state.star).merge({path: 'star.png'})
      end
    
      # Renders the target on both grids
      def render_target
        outputs.sprites << scale_up(state.target).merge({path: 'target.png'})
        outputs.sprites << early_exit_scale_up(state.target).merge({path: '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).merge(path_color)
            outputs.solids << early_exit_scale_up(cell).merge(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
          heat_color = red.merge({a: alpha })
          outputs.solids << scale_up(visited_cell).merge(heat_color)
        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
          heat_color = red.merge({a: alpha })
          outputs.solids << early_exit_scale_up(visited_cell).merge(heat_color)
        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)
        if cell.size == 2
          return {
            x: cell.x * grid.cell_size,
            y: cell.y * grid.cell_size,
            w: grid.cell_size,
            h: grid.cell_size
          }
        else
          return {
            x: cell.x * grid.cell_size,
            y: cell.y * grid.cell_size,
            w: cell.w * grid.cell_size,
            h: cell.h * grid.cell_size
          }
        end
      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.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.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.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.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.key?(neighbor) || state.walls.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.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]
        { r: 221, g: 212, b: 213 }
      end
    
      # Camo Green
      def wall_color
        { r: 134, g: 134, b: 120 }
      end
    
      # Pastel White
      def path_color
        { r: 231, g: 230, b: 228 }
      end
    
      def red
        { r: 255, g: 0, b: 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
        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
      $early_exit_breadth_first_search.args = args
      $early_exit_breadth_first_search.tick
    end
    
    
    def reset
      $early_exit_breadth_first_search = nil
    end
    
    

    Dijkstra - main.rb link

    # ./samples/13_path_finding_algorithms/05_dijkstra/app/main.rb
    # Contributors outside of DragonRuby who also hold Copyright:
    # - Sujay Vadlakonda: https://github.com/sujayvadlakonda
    
    # 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).merge(unvisited_color)
        outputs.solids << move_and_scale_up(grid.rect).merge(unvisited_color)
      end
    
      # Draws grid lines to show the division of the grid into cells
      def render_grid_lines
        outputs.lines << (0..grid.width).map { |x| vertical_line(x) }
        outputs.lines << (0..grid.width).map { |x| shifted_vertical_line(x) }
        outputs.lines << (0..grid.height).map { |y| horizontal_line(y) }
        outputs.lines << (0..grid.height).map { |y| shifted_horizontal_line(y) }
      end
    
      # A line the size of the grid, multiplied by the cell size for rendering
      def vertical_line x
        line = { x: x, y: 0, w: 0, h: grid.height }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # A line the size of the grid, multiplied by the cell size for rendering
      def horizontal_line y
        line = { x: 0, y: y, w: grid.width, h: 0 }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Translate vertical line by the size of the grid and 1
      def shifted_vertical_line x
        vertical_line(x + grid.width + 1)
      end
    
      # Get horizontal line and shift to the right
      def shifted_horizontal_line y
        line = { x: grid.width + 1, y: y, w: grid.width, h: 0 }
        line.transform_values { |v| v * grid.cell_size }
      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
    
      # 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
          heat_color = red.merge({a: alpha })
          outputs.solids << scale_up(visited_cell).merge(heat_color)
        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 && next_endpoint
            # Draw a path between these two cells
            path = get_path_between(endpoint, next_endpoint)
            outputs.solids << scale_up(path).merge(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
    
      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
          heat_color = red.merge({a: alpha})
          outputs.solids << move_and_scale_up(visited_cell).merge(heat_color)
        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 && next_endpoint
            # Draw a path between them
            path = get_path_between(endpoint, next_endpoint)
            outputs.solids << move_and_scale_up(path).merge(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).merge({path: 'star.png'})
        outputs.sprites << move_and_scale_up(state.star).merge({path: 'star.png'})
      end
    
      # Renders the target on both grids
      def render_target
        outputs.sprites << scale_up(state.target).merge({path: 'target.png'})
        outputs.sprites << move_and_scale_up(state.target).merge({path: 'target.png'})
      end
    
      def render_hills
        state.hills.each_key do |hill|
          outputs.solids << scale_up(hill).merge(hill_color)
          outputs.solids << move_and_scale_up(hill).merge(hill_color)
        end
      end
    
      # Draws the walls on both grids
      def render_walls
        state.walls.each_key do |wall|
          outputs.solids << scale_up(wall).merge(wall_color)
          outputs.solids << move_and_scale_up(wall).merge(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
    
      # 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)
        if cell.size == 2
          return {
            x: cell.x * grid.cell_size,
            y: cell.y * grid.cell_size,
            w: grid.cell_size,
            h: grid.cell_size
          }
        else
          return {
            x: cell.x * grid.cell_size,
            y: cell.y * grid.cell_size,
            w: cell.w * grid.cell_size,
            h: cell.h * grid.cell_size
          }
        end
      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.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.key?(neighbor) || state.walls.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.key?(neighbor) or state.walls.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)
        return 5 if state.hills.key? cell
        1
      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.key?(cell_closest_to_mouse) or state.hills.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.key?(cell_closest_to_mouse2) or state.hills.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.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.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.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.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
        { r: 221, g: 212, b: 213 }
      end
    
      # Camo Green
      def wall_color
        { r: 134, g: 134, b: 120 }
      end
    
      # Pastel White
      def path_color
        { r: 231, g: 230, b: 228 }
      end
    
      def red
        { r: 255, g: 0, b: 0 }
      end
    
      # A Green
      def hill_color
        { r: 139, g: 173, b: 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
        GTK.reset
        reset
        return
      end
    
      # Every tick, new args are passed, and the Dijkstra tick method is called
      $movement_costs ||= Movement_Costs.new
      $movement_costs.args = args
      $movement_costs.tick
    end
    
    
    def reset
      $movement_costs = nil
    end
    
    

    Heuristic - main.rb link

    # ./samples/13_path_finding_algorithms/06_heuristic/app/main.rb
    # Contributors outside of DragonRuby who also hold Copyright:
    # - Sujay Vadlakonda: https://github.com/sujayvadlakonda
    
    # 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     ||= {}
        # 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 state.current_step
          state.current_step = 0
        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).merge(default_color)
    
        outputs.lines << (0..grid.width).map { |x| bfs_vertical_line(x) }
        outputs.lines << (0..grid.height).map { |y| bfs_horizontal_line(y) }
      end
    
      def render_heuristic_grid
        # A large rect the size of the grid
        outputs.solids << heuristic_scale_up(grid.rect).merge(default_color)
    
        outputs.lines << (0..grid.width).map { |x| heuristic_vertical_line(x) }
        outputs.lines << (0..grid.height).map { |y| heuristic_horizontal_line(y) }
      end
    
      # Returns a vertical line for a column of the first grid
      def bfs_vertical_line x
        line = { x: x, y: 0, w: 0, h: grid.height }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Returns a horizontal line for a column of the first grid
      def bfs_horizontal_line y
        line = { x: 0, y: y, w: grid.width, h: 0 }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Returns a vertical line for a column of the second grid
      def heuristic_vertical_line x
        bfs_vertical_line(x + grid.width + 1)
      end
    
      # Returns a horizontal line for a column of the second grid
      def heuristic_horizontal_line y
        line = { x: grid.width + 1, y: y, w: grid.width, h: 0 }
        line.transform_values { |v| v * grid.cell_size }
      end
    
      # Renders the star on the first grid
      def render_bfs_star
        outputs.sprites << bfs_scale_up(grid.star).merge({ path: 'star.png' })
      end
    
      # Renders the star on the second grid
      def render_heuristic_star
        outputs.sprites << heuristic_scale_up(grid.star).merge({ path: 'star.png' })
      end
    
      # Renders the target on the first grid
      def render_bfs_target
        outputs.sprites << bfs_scale_up(grid.target).merge({ path: 'target.png' })
      end
    
      # Renders the target on the second grid
      def render_heuristic_target
        outputs.sprites << heuristic_scale_up(grid.target).merge({ path: 'target.png' })
      end
    
      # Renders the walls on the first grid
      def render_bfs_walls
        outputs.solids << grid.walls.map do |key, value|
          bfs_scale_up(key).merge(wall_color)
        end
      end
    
      # Renders the walls on the second grid
      def render_heuristic_walls
        outputs.solids << grid.walls.map do |key, value|
          heuristic_scale_up(key).merge(wall_color)
        end
      end
    
      # Renders the visited cells on the first grid
      def render_bfs_visited
        outputs.solids << bfs.came_from.map do |key, value|
          bfs_scale_up(key).merge(visited_color)
        end
      end
    
      # Renders the visited cells on the second grid
      def render_heuristic_visited
        outputs.solids << heuristic.came_from.map do |key, value|
          heuristic_scale_up(key).merge(visited_color)
        end
      end
    
      # Renders the frontier cells on the first grid
      def render_bfs_frontier
        outputs.solids << bfs.frontier.map do |cell|
          bfs_scale_up(cell).merge(frontier_color)
        end
      end
    
      # Renders the frontier cells on the second grid
      def render_heuristic_frontier
        outputs.solids << heuristic.frontier.map do |cell|
          heuristic_scale_up(cell).merge(frontier_color)
        end
      end
    
      # Renders the path found by the breadth first search on the first grid
      def render_bfs_path
        outputs.solids << bfs.path.map do |path|
          bfs_scale_up(path).merge(path_color)
        end
      end
    
      # Renders the path found by the heuristic search on the second grid
      def render_heuristic_path
        outputs.solids << heuristic.path.map do |path|
          heuristic_scale_up(path).merge(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 && 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 && 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 && 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 && 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)
        x = cell.x * grid.cell_size
        y = cell.y * grid.cell_size
        w = cell.w.zero? ? grid.cell_size : cell.w * grid.cell_size
        h = cell.h.zero? ? grid.cell_size : cell.h * grid.cell_size
        {x: x, y: y, w: w, h: h}
        # {x:, y:, w:, h:}
      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_ce