• 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 occured
      # 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 = args.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
    
    

    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
    
    

    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
      # args.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)
      # args.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 = [
          [  0, 0.25, 0.75, 1.0],
          [1.0, 0.75, 0.25,   0]
        ]
    
        throw_duration = catch_at - throw_at
    
        current_progress = args.easing.ease_spline throw_at,
                                                   Kernel.tick_count,
                                                   throw_duration,
                                                   spline
    
        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 occured,
      # 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 = args.gtk.read_file "data/terrain.txt"
      if !contents
        args.gtk.write_file "data/terrain.txt", ""
      end
    
      # read the terrain data from disk which is a csv
      args.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"
      args.gtk.write_file 'data/terrain.txt', terrain_csv
    end
    
    

    Simple Aabb Collision With Map Editor - Data - terrain.txt link

    # ./samples/04_physics_and_collisions/01_simple_aabb_collision_with_map_editor/data/terrain.txt
    352,320,32,32
    352,352,32,32
    352,384,32,32
    352,256,32,32
    352,192,32,32
    352,224,32,32
    

    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     = args.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 = $args.grid.right
    
            @max_velocity = 7
            @collision_enabled = true
        end
    
        #Move the ball according to its velocity
        def update args
          @center.x += @velocity.x
          @center.y += @velocity.y
          @velocity.y += GRAVITY
    
          alpha = 0.2
          if @center.y-@radius <= 0
            @velocity.y  = (@velocity.y.abs*0.7).abs
            @velocity.x  = (@velocity.x.abs*0.9).abs * ((@velocity.x < 0) ? -1 : 1)
    
            if @velocity.y.abs() < alpha
              @velocity.y=0
            end
            if @velocity.x.abs() < alpha
              @velocity.x=0
            end
          end
    
          if @center.x > args.grid.right+@radius*2
            @center.x = 0-@radius
          elsif @center.x< 0-@radius*2
            @center.x = args.grid.right + @radius
          end
        end
    
        def wallBounds args
            #if @x < @left_wall || @x + @width > @right_wall
                #@velocity.x *= -1.1
                #if @velocity.x > @max_velocity
                    #@velocity.x = @max_velocity
                #elsif @velocity.x < @max_velocity * -1
                    #@velocity.x = @max_velocity * -1
                #end
            #end
            #if @y < 0 || @y + @height > args.grid.h
                #@velocity.y *= -1.1
                #if @velocity.y > @max_velocity
                    #@velocity.y = @max_velocity
                #elsif @velocity.y < @max_velocity * -1
                    #@velocity.y = @max_velocity * -1
                #end
            #end
        end
    
        #render the ball to the screen
        def draw args
            #args.outputs.solids << [@x, @y, @width, @height, 255, 255, 0];
            args.outputs.sprites << [
              @center.x-@radius,
              @center.y-@radius,
              @radius*2,
              @radius*2,
              "sprites/circle-white.png",
              0,
              255,
              255,    #r
              0,    #g
              255   #b
            ]
        end
      end
    
    

    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 occured
          areaRec =  triArea.call(rpointA, rpointB, rpointC)+triArea.call(rpointA, rpointC, rpointD)
          areaSum = [
            triArea.call(point, rpointA, rpointB),triArea.call(point, rpointB, rpointC),
            triArea.call(point, rpointC, rpointD),triArea.call(point, rpointA, rpointD)
          ].inject(0){|sum,x| sum + x }
          e = 0.0001 #allow for minor error
          if areaRec>= areaSum-e and areaRec<= areaSum+e
            result = true
            #return true
            break
          end
        end
    
        #args.outputs.lines << [@pointA.x, @pointA.y, @pointB.x, @pointB.y,     000, 000, 000]
        #args.outputs.lines << [rpointA.x, rpointA.y, rpointB.x, rpointB.y,     255, 000, 000]
        #args.outputs.lines << [rpointC.x, rpointC.y, rpointD.x, rpointD.y,     000, 000, 255]
    
    
        #puts (rpointA.x.to_s + " " +  rpointA.y.to_s + " " + rpointB.x.to_s + " "+ rpointB.y.to_s)
        return result
      end #end collision?
    
      def getRepelMagnitude (fbx, fby, vrx, vry, ballMag)
        a = fbx ; b = vrx ; c = fby
        d = vry ; e = ballMag
        if b**2 + d**2 == 0
          #unexpected
        end
        x1 = (-a*b+-c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 + d**2 - a**2 * d**2)**0.5)/(b**2 + d**2)
        x2 = -((a*b + c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 * d**2 - a**2 * d**2)**0.5)/(b**2 + d**2))
        err = 0.00001
        o = ((fbx + x1*vrx)**2 + (fby + x1*vry)**2 ) ** 0.5
        p = ((fbx + x2*vrx)**2 + (fby + x2*vry)**2 ) ** 0.5
        r = 0
        if (ballMag >= o-err and ballMag <= o+err)
          r = x1
        elsif (ballMag >= p-err and ballMag <= p+err)
          r = x2
        else
          #unexpected
        end
        return r
      end
    
      def collide args, ball
        slope = collisionSlope args
    
        # perpVect: normal vector perpendicular to collision
        perpVect = {x: @pointB.x - @pointA.x, y:@pointB.y - @pointA.y}
        mag  = (perpVect.x**2 + perpVect.y**2)**0.5
        perpVect = {x: perpVect.x/(mag), y: perpVect.y/(mag)}
        perpVect = {x: -perpVect.y, y: perpVect.x}
        if perpVect.y > 0 #ensure perpVect points upward
          perpVect = {x: perpVect.x*-1, y: perpVect.y*-1}
        end
        previousPosition = {
          x:ball.x-ball.velocity.x,
          y:ball.y-ball.velocity.y
        }
        yInterc = @pointA.y + -slope*@pointA.x
        if slope == INFINITY
          if previousPosition.x < @pointA.x
            perpVect = {x: perpVect.x*-1, y: perpVect.y*-1}
            yInterc = -INFINITY
          end
        elsif previousPosition.y < slope*previousPosition.x + yInterc #check if ball is bellow or above the collider to determine if perpVect is - or +
          perpVect = {x: perpVect.x*-1, y: perpVect.y*-1}
        end
    
        velocityMag = (ball.velocity.x**2 + ball.velocity.y**2)**0.5
        theta_ball=Math.atan2(ball.velocity.y,ball.velocity.x) #the angle of the ball's velocity
        theta_repel=Math.atan2(perpVect.y,perpVect.x) #the angle of the repelling force(perpVect)
    
        fbx = velocityMag * Math.cos(theta_ball) #the x component of the ball's velocity
        fby = velocityMag * Math.sin(theta_ball) #the y component of the ball's velocity
    
        #the magnitude of the repelling force
        repelMag = getRepelMagnitude(fbx, fby, perpVect.x, perpVect.y, (ball.velocity.x**2 + ball.velocity.y**2)**0.5)
        frx = repelMag* Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx
        fry = repelMag* Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby
    
        fsumx = fbx+frx #sum of x forces
        fsumy = fby+fry #sum of y forces
        fr = velocityMag#fr is the resulting magnitude
        thetaNew = Math.atan2(fsumy, fsumx)  #thetaNew is the resulting angle
        xnew = fr*Math.cos(thetaNew)#resulting x velocity
        ynew = fr*Math.sin(thetaNew)#resulting y velocity
        if (velocityMag < MAX_VELOCITY)
          ball.velocity =  Vector2d.new(xnew*1.1, ynew*1.1)
        else
          ball.velocity =  Vector2d.new(xnew, ynew)
        end
    
      end
    end
    
    

    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, $args.grid.right, $args.grid.top, bgClr[:r], bgClr[:g], bgClr[:b]];
      args.outputs.borders << args.state.game_area
    
      render_instructions args
      render_shapes args
    
      render_balls args
    
      #args.state.rectangle.draw args
    
      args.outputs.sprites << [$args.grid.right-(args.state.board_width + args.grid.w / 8), 0, $args.grid.right, $args.grid.top, "sprites/square-white-2.png", 0, 255, bgClr[:r], bgClr[:g], bgClr[:b]]
      args.outputs.sprites << [0, 0, (args.state.board_width + args.grid.w / 8), $args.grid.top, "sprites/square-white-2.png", 0, 255, bgClr[:r], bgClr[:g], bgClr[:b]]
    
    end
    
    begin :render_methods
      def render_instructions args
        #gtk.current_framerate
        args.outputs.labels << [20, $args.grid.top-20, "FPS: " + $gtk.current_framerate.to_s]
        if (args.state.balls != nil && args.state.balls[0] != nil)
            bx =  args.state.balls[0].velocity.x
            by =  args.state.balls[0].velocity.y
            bmg = (bx**2.0 + by**2.0)**0.5
            args.outputs.labels << [20, $args.grid.top-20-20, "V: " + bmg.to_s ]
        end
    
    
      end
    
      def render_shapes args
        for s in args.state.squares
          s.draw args
        end
    
        for l in args.state.lines
          l.draw args
        end
    
        for t in args.state.tshapes
          t.draw args
        end
    
    
      end
    
      def render_balls args
        #args.state.balls.each do |ball|
          #ball.draw args
        #end
    
        args.outputs.sprites << args.state.balls.map do |ball|
          ball.getDraw args
        end
      end
    end
    
    #Calls all methods necessary for performing calculations
    def calc args
      for b in args.state.ballParents
        b.update args
      end
    
      for s in args.state.squares
        s.update args
      end
    
      for l in args.state.lines
        l.update args
      end
    
      for t in args.state.tshapes
        t.update args
      end
    
    
    
    end
    
    begin :calc_methods
    
    end
    
    def tick args
      defaults args
      render args
      calc args
    end
    
    

    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=$args.grid.right/2.0 - @width/2.0
        @y=$args.grid.top/2.0 - @height/2.0
    
        @xtmp = @width  * (1.0/10.0)
        @ytmp = @height * (1.0/10.0)
    
        #ball0 = args.state.balls[0]
        #hypotenuse = (args.state.balls[0].width**2 + args.state.balls[0].height**2)**0.5
        hypotenuse=args.state.ball_hypotenuse
        @boldXY = {x:(@x-hypotenuse/2)-1, y:(@y-hypotenuse/2)-1}
        @boldWidth = @width + hypotenuse + 2
        @boldHeight = @height + hypotenuse + 2
        @bold = [(@x-hypotenuse/2)-1,(@y-hypotenuse/2)-1,@width + hypotenuse + 2,@height + hypotenuse + 2]
    
    
        @points = [
          {x:@x,        y:@y+@ytmp},
          {x:@x+@xtmp,        y:@y},
          {x:@x+@width-@xtmp, y:@y},
          {x:@x+@width, y:@y+@ytmp},
          {x:@x+@width, y:@y+@height-@ytmp},#
          {x:@x+@width-@xtmp, y:@y+@height},
          {x:@x+@xtmp,        y:@y+@height},
          {x:@x,        y:@y+@height-@ytmp}
        ]
    
        @colliders = []
        #i = 0
        #while i < @points.length-1
          #@colliders.append(LinearCollider.new(@points[i],@points[i+1],:pos))
          #i+=1
        #end
        @colliders.append(LinearCollider.new(@points[0],@points[1], :neg))
        @colliders.append(LinearCollider.new(@points[1],@points[2], :neg))
        @colliders.append(LinearCollider.new(@points[2],@points[3], :neg))
        @colliders.append(LinearCollider.new(@points[3],@points[4], :neg))
        @colliders.append(LinearCollider.new(@points[4],@points[5], :pos))
        @colliders.append(LinearCollider.new(@points[5],@points[6], :pos))
        @colliders.append(LinearCollider.new(@points[6],@points[7], :pos))
        @colliders.append(LinearCollider.new(@points[0],@points[7], :pos))
    
      end
    
      def update args
    
        for b in args.state.balls
          if [b.x, b.y, b.width, b.height].intersect_rect?(@bold)
            for c in @colliders
              if c.collision?(args, b.getPoints(args),b)
                c.collide args, b
              end
            end
          end
        end
      end
    
      def draw args
        args.outputs.sprites << [
          @x,                                       # X
          @y,                                       # Y
          @width,                                   # W
          @height,                                  # H
          @image,                                   # PATH
          0,                                        # ANGLE
          255,                                      # ALPHA
          219,                                      # RED_SATURATION
          112,                                      # GREEN_SATURATION
          147                                       # BLUE_SATURATION
        ]
        #args.outputs.sprites << [@x, @y, @width, @height, "sprites/roundSquare_small_black.png"]
      end
    
      def serialize
      	{x: @x, y:@y}
      end
    
      def inspect
      	serialize.to_s
      end
    
      def to_s
      	serialize.to_s
      end
    end
    
    

    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 = args.state.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 = args.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
    
    

    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:;
      # args.layout.rect(row:, col:, w:, h:) is method that will
      # return a rectangle inside of a grid with 12 rows and 24 columns
      rect = args.layout.rect row: row, col: col, w: 3, h: 1
    
      # get senter of rect for label
      center = args.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 = opts.args.layout.rect(w: 0.5, h: 0.5, col: 2)
      final_rect = checkbox_template.center_inside_rect_y(opts.args.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  = opts.args.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 = final_rect.rect_center_point
    
      {
        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)
      # args.layout.rect returns a hash which we merge values with to create primitives
      # using args.layout.rect removes the need for pixel pushing
    
      # args.outputs.debug << args.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 << args.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 << args.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 << args.layout.rect(row: 1, col: 0, w: 7, h: 0)
                                       .line!(white_color)
    
      # screen location
      results.primitives << args.layout.point(row: 1.0, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "screen:")
    
      results.primitives << args.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 << args.layout.point(row: 1.5, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "position:")
    
      results.primitives << args.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 << args.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 << args.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 << args.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 << args.layout.point(row: 3.5, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "looping:")
    
      checkbox_template = args.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 << args.layout.point(row: 4.0, col: 0, row_anchor: 0.5)
                                       .merge(label_style)
                                       .merge(text: "paused:")
    
      checkbox_template = args.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: args.layout.rect(row: 5, col: 0, w: 7, h: 1) }
    
      results.primitives << results.delete_button_rect.rect.to_solid(r: 180)
    
      results.primitives << args.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 = args.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
    
    

    Audio Mixer - server_ip_address.txt link

    # ./samples/07_advanced_audio/01_audio_mixer/app/server_ip_address.txt
    192.168.1.65
    

    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 ", "
            args.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? (args.layout.rect(row: 0).yield_self { |r| r.merge y: r.y + r.h.half, h: r.h.half }))
          args.gtk.openurl 'https://www.youtube.com/watch?v=zEzovM5jT-k&ab_channel=AmirRajan'
        end
      end
    
      def 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: (args.layout.rect (opts.merge w: 2, h: 1))
    
        button_def[:border] = button_def[:rect].merge r: 0, g: 0, b: 0
    
        label_offset_x = 5
        label_offset_y = 30
    
        button_def[:label]  = button_def[:rect].merge text: opts[:text],
                                                      size_enum: -2.5,
                                                      x: button_def[:rect].x + label_offset_x,
                                                      y: button_def[:rect].y + label_offset_y
    
        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 = args.layout.rect(row: 5, col: 13)
          x_scale    = 10
          y_scale    = 100
          max_points = 25
    
          points = wave
          if wave.length > max_points
            resolution = wave.length.idiv max_points
            points = wave.find_all.with_index { |y, i| (i % resolution == 0) }
          end
    
          args.outputs.static_lines << points.map_with_index do |y, x|
            next_y = points[x + 1]
    
            if next_y
              {
                x:  starting_rect.x + (x * x_scale),
                y:  starting_rect.y + starting_rect.h.half + y_scale * y,
                x2: starting_rect.x + ((x + 1) * x_scale),
                y2: starting_rect.y + starting_rect.h.half + y_scale * next_y,
                r:  r,
                g:  g,
                b:  b
              }
            end
          end
    
          args.outputs.static_sprites << points.map_with_index do |y, x|
            {
              x:  (starting_rect.x + (x * x_scale)) - 2,
              y:  (starting_rect.y + starting_rect.h.half + y_scale * y) - 2,
              w:  4,
              h:  4,
              path: 'sprites/square-white.png',
              r: r,
              g: g,
              b: b
            }
          end
    
          args.state.graphed_at = 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 args.state.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 = args.state.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 args.state.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) && args.state.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 ||= args.state.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 ||= args.state.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 args.state.tick_count < args.state.start_playing_on_tick
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: "Count down: #{(args.state.start_playing_on_tick - args.state.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, args.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 = args.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
          args.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 << args.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 = args.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 << args.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 = args.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 = args.geometry.angle_to target_hero, other_hero
    
      # calculate the distance between the target hero and the other hero
      distance_to_other_hero = args.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 - ((args.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 !args.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
          },
          viewport: {
            x: 0,
            y: 0,
            w: 720,
            h: 720
          },
          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 = nil
    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
    $gtk.reset
    
    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
    
    

    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
      args.state.swinging_light_perc       = args.state
                                                 .swinging_light_start_at
                                                 .ease_spline_extended 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 * args.state.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 = args.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 = args.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 = args.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 = args.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 << args.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 << args.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 << args.layout.rect(row: row, col: 6, w: 1, h: 1).merge(**light_blue)
      end
    
      2.map_with_index do |row|
        args.outputs.solids << args.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 << args.layout.rect(row: row, col: 6 + 2, w: 2, h: 1).merge(**light_blue)
      end
    
      2.map_with_index do |row|
        args.outputs.solids << args.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 << args.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 << args.layout.rect(row: 0, col: 12 + col, w: 1, h: 1).merge(**yellow)
      end
    
      3.times do |col|
        args.outputs.solids << args.layout.rect(row: 1, col: 12 + col * 2, w: 2, h: 1).merge(**yellow)
      end
    
      6.times do |col|
        args.outputs.solids << args.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 << args.layout.rect(row: 4, col: 12).merge(text: "max width/height examples", anchor_x: 0.5, anchor_y: 0.5)
      args.outputs.solids << args.layout.rect(row: 4, col: 0, w: 24, h: 2).merge(a: 64, **green)
    
      # max height
      args.outputs.solids << args.layout.rect(row: 4, col: 0, w: 24, h: 2, max_height: 1).merge(a: 64, **green)
    
      # max width
      args.outputs.solids << args.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 << args.layout.rect(row: 6, col: 3, w: 6, h: 5)
      args.outputs.labels << args.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 << args.layout.rect(row: 7, col: 4.5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 7, col: 5.5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 7, col: 6.5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 8, col: 4.5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 8, col: 5.5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 8, col: 6.5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 9, col: 4.5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 9, col: 5.5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 9, col: 6.5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 6, col: 9, w: 6, h: 5)
      args.outputs.labels << args.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 = args.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 << args.layout.rect_center(
        args.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 << args.layout.rect_center(
        args.layout.rect(w: 5, h: 1), # inner rect
        outer_rect, # outer rect
      )
    end
    
    def render_rect_group_examples args
      args.outputs.labels << args.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 << args.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 << args.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 << args.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 << args.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
    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.native_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" }
    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 }
    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 << args.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 << args.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 << args.layout.rect(row: row, col: 0, w: 1, h: 1).merge(**light_blue)
      end
    
      2.map_with_index do |row|
        args.outputs.solids << args.layout.rect(row: row * 2, col: 1, w: 1, h: 2).merge(**light_blue)
      end
    
      4.map_with_index do |row|
        args.outputs.solids << args.layout.rect(row: row, col: 2, w: 2, h: 1).merge(**light_blue)
      end
    
      2.map_with_index do |row|
        args.outputs.solids << args.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 << args.layout.rect(row: 1, col: 9).merge(text: "column examples", vertical_alignment_enum: 1, alignment_enum: 1)
      6.times do |col|
        args.outputs.solids << args.layout.rect(row: 0, col: 6 + col, w: 1, h: 1).merge(**yellow)
      end
    
      3.times do |col|
        args.outputs.solids << args.layout.rect(row: 1, col: 6 + col * 2, w: 2, h: 1).merge(**yellow)
      end
    
      6.times do |col|
        args.outputs.solids << args.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 << args.layout.rect(row: 4, col: 6).merge(text: "max width/height examples", vertical_alignment_enum: 1, alignment_enum: 1)
      args.outputs.solids << args.layout.rect(row: 4, col: 0, w: 12, h: 2).merge(a: 64, **green)
    
      # max height
      args.outputs.solids << args.layout.rect(row: 4, col: 0, w: 12, h: 2, max_height: 1).merge(a: 64, **green)
    
      # max width
      args.outputs.solids << args.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 << args.layout.rect(row: 5.5, col: 6).merge(text: "labels using args.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 << args.layout.rect(row: 7, col: 4).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 7, col: 5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 7, col: 6).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 8, col: 4).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 8, col: 5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 8, col: 6).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 9, col: 4).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 9, col: 5).merge(**grey)
      args.outputs.labels << args.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 << args.layout.rect(row: 9, col: 6).merge(**grey)
      args.outputs.labels << args.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 << args.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 = args.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 << args.layout.rect_center(
        args.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 << args.layout.rect_center(
        args.layout.rect(w: 5, h: 1), # inner rect
        outer_rect, # outer rect
      )
    
      args.outputs.labels << args.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 << args.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 << args.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 << args.layout.rect_group(row: 19,
                                                    col: 1,
                                                    dcol: 2,
                                                    w: 2,
                                                    h: 1,
                                                    group: colors)
    
      args.outputs.solids << args.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 = [
        [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 = 0.ease_spline_extended args.state.simulation_tick, args.state.duration, args.state.spline
      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.ease_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
    
    

    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."
        args.gtk.console.set_command "reset_with count: 100"
      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 << args.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."
        args.gtk.console.set_command "reset_with count: 100"
      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 << args.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."
        args.gtk.console.set_command "reset_with count: 100"
      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 << args.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."
        args.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
    
      # update
      args.state.stars.each(&:move)
    
      # render
      args.outputs.sprites << args.state.stars
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << args.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."
        args.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 }
        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 << args.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."
        args.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 }
        args.outputs.static_sprites << args.state.stars
      end
    
      # render framerate
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << args.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 occured
                         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, args.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.bullets    ||= []
      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
          b = {
            id: args.state.id_seed,
            x: 60,
            y: 60,
            w: 10,
            h: 10,
            dy: rand(20) + 10,
            dx: rand(20) + 10,
            path: 'sprites/square/blue.png'
          }
    
          args.state.bullets << b # if b.id == 122
    
          args.state.id_seed += 1
      end
    
      terrain = args.state.terrain
    
      args.state.bullets.each do |b|
        next if b.still
        # if b.still
        #   x_dir = if rand > 0.5
        #             -1
        #           else
        #             1
        #           end
    
        #   y_dir = if rand > 0.5
        #             -1
        #           else
        #             1
        #           end
    
        #   b.dy = rand(20) + 10 * x_dir
        #   b.dx = rand(20) + 10 * y_dir
        #   b.still = false
        #   b.on_floor = false
        # end
    
        if b.on_floor
          b.dx *= 0.9
        end
    
        b.x += b.dx
    
        collision_x = args.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 = args.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 space bar to add squares." }
      args.outputs.labels << { x: 60, y: 90.from_top, text: "FPS: #{args.gtk.current_framerate.to_sf}" }
      args.outputs.labels << { x: 60, y: 120.from_top, text: "Count: #{args.state.bullets.length}" }
      args.outputs.borders << args.state.terrain
      args.outputs.sprites << args.state.bullets
    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
        args.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 = args.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 = args.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 = args.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: #{args.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 ||= [
        args.layout.rect(row: 0, col: 0, w: 1, h: 1).merge(id: :option1, text: "Option 1", checked: false, changed_at: -120),
        args.layout.rect(row: 1, col: 0, w: 1, h: 1).merge(id: :option1, text: "Option 2", checked: false, changed_at: -120),
        args.layout.rect(row: 2, col: 0, w: 1, h: 1).merge(id: :option1, text: "Option 3", checked: false, changed_at: -120),
        args.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 * args.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 * args.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 = args.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 = args.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 = args.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 - 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
        args.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 - 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 = args.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 = args.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 = args.gtk.serialize_state args.state
      assert_hash_strings! assert, result, "{:entity_id=>1, :entity_keys_by_ref=>{}, :tick_count=>-1, :player_one=>\"test\"}"
    
      args.gtk.write_file 'state.txt', ''
      result = args.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 = args.gtk.deserialize_state '{:entity_id=>3, :tick_count=>-1, :player_one=>"test"}'
      assert.equal! result.player_one, "test"
    
      args.gtk.write_file 'state.txt',  '{:entity_id=>3, :tick_count=>-1, :player_one=>"test"}'
      result = args.gtk.deserialize_state 'state.txt'
      assert.equal! result.player_one, "test"
    end
    
    def test_very_large_serialization args, assert
      args.gtk.write_file("logs/log.txt", "")
      size = 3000
      size.map_with_index do |i|
        args.state.send("k#{i}=".to_sym, i)
      end
    
      result = args.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 = args.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 = args.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 = args.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 = args.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 = args.gtk.serialize_state args.state
    
      deserialized_state = args.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
      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 = args.gtk.serialize_state args.state
    
      deserialized_state = args.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 = args.gtk.serialize_state args.state
    
      deserialized_state = args.gtk.deserialize_state serialized_state
      assert.equal! deserialized_state.strict_entity.one, deserialized_state.strict_entity.two
    end
    
    def test_serialization_excludes_thrash_count args, assert
      args.state.player.name = "Ryu"
      # force a nil pun
      if args.state.player.age > 30
      end
      assert.equal! args.state.player.as_hash[:__thrash_count__][:>], 1
      result = args.gtk.serialize_state args.state
      assert.false! (result.include? "__thrash_count__"),
                    "The __thrash_count__ key exists in state when it shouldn't have."
    end
    
    def test_serialization_does_not_mix_up_zero_and_true args, assert
      args.state.enemy.evil = true
      args.state.enemy.hp = 0
      serialized = args.gtk.serialize_state args.state.enemy
    
      deserialized = args.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 - 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
      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, 11
    
    def tick args
      args.outputs.background_color = [0, 0, 0]
    
      # 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 << [640, 600, "This app shows random images from the Internet.", 10, 1, 255, 255, 255]
        args.outputs.labels << [640, 500, "Quit in the next few seconds if this is a problem.", 10, 1, 255, 255, 255]
        args.outputs.labels << [640, 350, "#{(args.state.warning_debounce / 60.0).to_i}", 10, 1, 255, 255, 255]
        return
      end
    
      args.state.download_debounce ||= 0   # start immediately, reset to non zero later.
      args.state.photos ||= []
    
      # 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 << [ 100 + rand(1080), 500 - rand(480), fname, rand(80) - 40 ]
          end
          args.state.download = nil
          args.state.download_debounce = (rand(3) + 2) * 60
        end
      end
    
      # draw any downloaded photos...
      args.state.photos.each { |i|
        args.outputs.primitives << [i[0], i[1], 200, 300, i[2], i[3]].sprite
      }
    
      # Draw a download progress bar...
      args.outputs.primitives << [0, 0, 1280, 30, 0, 0, 0, 255].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 << [0, 0, 1280 * pct, 30, 0, 0, 255, 255].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:
      args.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 Get - Metadata - cvars.txt link

    # ./samples/11_http/02_in_game_web_server_http_get/metadata/cvars.txt
    webserver.enabled=true
    webserver.port=3000
    webserver.remote_clients=false
    
    

    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:
      args.gtk.start_server! port: $cvars["webserver.port"].value, enable_in_prod: true
    
      # defaults
      args.state.post_button      = args.layout.rect(row: 0, col: 0, w: 5, h: 1).merge(text: "execute http_post")
      args.state.post_body_button = args.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}" }
          # ==================================
    
          args.gtk.http_post "http://localhost:9001/testing",
                             form_fields,
                             ["Content-Type: application/x-www-form-urlencoded"]
    
          args.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}\"}"
          # ==================================
    
          args.gtk.http_post_body "http://localhost:9001/testing",
                                  json,
                                  ["Content-Type: application/json", "Content-Length: #{json.length}"]
    
          args.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 = args.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
    
    

    In Game Web Server Http Post - Metadata - cvars.txt link

    # ./samples/11_http/03_in_game_web_server_http_post/metadata/cvars.txt
    webserver.enabled=true
    webserver.port=9001
    webserver.remote_clients=false
    
    

    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 << args.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 - license.txt link

    # ./samples/12_c_extensions/04_handcrafted_extension/license.txt
    Copyright 2022 DragonRuby LLC
    
    MIT License
    
    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    
    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    
    

    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."
        args.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 << args.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
    
    

    Handcrafted Extension Advanced - license.txt link

    # ./samples/12_c_extensions/04_handcrafted_extension_advanced/license.txt
    Copyright 2022 DragonRuby LLC
    
    MIT License
    
    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    
    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    
    

    Handcrafted Extension Advanced - Metadata - cvars.txt link

    # ./samples/12_c_extensions/04_handcrafted_extension_advanced/metadata/cvars.txt
    log.filter_subsystems=HTTPServer
    
    # Whether or not the game should use the whole display, be sure to
    # expose $gtk.set_window_fullscreen(false) or $gtk.request_quit
    # to the player so they can get out of full screen mode.
    # renderer.fullscreen=true
    
    # Milliseconds to sleep per frame when in the background (zero to disable)
    # renderer.background_sleep=0
    
    # Set the window as borderless.
    # Note: the ability to quit the application via OS shortcuts will not
    # work if this value is true and you must provide a means to exit the
    # game and wire it up to $gtk.request_quit
    # renderer.borderless=true
    
    

    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 args.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 && args.gtk.platform?(:ios)
        args.gtk.dlopen 'ext'
        include FFI::CExt
        puts "the results of hello world are:"
        puts hello_world()
        $gtk.console.show
      end
    end
    
    

    Ios Metadata - cvars.txt link

    # ./samples/12_c_extensions/05_ios_c_extensions/metadata/cvars.txt
    
    

    Ios Metadata - ios_metadata.txt link

    # ./samples/12_c_extensions/05_ios_c_extensions/metadata/ios_metadata.txt
    teamid=TEAMID
    appid=APPID
    appname=ICON NAME
    version=1.0
    devcert=NAME OF DEV CERT
    prodcert=NAME OF PROD CERT
    
    

    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 Mac Extension - readme.txt link

    # ./samples/12_c_extensions/06_handcrafted_mac_extension/readme.txt
    Build script for C Extension is located under ./mygame/app-native/build.sh.
    
    The build script assumes that the sample code has been copied over to the root mygame directory.
    
    

    Handcrafted Steam Extensions-native - Steam - download.txt link

    # ./samples/12_c_extensions/07_handcrafted_steam_extensions/app-native/steam/download.txt
    Download Steamworks SDK, unzip, and place contents of "sdk" folder here: https://partner.steamgames.com/downloads/list
    
    

    Handcrafted Steam Extensions-native - steam_appid.txt link

    # ./samples/12_c_extensions/07_handcrafted_steam_extensions/app-native/steam_appid.txt
    2425950
    
    

    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 Steam Extensions - readme.txt link

    # ./samples/12_c_extensions/07_handcrafted_steam_extensions/readme.txt
    Build script for C Extension is located under ./mygame/app-native/build.sh.
    
    The build script assumes that the sample code has been copied over to the root mygame directory.
    
    

    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
        args.gtk.reset
        reset
        return
      end
    
      # Every tick, new args are passed, and the Breadth First Search tick is called
      $breadth_first_search ||= BreadthFirstSearch.new(args)
      $breadth_first_search.args = args
      $breadth_first_search.tick
    end
    
    
    def reset
      $breadth_first_search = nil
    end
    
    

    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
        args.gtk.reset
        reset
        return
      end
    
      $detailed_breadth_first_search ||= DetailedBreadthFirstSearch.new(args)
      $detailed_breadth_first_search.args = args
      $detailed_breadth_first_search.tick
    end
    
    
    def reset
      $detailed_breadth_first_search = nil
    end
    
    

    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
        args.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
        args.gtk.reset
        reset
        return
      end
    
      # Every tick, new args are passed, and the Breadth First Search tick is called
      $early_exit_breadth_first_search ||= EarlyExitBreadthFirstSearch.new
      $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
        args.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_cell_closest_to_mouse
        end
        unless old_star == grid.star
          recalculate_searches
        end
      end
    
      # Moves the target to the grid closest to the mouse in the first grid
      # Only recalculate_searchess the search if the target changes position
      # Called whenever the user is editing the target (puts mouse down on target)
      def process_input_bfs_target
        old_target = grid.target.clone
        unless bfs_cell_closest_to_mouse == grid.star
          grid.target = bfs_cell_closest_to_mouse
        end
        unless old_target == grid.target
          recalculate_searches
        end
      end
    
      # Moves the target to the cell closest to the mouse in the second grid
      # Only recalculate_searchess the search if the target changes position
      # Called whenever the user is editing the target (puts mouse down on target)
      def process_input_heuristic_target
        old_target = grid.target.clone
        unless heuristic_cell_closest_to_mouse == grid.star
          grid.target = heuristic_cell_closest_to_mouse
        end
        unless old_target == grid.target
          recalculate_searches
        end
      end
    
      # Removes walls in the first grid that are under the cursor
      def process_input_bfs_remove_wall
        # The mouse needs to be inside the grid, because we only want to remove walls
        # the cursor is directly over
        # Recalculations should only occur when a wall is actually deleted
        if bfs_mouse_over_grid?
          if grid.walls.key?(bfs_cell_closest_to_mouse)
            grid.walls.delete(bfs_cell_closest_to_mouse)
            recalculate_searches
          end
        end
      end
    
      # Removes walls in the second grid that are under the cursor
      def process_input_heuristic_remove_wall
        # The mouse needs to be inside the grid, because we only want to remove walls
        # the cursor is directly over
        # Recalculations should only occur when a wall is actually deleted
        if heuristic_mouse_over_grid?
          if grid.walls.key?(heuristic_cell_closest_to_mouse)
            grid.walls.delete(heuristic_cell_closest_to_mouse)
            recalculate_searches
          end
        end
      end
      # Adds a wall in the first grid in the cell the mouse is over
      def process_input_bfs_add_wall
        if bfs_mouse_over_grid?
          unless grid.walls.key?(bfs_cell_closest_to_mouse)
            grid.walls[bfs_cell_closest_to_mouse] = true
            recalculate_searches
          end
        end
      end
    
      # Adds a wall in the second grid in the cell the mouse is over
      def process_input_heuristic_add_wall
        if heuristic_mouse_over_grid?
          unless grid.walls.key?(heuristic_cell_closest_to_mouse)
            grid.walls[heuristic_cell_closest_to_mouse] = true
            recalculate_searches
          end
        end
      end
    
      # When the user grabs the star and puts their cursor to the far right
      # and moves up and down, the star is supposed to move along the grid as well
      # Finding the cell closest to the mouse helps with this
      def bfs_cell_closest_to_mouse
        # Closest cell to the mouse in the first grid
        x = (inputs.mouse.point.x / grid.cell_size).to_i
        y = (inputs.mouse.point.y / grid.cell_size).to_i
        # Bound x and y to the grid
        x = grid.width - 1 if x > grid.width - 1
        y = grid.height - 1 if y > grid.height - 1
        # Return closest cell
        [x, y]
      end
    
      # When the user grabs the star and puts their cursor to the far right
      # and moves up and down, the star is supposed to move along the grid as well
      # Finding the cell closest to the mouse in the second grid helps with this
      def heuristic_cell_closest_to_mouse
        # Closest cell grid to the mouse in the second
        x = (inputs.mouse.point.x / grid.cell_size).to_i
        y = (inputs.mouse.point.y / grid.cell_size).to_i
        # Translate the cell to the first grid
        x -= grid.width + 1
        # Bound x and y to the first grid
        x = 0 if x < 0
        y = 0 if y < 0
        x = grid.width - 1 if x > grid.width - 1
        y = grid.height - 1 if y > grid.height - 1
        # Return closest cell
        [x, y]
      end
    
      def recalculate_searches
        # Reset the searches
        bfs.came_from    = {}
        bfs.frontier     = []
        bfs.path         = []
        heuristic.came_from = {}
        heuristic.frontier  = []
        heuristic.path      = []
    
        # Move the searches forward to the current step
        state.current_step.times { move_searches_one_step_forward }
      end
    
      def move_searches_one_step_forward
        bfs_one_step_forward
        heuristic_one_step_forward
      end
    
      def bfs_one_step_forward
        return if bfs.came_from.key?(grid.target)
    
        # Only runs at the beginning of the search as setup.
        if bfs.came_from.empty?
          bfs.frontier << grid.star
          bfs.came_from[grid.star] = nil
        end
    
        # A step in the search
        unless bfs.frontier.empty?
          # Takes the next frontier cell
          new_frontier = bfs.frontier.shift
          # For each of its neighbors
          adjacent_neighbors(new_frontier).each do |neighbor|
            # That have not been visited and are not walls
            unless bfs.came_from.key?(neighbor) || grid.walls.key?(neighbor)
              # Add them to the frontier and mark them as visited
              bfs.frontier << neighbor
              bfs.came_from[neighbor] = new_frontier
            end
          end
        end
    
        # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
        # Comment this line and let a path generate to see the difference
        bfs.frontier = bfs.frontier.sort_by { |cell| proximity_to_star(cell) }
    
        # If the search found the target
        if bfs.came_from.key?(grid.target)
          # Calculate the path between the target and star
          bfs_calc_path
        end
      end
    
      # Calculates the path between the target and star for the breadth first search
      # Only called when the breadth first search finds the target
      def bfs_calc_path
        # Start from the target
        endpoint = grid.target
        # And the cell it came from
        next_endpoint = bfs.came_from[endpoint]
        while endpoint && next_endpoint
          # Draw a path between these two cells and store it
          path = get_path_between(endpoint, next_endpoint)
          bfs.path << path
          # And get the next pair of cells
          endpoint = next_endpoint
          next_endpoint = bfs.came_from[endpoint]
          # Continue till there are no more cells
        end
      end
    
      # Moves the heuristic search forward one step
      # Can be called from tick while the animation is playing
      # Can also be called when recalculating the searches after the user edited the grid
      def heuristic_one_step_forward
        # Stop the search if the target has been found
        return if heuristic.came_from.key?(grid.target)
    
        # If the search has not begun
        if heuristic.came_from.empty?
          # Setup the search to begin from the star
          heuristic.frontier << grid.star
          heuristic.came_from[grid.star] = nil
        end
    
        # One step in the heuristic search
    
        # Unless there are no more cells to explore from
        unless heuristic.frontier.empty?
          # Get the next cell to explore from
          new_frontier = heuristic.frontier.shift
          # For each of its neighbors
          adjacent_neighbors(new_frontier).each do |neighbor|
            # That have not been visited and are not walls
            unless heuristic.came_from.key?(neighbor) || grid.walls.key?(neighbor)
              # Add them to the frontier and mark them as visited
              heuristic.frontier << neighbor
              heuristic.came_from[neighbor] = new_frontier
            end
          end
        end
    
        # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
        heuristic.frontier = heuristic.frontier.sort_by { |cell| proximity_to_star(cell) }
        # Sort the frontier so cells that are close to the target are then prioritized
        heuristic.frontier = heuristic.frontier.sort_by { |cell| heuristic_heuristic(cell) }
    
        # If the search found the target
        if heuristic.came_from.key?(grid.target)
          # Calculate the path between the target and star
          heuristic_calc_path
        end
      end
    
      # Returns one-dimensional absolute distance between cell and target
      # Returns a number to compare distances between cells and the target
      def heuristic_heuristic(cell)
        (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs
      end
    
      # Calculates the path between the target and star for the heuristic search
      # Only called when the heuristic search finds the target
      def heuristic_calc_path
        # Start from the target
        endpoint = grid.target
        # And the cell it came from
        next_endpoint = heuristic.came_from[endpoint]
        while endpoint && next_endpoint
          # Draw a path between these two cells and store it
          path = get_path_between(endpoint, next_endpoint)
          heuristic.path << path
          # And get the next pair of cells
          endpoint = next_endpoint
          next_endpoint = heuristic.came_from[endpoint]
          # Continue till there are no more cells
        end
      end
    
      # Returns a list of adjacent cells
      # Used to determine what the next cells to be added to the frontier are
      def adjacent_neighbors(cell)
        neighbors = []
    
        # Gets all the valid neighbors into the array
        # From southern neighbor, clockwise
        neighbors << [cell.x    , cell.y - 1] unless cell.y == 0
        neighbors << [cell.x - 1, cell.y    ] unless cell.x == 0
        neighbors << [cell.x    , cell.y + 1] unless cell.y == grid.height - 1
        neighbors << [cell.x + 1, cell.y    ] unless cell.x == grid.width - 1
    
        neighbors
      end
    
      # Finds the vertical and horizontal distance of a cell from the star
      # and returns the larger value
      # This method is used to have a zigzag pattern in the rendered path
      # A cell that is [5, 5] from the star,
      # is explored before over a cell that is [0, 7] away.
      # So, if possible, the search tries to go diagonal (zigzag) first
      def proximity_to_star(cell)
        distance_x = (grid.star.x - cell.x).abs
        distance_y = (grid.star.y - cell.y).abs
    
        [distance_x, distance_y].max
      end
    
      # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored.
      def grid
        state.grid
      end
    
      def buttons
        state.buttons
      end
    
      def slider
        state.slider
      end
    
      def bfs
        state.bfs
      end
    
      def heuristic
        state.heuristic
      end
    
      # Descriptive aliases for colors
      def default_color
        { r: 221, g: 212, b: 213 }
      end
    
      def wall_color
        { r: 134, g: 134, b: 120 }
      end
    
      def visited_color
        { r: 204, g: 191, b: 179 }
      end
    
      def frontier_color
        { r: 103, g: 136, b: 204, a: 200 }
      end
    
      def path_color
        { r: 231, g: 230, b: 228 }
      end
    
      def button_color
        [190, 190, 190] # Gray
      end
    end
    # Method that is called by DragonRuby periodically
    # Used for updating animations and calculations
    def tick args
    
      # Pressing r will reset the application
      if args.inputs.keyboard.key_down.r
        args.gtk.reset
        reset
        return
      end
    
      # Every tick, new args are passed, and the Breadth First Search tick is called
      $heuristic ||= Heuristic.new
      $heuristic.args = args
      $heuristic.tick
    end
    
    
    def reset
      $heuristic = nil
    end
    
    

    Heuristic With Walls - main.rb link

    # ./samples/13_path_finding_algorithms/07_heuristic_with_walls/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
    
    # This time the heuristic search still explored less of the grid, hence finishing faster.
    # However, it did not find the shortest path between the star and the target.
    
    # The only difference between this app and Heuristic is the change of the starting position.
    
    class Heuristic_With_Walls
      attr_gtk
    
      def tick
        defaults
        render
        input
        # If animation is playing, and max steps have not been reached
        # Move the search a step forward
        if state.play && state.current_step < state.max_steps
          # Variable that tells the program what step to recalculate up to
          state.current_step += 1
          move_searches_one_step_forward
        end
      end
    
      def defaults
        # Variables to edit the size and appearance of the grid
        # Freely customizable to user's liking
        grid.width     ||= 15
        grid.height    ||= 15
        grid.cell_size ||= 40
        grid.rect      ||= [0, 0, grid.width, grid.height]
    
        grid.star      ||= [0, 2]
        grid.target    ||= [14, 12]
        grid.walls     ||= {
          [2, 2] => true,
          [3, 2] => true,
          [4, 2] => true,
          [5, 2] => true,
          [6, 2] => true,
          [7, 2] => true,
          [8, 2] => true,
          [9, 2] => true,
          [10, 2] => true,
          [11, 2] => true,
          [12, 2] => true,
          [12, 3] => true,
          [12, 4] => true,
          [12, 5] => true,
          [12, 6] => true,
          [12, 7] => true,
          [12, 8] => true,
          [12, 9] => true,
          [12, 10] => true,
          [12, 11] => true,
          [12, 12] => true,
          [2, 12] => true,
          [3, 12] => true,
          [4, 12] => true,
          [5, 12] => true,
          [6, 12] => true,
          [7, 12] => true,
          [8, 12] => true,
          [9, 12] => true,
          [10, 12] => true,
          [11, 12] => true,
          [12, 12] => true
        }
        # There are no hills in the Heuristic Search Demo
    
        # What the user is currently editing on the grid
        # We store this value, because we want to remember the value even when
        # the user's cursor is no longer over what they're interacting with, but
        # they are still clicking down on the mouse.
        state.user_input ||= :none
    
        # These variables allow the breadth first search to take place
        # Came_from is a hash with a key of a cell and a value of the cell that was expanded from to find the key.
        # Used to prevent searching cells that have already been found
        # and to trace a path from the target back to the starting point.
        # Frontier is an array of cells to expand the search from.
        # The search is over when there are no more cells to search from.
        # Path stores the path from the target to the star, once the target has been found
        # It prevents calculating the path every tick.
        bfs.came_from  ||= {}
        bfs.frontier   ||= []
        bfs.path       ||= []
    
        heuristic.came_from ||= {}
        heuristic.frontier  ||= []
        heuristic.path      ||= []
    
        # Stores which step of the animation is being rendered
        # When the user moves the star or messes with the walls,
        # the searches are recalculated up to this step
        unless 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_cell_closest_to_mouse
        end
        unless old_star == grid.star
          recalculate_searches
        end
      end
    
      # Moves the target to the grid closest to the mouse in the first grid
      # Only recalculate_searchess the search if the target changes position
      # Called whenever the user is editing the target (puts mouse down on target)
      def process_input_bfs_target
        old_target = grid.target.clone
        unless bfs_cell_closest_to_mouse == grid.star
          grid.target = bfs_cell_closest_to_mouse
        end
        unless old_target == grid.target
          recalculate_searches
        end
      end
    
      # Moves the target to the cell closest to the mouse in the second grid
      # Only recalculate_searchess the search if the target changes position
      # Called whenever the user is editing the target (puts mouse down on target)
      def process_input_heuristic_target
        old_target = grid.target.clone
        unless heuristic_cell_closest_to_mouse == grid.star
          grid.target = heuristic_cell_closest_to_mouse
        end
        unless old_target == grid.target
          recalculate_searches
        end
      end
    
      # Removes walls in the first grid that are under the cursor
      def process_input_bfs_remove_wall
        # The mouse needs to be inside the grid, because we only want to remove walls
        # the cursor is directly over
        # Recalculations should only occur when a wall is actually deleted
        if bfs_mouse_over_grid?
          if grid.walls.key?(bfs_cell_closest_to_mouse)
            grid.walls.delete(bfs_cell_closest_to_mouse)
            recalculate_searches
          end
        end
      end
    
      # Removes walls in the second grid that are under the cursor
      def process_input_heuristic_remove_wall
        # The mouse needs to be inside the grid, because we only want to remove walls
        # the cursor is directly over
        # Recalculations should only occur when a wall is actually deleted
        if heuristic_mouse_over_grid?
          if grid.walls.key?(heuristic_cell_closest_to_mouse)
            grid.walls.delete(heuristic_cell_closest_to_mouse)
            recalculate_searches
          end
        end
      end
      # Adds a wall in the first grid in the cell the mouse is over
      def process_input_bfs_add_wall
        if bfs_mouse_over_grid?
          unless grid.walls.key?(bfs_cell_closest_to_mouse)
            grid.walls[bfs_cell_closest_to_mouse] = true
            recalculate_searches
          end
        end
      end
    
      # Adds a wall in the second grid in the cell the mouse is over
      def process_input_heuristic_add_wall
        if heuristic_mouse_over_grid?
          unless grid.walls.key?(heuristic_cell_closest_to_mouse)
            grid.walls[heuristic_cell_closest_to_mouse] = true
            recalculate_searches
          end
        end
      end
    
      # When the user grabs the star and puts their cursor to the far right
      # and moves up and down, the star is supposed to move along the grid as well
      # Finding the cell closest to the mouse helps with this
      def bfs_cell_closest_to_mouse
        # Closest cell to the mouse in the first grid
        x = (inputs.mouse.point.x / grid.cell_size).to_i
        y = (inputs.mouse.point.y / grid.cell_size).to_i
        # Bound x and y to the grid
        x = grid.width - 1 if x > grid.width - 1
        y = grid.height - 1 if y > grid.height - 1
        # Return closest cell
        [x, y]
      end
    
      # When the user grabs the star and puts their cursor to the far right
      # and moves up and down, the star is supposed to move along the grid as well
      # Finding the cell closest to the mouse in the second grid helps with this
      def heuristic_cell_closest_to_mouse
        # Closest cell grid to the mouse in the second
        x = (inputs.mouse.point.x / grid.cell_size).to_i
        y = (inputs.mouse.point.y / grid.cell_size).to_i
        # Translate the cell to the first grid
        x -= grid.width + 1
        # Bound x and y to the first grid
        x = 0 if x < 0
        y = 0 if y < 0
        x = grid.width - 1 if x > grid.width - 1
        y = grid.height - 1 if y > grid.height - 1
        # Return closest cell
        [x, y]
      end
    
      def recalculate_searches
        # Reset the searches
        bfs.came_from    = {}
        bfs.frontier     = []
        bfs.path         = []
        heuristic.came_from = {}
        heuristic.frontier  = []
        heuristic.path      = []
    
        # Move the searches forward to the current step
        state.current_step.times { move_searches_one_step_forward }
      end
    
      def move_searches_one_step_forward
        bfs_one_step_forward
        heuristic_one_step_forward
      end
    
      def bfs_one_step_forward
        return if bfs.came_from.key?(grid.target)
    
        # Only runs at the beginning of the search as setup.
        if bfs.came_from.empty?
          bfs.frontier << grid.star
          bfs.came_from[grid.star] = nil
        end
    
        # A step in the search
        unless bfs.frontier.empty?
          # Takes the next frontier cell
          new_frontier = bfs.frontier.shift
          # For each of its neighbors
          adjacent_neighbors(new_frontier).each do |neighbor|
            # That have not been visited and are not walls
            unless bfs.came_from.key?(neighbor) || grid.walls.key?(neighbor)
              # Add them to the frontier and mark them as visited
              bfs.frontier << neighbor
              bfs.came_from[neighbor] = new_frontier
            end
          end
        end
    
        # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
        # Comment this line and let a path generate to see the difference
        bfs.frontier = bfs.frontier.sort_by { |cell| proximity_to_star(cell) }
    
        # If the search found the target
        if bfs.came_from.key?(grid.target)
          # Calculate the path between the target and star
          bfs_calc_path
        end
      end
    
      # Calculates the path between the target and star for the breadth first search
      # Only called when the breadth first search finds the target
      def bfs_calc_path
        # Start from the target
        endpoint = grid.target
        # And the cell it came from
        next_endpoint = bfs.came_from[endpoint]
        while endpoint && next_endpoint
          # Draw a path between these two cells and store it
          path = get_path_between(endpoint, next_endpoint)
          bfs.path << path
          # And get the next pair of cells
          endpoint = next_endpoint
          next_endpoint = bfs.came_from[endpoint]
          # Continue till there are no more cells
        end
      end
    
      # Moves the heuristic search forward one step
      # Can be called from tick while the animation is playing
      # Can also be called when recalculating the searches after the user edited the grid
      def heuristic_one_step_forward
        # Stop the search if the target has been found
        return if heuristic.came_from.key?(grid.target)
    
        # If the search has not begun
        if heuristic.came_from.empty?
          # Setup the search to begin from the star
          heuristic.frontier << grid.star
          heuristic.came_from[grid.star] = nil
        end
    
        # One step in the heuristic search
    
        # Unless there are no more cells to explore from
        unless heuristic.frontier.empty?
          # Get the next cell to explore from
          new_frontier = heuristic.frontier.shift
          # For each of its neighbors
          adjacent_neighbors(new_frontier).each do |neighbor|
            # That have not been visited and are not walls
            unless heuristic.came_from.key?(neighbor) || grid.walls.key?(neighbor)
              # Add them to the frontier and mark them as visited
              heuristic.frontier << neighbor
              heuristic.came_from[neighbor] = new_frontier
            end
          end
        end
    
        # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
        heuristic.frontier = heuristic.frontier.sort_by { |cell| proximity_to_star(cell) }
        # Sort the frontier so cells that are close to the target are then prioritized
        heuristic.frontier = heuristic.frontier.sort_by { |cell| heuristic_heuristic(cell) }
    
        # If the search found the target
        if heuristic.came_from.key?(grid.target)
          # Calculate the path between the target and star
          heuristic_calc_path
        end
      end
    
      # Returns one-dimensional absolute distance between cell and target
      # Returns a number to compare distances between cells and the target
      def heuristic_heuristic(cell)
        (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs
      end
    
      # Calculates the path between the target and star for the heuristic search
      # Only called when the heuristic search finds the target
      def heuristic_calc_path
        # Start from the target
        endpoint = grid.target
        # And the cell it came from
        next_endpoint = heuristic.came_from[endpoint]
        while endpoint && next_endpoint
          # Draw a path between these two cells and store it
          path = get_path_between(endpoint, next_endpoint)
          heuristic.path << path
          # And get the next pair of cells
          endpoint = next_endpoint
          next_endpoint = heuristic.came_from[endpoint]
          # Continue till there are no more cells
        end
      end
    
      # Returns a list of adjacent cells
      # Used to determine what the next cells to be added to the frontier are
      def adjacent_neighbors(cell)
        neighbors = []
    
        # Gets all the valid neighbors into the array
        # From southern neighbor, clockwise
        neighbors << [cell.x    , cell.y - 1] unless cell.y == 0
        neighbors << [cell.x - 1, cell.y    ] unless cell.x == 0
        neighbors << [cell.x    , cell.y + 1] unless cell.y == grid.height - 1
        neighbors << [cell.x + 1, cell.y    ] unless cell.x == grid.width - 1
    
        neighbors
      end
    
      # Finds the vertical and horizontal distance of a cell from the star
      # and returns the larger value
      # This method is used to have a zigzag pattern in the rendered path
      # A cell that is [5, 5] from the star,
      # is explored before over a cell that is [0, 7] away.
      # So, if possible, the search tries to go diagonal (zigzag) first
      def proximity_to_star(cell)
        distance_x = (grid.star.x - cell.x).abs
        distance_y = (grid.star.y - cell.y).abs
    
        [distance_x, distance_y].max
      end
    
      # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored.
      def grid
        state.grid
      end
    
      def buttons
        state.buttons
      end
    
      def slider
        state.slider
      end
    
      def bfs
        state.bfs
      end
    
      def heuristic
        state.heuristic
      end
    
      # Descriptive aliases for colors
      def default_color
        { r: 221, g: 212, b: 213 }
      end
    
      def wall_color
        { r: 134, g: 134, b: 120 }
      end
    
      def visited_color
        { r: 204, g: 191, b: 179 }
      end
    
      def frontier_color
        { r: 103, g: 136, b: 204, a: 200 }
      end
    
      def path_color
        { r: 231, g: 230, b: 228 }
      end
    
      def button_color
        [190, 190, 190] # Gray
      end
    end
    # Method that is called by DragonRuby periodically
    # Used for updating animations and calculations
    def tick args
    
      # Pressing r will reset the application
      if args.inputs.keyboard.key_down.r
        args.gtk.reset
        reset
        return
      end
    
      # Every tick, new args are passed, and the Breadth First Search tick is called
      $heuristic_with_walls ||= Heuristic_With_Walls.new
      $heuristic_with_walls.args = args
      $heuristic_with_walls.tick
    end
    
    
    def reset
      $heuristic_with_walls = nil
    end
    
    

    A Star - main.rb link

    # ./samples/13_path_finding_algorithms/08_a_star/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 A* Search works by incorporating both the distance from the starting point
    # and the distance from the target in its heurisitic.
    
    # It tends to find the correct (shortest) path even when the Greedy Best-First Search does not,
    # and it explores less of the grid, and is therefore faster, than Dijkstra's Search.
    
    class A_Star_Algorithm
      attr_gtk
    
      def tick
        defaults
        render
        input
    
        if dijkstra.came_from.empty?
          calc_searches
        end
      end
    
      def defaults
        # Variables to edit the size and appearance of the grid
        # Freely customizable to user's liking
        grid.width     ||= 15
        grid.height    ||= 15
        grid.cell_size ||= 27
        grid.rect      ||= [0, 0, grid.width, grid.height]
    
        grid.star      ||= [0, 2]
        grid.target    ||= [11, 13]
        grid.walls     ||= {
          [2, 2] => true,
          [3, 2] => true,
          [4, 2] => true,
          [5, 2] => true,
          [6, 2] => true,
          [7, 2] => true,
          [8, 2] => true,
          [9, 2] => true,
          [10, 2] => true,
          [11, 2] => true,
          [12, 2] => true,
          [12, 3] => true,
          [12, 4] => true,
          [12, 5] => true,
          [12, 6] => true,
          [12, 7] => true,
          [12, 8] => true,
          [12, 9] => true,
          [12, 10] => true,
          [12, 11] => true,
          [12, 12] => true,
          [5, 12] => true,
          [6, 12] => true,
          [7, 12] => true,
          [8, 12] => true,
          [9, 12] => true,
          [10, 12] => true,
          [11, 12] => true,
          [12, 12] => true
        }
    
        # What the user is currently editing on the grid
        # We store this value, because we want to remember the value even when
        # the user's cursor is no longer over what they're interacting with, but
        # they are still clicking down on the mouse.
        state.user_input ||= :none
    
        # These variables allow the breadth first search to take place
        # Came_from is a hash with a key of a cell and a value of the cell that was expanded from to find the key.
        # Used to prevent searching cells that have already been found
        # and to trace a path from the target back to the starting point.
        # Frontier is an array of cells to expand the search from.
        # The search is over when there are no more cells to search from.
        # Path stores the path from the target to the star, once the target has been found
        # It prevents calculating the path every tick.
        dijkstra.came_from   ||= {}
        dijkstra.cost_so_far ||= {}
        dijkstra.frontier    ||= []
        dijkstra.path        ||= []
    
        greedy.came_from ||= {}
        greedy.frontier  ||= []
        greedy.path      ||= []
    
        a_star.frontier    ||= []
        a_star.came_from   ||= {}
        a_star.path        ||= []
        a_star.cost_so_far ||= {}
      end
    
      # All methods with render draw stuff on the screen
      # UI has buttons, the slider, and labels
      # The search specific rendering occurs in the respective methods
      def render
        render_labels
        render_dijkstra
        render_greedy
        render_a_star
      end
    
      def render_labels
        outputs.labels << [150, 450, "Dijkstra's"]
        outputs.labels << [550, 450, "Greedy Best-First"]
        outputs.labels << [1025, 450, "A* Search"]
      end
    
      def render_dijkstra
        render_dijkstra_grid
        render_dijkstra_star
        render_dijkstra_target
        render_dijkstra_visited
        render_dijkstra_walls
        render_dijkstra_path
      end
    
      def render_greedy
        render_greedy_grid
        render_greedy_star
        render_greedy_target
        render_greedy_visited
        render_greedy_walls
        render_greedy_path
      end
    
      def render_a_star
        render_a_star_grid
        render_a_star_star
        render_a_star_target
        render_a_star_visited
        render_a_star_walls
        render_a_star_path
      end
    
      # This method handles user input every tick
      def input
        # If the mouse was lifted this tick
        if inputs.mouse.up
          # Set current input to none
          state.user_input = :none
        end
    
        # If the mouse was clicked this tick
        if inputs.mouse.down
          # Determine what the user is editing and appropriately edit the state.user_input variable
          determine_input
        end
    
        # Process user input based on user_input variable and current mouse position
        process_input
      end
    
      # Determines what the user is editing
      # This method is called when the mouse is clicked down
      def determine_input
        # If the mouse is over the star in the first grid
        if dijkstra_mouse_over_star?
          # The user is editing the star from the first grid
          state.user_input = :dijkstra_star
        # If the mouse is over the star in the second grid
        elsif greedy_mouse_over_star?
          # The user is editing the star from the second grid
          state.user_input = :greedy_star
        # If the mouse is over the star in the third grid
        elsif a_star_mouse_over_star?
          # The user is editing the star from the third grid
          state.user_input = :a_star_star
        # If the mouse is over the target in the first grid
        elsif dijkstra_mouse_over_target?
          # The user is editing the target from the first grid
          state.user_input = :dijkstra_target
        # If the mouse is over the target in the second grid
        elsif greedy_mouse_over_target?
          # The user is editing the target from the second grid
          state.user_input = :greedy_target
        # If the mouse is over the target in the third grid
        elsif a_star_mouse_over_target?
          # The user is editing the target from the third grid
          state.user_input = :a_star_target
        # If the mouse is over a wall in the first grid
        elsif dijkstra_mouse_over_wall?
          # The user is removing a wall from the first grid
          state.user_input = :dijkstra_remove_wall
        # If the mouse is over a wall in the second grid
        elsif greedy_mouse_over_wall?
          # The user is removing a wall from the second grid
          state.user_input = :greedy_remove_wall
        # If the mouse is over a wall in the third grid
        elsif a_star_mouse_over_wall?
          # The user is removing a wall from the third grid
          state.user_input = :a_star_remove_wall
        # If the mouse is over the first grid
        elsif dijkstra_mouse_over_grid?
          # The user is adding a wall from the first grid
          state.user_input = :dijkstra_add_wall
        # If the mouse is over the second grid
        elsif greedy_mouse_over_grid?
          # The user is adding a wall from the second grid
          state.user_input = :greedy_add_wall
        # If the mouse is over the third grid
        elsif a_star_mouse_over_grid?
          # The user is adding a wall from the third grid
          state.user_input = :a_star_add_wall
        end
      end
    
      # Processes click and drag based on what the user is currently dragging
      def process_input
        if state.user_input == :dijkstra_star
          process_input_dijkstra_star
        elsif state.user_input == :greedy_star
          process_input_greedy_star
        elsif state.user_input == :a_star_star
          process_input_a_star_star
        elsif state.user_input == :dijkstra_target
          process_input_dijkstra_target
        elsif state.user_input == :greedy_target
          process_input_greedy_target
        elsif state.user_input == :a_star_target
          process_input_a_star_target
        elsif state.user_input == :dijkstra_remove_wall
          process_input_dijkstra_remove_wall
        elsif state.user_input == :greedy_remove_wall
          process_input_greedy_remove_wall
        elsif state.user_input == :a_star_remove_wall
          process_input_a_star_remove_wall
        elsif state.user_input == :dijkstra_add_wall
          process_input_dijkstra_add_wall
        elsif state.user_input == :greedy_add_wall
          process_input_greedy_add_wall
        elsif state.user_input == :a_star_add_wall
          process_input_a_star_add_wall
        end
      end
    
      def render_dijkstra_grid
        # A large rect the size of the grid
        outputs.solids << dijkstra_scale_up(grid.rect).merge(default_color)
    
        outputs.lines << (0..grid.width).map { |x| dijkstra_vertical_line(x) }
        outputs.lines << (0..grid.height).map { |y| dijkstra_horizontal_line(y) }
      end
    
      def render_greedy_grid
        # A large rect the size of the grid
        outputs.solids << greedy_scale_up(grid.rect).merge(default_color)
    
        outputs.lines << (0..grid.width).map { |x| greedy_vertical_line(x) }
        outputs.lines << (0..grid.height).map { |y| greedy_horizontal_line(y) }
      end
    
      def render_a_star_grid
        # A large rect the size of the grid
        outputs.solids << a_star_scale_up(grid.rect).merge(default_color)
    
        outputs.lines << (0..grid.width).map { |x| a_star_vertical_line(x) }
        outputs.lines << (0..grid.height).map { |y| a_star_horizontal_line(y) }
      end
    
      # Returns a vertical line for a column of the first grid
      def dijkstra_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 dijkstra_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 greedy_vertical_line x
        dijkstra_vertical_line(x + grid.width + 1)
      end
    
      # Returns a horizontal line for a column of the second grid
      def greedy_horizontal_line y
        line = { x: grid.width + 1, 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 third grid
      def a_star_vertical_line x
        dijkstra_vertical_line(x + grid.width + 1 + grid.width + 1)
      end
    
      # Returns a horizontal line for a column of the third grid
      def a_star_horizontal_line y
        line = { x: grid.width + 1 + 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_dijkstra_star
        outputs.sprites << dijkstra_scale_up(grid.star).merge({ path: 'star.png' })
      end
    
      # Renders the star on the second grid
      def render_greedy_star
        outputs.sprites << greedy_scale_up(grid.star).merge({ path: 'star.png' })
      end
    
      # Renders the star on the third grid
      def render_a_star_star
        outputs.sprites << a_star_scale_up(grid.star).merge({ path: 'star.png' })
      end
    
      # Renders the target on the first grid
      def render_dijkstra_target
        outputs.sprites << dijkstra_scale_up(grid.target).merge({ path: 'target.png' })
      end
    
      # Renders the target on the second grid
      def render_greedy_target
        outputs.sprites << greedy_scale_up(grid.target).merge({ path: 'target.png' })
      end
    
      # Renders the target on the third grid
      def render_a_star_target
        outputs.sprites << a_star_scale_up(grid.target).merge({ path: 'target.png' })
      end
    
      # Renders the walls on the first grid
      def render_dijkstra_walls
        outputs.solids << grid.walls.map do |key, value|
          dijkstra_scale_up(key).merge(wall_color)
        end
      end
    
      # Renders the walls on the second grid
      def render_greedy_walls
        outputs.solids << grid.walls.map do |key, value|
          greedy_scale_up(key).merge(wall_color)
        end
      end
    
      # Renders the walls on the third grid
      def render_a_star_walls
        outputs.solids << grid.walls.map do |key, value|
          a_star_scale_up(key).merge(wall_color)
        end
      end
    
      # Renders the visited cells on the first grid
      def render_dijkstra_visited
        outputs.solids << dijkstra.came_from.map do |key, value|
          dijkstra_scale_up(key).merge(visited_color)
        end
      end
    
      # Renders the visited cells on the second grid
      def render_greedy_visited
        outputs.solids << greedy.came_from.map do |key, value|
          greedy_scale_up(key).merge(visited_color)
        end
      end
    
      # Renders the visited cells on the third grid
      def render_a_star_visited
        outputs.solids << a_star.came_from.map do |key, value|
          a_star_scale_up(key).merge(visited_color)
        end
      end
    
      # Renders the path found by the breadth first search on the first grid
      def render_dijkstra_path
        outputs.solids << dijkstra.path.map do |path|
          dijkstra_scale_up(path).merge(path_color)
        end
      end
    
      # Renders the path found by the greedy search on the second grid
      def render_greedy_path
        outputs.solids << greedy.path.map do |path|
          greedy_scale_up(path).merge(path_color)
        end
      end
    
      # Renders the path found by the a_star search on the third grid
      def render_a_star_path
        outputs.solids << a_star.path.map do |path|
          a_star_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 = []
    
        # 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 dijkstra_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
    
      # Translates the given cell grid.width + 1 to the right and then scales up
      # Used to draw cells for the second grid
      # This method does not work for lines,
      # so separate methods exist for the grid lines
      def greedy_scale_up(cell)
        # Prevents the original value of cell from being edited
        cell = cell.clone
        # Translates the cell to the second grid equivalent
        cell.x += grid.width + 1
        # Proceeds as if scaling up for the first grid
        dijkstra_scale_up(cell)
      end
    
      # Translates the given cell (grid.width + 1) * 2 to the right and then scales up
      # Used to draw cells for the third grid
      # This method does not work for lines,
      # so separate methods exist for the grid lines
      def a_star_scale_up(cell)
        # Prevents the original value of cell from being edited
        cell = cell.clone
        # Translates the cell to the second grid equivalent
        cell.x += grid.width + 1
        # Translates the cell to the third grid equivalent
        cell.x += grid.width + 1
        # Proceeds as if scaling up for the first grid
        dijkstra_scale_up(cell)
      end
    
      # Signal that the user is going to be moving the star from the first grid
      def dijkstra_mouse_over_star?
        inputs.mouse.point.inside_rect?(dijkstra_scale_up(grid.star))
      end
    
      # Signal that the user is going to be moving the star from the second grid
      def greedy_mouse_over_star?
        inputs.mouse.point.inside_rect?(greedy_scale_up(grid.star))
      end
    
      # Signal that the user is going to be moving the star from the third grid
      def a_star_mouse_over_star?
        inputs.mouse.point.inside_rect?(a_star_scale_up(grid.star))
      end
    
      # Signal that the user is going to be moving the target from the first grid
      def dijkstra_mouse_over_target?
        inputs.mouse.point.inside_rect?(dijkstra_scale_up(grid.target))
      end
    
      # Signal that the user is going to be moving the target from the second grid
      def greedy_mouse_over_target?
        inputs.mouse.point.inside_rect?(greedy_scale_up(grid.target))
      end
    
      # Signal that the user is going to be moving the target from the third grid
      def a_star_mouse_over_target?
        inputs.mouse.point.inside_rect?(a_star_scale_up(grid.target))
      end
    
      # Signal that the user is going to be removing walls from the first grid
      def dijkstra_mouse_over_wall?
        grid.walls.each_key do | wall |
          return true if inputs.mouse.point.inside_rect?(dijkstra_scale_up(wall))
        end
    
        false
      end
    
      # Signal that the user is going to be removing walls from the second grid
      def greedy_mouse_over_wall?
        grid.walls.each_key do | wall |
          return true if inputs.mouse.point.inside_rect?(greedy_scale_up(wall))
        end
    
        false
      end
    
      # Signal that the user is going to be removing walls from the third grid
      def a_star_mouse_over_wall?
        grid.walls.each_key do | wall |
          return true if inputs.mouse.point.inside_rect?(a_star_scale_up(wall))
        end
    
        false
      end
    
      # Signal that the user is going to be adding walls from the first grid
      def dijkstra_mouse_over_grid?
        inputs.mouse.point.inside_rect?(dijkstra_scale_up(grid.rect))
      end
    
      # Signal that the user is going to be adding walls from the second grid
      def greedy_mouse_over_grid?
        inputs.mouse.point.inside_rect?(greedy_scale_up(grid.rect))
      end
    
      # Signal that the user is going to be adding walls from the third grid
      def a_star_mouse_over_grid?
        inputs.mouse.point.inside_rect?(a_star_scale_up(grid.rect))
      end
    
      # Moves the star to the cell closest to the mouse in the first grid
      # Only resets the search if the star changes position
      # Called whenever the user is editing the star (puts mouse down on star)
      def process_input_dijkstra_star
        old_star = grid.star.clone
        unless dijkstra_cell_closest_to_mouse == grid.target
          grid.star = dijkstra_cell_closest_to_mouse
        end
        unless old_star == grid.star
          reset_searches
        end
      end
    
      # Moves the star to the cell closest to the mouse in the second grid
      # Only resets the search if the star changes position
      # Called whenever the user is editing the star (puts mouse down on star)
      def process_input_greedy_star
        old_star = grid.star.clone
        unless greedy_cell_closest_to_mouse == grid.target
          grid.star = greedy_cell_closest_to_mouse
        end
        unless old_star == grid.star
          reset_searches
        end
      end
    
      # Moves the star to the cell closest to the mouse in the third grid
      # Only resets the search if the star changes position
      # Called whenever the user is editing the star (puts mouse down on star)
      def process_input_a_star_star
        old_star = grid.star.clone
        unless a_star_cell_closest_to_mouse == grid.target
          grid.star = a_star_cell_closest_to_mouse
        end
        unless old_star == grid.star
          reset_searches
        end
      end
    
      # Moves the target to the grid closest to the mouse in the first grid
      # Only reset_searchess the search if the target changes position
      # Called whenever the user is editing the target (puts mouse down on target)
      def process_input_dijkstra_target
        old_target = grid.target.clone
        unless dijkstra_cell_closest_to_mouse == grid.star
          grid.target = dijkstra_cell_closest_to_mouse
        end
        unless old_target == grid.target
          reset_searches
        end
      end
    
      # Moves the target to the cell closest to the mouse in the second grid
      # Only reset_searchess the search if the target changes position
      # Called whenever the user is editing the target (puts mouse down on target)
      def process_input_greedy_target
        old_target = grid.target.clone
        unless greedy_cell_closest_to_mouse == grid.star
          grid.target = greedy_cell_closest_to_mouse
        end
        unless old_target == grid.target
          reset_searches
        end
      end
    
      # Moves the target to the cell closest to the mouse in the third grid
      # Only reset_searchess the search if the target changes position
      # Called whenever the user is editing the target (puts mouse down on target)
      def process_input_a_star_target
        old_target = grid.target.clone
        unless a_star_cell_closest_to_mouse == grid.star
          grid.target = a_star_cell_closest_to_mouse
        end
        unless old_target == grid.target
          reset_searches
        end
      end
    
      # Removes walls in the first grid that are under the cursor
      def process_input_dijkstra_remove_wall
        # The mouse needs to be inside the grid, because we only want to remove walls
        # the cursor is directly over
        # Recalculations should only occur when a wall is actually deleted
        if dijkstra_mouse_over_grid?
          if grid.walls.has_key?(dijkstra_cell_closest_to_mouse)
            grid.walls.delete(dijkstra_cell_closest_to_mouse)
            reset_searches
          end
        end
      end
    
      # Removes walls in the second grid that are under the cursor
      def process_input_greedy_remove_wall
        # The mouse needs to be inside the grid, because we only want to remove walls
        # the cursor is directly over
        # Recalculations should only occur when a wall is actually deleted
        if greedy_mouse_over_grid?
          if grid.walls.key?(greedy_cell_closest_to_mouse)
            grid.walls.delete(greedy_cell_closest_to_mouse)
            reset_searches
          end
        end
      end
    
      # Removes walls in the third grid that are under the cursor
      def process_input_a_star_remove_wall
        # The mouse needs to be inside the grid, because we only want to remove walls
        # the cursor is directly over
        # Recalculations should only occur when a wall is actually deleted
        if a_star_mouse_over_grid?
          if grid.walls.key?(a_star_cell_closest_to_mouse)
            grid.walls.delete(a_star_cell_closest_to_mouse)
            reset_searches
          end
        end
      end
    
      # Adds a wall in the first grid in the cell the mouse is over
      def process_input_dijkstra_add_wall
        if dijkstra_mouse_over_grid?
          unless grid.walls.key?(dijkstra_cell_closest_to_mouse)
            grid.walls[dijkstra_cell_closest_to_mouse] = true
            reset_searches
          end
        end
      end
    
      # Adds a wall in the second grid in the cell the mouse is over
      def process_input_greedy_add_wall
        if greedy_mouse_over_grid?
          unless grid.walls.key?(greedy_cell_closest_to_mouse)
            grid.walls[greedy_cell_closest_to_mouse] = true
            reset_searches
          end
        end
      end
    
      # Adds a wall in the third grid in the cell the mouse is over
      def process_input_a_star_add_wall
        if a_star_mouse_over_grid?
          unless grid.walls.key?(a_star_cell_closest_to_mouse)
            grid.walls[a_star_cell_closest_to_mouse] = true
            reset_searches
          end
        end
      end
    
      # When the user grabs the star and puts their cursor to the far right
      # and moves up and down, the star is supposed to move along the grid as well
      # Finding the cell closest to the mouse helps with this
      def dijkstra_cell_closest_to_mouse
        # Closest cell to the mouse in the first grid
        x = (inputs.mouse.point.x / grid.cell_size).to_i
        y = (inputs.mouse.point.y / grid.cell_size).to_i
        # Bound x and y to the grid
        x = grid.width - 1 if x > grid.width - 1
        y = grid.height - 1 if y > grid.height - 1
        # Return closest cell
        [x, y]
      end
    
      # When the user grabs the star and puts their cursor to the far right
      # and moves up and down, the star is supposed to move along the grid as well
      # Finding the cell closest to the mouse in the second grid helps with this
      def greedy_cell_closest_to_mouse
        # Closest cell grid to the mouse in the second
        x = (inputs.mouse.point.x / grid.cell_size).to_i
        y = (inputs.mouse.point.y / grid.cell_size).to_i
        # Translate the cell to the first grid
        x -= grid.width + 1
        # Bound x and y to the first grid
        x = 0 if x < 0
        y = 0 if y < 0
        x = grid.width - 1 if x > grid.width - 1
        y = grid.height - 1 if y > grid.height - 1
        # Return closest cell
        [x, y]
      end
    
      # When the user grabs the star and puts their cursor to the far right
      # and moves up and down, the star is supposed to move along the grid as well
      # Finding the cell closest to the mouse in the third grid helps with this
      def a_star_cell_closest_to_mouse
        # Closest cell grid to the mouse in the second
        x = (inputs.mouse.point.x / grid.cell_size).to_i
        y = (inputs.mouse.point.y / grid.cell_size).to_i
        # Translate the cell to the first grid
        x -= (grid.width + 1) * 2
        # Bound x and y to the first grid
        x = 0 if x < 0
        y = 0 if y < 0
        x = grid.width - 1 if x > grid.width - 1
        y = grid.height - 1 if y > grid.height - 1
        # Return closest cell
        [x, y]
      end
    
      def reset_searches
        # Reset the searches
        dijkstra.came_from      = {}
        dijkstra.cost_so_far    = {}
        dijkstra.frontier       = []
        dijkstra.path           = []
    
        greedy.came_from = {}
        greedy.frontier  = []
        greedy.path      = []
        a_star.came_from = {}
        a_star.frontier  = []
        a_star.path      = []
      end
    
      def calc_searches
        calc_dijkstra
        calc_greedy
        calc_a_star
        # Move the searches forward to the current step
        # state.current_step.times { move_searches_one_step_forward }
      end
    
      def calc_dijkstra
        # Sets up the search to begin from the star
        dijkstra.frontier << grid.star
        dijkstra.came_from[grid.star] = nil
        dijkstra.cost_so_far[grid.star] = 0
    
        # Until the target is found or there are no more cells to explore from
        until dijkstra.came_from.key?(grid.target) or dijkstra.frontier.empty?
          # Take the next frontier cell. The first element is the cell, the second is the priority.
          new_frontier = dijkstra.frontier.shift#[0]
          # For each of its neighbors
          adjacent_neighbors(new_frontier).each do | neighbor |
            # That have not been visited and are not walls
            unless dijkstra.came_from.key?(neighbor) or grid.walls.key?(neighbor)
              # Add them to the frontier and mark them as visited
              dijkstra.frontier << neighbor
              dijkstra.came_from[neighbor] = new_frontier
              dijkstra.cost_so_far[neighbor] = dijkstra.cost_so_far[new_frontier] + 1
            end
          end
    
          # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
          # Comment this line and let a path generate to see the difference
          dijkstra.frontier = dijkstra.frontier.sort_by {| cell | proximity_to_star(cell) }
          dijkstra.frontier = dijkstra.frontier.sort_by {| cell | dijkstra.cost_so_far[cell] }
        end
    
    
        # If the search found the target
        if dijkstra.came_from.key?(grid.target)
          # Calculate the path between the target and star
          dijkstra_calc_path
        end
      end
    
      def calc_greedy
        # Sets up the search to begin from the star
        greedy.frontier << grid.star
        greedy.came_from[grid.star] = nil
    
        # Until the target is found or there are no more cells to explore from
        until greedy.came_from.key?(grid.target) or greedy.frontier.empty?
          # Take the next frontier cell
          new_frontier = greedy.frontier.shift
          # For each of its neighbors
          adjacent_neighbors(new_frontier).each do | neighbor |
            # That have not been visited and are not walls
            unless greedy.came_from.key?(neighbor) or grid.walls.key?(neighbor)
              # Add them to the frontier and mark them as visited
              greedy.frontier << neighbor
              greedy.came_from[neighbor] = new_frontier
            end
          end
          # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
          # Comment this line and let a path generate to see the difference
          greedy.frontier = greedy.frontier.sort_by {| cell | proximity_to_star(cell) }
          # Sort the frontier so cells that are close to the target are then prioritized
          greedy.frontier = greedy.frontier.sort_by {| cell | greedy_heuristic(cell)  }
        end
    
    
        # If the search found the target
        if greedy.came_from.key?(grid.target)
          # Calculate the path between the target and star
          greedy_calc_path
        end
      end
    
      def calc_a_star
        # Setup the search to start from the star
        a_star.came_from[grid.star] = nil
        a_star.cost_so_far[grid.star] = 0
        a_star.frontier << grid.star
    
        # Until there are no more cells to explore from or the search has found the target
        until a_star.frontier.empty? or a_star.came_from.key?(grid.target)
          # Get the next cell to expand from
          current_frontier = a_star.frontier.shift
    
          # For each of that cells neighbors
          adjacent_neighbors(current_frontier).each do | neighbor |
            # That have not been visited and are not walls
            unless a_star.came_from.key?(neighbor) or grid.walls.key?(neighbor)
              # Add them to the frontier and mark them as visited
              a_star.frontier << neighbor
              a_star.came_from[neighbor] = current_frontier
              a_star.cost_so_far[neighbor] = a_star.cost_so_far[current_frontier] + 1
            end
          end
    
          # Sort the frontier so that cells that are in a zigzag pattern are prioritized over those in an line
          # Comment this line and let a path generate to see the difference
          a_star.frontier = a_star.frontier.sort_by {| cell | proximity_to_star(cell) }
          a_star.frontier = a_star.frontier.sort_by {| cell | a_star.cost_so_far[cell] + greedy_heuristic(cell) }
        end
    
        # If the search found the target
        if a_star.came_from.key?(grid.target)
          # Calculate the path between the target and star
          a_star_calc_path
        end
      end
    
      # Calculates the path between the target and star for the breadth first search
      # Only called when the breadth first search finds the target
      def dijkstra_calc_path
        # Start from the target
        endpoint = grid.target
        # And the cell it came from
        next_endpoint = dijkstra.came_from[endpoint]
        while endpoint && next_endpoint
          # Draw a path between these two cells and store it
          path = get_path_between(endpoint, next_endpoint)
          dijkstra.path << path
          # And get the next pair of cells
          endpoint = next_endpoint
          next_endpoint = dijkstra.came_from[endpoint]
          # Continue till there are no more cells
        end
      end
    
      # Returns one-dimensional absolute distance between cell and target
      # Returns a number to compare distances between cells and the target
      def greedy_heuristic(cell)
        (grid.target.x - cell.x).abs + (grid.target.y - cell.y).abs
      end
    
      # Calculates the path between the target and star for the greedy search
      # Only called when the greedy search finds the target
      def greedy_calc_path
        # Start from the target
        endpoint = grid.target
        # And the cell it came from
        next_endpoint = greedy.came_from[endpoint]
        while endpoint && next_endpoint
          # Draw a path between these two cells and store it
          path = get_path_between(endpoint, next_endpoint)
          greedy.path << path
          # And get the next pair of cells
          endpoint = next_endpoint
          next_endpoint = greedy.came_from[endpoint]
          # Continue till there are no more cells
        end
      end
    
      # Calculates the path between the target and star for the a_star search
      # Only called when the a_star search finds the target
      def a_star_calc_path
        # Start from the target
        endpoint = grid.target
        # And the cell it came from
        next_endpoint = a_star.came_from[endpoint]
    
        while endpoint && next_endpoint
          # Draw a path between these two cells and store it
          path = get_path_between(endpoint, next_endpoint)
          a_star.path << path
          # And get the next pair of cells
          endpoint = next_endpoint
          next_endpoint = a_star.came_from[endpoint]
          # Continue till there are no more cells
        end
      end
    
      # Returns a list of adjacent cells
      # Used to determine what the next cells to be added to the frontier are
      def adjacent_neighbors(cell)
        neighbors = []
    
        # Gets all the valid neighbors into the array
        # From southern neighbor, clockwise
        neighbors << [cell.x    , cell.y - 1] unless cell.y == 0
        neighbors << [cell.x - 1, cell.y    ] unless cell.x == 0
        neighbors << [cell.x    , cell.y + 1] unless cell.y == grid.height - 1
        neighbors << [cell.x + 1, cell.y    ] unless cell.x == grid.width - 1
    
        neighbors
      end
    
      # Finds the vertical and horizontal distance of a cell from the star
      # and returns the larger value
      # This method is used to have a zigzag pattern in the rendered path
      # A cell that is [5, 5] from the star,
      # is explored before over a cell that is [0, 7] away.
      # So, if possible, the search tries to go diagonal (zigzag) first
      def proximity_to_star(cell)
        distance_x = (grid.star.x - cell.x).abs
        distance_y = (grid.star.y - cell.y).abs
    
        if distance_x > distance_y
          return distance_x
        else
          return distance_y
        end
      end
    
      # Methods that allow code to be more concise. Subdivides args.state, which is where all variables are stored.
      def grid
        state.grid
      end
    
      def dijkstra
        state.dijkstra
      end
    
      def greedy
        state.greedy
      end
    
      def a_star
        state.a_star
      end
    
      # Descriptive aliases for colors
      def default_color
        { r: 221, g: 212, b: 213 }
      end
    
      def wall_color
        { r: 134, g: 134, b: 120 }
      end
    
      def visited_color
        { r: 204, g: 191, b: 179 }
      end
    
      def path_color
        { r: 231, g: 230, b: 228 }
      end
    
      def button_color
        [190, 190, 190] # Gray
      end
    end
    
    
    # Method that is called by DragonRuby periodically
    # Used for updating animations and calculations
    def tick args
    
      # Pressing r will reset the application
      if args.inputs.keyboard.key_down.r
        args.gtk.reset
        reset
        return
      end
    
      # Every tick, new args are passed, and the Breadth First Search tick is called
      $a_star_algorithm ||= A_Star_Algorithm.new
      $a_star_algorithm.args = args
      $a_star_algorithm.tick
    end
    
    
    def reset
      $a_star_algorithm = nil
    end
    
    

    Tower Defense - main.rb link

    # ./samples/13_path_finding_algorithms/09_tower_defense/app/main.rb
    # Contributors outside of DragonRuby who also hold Copyright:
    # - Sujay Vadlakonda: https://github.com/sujayvadlakonda
    
    # An example of some major components in a tower defence game
    # The pathing of the tanks is determined by A* algorithm -- try editing the walls
    
    # The turrets shoot bullets at the closest tank. The bullets are heat-seeking
    
    def tick args
      $gtk.reset if args.inputs.keyboard.key_down.r
      defaults args
      render args
      calc args
    end
    
    def defaults args
      args.outputs.background_color = wall_color
      args.state.grid_size = 5
      args.state.tile_size = 50
      args.state.grid_start ||= [0, 0]
      args.state.grid_goal  ||= [4, 4]
    
      # Try editing these walls to see the path change!
      args.state.walls ||= {
        [0, 4] => true,
        [1, 3] => true,
        [3, 1] => true,
        # [4, 0] => true,
      }
    
      args.state.a_star.frontier ||= []
      args.state.a_star.came_from ||= {}
      args.state.a_star.path ||= []
    
      args.state.tanks ||= []
      args.state.tank_spawn_period ||= 60
      args.state.tank_sprite_path ||= 'sprites/circle/white.png'
      args.state.tank_speed ||= 1
    
      args.state.turret_shoot_period = 10
      # Turrets can be entered as [x, y] but are immediately mapped to hashes
      # Walls are also added where the turrets are to prevent tanks from pathing over them
      args.state.turrets ||= [
        [2, 2]
      ].each { |turret| args.state.walls[turret] = true}.map do |x, y|
        {
          x: x * args.state.tile_size,
          y: y * args.state.tile_size,
          w: args.state.tile_size,
          h: args.state.tile_size,
          path: 'sprites/circle/gray.png',
          range: 100
        }
      end
    
      args.state.bullet_size ||= 25
      args.state.bullets ||= []
      args.state.bullet_path ||= 'sprites/circle/orange.png'
    end
    
    def render args
      render_grid args
      render_a_star args
      args.outputs.sprites << args.state.tanks
      args.outputs.sprites << args.state.turrets
      args.outputs.sprites << args.state.bullets
    end
    
    def render_grid args
      # Draw a square the size and color of the grid
      args.outputs.solids << {
        x: 0,
        y: 0,
        w: args.state.grid_size * args.state.tile_size,
        h: args.state.grid_size * args.state.tile_size,
      }.merge(grid_color)
    
      # Draw lines across the grid to show tiles
      (args.state.grid_size + 1).times do | value |
        render_horizontal_line(args, value)
        render_vertical_line(args, value)
      end
    
      # Render special tiles
      render_tile(args, args.state.grid_start, start_color)
      render_tile(args, args.state.grid_goal, goal_color)
      args.state.walls.keys.each { |wall| render_tile(args, wall, wall_color) }
    end
    
    def render_vertical_line args, x
      args.outputs.lines << {
        x: x * args.state.tile_size,
        y: 0,
        w: 0,
        h: args.state.grid_size * args.state.tile_size
      }
    end
    
    def render_horizontal_line args, y
      args.outputs.lines << {
        x: 0,
        y: y * args.state.tile_size,
        w: args.state.grid_size * args.state.tile_size,
        h: 0
      }
    end
    
    def render_tile args, tile, color
      args.outputs.solids << {
        x: tile.x * args.state.tile_size,
        y: tile.y * args.state.tile_size,
        w: args.state.tile_size,
        h: args.state.tile_size,
        r: color[0],
        g: color[1],
        b: color[2]
      }
    end
    
    def calc args
      calc_a_star args
      calc_tanks args
      calc_turrets args
      calc_bullets args
    end
    
    def calc_a_star args
      # Only does this one time
      return unless args.state.a_star.path.empty?
    
      # Start the search from the grid start
      args.state.a_star.frontier << args.state.grid_start
      args.state.a_star.came_from[args.state.grid_start] = nil
    
      # Until a path to the goal has been found or there are no more tiles to explore
      until (args.state.a_star.came_from.key?(args.state.grid_goal) || args.state.a_star.frontier.empty?)
        # For the first tile in the frontier
        tile_to_expand_from = args.state.a_star.frontier.shift
        # Add each of its neighbors to the frontier
        neighbors(args, tile_to_expand_from).each do |tile|
          args.state.a_star.frontier << tile
          args.state.a_star.came_from[tile] = tile_to_expand_from
        end
      end
    
      # Stop calculating a path if the goal was never reached
      return unless args.state.a_star.came_from.key? args.state.grid_goal
    
      # Fill path by tracing back from the goal
      current_cell = args.state.grid_goal
      while current_cell
        args.state.a_star.path.unshift current_cell
        current_cell = args.state.a_star.came_from[current_cell]
      end
    
      puts "The path has been calculated"
      puts args.state.a_star.path
    end
    
    def calc_tanks args
      spawn_tank args
      move_tanks args
    end
    
    def move_tanks args
      # Remove tanks that have reached the end of their path
      args.state.tanks.reject! { |tank| tank[:a_star].empty? }
    
      # Tanks have an array that has each tile it has to go to in order from a* path
      args.state.tanks.each do | tank |
        destination = tank[:a_star][0]
        # Move the tank towards the destination
        tank[:x] += copy_sign(args.state.tank_speed, ((destination.x * args.state.tile_size) - tank[:x]))
        tank[:y] += copy_sign(args.state.tank_speed, ((destination.y * args.state.tile_size) - tank[:y]))
        # If the tank has reached its destination
        if (destination.x * args.state.tile_size) == tank[:x] &&
            (destination.y * args.state.tile_size) == tank[:y]
          # Set the destination to the next point in the path
          tank[:a_star].shift
        end
      end
    end
    
    def calc_turrets args
      return unless Kernel.tick_count.mod_zero? args.state.turret_shoot_period
      args.state.turrets.each do | turret |
        # Finds the closest tank
        target = nil
        shortest_distance = turret[:range] + 1
        args.state.tanks.each do | tank |
          distance = distance_between(turret[:x], turret[:y], tank[:x], tank[:y])
          if distance < shortest_distance
            target = tank
            shortest_distance = distance
          end
        end
        # If there is a tank in range, fires a bullet
        if target
          args.state.bullets << {
            x: turret[:x],
            y: turret[:y],
            w: args.state.bullet_size,
            h: args.state.bullet_size,
            path: args.state.bullet_path,
            # Note that this makes it heat-seeking, because target is passed by reference
            # Could do target.clone to make the bullet go to where the tank initially was
            target: target
          }
        end
      end
    end
    
    def calc_bullets args
      # Bullets aim for the center of their targets
      args.state.bullets.each { |bullet| move bullet, center_of(bullet[:target])}
      args.state.bullets.reject! { |b| b.intersect_rect? b[:target] }
    end
    
    def center_of object
      object = object.clone
      object[:x] += 0.5
      object[:y] += 0.5
      object
    end
    
    def render_a_star args
      args.state.a_star.path.map do |tile|
        # Map each x, y coordinate to the center of the tile and scale up
        [(tile.x + 0.5) * args.state.tile_size, (tile.y + 0.5) * args.state.tile_size]
      end.inject do | point_a,  point_b |
        # Render the line between each point
        args.outputs.lines << [point_a.x, point_a.y, point_b.x, point_b.y, a_star_color]
        point_b
      end
    end
    
    # Moves object to target at speed
    def move object, target, speed = 1
      if target.is_a? Hash
        object[:x] += copy_sign(speed, target[:x] - object[:x])
        object[:y] += copy_sign(speed, target[:y] - object[:y])
      else
        object[:x] += copy_sign(speed, target.x - object[:x])
        object[:y] += copy_sign(speed, target.y - object[:y])
      end
    end
    
    
    def distance_between a_x, a_y, b_x, b_y
      (((b_x - a_x) ** 2) + ((b_y - a_y) ** 2)) ** 0.5
    end
    
    def copy_sign value, sign
      return 0 if sign == 0
      return value if sign > 0
      -value
    end
    
    def spawn_tank args
      return unless Kernel.tick_count.mod_zero? args.state.tank_spawn_period
      args.state.tanks << {
        x: args.state.grid_start.x,
        y: args.state.grid_start.y,
        w: args.state.tile_size,
        h: args.state.tile_size,
        path: args.state.tank_sprite_path,
        a_star: args.state.a_star.path.clone
      }
    end
    
    def neighbors args, tile
      [[tile.x, tile.y - 1],
       [tile.x, tile.y + 1],
       [tile.x + 1, tile.y],
       [tile.x - 1, tile.y]].reject do |neighbor|
        args.state.a_star.came_from.key?(neighbor) || tile_out_of_bounds?(args, neighbor) ||
          args.state.walls.key?(neighbor)
      end
    end
    
    def tile_out_of_bounds? args, tile
      tile.x < 0 || tile.y < 0 || tile.x >= args.state.grid_size || tile.y >= args.state.grid_size
    end
    
    def grid_color
      { r: 133, g: 226, b: 144 }
    end
    
    def start_color
      [226, 144, 133]
    end
    
    def goal_color
      [226, 133, 144]
    end
    
    def wall_color
      [133, 144, 226]
    end
    
    def a_star_color
      [0, 0, 255]
    end
    
    

    Moveable Squares - main.rb link

    # ./samples/13_path_finding_algorithms/10_moveable_squares/app/main.rb
    class Game
      attr_gtk
    
      def tick
        defaults
        calc
        render
      end
    
      def defaults
        state.square_size ||= 16
        if !state.world
          state.world = {
            w: 80,
            h: 45,
            player: {
              x: 15,
              y: 15,
              speed: 6
            },
            walls: [
              { x: 16, y: 16 },
              { x: 15, y: 16 },
              { x: 14, y: 17 },
              { x: 14, y: 13 },
              { x: 15, y: 13 },
              { x: 16, y: 13 },
              { x: 17, y: 13 }
            ]
          }
        end
      end
    
      def calc
        player = world.player
        player.rect = { x: player.x * state.square_size, y: player.y * state.square_size, w: state.square_size, h: state.square_size }
        player.moveable_squares = entity_moveable_squares world.player
        if inputs.keyboard.key_down.plus
          state.world.player.speed += 1
        elsif inputs.keyboard.key_down.minus
          state.world.player.speed -= 1
          state.world.player.speed = 1 if state.world.player.speed < 1
        end
    
        mouse_ordinal_x = inputs.mouse.x.idiv state.square_size
        mouse_ordinal_y = inputs.mouse.y.idiv state.square_size
    
        if inputs.mouse.click
          if world.walls.any? { |enemy| enemy.x == mouse_ordinal_x && enemy.y == mouse_ordinal_y }
            world.walls.reject! { |enemy| enemy.x == mouse_ordinal_x && enemy.y == mouse_ordinal_y }
          else
            world.walls << { x: mouse_ordinal_x, y: mouse_ordinal_y, speed: 3 }
          end
        end
    
        state.hovered_square = world.player.moveable_squares.find do |square|
          mouse_ordinal_x == square.x && mouse_ordinal_y == square.y
        end
      end
    
      def render
        outputs.primitives << { x: 30, y: 30.from_top, text: "+/- to increase decrease movement radius." }
        outputs.primitives << { x: 30, y: 60.from_top, text: "click to add/remove wall." }
        outputs.primitives << { x: 30, y: 90.from_top, text: "FPS: #{$gtk.current_framerate.to_sf}" }
        if Kernel.tick_count <= 1
          outputs[:world_grid].w = 1280
          outputs[:world_grid].h = 720
          outputs[:world_grid].primitives << state.world.w.flat_map do |x|
            state.world.h.map do |y|
              {
                x: x * state.square_size,
                y: y * state.square_size,
                w: state.square_size,
                h: state.square_size,
                r: 0,
                g: 0,
                b: 0,
                a: 128
              }.border!
            end
          end
        end
    
        outputs[:world_overlay].w = 1280
        outputs[:world_overlay].h = 720
    
        if state.hovered_square
          outputs[:world_overlay].primitives << path_to_square_prefab(state.hovered_square)
        end
    
        outputs[:world_overlay].primitives << world.player.moveable_squares.map do |square|
          square_prefab square, { r: 0, g: 0, b: 128, a: 128 }
        end
    
        outputs[:world_overlay].primitives << world.walls.map do |enemy|
          square_prefab enemy, { r: 128, g: 0, b: 0, a: 200 }
        end
    
        outputs[:world_overlay].primitives << square_prefab(world.player, { r: 0, g: 128, b: 0, a: 200 })
    
        outputs[:world].w = 1280
        outputs[:world].h = 720
        outputs[:world].primitives << { x: 0, y: 0, w: 1280, h: 720, path: :world_grid }
        outputs[:world].primitives << { x: 0, y: 0, w: 1280, h: 720, path: :world_overlay }
        outputs.primitives << { x: 0, y: 0, w: 1280, h: 720, path: :world }
      end
    
      def square_prefab square, color
        {
          x: square.x * state.square_size,
          y: square.y * state.square_size,
          w: state.square_size,
          h: state.square_size,
          **color,
          path: :solid
        }
      end
    
      def path_to_square_prefab moveable_square
        prefab = []
        color = { r: 0, g: 0, b: 128, a: 80 }
        if moveable_square
          prefab << square_prefab(moveable_square, color)
          prefab << path_to_square_prefab(moveable_square.source)
        end
        prefab
      end
    
      def world
        state.world
      end
    
      def entity_moveable_squares entity
        results = {}
        queue = {}
        queue[entity.x] ||= {}
        queue[entity.x][entity.y] = entity
        entity_moveable_squares_recur queue, results while !queue.empty?
        results.flat_map do |x, ys|
          ys.map do |y, value|
            value
          end
        end
      end
    
      def entity_moveable_squares_recur queue, results
        x, ys = queue.first
        return if !x
        return if !ys
        y, to_process = ys.first
        return if !to_process
        queue[to_process.x].delete y
        queue.delete x if queue[x].empty?
        return if results[to_process.x] && results[to_process.x] && results[to_process.x][to_process.y]
    
        neighbors = MoveableLocations.neighbors world, to_process
        neighbors.each do |neighbor|
          if !queue[neighbor.x] || !queue[neighbor.x][neighbor.y]
            queue[neighbor.x] ||= {}
            queue[neighbor.x][neighbor.y] = neighbor
          end
        end
    
        results[to_process.x] ||= {}
        results[to_process.x][to_process.y] = to_process
      end
    end
    
    class MoveableLocations
      class << self
        def neighbors world, square
          return [] if !square
          return [] if square.speed <= 0
          north_square = { x: square.x, y: square.y + 1, speed: square.speed - 1, source: square }
          south_square = { x: square.x, y: square.y - 1, speed: square.speed - 1, source: square }
          east_square  = { x: square.x + 1, y: square.y, speed: square.speed - 1, source: square }
          west_square  = { x: square.x - 1, y: square.y, speed: square.speed - 1, source: square }
          north_east_square = { x: square.x + 1, y: square.y + 1, speed: square.speed - 2, source: square }
          north_west_square = { x: square.x - 1, y: square.y + 1, speed: square.speed - 2, source: square }
          south_east_square = { x: square.x + 1, y: square.y - 1, speed: square.speed - 2, source: square }
          south_west_square = { x: square.x - 1, y: square.y - 1, speed: square.speed - 2, source: square }
          result = []
          north_available = valid? world, north_square
          south_available = valid? world, south_square
          east_available  = valid? world, east_square
          west_available  = valid? world, west_square
          north_east_available = valid? world, north_east_square
          north_west_available = valid? world, north_west_square
          south_east_available = valid? world, south_east_square
          south_west_available = valid? world, south_west_square
          result << north_square if north_available
          result << south_square if south_available
          result << east_square  if east_available
          result << west_square  if west_available
          result << north_east_square if north_available && east_available && north_east_available
          result << north_west_square if north_available && west_available && north_west_available
          result << south_east_square if south_available && east_available && south_east_available
          result << south_west_square if south_available && west_available && south_west_available
          result
        end
    
        def valid? world, square
          return false if !square
          return false if square.speed < 0
          return false if square.x < 0 || square.x >= world.w || square.y < 0 || square.y >= world.h
          return false if world.walls.any? { |enemy| enemy.x == square.x && enemy.y == square.y }
          return false if world.player.x == square.x && world.player.y == square.y
          return true
        end
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    $gtk.reset
    
    

    Vr link

    Skybox - main.rb link

    # ./samples/14_vr/01_skybox/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      tick_game args
    end
    
    

    Skybox - tick.rb link

    # ./samples/14_vr/01_skybox/app/tick.rb
    def skybox args, x, y, z, size
      sprite = { a: 80, path: 'sprites/box.png' }
    
      front      = { x: x, y: y, z: z, w: size, h: size, **sprite }
      front_720  = { x: x, y: y, z: z + 1, w: size, h: size * 9.fdiv(16), **sprite }
      back       = { x: x, y: y, z: z + size, w: size, h: size, **sprite }
      bottom     = { x: x, y: y - size.half, z: z + size.half, w: size, h: size, angle_x: 90, **sprite }
      top        = { x: x, y: y + size.half, z: z + size.half, w: size, h: size, angle_x: 90, **sprite }
      left       = { x: x - size.half, y: y, w: size, h: size, z: z + size.half, angle_y: 90, **sprite }
      right      = { x: x + size.half, y: y, w: size, h: size, z: z + size.half, angle_y: 90, **sprite }
    
      args.outputs.sprites << [back,
                               left,
                               top,
                               bottom,
                               right,
                               front,
                               front_720]
    end
    
    def tick_game args
      args.outputs.background_color = [0, 0, 0]
    
      args.state.z     ||= 0
      args.state.scale ||= 0.05
    
      if args.inputs.controller_one.key_down.a
        if args.grid.name == :bottom_left
          args.grid.origin_center!
        else
          args.grid.origin_bottom_left!
        end
      end
    
      args.state.scale += args.inputs.controller_one.right_analog_x_perc * 0.01
      args.state.z -= args.inputs.controller_one.right_analog_y_perc * 1.5
    
      args.state.scale = args.state.scale.clamp(0.05, 1.0)
      args.state.z = 0    if args.state.z < 0
      args.state.z = 1280 if args.state.z > 1280
    
      skybox args, 0, 0, args.state.z, 1280 * args.state.scale
    
      render_guides args
    end
    
    def render_guides args
      label_style = { alignment_enum: 1,
                      size_enum: -2,
                      vertical_alignment_enum: 0, r: 255, g: 255, b: 255 }
    
      instructions = [
        "controller position: #{args.inputs.controller_one.left_hand.x} #{args.inputs.controller_one.left_hand.y} #{args.inputs.controller_one.left_hand.z}",
        "scale: #{args.state.scale.to_sf} (right analog left/right)",
        "z: #{args.state.z.to_sf} (right analog up/down)",
        "origin: :#{args.grid.name} (A button)",
      ]
    
      args.outputs.labels << instructions.map_with_index do |text, i|
        { x: 640,
          y: 100 + ((instructions.length - (i + 3)) * 22),
          z: args.state.z + 2,
          a: 255,
          text: text,
          ** label_style,
          alignment_enum: 1,
          vertical_alignment_enum: 0 }
      end
    
      # lines for scaled box
      size      = 1280 * args.state.scale
      size_16_9 = size * 9.fdiv(16)
    
      args.outputs.primitives << [
        { x: size - 1280, y: size,        z:            0, w: 1280 * 2, r: 128, g: 128, b: 128, a:  64 }.line!,
        { x: size - 1280, y: size,        z: args.state.z + 2, w: 1280 * 2, r: 128, g: 128, b: 128, a: 255 }.line!,
    
        { x: size - 1280, y: size_16_9,   z:            0, w: 1280 * 2, r: 128, g: 128, b: 128, a:  64 }.line!,
        { x: size - 1280, y: size_16_9,   z: args.state.z + 2, w: 1280 * 2, r: 128, g: 128, b: 128, a: 255 }.line!,
    
        { x: size,        y: size - 1280, z:            0, h: 1280 * 2, r: 128, g: 128, b: 128, a:  64 }.line!,
        { x: size,        y: size - 1280, z: args.state.z + 2, h: 1280 * 2, r: 128, g: 128, b: 128, a: 255 }.line!,
    
        { x: size,        y: size,        z: args.state.z + 3, size_enum: -2,
          vertical_alignment_enum: 0,
          text: "#{size.to_sf}, #{size.to_sf}, #{args.state.z.to_sf}",
          r: 255, g: 255, b: 255, a: 255 }.label!,
    
        { x: size,        y: size_16_9,   z: args.state.z + 3, size_enum: -2,
          vertical_alignment_enum: 0,
          text: "#{size.to_sf}, #{size_16_9.to_sf}, #{args.state.z.to_sf}",
          r: 255, g: 255, b: 255, a: 255 }.label!,
      ]
    
      xs = [
        { description: "left",   x:    0, alignment_enum: 0 },
        { description: "center", x:  640, alignment_enum: 1 },
        { description: "right",  x: 1280, alignment_enum: 2 },
      ]
    
      ys = [
        { description: "bottom",        y:    0, vertical_alignment_enum: 0 },
        { description: "center",        y:  640, vertical_alignment_enum: 1 },
        { description: "center (720p)", y:  360, vertical_alignment_enum: 1 },
        { description: "top",           y: 1280, vertical_alignment_enum: 2 },
        { description: "top (720p)",    y:  720, vertical_alignment_enum: 2 },
      ]
    
      args.outputs.primitives << xs.product(ys).map do |(xdef, ydef)|
        [
          { x: xdef.x,
            y: ydef.y,
            z: args.state.z + 3,
            text: "#{xdef.x.to_sf}, #{ydef.y.to_sf} #{args.state.z.to_sf}",
            **label_style,
            alignment_enum: xdef.alignment_enum,
            vertical_alignment_enum: ydef.vertical_alignment_enum
          },
          { x: xdef.x,
            y: ydef.y - 20,
            z: args.state.z + 3,
            text: "#{ydef.description}, #{xdef.description}",
            **label_style,
            alignment_enum: xdef.alignment_enum,
            vertical_alignment_enum: ydef.vertical_alignment_enum
          }
        ]
      end
    
      args.outputs.primitives << xs.product(ys).map do |(xdef, ydef)|
        [
          {
            x: xdef.x - 1280,
            y: ydef.y,
            w: 1280 * 2,
            a: 64,
            r: 128, g: 128, b: 128
          }.line!,
          {
            x: xdef.x,
            y: ydef.y - 720,
            h: 720 * 2,
            a: 64,
            r: 128, g: 128, b: 128
          }.line!,
        ].map do |p|
          [
            p.merge(z:            0, a:  64),
            p.merge(z: args.state.z + 2, a: 255)
          ]
        end
      end
    end
    
    $gtk.reset
    
    

    Top Down Rpg - main.rb link

    # ./samples/14_vr/02_top_down_rpg/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      tick_game args
    end
    
    

    Top Down Rpg - tick.rb link

    # ./samples/14_vr/02_top_down_rpg/app/tick.rb
    class Game
      attr_gtk
    
      def tick
        outputs.background_color = [0, 0, 0]
        args.state.tile_size     = 80
        args.state.player_speed  = 4
        args.state.player      ||= tile(args, 7, 3, 0, 128, 180)
        generate_map args
    
        # adds walls, goal, and player to args.outputs.solids so they appear on screen
        args.outputs.solids << args.state.goal
        args.outputs.solids << args.state.walls
        args.outputs.solids << args.state.player
    
        args.outputs.solids << args.state.walls.map { |s| s.to_hash.merge(z: 2, g: 80) }
        args.outputs.solids << args.state.walls.map { |s| s.to_hash.merge(z: 10, g: 255, a: 50) }
    
        # if player's box intersects with goal, a label is output onto the screen
        if args.state.player.intersect_rect? args.state.goal
          args.outputs.labels << { x: 640,
                                   y: 360,
                                   z: 10,
                                   text: "YOU'RE A GOD DAMN WIZARD, HARRY.",
                                   size_enum: 10,
                                   alignment_enum: 1,
                                   vertical_alignment_enum: 1,
                                   r: 255,
                                   g: 255,
                                   b: 255 }
        end
    
        move_player args, -1,  0 if args.inputs.keyboard.left  || args.inputs.controller_one.left # x position decreases by 1 if left key is pressed
        move_player args,  1,  0 if args.inputs.keyboard.right || args.inputs.controller_one.right # x position increases by 1 if right key is pressed
        move_player args,  0, -1 if args.inputs.keyboard.up    || args.inputs.controller_one.down # y position increases by 1 if up is pressed
        move_player args,  0,  1 if args.inputs.keyboard.down  || args.inputs.controller_one.up # y position decreases by 1 if down is pressed
      end
    
      # Sets position, size, and color of the tile
      def tile args, x, y, *color
        [x * args.state.tile_size, # sets definition for array using method parameters
         y * args.state.tile_size, # multiplying by tile_size sets x and y to correct position using pixel values
         args.state.tile_size,
         args.state.tile_size,
         *color]
      end
    
      # Creates map by adding tiles to the wall, as well as a goal (that the player needs to reach)
      def generate_map args
        return if args.state.area
    
        # Creates the area of the map. There are 9 rows running horizontally across the screen
        # and 16 columns running vertically on the screen. Any spot with a "1" is not
        # open for the player to move into (and is green), and any spot with a "0" is available
        # for the player to move in.
        args.state.area = [
          [1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1,],
          [1, 1, 1, 2, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1,], # the "2" represents the goal
          [1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1,],
          [1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1,],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
          [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,],
          [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,],
          [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,],
          [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
        ].reverse # reverses the order of the area collection
    
        # By reversing the order, the way that the area appears above is how it appears
        # on the screen in the game. If we did not reverse, the map would appear inverted.
    
        #The wall starts off with no tiles.
        args.state.walls = []
    
        # If v is 1, a green tile is added to args.state.walls.
        # If v is 2, a black tile is created as the goal.
        args.state.area.map_2d do |y, x, v|
          if    v == 1
            args.state.walls << tile(args, x, y, 0, 255, 0) # green tile
          elsif v == 2 # notice there is only one "2" above because there is only one single goal
            args.state.goal   = tile(args, x, y, 180,  0, 0) # black tile
          end
        end
      end
    
      # Allows the player to move their box around the screen
      def move_player args, *vector
        box = args.state.player.shift_rect(vector) # box is able to move at an angle
    
        # If the player's box hits a wall, it is not able to move further in that direction
        return if args.state.walls
                    .any_intersect_rect?(box)
    
        # Player's box is able to move at angles (not just the four general directions) fast
        args.state.player =
          args.state.player
            .shift_rect(vector.x * args.state.player_speed, # if we don't multiply by speed, then
                        vector.y * args.state.player_speed) # the box will move extremely slow
      end
    end
    
    $game = Game.new
    
    def tick_game args
      $game.args = args
      $game.tick
    end
    
    $gtk.reset
    
    

    Space Invaders - main.rb link

    # ./samples/14_vr/03_space_invaders/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      tick_game args
    end
    
    

    Space Invaders - tick.rb link

    # ./samples/14_vr/03_space_invaders/app/tick.rb
    class Game
      attr_gtk
    
      def tick
        grid.origin_center!
        defaults
        outputs.background_color = [0, 0, 0]
        args.outputs.sprites << state.enemies.map { |e| enemy_prefab e }.to_a
      end
    
      def defaults
        state.enemy_sprite_size = 64
        state.row_size = 16
        state.max_rows = 20
        state.enemies ||= 32.map_with_index do |i|
          x = i % 16
          y = i.idiv 16
          { row: y, col: x }
        end
      end
    
      def enemy_prefab enemy
        if enemy.row > state.max_rows
          raise "#{enemy}"
        end
        relative_row = enemy.row + 1
        z = 50 - relative_row * 10
        x = (enemy.col * state.enemy_sprite_size) - (state.enemy_sprite_size * state.row_size).idiv(2)
        enemy_sprite(x, enemy.row * 10 + 100, z * 10, enemy)
      end
    
      def enemy_sprite x, y, z, meta
        index = 0.frame_index count: 2, hold_for: 50, repeat: true
        { x: x,
          y: y,
          z: z,
          w: state.enemy_sprite_size,
          h: state.enemy_sprite_size,
          path: 'sprites/enemy.png',
          source_x: 128 * index,
          source_y: 0,
          source_w: 128,
          source_h: 128,
          meta: meta }
      end
    end
    
    $game = Game.new
    
    def tick_game args
      $game.args = args
      $game.tick
    end
    
    $gtk.reset
    
    

    Let There Be Light - main.rb link

    # ./samples/14_vr/04_let_there_be_light/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      tick_game args
    end
    
    

    Let There Be Light - tick.rb link

    # ./samples/14_vr/04_let_there_be_light/app/tick.rb
    class Game
      attr_gtk
    
      def tick
        grid.origin_center!
        defaults
        state.angle_shift_x ||= 180
        state.angle_shift_y ||= 180
    
        if inputs.controller_one.right_analog_y_perc.round(2) != 0.00
          args.state.star_distance += (inputs.controller_one.right_analog_y_perc * 0.25) ** 2 * inputs.controller_one.right_analog_y_perc.sign
          state.star_distance = state.star_distance.clamp(state.min_star_distance, state.max_star_distance)
          state.star_sprites = calc_star_primitives
        elsif inputs.controller_one.down
          args.state.star_distance += (1.0 * 0.25) ** 2
          state.star_distance = state.star_distance.clamp(state.min_star_distance, state.max_star_distance)
          state.star_sprites = calc_star_primitives
        elsif inputs.controller_one.up
          args.state.star_distance -= (1.0 * 0.25) ** 2
          state.star_distance = state.star_distance.clamp(state.min_star_distance, state.max_star_distance)
          state.star_sprites = calc_star_primitives
        end
    
        render
      end
    
      def calc_star_primitives
        args.state.stars.map do |s|
          w = (32 * state.star_distance).clamp(1, 32)
          h = (32 * state.star_distance).clamp(1, 32)
          x = (state.max.x * state.star_distance) * s.xr
          y = (state.max.y * state.star_distance) * s.yr
          z = state.center.z + (state.max.z * state.star_distance * 10 * s.zr)
    
          angle_x = Math.atan2(z - 600, y).to_degrees + 90
          angle_y = Math.atan2(z - 600, x).to_degrees + 90
    
          draw_x = x - w.half
          draw_y = y - 40 - h.half
          draw_z = z
    
          { x: draw_x,
            y: draw_y,
            z: draw_z,
            b: 255,
            w: w,
            h: h,
            angle_x: angle_x,
            angle_y: angle_y,
            path: 'sprites/star.png' }
        end
      end
    
      def render
        outputs.background_color = [0, 0, 0]
        if state.star_distance <= 1.0
          text_alpha = (1 - state.star_distance) * 255
          args.outputs.labels << { x: 0, y: 50, text: "Let there be light.", r: 255, g: 255, b: 255, size_enum: 1, alignment_enum: 1, a: text_alpha }
          args.outputs.labels << { x: 0, y: 25, text: "(right analog: up/down)", r: 255, g: 255, b: 255, size_enum: -2, alignment_enum: 1, a: text_alpha }
        end
    
        args.outputs.sprites << state.star_sprites
      end
    
      def random_point
        r = { xr: 2.randomize(:ratio) - 1,
              yr: 2.randomize(:ratio) - 1,
              zr: 2.randomize(:ratio) - 1 }
        if (r.xr ** 2 + r.yr ** 2 + r.zr ** 2) > 1.0
          return random_point
        else
          return r
        end
      end
    
      def defaults
        state.max_star_distance ||= 100
        state.min_star_distance ||= 0.001
        state.star_distance     ||= 0.001
        state.star_angle        ||= 0
    
        state.center.x       ||= 0
        state.center.y       ||= 0
        state.center.z       ||= 30
        state.max.x          ||= 640
        state.max.y          ||= 640
        state.max.z          ||= 50
    
        state.stars ||= 1500.map do
          random_point
        end
    
        state.star_sprites ||= calc_star_primitives
      end
    end
    
    $game = Game.new
    
    def tick_game args
      $game.args = args
      $game.tick
    end
    
    $gtk.reset
    
    

    Draw A Cube - main.rb link

    # ./samples/14_vr/05_draw_a_cube/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      tick_game args
    end
    
    

    Draw A Cube - tick.rb link

    # ./samples/14_vr/05_draw_a_cube/app/tick.rb
    def cube args, x, y, z, size
      sprite = { w: size, h: size, path: 'sprites/square/blue.png', a: 80 }
      back   = { x: x,                 y: y,                 z: z - size.half + 1,              **sprite }
      front  = { x: x,                 y: y,                 z: z + size.half - 1,              **sprite }
      top    = { x: x,                 y: y + size.half - 1, z: z,                 angle_x: 90, **sprite }
      bottom = { x: x,                 y: y - size.half + 1, z: z,                 angle_x: 90, **sprite }
      left   = { x: x - size.half + 1, y: y,                 z: z,                 angle_y: 90, **sprite }
      right  = { x: x + size.half - 1, y: y,                 z: z,                 angle_y: 90, **sprite }
    
      args.outputs.sprites << [back, left, top, bottom, right, front]
    end
    
    def tick_game args
      args.grid.origin_center!
      args.outputs.background_color = [0, 0, 0]
    
      args.state.x ||= 0
      args.state.y ||= 0
    
      args.state.x += 10 * args.inputs.controller_one.right_analog_x_perc
      args.state.y += 10 * args.inputs.controller_one.right_analog_y_perc
    
      cube args, args.state.x, args.state.y, 0, 100
    end
    
    

    Draw A Cube With Triangles - main.rb link

    # ./samples/14_vr/05_draw_a_cube_with_triangles/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      tick_game args
    end
    
    

    Draw A Cube With Triangles - tick.rb link

    # ./samples/14_vr/05_draw_a_cube_with_triangles/app/tick.rb
    include MatrixFunctions
    
    def tick args
      args.grid.origin_center!
    
      # model A
      args.state.a = [
        [vec4(0, 0, 0, 1),   vec4(0.1, 0, 0, 1),   vec4(0, 0.1, 0, 1)],
        [vec4(0.1, 0, 0, 1), vec4(0.1, 0.1, 0, 1), vec4(0, 0.1, 0, 1)]
      ]
    
      # model to world
      args.state.back = mul_triangles args,
                                      args.state.a,
                                      (translate -0.05, -0.05, 0),
                                      (translate 0, 0, -0.05),
                                      (rotate_x Kernel.tick_count),
                                      (rotate_y Kernel.tick_count),
                                      (rotate_z Kernel.tick_count)
    
      args.state.front = mul_triangles args,
                                       args.state.a,
                                       (translate -0.05, -0.05, 0),
                                       (translate 0, 0, 0.05),
                                       (rotate_x Kernel.tick_count),
                                       (rotate_y Kernel.tick_count),
                                       (rotate_z Kernel.tick_count)
    
      args.state.left = mul_triangles args,
                                      args.state.a,
                                      (translate -0.05, -0.05, 0),
                                      (rotate_y 90),
                                      (translate -0.05, 0, 0),
                                      (rotate_x Kernel.tick_count),
                                      (rotate_y Kernel.tick_count),
                                      (rotate_z Kernel.tick_count)
    
      args.state.right = mul_triangles args,
                                       args.state.a,
                                       (translate -0.05, -0.05, 0),
                                       (rotate_y 90),
                                       (translate  0.05, 0, 0),
                                       (rotate_x Kernel.tick_count),
                                       (rotate_y Kernel.tick_count),
                                       (rotate_z Kernel.tick_count)
    
      args.state.top = mul_triangles args,
                                     args.state.a,
                                     (translate -0.05, -0.05, 0),
                                     (rotate_x 90),
                                     (translate 0, 0.05, 0),
                                     (rotate_x Kernel.tick_count),
                                     (rotate_y Kernel.tick_count),
                                     (rotate_z Kernel.tick_count)
    
      args.state.bottom = mul_triangles args,
                                        args.state.a,
                                        (translate -0.05, -0.05, 0),
                                        (rotate_x 90),
                                        (translate 0, -0.05, 0),
                                        (rotate_x Kernel.tick_count),
                                        (rotate_y Kernel.tick_count),
                                        (rotate_z Kernel.tick_count)
    
      render_square args, args.state.back
      render_square args, args.state.front
      render_square args, args.state.left
      render_square args, args.state.right
      render_square args, args.state.top
      render_square args, args.state.bottom
    end
    
    def render_square args, triangles
      args.outputs.sprites << { x:  triangles[0][0].x * 1280,
                                y:  triangles[0][0].y * 1280,
                                z:  triangles[0][0].z * 1280,
                                x2: triangles[0][1].x * 1280,
                                y2: triangles[0][1].y * 1280,
                                z2: triangles[0][1].z * 1280,
                                x3: triangles[0][2].x * 1280,
                                y3: triangles[0][2].y * 1280,
                                z3: triangles[0][2].z * 1280,
                                a: 255,
                                source_x:   0,
                                source_y:   0,
                                source_x2: 80,
                                source_y2:  0,
                                source_x3:  0,
                                source_y3: 80,
                                path: 'sprites/square/red.png' }
    
      args.outputs.sprites << { x:  triangles[1][0].x * 1280,
                                y:  triangles[1][0].y * 1280,
                                z:  triangles[1][0].z * 1280,
                                x2: triangles[1][1].x * 1280,
                                y2: triangles[1][1].y * 1280,
                                z2: triangles[1][1].z * 1280,
                                x3: triangles[1][2].x * 1280,
                                y3: triangles[1][2].y * 1280,
                                z3: triangles[1][2].z * 1280,
                                a: 255,
                                source_x:  80,
                                source_y:   0,
                                source_x2: 80,
                                source_y2: 80,
                                source_x3:  0,
                                source_y3: 80,
                                path: 'sprites/square/red.png' }
    end
    
    def mul_triangles args, triangles, *mul_def
      triangles.map do |vecs|
        vecs.map do |vec|
          mul vec, *mul_def
        end
      end
    end
    
    def 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
    
    

    Gimbal Lock - main.rb link

    # ./samples/14_vr/05_gimbal_lock/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    

    Gimbal Lock - tick.rb link

    # ./samples/14_vr/05_gimbal_lock/app/tick.rb
    class Game
      attr_gtk
    
      def tick
        grid.origin_center!
        state.angle_x ||= 0
        state.angle_y ||= 0
        state.angle_z ||= 0
    
        if inputs.left
          state.angle_z += 1
        elsif inputs.right
          state.angle_z -= 1
        end
    
        if inputs.up
          state.angle_x += 1
        elsif inputs.down
          state.angle_x -= 1
        end
    
        if inputs.controller_one.a
          state.angle_y += 1
        elsif inputs.controller_one.b
          state.angle_y -= 1
        end
    
        outputs.sprites << {
          x: 0,
          y: 0,
          w: 100,
          h: 100,
          path: 'sprites/square/blue.png',
          angle_x: state.angle_x,
          angle_y: state.angle_y,
          angle: state.angle_z,
        }
      end
    end
    
    

    Citadels - main.rb link

    # ./samples/14_vr/06_citadels/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    

    Citadels - tick.rb link

    # ./samples/14_vr/06_citadels/app/tick.rb
    class Game
      attr_gtk
    
      def citadel x, y, z
        angle = Kernel.tick_count.idiv(10) % 360
        adjacent = 40
        adjacent = adjacent.ceil
        angle = Math.atan2(40, 70).to_degrees
        y += 500
        x -= 40
        back_sprites = [
          { z: z - 40 + adjacent.half,
            x: x,
            y: y + 75,
            w: 80, h: 80, angle_x: angle, path: "sprites/triangle/equilateral/blue.png" },
          { z: z - 40,
            x: x,
            y: y - 400 + 80,
            w: 80, h: 400, path: "sprites/square/blue.png" },
        ]
    
        left_sprites = [
          { z: z,
            x: x - 40 + adjacent.half,
            y: y + 75,
            w: 80, h: 80, angle_x: -angle, angle_y: 90, path: "sprites/triangle/equilateral/blue.png" },
          { z: z,                      x: x - 40,
            y: y - 400 + 80,
            w: 80, h: 400, angle_y: 90, path: "sprites/square/blue.png" },
        ]
    
        right_sprites = [
          { z: z,
            x: x + 40 - adjacent.half,
            y: y + 75,
            w: 80, h: 80, angle_x: angle, angle_y: 90, path: "sprites/triangle/equilateral/blue.png" },
          { z: z,
            x: x + 40,
            y: y - 400 + 80,
            w: 80, h: 400, angle_y: 90, path: "sprites/square/blue.png" },
        ]
    
        front_sprites = [
          { z: z + 40 - adjacent.half,
            x: x,
            y: y + 75,
            w: 80, h: 80, angle_x: -angle, path: "sprites/triangle/equilateral/blue.png" },
          { z: z + 40,
            x: x,
            y: y - 400 + 80,
            w: 80, h: 400, path: "sprites/square/blue.png" },
        ]
    
        if x > 700
          [
            back_sprites,
            right_sprites,
            front_sprites,
            left_sprites,
          ]
        elsif x < 600
          [
            back_sprites,
            left_sprites,
            front_sprites,
            right_sprites,
          ]
        else
          [
            back_sprites,
            left_sprites,
            right_sprites,
            front_sprites,
          ]
        end
    
      end
    
      def tick
        state.z ||= 200
        state.z += inputs.controller_one.right_analog_y_perc
        state.columns ||= 100.map do
          {
            x: rand(12) * 400,
            y: 0,
            z: rand(12) * 400,
          }
        end
    
        outputs.sprites << state.columns.map do |col|
          citadel(col.x - 640, col.y - 400, state.z - col.z)
        end
      end
    end
    
    $game = Game.new
    
    def tick_game args
      $game.args = args
      $game.tick
    end
    
    $gtk.reset
    
    

    Flappy credits.txt link

    # ./samples/14_vr/07_flappy_vr/CREDITS.txt
    code: Amir Rajan, https://twitter.com/amirrajan
    graphics and audio: Nick Culbertson, https://twitter.com/MobyPixel
    
    
    

    Flappy main.rb link

    # ./samples/14_vr/07_flappy_vr/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      tick_game args
    end
    
    

    Flappy tick.rb link

    # ./samples/14_vr/07_flappy_vr/app/tick.rb
    class FlappyDragon
      attr_accessor :grid, :inputs, :state, :outputs
    
      def background_z
        -640
      end
    
      def flappy_sprite_z
        -120
      end
    
      def game_text_z
        0
      end
    
      def menu_overlay_z
        10
      end
    
      def menu_text_z
        menu_overlay_z + 1
      end
    
      def flash_z
        1
      end
    
      def tick
        defaults
        render
        calc
        process_inputs
      end
    
      def defaults
        state.flap_power              = 11
        state.gravity                 = 0.9
        state.ceiling                 = 600
        state.ceiling_flap_power      = 6
        state.wall_countdown_length   = 100
        state.wall_gap_size           = 100
        state.wall_countdown        ||= 0
        state.hi_score              ||= 0
        state.score                 ||= 0
        state.walls                 ||= []
        state.x_starting_point      ||= 640
        state.x                     ||= state.x_starting_point
        state.y                     ||= 500
        state.z                     ||= -120
        state.dy                    ||= 0
        state.scene                 ||= :menu
        state.scene_at              ||= 0
        state.difficulty            ||= :normal
        state.new_difficulty        ||= :normal
        state.countdown             ||= 4.seconds
        state.flash_at              ||= 0
      end
    
      def render
        outputs.sounds << "sounds/flappy-song.ogg" if Kernel.tick_count == 1
        render_score
        render_menu
        render_game
      end
    
      def render_score
        outputs.primitives << { x: 10, y: 710, z: game_text_z, text: "HI SCORE: #{state.hi_score}", **large_white_typeset }
        outputs.primitives << { x: 10, y: 680, z: game_text_z, text: "SCORE: #{state.score}", **large_white_typeset }
        outputs.primitives << { x: 10, y: 650, z: game_text_z, text: "DIFFICULTY: #{state.difficulty.upcase}", **large_white_typeset }
      end
    
      def render_menu
        return unless state.scene == :menu
        render_overlay
    
        outputs.labels << { x: 640, y: 700, z: menu_text_z, text: "Flappy Dragon", size_enum: 50, alignment_enum: 1, **white }
        outputs.labels << { x: 640, y: 500, z: menu_text_z, text: "Instructions: Press Spacebar to flap. Don't die.", size_enum: 4, alignment_enum: 1, **white }
        outputs.labels << { x: 430, y: 430, z: menu_text_z, text: "[Tab]    Change difficulty", size_enum: 4, alignment_enum: 0, **white }
        outputs.labels << { x: 430, y: 400, z: menu_text_z, text: "[Enter]  Start at New Difficulty ", size_enum: 4, alignment_enum: 0, **white }
        outputs.labels << { x: 430, y: 370, z: menu_text_z, text: "[Escape] Cancel/Resume ", size_enum: 4, alignment_enum: 0, **white }
        outputs.labels << { x: 640, y: 300, z: menu_text_z, text: "(mouse, touch, and game controllers work, too!) ", size_enum: 4, alignment_enum: 1, **white }
        outputs.labels << { x: 640, y: 200, z: menu_text_z, text: "Difficulty: #{state.new_difficulty.capitalize}", size_enum: 4, alignment_enum: 1, **white }
    
        outputs.labels << { x: 10, y: 100, z: menu_text_z, text: "Code:   @amirrajan",     **white }
        outputs.labels << { x: 10, y:  80, z: menu_text_z, text: "Art:    @mobypixel",     **white }
        outputs.labels << { x: 10, y:  60, z: menu_text_z, text: "Music:  @mobypixel",     **white }
        outputs.labels << { x: 10, y:  40, z: menu_text_z, text: "Engine: DragonRuby GTK", **white }
      end
    
      def render_overlay
        overlay_rect = grid.rect.scale_rect(1.5, 0, 0)
        outputs.primitives << { x: overlay_rect.x - overlay_rect.w,
                                y: overlay_rect.y - overlay_rect.h,
                                w: overlay_rect.w * 4,
                                h: overlay_rect.h * 2,
                                z: menu_overlay_z,
                                r: 0, g: 0, b: 0, a: 230 }.solid!
      end
    
      def render_game
        outputs.background_color = [0, 0, 0]
        render_game_over
        render_background
        render_walls
        render_dragon
        render_flash
      end
    
      def render_game_over
        return unless state.scene == :game
        outputs.labels << { x: 638, y: 358, text: score_text,     z: game_text_z - 1,  size_enum: 20, alignment_enum: 1 }
        outputs.labels << { x: 635, y: 360, text: score_text,     z: game_text_z,  size_enum: 20, alignment_enum: 1, r: 255, g: 255, b: 255 }
        outputs.labels << { x: 638, y: 428, text: countdown_text, z: game_text_z - 1,  size_enum: 20, alignment_enum: 1 }
        outputs.labels << { x: 635, y: 430, text: countdown_text, z: game_text_z,  size_enum: 20, alignment_enum: 1, r: 255, g: 255, b: 255 }
      end
    
      def render_background
        scroll_point_at   = Kernel.tick_count
        scroll_point_at   = state.scene_at if state.scene == :menu
        scroll_point_at   = state.death_at if state.countdown > 0
        scroll_point_at ||= 0
    
        outputs.sprites << { x: -640, y: -360, z: background_z, w: 1280 * 2, h: 720 * 2, path: 'sprites/background.png' }
        outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_back.png',   0.25, 1)
        outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_middle.png', 0.50, 50)
        outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_front.png',  1.00, 100, -80)
      end
    
      def scrolling_background at, path, rate, z, y = 0
        rate *= 2
        w = 1440 * 2
        h =  720 * 2
        [
          { x: w - at.*(rate) % w - w.half.half, y: y * 2 - 360, z: background_z + z, w: w, h: h, path: path },
          { x: 0 - at.*(rate) % w - w.half.half, y: y * 2 - 360, z: background_z + z, w: w, h: h, path: path },
        ]
      end
    
      def render_walls
        state.walls.each do |w|
          w.top_section = { x: w.x,
                            y: w.bottom_height - 720,
                            z: -120,
                            w: 100,
                            h: 720,
                            path: 'sprites/wall.png',
                            angle: 180 }
    
          w.bottom_section = { x: w.x,
                               y: w.top_y,
                               z: -120,
                               w: 100,
                               h: 720,
                               path: 'sprites/wallbottom.png',
                               angle: 0}
          w.sprites = [
            model_for(w.top_section),
            model_for(w.bottom_section)
          ]
        end
    
        outputs.sprites << state.walls.find_all { |w| w.x >= state.x }.reverse.map(&:sprites)
        outputs.sprites << state.walls.find_all { |w| w.x <  state.x }.map(&:sprites)
      end
    
      def model_for wall
        ratio = (wall.x - state.x_starting_point).abs.fdiv(2560 + state.x_starting_point)
        z_ratio = ratio ** 2
        z_offset = (2560 * 2) * z_ratio
        x_offset = z_offset * 0.25
    
        if wall.x < state.x
          x_offset *= -1
        end
    
        distance_from_background_to_flappy = (background_z - flappy_sprite_z).abs
        distance_to_front = z_offset
    
        if -z_offset < background_z + 100 + wall.w * 2
          a = 0
        else
          percentage_to_front = distance_to_front / distance_from_background_to_flappy
          a = 255 * (1 - percentage_to_front)
        end
    
    
        back  = { x:     wall.x + x_offset,
                  y:     wall.y,
                  z:     wall.z - wall.w.half - z_offset,
                  a:     a,
                  w:     wall.w,
                  h:     wall.h,
                  path:  wall.path,
                  angle: wall.angle }
        front = { x:     wall.x + x_offset,
                  y:     wall.y,
                  z:     wall.z + wall.w.half - z_offset,
                  a:     a,
                  w:     wall.w,
                  h:     wall.h,
                  path:  wall.path,
                  angle: wall.angle }
        left  = { x:     wall.x - wall.w.half + x_offset,
                  y:     wall.y,
                  z:     wall.z - z_offset,
                  a:     a,
                  angle_y: 90,
                  w:     wall.w,
                  h:     wall.h,
                  path:  wall.path,
                  angle: wall.angle }
        right = { x:     wall.x + wall.w.half + x_offset,
                  y:     wall.y,
                  z:     wall.z - z_offset,
                  a:     a,
                  angle_y: 90,
                  w:     wall.w,
                  h:     wall.h,
                  path:  wall.path,
                  angle: wall.angle }
        if    (wall.x - wall.w - state.x).abs < 200
          [back, left, right, front]
        elsif wall.x < state.x
          [back, left, front, right]
        else
          [back, right, front, left]
        end
      end
    
      def render_dragon
        state.show_death = true if state.countdown == 3.seconds
    
        if state.show_death == false || !state.death_at
          animation_index = state.flapped_at.frame_index 6, 2, false if state.flapped_at
          sprite_name = "sprites/dragon_fly#{animation_index.or(0) + 1}.png"
          state.dragon_sprite = { x: state.x, y: state.y, z: state.z, w: 100, h: 80, path: sprite_name, angle: state.dy * 1.2 }
        else
          sprite_name = "sprites/dragon_die.png"
          state.dragon_sprite = { x: state.x, y: state.y, z: state.z, w: 100, h: 80, path: sprite_name, angle: state.dy * 1.2 }
          sprite_changed_elapsed    = state.death_at.elapsed_time - 1.seconds
          state.dragon_sprite.angle += (sprite_changed_elapsed ** 1.3) * state.death_fall_direction * -1
          state.dragon_sprite.x     += (sprite_changed_elapsed ** 1.2) * state.death_fall_direction
          state.dragon_sprite.y     += (sprite_changed_elapsed * 14 - sprite_changed_elapsed ** 1.6)
          state.z     += 0.3
        end
    
        outputs.sprites << state.dragon_sprite
      end
    
      def render_flash
        return unless state.flash_at
    
        outputs.primitives << { **grid.rect.to_hash,
                                **white,
                                z: flash_z,
                                a: 255 * state.flash_at.ease(20, :flip) }.solid!
    
        state.flash_at = 0 if state.flash_at.elapsed_time > 20
      end
    
      def calc
        return unless state.scene == :game
        reset_game if state.countdown == 1
        state.countdown -= 1 and return if state.countdown > 0
        calc_walls
        calc_flap
        calc_game_over
      end
    
      def calc_walls
        state.walls.each { |w| w.x -= 8 }
    
        walls_count_before_removal = state.walls.length
    
        state.walls.reject! { |w| w.x < -2560 + state.x_starting_point }
    
        state.score += 1 if state.walls.count < walls_count_before_removal
    
        state.wall_countdown -= 1 and return if state.wall_countdown > 0
    
        state.walls << state.new_entity(:wall) do |w|
          w.x             = 2560 + state.x_starting_point
          w.opening       = grid.top
                                .randomize(:ratio)
                                .greater(200)
                                .lesser(520)
          w.opening -= w.opening * 0.5
          w.bottom_height = w.opening - state.wall_gap_size
          w.top_y         = w.opening + state.wall_gap_size
        end
    
        state.wall_countdown = state.wall_countdown_length
      end
    
      def calc_flap
        state.y += state.dy
        state.dy = state.dy.lesser state.flap_power
        state.dy -= state.gravity
        return if state.y < state.ceiling
        state.y  = state.ceiling
        state.dy = state.dy.lesser state.ceiling_flap_power
      end
    
      def calc_game_over
        return unless game_over?
    
        state.death_at = Kernel.tick_count
        state.death_from = state.walls.first
        state.death_fall_direction = -1
        state.death_fall_direction =  1 if state.x > state.death_from.x
        outputs.sounds << "sounds/hit-sound.wav"
        begin_countdown
      end
    
      def process_inputs
        process_inputs_menu
        process_inputs_game
      end
    
      def process_inputs_menu
        return unless state.scene == :menu
    
        changediff = inputs.keyboard.key_down.tab || inputs.controller_one.key_down.select
        if inputs.mouse.click
          p = inputs.mouse.click.point
          if (p.y >= 165) && (p.y < 200) && (p.x >= 500) && (p.x < 800)
            changediff = true
          end
        end
    
        if changediff
          case state.new_difficulty
          when :easy
            state.new_difficulty = :normal
          when :normal
            state.new_difficulty = :hard
          when :hard
            state.new_difficulty = :flappy
          when :flappy
            state.new_difficulty = :easy
          end
        end
    
        if inputs.keyboard.key_down.enter || inputs.controller_one.key_down.start || inputs.controller_one.key_down.a
          state.difficulty = state.new_difficulty
          change_to_scene :game
          reset_game false
          state.hi_score = 0
          begin_countdown
        end
    
        if inputs.keyboard.key_down.escape || (inputs.mouse.click && !changediff) || inputs.controller_one.key_down.b
          state.new_difficulty = state.difficulty
          change_to_scene :game
        end
      end
    
      def process_inputs_game
        return unless state.scene == :game
    
        clicked_menu = false
        if inputs.mouse.click
          p = inputs.mouse.click.point
          clicked_menu = (p.y >= 620) && (p.x < 275)
        end
    
        if clicked_menu || inputs.keyboard.key_down.escape || inputs.keyboard.key_down.enter || inputs.controller_one.key_down.start
          change_to_scene :menu
        elsif (inputs.mouse.down || inputs.mouse.click || inputs.keyboard.key_down.space || inputs.controller_one.key_down.a) && state.countdown == 0
          state.dy = 0
          state.dy += state.flap_power
          state.flapped_at = Kernel.tick_count
          outputs.sounds << "sounds/fly-sound.wav"
        end
      end
    
      def white
        { r: 255, g: 255, b: 255 }
      end
    
      def large_white_typeset
        { size_enum: 5, alignment_enum: 0, r: 255, g: 255, b: 255 }
      end
    
      def at_beginning?
        state.walls.count == 0
      end
    
      def dragon_collision_box
        { x: state.dragon_sprite.x,
          y: state.dragon_sprite.y,
          w: state.dragon_sprite.w,
          h: state.dragon_sprite.h }
             .scale_rect(1.0 - collision_forgiveness, 0.5, 0.5)
             .rect_shift_right(10)
             .rect_shift_up(state.dy * 2)
      end
    
      def game_over?
        return true if state.y <= 0.-(500 * collision_forgiveness) && !at_beginning?
    
        state.walls
             .find_all { |w| w.top_section && w.bottom_section }
             .flat_map { |w| [w.top_section, w.bottom_section] }
             .any?     { |s| s.intersect_rect?(dragon_collision_box) }
      end
    
      def collision_forgiveness
        case state.difficulty
        when :easy
          0.9
        when :normal
          0.7
        when :hard
          0.5
        when :flappy
          0.3
        else
          0.9
        end
      end
    
      def countdown_text
        state.countdown ||= -1
        return ""          if state.countdown == 0
        return "GO!"       if state.countdown.idiv(60) == 0
        return "GAME OVER" if state.death_at
        return "READY?"
      end
    
      def begin_countdown
        state.countdown = 4.seconds
      end
    
      def score_text
        return ""                        unless state.countdown > 1.seconds
        return ""                        unless state.death_at
        return "SCORE: 0 (LOL)"          if state.score == 0
        return "HI SCORE: #{state.score}" if state.score == state.hi_score
        return "SCORE: #{state.score}"
      end
    
      def reset_game set_flash = true
        state.flash_at = Kernel.tick_count if set_flash
        state.walls = []
        state.y = 500
        state.x =  state.x_starting_point
        state.z = flappy_sprite_z
        state.dy = 0
        state.hi_score = state.hi_score.greater(state.score)
        state.score = 0
        state.wall_countdown = state.wall_countdown_length.fdiv(2)
        state.show_death = false
        state.death_at = nil
      end
    
      def change_to_scene scene
        state.scene = scene
        state.scene_at = Kernel.tick_count
        inputs.keyboard.clear
        inputs.controller_one.clear
      end
    end
    
    $flappy_dragon = FlappyDragon.new
    
    def tick_game args
      $flappy_dragon.grid = args.grid
      $flappy_dragon.inputs = args.inputs
      $flappy_dragon.state = args.state
      $flappy_dragon.outputs = args.outputs
      $flappy_dragon.tick
    end
    
    $gtk.reset
    
    

    Cubeworld main.rb link

    # ./samples/14_vr/08_cubeworld_vr/app/main.rb
    require 'app/tick.rb'
    
    def tick args
      args.gtk.start_server! port: 9001, enable_in_prod: true
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    

    Cubeworld tick.rb link

    # ./samples/14_vr/08_cubeworld_vr/app/tick.rb
    class Game
      include MatrixFunctions
    
      attr_gtk
    
      def cube x:, y:, z:, angle_x:, angle_y:, angle_z:;
        combined = mul (rotate_x angle_x),
                       (rotate_y angle_y),
                       (rotate_z angle_z),
                       (translate x, y, z)
    
        face_1 = mul_triangles state.baseline_cube.face_1, combined
        face_2 = mul_triangles state.baseline_cube.face_2, combined
        face_3 = mul_triangles state.baseline_cube.face_3, combined
        face_4 = mul_triangles state.baseline_cube.face_4, combined
        face_5 = mul_triangles state.baseline_cube.face_5, combined
        face_6 = mul_triangles state.baseline_cube.face_6, combined
    
        [
          face_1,
          face_2,
          face_3,
          face_4,
          face_5,
          face_6
        ]
      end
    
      def random_point
        r = { xr: 2.randomize(:ratio) - 1,
              yr: 2.randomize(:ratio) - 1,
              zr: 2.randomize(:ratio) - 1 }
        if (r.xr ** 2 + r.yr ** 2 + r.zr ** 2) > 1.0
          return random_point
        else
          return r
        end
      end
    
      def random_cube_attributes
        state.cube_count.map_with_index do |i|
          point_on_sphere = random_point
          radius = rand * 10 + 3
          {
            x: point_on_sphere.xr * radius,
            y: point_on_sphere.yr * radius,
            z: 6.4 + point_on_sphere.zr * radius
          }
        end
      end
    
      def defaults
        state.cube_count ||= 1
        state.cube_attributes ||= random_cube_attributes
        if !state.baseline_cube
          state.baseline_cube = {
            face_1: [
              [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)]
            ],
            face_2: [
              [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)]
            ],
            face_3: [
              [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)]
            ],
            face_4: [
              [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)]
            ],
            face_5: [
              [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)]
            ],
            face_6: [
              [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)]
            ]
          }
    
          state.baseline_cube.face_1 = mul_triangles state.baseline_cube.face_1,
                                                     (translate -0.25, -0.25, 0),
                                                     (translate  0, 0, 0.25)
    
          state.baseline_cube.face_2 = mul_triangles state.baseline_cube.face_2,
                                                     (translate -0.25, -0.25, 0),
                                                     (translate  0, 0, -0.25)
    
          state.baseline_cube.face_3 = mul_triangles state.baseline_cube.face_3,
                                                     (translate -0.25, -0.25, 0),
                                                     (rotate_y 90),
                                                     (translate -0.25,  0, 0)
    
          state.baseline_cube.face_4 = mul_triangles state.baseline_cube.face_4,
                                                     (translate -0.25, -0.25, 0),
                                                     (rotate_y 90),
                                                     (translate  0.25,  0, 0)
    
          state.baseline_cube.face_5 = mul_triangles state.baseline_cube.face_5,
                                                     (translate -0.25, -0.25, 0),
                                                     (rotate_x 90),
                                                     (translate  0,  0.25, 0)
    
          state.baseline_cube.face_6 = mul_triangles state.baseline_cube.face_6,
                                                     (translate -0.25, -0.25, 0),
                                                     (rotate_x 90),
                                                     (translate  0,  -0.25, 0)
        end
      end
    
      def tick
        args.grid.origin_center!
        defaults
    
        if inputs.controller_one.key_down.a
          state.cube_count += 1
          state.cube_attributes = random_cube_attributes
        elsif inputs.controller_one.key_down.b
          state.cube_count -= 1 if state.cube_count > 1
          state.cube_attributes = random_cube_attributes
        end
    
        state.cube_attributes.each do |c|
          render_cube (cube x: c.x, y: c.y, z: c.z,
                            angle_x: Kernel.tick_count,
                            angle_y: Kernel.tick_count,
                            angle_z: Kernel.tick_count)
        end
    
        args.outputs.background_color = [255, 255, 255]
        framerate_primitives = args.gtk.current_framerate_primitives
        framerate_primitives.find { |p| p.text }.each { |p| p.z = 1 }
        framerate_primitives[-1].text = "cube count: #{state.cube_count} (#{state.cube_count * 12} triangles)"
        args.outputs.primitives << framerate_primitives
      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 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 mul_triangles model, *mul_def
        model.map do |vecs|
          vecs.map do |vec|
            vec = mul vec, *mul_def
          end
        end
      end
    
      def render_cube cube
        render_face cube[0]
        render_face cube[1]
        render_face cube[2]
        render_face cube[3]
        render_face cube[4]
        render_face cube[5]
      end
    
      def render_face face
        triangle_1 = face[0]
        args.outputs.sprites << {
          x:  triangle_1[0].x * 100,   y: triangle_1[0].y * 100,  z: triangle_1[0].z * 100,
          x2: triangle_1[1].x * 100,  y2: triangle_1[1].y * 100, z2: triangle_1[1].z * 100,
          x3: triangle_1[2].x * 100,  y3: triangle_1[2].y * 100, z3: triangle_1[2].z * 100,
          source_x:   0, source_y:   0,
          source_x2: 80, source_y2:  0,
          source_x3:  0, source_y3: 80,
          path: 'sprites/square/blue.png'
        }
    
        triangle_2 = face[1]
        args.outputs.sprites << {
          x:  triangle_2[0].x * 100,   y: triangle_2[0].y * 100,  z: triangle_2[0].z * 100,
          x2: triangle_2[1].x * 100,  y2: triangle_2[1].y * 100, z2: triangle_2[1].z * 100,
          x3: triangle_2[2].x * 100,  y3: triangle_2[2].y * 100, z3: triangle_2[2].z * 100,
          source_x:  80, source_y:   0,
          source_x2: 80, source_y2: 80,
          source_x3:  0, source_y3: 80,
          path: 'sprites/square/blue.png'
        }
      end
    end
    
    

    Genre 3d link

    3d Cube - main.rb link

    # ./samples/99_genre_3d/01_3d_cube/app/main.rb
    STARTX             = 0.0
    STARTY             = 0.0
    ENDY               = 20.0
    ENDX               = 20.0
    SPINPOINT          = 10
    SPINDURATION       = 400
    POINTSIZE          = 8
    BOXDEPTH           = 40
    YAW                = 1
    DISTANCE           = 10
    
    def tick args
      args.outputs.background_color = [0, 0, 0]
      a = Math.sin(Kernel.tick_count / SPINDURATION) * Math.tan(Kernel.tick_count / SPINDURATION)
      s = Math.sin(a)
      c = Math.cos(a)
      x = STARTX
      y = STARTY
      offset_x = (1280 - (ENDX - STARTX)) / 2
      offset_y =  (360 - (ENDY - STARTY)) / 2
    
      srand(1)
      while y < ENDY do
        while x < ENDX do
          if (y == STARTY ||
              y == (ENDY / 0.5) * 2 ||
              y == (ENDY / 0.5) * 2 + 0.5 ||
              y == ENDY - 0.5 ||
              x == STARTX ||
              x == ENDX - 0.5)
            z = rand(BOXDEPTH)
            z *= Math.sin(a / 2)
            x -= SPINPOINT
            u = (x * c) - (z * s)
            v = (x * s) + (z * c)
            k = DISTANCE.fdiv(100) + (v / 500 * YAW)
            u = u / k
            v = y / k
            w = POINTSIZE / 10 / k
            args.outputs.sprites << { x: offset_x + u - w, y: offset_y + v - w, w: w, h: w, path: 'sprites/square-blue.png'}
            x += SPINPOINT
          end
          x += 0.5
        end
        y += 0.5
        x = STARTX
      end
    end
    
    $gtk.reset
    
    

    Wireframe - main.rb link

    # ./samples/99_genre_3d/02_wireframe/app/main.rb
    def tick args
      args.state.model   ||= Object3D.new('data/shuttle.off')
      args.state.mtx     ||= rotate3D(0, 0, 0)
      args.state.inv_mtx ||= rotate3D(0, 0, 0)
      delta_mtx          = rotate3D(args.inputs.up_down * 0.01, input_roll(args) * 0.01, args.inputs.left_right * 0.01)
      args.outputs.lines << args.state.model.edges
      args.state.model.fast_3x3_transform! args.state.inv_mtx
      args.state.inv_mtx = mtx_mul(delta_mtx.transpose, args.state.inv_mtx)
      args.state.mtx     = mtx_mul(args.state.mtx, delta_mtx)
      args.state.model.fast_3x3_transform! args.state.mtx
      args.outputs.background_color = [0, 0, 0]
      args.outputs.debug << args.gtk.framerate_diagnostics_primitives
    end
    
    def input_roll args
      roll = 0
      roll += 1 if args.inputs.keyboard.e
      roll -= 1 if args.inputs.keyboard.q
      roll
    end
    
    def rotate3D(theta_x = 0.1, theta_y = 0.1, theta_z = 0.1)
      c_x, s_x = Math.cos(theta_x), Math.sin(theta_x)
      c_y, s_y = Math.cos(theta_y), Math.sin(theta_y)
      c_z, s_z = Math.cos(theta_z), Math.sin(theta_z)
      rot_x    = [[1, 0, 0], [0, c_x, -s_x], [0, s_x, c_x]]
      rot_y    = [[c_y, 0, s_y], [0, 1, 0], [-s_y, 0, c_y]]
      rot_z    = [[c_z, -s_z, 0], [s_z, c_z, 0], [0, 0, 1]]
      mtx_mul(mtx_mul(rot_x, rot_y), rot_z)
    end
    
    def mtx_mul(a, b)
      is = (0...a.length)
      js = (0...b[0].length)
      ks = (0...b.length)
      is.map do |i|
        js.map do |j|
          ks.map do |k|
            a[i][k] * b[k][j]
          end.reduce(&:plus)
        end
      end
    end
    
    class Object3D
      attr_reader :vert_count, :face_count, :edge_count, :verts, :faces, :edges
    
      def initialize(path)
        @vert_count = 0
        @face_count = 0
        @edge_count = 0
        @verts      = []
        @faces      = []
        @edges      = []
        _init_from_file path
      end
    
      def _init_from_file path
        file_lines = $gtk.read_file(path).split("\n")
                         .reject { |line| line.start_with?('#') || line.split(' ').length == 0 } # Strip out simple comments and blank lines
                         .map { |line| line.split('#')[0] } # Strip out end of line comments
                         .map { |line| line.split(' ') } # Tokenize by splitting on whitespace
        raise "OFF file did not start with OFF." if file_lines.shift != ["OFF"] # OFF meshes are supposed to begin with "OFF" as the first line.
        raise "<NVertices NFaces NEdges> line malformed" if file_lines[0].length != 3 # The second line needs to have 3 numbers. Raise an error if it doesn't.
        @vert_count, @face_count, @edge_count = file_lines.shift&.map(&:to_i) # Update the counts
        # Only the vertex and face counts need to be accurate. Raise an error if they are inaccurate.
        raise "Incorrect number of vertices and/or faces (Parsed VFE header: #{@vert_count} #{@face_count} #{@edge_count})" if file_lines.length != @vert_count + @face_count
        # Grab all the lines describing vertices.
        vert_lines = file_lines[0, @vert_count]
        # Grab all the lines describing faces.
        face_lines = file_lines[@vert_count, @face_count]
        # Create all the vertices
        @verts = vert_lines.map_with_index { |line, id| Vertex.new(line, id) }
        # Create all the faces
        @faces = face_lines.map { |line| Face.new(line, @verts) }
        # Create all the edges
        @edges = @faces.flat_map(&:edges).uniq do |edge|
          sorted = edge.sorted
          [sorted.point_a, sorted.point_b]
        end
      end
    
      def fast_3x3_transform! mtx
        @verts.each { |vert| vert.fast_3x3_transform! mtx }
      end
    end
    
    class Face
    
      attr_reader :verts, :edges
    
      def initialize(data, verts)
        vert_count = data[0].to_i
        vert_ids   = data[1, vert_count].map(&:to_i)
        @verts     = vert_ids.map { |i| verts[i] }
        @edges     = []
        (0...vert_count).each { |i| @edges[i] = Edge.new(verts[vert_ids[i - 1]], verts[vert_ids[i]]) }
        @edges.rotate! 1
      end
    end
    
    class Edge
      attr_reader :point_a, :point_b
    
      def initialize(point_a, point_b)
        @point_a = point_a
        @point_b = point_b
      end
    
      def sorted
        @point_a.id < @point_b.id ? self : Edge.new(@point_b, @point_a)
      end
    
      def draw_override ffi
        ffi.draw_line(@point_a.render_x, @point_a.render_y, @point_b.render_x, @point_b.render_y, 255, 0, 0, 128)
        ffi.draw_line(@point_a.render_x+1, @point_a.render_y, @point_b.render_x+1, @point_b.render_y, 255, 0, 0, 128)
        ffi.draw_line(@point_a.render_x, @point_a.render_y+1, @point_b.render_x, @point_b.render_y+1, 255, 0, 0, 128)
        ffi.draw_line(@point_a.render_x+1, @point_a.render_y+1, @point_b.render_x+1, @point_b.render_y+1, 255, 0, 0, 128)
      end
    
      def primitive_marker
        :line
      end
    end
    
    class Vertex
      attr_accessor :x, :y, :z, :id
    
      def initialize(data, id)
        @x  = data[0].to_f
        @y  = data[1].to_f
        @z  = data[2].to_f
        @id = id
      end
    
      def fast_3x3_transform! mtx
        _x, _y, _z = @x, @y, @z
        @x         = mtx[0][0] * _x + mtx[0][1] * _y + mtx[0][2] * _z
        @y         = mtx[1][0] * _x + mtx[1][1] * _y + mtx[1][2] * _z
        @z         = mtx[2][0] * _x + mtx[2][1] * _y + mtx[2][2] * _z
      end
    
      def render_x
        @x * (10 / (5 - @y)) * 170 + 640
      end
    
      def render_y
        @z * (10 / (5 - @y)) * 170 + 360
      end
    end
    

    Wireframe - Data - what-is-this.txt link

    # ./samples/99_genre_3d/02_wireframe/data/what-is-this.txt
    https://en.wikipedia.org/wiki/OFF_(file_format)
    

    Yaw Pitch Roll - main.rb link

    # ./samples/99_genre_3d/03_yaw_pitch_roll/app/main.rb
    class Game
      include MatrixFunctions
    
      attr_gtk
    
      def tick
        defaults
        render
        input
      end
    
      def player_ship
        [
          # engine back
          (vec4  -1,  -1,  1,  0),
          (vec4  -1,   1,  1,  0),
    
          (vec4  -1,   1,  1,  0),
          (vec4   1,   1,  1,  0),
    
          (vec4   1,   1,  1,  0),
          (vec4   1,  -1,  1,  0),
    
          (vec4   1,  -1,  1,  0),
          (vec4  -1,  -1,  1,  0),
    
          # engine front
          (vec4  -1,  -1,  -1,  0),
          (vec4  -1,   1,  -1,  0),
    
          (vec4  -1,   1,  -1,  0),
          (vec4   1,   1,  -1,  0),
    
          (vec4   1,   1,  -1,  0),
          (vec4   1,  -1,  -1,  0),
    
          (vec4   1,  -1,  -1,  0),
          (vec4  -1,  -1,  -1,  0),
    
          # engine left
          (vec4  -1,   -1,  -1,  0),
          (vec4  -1,   -1,   1,  0),
    
          (vec4  -1,   -1,   1,  0),
          (vec4  -1,    1,   1,  0),
    
          (vec4  -1,    1,   1,  0),
          (vec4  -1,    1,  -1,  0),
    
          (vec4  -1,    1,  -1,  0),
          (vec4  -1,   -1,  -1,  0),
    
          # engine right
          (vec4   1,   -1,  -1,  0),
          (vec4   1,   -1,   1,  0),
    
          (vec4   1,   -1,   1,  0),
          (vec4   1,    1,   1,  0),
    
          (vec4   1,    1,   1,  0),
          (vec4   1,    1,  -1,  0),
    
          (vec4   1,    1,  -1,  0),
          (vec4   1,   -1,  -1,  0),
    
          # top front of engine to front of ship
          (vec4   1,    1,  1,  0),
          (vec4   0,   -1,  9,  0),
    
          (vec4   0,   -1,  9,  0),
          (vec4  -1,    1,  1,  0),
    
          # bottom front of engine
          (vec4   1,   -1,  1,  0),
          (vec4   0,   -1,  9,  0),
    
          (vec4  -1,   -1,  1,  0),
          (vec4   0,   -1,  9,  0),
    
          # right wing
          # front of wing
          (vec4  1,  0.10,   1,  0),
          (vec4  9,  0.10,  -1,  0),
    
          (vec4   9,  0.10,  -1,  0),
          (vec4  10,  0.10,  -2,  0),
    
          # back of wing
          (vec4  1,  0.10,  -1,  0),
          (vec4  9,  0.10,  -1,  0),
    
          (vec4  10,  0.10,  -2,  0),
          (vec4   8,  0.10,  -1,  0),
    
          # front of wing
          (vec4  1,  -0.10,   1,  0),
          (vec4  9,  -0.10,  -1,  0),
    
          (vec4   9,  -0.10,  -1,  0),
          (vec4  10,  -0.10,  -2,  0),
    
          # back of wing
          (vec4  1,  -0.10,  -1,  0),
          (vec4  9,  -0.10,  -1,  0),
    
          (vec4  10,  -0.10,  -2,  0),
          (vec4   8,  -0.10,  -1,  0),
    
          # left wing
          # front of wing
          (vec4  -1,  0.10,   1,  0),
          (vec4  -9,  0.10,  -1,  0),
    
          (vec4  -9,  0.10,  -1,  0),
          (vec4  -10,  0.10,  -2,  0),
    
          # back of wing
          (vec4  -1,  0.10,  -1,  0),
          (vec4  -9,  0.10,  -1,  0),
    
          (vec4  -10,  0.10,  -2,  0),
          (vec4  -8,  0.10,  -1,  0),
    
          # front of wing
          (vec4  -1,  -0.10,   1,  0),
          (vec4  -9,  -0.10,  -1,  0),
    
          (vec4  -9,  -0.10,  -1,  0),
          (vec4  -10,  -0.10,  -2,  0),
    
          # back of wing
          (vec4  -1,  -0.10,  -1,  0),
          (vec4  -9,  -0.10,  -1,  0),
          (vec4  -10,  -0.10,  -2,  0),
          (vec4   -8,  -0.10,  -1,  0),
    
          # left fin
          # top
          (vec4  -1,  0.10,  1,  0),
          (vec4  -1,  3,  -3,  0),
    
          (vec4  -1,  0.10,  -1,  0),
          (vec4  -1,  3,  -3,  0),
    
          (vec4  -1.1,  0.10,  1,  0),
          (vec4  -1.1,  3,  -3,  0),
    
          (vec4  -1.1,  0.10,  -1,  0),
          (vec4  -1.1,  3,  -3,  0),
    
          # bottom
          (vec4  -1,  -0.10,  1,  0),
          (vec4  -1,  -2,  -2,  0),
    
          (vec4  -1,  -0.10,  -1,  0),
          (vec4  -1,  -2,  -2,  0),
    
          (vec4  -1.1,  -0.10,  1,  0),
          (vec4  -1.1,  -2,  -2,  0),
    
          (vec4  -1.1,  -0.10,  -1,  0),
          (vec4  -1.1,  -2,  -2,  0),
    
          # right fin
          (vec4   1,  0.10,  1,  0),
          (vec4   1,  3,  -3,  0),
    
          (vec4   1,  0.10,  -1,  0),
          (vec4   1,  3,  -3,  0),
    
          (vec4   1.1,  0.10,  1,  0),
          (vec4   1.1,  3,  -3,  0),
    
          (vec4   1.1,  0.10,  -1,  0),
          (vec4   1.1,  3,  -3,  0),
    
          # bottom
          (vec4   1,  -0.10,  1,  0),
          (vec4   1,  -2,  -2,  0),
    
          (vec4   1,  -0.10,  -1,  0),
          (vec4   1,  -2,  -2,  0),
    
          (vec4   1.1,  -0.10,  1,  0),
          (vec4   1.1,  -2,  -2,  0),
    
          (vec4   1.1,  -0.10,  -1,  0),
          (vec4   1.1,  -2,  -2,  0),
        ]
      end
    
      def defaults
        state.points ||= player_ship
        state.shifted_points ||= state.points.map { |point| point }
    
        state.scale   ||= 1
        state.angle_x ||= 0
        state.angle_y ||= 0
        state.angle_z ||= 0
      end
    
      def angle_z_matrix degrees
        cos_t = Math.cos degrees.to_radians
        sin_t = Math.sin degrees.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 angle_y_matrix degrees
        cos_t = Math.cos degrees.to_radians
        sin_t = Math.sin degrees.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 angle_x_matrix degrees
        cos_t = Math.cos degrees.to_radians
        sin_t = Math.sin degrees.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 scale_matrix factor
        (mat4 factor,      0,      0, 0,
              0,      factor,      0, 0,
              0,           0, factor, 0,
              0,           0,      0, 1)
      end
    
      def input
        if (inputs.keyboard.shift && inputs.keyboard.p)
          state.scale -= 0.1
        elsif  inputs.keyboard.p
          state.scale += 0.1
        end
    
        if inputs.mouse.wheel
          state.scale += inputs.mouse.wheel.y
        end
    
        state.scale = state.scale.clamp(0.1, 1000)
    
        if (inputs.keyboard.shift && inputs.keyboard.y) || inputs.keyboard.right
          state.angle_y += 1
        elsif (inputs.keyboard.y) || inputs.keyboard.left
          state.angle_y -= 1
        end
    
        if (inputs.keyboard.shift && inputs.keyboard.x) || inputs.keyboard.down
          state.angle_x -= 1
        elsif (inputs.keyboard.x || inputs.keyboard.up)
          state.angle_x += 1
        end
    
        if inputs.keyboard.shift && inputs.keyboard.z
          state.angle_z += 1
        elsif inputs.keyboard.z
          state.angle_z -= 1
        end
    
        if inputs.keyboard.zero
          state.angle_x = 0
          state.angle_y = 0
          state.angle_z = 0
        end
    
        angle_x = state.angle_x
        angle_y = state.angle_y
        angle_z = state.angle_z
        scale   = state.scale
    
        s_matrix = scale_matrix state.scale
        x_matrix = angle_z_matrix angle_z
        y_matrix = angle_y_matrix angle_y
        z_matrix = angle_x_matrix angle_x
    
        state.shifted_points = state.points.map do |point|
          (mul point, y_matrix, x_matrix, z_matrix, s_matrix).merge(original: point)
        end
      end
    
      def thick_line line
        [
          line.merge(y: line.y - 1, y2: line.y2 - 1, r: 0, g: 0, b: 0),
          line.merge(x: line.x - 1, x2: line.x2 - 1, r: 0, g: 0, b: 0),
          line.merge(x: line.x - 0, x2: line.x2 - 0, r: 0, g: 0, b: 0),
          line.merge(y: line.y + 1, y2: line.y2 + 1, r: 0, g: 0, b: 0),
          line.merge(x: line.x + 1, x2: line.x2 + 1, r: 0, g: 0, b: 0)
        ]
      end
    
      def render
        outputs.lines << state.shifted_points.each_slice(2).map do |(p1, p2)|
          perc = 0
          thick_line({ x:  p1.x.*(10) + 640, y:  p1.y.*(10) + 320,
                       x2: p2.x.*(10) + 640, y2: p2.y.*(10) + 320,
                       r: 255 * perc,
                       g: 255 * perc,
                       b: 255 * perc })
        end
    
        outputs.labels << [ 10, 700, "angle_x: #{state.angle_x.to_sf}", 0]
        outputs.labels << [ 10, 670, "x, shift+x", 0]
    
        outputs.labels << [210, 700, "angle_y: #{state.angle_y.to_sf}", 0]
        outputs.labels << [210, 670, "y, shift+y", 0]
    
        outputs.labels << [410, 700, "angle_z: #{state.angle_z.to_sf}", 0]
        outputs.labels << [410, 670, "z, shift+z", 0]
    
        outputs.labels << [610, 700, "scale: #{state.scale.to_sf}", 0]
        outputs.labels << [610, 670, "p, shift+p", 0]
      end
    end
    
    $game = Game.new
    
    def tick args
      $game.args = args
      $game.tick
    end
    
    def set_angles x, y, z
      $game.state.angle_x = x
      $game.state.angle_y = y
      $game.state.angle_z = z
    end
    
    $gtk.reset
    
    

    Ray Caster - main.rb link

    # ./samples/99_genre_3d/04_ray_caster/app/main.rb
    # https://github.com/BrennerLittle/DragonRubyRaycast
    # https://github.com/3DSage/OpenGL-Raycaster_v1
    # https://www.youtube.com/watch?v=gYRrGTC7GtA&ab_channel=3DSage
    
    def tick args
      defaults args
      calc args
      render args
      args.outputs.sprites << { x: 0, y: 0, w: 1280 * 2.66, h: 720 * 2.25, path: :screen }
      args.outputs.labels  << { x: 30, y: 30.from_top, text: "FPS: #{args.gtk.current_framerate.to_sf}" }
    end
    
    def defaults args
      args.state.stage ||= {
        w: 8,
        h: 8,
        sz: 64,
        layout: [
          1, 1, 1, 1, 1, 1, 1, 1,
          1, 0, 1, 0, 0, 0, 0, 1,
          1, 0, 1, 0, 0, 1, 0, 1,
          1, 0, 1, 0, 0, 0, 0, 1,
          1, 0, 0, 0, 0, 0, 0, 1,
          1, 0, 0, 0, 0, 1, 0, 1,
          1, 0, 0, 0, 0, 0, 0, 1,
          1, 1, 1, 1, 1, 1, 1, 1,
        ]
      }
    
      args.state.player ||= {
        x: 250,
        y: 250,
        dx: 1,
        dy: 0,
        angle: 0
      }
    end
    
    def calc args
      xo = 0
    
      if args.state.player.dx < 0
        xo = -20
      else
        xo = 20
      end
    
      yo = 0
    
      if args.state.player.dy < 0
        yo = -20
      else
        yo = 20
      end
    
      ipx = args.state.player.x.idiv 64.0
      ipx_add_xo = (args.state.player.x + xo).idiv 64.0
      ipx_sub_xo = (args.state.player.x - xo).idiv 64.0
    
      ipy = args.state.player.y.idiv 64.0
      ipy_add_yo = (args.state.player.y + yo).idiv 64.0
      ipy_sub_yo = (args.state.player.y - yo).idiv 64.0
    
      if args.inputs.keyboard.right
        args.state.player.angle -= 5
        args.state.player.angle = args.state.player.angle % 360
        args.state.player.dx = args.state.player.angle.cos_d
        args.state.player.dy = -args.state.player.angle.sin_d
      end
    
      if args.inputs.keyboard.left
        args.state.player.angle += 5
        args.state.player.angle = args.state.player.angle % 360
        args.state.player.dx = args.state.player.angle.cos_d
        args.state.player.dy = -args.state.player.angle.sin_d
      end
    
      if args.inputs.keyboard.up
        if args.state.stage.layout[ipy * args.state.stage.w + ipx_add_xo] == 0
          args.state.player.x += args.state.player.dx * 5
        end
    
        if args.state.stage.layout[ipy_add_yo * args.state.stage.w + ipx] == 0
          args.state.player.y += args.state.player.dy * 5
        end
      end
    
      if args.inputs.keyboard.down
        if args.state.stage.layout[ipy * args.state.stage.w + ipx_sub_xo] == 0
          args.state.player.x -= args.state.player.dx * 5
        end
    
        if args.state.stage.layout[ipy_sub_yo * args.state.stage.w + ipx] == 0
          args.state.player.y -= args.state.player.dy * 5
        end
      end
    end
    
    def render args
      args.outputs[:screen].sprites << { x: 0,
                                         y: 160,
                                         w: 750,
                                         h: 160,
                                         path: :pixel,
                                         r: 89,
                                         g: 125,
                                         b: 206 }
    
      args.outputs[:screen].sprites << { x: 0,
                                         y: 0,
                                         w: 750,
                                         h: 160,
                                         path: :pixel,
                                         r: 117,
                                         g: 113,
                                         b: 97 }
    
    
      ra = (args.state.player.angle + 30) % 360
    
      60.times do |r|
        dof = 0
        side = 0
        dis_v = 100000
        ra_tan = ra.tan_d
    
        if ra.cos_d > 0.001
          rx = ((args.state.player.x >> 6) << 6) + 64
          ry = (args.state.player.x - rx) * ra_tan + args.state.player.y;
          xo = 64
          yo = -xo * ra_tan
        elsif ra.cos_d < -0.001
          rx = ((args.state.player.x >> 6) << 6) - 0.0001
          ry = (args.state.player.x - rx) * ra_tan + args.state.player.y
          xo = -64
          yo = -xo * ra_tan
        else
          rx = args.state.player.x
          ry = args.state.player.y
          dof = 8
        end
    
        while dof < 8
          mx = rx >> 6
          mx = mx.to_i
          my = ry >> 6
          my = my.to_i
          mp = my * args.state.stage.w + mx
          if mp > 0 && mp < args.state.stage.w * args.state.stage.h && args.state.stage.layout[mp] == 1
            dof = 8
            dis_v = ra.cos_d * (rx - args.state.player.x) - ra.sin_d * (ry - args.state.player.y)
          else
            rx += xo
            ry += yo
            dof += 1
          end
        end
    
        vx = rx
        vy = ry
    
        dof = 0
        dis_h = 100000
        ra_tan = 1.0 / ra_tan
    
        if ra.sin_d > 0.001
          ry = ((args.state.player.y >> 6) << 6) - 0.0001;
          rx = (args.state.player.y - ry) * ra_tan + args.state.player.x;
          yo = -64;
          xo = -yo * ra_tan;
        elsif ra.sin_d < -0.001
          ry = ((args.state.player.y >> 6) << 6) + 64;
          rx = (args.state.player.y - ry) * ra_tan + args.state.player.x;
          yo = 64;
          xo = -yo * ra_tan;
        else
          rx = args.state.player.x
          ry = args.state.player.y
          dof = 8
        end
    
        while dof < 8
          mx = (rx) >> 6
          my = (ry) >> 6
          mp = my * args.state.stage.w + mx
          if mp > 0 && mp < args.state.stage.w * args.state.stage.h && args.state.stage.layout[mp] == 1
            dof = 8
            dis_h = ra.cos_d * (rx - args.state.player.x) - ra.sin_d * (ry - args.state.player.y)
          else
            rx += xo
            ry += yo
            dof += 1
          end
        end
    
        color = { r: 52, g: 101, b: 36 }
    
        if dis_v < dis_h
          rx = vx
          ry = vy
          dis_h = dis_v
          color = { r: 109, g: 170, b: 44 }
        end
    
        ca = (args.state.player.angle - ra) % 360
        dis_h = dis_h * ca.cos_d
        line_h = (args.state.stage.sz * 320) / (dis_h)
        line_h = 320 if line_h > 320
    
        line_off = 160 - (line_h >> 1)
    
        args.outputs[:screen].sprites << {
          x: r * 8,
          y: line_off,
          w: 8,
          h: line_h,
          path: :pixel,
          **color
        }
    
        ra = (ra - 1) % 360
      end
    end
    
    

    Ray Caster Advanced - main.rb link

    # ./samples/99_genre_3d/04_ray_caster_advanced/app/main.rb
    =begin
    
    This sample is a more advanced example of raycasting that extends the previous 04_ray_caster sample.
    Refer to the prior sample to to understand the fundamental raycasting algorithm.
    This sample adds:
     * higher resolution of raycasting
     * Wall textures
     * Simple "drop off" lighting
     * Weapon firing
     * Drawing of sprites within the level.
    
    # Contributors outside of DragonRuby who also hold Copyright:
    # - James Stocks: https://github.com/james-stocks
    
    =end
    
    # https://github.com/BrennerLittle/DragonRubyRaycast
    # https://github.com/3DSage/OpenGL-Raycaster_v1
    # https://www.youtube.com/watch?v=gYRrGTC7GtA&ab_channel=3DSage
    
    def tick args
      defaults args
      update_player args
      update_missiles args
      update_enemies args
      render args
      args.outputs.sprites << { x: 0, y: 0, w: 1280 * 1.5, h: 720 * 1.2, path: :screen }
      args.outputs.labels  << { x: 30, y: 30.from_top, text: "FPS: #{args.gtk.current_framerate.to_sf} X: #{args.state.player.x} Y: #{args.state.player.y}" }
    end
    
    def defaults args
      args.state.stage ||= {
        w: 8,       # Width of the tile map
        h: 8,       # Height of the tile map
        sz: 64,     # To define a 3D space, define a size (in arbitrary units) we consider one map tile to be.
        layout: [
          1, 1, 1, 1, 2, 1, 1, 1,
          1, 0, 1, 0, 0, 0, 0, 1,
          1, 0, 1, 0, 0, 3, 0, 1,
          1, 0, 1, 0, 0, 0, 0, 2,
          1, 0, 0, 0, 0, 0, 0, 1,
          1, 0, 0, 0, 0, 3, 0, 1,
          1, 0, 0, 0, 0, 0, 0, 1,
          1, 1, 1, 2, 1, 1, 1, 1,
        ]
      }
    
      args.state.player ||= {
        x: 250,
        y: 250,
        dx: 1,
        dy: 0,
        angle: 0,
        fire_cooldown_wait: 0,
        fire_cooldown_duration: 15
      }
    
      # Add an initial alien enemy.
      # The :bright property indicates that this entity doesn't produce light and should appear dimmer over distance.
      args.state.enemies ||= [{ x: 280, y: 280, type: :alien, bright: false, expired: false }]
      args.state.missiles ||= []
      args.state.splashes ||= []
    end
    
    # Update the player's input and movement
    def update_player args
    
      player = args.state.player
      player.fire_cooldown_wait -= 1 if player.fire_cooldown_wait > 0
    
      xo = 0
    
      if player.dx < 0
        xo = -20
      else
        xo = 20
      end
    
      yo = 0
    
      if player.dy < 0
        yo = -20
      else
        yo = 20
      end
    
      ipx = player.x.idiv 64.0
      ipx_add_xo = (player.x + xo).idiv 64.0
      ipx_sub_xo = (player.x - xo).idiv 64.0
    
      ipy = player.y.idiv 64.0
      ipy_add_yo = (player.y + yo).idiv 64.0
      ipy_sub_yo = (player.y - yo).idiv 64.0
    
      if args.inputs.keyboard.right
        player.angle -= 5
        player.angle = player.angle % 360
        player.dx = player.angle.cos_d
        player.dy = -player.angle.sin_d
      end
    
      if args.inputs.keyboard.left
        player.angle += 5
        player.angle = player.angle % 360
        player.dx = player.angle.cos_d
        player.dy = -player.angle.sin_d
      end
    
      if args.inputs.keyboard.up
        if args.state.stage.layout[ipy * args.state.stage.w + ipx_add_xo] == 0
          player.x += player.dx * 5
        end
    
        if args.state.stage.layout[ipy_add_yo * args.state.stage.w + ipx] == 0
          player.y += player.dy * 5
        end
      end
    
      if args.inputs.keyboard.down
        if args.state.stage.layout[ipy * args.state.stage.w + ipx_sub_xo] == 0
          player.x -= player.dx * 5
        end
    
        if args.state.stage.layout[ipy_sub_yo * args.state.stage.w + ipx] == 0
          player.y -= player.dy * 5
        end
      end
    
      if args.inputs.keyboard.key_down.space && player.fire_cooldown_wait == 0
        m = { x: player.x, y: player.y, angle: player.angle, speed: 6, type: :missile, bright: true, expired: false }
        # Immediately move the missile forward a frame so it spawns ahead of the player
        m.x += m.angle.cos_d * m.speed
        m.y -= m.angle.sin_d * m.speed
        args.state.missiles << m
        player.fire_cooldown_wait = player.fire_cooldown_duration
      end
    end
    
    def update_missiles args
      # Remove expired missiles by mapping expired missiles to `nil` and then calling `compact!` to
      # remove nil entries.
      args.state.missiles.map! { |m| m.expired ? nil : m }
      args.state.missiles.compact!
    
      args.state.missiles.each do |m|
        new_x = m.x + m.angle.cos_d * m.speed
        new_y = m.y - m.angle.sin_d * m.speed
        # Hit enemies
        args.state.enemies.each do |e|
            if (new_x - e.x).abs < 16 && (new_y - e.y).abs < 16
                e.expired = true
                m.expired = true
                args.state.splashes << { x: m.x, y: m.y, ttl: 5, type: :splash, bright: true }
                next
            end
        end
        # Hit walls
        if(args.state.stage.layout[(new_y / 64).to_i * args.state.stage.w + (new_x / 64).to_i] != 0)
          m.expired = true
          args.state.splashes << { x: m.x, y: m.y, ttl: 5, type: :splash, bright: true }
        else
          m.x = new_x
          m.y = new_y
        end
      end
      args.state.splashes.map! { |s| s.ttl <= 0 ? nil : s }
      args.state.splashes.compact!
      args.state.splashes.each do |s|
        s.ttl -= 1
      end
    end
    
    def update_enemies args
        args.state.enemies.map! { |e| e.expired ?  nil : e }
        args.state.enemies.compact!
    end
    
    def render args
      # Render the sky
      args.outputs[:screen].sprites << { x: 0,
                                         y: 320,
                                         w: 960,
                                         h: 320,
                                         path: :pixel,
                                         r: 89,
                                         g: 125,
                                         b: 206 }
    
      # Render the floor
      args.outputs[:screen].sprites << { x: 0,
                                         y: 0,
                                         w: 960,
                                         h: 320,
                                         path: :pixel,
                                         r: 117,
                                         g: 113,
                                         b: 97 }
    
      ra = (args.state.player.angle + 30) % 360
    
      # Collect sprites for the raycast view into an array - these will all be rendered with a single draw call.
      # This gives a substantial performance improvement over the previous sample where there was one draw call
      # per sprite.
      sprites_to_draw = []
    
      # Save distances of each wall hit. This is used subsequently when drawing sprites.
      depths = []
    
      # Cast 120 rays across 60 degress - we'll consider the next 0.5 degrees each ray
      120.times do |r|
    
        # The next ~120 lines are largely the same as the previous sample. The changes are:
        # - Increment by 0.5 degrees instead of 1 degree for the next ray.
        # - When a wall hit is found, the distance is stored in the `depths` array.
        #   - `depths` is used later when rendering enemies and bullet.
        # - We draw a slice of a wall texture instead of a solid color.
        # - The wall strip for the array hit is appended to `sprites_to_draw` instead of being drawn immediately.
        dof = 0
        max_dof = 8
        dis_v = 100000
    
        ra_tan = Math.tan(ra * Math::PI / 180)
    
        if ra.cos_d > 0.001
          rx = ((args.state.player.x >> 6) << 6) + 64
    
          ry = (args.state.player.x - rx) * ra_tan + args.state.player.y;
          xo = 64
          yo = -xo * ra_tan
        elsif ra.cos_d < -0.001
          rx = ((args.state.player.x >> 6) << 6) - 0.0001
          ry = (args.state.player.x - rx) * ra_tan + args.state.player.y
          xo = -64
          yo = -xo * ra_tan
        else
          rx = args.state.player.x
          ry = args.state.player.y
          dof = max_dof
        end
    
        while dof < max_dof
          mx = rx >> 6
          mx = mx.to_i
          my = ry >> 6
          my = my.to_i
          mp = my * args.state.stage.w + mx
          if mp > 0 && mp < args.state.stage.w * args.state.stage.h && args.state.stage.layout[mp] > 0
            dof = max_dof
            dis_v = ra.cos_d * (rx - args.state.player.x) - ra.sin_d * (ry - args.state.player.y)
            wall_texture_v = args.state.stage.layout[mp]
          else
            rx += xo
            ry += yo
            dof += 1
          end
        end
    
        vx = rx
        vy = ry
    
        dof = 0
        dis_h = 100000
        ra_tan = 1.0 / ra_tan
    
        if ra.sin_d > 0.001
          ry = ((args.state.player.y >> 6) << 6) - 0.0001;
          rx = (args.state.player.y - ry) * ra_tan + args.state.player.x;
          yo = -64;
          xo = -yo * ra_tan;
        elsif ra.sin_d < -0.001
          ry = ((args.state.player.y >> 6) << 6) + 64;
          rx = (args.state.player.y - ry) * ra_tan + args.state.player.x;
          yo = 64;
          xo = -yo * ra_tan;
        else
          rx = args.state.player.x
          ry = args.state.player.y
          dof = 8
        end
    
        while dof < 8
          mx = (rx) >> 6
          my = (ry) >> 6
          mp = my * args.state.stage.w + mx
          if mp > 0 && mp < args.state.stage.w * args.state.stage.h && args.state.stage.layout[mp] > 0
            dof = 8
            dis_h = ra.cos_d * (rx - args.state.player.x) - ra.sin_d * (ry - args.state.player.y)
            wall_texture = args.state.stage.layout[mp]
          else
            rx += xo
            ry += yo
            dof += 1
          end
        end
    
        dist = dis_h
        if dis_v < dis_h
          rx = vx
          ry = vy
          dist = dis_v
          wall_texture = wall_texture_v
        end
        # Store the distance for a wall hit at this angle
        depths << dist
    
        # Adjust for fish-eye across FOV
        ca = (args.state.player.angle - ra) % 360
        dist = dist * ca.cos_d
        # Determine the render height for the strip proportional to the display height
        line_h = (args.state.stage.sz * 640) / (dist)
    
        line_off = 320 - (line_h >> 1)
    
        # Tint the wall strip - the further away it is, the darker.
        tint = 1.0 - (dist / 500)
    
        # Wall texturing - Determine the section of source texture to use
        tx = dis_v > dis_h ? (rx.to_i % 64).to_i : (ry.to_i % 64).to_i
        # If player is looking backwards towards a tile then flip the side of the texture to sample.
        # The sample wall textures have a diagonal stripe pattern - if you comment out these 2 lines,
        # you will see what goes wrong with texturing.
        tx = 63 - tx if (ra > 180 && dis_v > dis_h)
        tx = 63 - tx if (ra > 90 && ra < 270 && dis_v < dis_h)
    
        sprites_to_draw << {
          x: r * 8,
          y: line_off,
          w: 8,
          h: line_h,
          path: "sprites/wall_#{wall_texture}.png",
          source_x: tx,
          source_w: 1,
          r: 255 * tint,
          g: 255 * tint,
          b: 255 * tint
        }
    
        # Increment the raycast angle for the next iteration of this loop
        ra = (ra - 0.5) % 360
      end
    
      # Render sprites
      # Use common render code for enemies, missiles and explosion splashes.
      # This works because they are all hashes with :x, :y, and :type fields.
      things_to_draw = []
      things_to_draw.push(*args.state.enemies)
      things_to_draw.push(*args.state.missiles)
      things_to_draw.push(*args.state.splashes)
    
      # Do a first-pass on the things to draw, calculate distance from player and then
      # sort so more-distant things are drawn first.
      things_to_draw.each do |t|
        t[:dist] = args.geometry.distance([args.state.player[:x],args.state.player[:y]],[t[:x],t[:y]]).abs
      end
      things_to_draw = things_to_draw.sort_by { |t| t[:dist] }.reverse
    
      # Now draw everything, most distant entities first.
      things_to_draw.each do |t|
          distance_to_thing = t[:dist]
          # The crux of drawing a sprite in a raycast view is to:
          #   1. rotate the enemy around the player's position and viewing angle to get a position relative to the view.
          #   2. Translate that position from "3D space" to screen pixels.
          # The next 6 lines get the entitiy's position relative to the player position and angle:
          tx = t[:x] - args.state.player.x
          ty = t[:y] - args.state.player.y
          cs = Math.cos(args.state.player.angle * Math::PI / 180)
          sn = Math.sin(args.state.player.angle * Math::PI / 180)
          dx = ty * cs + tx * sn
          dy = tx * cs - ty * sn
    
          # The next 5 lines determine the screen x and y of (the center of) the entity, and a scale
          next if dy == 0 # Avoid invalid Infinity/NaN calculations if the projected Y is 0
          ody = dy
          dx = dx*640/(dy) + 480
          dy = 32/dy + 192
          scale = 64*360/(ody / 2)
    
          tint = t[:bright] ? 1.0 : 1.0 - (distance_to_thing / 500)
    
          # Now we know the x and y on-screen for the entity, and its scale, we can draw it.
          # Simply drawing the sprite on the screen doesn't work in a raycast view because the entity might be partly obscured by a wall.
          # Instead we draw the entity in vertical strips, skipping strips if a wall is closer to the player on that strip of the screen.
    
          # Since dx stores the center x of the enemy on-screen, we start half the scale of the enemy to the left of dx
          x = dx - scale/2
          next if (x > 960 or (dx + scale/2 <= 0)) # Skip rendering if the X position is entirely off-screen
          strip = 0                    # Keep track of the number of strips we've drawn
          strip_width = scale / 64     # Draw the sprite in 64 strips
          sample_width = 1             # For each strip we will sample 1/64 of sprite image, here we assume 64x64 sprites
    
          until x >= dx + scale/2 do
              if x > 0 && x < 960
                  # Here we get the distance to the wall for this strip on the screen
                  wall_depth = depths[(x.to_i/8)]
                  if ((distance_to_thing < wall_depth))
                      sprites_to_draw << {
                          x: x,
                          y: dy + 120 - scale * 0.6,
                          w: strip_width,
                          h: scale,
                          path: "sprites/#{t[:type]}.png",
                          source_x: strip * sample_width,
                          source_w: sample_width,
                          r: 255 * tint,
                          g: 255 * tint,
                          b: 255 * tint
                      }
                  end
              end
              x += strip_width
              strip += 1
          end
      end
    
      # Draw all the sprites we collected in the array to the render target
      args.outputs[:screen].sprites << sprites_to_draw
    end
    
    

    Genre Arcade link

    Bullet Heaven - main.rb link

    # ./samples/99_genre_arcade/bullet_heaven/app/main.rb
    class Game
      attr_gtk
    
      def initialize
        @level_scene = LevelScene.new
        @shop_scene = ShopScene.new
      end
    
      def tick
        defaults
        current_scene.args = args
        current_scene.tick
        if state.next_scene
          state.scene = state.next_scene
          state.scene_at = Kernel.tick_count
          state.next_scene = nil
        end
      end
    
      def current_scene
        if state.scene == :level
          @level_scene
        elsif state.scene == :shop
          @shop_scene
        end
      end
    
      def defaults
        state.shield ||= 10
        state.assembly_points ||= 4
        state.scene ||= :level
        state.bullets ||= []
        state.enemies ||= []
        state.bullet_speed ||= 5
        state.turret_position ||= { x: 640, y: 0 }
        state.blaster_spread ||= 1
        state.blaster_rate ||= 60
        state.level ||= 1
        state.bullet_damage ||= 1
        state.enemy_spawn_rate ||= 120
        state.enemy_min_health ||= 1
        state.enemy_health_range ||= 2
        state.enemies_to_spawn ||= 5
        state.enemies_spawned ||= 0
        state.enemy_dy ||= -0.2
      end
    end
    
    class ShopScene
      attr_gtk
    
      def activate
        state.module_selected = nil
        state.available_module_1 = :blaster_spread
        state.available_module_2 = :bullet_damage
        state.available_module_3 = if state.blaster_rate > 3
                                     :blaster_rate
                                   else
                                     nil
                                   end
      end
    
      def tick
        if state.scene_at == Kernel.tick_count - 1
          activate
        end
    
        state.next_wave_button ||= layout.rect(row: 0, col: 20, w: 4, h: 2)
        state.module_1_button  ||= layout.rect(row: 10, col: 0, w: 8, h: 2)
        state.module_2_button  ||= layout.rect(row: 10, col: 8, w: 8, h: 2)
        state.module_3_button  ||= layout.rect(row: 10, col: 16, w: 8, h: 2)
    
        calc
        render
      end
    
      def increase_difficulty_and_start_level
        state.next_scene = :level
        state.enemies_spawned = 0
        state.enemies = []
        state.level += 1
        state.enemy_spawn_rate = (state.enemy_spawn_rate * 0.95).to_i
        state.enemy_min_health = (state.enemy_min_health * 1.1).to_i + 1
        state.enemy_health_range = state.enemy_min_health * 2
        state.enemies_to_spawn = (state.enemies_to_spawn * 1.1).to_i + 2
        state.enemy_dy *= 1.05
      end
    
      def calc
        if state.module_selected
          if inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.next_wave_button)
            increase_difficulty_and_start_level
          end
        else
          if inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.module_1_button)
            perform_upgrade state.available_module_1
            state.available_module_1 = nil
            state.module_selected = true
          elsif inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.module_2_button)
            perform_upgrade state.available_module_2
            state.available_module_2 = nil
            state.module_selected = true
          elsif inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.module_3_button)
            perform_upgrade state.available_module_3
            state.available_module_3 = nil
            state.module_selected = true
          end
        end
      end
    
      def perform_upgrade module_name
        return if state.module_selected
        if module_name == :bullet_damage
          state.bullet_damage += 1
        elsif module_name == :blaster_rate
          state.blaster_rate = (state.blaster_rate * 0.85).to_i
          state.blaster_rate = 3 if state.blaster_rate < 3
        elsif module_name == :blaster_spread
          state.blaster_spread += 2
        else
          raise "perform_upgade: Unknown module: #{module_name}"
        end
      end
    
      def render
        outputs.background_color = [0, 0, 0]
        # outputs.primitives << layout.debug_primitives.map { |p| p.merge a: 80 }
    
        outputs.labels << layout.rect(row: 0, col: 11, w: 2, h: 1)
                                .center
                                .merge(text: "Select Upgrade", anchor_x: 0.5, anchor_y: 0.5, size_px: 50, r: 255, g: 255, b: 255)
    
        if state.module_selected
          outputs.primitives << button_prefab(state.next_wave_button, "Next Wave", a: 255)
        end
    
        a = if state.module_selected
              80
            else
              255
            end
    
        outputs.primitives << button_prefab(state.module_1_button, state.available_module_1, a: a)
        outputs.primitives << button_prefab(state.module_2_button, state.available_module_2, a: a)
        outputs.primitives << button_prefab(state.module_3_button, state.available_module_3, a: a)
      end
    
      def button_prefab rect, text, a: 255
        return nil if !text
        [
          rect.merge(path: :solid, r: 255, g: 255, b: 255, a: a),
          geometry.center(rect).merge(text: text.gsub("_", " "), anchor_x: 0.5, anchor_y: 0.5, r: 0, g: 0, b: 0, size_px: rect.h.idiv(4))
        ]
      end
    end
    
    class LevelScene
      attr_gtk
    
      def tick
        if inputs.keyboard.key_down.g
          state.enemies_spawned = state.enemies_to_spawn
          state.enemies = []
        elsif inputs.keyboard.key_down.forward_slash
          roll = rand
          if roll < 0.33
            state.bullet_damage += 1
            $gtk.notify_extended! message: "bullet damage increased: #{state.bullet_damage}", env: :prod
          elsif roll < 0.66
            if state.blaster_rate > 3
              state.blaster_rate = (state.blaster_rate * 0.85).to_i
              state.blaster_rate = 3 if state.blaster_rate < 3
              $gtk.notify_extended! message: "blaster rate upgraded: #{state.blaster_rate}", env: :prod
            else
              $gtk.notify_extended! message: "blaster rate already at fastest.", env: :prod
            end
          else
            state.blaster_spread += 2
            $gtk.notify_extended! message: "blaster spread increased: #{state.blaster_spread}", env: :prod
          end
        end
    
        calc
        render
      end
    
      def calc
        calc_bullets
        calc_enemies
        calc_bullet_hits
        calc_enemy_push_back
        calc_deaths
      end
    
      def calc_deaths
        state.enemies.reject! { |e| e.hp <= 0 }
        state.bullets.reject! { |b| b.dead_at }
      end
    
      def enemy_prefab enemy
        b = (enemy.hp / (state.enemy_min_health + state.enemy_health_range)) * 255
        [
          enemy.merge(path: :solid, r: 128, g: 0, b: b),
          geometry.center(enemy).merge(text: enemy.hp, anchor_x: 0.5, anchor_y: 0.5, r: 255, g: 255, b: 255, size_px: enemy.h * 0.5)
        ]
      end
    
      def render
        outputs.background_color = [0, 0, 0]
        level_completion_perc = (state.enemies_spawned - state.enemies.length).fdiv(state.enemies_to_spawn)
        outputs.primitives << { x: 30, y: 30.from_top, text: "Wave: #{state.level} (#{(level_completion_perc * 100).to_i}% complete)", r: 255, g: 255, b: 255 }
        outputs.primitives << { x: 30, y: 60.from_top, text: "Press G to skip to end of the current wave.", r: 255, g: 255, b: 255 }
        outputs.primitives << { x: 30, y: 90.from_top, text: "Press / to get a random upgrade immediately.", r: 255, g: 255, b: 255 }
    
        outputs.sprites << state.bullets.map do |b|
          b.merge w: 10, h: 10, path: :solid, r: 0, g: 255, b: 255
        end
    
        outputs.primitives << state.enemies.map { |e| enemy_prefab e }
      end
    
      def calc_bullets
        if Kernel.tick_count.zmod? state.blaster_rate
          bullet_count = state.blaster_spread
          min_degrees = state.blaster_spread.idiv(2) * -2
          bullet_count.times do |i|
            degree_offset = min_degrees + (i * 2)
            state.bullets << { x: 640,
                               y: 0,
                               dy: (attack_angle + degree_offset).vector_y * state.bullet_speed,
                               dx: (attack_angle + degree_offset).vector_x * state.bullet_speed }
          end
        end
    
        state.bullets.each do |b|
          b.x += b.dx
          b.y += b.dy
        end
    
        state.bullets.reject! { |b| b.y < 0 || b.y > 720 || b.x > 1280 || b.x < 0 }
      end
    
      def calc_enemies
        if Kernel.tick_count.zmod?(state.enemy_spawn_rate) && state.enemies_spawned < state.enemies_to_spawn
          state.enemies_spawned += 1
          x = rand(1280 - 96) + 48
          y = 720
          hp = state.enemy_min_health + rand(state.enemy_health_range)
          state.enemies << { x: x,
                             y: y,
                             w: 48,
                             h: 48,
                             push_back_x: 0,
                             push_back_y: 0,
                             spawn_at: Kernel.tick_count,
                             dy: state.enemy_dy,
                             start_hp: hp,
                             hp: hp }
        end
    
        state.enemies.each do |e|
          if e.y + e.h > 720
            e.y -= (((e.y + e.h) - 720) / e.h) * 10
          end
    
          e.y += e.dy
    
          if e.x < 0 && e.push_back_x < 0
            e.push_back_x = e.push_back_x.abs
          elsif (e.x + e.w) > 1280 && e.push_back_x > 0
            e.push_back_x = e.push_back_x.abs * -1
          end
    
          e.x += e.push_back_x
          e.y += e.push_back_y
    
          e.push_back_x *= 0.9
          e.push_back_y *= 0.9
        end
    
        state.enemies.reject! { |e| e.y < 0 }
    
        if state.enemies.empty? && state.enemies_spawned >= state.enemies_to_spawn
          state.next_scene = :shop
          state.bullets.clear
        end
      end
    
      def calc_bullet_hits
        state.bullets.each do |b|
          state.enemies.each do |e|
            if geometry.intersect_rect? b.merge(w: 4, h: 4, anchor_x: 0.5, anchor_x: 0.5), e
              e.hp -= state.bullet_damage
              push_back_angle = geometry.angle b, geometry.center(e)
              push_back_x = push_back_angle.vector_x * state.bullet_damage * 0.1
              push_back_y = push_back_angle.vector_y * state.bullet_damage * 0.1
              e.push_back_x += push_back_x
              e.push_back_y += push_back_y
              e.hit_at = Kernel.tick_count
              b.dead_at = Kernel.tick_count
            end
          end
        end
      end
    
      def calc_enemy_push_back
        state.enemies.sort_by { |e| -e.y }.each do |e|
          has_pushed_back = false
          other_enemies = geometry.find_all_intersect_rect e, state.enemies
          other_enemies.each do |e2|
            next if e == e2
            push_back_angle = geometry.angle geometry.center(e), geometry.center(e2)
            e2.push_back_x += (e.push_back_x).fdiv(other_enemies.length) * 0.7
            e2.push_back_y += (e.push_back_y).fdiv(other_enemies.length) * 0.7
            has_pushed_back = true
          end
    
          if has_pushed_back
            e.push_back_x *= 0.2
            e.push_back_y *= 0.2
          end
        end
      end
    
      def attack_angle
        geometry.angle state.turret_position, inputs.mouse
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    def reset
      $game = nil
    end
    
    $gtk.reset
    
    

    Bullet Hell - main.rb link

    # ./samples/99_genre_arcade/bullet_hell/app/main.rb
    def tick args
      args.state.base_columns   ||= 10.map { |n| 50 * n + 1280 / 2 - 5 * 50 + 5 }
      args.state.base_rows      ||= 5.map { |n| 50 * n + 720 - 5 * 50 }
      args.state.offset_columns = 10.map { |n| (n - 4.5) * Math.sin(Kernel.tick_count.to_radians) * 12 }
      args.state.offset_rows    = 5.map { 0 }
      args.state.columns        = 10.map { |i| args.state.base_columns[i] + args.state.offset_columns[i] }
      args.state.rows           = 5.map { |i| args.state.base_rows[i] + args.state.offset_rows[i] }
      args.state.explosions     ||= []
      args.state.enemies        ||= []
      args.state.score          ||= 0
      args.state.wave           ||= 0
      if args.state.enemies.empty?
        args.state.wave      += 1
        args.state.wave_root = Math.sqrt(args.state.wave)
        args.state.enemies   = make_enemies
      end
      args.state.player         ||= {x: 620, y: 80, w: 40, h: 40, path: 'sprites/circle-gray.png', angle: 90, cooldown: 0, alive: true}
      args.state.enemy_bullets  ||= []
      args.state.player_bullets ||= []
      args.state.lives          ||= 3
      args.state.missed_shots   ||= 0
      args.state.fired_shots    ||= 0
    
      update_explosions args
      update_enemy_positions args
    
      if args.inputs.left && args.state.player[:x] > (300 + 5)
        args.state.player[:x] -= 5
      end
      if args.inputs.right && args.state.player[:x] < (1280 - args.state.player[:w] - 300 - 5)
        args.state.player[:x] += 5
      end
    
      args.state.enemy_bullets.each do |bullet|
        bullet[:x] += bullet[:dx]
        bullet[:y] += bullet[:dy]
      end
      args.state.player_bullets.each do |bullet|
        bullet[:x] += bullet[:dx]
        bullet[:y] += bullet[:dy]
      end
    
      args.state.enemy_bullets  = args.state.enemy_bullets.find_all { |bullet| bullet[:y].between?(-16, 736) }
      args.state.player_bullets = args.state.player_bullets.find_all do |bullet|
        if bullet[:y].between?(-16, 736)
          true
        else
          args.state.missed_shots += 1
          false
        end
      end
    
      args.state.enemies = args.state.enemies.reject do |enemy|
        if args.state.player[:alive] && 1500 > (args.state.player[:x] - enemy[:x]) ** 2 + (args.state.player[:y] - enemy[:y]) ** 2
          args.state.explosions << {x: enemy[:x] + 4, y: enemy[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0}
          args.state.explosions << {x: args.state.player[:x] + 4, y: args.state.player[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0}
          args.state.player[:alive] = false
          true
        else
          false
        end
      end
      args.state.enemy_bullets.each do |bullet|
        if args.state.player[:alive] && 400 > (args.state.player[:x] - bullet[:x] + 12) ** 2 + (args.state.player[:y] - bullet[:y] + 12) ** 2
          args.state.explosions << {x: args.state.player[:x] + 4, y: args.state.player[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0}
          args.state.player[:alive] = false
          bullet[:despawn]          = true
        end
      end
      args.state.enemies = args.state.enemies.reject do |enemy|
        args.state.player_bullets.any? do |bullet|
          if 400 > (enemy[:x] - bullet[:x] + 12) ** 2 + (enemy[:y] - bullet[:y] + 12) ** 2
            args.state.explosions << {x: enemy[:x] + 4, y: enemy[:y] + 4, w: 32, h: 32, path: 'sprites/explosion-0.png', age: 0}
            bullet[:despawn] = true
            args.state.score += 1000 * args.state.wave
            true
          else
            false
          end
        end
      end
    
      args.state.player_bullets = args.state.player_bullets.reject { |bullet| bullet[:despawn] }
      args.state.enemy_bullets  = args.state.enemy_bullets.reject { |bullet| bullet[:despawn] }
    
      args.state.player[:cooldown] -= 1
      if args.inputs.keyboard.key_held.space && args.state.player[:cooldown] <= 0 && args.state.player[:alive]
        args.state.player_bullets << {x: args.state.player[:x] + 12, y: args.state.player[:y] + 28, w: 16, h: 16, path: 'sprites/star.png', dx: 0, dy: 8}.sprite
        args.state.fired_shots       += 1
        args.state.player[:cooldown] = 10 + 20 / args.state.wave
      end
      args.state.enemies.each do |enemy|
        if Math.rand < 0.0005 + 0.0005 * args.state.wave && args.state.player[:alive] && enemy[:move_state] == :normal
          args.state.enemy_bullets << {x: enemy[:x] + 12, y: enemy[:y] - 8, w: 16, h: 16, path: 'sprites/star.png', dx: 0, dy: -3 - args.state.wave_root}.sprite
        end
      end
    
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << args.state.enemies.map do |enemy|
        [enemy[:x], enemy[:y], 40, 40, enemy[:path], -90].sprite
      end
      args.outputs.primitives << args.state.player if args.state.player[:alive]
      args.outputs.primitives << args.state.explosions
      args.outputs.primitives << args.state.player_bullets
      args.outputs.primitives << args.state.enemy_bullets
      accuracy = args.state.fired_shots.zero? ? 1 : (args.state.fired_shots - args.state.missed_shots) / args.state.fired_shots
      args.outputs.primitives << [
        [0, 0, 300, 720, 96, 0, 0].solid,
        [1280 - 300, 0, 300, 720, 96, 0, 0].solid,
        [1280 - 290, 60, "Wave     #{args.state.wave}", 255, 255, 255].label,
        [1280 - 290, 40, "Accuracy #{(accuracy * 100).floor}%", 255, 255, 255].label,
        [1280 - 290, 20, "Score    #{(args.state.score * accuracy).floor}", 255, 255, 255].label,
      ]
      args.outputs.primitives << args.state.lives.map do |n|
        [1280 - 290 + 50 * n, 80, 40, 40, 'sprites/circle-gray.png', 90].sprite
      end
      #args.outputs.debug << args.gtk.framerate_diagnostics_primitives
    
      if (!args.state.player[:alive]) && args.state.enemy_bullets.empty? && args.state.explosions.empty? && args.state.enemies.all? { |enemy| enemy[:move_state] == :normal }
        args.state.player[:alive] = true
        args.state.player[:x]     = 624
        args.state.player[:y]     = 80
        args.state.lives          -= 1
        if args.state.lives == -1
          args.state.clear!
        end
      end
    end
    
    def make_enemies
      enemies = []
      enemies += 10.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 0, col: n, path: 'sprites/circle-orange.png', move_state: :retreat} }
      enemies += 10.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 1, col: n, path: 'sprites/circle-orange.png', move_state: :retreat} }
      enemies += 8.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 2, col: n + 1, path: 'sprites/circle-blue.png', move_state: :retreat} }
      enemies += 8.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 3, col: n + 1, path: 'sprites/circle-blue.png', move_state: :retreat} }
      enemies += 4.map { |n| {x: Math.rand * 1280 * 2 - 640, y: Math.rand * 720 * 2 + 720, row: 4, col: n + 3, path: 'sprites/circle-green.png', move_state: :retreat} }
      enemies
    end
    
    def update_explosions args
      args.state.explosions.each do |explosion|
        explosion[:age]  += 0.5
        explosion[:path] = "sprites/explosion-#{explosion[:age].floor}.png"
      end
      args.state.explosions = args.state.explosions.reject { |explosion| explosion[:age] >= 7 }
    end
    
    def update_enemy_positions args
      args.state.enemies.each do |enemy|
        if enemy[:move_state] == :normal
          enemy[:x]          = args.state.columns[enemy[:col]]
          enemy[:y]          = args.state.rows[enemy[:row]]
          enemy[:move_state] = :dive if Math.rand < 0.0002 + 0.00005 * args.state.wave && args.state.player[:alive]
        elsif enemy[:move_state] == :dive
          enemy[:target_x] ||= args.state.player[:x]
          enemy[:target_y] ||= args.state.player[:y]
          dx               = enemy[:target_x] - enemy[:x]
          dy               = enemy[:target_y] - enemy[:y]
          vel              = Math.sqrt(dx * dx + dy * dy)
          speed_limit      = 2 + args.state.wave_root
          if vel > speed_limit
            dx /= vel / speed_limit
            dy /= vel / speed_limit
          end
          if vel < 1 || !args.state.player[:alive]
            enemy[:move_state] = :retreat
          end
          enemy[:x] += dx
          enemy[:y] += dy
        elsif enemy[:move_state] == :retreat
          enemy[:target_x] = args.state.columns[enemy[:col]]
          enemy[:target_y] = args.state.rows[enemy[:row]]
          dx               = enemy[:target_x] - enemy[:x]
          dy               = enemy[:target_y] - enemy[:y]
          vel              = Math.sqrt(dx * dx + dy * dy)
          speed_limit      = 2 + args.state.wave_root
          if vel > speed_limit
            dx /= vel / speed_limit
            dy /= vel / speed_limit
          elsif vel < 1
            enemy[:move_state] = :normal
            enemy[:target_x]   = nil
            enemy[:target_y]   = nil
          end
          enemy[:x] += dx
          enemy[:y] += dy
        end
      end
    end
    
    

    Dueling Starships - main.rb link

    # ./samples/99_genre_arcade/dueling_starships/app/main.rb
    class DuelingSpaceships
      attr_gtk
    
      def tick
        defaults
        render
        calc
        input
      end
    
      def defaults
        outputs.background_color = [0, 0, 0]
        state.ship_blue       ||= new_blue_ship
        state.ship_red        ||= new_red_ship
        state.flames          ||= []
        state.bullets         ||= []
        state.ship_blue_score ||= 0
        state.ship_red_score  ||= 0
        state.stars           ||= 100.map do
          (rand + 2).yield_self do |size|
            { x: grid.w_half.randomize(:sign, :ratio),
              y: grid.h_half.randomize(:sign, :ratio),
              w: size,
              h: size,
              r: 128 + 128 * rand,
              g: 255,
              b: 255,
              path: :solid }
          end
        end
      end
    
      def new_ship x:, y:, angle:, path:, bullet_path:, color:;
        { x: x, y: y, w: 66, h: 66,
          dy: 0, dx: 0,
          anchor_x: 0.5, anchor_y: 0.5,
          damage: 0,
          dead: false,
          angle: angle,
          a: 255,
          path: path,
          bullet_sprite_path: bullet_path,
          color: color,
          created_at: Kernel.tick_count,
          last_bullet_at: 0,
          fire_rate: 10 }
      end
    
      def new_red_ship
        new_ship x: 400,
                 y: 250.randomize(:sign, :ratio),
                 angle: 180, path: 'sprites/ship_red.png',
                 bullet_path: 'sprites/red_bullet.png',
                 color: { r: 255, g: 90, b: 90 }
      end
    
      def new_blue_ship
        new_ship x: -400,
                 y: 250.randomize(:sign, :ratio),
                 angle: 0,
                 path: 'sprites/ship_blue.png',
                 bullet_path: 'sprites/blue_bullet.png',
                 color: { r: 110, g: 140, b: 255 }
      end
    
      def render
        render_instructions
        render_score
        render_universe
        render_flames
        render_ships
        render_bullets
      end
    
      def render_ships
        outputs.primitives << ship_prefab(state.ship_blue)
        outputs.primitives << ship_prefab(state.ship_red)
      end
    
      def render_instructions
        return if state.ship_blue.dx  > 0  || state.ship_blue.dy > 0  ||
                  state.ship_red.dx   > 0  || state.ship_red.dy  > 0  ||
                  state.flames.length > 0
    
        outputs.labels << { x: grid.left.shift_right(30),
                            y: grid.bottom.shift_up(30),
                            text: "Two gamepads needed to play. R1 to accelerate. Left and right on D-PAD to turn ship. Hold A to shoot. Press B to drop mines.",
                            r: 255, g: 255, b: 255 }
      end
    
      def calc
        calc_flames
        calc_ships
        calc_bullets
        calc_winner
      end
    
      def input
        input_accelerate
        input_turn
        input_bullets_and_mines
      end
    
      def render_score
        outputs.labels << { x: grid.left.shift_right(80),
                            y: grid.top.shift_down(40),
                            text: state.ship_blue_score,
                            size_enum: 30,
                            alignment_enum: 1, **state.ship_blue.color }
    
        outputs.labels << { x: grid.right.shift_left(80),
                            y: grid.top.shift_down(40),
                            text: state.ship_red_score,
                            size_enum: 30,
                            alignment_enum: 1, **state.ship_red.color }
      end
    
      def render_universe
        args.outputs.background_color = [0, 0, 0]
        outputs.sprites << state.stars
      end
    
      def apply_round_finished_alpha entity
        return entity unless state.round_finished_at
        entity.merge(a: (entity.a || 0) * state.round_finished_at.ease(2.seconds, :flip))
      end
    
      def ship_prefab ship
        [
          apply_round_finished_alpha(**ship,
                                     a: ship.dead ? 0 : 255 * ship.created_at.ease(2.seconds)),
    
          apply_round_finished_alpha(x: ship.x,
                                     y: ship.y + 100,
                                     text: "." * (5 - ship.damage.clamp(0, 5)),
                                     size_enum: 20,
                                     alignment_enum: 1,
                                     **ship.color)
        ]
      end
    
      def render_flames
        outputs.sprites << state.flames.map do |flame|
          apply_round_finished_alpha(flame.merge(a: 255 * flame.created_at.ease(flame.lifetime, :flip)))
        end
      end
    
      def render_bullets
        outputs.sprites << state.bullets.map do |b|
          apply_round_finished_alpha(b.merge(a: 255 * b.owner.created_at.ease(2.seconds)))
        end
      end
    
      def wrap_location! location
        location.merge! x: location.x.clamp_wrap(grid.left, grid.right),
                        y: location.y.clamp_wrap(grid.bottom, grid.top)
      end
    
      def calc_flames
        state.flames =
          state.flames
               .reject { |p| p.created_at.elapsed_time > p.lifetime }
               .map do |p|
                 p.speed *= 0.9
                 p.y += p.angle.vector_y(p.speed)
                 p.x += p.angle.vector_x(p.speed)
                 wrap_location! p
               end
      end
    
      def all_ships
        [state.ship_blue, state.ship_red]
      end
    
      def alive_ships
        all_ships.reject { |s| s.dead }
      end
    
      def calc_bullet bullet
        bullet.y += bullet.angle.vector_y(bullet.speed)
        bullet.x += bullet.angle.vector_x(bullet.speed)
        wrap_location! bullet
        explode_bullet! bullet, particle_count: 5 if bullet.created_at.elapsed_time > bullet.lifetime
        return if bullet.exploded
        return if state.round_finished
        alive_ships.each do |s|
          if s != bullet.owner && s.intersect_rect?(bullet)
            explode_bullet! bullet, particle_count: 10
            s.damage += 1
          end
        end
      end
    
      def calc_bullets
        state.bullets.each    { |b| calc_bullet b }
        state.bullets.reject! { |b| b.exploded }
      end
    
      def new_flame x:, y:, angle:, a:, lifetime:, speed:;
        { angle: angle,
          speed: speed,
          lifetime: lifetime,
          path: 'sprites/flame.png',
          x: x,
          y: y,
          w: 6,
          h: 6,
          anchor_x: 0.5,
          anchor_y: 0.5,
          created_at: Kernel.tick_count,
          a: a }
      end
    
      def create_explosion! source:, particle_count:, max_speed:, lifetime:;
        state.flames.concat(particle_count.map do
                              new_flame x: source.x,
                                        y: source.y,
                                        speed: max_speed * rand,
                                        angle: 360 * rand,
                                        lifetime: lifetime,
                                        a: source.a
                            end)
      end
    
      def explode_bullet! bullet, particle_count: 5
        bullet.exploded = true
        create_explosion! source: bullet,
                          particle_count: particle_count,
                          max_speed: 5,
                          lifetime: 10
      end
    
      def calc_ship ship
        ship.x += ship.dx
        ship.y += ship.dy
        wrap_location! ship
      end
    
      def calc_ships
        all_ships.each { |s| calc_ship s }
        return if all_ships.any? { |s| s.dead }
        return if state.round_finished
        return unless state.ship_blue.intersect_rect?(state.ship_red)
        state.ship_blue.damage = 5
        state.ship_red.damage  = 5
      end
    
      def create_thruster_flames! ship
        state.flames << new_flame(x: ship.x - ship.angle.vector_x(40) + 5.randomize(:sign, :ratio),
                                  y: ship.y - ship.angle.vector_y(40) + 5.randomize(:sign, :ratio),
                                  angle: ship.angle + 180 + 60.randomize(:sign, :ratio),
                                  speed: 5.randomize(:ratio),
                                  a: 255 * ship.created_at.elapsed_time.ease(2.seconds),
                                  lifetime: 30)
      end
    
      def input_accelerate_ship should_move_ship, ship
        return if ship.dead
    
        should_move_ship &&= (ship.dx + ship.dy).abs < 5
    
        if should_move_ship
          create_thruster_flames! ship
          ship.dx += ship.angle.vector_x 0.050
          ship.dy += ship.angle.vector_y 0.050
        else
          ship.dx *= 0.99
          ship.dy *= 0.99
        end
      end
    
      def input_accelerate
        input_accelerate_ship inputs.controller_one.key_held.r1 || inputs.keyboard.up, state.ship_blue
        input_accelerate_ship inputs.controller_two.key_held.r1, state.ship_red
      end
    
      def input_turn_ship direction, ship
        ship.angle -= 3 * direction
      end
    
      def input_turn
        input_turn_ship inputs.controller_one.left_right + inputs.keyboard.left_right, state.ship_blue
        input_turn_ship inputs.controller_two.left_right, state.ship_red
      end
    
      def new_bullet x:, y:, ship:, angle:, speed:, lifetime:;
        { owner: ship,
          angle: angle,
          speed: speed,
          lifetime: lifetime,
          created_at: Kernel.tick_count,
          path: ship.bullet_sprite_path,
          anchor_x: 0.5,
          anchor_y: 0.5,
          w: 10,
          h: 10,
          x: x,
          y: y }
      end
    
      def input_bullet create_bullet, ship
        return unless create_bullet
        return if ship.dead
        return if ship.last_bullet_at.elapsed_time < ship.fire_rate
    
        ship.last_bullet_at = Kernel.tick_count
    
        state.bullets << new_bullet(x: ship.x + ship.angle.vector_x * 32,
                                    y: ship.y + ship.angle.vector_y * 32,
                                    ship: ship,
                                    angle: ship.angle,
                                    speed: 5 + ship.dx * ship.angle.vector_x + ship.dy * ship.angle.vector_y,
                                    lifetime: 120)
      end
    
      def input_mine create_mine, ship
        return unless create_mine
        return if ship.dead
    
        state.bullets << new_bullet(x: ship.x + ship.angle.vector_x * -50,
                                    y: ship.y + ship.angle.vector_y * -50,
                                    ship: ship,
                                    angle: 360.randomize(:sign, :ratio),
                                    speed: 0.02,
                                    lifetime: 600)
      end
    
      def input_bullets_and_mines
        return if state.bullets.length > 100
    
        input_bullet(inputs.controller_one.key_held.a || inputs.keyboard.key_held.space,
                     state.ship_blue)
    
        input_mine(inputs.controller_one.key_down.b || inputs.keyboard.key_down.down,
                   state.ship_blue)
    
        input_bullet(inputs.controller_two.key_held.a, state.ship_red)
    
        input_mine(inputs.controller_two.key_down.b, state.ship_red)
      end
    
      def calc_kill_ships
        alive_ships.find_all { |s| s.damage >= 5 }
                   .each do |s|
                     s.dead = true
                     create_explosion! source: s,
                                       particle_count: 20,
                                       max_speed: 20,
                                       lifetime: 30
                   end
      end
    
      def calc_score
        return if state.round_finished
        return if alive_ships.length > 1
    
        if alive_ships.first == state.ship_red
          state.ship_red_score += 1
        elsif alive_ships.first == state.ship_blue
          state.ship_blue_score += 1
        end
    
        state.round_finished = true
      end
    
      def calc_reset_ships
        return unless state.round_finished
        state.round_finished_at ||= Kernel.tick_count
        return if state.round_finished_at.elapsed_time <= 2.seconds
        start_new_round!
      end
    
      def start_new_round!
        state.ship_blue = new_blue_ship
        state.ship_red  = new_red_ship
        state.round_finished = false
        state.round_finished_at = nil
        state.flames.clear
        state.bullets.clear
      end
    
      def calc_winner
        calc_kill_ships
        calc_score
        calc_reset_ships
      end
    end
    
    $dueling_spaceship = DuelingSpaceships.new
    
    def tick args
      args.grid.origin_center!
      $dueling_spaceship.args = args
      $dueling_spaceship.tick
    end
    
    

    arcade/flappy dragon/credits.txt link

    # ./samples/99_genre_arcade/flappy_dragon/CREDITS.txt
    code: Amir Rajan, https://twitter.com/amirrajan
    graphics and audio: Nick Culbertson, https://twitter.com/MobyPixel
    
    
    

    arcade/flappy dragon/main.rb link

    # ./samples/99_genre_arcade/flappy_dragon/app/main.rb
    class FlappyDragon
      attr_accessor :grid, :inputs, :state, :outputs
    
      def tick
        defaults
        render
        calc
        process_inputs
      end
    
      def defaults
        state.flap_power              = 11
        state.gravity                 = 0.9
        state.ceiling                 = 600
        state.ceiling_flap_power      = 6
        state.wall_countdown_length   = 100
        state.wall_gap_size           = 100
        state.wall_countdown        ||= 0
        state.hi_score              ||= 0
        state.score                 ||= 0
        state.walls                 ||= []
        state.x                     ||= 50
        state.y                     ||= 500
        state.dy                    ||= 0
        state.scene                 ||= :menu
        state.scene_at              ||= 0
        state.difficulty            ||= :normal
        state.new_difficulty        ||= :normal
        state.countdown             ||= 4.seconds
        state.flash_at              ||= 0
      end
    
      def render
        outputs.sounds << "sounds/flappy-song.ogg" if Kernel.tick_count == 1
        render_score
        render_menu
        render_game
      end
    
      def render_score
        outputs.primitives << { x: 10, y: 710, text: "HI SCORE: #{state.hi_score}", **large_white_typeset }
        outputs.primitives << { x: 10, y: 680, text: "SCORE: #{state.score}", **large_white_typeset }
        outputs.primitives << { x: 10, y: 650, text: "DIFFICULTY: #{state.difficulty.upcase}", **large_white_typeset }
      end
    
      def render_menu
        return unless state.scene == :menu
        render_overlay
    
        outputs.labels << { x: 640, y: 700, text: "Flappy Dragon", size_enum: 50, alignment_enum: 1, **white }
        outputs.labels << { x: 640, y: 500, text: "Instructions: Press Spacebar to flap. Don't die.", size_enum: 4, alignment_enum: 1, **white }
        outputs.labels << { x: 430, y: 430, text: "[Tab]    Change difficulty", size_enum: 4, alignment_enum: 0, **white }
        outputs.labels << { x: 430, y: 400, text: "[Enter]  Start at New Difficulty ", size_enum: 4, alignment_enum: 0, **white }
        outputs.labels << { x: 430, y: 370, text: "[Escape] Cancel/Resume ", size_enum: 4, alignment_enum: 0, **white }
        outputs.labels << { x: 640, y: 300, text: "(mouse, touch, and game controllers work, too!) ", size_enum: 4, alignment_enum: 1, **white }
        outputs.labels << { x: 640, y: 200, text: "Difficulty: #{state.new_difficulty.capitalize}", size_enum: 4, alignment_enum: 1, **white }
    
        outputs.labels << { x: 10, y: 100, text: "Code:   @amirrajan",     **white }
        outputs.labels << { x: 10, y:  80, text: "Art:    @mobypixel",     **white }
        outputs.labels << { x: 10, y:  60, text: "Music:  @mobypixel",     **white }
        outputs.labels << { x: 10, y:  40, text: "Engine: DragonRuby GTK", **white }
      end
    
      def render_overlay
        overlay_rect = grid.rect.scale_rect(1.1, 0, 0)
        outputs.primitives << { x: overlay_rect.x,
                                y: overlay_rect.y,
                                w: overlay_rect.w,
                                h: overlay_rect.h,
                                r: 0, g: 0, b: 0, a: 230 }.solid!
      end
    
      def render_game
        render_game_over
        render_background
        render_walls
        render_dragon
        render_flash
      end
    
      def render_game_over
        return unless state.scene == :game
        outputs.labels << { x: 638, y: 358, text: score_text,     size_enum: 20, alignment_enum: 1 }
        outputs.labels << { x: 635, y: 360, text: score_text,     size_enum: 20, alignment_enum: 1, r: 255, g: 255, b: 255 }
        outputs.labels << { x: 638, y: 428, text: countdown_text, size_enum: 20, alignment_enum: 1 }
        outputs.labels << { x: 635, y: 430, text: countdown_text, size_enum: 20, alignment_enum: 1, r: 255, g: 255, b: 255 }
      end
    
      def render_background
        outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: 'sprites/background.png' }
    
        scroll_point_at   = Kernel.tick_count
        scroll_point_at   = state.scene_at if state.scene == :menu
        scroll_point_at   = state.death_at if state.countdown > 0
        scroll_point_at ||= 0
    
        outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_back.png',   0.25)
        outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_middle.png', 0.50)
        outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_front.png',  1.00, -80)
      end
    
      def scrolling_background at, path, rate, y = 0
        [
          { x:    0 - at.*(rate) % 1440, y: y, w: 1440, h: 720, path: path },
          { x: 1440 - at.*(rate) % 1440, y: y, w: 1440, h: 720, path: path }
        ]
      end
    
      def render_walls
        state.walls.each do |w|
          w.sprites = [
            { x: w.x, y: w.bottom_height - 720, w: 100, h: 720, path: 'sprites/wall.png',       angle: 180 },
            { x: w.x, y: w.top_y,               w: 100, h: 720, path: 'sprites/wallbottom.png', angle: 0 }
          ]
        end
        outputs.sprites << state.walls.map(&:sprites)
      end
    
      def render_dragon
        state.show_death = true if state.countdown == 3.seconds
    
        if state.show_death == false || !state.death_at
          animation_index = state.flapped_at.frame_index 6, 2, false if state.flapped_at
          sprite_name = "sprites/dragon_fly#{(animation_index || 0) + 1}.png"
          state.dragon_sprite = { x: state.x, y: state.y, w: 100, h: 80, path: sprite_name, angle: state.dy * 1.2 }
        else
          sprite_name = "sprites/dragon_die.png"
          state.dragon_sprite = { x: state.x, y: state.y, w: 100, h: 80, path: sprite_name, angle: state.dy * 1.2 }
          sprite_changed_elapsed    = state.death_at.elapsed_time - 1.seconds
          state.dragon_sprite.angle += (sprite_changed_elapsed ** 1.3) * state.death_fall_direction * -1
          state.dragon_sprite.x     += (sprite_changed_elapsed ** 1.2) * state.death_fall_direction
          state.dragon_sprite.y     += (sprite_changed_elapsed * 14 - sprite_changed_elapsed ** 1.6)
        end
    
        outputs.sprites << state.dragon_sprite
      end
    
      def render_flash
        return unless state.flash_at
    
        outputs.primitives << { **grid.rect.to_hash,
                                **white,
                                a: 255 * state.flash_at.ease(20, :flip) }.solid!
    
        state.flash_at = 0 if state.flash_at.elapsed_time > 20
      end
    
      def calc
        return unless state.scene == :game
        reset_game if state.countdown == 1
        state.countdown -= 1 and return if state.countdown > 0
        calc_walls
        calc_flap
        calc_game_over
      end
    
      def calc_walls
        state.walls.each { |w| w.x -= 8 }
    
        walls_count_before_removal = state.walls.length
    
        state.walls.reject! { |w| w.x < -100 }
    
        state.score += 1 if state.walls.count < walls_count_before_removal
    
        state.wall_countdown -= 1 and return if state.wall_countdown > 0
    
        state.walls << state.new_entity(:wall) do |w|
          w.x             = grid.right
          w.opening       = grid.top
                                .randomize(:ratio)
                                .greater(200)
                                .lesser(520)
          w.bottom_height = w.opening - state.wall_gap_size
          w.top_y         = w.opening + state.wall_gap_size
        end
    
        state.wall_countdown = state.wall_countdown_length
      end
    
      def calc_flap
        state.y += state.dy
        state.dy = state.dy.lesser state.flap_power
        state.dy -= state.gravity
        return if state.y < state.ceiling
        state.y  = state.ceiling
        state.dy = state.dy.lesser state.ceiling_flap_power
      end
    
      def calc_game_over
        return unless game_over?
    
        state.death_at = Kernel.tick_count
        state.death_from = state.walls.first
        state.death_fall_direction = -1
        state.death_fall_direction =  1 if state.x > state.death_from.x
        outputs.sounds << "sounds/hit-sound.wav"
        begin_countdown
      end
    
      def process_inputs
        process_inputs_menu
        process_inputs_game
      end
    
      def process_inputs_menu
        return unless state.scene == :menu
    
        changediff = inputs.keyboard.key_down.tab || inputs.controller_one.key_down.select
        if inputs.mouse.click
          p = inputs.mouse.click.point
          if (p.y >= 165) && (p.y < 200) && (p.x >= 500) && (p.x < 800)
            changediff = true
          end
        end
    
        if changediff
          case state.new_difficulty
          when :easy
            state.new_difficulty = :normal
          when :normal
            state.new_difficulty = :hard
          when :hard
            state.new_difficulty = :flappy
          when :flappy
            state.new_difficulty = :easy
          end
        end
    
        if inputs.keyboard.key_down.enter || inputs.controller_one.key_down.start || inputs.controller_one.key_down.a
          state.difficulty = state.new_difficulty
          change_to_scene :game
          reset_game false
          state.hi_score = 0
          begin_countdown
        end
    
        if inputs.keyboard.key_down.escape || (inputs.mouse.click && !changediff) || inputs.controller_one.key_down.b
          state.new_difficulty = state.difficulty
          change_to_scene :game
        end
      end
    
      def process_inputs_game
        return unless state.scene == :game
    
        clicked_menu = false
        if inputs.mouse.click
          p = inputs.mouse.click.point
          clicked_menu = (p.y >= 620) && (p.x < 275)
        end
    
        if clicked_menu || inputs.keyboard.key_down.escape || inputs.keyboard.key_down.enter || inputs.controller_one.key_down.start
          change_to_scene :menu
        elsif (inputs.mouse.down || inputs.mouse.click || inputs.keyboard.key_down.space || inputs.controller_one.key_down.a) && state.countdown == 0
          state.dy = 0
          state.dy += state.flap_power
          state.flapped_at = Kernel.tick_count
          outputs.sounds << "sounds/fly-sound.wav"
        end
      end
    
      def white
        { r: 255, g: 255, b: 255 }
      end
    
      def large_white_typeset
        { size_enum: 5, alignment_enum: 0, r: 255, g: 255, b: 255 }
      end
    
      def at_beginning?
        state.walls.count == 0
      end
    
      def dragon_collision_box
        state.dragon_sprite
             .scale_rect(1.0 - collision_forgiveness, 0.5, 0.5)
             .rect_shift_right(10)
             .rect_shift_up(state.dy * 2)
      end
    
      def game_over?
        return true if state.y <= 0.-(500 * collision_forgiveness) && !at_beginning?
    
        state.walls
            .flat_map { |w| w.sprites }
            .any? do |s|
              s && s.intersect_rect?(dragon_collision_box)
            end
      end
    
      def collision_forgiveness
        case state.difficulty
        when :easy
          0.9
        when :normal
          0.7
        when :hard
          0.5
        when :flappy
          0.3
        else
          0.9
        end
      end
    
      def countdown_text
        state.countdown ||= -1
        return ""          if state.countdown == 0
        return "GO!"       if state.countdown.idiv(60) == 0
        return "GAME OVER" if state.death_at
        return "READY?"
      end
    
      def begin_countdown
        state.countdown = 4.seconds
      end
    
      def score_text
        return ""                        unless state.countdown > 1.seconds
        return ""                        unless state.death_at
        return "SCORE: 0 (LOL)"          if state.score == 0
        return "HI SCORE: #{state.score}" if state.score == state.hi_score
        return "SCORE: #{state.score}"
      end
    
      def reset_game set_flash = true
        state.flash_at = Kernel.tick_count if set_flash
        state.walls = []
        state.y = 500
        state.dy = 0
        state.hi_score = state.hi_score.greater(state.score)
        state.score = 0
        state.wall_countdown = state.wall_countdown_length.fdiv(2)
        state.show_death = false
        state.death_at = nil
      end
    
      def change_to_scene scene
        state.scene = scene
        state.scene_at = Kernel.tick_count
        inputs.keyboard.clear
        inputs.controller_one.clear
      end
    end
    
    $flappy_dragon = FlappyDragon.new
    
    def tick args
      $flappy_dragon.grid = args.grid
      $flappy_dragon.inputs = args.inputs
      $flappy_dragon.state = args.state
      $flappy_dragon.outputs = args.outputs
      $flappy_dragon.tick
    end
    
    

    Pong - main.rb link

    # ./samples/99_genre_arcade/pong/app/main.rb
    def tick args
      defaults args
      render args
      calc args
      input args
    end
    
    def defaults args
      args.state.ball ||= {
        debounce: 3 * 60,
        size: 10,
        size_half: 5,
        x: 640,
        y: 360,
        dx: 5.randomize(:sign),
        dy: 5.randomize(:sign)
      }
    
      args.state.paddle ||= {
        w: 10,
        h: 120
      }
    
      args.state.left_paddle  ||= { y: 360, score: 0 }
      args.state.right_paddle ||= { y: 360, score: 0 }
    end
    
    def render args
      render_center_line args
      render_scores args
      render_countdown args
      render_ball args
      render_paddles args
      render_instructions args
    end
    
    begin :render_methods
      def render_center_line args
        args.outputs.lines  << [640, 0, 640, 720]
      end
    
      def render_scores args
        args.outputs.labels << [
          { x: 320,
            y: 650,
            text: args.state.left_paddle.score,
            size_px: 40,
            anchor_x: 0.5,
            anchor_y: 0.5 },
          { x: 960,
            y: 650,
            text: args.state.right_paddle.score,
            size_px: 40,
            anchor_x: 0.5,
            anchor_y: 0.5 }
        ]
      end
    
      def render_countdown args
        return unless args.state.ball.debounce > 0
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: "%.2f" % args.state.ball.debounce.fdiv(60),
                                 size_px: 40,
                                 anchor_x: 0.5,
                                 anchor_y: 0.5 }
      end
    
      def render_ball args
        args.outputs.solids << solid_ball(args)
      end
    
      def render_paddles args
        args.outputs.solids << solid_left_paddle(args)
        args.outputs.solids << solid_right_paddle(args)
      end
    
      def render_instructions args
        args.outputs.labels << { x: 320,
                                 y: 30,
                                 text: "W and S keys to move left paddle.",
                                 anchor_x: 0.5,
                                 anchor_y: 0.5 }
        args.outputs.labels << { x: 920,
                                 y: 30,
                                 text: "O and L keys to move right paddle.",
                                 anchor_x: 0.5,
                                 anchor_y: 0.5 }
      end
    end
    
    def calc args
      args.state.ball.debounce -= 1 and return if args.state.ball.debounce > 0
      calc_move_ball args
      calc_collision_with_left_paddle args
      calc_collision_with_right_paddle args
      calc_collision_with_walls args
    end
    
    begin :calc_methods
      def calc_move_ball args
        args.state.ball.x += args.state.ball.dx
        args.state.ball.y += args.state.ball.dy
      end
    
      def calc_collision_with_left_paddle args
        if solid_left_paddle(args).intersect_rect? solid_ball(args)
          args.state.ball.dx *= -1
        elsif args.state.ball.x < 0
          args.state.right_paddle.score += 1
          calc_reset_round args
        end
      end
    
      def calc_collision_with_right_paddle args
        if solid_right_paddle(args).intersect_rect? solid_ball(args)
          args.state.ball.dx *= -1
        elsif args.state.ball.x > 1280
          args.state.left_paddle.score += 1
          calc_reset_round args
        end
      end
    
      def calc_collision_with_walls args
        if args.state.ball.y + args.state.ball.size_half > 720
          args.state.ball.y = 720 - args.state.ball.size_half
          args.state.ball.dy *= -1
        elsif args.state.ball.y - args.state.ball.size_half < 0
          args.state.ball.y = args.state.ball.size_half
          args.state.ball.dy *= -1
        end
      end
    
      def calc_reset_round args
        args.state.ball.x = 640
        args.state.ball.y = 360
        args.state.ball.dx = 5.randomize(:sign)
        args.state.ball.dy = 5.randomize(:sign)
        args.state.ball.debounce = 3 * 60
      end
    end
    
    def input args
      input_left_paddle args
      input_right_paddle args
    end
    
    def input_left_paddle args
      if args.inputs.controller_one.key_down.down  || args.inputs.keyboard.key_down.s
        args.state.left_paddle.y -= 40
      elsif args.inputs.controller_one.key_down.up || args.inputs.keyboard.key_down.w
        args.state.left_paddle.y += 40
      end
    end
    
    def input_right_paddle args
      if args.inputs.controller_two.key_down.down  || args.inputs.keyboard.key_down.l
        args.state.right_paddle.y -= 40
      elsif args.inputs.controller_two.key_down.up || args.inputs.keyboard.key_down.o
        args.state.right_paddle.y += 40
      end
    end
    
    def solid_ball args
      { x: args.state.ball.x,
        y: args.state.ball.y,
        w: args.state.ball.size,
        h: args.state.ball.size,
        anchor_x: 0.5,
        anchor_y: 0.5 }
    end
    
    def solid_left_paddle args
      { x: 0,
        y: args.state.left_paddle.y,
        w: args.state.paddle.w,
        h: args.state.paddle.h,
        anchor_y: 0.5 }
    end
    
    def solid_right_paddle args
      { x: 1280 - args.state.paddle.w,
        y: args.state.right_paddle.y,
        w: args.state.paddle.w,
        h: args.state.paddle.h,
        anchor_y: 0.5 }
    end
    
    

    Snakemoji - main.rb link

    # ./samples/99_genre_arcade/snakemoji/app/main.rb
    # coding: utf-8
    ################################
    #  So I was working on a snake game while
    #  learning DragonRuby, and at some point I had a thought
    #  what if I use "😀" as a function name, surely it wont work right...?
    #  RIGHT....?
    #  BUT IT DID, IT WORKED
    #  it all went downhill from then
    #  Created by Anton K. (ai Doge)
    #  https://gist.github.com/scorp200
    #############LICENSE############
    #  Feel free to use this anywhere and however you want
    #  You can sell this to EA for $1,000,000 if you want, its completely free.
    #  Just rememeber you are helping this... thing... to spread...
    #  ALSO! I am not liable for any mental, physical or financial damage caused.
    #############LICENSE############
    
    
    class Array
      #Helper function
      def move! vector
        self.x += vector.x
        self.y += vector.y
        return self
      end
    
      #Helper function to draw snake body
      def draw! 🎮, 📺, color
        translate 📺.solids, 🎮.⛓, [self.x * 🎮.⚖️ + 🎮.🛶 / 2, self.y * 🎮.⚖️ + 🎮.🛶 / 2, 🎮.⚖️ - 🎮.🛶, 🎮.⚖️ - 🎮.🛶, color]
      end
    
      #This is where it all started, I was trying to find  good way to multiply a map by a number, * is already used so is **
      #I kept trying different combinations of symbols, when suddenly...
      def 😀 value
        self.map {|d| d * value}
      end
    end
    
    #Draw stuff with an offset
    def translate output_collection, ⛓, what
      what.x += ⛓.x
      what.y += ⛓.y
      output_collection << what
    end
    
    BLUE = [33, 150, 243]
    RED = [244, 67, 54]
    GOLD = [255, 193, 7]
    LAST = 0
    
    def tick args
      defaults args.state
      render args.state, args.outputs
      input args.state, args.inputs
      update args.state
    end
    
    def update 🎮
      #Update every 10 frames
      if 🎮.tick_count.mod_zero? 10
        #Add new snake body piece at head's location
        🎮.🐍 << [*🎮.🤖]
        #Assign Next Direction to Direction
        🎮.🚗 = *🎮.🚦
    
        #Trim the snake a bit if its longer than current size
        if 🎮.🐍.length > 🎮.🛒
          🎮.🐍 = 🎮.🐍[-🎮.🛒..-1]
        end
    
        #Move the head in the Direction
        🎮.🤖.move! 🎮.🚗
    
        #If Head is outside the playing field, or inside snake's body restart game
        if 🎮.🤖.x < 0 || 🎮.🤖.x >= 🎮.🗺.x || 🎮.🤖.y < 0 || 🎮.🤖.y >= 🎮.🗺.y || 🎮.🚗 != [0, 0] && 🎮.🐍.any? {|s| s == 🎮.🤖}
          LAST = 🎮.💰
          🎮.as_hash.clear
          return
        end
    
        #If head lands on food add size and score
        if 🎮.🤖 == 🎮.🍎
          🎮.🛒 += 1
          🎮.💰 += (🎮.🛒 * 0.8).floor.to_i + 5
          spawn_🍎 🎮
          puts 🎮.🍎
        end
      end
    
      #Every second remove 1 point
      if 🎮.💰 > 0 && 🎮.tick_count.mod_zero?(60)
        🎮.💰 -= 1
      end
    end
    
    def spawn_🍎 🎮
      #Food
      🎮.🍎 ||= [*🎮.🤖]
      #Randomly spawns food inside the playing field, keep doing this if the food keeps landing on the snake's body
      while 🎮.🐍.any? {|s| s == 🎮.🍎} || 🎮.🍎 == 🎮.🤖 do
        🎮.🍎 = [rand(🎮.🗺.x), rand(🎮.🗺.y)]
      end
    end
    
    def render 🎮, 📺
      #Paint the background black
      📺.solids << [0, 0, 1280, 720, 0, 0, 0, 255]
      #Draw a border for the playing field
      translate 📺.borders, 🎮.⛓, [0, 0, 🎮.🗺.x * 🎮.⚖️, 🎮.🗺.y * 🎮.⚖️, 255, 255, 255]
    
      #Draw the snake's body
      🎮.🐍.map do |🐍| 🐍.draw! 🎮, 📺, BLUE end
      #Draw the head
      🎮.🤖.draw! 🎮, 📺, BLUE
      #Draw the food
      🎮.🍎.draw! 🎮, 📺, RED
    
      #Draw current score
      translate 📺.labels, 🎮.⛓, [5, 715, "Score: #{🎮.💰}", GOLD]
      #Draw your last score, if any
      translate 📺.labels, 🎮.⛓, [[*🎮.🤖.😀(🎮.⚖️)].move!([0, 🎮.⚖️ * 2]), "Your Last score is #{LAST}", 0, 1, GOLD] unless LAST == 0 || 🎮.🚗 != [0, 0]
      #Draw starting message, only if Direction is 0
      translate 📺.labels, 🎮.⛓, [🎮.🤖.😀(🎮.⚖️), "Press any Arrow key to start", 0, 1, GOLD] unless 🎮.🚗 != [0, 0]
    end
    
    def input 🎮, 🕹
      #Left and Right keyboard input, only change if X direction is 0
      if 🕹.keyboard.key_held.left && 🎮.🚗.x == 0
        🎮.🚦 = [-1, 0]
      elsif 🕹.keyboard.key_held.right && 🎮.🚗.x == 0
        🎮.🚦 = [1, 0]
      end
    
      #Up and Down keyboard input, only change if Y direction is 0
      if 🕹.keyboard.key_held.up && 🎮.🚗.y == 0
        🎮.🚦 = [0, 1]
      elsif 🕹.keyboard.key_held.down && 🎮.🚗.y == 0
        🎮.🚦 = [0, -1]
      end
    end
    
    def defaults 🎮
      #Playing field size
      🎮.🗺 ||= [20, 20]
      #Scale for drawing, screen height / Field height
      🎮.⚖️ ||= 720 / 🎮.🗺.y
      #Offset, offset all rendering to the center of the screen
      🎮.⛓ ||= [(1280 - 720).fdiv(2), 0]
      #Padding, make the snake body slightly smaller than the scale
      🎮.🛶 ||= (🎮.⚖️ * 0.2).to_i
      #Snake Size
      🎮.🛒 ||= 3
      #Snake head, the only part we are actually controlling
      🎮.🤖 ||= [🎮.🗺.x / 2, 🎮.🗺.y / 2]
      #Snake body map, follows the head
      🎮.🐍 ||= []
      #Direction the head moves to
      🎮.🚗 ||= [0, 0]
      #Next_Direction, during input check only change this variable and then when game updates asign this to Direction
      🎮.🚦 ||= [*🎮.🚗]
      #Your score
      🎮.💰 ||= 0
      #Spawns Food randomly
      spawn_🍎(🎮) unless 🎮.🍎
    end
    
    

    Solar System - main.rb link

    # ./samples/99_genre_arcade/solar_system/app/main.rb
    # Focused tutorial video: https://s3.amazonaws.com/s3.dragonruby.org/dragonruby-nddnug-workshop.mp4
    # Workshop/Presentation which provides motivation for creating a game engine: https://www.youtube.com/watch?v=S3CFce1arC8
    
    def defaults args
      args.outputs.background_color = [0, 0, 0]
      args.state.x ||= 640
      args.state.y ||= 360
      args.state.stars ||= 100.map do
        [1280 * rand, 720 * rand, rand.fdiv(10), 255 * rand, 255 * rand, 255 * rand]
      end
    
      args.state.sun ||= args.state.new_entity(:sun) do |s|
        s.s = 100
        s.path = 'sprites/sun.png'
      end
    
      args.state.planets = [
        [:mercury,   65,  5,          88],
        [:venus,    100, 10,         225],
        [:earth,    120, 10,         365],
        [:mars,     140,  8,         687],
        [:jupiter,  280, 30, 365 *  11.8],
        [:saturn,   350, 20, 365 *  29.5],
        [:uranus,   400, 15, 365 *    84],
        [:neptune,  440, 15, 365 * 164.8],
        [:pluto,    480,  5, 365 * 247.8],
      ].map do |name, distance, size, year_in_days|
        args.state.new_entity(name) do |p|
          p.path = "sprites/#{name}.png"
          p.distance = distance * 0.7
          p.s = size * 0.7
          p.year_in_days = year_in_days
        end
      end
    
      args.state.ship ||= args.state.new_entity(:ship) do |s|
        s.x = 1280 * rand
        s.y = 720 * rand
        s.angle = 0
      end
    end
    
    def to_sprite args, entity
      x = 0
      y = 0
    
      if entity.year_in_days
        day = Kernel.tick_count
        day_in_year = day % entity.year_in_days
        entity.random_start_day ||= day_in_year * rand
        percentage_of_year = day_in_year.fdiv(entity.year_in_days)
        angle = 365 * percentage_of_year
        x = angle.vector_x(entity.distance)
        y = angle.vector_y(entity.distance)
      end
    
      [640 + x - entity.s.half, 360 + y - entity.s.half, entity.s, entity.s, entity.path]
    end
    
    def render args
      args.outputs.solids << [0, 0, 1280, 720]
    
      args.outputs.sprites << args.state.stars.map do |x, y, _, r, g, b|
        [x, y, 10, 10, 'sprites/star.png', 0, 100, r, g, b]
      end
    
      args.outputs.sprites << to_sprite(args, args.state.sun)
      args.outputs.sprites << args.state.planets.map { |p| to_sprite args, p }
      args.outputs.sprites << [args.state.ship.x, args.state.ship.y, 20, 20, 'sprites/ship.png', args.state.ship.angle]
    end
    
    def calc args
      args.state.stars = args.state.stars.map do |x, y, speed, r, g, b|
        x += speed
        y += speed
        x = 0 if x > 1280
        y = 0 if y > 720
        [x, y, speed, r, g, b]
      end
    
      if Kernel.tick_count == 0
        args.audio[:bg_music] = {
          input: 'sounds/bg.ogg',
          looping: true
        }
      end
    end
    
    def process_inputs args
      if args.inputs.keyboard.left || args.inputs.controller_one.key_held.left
        args.state.ship.angle += 1
      elsif args.inputs.keyboard.right || args.inputs.controller_one.key_held.right
        args.state.ship.angle -= 1
      end
    
      if args.inputs.keyboard.up || args.inputs.controller_one.key_held.a
        args.state.ship.x += args.state.ship.angle.x_vector
        args.state.ship.y += args.state.ship.angle.y_vector
      end
    end
    
    def tick args
      defaults args
      render args
      calc args
      process_inputs args
    end
    
    def r
      $gtk.reset
    end
    
    

    Sound Golf - main.rb link

    # ./samples/99_genre_arcade/sound_golf/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:
     - 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).
    
     - 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.
    
     - 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.
    
     - find_all: Finds all elements from a collection that meet a certain requirements (and excludes the ones that don't).
    
     - first: Returns the first element of an array.
    
     - inside_rect: Returns true or false depending on if the point is inside the rect.
    
     - to_sym: Returns symbol corresponding to string. Will create a symbol if it does
       not already exist.
    
    =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
      defaults args
      render args
      calc args
      input_mouse args
      tick_instructions args, "Sample app shows how to play sounds. args.outputs.sounds << \"path_to_wav.wav\""
    end
    
    # Sets default values and creates empty collections
    # Initialization happens in the first frame only
    def defaults args
      args.state.notes ||= []
      args.state.click_feedbacks ||= []
      args.state.current_level ||= 1
      args.state.times_wrong ||= 0 # when game starts, user hasn't guessed wrong yet
    end
    
    # Uses a label to display current level, and shows the score
    # Creates a button to play the sample note, and displays the available notes that could be a potential match
    def render args
    
      # grid.w_half positions the label in the horizontal center of the screen.
      args.outputs.labels << [args.grid.w_half, args.grid.top.shift_down(40), "Hole #{args.state.current_level} of 9", 0, 1, 0, 0, 0]
    
      render_score args # shows score on screen
    
      args.state.play_again_button ||= { x: 560, y: args.grid.h * 3 / 4 - 40, w: 160, h: 60, label: 'again' } # array definition, text/title
      args.state.play_note_button ||= { x: 560, y: args.grid.h * 3 / 4 - 40, w: 160, h: 60, label: 'play' }
    
      if args.state.game_over # if game is over, a "play again" button is shown
        # Calculations ensure that Play Again label is displayed in center of border
        # Remove calculations from y parameters and see what happens to border and label placement
        args.outputs.labels <<  [args.grid.w_half, args.grid.h * 3 / 4, "Play Again", 0, 1, 0, 0, 0] # outputs label
        args.outputs.borders << args.state.play_again_button # outputs border
      else # otherwise, if game is not over
        # Calculations ensure that label appears in center of border
        args.outputs.labels <<  [args.grid.w_half, args.grid.h * 3 / 4, "Play Note ##{args.state.current_level}", 0, 1, 0, 0, 0] # outputs label
        args.outputs.borders << args.state.play_note_button # outputs border
      end
    
      return if args.state.game_over # return if game is over
    
      args.outputs.labels <<   [args.grid.w_half, 400, "I think the note is a(n)...",  0, 1, 0, 0, 0] # outputs label
    
      # Shows all of the available notes that can be potential matches.
      available_notes.each_with_index do |note, i|
        args.state.notes[i] ||= piano_button(args, note, i + 1) # calls piano_button method on each note (creates label and border)
        args.outputs.labels <<   args.state.notes[i].label # outputs note on screen with a label and a border
        args.outputs.borders <<  args.state.notes[i].border
      end
    
      # Shows whether or not the user is correct by filling the screen with either red or green
      args.outputs.solids << args.state.click_feedbacks.map { |c| c.solid }
    end
    
    # Shows the score (number of times the user guesses wrong) onto the screen using labels.
    def render_score args
      if args.state.times_wrong == 0 # if the user has guessed wrong zero times, the score is par
        args.outputs.labels << [args.grid.w_half, args.grid.top.shift_down(80), "Score: PAR", 0, 1, 0, 0, 0]
      else # otherwise, number of times the user has guessed wrong is shown
        args.outputs.labels << [args.grid.w_half, args.grid.top.shift_down(80), "Score: +#{args.state.times_wrong}", 0, 1, 0, 0, 0] # shows score using string interpolation
      end
    end
    
    # Sets the target note for the level and performs calculations on click_feedbacks.
    def calc args
      args.state.target_note ||= available_notes.sample # chooses a note from available_notes collection as target note
      args.state.click_feedbacks.each    { |c| c.solid[-1] -= 5 } # remove this line and solid color will remain on screen indefinitely
      # comment this line out and the solid color will keep flashing on screen instead of being removed from click_feedbacks collection
      args.state.click_feedbacks.reject! { |c| c.solid[-1] <= 0 }
    end
    
    # Uses input from the user to play the target note, as well as the other notes that could be a potential match.
    def input_mouse args
      return unless args.inputs.mouse.click # return unless the mouse is clicked
    
      # finds button that was clicked by user
      button_clicked = args.outputs.borders.find_all do |b| # go through borders collection to find all borders that meet requirements
        args.inputs.mouse.click.point.inside_rect? b # find button border that mouse was clicked inside of
      end.find_all { |b| b.is_a? Hash }.first # reject, return first element
    
      return unless button_clicked # return unless button_clicked as a value (a button was clicked)
    
      queue_click_feedback args, # calls queue_click_feedback method on the button that was clicked
                           button_clicked.x,
                           button_clicked.y,
                           button_clicked.w,
                           button_clicked.h,
                           150, 100, 200 # sets color of button to shade of purple
    
      if button_clicked[:label] == 'play' # if "play note" button is pressed
        args.outputs.sounds << "sounds/#{args.state.target_note}.wav" # sound of target note is output
      elsif button_clicked[:label] == 'again' # if "play game again" button is pressed
        args.state.target_note = nil # no target note
        args.state.current_level = 1 # starts at level 1 again
        args.state.times_wrong = 0 # starts off with 0 wrong guesses
        args.state.game_over = false # the game is not over (because it has just been restarted)
      else # otherwise if neither of those buttons were pressed
        args.outputs.sounds << "sounds/#{button_clicked[:label]}.wav" # sound of clicked note is played
        if button_clicked[:label] == args.state.target_note # if clicked note is target note
          args.state.target_note = nil # target note is emptied
    
          if args.state.current_level < 9 # if game hasn't reached level 9
            args.state.current_level += 1 # game goes to next level
          else # otherwise, if game has reached level 9
            args.state.game_over = true # the game is over
          end
    
          queue_click_feedback args, 0, 0, args.grid.w, args.grid.h, 100, 200, 100 # green shown if user guesses correctly
        else # otherwise, if clicked note is not target note
          args.state.times_wrong += 1 # increments times user guessed wrong
          queue_click_feedback args, 0, 0, args.grid.w, args.grid.h, 200, 100, 100 # red shown is user guesses wrong
        end
      end
    end
    
    # Creates a collection of all of the available notes as symbols
    def available_notes
      [:C3, :D3, :E3, :F3, :G3, :A3, :B3, :C4]
    end
    
    # Creates buttons for each note, and sets a label (the note's name) and border for each note's button.
    def piano_button args, note, position
      args.state.new_entity(:button) do |b| # declares button as new entity
        b.label  =  [460 + 40.mult(position), args.grid.h * 0.4, "#{note}", 0, 1, 0, 0, 0] # label definition
        b.border =  { x: 460 + 40.mult(position) - 20, y: args.grid.h * 0.4 - 32, w: 40, h: 40, label: note } # border definition, text/title; 20 subtracted so label is in center of border
      end
    end
    
    # Color of click feedback changes depending on what button was clicked, and whether the guess is right or wrong
    # If a button is clicked, the inside of button is purple (see input_mouse method)
    # If correct note is clicked, screen turns green
    # If incorrect note is clicked, screen turns red (again, see input_mouse method)
    def queue_click_feedback args, x, y, w, h, *color
      args.state.click_feedbacks << args.state.new_entity(:click_feedback) do |c| # declares feedback as new entity
        c.solid =  [x, y, w, h, *color, 255] # sets color
      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
    
    

    Squares - main.rb link

    # ./samples/99_genre_arcade/squares/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.ease_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
    
    # the start scene is loaded when the game is started
    # it contains a PulseButton that starts the game by setting the next_scene to :game and
    # setting the started_at time
    class StartScene
      attr_gtk
    
      def initialize args
        self.args = args
        @play_button = PulseButton.new layout.rect(row: 6, col: 11, w: 2, h: 2), "play" do
          state.next_scene = :game
          state.events.game_started_at = Kernel.tick_count
          state.events.game_over_at = nil
        end
      end
    
      def tick
        return if state.current_scene != :start
        @play_button.tick Kernel.tick_count, inputs.mouse
        outputs[:start_scene].labels << layout.point(row: 0, col: 12).merge(text: "Squares", anchor_x: 0.5, anchor_y: 0.5, size_px: 64)
        outputs[:start_scene].primitives << @play_button.prefab(easing)
      end
    end
    
    # the game over scene is displayed when the game is over
    # it contains a PulseButton that restarts the game by setting the next_scene to :game and
    # setting the game_retried_at time
    class GameOverScene
      attr_gtk
    
      def initialize args
        self.args = args
        @replay_button = PulseButton.new layout.rect(row: 6, col: 11, w: 2, h: 2), "replay" do
          state.next_scene = :game
          state.events.game_retried_at = Kernel.tick_count
          state.events.game_over_at = nil
        end
      end
    
      def tick
        return if state.current_scene != :game_over
        @replay_button.tick Kernel.tick_count, inputs.mouse
        outputs[:game_over_scene].labels << layout.point(row: 0, col: 12).merge(text: "Game Over", anchor_x: 0.5, anchor_y: 0.5, size_px: 64)
        outputs[:game_over_scene].primitives << @replay_button.prefab(easing)
    
        rect = layout.point row: 2, col: 12
        outputs[:game_over_scene].primitives << rect.merge(text: state.score_last_game, anchor_x: 0.5, anchor_y: 0.5, size_px: 128, **state.red_color)
    
        rect = layout.point row: 4, col: 12
        outputs[:game_over_scene].primitives << rect.merge(text: "BEST #{state.best_score}", anchor_x: 0.5, anchor_y: 0.5, size_px: 64, **state.gray_color)
      end
    end
    
    # the game scene contains the game logic
    class GameScene
      attr_gtk
    
      def tick
        defaults
        calc
        render
      end
    
      def defaults
        return if started_at != Kernel.tick_count
    
        # initalization of scene_state variables for the game
        scene_state.score_animation_spline = [[0.0, 0.66, 1.0, 1.0], [1.0, 0.33, 0.0, 0.0]]
        scene_state.launch_particle_queue = []
        scene_state.scale_down_particles_queue = []
        scene_state.score = 0
        scene_state.square_number = 1
        scene_state.squares = []
        scene_state.square_spawn_rate = 60
        scene_state.movement_outer_rect = layout.rect(row: 11, col: 7, w: 10, h: 1).merge(path: :pixel, **state.gray_color)
    
        scene_state.player = { x: geometry.rect_center_point(movement_outer_rect).x,
                               y: movement_outer_rect.y,
                               w: movement_outer_rect.h,
                               h: movement_outer_rect.h,
                               path: :pixel,
                               movement_direction: 1,
                               movement_speed: 8,
                               **args.state.red_color }
    
        scene_state.movement_inner_rect = { x: movement_outer_rect.x + player.w * 1,
                                            y: movement_outer_rect.y,
                                            w: movement_outer_rect.w - player.w * 2,
                                            h: movement_outer_rect.h }
      end
    
      def calc
        calc_game_over_at
        calc_particles
    
        # game logic is only calculated if the current scene is :game
        return if state.current_scene != :game
    
        # we don't want the game loop to start for half a second after the game starts
        # this gives enough time for the game scene to animate in
        return if !started_at || started_at.elapsed_time <= 30
    
        calc_player
        calc_squares
        calc_game_over
      end
    
      # this function calculates the point in the time the game is over
      # an intermediary variable stored in scene_state.death_at is consulted
      # before transitioning to the game over scene to ensure that particle animations
      # have enough time to complete before the game over scene is rendered
      def calc_game_over_at
        return if !death_at
        return if death_at.elapsed_time < 120
        state.events.game_over_at ||= Kernel.tick_count
      end
    
      # this function calculates the particles
      # there are two queues of particles that are processed
      # the launch_particle_queue contains particles that are launched when the player is hit
      # the scale_down_particles_queue contains particles that need to be scaled down
      def calc_particles
        return if !started_at
    
        scene_state.launch_particle_queue.each do |p|
          p.x += p.launch_angle.vector_x * p.speed
          p.y += p.launch_angle.vector_y * p.speed
          p.speed *= 0.90
          p.d_a ||= 1
          p.a -= 1 * p.d_a
          p.d_a *= 1.1
        end
    
        scene_state.launch_particle_queue.reject! { |p| p.a <= 0 }
    
        scene_state.scale_down_particles_queue.each do |p|
          next if p.start_at > Kernel.tick_count
          p.scale_speed = p.scale_speed.abs
          p.x += p.scale_speed
          p.y += p.scale_speed
          p.w -= p.scale_speed * 2
          p.h -= p.scale_speed * 2
        end
    
        scene_state.scale_down_particles_queue.reject! { |p| p.w <= 0 }
      end
    
      def render
        return if !started_at
        scene_outputs.primitives << game_scene_score_prefab
        scene_outputs.primitives << scene_state.movement_outer_rect.merge(a: 128)
        scene_outputs.primitives << squares
        scene_outputs.primitives << player_prefab
        scene_outputs.primitives << scene_state.launch_particle_queue
        scene_outputs.primitives << scene_state.scale_down_particles_queue
      end
    
      # this function returns the rendering primitive for the score
      def game_scene_score_prefab
        score = if death_at
                  state.score_last_game
                else
                  scene_state.score
                end
    
        label_scale_prec = easing.ease_spline(scene_state.score_at || 0, Kernel.tick_count, 15, scene_state.score_animation_spline)
        rect = layout.point row: 4, col: 12
        rect.merge(text: score, anchor_x: 0.5, anchor_y: 0.5, size_px: 128 + 50 * label_scale_prec, **state.gray_color)
      end
    
      def player_prefab
        return nil if death_at
        scale_perc = easing.ease(started_at + 30, Kernel.tick_count, 15, :smooth_start_quad, :flip)
        player.merge(x: player.x - player.w / 2 * scale_perc, y: player.y + player.h / 2 * scale_perc,
                     w: player.w * (1 - scale_perc), h: player.h * (1 - scale_perc))
      end
    
      # controls the player movement and change in direction of the player when the mouse is clicked
      def calc_player
        player.x += player.movement_speed * player.movement_direction
        player.movement_direction *= -1 if !geometry.inside_rect? player, scene_state.movement_outer_rect
        return if !inputs.mouse.click
        return if !geometry.inside_rect? player, movement_inner_rect
        player.movement_direction = -player.movement_direction
      end
    
      # computes the squares movement
      def calc_squares
        squares << new_square if Kernel.tick_count.zmod? scene_state.square_spawn_rate
    
        squares.each do |square|
          square.angle += 1
          square.x += square.dx
          square.y += square.dy
        end
    
        squares.reject! { |square| (square.y + square.h) < 0 }
      end
    
      # determines if score should be incremented or if the game should be over
      def calc_game_over
        collision = geometry.find_intersect_rect player, squares
        return if !collision
        if collision.type == :good
          scene_state.score += 1
          scene_state.score_at = Kernel.tick_count
          scene_state.scale_down_particles_queue << collision.merge(start_at: Kernel.tick_count, scale_speed: -2)
          squares.delete collision
        else
          generate_death_particles
          state.best_score = scene_state.score if scene_state.score > state.best_score
          squares.clear
          state.score_last_game = scene_state.score
          scene_state.score = 0
          scene_state.square_number = 1
          scene_state.death_at = Kernel.tick_count
          state.next_scene = :game_over
        end
      end
    
      # this function generates the particles when the player is hit
      def generate_death_particles
        square_particles = squares.map { |b| b.merge(start_at: Kernel.tick_count + 60, scale_speed: -1) }
    
        scene_state.scale_down_particles_queue.concat square_particles
    
        # generate 12 particles with random size, launch angle and speed
        player_particles = 12.map do
          size = rand * player.h * 0.5 + 10
          player.merge(w: size, h: size, a: 255, launch_angle: rand * 180, speed: 10 + rand * 50)
        end
    
        scene_state.launch_particle_queue.concat player_particles
      end
    
      # this function returns a new square
      # every 5th square is a good square (increases the score)
      def new_square
        x = movement_inner_rect.x + rand * movement_inner_rect.w
    
        dx = if x > geometry.rect_center_point(movement_inner_rect).x
               -0.9
             else
               0.9
             end
    
        if scene_state.square_number.zmod? 5
          type = :good
          color = state.red_color
        else
          type = :bad
          color = { r: 0, g: 0, b: 0 }
        end
    
        scene_state.square_number += 1
    
        { x: x - 16, y: 1300, w: 32, h: 32,
          dx: dx, dy: -5,
          angle: 0, type: type,
          path: :pixel, **color }
      end
    
      # death_at is the point in time that the player died
      # the death_at value is an intermediary variable that is used to calculate the death animation
      # before setting state.game_over_at
      def death_at
        return nil if !scene_state.death_at
        return nil if scene_state.death_at < started_at
        scene_state.death_at
      end
    
      # started_at is the point in time that the player started (or retried) the game
      def started_at
        state.events.game_retried_at || state.events.game_started_at
      end
    
      def scene_state
        state.game_scene ||= {}
      end
    
      def scene_outputs
        outputs[:game_scene]
      end
    
      def player
        scene_state.player
      end
    
      def movement_outer_rect
        scene_state.movement_outer_rect
      end
    
      def movement_inner_rect
        scene_state.movement_inner_rect
      end
    
      def squares
        scene_state.squares
      end
    end
    
    class RootScene
      attr_gtk
    
      def initialize args
        self.args = args
        @start_scene = StartScene.new args
        @game_scene = GameScene.new
        @game_over_scene = GameOverScene.new args
      end
    
      def tick
        outputs.background_color = [237, 237, 237]
        init_game
        state.scene_at_tick_start = state.current_scene
        tick_start_scene
        tick_game_scene
        tick_game_over_scene
        render_scenes
        transition_to_next_scene
      end
    
      def tick_start_scene
        @start_scene.args = args
        @start_scene.tick
      end
    
      def tick_game_scene
        @game_scene.args = args
        @game_scene.tick
      end
    
      def tick_game_over_scene
        @game_over_scene.args = args
        @game_over_scene.tick
      end
    
      # initlalization of game state that is shared between scenes
      def init_game
        return if Kernel.tick_count != 0
    
        state.current_scene = :start
    
        state.red_color = { r: 222, g: 63, b: 66 }
        state.gray_color = { r: 128, g: 128, b: 128 }
    
        state.events ||= {
          game_over_at: nil,
          game_started_at: nil,
          game_retried_at: nil
        }
    
        state.score_last_game = 0
        state.best_score = 0
        state.viewport = { x: 0, y: 0, w: 1280, h: 720 }
      end
    
      def transition_to_next_scene
        if state.scene_at_tick_start != state.current_scene
          raise "state.current_scene was changed during the tick. This is not allowed (use state.next_scene to set the scene to transfer to)."
        end
    
        return if !state.next_scene
        state.current_scene = state.next_scene
        state.next_scene = nil
      end
    
      # this function renders the scenes with a transition effect
      # based off of timestamps stored in state.events
      def render_scenes
        if state.events.game_over_at
          in_y = transition_in_y state.events.game_over_at
          out_y = transition_out_y state.events.game_over_at
          outputs.sprites << state.viewport.merge(y: out_y, path: :game_scene)
          outputs.sprites << state.viewport.merge(y: in_y, path: :game_over_scene)
        elsif state.events.game_retried_at
          in_y = transition_in_y state.events.game_retried_at
          out_y = transition_out_y state.events.game_retried_at
          outputs.sprites << state.viewport.merge(y: out_y, path: :game_over_scene)
          outputs.sprites << state.viewport.merge(y: in_y, path: :game_scene)
        elsif state.events.game_started_at
          in_y = transition_in_y state.events.game_started_at
          out_y = transition_out_y state.events.game_started_at
          outputs.sprites << state.viewport.merge(y: out_y, path: :start_scene)
          outputs.sprites << state.viewport.merge(y: in_y, path: :game_scene)
        else
          in_y = transition_in_y 0
          start_scene_perc = easing.ease(0, Kernel.tick_count, 30, :smooth_stop_quad, :flip)
          outputs.sprites << state.viewport.merge(y: in_y, path: :start_scene)
        end
      end
    
      def transition_in_y start_at
        easing.ease(start_at, Kernel.tick_count, 30, :smooth_stop_quad, :flip) * -1280
      end
    
      def transition_out_y start_at
        easing.ease(start_at, Kernel.tick_count, 30, :smooth_stop_quad) * 1280
      end
    end
    
    def tick args
      $game ||= RootScene.new args
      $game.args = args
      $game.tick
    
      if args.inputs.keyboard.key_down.forward_slash
        @show_fps = !@show_fps
      end
      if @show_fps
        args.outputs.primitives << args.gtk.current_framerate_primitives
      end
    end
    
    $gtk.reset
    
    

    Twinstick - main.rb link

    # ./samples/99_genre_arcade/twinstick/app/main.rb
    def tick args
      args.state.player         ||= {x: 600, y: 320, w: 80, h: 80, path: 'sprites/circle-white.png', vx: 0, vy: 0, health: 10, cooldown: 0, score: 0}
      args.state.enemies        ||= []
      args.state.player_bullets ||= []
      spawn_enemies args
      kill_enemies args
      move_enemies args
      move_bullets args
      move_player args
      fire_player args
      args.state.player[:r] = args.state.player[:g] = args.state.player[:b] = (args.state.player[:health] * 25.5).clamp(0, 255)
      label_color           = args.state.player[:health] <= 5 ? 255 : 0
      args.outputs.labels << [
          {
              x: args.state.player.x + 40, y: args.state.player.y + 60, alignment_enum: 1, text: "#{args.state.player[:health]} HP",
              r: label_color, g: label_color, b: label_color
          }, {
              x: args.state.player.x + 40, y: args.state.player.y + 40, alignment_enum: 1, text: "#{args.state.player[:score]} PTS",
              r: label_color, g: label_color, b: label_color, size_enum: 2 - args.state.player[:score].to_s.length,
          }
      ]
      args.outputs.sprites << [args.state.player, args.state.enemies, args.state.player_bullets]
      args.state.clear! if args.state.player[:health] < 0 # Reset the game if the player's health drops below zero
    end
    
    def spawn_enemies args
      # Spawn enemies more frequently as the player's score increases.
      if rand < (100+args.state.player[:score])/(10000 + args.state.player[:score]) || Kernel.tick_count.zero?
        theta = rand * Math::PI * 2
        args.state.enemies << {
            x: 600 + Math.cos(theta) * 800, y: 320 + Math.sin(theta) * 800, w: 80, h: 80, path: 'sprites/circle-white.png',
            r: (256 * rand).floor, g: (256 * rand).floor, b: (256 * rand).floor
        }
      end
    end
    
    def kill_enemies args
      args.state.enemies.reject! do |enemy|
        # Check if enemy and player are within 80 pixels of each other (i.e. overlapping)
        if 6400 > (enemy.x - args.state.player.x) ** 2 + (enemy.y - args.state.player.y) ** 2
          # Enemy is touching player. Kill enemy, and reduce player HP by 1.
          args.state.player[:health] -= 1
        else
          args.state.player_bullets.any? do |bullet|
            # Check if enemy and bullet are within 50 pixels of each other (i.e. overlapping)
            if 2500 > (enemy.x - bullet.x + 30) ** 2 + (enemy.y - bullet.y + 30) ** 2
              # Increase player health by one for each enemy killed by a bullet after the first enemy, up to a maximum of 10 HP
              args.state.player[:health] += 1 if args.state.player[:health] < 10 && bullet[:kills] > 0
              # Keep track of how many enemies have been killed by this particular bullet
              bullet[:kills]             += 1
              # Earn more points by killing multiple enemies with one shot.
              args.state.player[:score]  += bullet[:kills]
            end
          end
        end
      end
    end
    
    def move_enemies args
      args.state.enemies.each do |enemy|
        # Get the angle from the enemy to the player
        theta   = Math.atan2(enemy.y - args.state.player.y, enemy.x - args.state.player.x)
        # Convert the angle to a vector pointing at the player
        dx, dy  = theta.to_degrees.vector 5
        # Move the enemy towards thr player
        enemy.x -= dx
        enemy.y -= dy
      end
    end
    
    def move_bullets args
      args.state.player_bullets.each do |bullet|
        # Move the bullets according to the bullet's velocity
        bullet.x += bullet[:vx]
        bullet.y += bullet[:vy]
      end
      args.state.player_bullets.reject! do |bullet|
        # Despawn bullets that are outside the screen area
        bullet.x < -20 || bullet.y < -20 || bullet.x > 1300 || bullet.y > 740
      end
    end
    
    def move_player args
      # Get the currently held direction.
      dx, dy                 = move_directional_vector args
      # Take the weighted average of the old velocities and the desired velocities.
      # Since move_directional_vector returns values between -1 and 1,
      #   and we want to limit the speed to 7.5, we multiply dx and dy by 7.5*0.1 to get 0.75
      args.state.player[:vx] = args.state.player[:vx] * 0.9 + dx * 0.75
      args.state.player[:vy] = args.state.player[:vy] * 0.9 + dy * 0.75
      # Move the player
      args.state.player.x    += args.state.player[:vx]
      args.state.player.y    += args.state.player[:vy]
      # If the player is about to go out of bounds, put them back in bounds.
      args.state.player.x    = args.state.player.x.clamp(0, 1201)
      args.state.player.y    = args.state.player.y.clamp(0, 640)
    end
    
    
    def fire_player args
      # Reduce the firing cooldown each tick
      args.state.player[:cooldown] -= 1
      # If the player is allowed to fire
      if args.state.player[:cooldown] <= 0
        dx, dy = shoot_directional_vector args # Get the bullet velocity
        return if dx == 0 && dy == 0 # If the velocity is zero, the player doesn't want to fire. Therefore, we just return early.
        # Add a new bullet to the list of player bullets.
        args.state.player_bullets << {
            x:     args.state.player.x + 30 + 40 * dx,
            y:     args.state.player.y + 30 + 40 * dy,
            w:     20, h: 20,
            path:  'sprites/circle-white.png',
            r:     0, g: 0, b: 0,
            vx:    10 * dx + args.state.player[:vx] / 7.5, vy: 10 * dy + args.state.player[:vy] / 7.5, # Factor in a bit of the player's velocity
            kills: 0
        }
        args.state.player[:cooldown] = 30 # Reset the cooldown
      end
    end
    
    # Custom function for getting a directional vector just for movement using WASD
    def move_directional_vector args
      dx = 0
      dx += 1 if args.inputs.keyboard.d
      dx -= 1 if args.inputs.keyboard.a
      dy = 0
      dy += 1 if args.inputs.keyboard.w
      dy -= 1 if args.inputs.keyboard.s
      if dx != 0 && dy != 0
        dx *= 0.7071
        dy *= 0.7071
      end
      [dx, dy]
    end
    
    # Custom function for getting a directional vector just for shooting using the arrow keys
    def shoot_directional_vector args
      dx = 0
      dx += 1 if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_held.right
      dx -= 1 if args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_held.left
      dy = 0
      dy += 1 if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_held.up
      dy -= 1 if args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_held.down
      if dx != 0 && dy != 0
        dx *= 0.7071
        dy *= 0.7071
      end
      [dx, dy]
    end
    
    

    Genre Board Game link

    Fifteen Puzzle - main.rb link

    # ./samples/99_genre_board_game/01_fifteen_puzzle/app/main.rb
    class Game
      attr_gtk
    
      def tick
        defaults
        calc
        render
      end
    
      def defaults
        # set a reliable seed when not in production so the
        # saved replay works correctly
        srand 0 if Kernel.tick_count == 0 && !gtk.production?
    
        # set rendering positions/properties
        state.cell_size     ||= 64
        state.left_margin   ||= (grid.w - 4 * state.cell_size) / 2
        state.bottom_margin ||= (grid.h - 4 * state.cell_size) / 2
    
        # if the board isn't initialized
        if !state.board || state.win
          # generate a solvable board
          state.board = solvable_board
          state.win = false
        end
      end
    
      def solvable_board
        # create a random board with cells of the
        # following format:
        # {
        #   value: 1,
        #   loc: { row: 0, col: 0 },
        #   previous_loc: { row: 0, col: 0 },
        #   clicked_at: 0
        # }
        results = 16.map_with_index do |i|
          { value: i + 1 }
        end.sort_by do |cell|
          rand
        end.map_with_index do |cell, index|
          row = index.idiv 4
          col = index % 4
          cell.merge loc: { row: row, col: col },
                     previous_loc: { row: row, col: col },
                     clicked_at: 0
        end
    
        # determine if the board is solvable
        # by counting the number of inversions
        # (a board is solvable if the number of inversions is even)
        solvable = number_of_inversions(results).even?
    
        # recursively call this method until a solvable board is generated
        return solvable_board if !solvable
    
        return results
      end
    
      def number_of_inversions board
        # get the number of rows
        number_of_rows = board.map { |cell| cell.loc.row }.uniq.count
    
        results = 0
    
        # for each row
        number_of_rows.times_with_index do |row|
          # find all the cells in the row
          # and count the number of inversions for that single row
          inversions_in_row = board.find_all { |cell| cell.loc.row == row }
                                   .map { |cell| cell.value }
                                   .each_cons(2)
                                   .map { |cell, next_cell| cell > next_cell ? 1 : 0 }
                                   .sum
    
          # add the number of inversions for that row to the total
          results += inversions_in_row
        end
    
        # return the total number of inversions
        results
      end
    
      def render
        outputs.sprites << board.map do |cell|
          # render the board centered in the middle of the screen
          prefab = cell_prefab cell
          prefab.merge x: state.left_margin + prefab.x, y: state.bottom_margin + prefab.y
        end
    
        # render the win message
        if state.won_at && state.won_at.elapsed_time < 180
          # define a bezier spline that will be used to
          # fade in the win message stay visible for a little bit
          # then fade out
          spline = [
            [  0, 0.25, 0.75, 1.0],
            [1.0, 1.0,  1.0,  1.0],
            [1.0, 0.75, 0.25,   0]
          ]
    
          alpha_percentage = args.easing.ease_spline state.won_at,
                                                     Kernel.tick_count,
                                                     180,
                                                     spline
    
          outputs.sprites << {
            x: 0,
            y: grid.h.half - 32,
            w: grid.w,
            h: 64,
            path: :pixel,
            r: 0,
            g: 0,
            b: 0,
            a: 255 * alpha_percentage,
          }
    
          outputs.labels << {
            x: grid.w.half,
            y: grid.h.half,
            text: "You won!",
            a: 255 * alpha_percentage,
            alignment_enum: 1,
            vertical_alignment_enum: 1,
            size_enum: 10,
            r: 255,
            g: 255,
            b: 255
          }
        end
      end
    
      def calc
        calc_input
        calc_win
      end
    
      def calc_input
        # return if the mouse isn't clicked
        return if !inputs.mouse.click
    
        # determine which cell was clicked
        clicked_cell = board.find do |cell|
          mouse_rect = {
            x: inputs.mouse.x - state.left_margin,
            y: inputs.mouse.y - state.bottom_margin,
            w: 1,
            h: 1,
          }
          mouse_rect.intersect_rect? render_rect(cell.loc)
        end
    
        # return if no cell was clicked
        return if !clicked_cell
    
        # find the empty cell
        empty_cell = board.find do |cell|
          cell.value == 16
        end
    
        # find the clicked cell's neighbors
        clicked_cell_neighbors = neighbors clicked_cell
    
        # return if the cell's neighbors doesn't include the empty cell
        return if !clicked_cell_neighbors.include?(empty_cell)
    
        # otherwise swap the clicked cell with the empty cell
        swap_with_empty clicked_cell, empty_cell
      end
    
      def calc_win
        sorted_values = board.sort_by { |cell| (cell.loc.col + 1) + (16 - (cell.loc.row * 4)) }
                             .map { |cell| cell.value }
    
        state.win = sorted_values == (1..16).to_a
    
        state.won_at ||= Kernel.tick_count if state.win
      end
    
      def swap_with_empty cell, empty
        # take not of the cell's current location (within previous_loc)
        cell.previous_loc = cell.loc
    
        # swap the cell's location with the empty cell's location and vice versa
        cell.loc, empty.loc = empty.loc, cell.loc
    
        # take note of the current tick count (which will be used for animation)
        cell.clicked_at = Kernel.tick_count
      end
    
      def cell_prefab cell
        # determine the percentage for the lerp that should be performed
        percentage = if cell.clicked_at
                       easing.ease cell.clicked_at, Kernel.tick_count, 15, :smooth_stop_quint, :flip
                     else
                       1
                     end
    
        # determine the cell's current render location
        cell_rect = render_rect cell.loc
    
        # determine the cell's previous render location
        previous_rect = render_rect cell.previous_loc
    
        # compute the difference between the current and previous render locations
        x = cell_rect.x + (previous_rect.x - cell_rect.x) * percentage
        y = cell_rect.y + (previous_rect.y - cell_rect.y) * percentage
    
        # return the cell prefab
        { x: x,
          y: y,
          w: state.cell_size,
          h: state.cell_size,
          path: "sprites/pieces/#{cell.value}.png" }
      end
    
      # helper method to determine the render location of a cell in local space
      # which excludes the margins
      def render_rect loc
        {
          x: loc.col * state.cell_size,
          y: loc.row * state.cell_size,
          w: state.cell_size,
          h: state.cell_size,
        }
      end
    
      # helper methods to determine neighbors of a cell
      def neighbors cell
        [
          above_cell(cell),
          below_cell(cell),
          left_cell(cell),
          right_cell(cell),
        ]
      end
    
      def below_cell cell
        find_cell cell, -1, 0
      end
    
      def above_cell cell
        find_cell cell, 1, 0
      end
    
      def left_cell cell
        find_cell cell, 0, -1
      end
    
      def right_cell cell
        find_cell cell, 0, 1
      end
    
      def find_cell cell, d_row, d_col
        board.find do |other_cell|
          cell.loc.row == other_cell.loc.row + d_row &&
          cell.loc.col == other_cell.loc.col + d_col
        end
      end
    
      def board
        state.board
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    $gtk.reset
    
    

    Genre Boss Battle link

    Boss Battle Game Jam - main.rb link

    # ./samples/99_genre_boss_battle/boss_battle_game_jam/app/main.rb
    class Game
      attr_gtk
    
      def tick
        defaults
        input
        calc
        render
      end
    
      def defaults
        state.high_score          ||= 0
        state.damage_render_queue ||= []
        game_reset if Kernel.tick_count == 0 || state.start_new_game
      end
    
      def game_reset
        state.start_new_game      = false
        state.game_over           = false
        state.game_over_countdown = nil
    
        state.player.tile_size          = 64
        state.player.speed              = 4
        state.player.slash_frames       = 15
        state.player.hp                 = 3
        state.player.damaged_at         = -1000
        state.player.x                  = 50
        state.player.y                  = 400
        state.player.dir_x              =  1
        state.player.dir_y              = -1
        state.player.is_moving          = false
    
        state.boss.damage               = 0
        state.boss.x                    = 800
        state.boss.y                    = 400
        state.boss.w                    = 256
        state.boss.h                    = 256
        state.boss.target_x             = 800
        state.boss.target_y             = 400
        state.boss.attack_cooldown      = 600
      end
    
      def input
        return if state.game_over
    
        player.is_moving = false
    
        if input_attack?
          player.slash_at = Kernel.tick_count
        end
    
        if !player_attacking?
          vector = inputs.directional_vector
          if vector
            next_player_x = player.x + vector.x * player.speed
            next_player_y = player.y + vector.y * player.speed
            player.x = next_player_x if player_x_inside_stage? next_player_x
            player.y = next_player_y if player_y_inside_stage? next_player_y
    
            player.is_moving = true
    
            player.dir_x = if vector.x < 0
                             -1
                           elsif vector.x > 0
                             1
                           else
                             player.dir_x
                           end
    
            player.dir_y = if vector.y < 0
                             -1
                           elsif vector.y > 0
                             1
                           else
                             player.dir_y
                           end
          end
        end
      end
    
      def input_attack?
        inputs.controller_one.key_down.a ||
        inputs.controller_one.key_down.b ||
        inputs.keyboard.key_down.j
      end
    
      def calc
        calc_player
        calc_boss
        calc_damage_render_queue
        calc_high_score
        calc_game_over
      end
    
      def calc_player
        player.slash_at = nil if !player_attacking?
        return unless player_slash_can_damage?
        if player_hit_box.intersect_rect? boss_hurt_box
          boss.damage += 1
          queue_damage player_hit_box.x + player_hit_box.w / 2 * player.dir_x,
                       player_hit_box.y + player_hit_box.h / 2
        end
      end
    
      def calc_boss
        boss.attack_cooldown -= 1
        if boss.attack_cooldown < 0
          boss.target_x = player.x - 100
          boss.target_y = player.y - 100
          boss.attack_cooldown = if    boss.damage > 200
                                   200
                                 elsif boss.damage > 150
                                   300
                                 elsif boss.damage > 100
                                   400
                                 elsif boss.damage > 50
                                   500
                                 else
                                   600
                                 end
        end
    
        dx = boss.target_x - boss.x
        dy = boss.target_y - boss.y
        boss.x += dx * 0.25 ** 2
        boss.y += dy * 0.25 ** 2
    
        if boss.intersect_rect?(player_hurt_box) && player.damaged_at.elapsed?(120)
          player.damaged_at = Kernel.tick_count
          player.hp -= 1
          player.hp  = 0 if player.hp < 0
        end
      end
    
      def calc_damage_render_queue
        state.damage_render_queue.each { |label| label.a -= 5 }
        state.damage_render_queue.reject! { |l| l.a < 0 }
      end
    
      def calc_high_score
        state.high_score = boss.damage if boss.damage > state.high_score
      end
    
      def calc_game_over
        if player.hp <= 0
          state.game_over = true
          state.game_over_countdown ||= 160
        end
    
        state.game_over_countdown -= 1 if state.game_over_countdown
        state.start_new_game = true    if state.game_over_countdown && state.game_over_countdown < 0
      end
    
      def render
        render_boss
        render_player
        render_damage_queue
        render_scores
        render_instructions
        render_game_over
        # render_debug
      end
    
      def render_player
        outputs.labels << { x: player.x + 5,
                            y: player.y + 5,
                            text: "hp: #{player.hp}" }
    
        if state.game_over
          outputs.labels << { x: player.x + player.tile_size / 2,
                              y: player.y + 85,
                              text: "RIP",
                              size_enum: 2,
                              alignment_enum: 1 }
        elsif !player.damaged_at.elapsed?(120)
          outputs.labels << { x: player.x + player.tile_size / 2,
                              y: player.y + 85,
                              text: "ouch!!",
                              size_enum: 2,
                              alignment_enum: 1 }
        end
    
        if state.game_over
          outputs.sprites << player_sprite_stand.merge(angle: -90, flip_horizontally: false)
        elsif player.slash_at
          outputs.sprites << player_sprite_slash
        elsif player.is_moving
          outputs.sprites << player_sprite_run
        else
          outputs.sprites << player_sprite_stand
        end
      end
    
      def render_boss
        outputs.sprites << boss_sprite
      end
    
      def render_damage_queue
        outputs.labels << state.damage_render_queue
      end
    
      def render_scores
        outputs.labels << { x: 30, y: 30.from_top, text: "curr score: #{boss.damage}" }
        outputs.labels << { x: 30, y: 50.from_top, text: "high score: #{state.high_score}" }
      end
    
      def render_instructions
        outputs.labels << { x: 30, y: 70, text: "Controls:" }
        outputs.labels << { x: 30, y: 50, text: "Keyboard:   WASD/Arrow keys to move. J to attack." }
        outputs.labels << { x: 30, y: 30, text: "Controller: D-Pad to move. A/B button to attack." }
      end
    
      def render_game_over
        return unless state.game_over
        outputs.labels << { x: 640, y: 360, text: "GAME OVER!!!", alignment_enum: 1, size_enum: 3 }
      end
    
      def render_debug
        outputs.borders << player_sprite_stand
        outputs.borders << player_hurt_box
        outputs.borders << player_hit_box
        outputs.borders << boss_hurt_box
        outputs.borders << boss_hit_box
      end
    
      def player
        state.player
      end
    
      def player_x_inside_stage? player_x
        return false if player_x < 0
        return false if (player_x + player.tile_size) > 1280
        return true
      end
    
      def player_y_inside_stage? player_y
        return false if player_y < 0
        return false if (player_y + player.tile_size) > 720
        return true
      end
    
      def player_attacking?
        return false if !player.slash_at
        return false if player.slash_at.elapsed?(player.slash_frames)
        return true
      end
    
      def player_slash_can_damage?
        return false if !player_attacking?
        return false if (player.slash_at + player.slash_frames.idiv(2)) != Kernel.tick_count
        return true
      end
    
      def player_hit_box
        sword_w = 50
        sword_h = 20
        if player.dir_x > 0
          {
            x: player.x + player.tile_size / 2 + sword_w / 2,
            y: player.y + player.tile_size / 2 - sword_h / 2,
            w: sword_w,
            h: sword_h
          }
        else
          {
            x: player.x + player.tile_size / 2 - sword_w / 2 - sword_w,
            y: player.y + player.tile_size / 2 - sword_h / 2,
            w: sword_w,
            h: sword_h
          }
        end
      end
    
      def player_hurt_box
        {
          x: player.x + 25,
          y: player.y + 25,
          w: 10,
          h: 10
        }
      end
    
      def player_sprite_run
        tile_index = 0.frame_index count:    6,
                                   hold_for: 3,
                                   repeat:   true
    
        tile_index = 0 if !player.is_moving
    
        {
          x:                 player.x,
          y:                 player.y,
          w:                 player.tile_size,
          h:                 player.tile_size,
          path:              'sprites/boss-battle/player-run-tile-sheet.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 player_sprite_stand
        {
          x:                 player.x,
          y:                 player.y,
          w:                 player.tile_size,
          h:                 player.tile_size,
          path:              'sprites/boss-battle/player-stand.png',
          flip_horizontally: player.dir_x > 0,
        }
      end
    
      def player_sprite_slash
        tile_index   = player.slash_at.frame_index count: 5,
                                                   hold_for: player.slash_frames.idiv(5),
                                                   repeat: false
    
        tile_index ||= 0
        tile_offset = 41.25
    
        if player.dir_x > 0
          {
            x:                 player.x - tile_offset,
            y:                 player.y - tile_offset,
            w:                 165,
            h:                 165,
            path:              'sprites/boss-battle/player-slash-tile-sheet.png',
            tile_x:            0 + (tile_index * 128),
            tile_y:            0,
            tile_w:            128,
            tile_h:            128,
            flip_horizontally: true
          }
        else
          {
            x:                 player.x - tile_offset - tile_offset / 2,
            y:                 player.y - tile_offset,
            w:                 165,
            h:                 165,
            path:              'sprites/boss-battle/player-slash-tile-sheet.png',
            tile_x:            0 + (tile_index * 128),
            tile_y:            0,
            tile_w:            128,
            tile_h:            128,
            flip_horizontally: false
          }
        end
      end
    
      def boss
        state.boss
      end
    
      def boss_hurt_box
        {
          x: boss.x,
          y: boss.y,
          w: boss.w,
          h: boss.h
        }
      end
    
      def boss_hit_box
        {
          x: boss.x,
          y: boss.y,
          w: boss.w,
          h: boss.h
        }
      end
    
      def boss_sprite
        case boss_attack_state
        when :sleeping
          { x: boss.x,
            y: boss.y,
            w: boss.w,
            h: boss.h,
            path: 'sprites/boss-battle/boss-sleeping.png' }
        when :aware
          { x: boss.x,
            y: boss.y,
            w: boss.w,
            h: boss.h,
            path: 'sprites/boss-battle/boss-aware.png' }
        when :annoyed
          { x: boss.x,
            y: boss.y,
            w: boss.w,
            h: boss.h,
            path: 'sprites/boss-battle/boss-annoyed.png' }
        when :will_attack
          shake_x  =  2 * rand
          shake_x *= -1 if rand < 0.5
    
          shake_y  =  2 * rand
          shake_y *= -1 if rand < 0.5
    
          { x: boss.x + shake_x,
            y: boss.y + shake_x,
            w: boss.w,
            h: boss.h,
            path: 'sprites/boss-battle/boss-will-attack.png' }
        when :attacking
          flip_horizontally = false
          flip_horizontally = true if boss.target_x > boss.x
    
          { x: boss.x,
            y: boss.y,
            w: boss.w,
            h: boss.h,
            flip_horizontally: flip_horizontally,
            path: 'sprites/boss-battle/boss-attacking.png' }
        else
          { x: boss.x, y: boss.y, w: boss.w, h: boss.h, r: 255, g: 0, b: 0 }
        end
      end
    
      def boss_attack_state
        if boss.target_x.round != boss.x.round || boss.target_y.round != boss.y.round
          :attacking
        elsif boss.attack_cooldown < 30
          :will_attack
        elsif boss.attack_cooldown < 120
          :annoyed
        elsif boss.attack_cooldown < 180
          :aware
        else
          :sleeping
        end
      end
    
      def queue_damage x, y
        rand_x_offset = rand * 20
        rand_y_offset = rand * 20
        rand_x_offset *= -1 if rand < 0.5
        rand_y_offset *= -1 if rand < 0.5
        state.damage_render_queue << { x: x + rand_x_offset, y: y + rand_y_offset, a: 255, text: "wack!" }
      end
    end
    
    $game = Game.new
    
    def tick args
      $game.args = args
      $game.tick
    end
    
    

    Genre Crafting link

    Alchemy Game Starting Point - main.rb link

    # ./samples/99_genre_crafting/alchemy_game_starting_point/app/main.rb
    # A collection of function related to elements of natrue
    class Element
      # returns the tile size in pixels { w:, h: }
      # Layout::rect is a virtual grid that is 24 columns by 12 rows
      def self.tile_size
        Layout::rect(w: 1, h: 1)
               .slice(:w, :h)
      end
    
      # given a point/position in pixels, returns a rect with
      # { x:, y:, w:, h:, center: { x:, y: } }
      def self.tile_rect x:, y:, anchor_x: 0, anchor_y: 0, **ignore
        w, h = tile_size.values_at(:w, :h)
        Geometry.rect_props x: x - w * anchor_x,
                            y: y - h * anchor_y,
                            w: w,
                            h: h
      end
    
      # given a element, and it's position, this fucntion
      # returns render primitives that represent the element
      # visually
      def self.prefab_icon element, x:, y:, anchor_x: 0, anchor_y: 0, **ignore
        # if the element is decorated with an added_at property,
        # it means that we want to apply a fade in effect to the
        # prefab
        a = if element.added_at && element.added_at.elapsed_time < 60
              # fade in slow to fast over 1 second
              perc = Easing.ease element.added_at, Kernel.tick_count, 60, :smooth_start_quint
              255 * perc
            else
              255
            end
    
        # given the elements position, create a tile rect with the sprite and alpha
        tile_rect(x: x, y: y).merge(path: "sprites/square/#{element.name}.png", a: a)
      end
    
      # this represents the element prefab it its entirety
      # the sprite, a background rect and a text label above the
      # background rect
      def self.prefab element, position, shift_x: 0, shift_y: 0
        rect = tile_rect x: position.x + shift_x,
                         y: position.y + shift_y
    
        [
          # icon
          prefab_icon(element, x: position.x, y: position.y),
          # background rect
          rect.merge(path: :solid, h: 16, r: 0, g: 0, b: 0, a: 200),
          # text label
          {
            x: rect.center.x,
            y: rect.y,
            text: "#{element.name}",
            anchor_x: 0.5,
            anchor_y: 0,
            size_px: 16,
            r: 255,
            g: 255,
            b: 255
          },
    
          # white border
          rect.merge(primitive_marker: :border, r: 255, g: 255, b: 255)
        ]
      end
    
      # given a collection of elements,
      # this function returns a collection of grouped elements
      # (elements that are intersecting each other, or connected
      # to each other, because of a mutual neighbor element)
      def self.create_groupings elements
        grouped_elements = []
    
        rects_with_source = elements.map do |r|
          r.rect.merge(source: r)
        end
    
        rects_with_source.each do |r|
          grouped = grouped_elements.find do |g|
            g.any? { |i| i.intersect_rect? r }
          end
    
          if !grouped
            grouped_elements << [r]
          else
            grouped << r
          end
        end
    
        grouped_elements.map do |e|
          e.map { |r| r.source }
        end.uniq
      end
    end
    
    class Game
      attr_gtk
    
      def tick
        defaults
        calc
        render
      end
    
      def defaults
        # elements of nature and what they require to be created
        state.elements ||= [
          { name: :violet,  requires: [:red, :blue, :black] },
          { name: :indigo,  requires: [:red, :blue, :white] },
          { name: :gray,    requires: [:white, :black] },
          { name: :green,   requires: [:blue, :yellow] },
          { name: :orange,  requires: [:red, :yellow] },
        ]
    
        # elements that have been discovered seeded with the basic elements
        state.discovered_elements ||= [
          { name: :white },
          { name: :black },
          { name: :red },
          { name: :yellow },
          { name: :blue },
        ]
    
        # the canvas area where elements are placed/mixed
        state.canvas ||= {
          rect: Layout::rect(row: 0, col: 0, w: 20, h: 12),
          elements: []
        }
    
        # fx queue for faiding out sprites
        state.fade_out_queue ||= []
    
        # fx queue for mouse particles
        state.mouse_particles_queue ||= []
    
        # invalid mixtures queue (used to signal invalid mixtures)
        state.invalid_mixtures_queue ||= []
      end
    
      # adds a clone of an element to the canvas area
      # used by mouse movement and click events
      # and element discovery
      def add_element_to_canvas! element, position, fade_in: false
        return if !element
        new_entry = element.copy
        new_entry.added_at = Kernel.tick_count if fade_in
        new_entry.position = { x: position.x, y: position.y }
        state.canvas.elements << new_entry
        new_entry
      end
    
      def input_mouse
        # if the mouse is clicked...
        if inputs.mouse.down
          # check to see if any of the elements in the toolbar
          # were clicked, if so, set the selected element to the
          # clicked element
          toolbar_element = state.discovered_elements
                                 .find do |r|
                                   inputs.mouse.intersect_rect? r.rect
                                 end
    
          if toolbar_element
            state.selected_element = toolbar_element
          end
    
          # if no toolbar element was clicked, then check to see
          # if an element on the canvas was clicked
          if !state.selected_element
            state.selected_element = state.canvas.elements.reverse.find do |r|
              inputs.mouse.intersect_rect? r.rect
            end
    
            # if an element was clicked, remove it from the canvas
            if state.selected_element
              state.canvas.elements.reject! { |r| r == state.selected_element }
            end
          end
    
          if state.selected_element
            state.selected_element = state.selected_element.copy
          end
        elsif inputs.mouse.held && inputs.mouse.moved
          # emit pretty particles when the mouse is held and moved
          if Kernel.tick_count.zmod? 2
            state.mouse_particles_queue << {
              x: inputs.mouse.x + 10.randomize(:ratio, :sign),
              y: inputs.mouse.y + 10.randomize(:ratio, :sign),
              w: 10, h: 10, path: "sprites/star.png"
            }
          end
        elsif inputs.mouse.up
          if state.selected_element
            # if mouse is released,
            # cr
            if inputs.mouse.intersect_rect?(state.canvas.rect)
              rect = Element.tile_rect(x: inputs.mouse.up.x,
                                       y: inputs.mouse.up.y,
                                       anchor_x: 0.5,
                                       anchor_y: 0.5)
    
    
              # add the element to the canvas area and create particles
              # around the element drop
              created_element = add_element_to_canvas! state.selected_element, rect
    
              # get all intersecting elements with the element that was just being dragged
              intersecting_elements = state.canvas.elements.find_all do |element|
                element != created_element && Geometry::intersect_rect?(element.rect, created_element.rect)
              end
    
              # shake elements if the element doesn't have any potential interactions
              notify_invalid_mixture! created_element, intersecting_elements
    
              state.mouse_particles_queue.concat(30.map do |i|
                                                   { x: rect.center.x + 10.randomize(:ratio, :sign),
                                                     y: rect.center.y + 10.randomize(:ratio, :sign),
                                                     start_at: Kernel.tick_count + i + rand(2),
                                                     w: 10, h: 10, path: "sprites/star.png" }
                                                 end)
    
            else
              # if the mouse was released outside of the canvas area
              # then delete the element/remove it from the canvas
              w, h = Element.tile_size.values_at(:w, :h)
    
              # add the element to the fade out queue
              state.fade_out_queue << Element.prefab_icon(state.selected_element,
                                                          x: inputs.mouse.up.x - w / 2,
                                                          y: inputs.mouse.up.y - h / 2,
                                                          anchor_x: 0.5,
                                                          anchor_y: 0.5)
            end
          end
    
          state.selected_element = nil
        end
      end
    
      def notify_invalid_mixture! source, intersecting_elements
        return if intersecting_elements.length == 0
    
        # look through all the intersecting elements
        # see if any of their requirements match the source element
        # or the intersecting element
        possible = intersecting_elements.any? do |r|
          state.elements.any? do |sr|
            sr.requires.include?(source.name) &&
            sr.requires.include?(r.name)
          end
        end
    
        # check to see if the source element and the intersecting element
        # are of the same type
        duplicate_ids = intersecting_elements.any? { |r| r.name == source.name }
    
        # play an error sound if the requirements for interactions don't match,
        # or if duplicate elements are touching
        if !possible || duplicate_ids
          state.invalid_mixtures_queue << { ref_id: source.object_id, at: Kernel.tick_count }
          intersecting_elements.each do |r|
            state.invalid_mixtures_queue << { ref_id: r.object_id, at: Kernel.tick_count }
          end
        end
      end
    
      def calc
        calc_collision_bodies
        input_mouse
        calc_discovered_elements
        calc_queues
        calc_collision_bodies
      end
    
      def calc_queues
        # process the fade out queue
        state.fade_out_queue.each do |fx|
          fx.dx ||= 0.1
          fx.dy ||= 0.1
          fx.a ||= 255
          fx.a -= 5
          fx.x += fx.dx
          fx.y += fx.dy
          fx.w -= fx.dx * 2 if fx.w > 0
          fx.h -= fx.dy * 2 if fx.h > 0
          fx.dx *= 1.1
          fx.dy *= 1.1
        end
    
        state.fade_out_queue.reject! { |fx| fx.a <= 0 }
    
        # process the mouse particles queue
        state.mouse_particles_queue.each do |mp|
          mp.start_at ||= Kernel.tick_count
          mp.a ||= 255
          if mp.start_at < Kernel.tick_count
            mp.dx ||= 1.randomize(:ratio, :sign)
            mp.dy ||= 1.randomize(:ratio, :sign)
            mp.x += mp.dx
            mp.y += mp.dy
            mp.a -= 5
            mp.dx *= 1.05
            mp.dy *= 1.05
          end
        end
    
        state.mouse_particles_queue.reject! { |mp| mp.a <= 0 }
    
        state.invalid_mixtures_queue.reject! do |fx|
          fx.at.elapsed_time > 15
        end
      end
    
      def calc_discovered_elements
        groups = Element.create_groupings state.canvas.elements
    
        while groups.length > 0
          # pop a group of elements from the groups array
          group = groups.pop
    
          # for all the elements, get their names, this
          # represets the collection of elements that are
          # needed for other elements to be created (based on their requirements)
          keys = group.map { |g| g.name }
          completed_element = nil
    
          # for all elements, check their requires, and see if
          # the group of elements that are touching match
          state.elements.each do |r|
            if r.requires.uniq - keys == []
              completed_element = r
              break
            end
          end
    
          # if an element can be created, then remove the elements
          # that were used to create the element
          if completed_element
            to_remove = []
            completed_element.requires.each do |r|
              group.each do |g|
                if r == g.name
                  to_remove << g
                  break
                end
              end
            end
    
            # compute the general center of the cluster of elements
            min_x = to_remove.map { |i| i.position.x }.min
            min_y = to_remove.map { |i| i.position.y }.min
            max_x = to_remove.map { |i| i.position.x }.max
            max_y = to_remove.map { |i| i.position.y }.max
            avg_x = (min_x + max_x) / 2
            avg_y = (min_y + max_y) / 2
    
            # remove each used element from the canvas
            # fade them out, and add the new element to the canvas
            to_remove.each do |r|
              state.canvas.elements.reject! { |i| i == r }
              state.fade_out_queue << Element.prefab_icon(r, r.position)
    
              add_element_to_canvas!(completed_element,
                                     Element.tile_rect(x: avg_x, y: avg_y),
                                     fade_in: true)
            end
    
            # if the newly created element is not in the list of discovered elements
            # then add it to the list of discovered elements
            if state.discovered_elements.none? { |i| i.name == completed_element.name }
              state.discovered_elements << { name: completed_element.name, added_at: Kernel.tick_count }
            end
          end
        end
      end
    
      def calc_collision_bodies
        state.discovered_elements.each_with_index do |e, i|
          r = Layout::rect(row: i, col: 20, w: 1, h: 1)
          e.merge! rect: Layout::rect(row: i, col: 20, w: 1, h: 1),
                   position: r.slice(:x, :y)
        end
    
        state.canvas.elements.each do |e|
          r = Element.tile_rect(e.position)
          e.merge! rect: r,
                   position: r.slice(:x, :y)
        end
    
        if state.selected_element
          r = Element.tile_rect(x: inputs.mouse.position.x, y: inputs.mouse.position.y, anchor_x: 0.5, anchor_y: 0.5)
          state.selected_element.merge!(rect: r, position: r.slice(:x, :y))
        end
      end
    
      def render
        render_bg
        render_toolbar
        render_canvas_elements
        render_selected_element
        render_queues
      end
    
      def render_queues
        outputs.primitives << state.fade_out_queue
        outputs.primitives << state.mouse_particles_queue.reject { |mp| mp.start_at > Kernel.tick_count }
      end
    
      def render_selected_element
        # if an element is selected, render it at the mouse position
        if state.selected_element
          w, h = Layout::rect(w: 1, h: 1).values_at(:w, :h)
          outputs.primitives << Element.prefab(state.selected_element,
                                               x: inputs.mouse.x - w / 2,
                                               y: inputs.mouse.y - h / 2)
        end
      end
    
      def render_bg
        # black letterbox
        outputs.background_color = [0, 0, 0]
    
        # canvas area with lighter purple
        outputs.primitives << Layout::rect(row: 0, col:  0, w: 20, h: 12).merge(path: :solid, r: 59, g: 58, b: 97)
    
        # toolbar area with darker purple
        outputs.primitives << Layout::rect(row: 0, col: 20, w: 4, h: 12).merge(path: :solid, r: 59, g: 58, b: 80)
    
        # border around the canvas area
        outputs.primitives << state.canvas.rect.merge(primitive_marker: :border, r: 255, g: 255, b: 255)
      end
    
      def render_toolbar
        unique_elements = (state.elements.map { |r| r.name } +
                           state.discovered_elements.map { |r| r.name }).uniq
        outputs.primitives << unique_elements.length.map.with_index do |r, i|
          if i <= state.discovered_elements.length - 1
            nil
          else
            # for all undiscovered elements, create a placeholder question mark box
            Layout::rect(row: i, col: 20)
                   .yield_self do |r|
                     [
                       r.merge(primitive_marker: :border, r: 255, g: 255, b: 255),
                       r.center.merge(text: "?", anchor_x: 0.5, anchor_y: 0.5, r: 255, g: 255, b: 255)
                     ]
                   end
          end
        end
    
        # create a prefab for each discovered element
        outputs.primitives << state.discovered_elements.map.with_index do |r, i|
          hover = if inputs.mouse.intersect_rect? r.rect
                    r.rect.merge(path: :solid, r: 0, g: 80, b: 80, a: 100)
                  end
    
          [Element.prefab(r, r.position), hover]
        end
      end
    
      def render_canvas_elements
        if inputs.mouse.held && state.selected_element
          grouped_elements = Element.create_groupings(state.canvas.elements)
    
          # get all elements that are connected to the selected element
          # (ie intersecting with the mouse)
          connected_to_mouse = grouped_elements.find_all do |g|
            g.find { |e| Geometry::intersect_rect? state.selected_element.rect, e.rect }
          end.flatten
    
          outputs.primitives << state.canvas.elements.map do |element|
            is_part_of_invalid_mixture = state.invalid_mixtures_queue.any? { |i| i.ref_id == element.object_id }
    
            shift_x, shift_y = if is_part_of_invalid_mixture
                                 [5.randomize(:ratio, :sign), 5.randomize(:ratio, :sign)]
                               else
                                 [0, 0]
                               end
    
            pre = Element.prefab element, element.position, shift_x: shift_x, shift_y: shift_y
            # if the element that is about to be rendered is connected to the selected element
            # then render it with a hover effect
            hover = if state.selected_element && connected_to_mouse.any? { |i| i == element }
                      element.rect.merge(path: :solid, r: 0, g: 80, b: 80, a: 100)
                    end
            [pre, hover]
          end
        else
          # hover effect for mouse intersecting topmost element
          mouse_intersecting_element = if !inputs.mouse.held
                                         state.canvas.elements.reverse.find do |element|
                                           Geometry::intersect_rect? inputs.mouse, element.rect
                                         end
                                       end
    
          outputs.primitives << state.canvas.elements.map do |element|
            is_part_of_invalid_mixture = state.invalid_mixtures_queue.any? { |i| i.ref_id == element.object_id }
    
            shift_x, shift_y = if is_part_of_invalid_mixture
                                 [5.randomize(:ratio, :sign), 5.randomize(:ratio, :sign)]
                               else
                                 [0, 0]
                               end
    
            pre = Element.prefab element, element.position, shift_x: shift_x, shift_y: shift_y
            hover = if mouse_intersecting_element == element
                      element.rect.merge(path: :solid, r: 0, g: 80, b: 80, a: 100)
                    end
            [pre, hover]
          end
        end
      end
    end
    
    $game = Game.new
    def tick args
      $game.args = args
      $game.tick
    end
    
    

    Craft Game Starting Point - main.rb link

    # ./samples/99_genre_crafting/craft_game_starting_point/app/main.rb
    # ==================================================
    # A NOTE TO JAM CRAFT PARTICIPANTS:
    # The comments and code in here are just as small piece of DragonRuby's capabilities.
    # Be sure to check out the rest of the sample apps. Start with README.txt and go from there!
    # ==================================================
    
    # def tick args is the entry point into your game. This function is called at
    # a fixed update time of 60hz (60 fps).
    def tick args
      # The defaults function intitializes the game.
      defaults args
    
      # After the game is initialized, render it.
      render args
    
      # After rendering the player should be able to respond to input.
      input args
    
      # After responding to input, the game performs any additional calculations.
      calc args
    end
    
    def defaults args
      # hide the mouse cursor for this game, we are going to render our own cursor
      if Kernel.tick_count == 0
        args.gtk.hide_cursor
      end
    
      args.state.click_ripples ||= []
    
      # everything is on a 1280x720 virtual canvas, so you can
      # hardcode locations
    
      # define the borders for where the inventory is located
      # args.state is a data structure that accepts any arbitrary parameters
      # so you can create an object graph without having to create any classes.
    
      # Bottom left is 0, 0. Top right is 1280, 720.
      # The inventory area is at the top of the screen
      # the number 80 is the size of all the sprites, so that is what is being
      # used to decide the with and height
      args.state.sprite_size = 80
    
      args.state.inventory_border.w  = args.state.sprite_size * 10
      args.state.inventory_border.h  = args.state.sprite_size * 3
      args.state.inventory_border.x  = 10
      args.state.inventory_border.y  = 710 - args.state.inventory_border.h
    
      # define the borders for where the crafting area is located
      # the crafting area is below the inventory area
      # the number 80 is the size of all the sprites, so that is what is being
      # used to decide the with and height
      args.state.craft_border.x =  10
      args.state.craft_border.y = 220
      args.state.craft_border.w = args.state.sprite_size * 3
      args.state.craft_border.h = args.state.sprite_size * 3
    
      # define the area where results are located
      # the crafting result is to the right of the craft area
      args.state.result_border.x =  10 + args.state.sprite_size * 3 + args.state.sprite_size
      args.state.result_border.y = 220 + args.state.sprite_size
      args.state.result_border.w = args.state.sprite_size
      args.state.result_border.h = args.state.sprite_size
    
      # initialize items for the first time if they are nil
      # you start with 15 wood, 1 chest, and 5 plank
      # Ruby has built in syntax for dictionaries (they look a lot like json objects).
      # Ruby also has a special type called a Symbol denoted with a : followed by a word.
      # Symbols are nice because they remove the need for magic strings.
      if !args.state.items
        args.state.items = [
          {
            id: :wood, # :wood is a Symbol, this is better than using "wood" for the id
            quantity: 15,
            path: 'sprites/wood.png',
            location: :inventory,
            ordinal_x: 0, ordinal_y: 0
          },
          {
            id: :chest,
            quantity: 1,
            path: 'sprites/chest.png',
            location: :inventory,
            ordinal_x: 1, ordinal_y: 0
          },
          {
            id: :plank,
            quantity: 5,
            path: 'sprites/plank.png',
            location: :inventory,
            ordinal_x: 2, ordinal_y: 0
          },
        ]
    
        # after initializing the oridinal positions, derive the pixel
        # locations assuming that the width and height are 80
        args.state.items.each { |item| set_inventory_position args, item }
      end
    
      # define all the oridinal positions of the inventory slots
      if !args.state.inventory_area
        args.state.inventory_area = [
          { ordinal_x: 0,  ordinal_y: 0 },
          { ordinal_x: 1,  ordinal_y: 0 },
          { ordinal_x: 2,  ordinal_y: 0 },
          { ordinal_x: 3,  ordinal_y: 0 },
          { ordinal_x: 4,  ordinal_y: 0 },
          { ordinal_x: 5,  ordinal_y: 0 },
          { ordinal_x: 6,  ordinal_y: 0 },
          { ordinal_x: 7,  ordinal_y: 0 },
          { ordinal_x: 8,  ordinal_y: 0 },
          { ordinal_x: 9,  ordinal_y: 0 },
          { ordinal_x: 0,  ordinal_y: 1 },
          { ordinal_x: 1,  ordinal_y: 1 },
          { ordinal_x: 2,  ordinal_y: 1 },
          { ordinal_x: 3,  ordinal_y: 1 },
          { ordinal_x: 4,  ordinal_y: 1 },
          { ordinal_x: 5,  ordinal_y: 1 },
          { ordinal_x: 6,  ordinal_y: 1 },
          { ordinal_x: 7,  ordinal_y: 1 },
          { ordinal_x: 8,  ordinal_y: 1 },
          { ordinal_x: 9,  ordinal_y: 1 },
          { ordinal_x: 0,  ordinal_y: 2 },
          { ordinal_x: 1,  ordinal_y: 2 },
          { ordinal_x: 2,  ordinal_y: 2 },
          { ordinal_x: 3,  ordinal_y: 2 },
          { ordinal_x: 4,  ordinal_y: 2 },
          { ordinal_x: 5,  ordinal_y: 2 },
          { ordinal_x: 6,  ordinal_y: 2 },
          { ordinal_x: 7,  ordinal_y: 2 },
          { ordinal_x: 8,  ordinal_y: 2 },
          { ordinal_x: 9,  ordinal_y: 2 },
        ]
    
        # after initializing the oridinal positions, derive the pixel
        # locations assuming that the width and height are 80
        args.state.inventory_area.each { |i| set_inventory_position args, i }
    
        # if you want to see the result you can use the Ruby function called "puts".
        # Uncomment this line to see the value.
        # puts args.state.inventory_area
    
        # You can see all things written via puts in DragonRuby's Console, or under logs/log.txt.
        # To bring up DragonRuby's Console, press the ~ key within the game.
      end
    
      # define all the oridinal positions of the craft slots
      if !args.state.craft_area
        args.state.craft_area = [
          { ordinal_x: 0, ordinal_y: 0 },
          { ordinal_x: 0, ordinal_y: 1 },
          { ordinal_x: 0, ordinal_y: 2 },
          { ordinal_x: 1, ordinal_y: 0 },
          { ordinal_x: 1, ordinal_y: 1 },
          { ordinal_x: 1, ordinal_y: 2 },
          { ordinal_x: 2, ordinal_y: 0 },
          { ordinal_x: 2, ordinal_y: 1 },
          { ordinal_x: 2, ordinal_y: 2 },
        ]
    
        # after initializing the oridinal positions, derive the pixel
        # locations assuming that the width and height are 80
        args.state.craft_area.each { |c| set_craft_position args, c }
      end
    end
    
    
    def render args
      # for the results area, create a sprite that show its boundaries
      args.outputs.primitives << { x: args.state.result_border.x,
                                   y: args.state.result_border.y,
                                   w: args.state.result_border.w,
                                   h: args.state.result_border.h,
                                   path: 'sprites/border-black.png' }
    
      # for each inventory spot, create a sprite
      # args.outputs.primitives is how DragonRuby performs a render.
      # Adding a single hash or multiple hashes to this array will tell
      # DragonRuby to render those primitives on that frame.
    
      # The .map function on Array is used instead of any kind of looping.
      # .map returns a new object for every object within an Array.
      args.outputs.primitives << args.state.inventory_area.map do |a|
        { x: a.x, y: a.y, w: a.w, h: a.h, path: 'sprites/border-black.png' }
      end
    
      # for each craft spot, create a sprite
      args.outputs.primitives << args.state.craft_area.map do |a|
        { x: a.x, y: a.y, w: a.w, h: a.h, path: 'sprites/border-black.png' }
      end
    
      # after the borders have been rendered, render the
      # items within those slots (and allow for highlighting)
      # if an item isn't currently being held
      allow_inventory_highlighting = !args.state.held_item
    
      # go through each item and render them
      # use Array's find_all method to remove any items that are currently being held
      args.state.items.find_all { |item| item[:location] != :held }.map do |item|
        # if an item is currently being held, don't render it in it's spot within the
        # inventory or craft area (this is handled via the find_all method).
    
        # the item_prefab returns a hash containing all the visual components of an item.
        # the main sprite, the black background, the quantity text, and a hover indication
        # if the mouse is currently hovering over the item.
        args.outputs.primitives << item_prefab(args, item, allow_inventory_highlighting, args.inputs.mouse)
      end
    
      # The last thing we want to render is the item currently being held.
      args.outputs.primitives << item_prefab(args, args.state.held_item, allow_inventory_highlighting, args.inputs.mouse)
    
      args.outputs.primitives << args.state.click_ripples
    
      # render a mouse cursor since we have the OS cursor hidden
      args.outputs.primitives << { x: args.inputs.mouse.x - 5, y: args.inputs.mouse.y - 5, w: 10, h: 10, path: 'sprites/circle-gray.png', a: 128 }
    end
    
    # Alrighty! This is where all the fun happens
    def input args
      # if the mouse is clicked and not item is currently being held
      # args.state.held_item is nil when the game starts.
      # If the player clicks, the property args.inputs.mouse.click will
      # be a non nil value, we don't want to process any of the code here
      # if the mouse hasn't been clicked
      return if !args.inputs.mouse.click
    
      # if a click occurred, add a ripple to the ripple queue
      args.state.click_ripples << { x: args.inputs.mouse.x - 5, y: args.inputs.mouse.y - 5, w: 10, h: 10, path: 'sprites/circle-gray.png', a: 128 }
    
      # if the mouse has been clicked, and no item is currently held...
      if !args.state.held_item
        # see if any of the items intersect the pointer using the inside_rect? method
        # the find method will either return the first object that returns true
        # for the match clause, or it'll return nil if nothing matches the match clause
        found = args.state.items.find do |item|
          # for each item in args.state.items, run the following boolean check
          args.inputs.mouse.click.point.inside_rect?(item)
        end
    
        # if an item intersects the mouse pointer, then set the item's location to :held and
        # set args.state.held_item to the item for later reference
        if found
          args.state.held_item = found
          found[:location] = :held
        end
    
      # if the mouse is clicked and an item is currently beign held....
      elsif args.state.held_item
        # determine if a slot within the craft area was clicked
        craft_area = args.state.craft_area.find { |a| args.inputs.mouse.click.point.inside_rect? a }
    
        # also determine if a slot within the inventory area was clicked
        inventory_area = args.state.inventory_area.find { |a| args.inputs.mouse.click.point.inside_rect? a }
    
        # if the click was within a craft area
        if craft_area
          # check to see if an item is already there and ignore the click if an item is found
          # item_at_craft_slot is a helper method that returns an item or nil for a given oridinal
          # position
          item_already_there = item_at_craft_slot args, craft_area[:ordinal_x], craft_area[:ordinal_y]
    
          # if an item *doesn't* exist in the craft area
          if !item_already_there
            # if the quantity they are currently holding is greater than 1
            if args.state.held_item[:quantity] > 1
              # remove one item (creating a seperate item of the same type), and place it
              # at the oridinal position and location of the craft area
              # the .merge method on Hash creates a new Hash, but updates any values
              # passed as arguments to merge
              new_item = args.state.held_item.merge(quantity: 1,
                                                    location: :craft,
                                                    ordinal_x: craft_area[:ordinal_x],
                                                    ordinal_y: craft_area[:ordinal_y])
    
              # after the item is crated, place it into the args.state.items collection
              args.state.items << new_item
    
              # then subtract one from the held item
              args.state.held_item[:quantity] -= 1
    
            # if the craft area is available and there is only one item being held
            elsif args.state.held_item[:quantity] == 1
              # instead of creating any new items just set the location of the held item
              # to the oridinal position of the craft area, and then nil out the
              # held item state so that a new item can be picked up
              args.state.held_item[:location] = :craft
              args.state.held_item[:ordinal_x] = craft_area[:ordinal_x]
              args.state.held_item[:ordinal_y] = craft_area[:ordinal_y]
              args.state.held_item = nil
            end
          end
    
        # if the selected area is an inventory area (as opposed to within the craft area)
        elsif inventory_area
    
          # check to see if there is already an item in that inventory slot
          # the item_at_inventory_slot helper method returns an item or nil
          item_already_there = item_at_inventory_slot args, inventory_area[:ordinal_x], inventory_area[:ordinal_y]
    
          # if there is already an item there, and the item types/id match
          if item_already_there && item_already_there[:id] == args.state.held_item[:id]
            # then merge the item quantities
            held_quantity = args.state.held_item[:quantity]
            item_already_there[:quantity] += held_quantity
    
            # remove the item being held from the items collection (since it's quantity is now 0)
            args.state.items.reject! { |i| i[:location] == :held }
    
            # nil out the held_item so a new item can be picked up
            args.state.held_item = nil
    
          # if there currently isn't an item there, then put the held item in the slot
          elsif !item_already_there
            args.state.held_item[:location] = :inventory
            args.state.held_item[:ordinal_x] = inventory_area[:ordinal_x]
            args.state.held_item[:ordinal_y] = inventory_area[:ordinal_y]
    
            # nil out the held_item so a new item can be picked up
            args.state.held_item = nil
          end
        end
      end
    end
    
    # the calc method is executed after input
    def calc args
      # make sure that the real position of the inventory
      # items are updated every frame to ensure that they
      # are placed correctly given their location and oridinal positions
      # instead of using .map, here we use .each (since we are not returning a new item and just updating the items in place)
      args.state.items.each do |item|
        # based on the location of the item, invoke the correct pixel conversion method
        if item[:location] == :inventory
          set_inventory_position args, item
        elsif item[:location] == :craft
          set_craft_position args, item
        elsif item[:location] == :held
          # if the item is held, center the item around the mouse pointer
          args.state.held_item.x = args.inputs.mouse.x - args.state.held_item.w.half
          args.state.held_item.y = args.inputs.mouse.y - args.state.held_item.h.half
        end
      end
    
      # for each hash/sprite in the click ripples queue,
      # expand its size by 20 percent and decrease its alpha
      # by 10.
      args.state.click_ripples.each do |ripple|
        delta_w = ripple.w * 1.2 - ripple.w
        delta_h = ripple.h * 1.2 - ripple.h
        ripple.x -= delta_w.half
        ripple.y -= delta_h.half
        ripple.w += delta_w
        ripple.h += delta_h
        ripple.a -= 10
      end
    
      # remove any items from the collection where the alpha value is less than equal to
      # zero using the reject! method (reject with an exclamation point at the end changes the
      # array value in place, while reject without the exclamation point returns a new array).
      args.state.click_ripples.reject! { |ripple| ripple.a <= 0 }
    end
    
    # helper function for finding an item at a craft slot
    def item_at_craft_slot args, ordinal_x, ordinal_y
      args.state.items.find { |i| i[:location] == :craft && i[:ordinal_x] == ordinal_x && i[:ordinal_y] == ordinal_y }
    end
    
    # helper function for finding an item at an inventory slot
    def item_at_inventory_slot args, ordinal_x, ordinal_y
      args.state.items.find { |i| i[:location] == :inventory && i[:ordinal_x] == ordinal_x && i[:ordinal_y] == ordinal_y }
    end
    
    # helper function that creates a visual representation of an item
    def item_prefab args, item, should_highlight, mouse
      return nil unless item
    
      overlay = nil
    
      x = item.x
      y = item.y
      w = item.w
      h = item.h
    
      if should_highlight && mouse.point.inside_rect?(item)
        overlay = { x: x, y: y, w: w, h: h, path: "sprites/square-blue.png", a: 130, }
      end
    
      [
        # sprites are hashes with a path property, this is the main sprite
        { x: x,      y: y, w: args.state.sprite_size, h: args.state.sprite_size, path: item[:path], },
    
        # this represents the black area in the bottom right corner of the main sprite so that the
        # quantity is visible
        { x: x + 55, y: y, w: 25, h: 25, path: "sprites/square-black.png", }, # sprites are hashes with a path property
    
        # labels are hashes with a text property
        { x: x + 56, y: y + 22, text: "#{item[:quantity]}", r: 255, g: 255, b: 255, },
    
        # this is the mouse overlay, if the overlay isn't applicable, then this value will be nil (nil values will not be rendered)
        overlay
      ]
    end
    
    # helper function for deriving the position of an item within inventory
    def set_inventory_position args, item
      item.x = args.state.inventory_border.x + item[:ordinal_x] * 80
      item.y = (args.state.inventory_border.y + args.state.inventory_border.h - 80) - item[:ordinal_y] * 80
      item.w = 80
      item.h = 80
    end
    
    # helper function for deriving the position of an item within the craft area
    def set_craft_position args, item
      item.x = args.state.craft_border.x + item[:ordinal_x] * 80
      item.y = (args.state.craft_border.y + args.state.inventory_border.h - 80) - item[:ordinal_y] * 80
      item.w = 80
      item.h = 80
    end
    
    # Any lines outside of a function will be executed when the file is reloaded.
    # So every time you save main.rb, the game will be reset.
    # Comment out the line below if you don't want this to happen.
    $gtk.reset
    
    

    Farming Game Starting Point - main.rb link

    # ./samples/99_genre_crafting/farming_game_starting_point/app/main.rb
    def tick args
      args.state.tile_size     = 80
      args.state.player_speed  = 4
      args.state.player      ||= tile(args, 7, 3, 0, 128, 180)
      generate_map args
      #press j to plant a green onion
      if args.inputs.keyboard.j
      #change this part you can change what you want to plant
       args.state.walls << tile(args, ((args.state.player.x+80)/args.state.tile_size), ((args.state.player.y)/args.state.tile_size), 255, 255, 255)
       args.state.plants << tile(args, ((args.state.player.x+80)/args.state.tile_size), ((args.state.player.y+80)/args.state.tile_size), 0, 160, 0)
      end
      # Adds walls, background, and player to args.outputs.solids so they appear on screen
      args.outputs.solids << [0,0,1280,720, 237,189,101]
      args.outputs.sprites << [0, 0, 1280, 720, 'sprites/background.png']
      args.outputs.solids << args.state.walls
      args.outputs.solids << args.state.player
      args.outputs.solids << args.state.plants
      args.outputs.labels << [320, 640, "press J to plant", 3, 1, 255, 0, 0, 200]
    
      move_player args, -1,  0 if args.inputs.keyboard.left # x position decreases by 1 if left key is pressed
      move_player args,  1,  0 if args.inputs.keyboard.right # x position increases by 1 if right key is pressed
      move_player args,  0,  1 if args.inputs.keyboard.up # y position increases by 1 if up is pressed
      move_player args,  0, -1 if args.inputs.keyboard.down # y position decreases by 1 if down is pressed
    end
    
    # Sets position, size, and color of the tile
    def tile args, x, y, *color
      [x * args.state.tile_size, # sets definition for array using method parameters
       y * args.state.tile_size, # multiplying by tile_size sets x and y to correct position using pixel values
       args.state.tile_size,
       args.state.tile_size,
       *color]
    end
    
    # Creates map by adding tiles to the wall, as well as a goal (that the player needs to reach)
    def generate_map args
      return if args.state.area
    
      # Creates the area of the map. There are 9 rows running horizontally across the screen
      # and 16 columns running vertically on the screen. Any spot with a "1" is not
      # open for the player to move into (and is green), and any spot with a "0" is available
      # for the player to move in.
      args.state.area = [
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
      ].reverse # reverses the order of the area collection
    
      # By reversing the order, the way that the area appears above is how it appears
      # on the screen in the game. If we did not reverse, the map would appear inverted.
    
      #The wall starts off with no tiles.
      args.state.walls = []
      args.state.plants = []
    
      # If v is 1, a green tile is added to args.state.walls.
      # If v is 2, a black tile is created as the goal.
      args.state.area.map_2d do |y, x, v|
        if    v == 1
          args.state.walls << tile(args, x, y, 255, 160, 156) # green tile
        end
      end
    end
    
    # Allows the player to move their box around the screen
    def move_player args, *vector
      box = args.state.player.shift_rect(vector) # box is able to move at an angle
    
      # If the player's box hits a wall, it is not able to move further in that direction
      return if args.state.walls
                    .any_intersect_rect?(box)
    
      # Player's box is able to move at angles (not just the four general directions) fast
      args.state.player =
        args.state.player
            .shift_rect(vector.x * args.state.player_speed, # if we don't multiply by speed, then
                        vector.y * args.state.player_speed) # the box will move extremely slow
    end
    
    

    Genre Dev Tools link

    Add Buttons To Console - main.rb link

    # ./samples/99_genre_dev_tools/add_buttons_to_console/app/main.rb
    # You can customize the buttons that show up in the Console.
    class GTK::Console::Menu
      # STEP 1: Override the custom_buttons function.
      def custom_buttons
        [
          (button id: :yay,
                  # row for button
                  row: 2,
                  # column for button
                  col: 18,
                  # text
                  text: "I AM CUSTOM",
                  # when clicked call the custom_button_clicked function
                  method: :custom_button_clicked),
    
          (button id: :yay,
                  # row for button
                  row: 2,
                  # column for button
                  col: 16,
                  # text
                  text: "CUSTOM ALSO",
                  # when clicked call the custom_button_also_clicked function
                  method: :custom_button_also_clicked)
        ]
      end
    
      # STEP 2: Define the function that should be called.
      def custom_button_clicked
        log "* INFO: I AM CUSTOM was clicked!"
      end
    
      def custom_button_also_clicked
        log "* INFO: Custom Button Clicked at #{Kernel.global_tick_count}!"
    
        all_buttons_as_string = $gtk.console.menu.buttons.map do |b|
          <<-S.strip
    ** id: #{b[:id]}
    :PROPERTIES:
    :id:     :#{b[:id]}
    :method: :#{b[:method]}
    :text:   #{b[:text]}
    :END:
    S
        end.join("\n")
    
        log <<-S
    * INFO: Here are all the buttons:
    #{all_buttons_as_string}
    S
      end
    end
    
    def tick args
      args.outputs.labels << [args.grid.center.x, args.grid.center.y,
                              "Open the DragonRuby Console to see the custom menu items.",
                              0, 1]
    end
    
    

    Animation Creator Starting Point - main.rb link

    # ./samples/99_genre_dev_tools/animation_creator_starting_point/app/main.rb
    class OneBitLowrezPaint
      attr_gtk
    
      def tick
        outputs.background_color = [0, 0, 0]
        defaults
        render_instructions
        render_canvas
        render_buttons_frame_selection
        render_animation_frame_thumbnails
        render_animation
        input_mouse_click
        input_keyboard
        calc_auto_export
        calc_buttons_frame_selection
        calc_animation_frames
        process_queue_create_sprite
        process_queue_reset_sprite
        process_queue_update_rt_animation_frame
      end
    
      def defaults
        state.animation_frames_per_second = 12
        queues.create_sprite ||= []
        queues.reset_sprite ||= []
        queues.update_rt_animation_frame ||= []
    
        if !state.animation_frames
          state.animation_frames ||= []
          add_animation_frame_to_end
        end
    
        state.last_mouse_down ||= 0
        state.last_mouse_up   ||= 0
    
        state.buttons_frame_selection.left = 10
        state.buttons_frame_selection.top  = grid.top - 10
        state.buttons_frame_selection.size = 20
        state.buttons_frame_selection.items ||= []
    
        defaults_canvas_sprite
    
        state.edit_mode ||= :drawing
      end
    
      def defaults_canvas_sprite
        rt_canvas.size   = 16
        rt_canvas.zoom   = 30
        rt_canvas.width  = rt_canvas.size * rt_canvas.zoom
        rt_canvas.height = rt_canvas.size * rt_canvas.zoom
        rt_canvas.sprite = { x: 0,
                             y: 0,
                             w: rt_canvas.width,
                             h: rt_canvas.height,
                             path: :rt_canvas }.center_inside_rect(x: 0, y: 0, w: 640, h: 720)
    
        return unless Kernel.tick_count == 1
    
        outputs[:rt_canvas].width      = rt_canvas.width
        outputs[:rt_canvas].height     = rt_canvas.height
        outputs[:rt_canvas].sprites   << (rt_canvas.size + 1).map_with_index do |x|
          (rt_canvas.size + 1).map_with_index do |y|
            path = 'sprites/square-white.png'
            path = 'sprites/square-blue.png' if x == 7 || x == 8
            { x: x * rt_canvas.zoom,
              y: y * rt_canvas.zoom,
              w: rt_canvas.zoom,
              h: rt_canvas.zoom,
              path: path,
              a: 50 }
          end
        end
      end
    
      def render_instructions
        instructions = [
          "* Hotkeys:",
          "- d: hold to erase, release to draw.",
          "- a: add frame.",
          "- c: copy frame.",
          "- v: paste frame.",
          "- x: delete frame.",
          "- b: go to previous frame.",
          "- f: go to next frame.",
          "- w: save to ./canvas directory.",
          "- l: load from ./canvas."
        ]
    
        instructions.each.with_index do |l, i|
          outputs.labels << { x: 840, y: 500 - (i * 20), text: "#{l}",
                              r: 180, g: 180, b: 180, size_enum: 0 }
        end
      end
    
      def render_canvas
        return if Kernel.tick_count.zero?
        outputs.sprites << rt_canvas.sprite
      end
    
      def render_buttons_frame_selection
        args.outputs.primitives << state.buttons_frame_selection.items.map_with_index do |b, i|
          label = { x: b.x + state.buttons_frame_selection.size.half,
                    y: b.y,
                    text: "#{i + 1}", r: 180, g: 180, b: 180,
                    size_enum: -4, alignment_enum: 1 }.label!
    
          selection_border = b.merge(r: 40, g: 40, b: 40).border!
    
          if i == state.animation_frames_selected_index
            selection_border = b.merge(r: 40, g: 230, b: 200).border!
          end
    
          [selection_border, label]
        end
      end
    
      def render_animation_frame_thumbnails
        return if Kernel.tick_count.zero?
    
        outputs[:current_animation_frame].width   = rt_canvas.size
        outputs[:current_animation_frame].height  = rt_canvas.size
        outputs[:current_animation_frame].solids <<  selected_animation_frame.pixels.map_with_index do |f, i|
          { x: f.x,
            y: f.y,
            w: 1,
            h: 1, r: 255, g: 255, b: 255 }
        end
    
        outputs.sprites << rt_canvas.sprite.merge(path: :current_animation_frame)
    
        state.animation_frames.map_with_index do |animation_frame, animation_frame_index|
          outputs.sprites << state.buttons_frame_selection.items[animation_frame_index].inner_rect
                                  .merge(path: animation_frame.rt_name)
        end
      end
    
      def render_animation
        sprite_index = 0.frame_index count: state.animation_frames.length,
                                     hold_for: 60 / state.animation_frames_per_second,
                                     repeat: true
    
        args.outputs.sprites << { x: 700 - 8,
                                  y: 120,
                                  w: 16,
                                  h: 16,
                                  path: (sprite_path sprite_index) }
    
        args.outputs.sprites << { x: 700 - 16,
                                  y: 230,
                                  w: 32,
                                  h: 32,
                                  path: (sprite_path sprite_index) }
    
        args.outputs.sprites << { x: 700 - 32,
                                  y: 360,
                                  w: 64,
                                  h: 64,
                                  path: (sprite_path sprite_index) }
    
        args.outputs.sprites << { x: 700 - 64,
                                  y: 520,
                                  w: 128,
                                  h: 128,
                                  path: (sprite_path sprite_index) }
      end
    
      def input_mouse_click
        if inputs.mouse.up
          state.last_mouse_up = Kernel.tick_count
        elsif inputs.mouse.moved && user_is_editing?
          edit_current_animation_frame inputs.mouse.point
        end
    
        return unless inputs.mouse.click
    
        clicked_frame_button = state.buttons_frame_selection.items.find do |b|
          inputs.mouse.point.inside_rect? b
        end
    
        if (clicked_frame_button)
          state.animation_frames_selected_index = clicked_frame_button.index
        end
    
        if (inputs.mouse.point.inside_rect? rt_canvas.sprite)
          state.last_mouse_down = Kernel.tick_count
          edit_current_animation_frame inputs.mouse.point
        end
      end
    
      def input_keyboard
        # w to save
        if inputs.keyboard.key_down.w
          t = Time.now
          state.save_description = "Time: #{t} (#{t.to_i})"
          gtk.serialize_state 'canvas/state.txt', state
          gtk.serialize_state "tmp/canvas_backups/#{t.to_i}/state.txt", state
          animation_frames.each_with_index do |animation_frame, i|
            queues.update_rt_animation_frame << { index: i,
                                                  at: Kernel.tick_count + i,
                                                  queue_sprite_creation: true }
            queues.create_sprite << { index: i,
                                      at: Kernel.tick_count + animation_frames.length + i,
                                      path_override: "tmp/canvas_backups/#{t.to_i}/sprite-#{i}.png" }
          end
          gtk.notify! "Canvas saved."
        end
    
        # l to load
        if inputs.keyboard.key_down.l
          args.state = gtk.deserialize_state 'canvas/state.txt'
          animation_frames.each_with_index do |a, i|
            queues.update_rt_animation_frame << { index: i,
                                                  at: Kernel.tick_count + i,
                                                  queue_sprite_creation: true }
          end
          gtk.notify! "Canvas loaded."
        end
    
        # d to go into delete mode, release to paint
        if inputs.keyboard.key_held.d
          state.edit_mode = :erasing
          gtk.notify! "Erasing." if inputs.keyboard.key_held.d == (Kernel.tick_count - 1)
        elsif inputs.keyboard.key_up.d
          state.edit_mode = :drawing
          gtk.notify! "Drawing."
        end
    
        # a to add a frame to the end
        if inputs.keyboard.key_down.a
          queues.create_sprite << { index: state.animation_frames_selected_index,
                                    at: Kernel.tick_count }
          queues.create_sprite << { index: state.animation_frames_selected_index + 1,
                                    at: Kernel.tick_count }
          add_animation_frame_to_end
          gtk.notify! "Frame added to end."
        end
    
        # c or t to copy
        if (inputs.keyboard.key_down.c || inputs.keyboard.key_down.t)
          state.clipboard = [selected_animation_frame.pixels].flatten
          gtk.notify! "Current frame copied."
        end
    
        # v or q to paste
        if (inputs.keyboard.key_down.v || inputs.keyboard.key_down.q) && state.clipboard
          selected_animation_frame.pixels = [state.clipboard].flatten
          queues.update_rt_animation_frame << { index: state.animation_frames_selected_index,
                                                at: Kernel.tick_count,
                                                queue_sprite_creation: true }
          gtk.notify! "Pasted."
        end
    
        # f to go forward/next frame
        if (inputs.keyboard.key_down.f)
          if (state.animation_frames_selected_index == (state.animation_frames.length - 1))
            state.animation_frames_selected_index = 0
          else
            state.animation_frames_selected_index += 1
          end
          gtk.notify! "Next frame."
        end
    
        # b to go back/previous frame
        if (inputs.keyboard.key_down.b)
          if (state.animation_frames_selected_index == 0)
            state.animation_frames_selected_index = state.animation_frames.length - 1
          else
            state.animation_frames_selected_index -= 1
          end
          gtk.notify! "Previous frame."
        end
    
        # x to delete frame
        if (inputs.keyboard.key_down.x) && animation_frames.length > 1
          state.clipboard = selected_animation_frame.pixels
          state.animation_frames = animation_frames.find_all { |v| v.index != state.animation_frames_selected_index }
          if state.animation_frames_selected_index >= state.animation_frames.length
            state.animation_frames_selected_index = state.animation_frames.length - 1
          end
          gtk.notify! "Frame deleted."
        end
      end
    
      def calc_auto_export
        return if user_is_editing?
        return if state.last_mouse_up.elapsed_time != 30
        # auto export current animation frame if there is no editing for 30 ticks
        queues.create_sprite << { index: state.animation_frames_selected_index,
                                  at: Kernel.tick_count }
      end
    
      def calc_buttons_frame_selection
        state.buttons_frame_selection.items = animation_frames.length.map_with_index do |i|
          { x: state.buttons_frame_selection.left + i * state.buttons_frame_selection.size,
            y: state.buttons_frame_selection.top - state.buttons_frame_selection.size,
            inner_rect: {
              x: (state.buttons_frame_selection.left + 2) + i * state.buttons_frame_selection.size,
              y: (state.buttons_frame_selection.top - state.buttons_frame_selection.size + 2),
              w: 16,
              h: 16,
            },
            w: state.buttons_frame_selection.size,
            h: state.buttons_frame_selection.size,
            index: i }
        end
      end
    
      def calc_animation_frames
        animation_frames.each_with_index do |animation_frame, i|
          animation_frame.index = i
          animation_frame.rt_name = "animation_frame_#{i}"
        end
      end
    
      def process_queue_create_sprite
        sprites_to_create = queues.create_sprite
                                  .find_all { |h| h.at.elapsed? }
    
        queues.create_sprite = queues.create_sprite - sprites_to_create
    
        sprites_to_create.each do |h|
          export_animation_frame h.index, h.path_override
        end
      end
    
      def process_queue_reset_sprite
        sprites_to_reset = queues.reset_sprite
                                 .find_all { |h| h.at.elapsed? }
    
        queues.reset_sprite -= sprites_to_reset
    
        sprites_to_reset.each { |h| gtk.reset_sprite (sprite_path h.index) }
      end
    
      def process_queue_update_rt_animation_frame
        animation_frames_to_update = queues.update_rt_animation_frame
                                           .find_all { |h| h.at.elapsed? }
    
        queues.update_rt_animation_frame -= animation_frames_to_update
    
        animation_frames_to_update.each do |h|
          update_animation_frame_render_target animation_frames[h.index]
    
          if h.queue_sprite_creation
            queues.create_sprite << { index: h.index,
                                      at: Kernel.tick_count + 1 }
          end
        end
      end
    
      def update_animation_frame_render_target animation_frame
        return if !animation_frame
    
        outputs[animation_frame.rt_name].width   = state.rt_canvas.size
        outputs[animation_frame.rt_name].height  = state.rt_canvas.size
        outputs[animation_frame.rt_name].solids << animation_frame.pixels.map do |f|
          { x: f.x,
            y: f.y,
            w: 1,
            h: 1, r: 255, g: 255, b: 255 }
        end
      end
    
      def animation_frames
        state.animation_frames
      end
    
      def add_animation_frame_to_end
        animation_frames << {
          index: animation_frames.length,
          pixels: [],
          rt_name: "animation_frame_#{animation_frames.length}"
        }
    
        state.animation_frames_selected_index = (animation_frames.length - 1)
        queues.update_rt_animation_frame << { index: state.animation_frames_selected_index,
                                              at: Kernel.tick_count,
                                              queue_sprite_creation: true }
      end
    
      def sprite_path i
        "canvas/sprite-#{i}.png"
      end
    
      def export_animation_frame i, path_override = nil
        return if !state.animation_frames[i]
    
        outputs.screenshots << state.buttons_frame_selection
                                    .items[i].inner_rect
                                    .merge(path: path_override || (sprite_path i))
    
        outputs.screenshots << state.buttons_frame_selection
                                    .items[i].inner_rect
                                    .merge(path: "tmp/sprite_backups/#{Time.now.to_i}-sprite-#{i}.png")
    
        queues.reset_sprite << { index: i, at: Kernel.tick_count }
      end
    
      def selected_animation_frame
        state.animation_frames[state.animation_frames_selected_index]
      end
    
      def edit_current_animation_frame point
        draw_area_point = (to_draw_area point)
        if state.edit_mode == :drawing && (!selected_animation_frame.pixels.include? draw_area_point)
          selected_animation_frame.pixels << draw_area_point
          queues.update_rt_animation_frame << { index: state.animation_frames_selected_index,
                                                at: Kernel.tick_count,
                                                queue_sprite_creation: !user_is_editing? }
        elsif state.edit_mode == :erasing && (selected_animation_frame.pixels.include? draw_area_point)
          selected_animation_frame.pixels = selected_animation_frame.pixels.reject { |p| p == draw_area_point }
          queues.update_rt_animation_frame << { index: state.animation_frames_selected_index,
                                                at: Kernel.tick_count,
                                                queue_sprite_creation: !user_is_editing? }
        end
      end
    
      def user_is_editing?
        state.last_mouse_down > state.last_mouse_up
      end
    
      def to_draw_area point
        x, y = point.x, point.y
        x -= rt_canvas.sprite.x
        y -= rt_canvas.sprite.y
        { x: x.idiv(rt_canvas.zoom),
          y: y.idiv(rt_canvas.zoom) }
      end
    
      def rt_canvas
        state.rt_canvas ||= state.new_entity(:rt_canvas)
      end
    
      def queues
        state.queues ||= state.new_entity(:queues)
      end
    end
    
    $game = OneBitLowrezPaint.new
    
    def tick args
      $game.args = args
      $game.tick
    end
    
    # $gtk.reset
    
    

    Frame By Frame - main.rb link

    # ./samples/99_genre_dev_tools/frame_by_frame/app/main.rb
    def tick args
      # create a tick count variant called clock
      # so I can manually control "tick_count"
      args.state.clock ||= 0
    
      # calc for frame by frame stepping
      calc_debug args
    
      # conditional calc of game
      calc_game args
    
      # always render game
      render_game args
    
      # increment clock
      if args.state.frame_by_frame
        if args.state.increment_frame > 0
          args.state.clock += 1
        end
      else
        args.state.clock += 1
      end
    end
    
    def calc_debug args
      # create an increment_frame counter for frame by frame
      # stepping
      args.state.increment_frame ||= 0
      args.state.increment_frame  -= 1
    
      # press l to increment by 30 frames or if any key is pressed
      if args.inputs.keyboard.key_down.l || args.inputs.keyboard.key_down.truthy_keys.length > 0
        args.state.increment_frame = 30
      end
    
      # enable disable frame by frame mode
      if args.inputs.keyboard.key_down.p
        if args.state.frame_by_frame == true
          args.state.frame_by_frame = false
        else
          args.state.frame_by_frame = true
          args.state.increment_frame = 0
        end
      end
    
      # press k to increment by one frame
      if args.inputs.keyboard.key_down.k
        args.state.increment_frame = 1
      end
    end
    
    def render_game args
      args.outputs.sprites << args.state.player
    end
    
    def calc_game args
      return if args.state.frame_by_frame && args.state.increment_frame < 0
    
      args.state.player ||= {
        x: 0,
        y: 360,
        w: 40,
        h: 40,
        anchor_x: 0.5,
        anchor_y: 0.5,
        path: :pixel,
        r: 0, g: 0, b: 255
      }
    
      args.state.player.x += 10
      args.state.player.y += args.inputs.up_down * 10
    
      if args.state.player.x > 1280
        args.state.player.x = 0
      end
    
      if args.state.player.y > 720
        args.state.player.y = 0
      elsif args.state.player.y < 0
        args.state.player.y = 720
      end
    end
    
    $gtk.reset
    
    

    Tile Editor Starting Point - main.rb link

    # ./samples/99_genre_dev_tools/tile_editor_starting_point/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - 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.
    
     - Ceil: Returns an integer number greater than or equal to the original
       with no decimal.
    
     Reminders:
    
     - ARRAY#inside_rect?: Returns true or false depending on if the point is inside a rect.
    
     - 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.sprites: An array. The values generate a sprite.
       The parameters are [X, Y, WIDTH, HEIGHT, IMAGE PATH]
       For more information about sprites, go to mygame/documentation/05-sprites.md.
    
     - 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.lines: An array. The values generate a line.
       The parameters are [X, Y, X2, Y2, RED, GREEN, BLUE]
       For more information about lines, go to mygame/documentation/04-lines.md.
    
     - 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.)
    
    =end
    
    # This sample app shows an empty grid that the user can paint in. There are different image tiles that
    # the user can use to fill the grid, and the "Clear" button can be pressed to clear the grid boxes.
    
    class TileEditor
      attr_accessor :inputs, :state, :outputs, :grid, :args
    
      # Runs all the methods necessary for the game to function properly.
      def tick
        defaults
        render
        check_click
        draw_buttons
      end
    
      # Sets default values
      # Initialization only happens in the first frame
      # NOTE: The values of some of these variables may seem confusingly large at first.
      # The gridSize is 1600 but it seems a lot smaller on the screen, for example.
      # But keep in mind that by using the "W", "A", "S", and "D" keys, you can
      # move the grid's view in all four directions for more grid spaces.
      def defaults
        state.tileCords      ||= []
        state.tileQuantity   ||= 6
        state.tileSize       ||= 50
        state.tileSelected   ||= 1
        state.tempX          ||= 50
        state.tempY          ||= 500
        state.speed          ||= 4
        state.centerX        ||= 4000
        state.centerY        ||= 4000
        state.originalCenter ||= [state.centerX, state.centerY]
        state.gridSize       ||= 1600
        state.lineQuantity   ||= 50
        state.increment      ||= state.gridSize / state.lineQuantity
        state.gridX          ||= []
        state.gridY          ||= []
        state.filled_squares ||= []
        state.grid_border    ||= [390, 140, 500, 500]
    
        get_grid unless state.tempX == 0 # calls get_grid in the first frame only
        determineTileCords unless state.tempX == 0 # calls determineTileCords in first frame
        state.tempX = 0 # sets tempX to 0; the two methods aren't called again
      end
    
      # Calculates the placement of lines or separators in the grid
      def get_grid
        curr_x = state.centerX - (state.gridSize / 2) # starts at left of grid
        deltaX = state.gridSize / state.lineQuantity # finds distance to place vertical lines evenly through width of grid
        (state.lineQuantity + 2).times do
          state.gridX << curr_x # adds curr_x to gridX collection
          curr_x += deltaX # increment curr_x by the distance between vertical lines
        end
    
        curr_y = state.centerY - (state.gridSize / 2) # starts at bottom of grid
        deltaY = state.gridSize / state.lineQuantity # finds distance to place horizontal lines evenly through height of grid
        (state.lineQuantity + 2).times do
          state.gridY << curr_y # adds curr_y to gridY collection
          curr_y += deltaY # increments curr_y to distance between horizontal lines
        end
      end
    
      # Determines coordinate positions of patterned tiles (on the left side of the grid)
      def determineTileCords
        state.tempCounter ||= 1 # initializes tempCounter to 1
        state.tileQuantity.times do # there are 6 different kinds of tiles
          state.tileCords += [[state.tempX, state.tempY, state.tempCounter]] # adds tile definition to collection
          state.tempX += 75 # increments tempX to put horizontal space between the patterned tiles
          state.tempCounter += 1 # increments tempCounter
          if state.tempX > 200 # if tempX exceeds 200 pixels
            state.tempX = 50 # a new row of patterned tiles begins
            state.tempY -= 75 # the new row is 75 pixels lower than the previous row
          end
        end
      end
    
      # Outputs objects (grid, tiles, etc) onto the screen
      def render
        outputs.sprites << state.tileCords.map do # outputs tileCords collection using images in sprites folder
          |x, y, order|
          [x, y, state.tileSize, state.tileSize, 'sprites/image' + order.to_s + ".png"]
        end
        outputs.solids << [0, 0, 1280, 720, 255, 255, 255] # outputs white background
        add_grid # outputs grid
        print_title # outputs title and current tile pattern
      end
    
      # Creates a grid by outputting vertical and horizontal grid lines onto the screen.
      # Outputs sprites for the filled_squares collection onto the screen.
      def add_grid
    
        # Outputs the grid's border.
        outputs.borders << state.grid_border
        temp = 0
    
        # Before looking at the code that outputs the vertical and horizontal lines in the
        # grid, take note of the fact that:
        # grid_border[1] refers to the border's bottom line (running horizontally),
        # grid_border[2] refers to the border's top line (running (horizontally),
        # grid_border[0] refers to the border's left line (running vertically),
        # and grid_border[3] refers to the border's right line (running vertically).
    
        #           [2]
        #       ----------
        #       |        |
        # [0]   |        | [3]
        #       |        |
        #       ----------
        #           [1]
    
        # Calculates the positions and outputs the x grid lines in the color gray.
        state.gridX.map do # perform an action on all elements of the gridX collection
          |x|
          temp += 1 # increment temp
    
          # if x's value is greater than (or equal to) the x value of the border's left side
          # and less than (or equal to) the x value of the border's right side
          if x >= state.centerX - (state.grid_border[2] / 2) && x <= state.centerX + (state.grid_border[2] / 2)
            delta = state.centerX - 640
            # vertical lines have the same starting and ending x positions
            # starting y and ending y positions lead from the bottom of the border to the top of the border
            outputs.lines << [x - delta, state.grid_border[1], x - delta, state.grid_border[1] + state.grid_border[2], 150, 150, 150] # sets definition of vertical line and outputs it
          end
        end
        temp = 0
    
        # Calculates the positions and outputs the y grid lines in the color gray.
        state.gridY.map do # perform an action on all elements of the gridY collection
          |y|
          temp += 1 # increment temp
    
          # if y's value is greater than (or equal to) the y value of the border's bottom side
          # and less than (or equal to) the y value of the border's top side
          if y >= state.centerY - (state.grid_border[3] / 2) && y <= state.centerY + (state.grid_border[3] / 2)
            delta = state.centerY - 393
            # horizontal lines have the same starting and ending y positions
            # starting x and ending x positions lead from the left side of the border to the right side of the border
            outputs.lines << [state.grid_border[0], y - delta, state.grid_border[0] + state.grid_border[3], y - delta, 150, 150, 150] # sets definition of horizontal line and outputs it
          end
        end
    
        # Sets values and outputs sprites for the filled_squares collection.
        state.filled_squares.map do # perform an action on every element of the filled_squares collection
          |x, y, w, h, sprite|
            # if x's value is greater than (or equal to) the x value of 17 pixels to the left of the border's left side
            # and less than (or equal to) the x value of the border's right side
            # and y's value is greater than (or equal to) the y value of the border's bottom side
            # and less than (or equal to) the y value of 25 pixels above the border's top side
            # NOTE: The allowance of 17 pixels and 25 pixels is due to the fact that a grid box may be slightly cut off or
            # not entirely visible in the grid's view (until it is moved using "W", "A", "S", "D")
            if x >= state.centerX - (state.grid_border[2] / 2) - 17 && x <= state.centerX + (state.grid_border[2] / 2) &&
               y >= state.centerY - (state.grid_border[3] / 2) && y <= state.centerY + (state.grid_border[3] / 2) + 25
              # calculations done to place sprites in grid spaces that are meant to filled in
              # mess around with the x and y values and see how the sprite placement changes
              outputs.sprites << [x - state.centerX + 630, y - state.centerY + 360, w, h, sprite]
            end
          end
    
          # outputs a white solid along the left side of the grid (change the color and you'll be able to see it against the white background)
          # state.increment subtracted in x parameter because solid's position is denoted by bottom left corner
          # state.increment subtracted in y parameter to avoid covering the title label
          outputs.primitives << [state.grid_border[0] - state.increment,
                                 state.grid_border[1] - state.increment, state.increment, state.grid_border[3] + (state.increment * 2),
                                 255, 255, 255].solid
    
          # outputs a white solid along the right side of the grid
          # state.increment subtracted from y parameter to avoid covering title label
          outputs.primitives << [state.grid_border[0] + state.grid_border[2],
                                 state.grid_border[1] - state.increment, state.increment, state.grid_border[3] + (state.increment * 2),
                                 255, 255, 255].solid
    
          # outputs a white solid along the bottom of the grid
          # state.increment subtracted from y parameter to avoid covering last row of grid boxes
          outputs.primitives << [state.grid_border[0] - state.increment, state.grid_border[1] - state.increment,
                                 state.grid_border[2] + (2 * state.increment), state.increment, 255, 255, 255].solid
    
          # outputs a white solid along the top of the grid
          outputs.primitives << [state.grid_border[0] - state.increment, state.grid_border[1] + state.grid_border[3],
                                 state.grid_border[2] + (2 * state.increment), state.increment, 255, 255, 255].solid
    
      end
    
      # Outputs title and current tile pattern
      def print_title
        outputs.labels << [640, 700, 'Mouse to Place Tile, WASD to Move Around', 7, 1] # title label
        outputs.lines << horizontal_separator(660, 0, 1280) # outputs horizontal separator
        outputs.labels << [1050, 500, 'Current:', 3, 1] # outputs Current label
        outputs.sprites << [1110, 474, state.tileSize / 2, state.tileSize / 2, 'sprites/image' + state.tileSelected.to_s + ".png"] # outputs sprite of current tile pattern using images in sprites folder; output is half the size of a tile
      end
    
      # Sets the starting position, ending position, and color for the horizontal separator.
      def horizontal_separator y, x, x2
        [x, y, x2, y, 150, 150, 150] # definition of separator; horizontal line means same starting/ending y
      end
    
      # Checks if the mouse is being clicked or dragged
      def check_click
        if inputs.keyboard.key_down.r # if the "r" key is pressed down
          $dragon.reset
        end
    
        if inputs.mouse.down #is mouse up or down?
          state.mouse_held = true
          if inputs.mouse.position.x < state.grid_border[0] # if mouse's x position is inside the grid's borders
            state.tileCords.map do # perform action on all elements of tileCords collection
              |x, y, order|
              # if mouse's x position is greater than (or equal to) the starting x position of a tile
              # and the mouse's x position is also less than (or equal to) the ending x position of that tile,
              # and the mouse's y position is greater than (or equal to) the starting y position of that tile,
              # and the mouse's y position is also less than (or equal to) the ending y position of that tile,
              # (BASICALLY, IF THE MOUSE'S POSITION IS WITHIN THE STARTING AND ENDING POSITIONS OF A TILE)
              if inputs.mouse.position.x >= x && inputs.mouse.position.x <= x + state.tileSize &&
                 inputs.mouse.position.y >= y && inputs.mouse.position.y <= y + state.tileSize
                state.tileSelected = order # that tile is selected
              end
            end
          end
        elsif inputs.mouse.up # otherwise, if the mouse is in the "up" state
          state.mouse_held = false # mouse is not 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 ||
            (inputs.mouse.previous_click.point.y - inputs.mouse.position.y).abs > 15) # Need to move 15 pixels before "drag"
          state.mouse_dragging = true
        end
    
        # if mouse is clicked inside grid's border, search_lines method is called with click input type
        if ((inputs.mouse.click) && (inputs.mouse.click.point.inside_rect? state.grid_border))
          search_lines(inputs.mouse.click.point, :click)
    
        # if mouse is dragged inside grid's border, search_lines method is called with drag input type
        elsif ((state.mouse_dragging) && (inputs.mouse.position.inside_rect? state.grid_border))
          search_lines(inputs.mouse.position, :drag)
        end
    
        # Changes grid's position on screen by moving it up, down, left, or right.
    
        # centerX is incremented by speed if the "d" key is pressed and if that sum is less than
        # the original left side of the center plus half the grid, minus half the top border of grid.
        # MOVES GRID RIGHT (increasing x)
        state.centerX += state.speed if inputs.keyboard.key_held.d &&
                                        (state.centerX + state.speed) < state.originalCenter[0] + (state.gridSize / 2) - (state.grid_border[2] / 2)
        # centerX is decremented by speed if the "a" key is pressed and if that difference is greater than
        # the original left side of the center minus half the grid, plus half the top border of grid.
        # MOVES GRID LEFT (decreasing x)
        state.centerX -= state.speed if inputs.keyboard.key_held.a &&
                                        (state.centerX - state.speed) > state.originalCenter[0] - (state.gridSize / 2) + (state.grid_border[2] / 2)
        # centerY is incremented by speed if the "w" key is pressed and if that sum is less than
        # the original bottom of the center plus half the grid, minus half the right border of grid.
        # MOVES GRID UP (increasing y)
        state.centerY += state.speed if inputs.keyboard.key_held.w &&
                                        (state.centerY + state.speed) < state.originalCenter[1] + (state.gridSize / 2) - (state.grid_border[3] / 2)
        # centerY is decremented by speed if the "s" key is pressed and if the difference is greater than
        # the original bottom of the center minus half the grid, plus half the right border of grid.
        # MOVES GRID DOWN (decreasing y)
        state.centerY -= state.speed if inputs.keyboard.key_held.s &&
                                        (state.centerY - state.speed) > state.originalCenter[1] - (state.gridSize / 2) + (state.grid_border[3] / 2)
      end
    
      # Performs calculations on the gridX and gridY collections, and sets values.
      # Sets the definition of a grid box, including the image that it is filled with.
      def search_lines (point, input_type)
        point.x += state.centerX - 630 # increments x and y
        point.y += state.centerY - 360
        findX = 0
        findY = 0
        increment = state.gridSize / state.lineQuantity # divides grid by number of separators
    
        state.gridX.map do # perform an action on every element of collection
          |x|
          # findX increments x by 10 if point.x is less than that sum and findX is currently 0
          findX = x + 10 if point.x < (x + 10) && findX == 0
        end
    
        state.gridY.map do
          |y|
          # findY is set to y if point.y is less than that value and findY is currently 0
          findY = y if point.y < (y) && findY == 0
        end
        # position of a box is denoted by bottom left corner, which is why the increment is being subtracted
        grid_box = [findX - (increment.ceil), findY - (increment.ceil), increment.ceil, increment.ceil,
                    "sprites/image" + state.tileSelected.to_s + ".png"] # sets sprite definition
    
        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 grid box dragged over is already filled in
            state.filled_squares << grid_box # box is filled in and added to filled_squares
          end
        end
      end
    
      # Creates a "Clear" button using labels and borders.
      def draw_buttons
        x, y, w, h = 390, 50, 240, 50
        state.clear_button        ||= state.new_entity(:button_with_fade)
    
        # x and y positions are set to display "Clear" label in center of the button
        # Try changing 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]
        state.clear_button.border ||= [x, y, w, h] # definition of button's border
    
        # If the mouse is clicked inside the borders of the clear button
        if inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.clear_button.border)
          state.clear_button.clicked_at = inputs.mouse.click.created_at # value is frame of mouse click
          state.filled_squares.clear # filled squares collection is emptied (squares are cleared)
          inputs.mouse.previous_click = nil # no previous click
        end
    
        outputs.labels << state.clear_button.label # outputs clear button
        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
    
    $tile_editor = TileEditor.new
    
    def tick args
      $tile_editor.inputs = args.inputs
      $tile_editor.grid = args.grid
      $tile_editor.args = args
      $tile_editor.outputs = args.outputs
      $tile_editor.state = args.state
      $tile_editor.tick
      tick_instructions args, "Roll your own tile editor. CLICK to select a sprite. CLICK in grid to place sprite. WASD to move around."
    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
    
    

    Genre Dungeon Crawl link

    Classics Jam - main.rb link

    # ./samples/99_genre_dungeon_crawl/classics_jam/app/main.rb
    class Game
      attr_gtk
    
      def tick
        defaults
        render
        input
        calc
      end
    
      def defaults
        player.x              ||= 640
        player.y              ||= 360
        player.w              ||= 16
        player.h              ||= 16
        player.attacked_at    ||= -1
        player.angle          ||= 0
        player.future_player  ||= future_player_position 0, 0
        player.projectiles    ||= []
        player.damage         ||= 0
        state.level           ||= create_level level_one_template
      end
    
      def render
        outputs.sprites << level.walls.map do |w|
          w.merge(path: 'sprites/square/gray.png')
        end
    
        outputs.sprites << level.spawn_locations.map do |s|
          s.merge(path: 'sprites/square/blue.png')
        end
    
        outputs.sprites << player.projectiles.map do |p|
          p.merge(path: 'sprites/square/blue.png')
        end
    
        outputs.sprites << level.enemies.map do |e|
          e.merge(path: 'sprites/square/red.png')
        end
    
        outputs.sprites << player.merge(path: 'sprites/circle/green.png', angle: player.angle)
    
        outputs.labels << { x: 30, y: 30.from_top, text: "damage: #{player.damage || 0}" }
      end
    
      def input
        player.angle = inputs.directional_angle || player.angle
        if inputs.controller_one.key_down.a || inputs.keyboard.key_down.space
          player.attacked_at = Kernel.tick_count
        end
      end
    
      def calc
        calc_player
        calc_projectiles
        calc_enemies
        calc_spawn_locations
      end
    
      def calc_player
        if player.attacked_at == Kernel.tick_count
          player.projectiles << { at: Kernel.tick_count,
                                  x: player.x,
                                  y: player.y,
                                  angle: player.angle,
                                  w: 4,
                                  h: 4 }.center_inside_rect(player)
        end
    
        if player.attacked_at.elapsed_time > 5
          future_player = future_player_position inputs.left_right * 2, inputs.up_down * 2
          future_player_collision = future_collision player, future_player, level.walls
          player.x = future_player_collision.x if !future_player_collision.dx_collision
          player.y = future_player_collision.y if !future_player_collision.dy_collision
        end
      end
    
      def calc_projectile_collisions entities
        entities.each do |e|
          e.damage ||= 0
          player.projectiles.each do |p|
            if !p.collided && (p.intersect_rect? e)
              p.collided = true
              e.damage  += 1
            end
          end
        end
      end
    
      def calc_projectiles
        player.projectiles.map! do |p|
          dx, dy = p.angle.vector 10
          p.merge(x: p.x + dx, y: p.y + dy)
        end
    
        calc_projectile_collisions level.walls + level.enemies + level.spawn_locations
        player.projectiles.reject! { |p| p.at.elapsed_time > 10000 }
        player.projectiles.reject! { |p| p.collided }
        level.enemies.reject! { |e| e.damage > e.hp }
        level.spawn_locations.reject! { |s| s.damage > s.hp }
      end
    
      def calc_enemies
        level.enemies.map! do |e|
          dx =  0
          dx =  1 if e.x < player.x
          dx = -1 if e.x > player.x
          dy =  0
          dy =  1 if e.y < player.y
          dy = -1 if e.y > player.y
          future_e           = future_entity_position dx, dy, e
          future_e_collision = future_collision e, future_e, level.enemies + level.walls
          e.next_x = e.x
          e.next_y = e.y
          e.next_x = future_e_collision.x if !future_e_collision.dx_collision
          e.next_y = future_e_collision.y if !future_e_collision.dy_collision
          e
        end
    
        level.enemies.map! do |e|
          e.x = e.next_x
          e.y = e.next_y
          e
        end
    
        level.enemies.each do |e|
          player.damage += 1 if e.intersect_rect? player
        end
      end
    
      def calc_spawn_locations
        level.spawn_locations.map! do |s|
          s.merge(countdown: s.countdown - 1)
        end
        level.spawn_locations
             .find_all { |s| s.countdown.neg? }
             .each do |s|
          s.countdown = s.rate
          s.merge(countdown: s.rate)
          new_enemy = create_enemy s
          if !(level.enemies.find { |e| e.intersect_rect? new_enemy })
            level.enemies << new_enemy
          end
        end
      end
    
      def create_enemy spawn_location
        to_cell(spawn_location.ordinal_x, spawn_location.ordinal_y).merge hp: 2
      end
    
      def create_level level_template
        {
          walls:           level_template.walls.map { |w| to_cell(w.ordinal_x, w.ordinal_y).merge(w) },
          enemies:         [],
          spawn_locations: level_template.spawn_locations.map { |s| to_cell(s.ordinal_x, s.ordinal_y).merge(s) }
        }
      end
    
      def level_one_template
        {
          walls:           [{ ordinal_x: 25, ordinal_y: 20},
                            { ordinal_x: 25, ordinal_y: 21},
                            { ordinal_x: 25, ordinal_y: 22},
                            { ordinal_x: 25, ordinal_y: 23}],
          spawn_locations: [{ ordinal_x: 10, ordinal_y: 10, rate: 120, countdown: 0, hp: 5 }]
        }
      end
    
      def player
        state.player ||= {}
      end
    
      def level
        state.level  ||= {}
      end
    
      def future_collision entity, future_entity, others
        dx_collision = others.find { |o| o != entity && (o.intersect_rect? future_entity.dx) }
        dy_collision = others.find { |o| o != entity && (o.intersect_rect? future_entity.dy) }
    
        {
          dx_collision: dx_collision,
          x: future_entity.dx.x,
          dy_collision: dy_collision,
          y: future_entity.dy.y
        }
      end
    
      def future_entity_position dx, dy, entity
        {
          dx:   entity.merge(x: entity.x + dx),
          dy:   entity.merge(y: entity.y + dy),
          both: entity.merge(x: entity.x + dx, y: entity.y + dy)
        }
      end
    
      def future_player_position  dx, dy
        future_entity_position dx, dy, player
      end
    
      def to_cell ordinal_x, ordinal_y
        { x: ordinal_x * 16, y: ordinal_y * 16, w: 16, h: 16 }
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    $gtk.reset
    $game = nil
    
    

    Genre Fighting link

    Special Move Inputs - main.rb link

    # ./samples/99_genre_fighting/01_special_move_inputs/app/main.rb
    def tick args
      #tick_instructions args, "Use LEFT and RIGHT arrow keys to move and SPACE to jump."
      defaults args
      render args
      input args
      calc args
    end
    
    # sets default values and creates empty collections
    # initialization only happens in the first frame
    def defaults args
      fiddle args
    
      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.player.r  ||= 0
      args.state.game_over_at ||= 0
      args.state.animation_time ||=0
    
      args.state.timeleft ||=0
      args.state.timeright ||=0
      args.state.lastpush ||=0
    
      args.state.inputlist ||=  ["j","k","l"]
    end
    
    # sets enemy, player, hammer values
    def fiddle args
      args.state.gravity                     = -0.5
      args.state.player_jump_power           = 10      # sets player values
      args.state.player_jump_power_duration  = 5
      args.state.player_max_run_speed        = 20
      args.state.player_speed_slowdown_rate  = 0.9
      args.state.player_acceleration         = 0.9
    end
    
    # outputs objects onto the screen
    def render args
      if (args.state.player.dx < 0.01) && (args.state.player.dx > -0.01)
        args.state.player.dx = 0
      end
    
      #move list
      (args.layout.rect_group row: 0, col_from_right: 8, drow: 0.3,
                              merge: { vertical_alignment_enum: 0, size_enum: -2 },
                              group: [
                                { text: "move:             WASD" },
                                { text: "jump:             Space" },
                                { text: "attack forwards:  J (while on ground" },
                                { text: "attack upwards:   K (while on groud)" },
                                { text: "attack backwards: J (while on ground and holding A)" },
                                { text: "attack downwards: K (while in air)" },
                                { text: "dash attack:      J, K in quick succession." },
                                { text: "shield: hold      J, K at the same time." },
                                { text: "dash backwards:   A, A in quick succession." },
                              ]).into args.outputs.labels
    
      # registered moves
      args.outputs.labels << { x: 0.to_layout_col,
                               y: 0.to_layout_row,
                               text: "input history",
                               size_enum: -2,
                               vertical_alignment_enum: 0 }
    
      (args.state.inputlist.take(5)).map do |s|
        { text: s, size_enum: -2, vertical_alignment_enum: 0 }
      end.yield_self do |group|
        (args.layout.rect_group row: 0.3, col: 0, drow: 0.3, group: group).into args.outputs.labels
      end
    
    
      #sprites
      player = [args.state.player.x, args.state.player.y,
                args.state.player.w, args.state.player.h,
                "sprites/square/white.png",
                args.state.player.r]
    
      playershield = [args.state.player.x - 20, args.state.player.y - 10,
                      args.state.player.w + 20, args.state.player.h + 20,
                      "sprites/square/blue.png",
                      args.state.player.r,
                      0]
    
      playerjab = [args.state.player.x + 32, args.state.player.y,
                   args.state.player.w, args.state.player.h,
                   "sprites/isometric/indigo.png",
                   args.state.player.r,
                   0]
    
      playerupper = [args.state.player.x, args.state.player.y + 32,
                     args.state.player.w, args.state.player.h,
                     "sprites/isometric/indigo.png",
                     args.state.player.r+90,
                     0]
    
      if ((Kernel.tick_count - args.state.lastpush) <= 15)
        if (args.state.inputlist[0] == "<<")
          player = [args.state.player.x, args.state.player.y,
                    args.state.player.w, args.state.player.h,
                    "sprites/square/yellow.png", args.state.player.r]
        end
    
        if (args.state.inputlist[0] == "shield")
          player = [args.state.player.x, args.state.player.y,
                    args.state.player.w, args.state.player.h,
                    "sprites/square/indigo.png", args.state.player.r]
    
          playershield = [args.state.player.x - 10, args.state.player.y - 10,
                          args.state.player.w + 20, args.state.player.h + 20,
                          "sprites/square/blue.png", args.state.player.r, 50]
        end
    
        if (args.state.inputlist[0] == "back-attack")
          playerjab = [args.state.player.x - 20, args.state.player.y,
                       args.state.player.w - 10, args.state.player.h,
                       "sprites/isometric/indigo.png", args.state.player.r, 255]
        end
    
        if (args.state.inputlist[0] == "forward-attack")
          playerjab = [args.state.player.x + 32, args.state.player.y,
                       args.state.player.w, args.state.player.h,
                       "sprites/isometric/indigo.png", args.state.player.r, 255]
        end
    
        if (args.state.inputlist[0] == "up-attack")
          playerupper = [args.state.player.x, args.state.player.y + 32,
                         args.state.player.w, args.state.player.h,
                         "sprites/isometric/indigo.png", args.state.player.r + 90, 255]
        end
    
        if (args.state.inputlist[0] == "dair")
          playerupper = [args.state.player.x, args.state.player.y - 32,
                         args.state.player.w, args.state.player.h,
                         "sprites/isometric/indigo.png", args.state.player.r + 90, 255]
        end
    
        if (args.state.inputlist[0] == "dash-attack")
          playerupper = [args.state.player.x, args.state.player.y + 32,
                         args.state.player.w, args.state.player.h,
                         "sprites/isometric/violet.png", args.state.player.r + 90, 255]
    
          playerjab = [args.state.player.x + 32, args.state.player.y,
                       args.state.player.w, args.state.player.h,
                       "sprites/isometric/violet.png", args.state.player.r, 255]
        end
      end
    
      args.outputs.sprites << playerjab
      args.outputs.sprites << playerupper
      args.outputs.sprites << player
      args.outputs.sprites << playershield
    
      args.outputs.solids << 20.map_with_index do |i| # uses 20 squares to form bridge
        [i * 64, args.state.bridge_top - 64, 64, 64]
      end
    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
    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.state.inputlist.length > 5
        args.state.inputlist.pop
      end
    
      should_process_special_move = (args.inputs.keyboard.key_down.j)           ||
                                    (args.inputs.keyboard.key_down.k)           ||
                                    (args.inputs.keyboard.key_down.a)           ||
                                    (args.inputs.keyboard.key_down.d)           ||
                                    (args.inputs.controller_one.key_down.y)     ||
                                    (args.inputs.controller_one.key_down.x)     ||
                                    (args.inputs.controller_one.key_down.left)  ||
                                    (args.inputs.controller_one.key_down.right)
    
      if (should_process_special_move)
        if (args.inputs.keyboard.key_down.j && args.inputs.keyboard.key_down.k) ||
           (args.inputs.controller_one.key_down.x && args.inputs.controller_one.key_down.y)
          args.state.inputlist.unshift("shield")
        elsif (args.inputs.keyboard.key_down.k || args.inputs.controller_one.key_down.y) &&
              (args.state.inputlist[0] == "forward-attack") && ((Kernel.tick_count - args.state.lastpush) <= 15)
          args.state.inputlist.unshift("dash-attack")
          args.state.player.dx = 20
        elsif (args.inputs.keyboard.key_down.j && args.inputs.keyboard.a) ||
              (args.inputs.controller_one.key_down.x && args.inputs.controller_one.key_down.left)
          args.state.inputlist.unshift("back-attack")
        elsif ( args.inputs.controller_one.key_down.x || args.inputs.keyboard.key_down.j)
          args.state.inputlist.unshift("forward-attack")
        elsif (args.inputs.keyboard.key_down.k || args.inputs.controller_one.key_down.y) &&
              (args.state.player.y > 128)
          args.state.inputlist.unshift("dair")
        elsif (args.inputs.keyboard.key_down.k || args.inputs.controller_one.key_down.y)
          args.state.inputlist.unshift("up-attack")
        elsif (args.inputs.controller_one.key_down.left || args.inputs.keyboard.key_down.a) &&
              (args.state.inputlist[0] == "<") &&
              ((Kernel.tick_count - args.state.lastpush) <= 10)
          args.state.inputlist.unshift("<<")
          args.state.player.dx = -15
        elsif (args.inputs.controller_one.key_down.left || args.inputs.keyboard.key_down.a)
          args.state.inputlist.unshift("<")
          args.state.timeleft = Kernel.tick_count
        elsif (args.inputs.controller_one.key_down.right || args.inputs.keyboard.key_down.d)
          args.state.inputlist.unshift(">")
        end
    
        args.state.lastpush = Kernel.tick_count
      end
    
      if args.inputs.keyboard.space || args.inputs.controller_one.r2   # 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.inputs.controller_one.key_up.r2
        args.state.player.jumped_at = nil # jumped_at is empty
        args.state.player.falling = true # the player is falling
      end
    
      if args.inputs.left # if left key is pressed
        if args.state.player.dx < -5
          args.state.player.dx = args.state.player.dx
        else
          args.state.player.dx = -5
        end
    
      elsif args.inputs.right # if right key is pressed
        if args.state.player.dx > 5
          args.state.player.dx = args.state.player.dx
        else
          args.state.player.dx = 5
        end
      else
        args.state.player.dx *= args.state.player_speed_slowdown_rate # dx is scaled down
      end
    
      if ((args.state.player.dx).abs > 5) #&& ((Kernel.tick_count - args.state.lastpush) <= 10)
        args.state.player.dx *= 0.95
      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
    
    

    Genre Lowrez link

    Nokia 3310 - main.rb link

    # ./samples/99_genre_lowrez/nokia_3310/app/main.rb
    require 'app/nokia.rb'
    
    def tick args
      # =======================================================================
      # ==== HELLO WORLD ======================================================
      # =======================================================================
      # Steps to get started:
      # 1. ~def tick args~ is the entry point for your game.
      # 2. There are quite a few code samples below, remove the "##"
      #    before each line and save the file to see the changes.
      # 3. 0,  0 is in bottom left and 83, 47 is in top right corner.
      # 4. Be sure to come to the discord channel if you need
      #    more help: [[http://discord.dragonruby.org]].
    
      # Commenting and uncommenting code:
      # - Add a "#" infront of lines to comment out code
      # - Remove the "#" infront of lines to comment out code
    
      # Invoke the hello_world subroutine/method
      hello_world args # <---- add a "#" to the beginning of the line to stop running this subroutine/method.
    
      # =======================================================================
      # ==== HOW TO RENDER A LABEL ============================================
      # =======================================================================
    
      # Uncomment the line below to invoke the how_to_render_a_label subroutine/method.
      # Note: The method is defined in this file with the signature ~def how_to_render_a_label args~
      #       Scroll down to the method to see the details.
    
      # Remove the "#" at the beginning of the line below
      # how_to_render_a_label args # <---- remove the "#" at the beginning of this line to run the method
    
    
      # =======================================================================
      # ==== HOW TO RENDER A FILLED SQUARE (SOLID) ============================
      # =======================================================================
      # Remove the "#" at the beginning of the line below
      # how_to_render_solids args
    
    
      # =======================================================================
      # ==== HOW TO RENDER AN UNFILLED SQUARE (BORDER) ========================
      # =======================================================================
      # Remove the "#" at the beginning of the line below
      # how_to_render_borders args
    
    
      # =======================================================================
      # ==== HOW TO RENDER A LINE =============================================
      # =======================================================================
      # Remove the "#" at the beginning of the line below
      # how_to_render_lines args
    
    
      # =======================================================================
      # == HOW TO RENDER A SPRITE =============================================
      # =======================================================================
      # Remove the "#" at the beginning of the line below
      # how_to_render_sprites args
    
    
      # =======================================================================
      # ==== HOW TO MOVE A SPRITE BASED OFF OF USER INPUT =====================
      # =======================================================================
      # Remove the "#" at the beginning of the line below
      # how_to_move_a_sprite args
    
    
      # =======================================================================
      # ==== HOW TO ANIMATE A SPRITE (SEPERATE PNGS) ==========================
      # =======================================================================
      # Remove the "#" at the beginning of the line below
      # how_to_animate_a_sprite args
    
    
      # =======================================================================
      # ==== HOW TO ANIMATE A SPRITE (SPRITE SHEET) ===========================
      # =======================================================================
      # Remove the "#" at the beginning of the line below
      # how_to_animate_a_sprite_sheet args
    
    
      # =======================================================================
      # ==== HOW TO DETERMINE COLLISION =============================================
      # =======================================================================
      # Remove the "#" at the beginning of the line below
      # how_to_determine_collision args
    
    
      # =======================================================================
      # ==== HOW TO CREATE BUTTONS ==================================================
      # =======================================================================
      # Remove the "#" at the beginning of the line below
      # how_to_create_buttons args
    
      # ==== The line below renders a debug grid, mouse information, and current tick
      # render_debug args
    end
    
    # =======================================================================
    # ==== HELLO WORLD ======================================================
    # =======================================================================
    def hello_world args
      args.nokia.solids  << { x: 0, y: 64, w: 10, h: 10, r: 255 }
    
      args.nokia.labels  << {
        x: 42,
        y: 46,
        text: "nokia 3310 jam 3",
        size_enum: NOKIA_FONT_SM,
        alignment_enum: 1,
        r: 0,
        g: 0,
        b: 0,
        a: 255,
        font: NOKIA_FONT_PATH
      }
    
      args.nokia.sprites << {
        x: 42 - 10,
        y: 26 - 10,
        w: 20,
        h: 20,
        path: 'sprites/monochrome-ship.png',
        a: 255,
        angle: Kernel.tick_count % 360
      }
    end
    
    # =======================================================================
    # ==== HOW TO RENDER A LABEL ============================================
    # =======================================================================
    def how_to_render_a_label args
      # NOTE: Text is aligned from the TOP LEFT corner
    
      # Render an EXTRA LARGE/XL label (remove the "#" in front of each line below)
      args.nokia.labels << { x: 0, y: 46, text: "Hello World",
                             size_enum: NOKIA_FONT_XL,
                             r: 0, g: 0, b: 0, a: 255,
                             font: NOKIA_FONT_PATH }
    
      # Render a LARGE/LG label (remove the "#" in front of each line below)
      args.nokia.labels << { x: 0, y: 29, text: "Hello World",
                              size_enum: NOKIA_FONT_LG,
                              r: 0, g: 0, b: 0, a: 255,
                              font: NOKIA_FONT_PATH }
    
      # Render a MEDIUM/MD label (remove the "#" in front of each line below)
      args.nokia.labels << { x: 0, y: 16, text: "Hello World",
                              size_enum: NOKIA_FONT_MD,
                              r: 0, g: 0, b: 0, a: 255,
                              font: NOKIA_FONT_PATH }
    
      # Render a SMALL/SM label (remove the "#" in front of each line below)
      args.nokia.labels << { x: 0, y: 7, text: "Hello World",
                              size_enum: NOKIA_FONT_SM,
                              r: 0, g: 0, b: 0, a: 255,
                              font: NOKIA_FONT_PATH }
    
      # You are provided args.nokia.default_label which returns a Hash that you
      # can ~merge~ properties with
      # Example 1
      args.nokia.labels << args.nokia
                                .default_label
                                .merge(text: "Default")
    
      # Example 2
      args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: 31,
                                       text: "Default")
    end
    
    # =============================================================================
    # ==== HOW TO RENDER FILLED SQUARES (SOLIDS) ==================================
    # =============================================================================
    def how_to_render_solids args
      # Render a square at 0, 0 with a width and height of 1
      args.nokia.solids << { x: 0, y: 0, w: 1, h: 1 }
    
      # Render a square at 1, 1 with a width and height of 2
      args.nokia.solids << { x: 1, y: 1, w: 2, h: 2 }
    
      # Render a square at 3, 3 with a width and height of 3
      args.nokia.solids << { x: 3, y: 3, w: 3, h: 3 }
    
      # Render a square at 6, 6 with a width and height of 4
      args.nokia.solids << { x: 6, y: 6, w: 4, h: 4 }
    end
    
    # =============================================================================
    # ==== HOW TO RENDER UNFILLED SQUARES (BORDERS) ===============================
    # =============================================================================
    def how_to_render_borders args
      # Render a square at 0, 0 with a width and height of 3
      args.nokia.borders << { x: 0, y: 0, w: 3, h: 3, a: 255 }
    
      # Render a square at 3, 3 with a width and height of 3
      args.nokia.borders << { x: 3, y: 3, w: 4, h: 4, a: 255 }
    
      # Render a square at 5, 5 with a width and height of 4
      args.nokia.borders << { x: 7, y: 7, w: 5, h: 5, a: 255 }
    end
    
    # =============================================================================
    # ==== HOW TO RENDER A LINE ===================================================
    # =============================================================================
    def how_to_render_lines args
      # Render a horizontal line at the bottom
      args.nokia.lines << { x: 0, y: 0, x2: 83, y2:  0 }
    
      # Render a vertical line at the left
      args.nokia.lines << { x: 0, y: 0, x2:  0, y2: 47 }
    
      # Render a diagonal line starting from the bottom left and going to the top right
      args.nokia.lines << { x: 0, y: 0, x2: 83, y2: 47 }
    end
    
    # =============================================================================
    # == HOW TO RENDER A SPRITE ===================================================
    # =============================================================================
    def how_to_render_sprites args
      # Loop 10 times and create 10 sprites in 10 positions
      # Render a sprite at the bottom left with a width and height of 5 and a path of 'sprites/monochrome-ship.png'
      10.times do |i|
        args.nokia.sprites << {
          x: i * 8.4,
          y: i * 4.8,
          w: 5,
          h: 5,
          path: 'sprites/monochrome-ship.png'
        }
      end
    
      # Given an array of positions create sprites
      positions = [
        { x: 20, y: 32 },
        { x: 45, y: 15 },
        { x: 72, y: 23 },
      ]
    
      positions.each do |position|
        # use Ruby's ~Hash#merge~ function to create a sprite
        args.nokia.sprites << position.merge(path: 'sprites/monochrome-ship.png',
                                              w: 5,
                                              h: 5)
      end
    end
    
    # =============================================================================
    # ==== HOW TO ANIMATE A SPRITE (SEPERATE PNGS) ==========================
    # =============================================================================
    def how_to_animate_a_sprite args
      # STEP 1: Define when you want the animation to start. The animation in this case will start in 3 seconds
      start_animation_on_tick = 180
    
      # STEP 2: Get the frame_index given the start tick.
      sprite_index = start_animation_on_tick.frame_index count: 7,     # how many sprites?
                                                         hold_for: 8,  # how long to hold each sprite?
                                                         repeat: true  # should it repeat?
    
      # STEP 3: frame_index will return nil if the frame hasn't arrived yet
      if sprite_index
        # if the sprite_index is populated, use it to determine the sprite path and render it
        sprite_path  = "sprites/explosion-#{sprite_index}.png"
        args.nokia.sprites << { x: 42 - 16,
                                 y: 47 - 32,
                                 w: 32,
                                 h: 32,
                                 path: sprite_path }
      else
        # if the sprite_index is nil, render a countdown instead
        countdown_in_seconds = ((start_animation_on_tick - Kernel.tick_count) / 60).round(1)
    
        args.nokia.labels  << args.nokia
                                   .default_label
                                   .merge(x: 0,
                                          y: 18,
                                          text: "Count Down: #{countdown_in_seconds.to_sf}",
                                          alignment_enum: 0)
      end
    
      # render the current tick and the resolved sprite index
      args.nokia.labels  << args.nokia
                                   .default_label
                                   .merge(x: 0,
                                          y: 11,
                                          text: "Tick: #{Kernel.tick_count}")
      args.nokia.labels  << args.nokia
                                   .default_label
                                   .merge(x: 0,
                                          y: 5,
                                          text: "sprite_index: #{sprite_index}")
    end
    
    # =============================================================================
    # ==== HOW TO ANIMATE A SPRITE (SPRITE SHEET) =================================
    # =============================================================================
    def how_to_animate_a_sprite_sheet args
      # STEP 1: Define when you want the animation to start. The animation in this case will start in 3 seconds
      start_animation_on_tick = 180
    
      # STEP 2: Get the frame_index given the start tick.
      sprite_index = start_animation_on_tick.frame_index count: 7,     # how many sprites?
                                                         hold_for: 8,  # how long to hold each sprite?
                                                         repeat: true  # should it repeat?
    
      # STEP 3: frame_index will return nil if the frame hasn't arrived yet
      if sprite_index
        # if the sprite_index is populated, use it to determine the source rectangle and render it
        args.nokia.sprites << {
          x: 42 - 16,
          y: 47 - 32,
          w: 32,
          h: 32,
          path:  "sprites/explosion-sheet.png",
          source_x: 32 * sprite_index,
          source_y: 0,
          source_w: 32,
          source_h: 32
        }
      else
        # if the sprite_index is nil, render a countdown instead
        countdown_in_seconds = ((start_animation_on_tick - Kernel.tick_count) / 60).round(1)
    
        args.nokia.labels  << args.nokia
                                   .default_label
                                   .merge(x: 0,
                                          y: 18,
                                          text: "Count Down: #{countdown_in_seconds.to_sf}",
                                          alignment_enum: 0)
      end
    
      # render the current tick and the resolved sprite index
      args.nokia.labels  << args.nokia
                                   .default_label
                                   .merge(x: 0,
                                          y: 11,
                                          text: "tick: #{Kernel.tick_count}")
      args.nokia.labels  << args.nokia
                                   .default_label
                                   .merge(x: 0,
                                          y: 5,
                                          text: "sprite_index: #{sprite_index}")
    end
    
    # =============================================================================
    # ==== HOW TO STORE STATE, ACCEPT INPUT, AND RENDER SPRITE BASED OFF OF STATE =
    # =============================================================================
    def how_to_move_a_sprite args
      args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: 42,
                                       y: 46, text: "Use Arrow Keys",
                                       alignment_enum: 1)
    
      args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: 42,
                                       y: 41, text: "Or WASD",
                                       alignment_enum: 1)
    
      args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: 42,
                                       y: 36, text: "Or Click",
                                       alignment_enum: 1)
    
      # set the initial values for x and y using ||= ("or equal operator")
      args.state.ship.x ||= 0
      args.state.ship.y ||= 0
    
      # if a mouse click occurs, update the ship's x and y to be the location of the click
      if args.nokia.mouse_click
        args.state.ship.x = args.nokia.mouse_click.x
        args.state.ship.y = args.nokia.mouse_click.y
      end
    
      # if a or left arrow is pressed/held, decrement the ships x position
      if args.nokia.keyboard.left
        args.state.ship.x -= 1
      end
    
      # if d or right arrow is pressed/held, increment the ships x position
      if args.nokia.keyboard.right
        args.state.ship.x += 1
      end
    
      # if s or down arrow is pressed/held, decrement the ships y position
      if args.nokia.keyboard.down
        args.state.ship.y -= 1
      end
    
      # if w or up arrow is pressed/held, increment the ships y position
      if args.nokia.keyboard.up
        args.state.ship.y += 1
      end
    
      # render the sprite to the screen using the position stored in args.state.ship
      args.nokia.sprites << {
        x: args.state.ship.x,
        y: args.state.ship.y,
        w: 5,
        h: 5,
        path: 'sprites/monochrome-ship.png',
        # parameters beyond this point are optional
        angle: 0, # Note: rotation angle is denoted in degrees NOT radians
        r: 255,
        g: 255,
        b: 255,
        a: 255
      }
    end
    
    # =======================================================================
    # ==== HOW TO DETERMINE COLLISION =======================================
    # =======================================================================
    def how_to_determine_collision args
      # Render the instructions
      args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: 42,
                                       y: 46, text: "Click Anywhere",
                                       alignment_enum: 1)
    
      # if a mouse click occurs:
      # - set ship_one if it isn't set
      # - set ship_two if it isn't set
      # - otherwise reset ship one and ship two
      if args.nokia.mouse_click
        # is ship_one set?
        if !args.state.ship_one
          args.state.ship_one = { x: args.nokia.mouse_click.x - 5,
                                  y: args.nokia.mouse_click.y - 5,
                                  w: 10,
                                  h: 10 }
        # is ship_one set?
        elsif !args.state.ship_two
          args.state.ship_two = { x: args.nokia.mouse_click.x - 5,
                                  y: args.nokia.mouse_click.y - 5,
                                  w: 10,
                                  h: 10 }
        # should we reset?
        else
          args.state.ship_one = nil
          args.state.ship_two = nil
        end
      end
    
      # render ship one if it's set
      if args.state.ship_one
        # use Ruby's .merge method which is available on ~Hash~ to set the sprite and alpha
        # render ship one
        args.nokia.sprites << args.state.ship_one.merge(path: 'sprites/monochrome-ship.png')
      end
    
      if args.state.ship_two
        # use Ruby's .merge method which is available on ~Hash~ to set the sprite and alpha
        # render ship two
        args.nokia.sprites << args.state.ship_two.merge(path: 'sprites/monochrome-ship.png')
      end
    
      # if both ship one and ship two are set, then determine collision
      if args.state.ship_one && args.state.ship_two
        # collision is determined using the intersect_rect? method
        if args.state.ship_one.intersect_rect? args.state.ship_two
          # if collision occurred, render the words collision!
          args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: 42,
                                       y: 5,
                                       text: "Collision!",
                                       alignment_enum: 1)
        else
          # if collision occurred, render the words no collision.
          args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: 42,
                                       y: 5,
                                       text: "No Collision.",
                                       alignment_enum: 1)
        end
      else
        # if both ship one and ship two aren't set, then render --
          args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: 42,
                                       y: 6,
                                       text: "--",
                                       alignment_enum: 1)
      end
    end
    
    # =============================================================================
    # ==== HOW TO CREATE BUTTONS ==================================================
    # =============================================================================
    def how_to_create_buttons args
      # Define a button style
      args.state.button_style = { w: 82, h: 10, }
    
      # Render instructions
      args.state.button_message ||= "Press a Button!"
      args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: 42,
                                       y: 82,
                                       text: args.state.button_message,
                                       alignment_enum: 1)
    
    
      # Creates button one using a border and a label
      args.state.button_one_border = args.state.button_style.merge( x: 1, y: 32)
      args.nokia.borders << args.state.button_one_border
      args.nokia.labels  << args.nokia
                                 .default_label
                                 .merge(x: args.state.button_one_border.x + 2,
                                        y: args.state.button_one_border.y + NOKIA_FONT_SM_HEIGHT + 2,
                                        text: "Button One")
    
      # Creates button two using a border and a label
      args.state.button_two_border = args.state.button_style.merge( x: 1, y: 20)
    
      args.nokia.borders << args.state.button_two_border
      args.nokia.labels << args.nokia
                                .default_label
                                .merge(x: args.state.button_two_border.x + 2,
                                       y: args.state.button_two_border.y + NOKIA_FONT_SM_HEIGHT + 2,
                                       text: "Button Two")
    
      # Initialize the state variable that tracks which button was clicked to "" (empty stringI
      args.state.last_button_clicked ||= "--"
    
      # If a click occurs, check to see if either button one, or button two was clicked
      # using the inside_rect? method of the mouse
      # set args.state.last_button_clicked accordingly
      if args.nokia.mouse_click
        if args.nokia.mouse_click.inside_rect? args.state.button_one_border
          args.state.last_button_clicked = "One Clicked!"
        elsif args.nokia.mouse_click.inside_rect? args.state.button_two_border
          args.state.last_button_clicked = "Two Clicked!"
        else
          args.state.last_button_clicked = "--"
        end
      end
    
      # Render the current value of args.state.last_button_clicked
      args.nokia.labels << args.nokia
                                 .default_label
                                 .merge(x: 42,
                                        y: 5,
                                        text: args.state.last_button_clicked,
                                        alignment_enum: 1)
    end
    
    def render_debug args
      if !args.state.grid_rendered
        (NOKIA_HEIGHT + 1).map_with_index do |i|
          args.outputs.static_debug << {
            x:  NOKIA_X_OFFSET,
            y:  NOKIA_Y_OFFSET + (i * NOKIA_ZOOM),
            x2: NOKIA_X_OFFSET + NOKIA_ZOOMED_WIDTH,
            y2: NOKIA_Y_OFFSET + (i * NOKIA_ZOOM),
            r: 128,
            g: 128,
            b: 128,
            a: 80
          }.line
        end
    
        (NOKIA_WIDTH + 1).map_with_index do |i|
          args.outputs.static_debug << {
            x:  NOKIA_X_OFFSET + (i * NOKIA_ZOOM),
            y:  NOKIA_Y_OFFSET,
            x2: NOKIA_X_OFFSET + (i * NOKIA_ZOOM),
            y2: NOKIA_Y_OFFSET + NOKIA_ZOOMED_HEIGHT,
            r: 128,
            g: 128,
            b: 128,
            a: 80
          }.line
        end
      end
    
      args.state.grid_rendered = true
    
      args.state.last_click ||= 0
      args.state.last_up    ||= 0
      args.state.last_click   = Kernel.tick_count if args.nokia.mouse_down # you can also use args.nokia.click
      args.state.last_up      = Kernel.tick_count if args.nokia.mouse_up
      args.state.label_style  = { size_enum: -1.5 }
    
      args.state.watch_list = [
        "Kernel.tick_count is:          #{Kernel.tick_count}",
        "args.nokia.mouse_position is:  #{args.nokia.mouse_position.x}, #{args.nokia.mouse_position.y}",
        "args.nokia.mouse_down tick:    #{args.state.last_click || "never"}",
        "args.nokia.mouse_up tick:      #{args.state.last_up || "false"}",
      ]
    
      args.outputs.debug << args.state
                                .watch_list
                                .map_with_index do |text, i|
        {
          x: 5,
          y: 720 - (i * 18),
          text: text,
          size_enum: -1.5,
          r: 255, g: 255, b: 255
        }.label!
      end
    
      args.outputs.debug << {
        x: 640,
        y:  25,
        text: "INFO: dev mode is currently enabled. Comment out the invocation of ~render_debug~ within the ~tick~ method to hide the debug layer.",
        size_enum: -0.5,
        alignment_enum: 1,
        r: 255, g: 255, b: 255
      }.label!
    end
    
    def snake_demo args
    
    end
    
    $gtk.reset
    
    

    Nokia 3310 - nokia.rb link

    # ./samples/99_genre_lowrez/nokia_3310/app/nokia.rb
    # Emulation of a 64x64 canvas. Don't change this file unless you know what you're doing :-)
    # Head over to main.rb and study the code there.
    
    NOKIA_WIDTH           = 84
    NOKIA_HEIGHT          = 48
    NOKIA_ZOOM            = 12
    NOKIA_ZOOMED_WIDTH    = NOKIA_WIDTH  * NOKIA_ZOOM
    NOKIA_ZOOMED_HEIGHT   = NOKIA_HEIGHT * NOKIA_ZOOM
    NOKIA_X_OFFSET        = (1280 - NOKIA_ZOOMED_WIDTH).half
    NOKIA_Y_OFFSET        = ( 720 - NOKIA_ZOOMED_HEIGHT).half
    
    NOKIA_FONT_XL         = -1
    NOKIA_FONT_XL_HEIGHT  = 20
    
    NOKIA_FONT_LG         = -3.5
    NOKIA_FONT_LG_HEIGHT  = 15
    
    NOKIA_FONT_MD         = -6
    NOKIA_FONT_MD_HEIGHT  = 10
    
    NOKIA_FONT_SM         = -8.5
    NOKIA_FONT_SM_HEIGHT  = 5
    
    NOKIA_FONT_PATH       = 'fonts/lowrez.ttf'
    
    
    class NokiaOutputs
      attr_accessor :width, :height
    
      def initialize args
        @args = args
      end
    
      def outputs_nokia
        return @args.outputs if Kernel.tick_count <= 0
        return @args.outputs[:nokia].transient!
      end
    
      def solids
        outputs_nokia.solids
      end
    
      def borders
        outputs_nokia.borders
      end
    
      def sprites
        outputs_nokia.sprites
      end
    
      def labels
        outputs_nokia.labels
      end
    
      def default_label
        {
          x: 0,
          y: 63,
          text: "",
          size_enum: NOKIA_FONT_SM,
          alignment_enum: 0,
          r: 0,
          g: 0,
          b: 0,
          a: 255,
          font: NOKIA_FONT_PATH
        }
      end
    
      def lines
        outputs_nokia.lines
      end
    
      def primitives
        outputs_nokia.primitives
      end
    
      def click
        return nil unless @args.inputs.mouse.click
        mouse
      end
    
      def mouse_click
        click
      end
    
      def mouse_down
        @args.inputs.mouse.down
      end
    
      def mouse_up
        @args.inputs.mouse.up
      end
    
      def mouse
        [
          ((@args.inputs.mouse.x - NOKIA_X_OFFSET).idiv(NOKIA_ZOOM)),
          ((@args.inputs.mouse.y - NOKIA_Y_OFFSET).idiv(NOKIA_ZOOM))
        ]
      end
    
      def mouse_position
        mouse
      end
    
      def keyboard
        @args.inputs.keyboard
      end
    end
    
    class GTK::Args
      def init_nokia
        return if @nokia
        @nokia = NokiaOutputs.new self
      end
    
      def nokia
        @nokia
      end
    end
    
    module GTK
      class Runtime
        alias_method :__original_tick_core__, :tick_core unless Runtime.instance_methods.include?(:__original_tick_core__)
    
        def tick_core
          @args.init_nokia
    
          __original_tick_core__
    
          return if Kernel.tick_count <= 0
    
          @args.render_target(:nokia)
               .labels
               .each do |l|
            l.y  += 1
            if (l.a || 255) > 128
              l.r = 67
              l.g = 82
              l.b = 61
              l.a = 255
            else
              l.a = 0
            end
          end
    
          @args.render_target(:nokia)
               .sprites
               .each do |s|
            if (s.a || 255) > 128
              s.a = 255
            else
              s.a = 0
            end
          end
    
          @args.render_target(:nokia)
               .solids
               .each do |s|
            if (s.a || 255) > 128
              s.r = 67
              s.g = 82
              s.b = 61
              s.a = 255
            else
              s.a = 0
            end
          end
    
          @args.render_target(:nokia)
               .borders
               .each do |s|
            if (s.a || 255) > 128
              s.r = 67
              s.g = 82
              s.b = 61
              s.a = 255
            else
              s.a = 0
            end
          end
    
          @args.render_target(:nokia)
               .lines
               .each do |l|
            l.y  += 1
            l.y2 += 1
            l.y2 += 1 if l.y != l.y2
            l.x2 += 1 if l.x != l.x2
    
            if (l.a || 255) > 128
              l.r = 67
              l.g = 82
              l.b = 61
              l.a = 255
            else
              l.a = 0
            end
          end
    
          @args.outputs.borders << {
            x: NOKIA_X_OFFSET      - 1,
            y: NOKIA_Y_OFFSET      - 1,
            w: NOKIA_ZOOMED_WIDTH  + 2,
            h: NOKIA_ZOOMED_HEIGHT + 2,
            r: 128, g: 128, b: 128
          }
    
    
          @args.outputs.background_color = [199, 240, 216]
    
          @args.outputs.solids << [0, 0, NOKIA_X_OFFSET, 720]
          @args.outputs.solids << [0, 0, 1280, NOKIA_Y_OFFSET]
          @args.outputs.solids << [NOKIA_X_OFFSET + NOKIA_ZOOMED_WIDTH, 0, NOKIA_X_OFFSET, 720]
          @args.outputs.solids << [0, NOKIA_Y_OFFSET.from_top, 1280, NOKIA_Y_OFFSET]
    
          @args.outputs
               .sprites << { x: NOKIA_X_OFFSET,
                             y: NOKIA_Y_OFFSET,
                             w: NOKIA_ZOOMED_WIDTH,
                             h: NOKIA_ZOOMED_HEIGHT,
                             source_x: 0,
                             source_y: 0,
                             source_w: NOKIA_WIDTH,
                             source_h: NOKIA_HEIGHT,
                             path: :nokia }
    
          if [email protected]_rendered
            (NOKIA_HEIGHT + 1).map_with_index do |i|
              @args.outputs.static_lines << {
                x:  NOKIA_X_OFFSET,
                y:  NOKIA_Y_OFFSET + (i * NOKIA_ZOOM),
                x2: NOKIA_X_OFFSET + NOKIA_ZOOMED_WIDTH,
                y2: NOKIA_Y_OFFSET + (i * NOKIA_ZOOM),
                r: 199,
                g: 240,
                b: 216,
                a: 100
              }.line!
            end
    
            (NOKIA_WIDTH + 1).map_with_index do |i|
              @args.outputs.static_lines << {
                x:  NOKIA_X_OFFSET + (i * NOKIA_ZOOM),
                y:  NOKIA_Y_OFFSET,
                x2: NOKIA_X_OFFSET + (i * NOKIA_ZOOM),
                y2: NOKIA_Y_OFFSET + NOKIA_ZOOMED_HEIGHT,
                r: 199,
                g: 240,
                b: 216,
                a: 100
              }.line!
            end
    
            @args.state.overlay_rendered = true
          end
        end
      end
    end
    
    

    Resolution 64x64 - lowrez.rb link

    # ./samples/99_genre_lowrez/resolution_64x64/app/lowrez.rb
    # Emulation of a 64x64 canvas. Don't change this file unless you know what you're doing :-)
    # Head over to main.rb and study the code there.
    
    LOWREZ_SIZE            = 64
    LOWREZ_ZOOM            = 10
    LOWREZ_ZOOMED_SIZE     = LOWREZ_SIZE * LOWREZ_ZOOM
    LOWREZ_X_OFFSET        = (1280 - LOWREZ_ZOOMED_SIZE).half
    LOWREZ_Y_OFFSET        = ( 720 - LOWREZ_ZOOMED_SIZE).half
    
    LOWREZ_FONT_XL         = -1
    LOWREZ_FONT_XL_HEIGHT  = 20
    
    LOWREZ_FONT_LG         = -3.5
    LOWREZ_FONT_LG_HEIGHT  = 15
    
    LOWREZ_FONT_MD         = -6
    LOWREZ_FONT_MD_HEIGHT  = 10
    
    LOWREZ_FONT_SM         = -8.5
    LOWREZ_FONT_SM_HEIGHT  = 5
    
    LOWREZ_FONT_PATH       = 'fonts/lowrez.ttf'
    
    
    class LowrezOutputs
      attr_accessor :width, :height
    
      def initialize args
        @args = args
        @background_color ||= [0, 0, 0]
        @args.outputs.background_color = @background_color
      end
    
      def background_color
        @background_color ||= [0, 0, 0]
      end
    
      def background_color= opts
        @background_color = opts
        @args.outputs.background_color = @background_color
    
        outputs_lowrez.solids << [0, 0, LOWREZ_SIZE, LOWREZ_SIZE, @background_color]
      end
    
      def outputs_lowrez
        return @args.outputs if Kernel.tick_count <= 0
        return @args.outputs[:lowrez]
      end
    
      def solids
        outputs_lowrez.solids
      end
    
      def borders
        outputs_lowrez.borders
      end
    
      def sprites
        outputs_lowrez.sprites
      end
    
      def labels
        outputs_lowrez.labels
      end
    
      def default_label
        {
          x: 0,
          y: 63,
          text: "",
          size_enum: LOWREZ_FONT_SM,
          alignment_enum: 0,
          r: 0,
          g: 0,
          b: 0,
          a: 255,
          font: LOWREZ_FONT_PATH
        }
      end
    
      def lines
        outputs_lowrez.lines
      end
    
      def primitives
        outputs_lowrez.primitives
      end
    
      def click
        return nil unless @args.inputs.mouse.click
        mouse
      end
    
      def mouse_click
        click
      end
    
      def mouse_down
        @args.inputs.mouse.down
      end
    
      def mouse_up
        @args.inputs.mouse.up
      end
    
      def mouse
        [
          ((@args.inputs.mouse.x - LOWREZ_X_OFFSET).idiv(LOWREZ_ZOOM)),
          ((@args.inputs.mouse.y - LOWREZ_Y_OFFSET).idiv(LOWREZ_ZOOM))
        ]
      end
    
      def mouse_position
        mouse
      end
    
      def keyboard
        @args.inputs.keyboard
      end
    end
    
    class GTK::Args
      def init_lowrez
        return if @lowrez
        @lowrez = LowrezOutputs.new self
      end
    
      def lowrez
        @lowrez
      end
    end
    
    module GTK
      class Runtime
        alias_method :__original_tick_core__, :tick_core unless Runtime.instance_methods.include?(:__original_tick_core__)
    
        def tick_core
          @args.init_lowrez
          __original_tick_core__
    
          return if Kernel.tick_count <= 0
    
          @args.render_target(:lowrez)
               .labels
               .each do |l|
            l.y  += 1
          end
    
          @args.render_target(:lowrez)
               .lines
               .each do |l|
            l.y  += 1
            l.y2 += 1
            l.y2 += 1 if l.y != l.y2
            l.x2 += 1 if l.x != l.x2
          end
    
          @args.outputs
               .sprites << { x: 320,
                             y: 40,
                             w: 640,
                             h: 640,
                             source_x: 0,
                             source_y: 0,
                             source_w: 64,
                             source_h: 64,
                             path: :lowrez }
        end
      end
    end
    
    

    Resolution 64x64 - main.rb link

    # ./samples/99_genre_lowrez/resolution_64x64/app/main.rb
    require 'app/lowrez.rb'
    
    def tick args
      # How to set the background color
      args.lowrez.background_color = [255, 255, 255]
    
      # ==== HELLO WORLD ======================================================
      # Steps to get started:
      # 1. ~def tick args~ is the entry point for your game.
      # 2. There are quite a few code samples below, remove the "##"
      #    before each line and save the file to see the changes.
      # 3. 0,  0 is in bottom left and 63, 63 is in top right corner.
      # 4. Be sure to come to the discord channel if you need
      #    more help: [[http://discord.dragonruby.org]].
    
      # Commenting and uncommenting code:
      # - Add a "#" infront of lines to comment out code
      # - Remove the "#" infront of lines to comment out code
    
      # Invoke the hello_world subroutine/method
      hello_world args # <---- add a "#" to the beginning of the line to stop running this subroutine/method.
      # =======================================================================
    
    
      # ==== HOW TO RENDER A LABEL ============================================
      # Uncomment the line below to invoke the how_to_render_a_label subroutine/method.
      # Note: The method is defined in this file with the signature ~def how_to_render_a_label args~
      #       Scroll down to the method to see the details.
    
      # Remove the "#" at the beginning of the line below
      # how_to_render_a_label args # <---- remove the "#" at the begging of this line to run the method
      # =======================================================================
    
    
      # ==== HOW TO RENDER A FILLED SQUARE (SOLID) ============================
      # Remove the "#" at the beginning of the line below
      # how_to_render_solids args
      # =======================================================================
    
    
      # ==== HOW TO RENDER AN UNFILLED SQUARE (BORDER) ========================
      # Remove the "#" at the beginning of the line below
      # how_to_render_borders args
      # =======================================================================
    
    
      # ==== HOW TO RENDER A LINE =============================================
      # Remove the "#" at the beginning of the line below
      # how_to_render_lines args
      # =======================================================================
    
    
      # == HOW TO RENDER A SPRITE =============================================
      # Remove the "#" at the beginning of the line below
      # how_to_render_sprites args
      # =======================================================================
    
    
      # ==== HOW TO MOVE A SPRITE BASED OFF OF USER INPUT =====================
      # Remove the "#" at the beginning of the line below
      # how_to_move_a_sprite args
      # =======================================================================
    
    
      # ==== HOW TO ANIMATE A SPRITE (SEPERATE PNGS) ==========================
      # Remove the "#" at the beginning of the line below
      # how_to_animate_a_sprite args
      # =======================================================================
    
    
      # ==== HOW TO ANIMATE A SPRITE (SPRITE SHEET) ===========================
      # Remove the "#" at the beginning of the line below
      # how_to_animate_a_sprite_sheet args
      # =======================================================================
    
    
      # ==== HOW TO DETERMINE COLLISION =============================================
      # Remove the "#" at the beginning of the line below
      # how_to_determine_collision args
      # =======================================================================
    
    
      # ==== HOW TO CREATE BUTTONS ==================================================
      # Remove the "#" at the beginning of the line below
      # how_to_create_buttons args
      # =======================================================================
    
    
      # ==== The line below renders a debug grid, mouse information, and current tick
      render_debug args
    end
    
    def hello_world args
      args.lowrez.solids  << { x: 0, y: 64, w: 10, h: 10, r: 255 }
    
      args.lowrez.labels  << {
        x: 32,
        y: 63,
        text: "lowrezjam 2020",
        size_enum: LOWREZ_FONT_SM,
        alignment_enum: 1,
        r: 0,
        g: 0,
        b: 0,
        a: 255,
        font: LOWREZ_FONT_PATH
      }
    
      args.lowrez.sprites << {
        x: 32 - 10,
        y: 32 - 10,
        w: 20,
        h: 20,
        path: 'sprites/lowrez-ship-blue.png',
        a: Kernel.tick_count % 255,
        angle: Kernel.tick_count % 360
      }
    end
    
    
    # =======================================================================
    # ==== HOW TO RENDER A LABEL ============================================
    # =======================================================================
    def how_to_render_a_label args
      # NOTE: Text is aligned from the TOP LEFT corner
    
      # Render an EXTRA LARGE/XL label (remove the "#" in front of each line below)
      args.lowrez.labels << { x: 0, y: 57, text: "Hello World",
                             size_enum: LOWREZ_FONT_XL,
                             r: 0, g: 0, b: 0, a: 255,
                             font: LOWREZ_FONT_PATH }
    
      # Render a LARGE/LG label (remove the "#" in front of each line below)
      args.lowrez.labels << { x: 0, y: 36, text: "Hello World",
                              size_enum: LOWREZ_FONT_LG,
                              r: 0, g: 0, b: 0, a: 255,
                              font: LOWREZ_FONT_PATH }
    
      # Render a MEDIUM/MD label (remove the "#" in front of each line below)
      args.lowrez.labels << { x: 0, y: 20, text: "Hello World",
                              size_enum: LOWREZ_FONT_MD,
                              r: 0, g: 0, b: 0, a: 255,
                              font: LOWREZ_FONT_PATH }
    
      # Render a SMALL/SM label (remove the "#" in front of each line below)
      args.lowrez.labels << { x: 0, y: 9, text: "Hello World",
                              size_enum: LOWREZ_FONT_SM,
                              r: 0, g: 0, b: 0, a: 255,
                              font: LOWREZ_FONT_PATH }
    
      # You are provided args.lowrez.default_label which returns a Hash that you
      # can ~merge~ properties with
      # Example 1
      args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(text: "Default")
    
      # Example 2
      args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(x: 31,
                                       text: "Default",
                                       r: 128,
                                       g: 128,
                                       b: 128)
    end
    
    ## # =============================================================================
    ## # ==== HOW TO RENDER FILLED SQUARES (SOLIDS) ==================================
    ## # =============================================================================
    def how_to_render_solids args
      # Render a red square at 0, 0 with a width and height of 1
      args.lowrez.solids << { x: 0, y: 0, w: 1, h: 1, r: 255, g: 0, b: 0, a: 255 }
    
      # Render a red square at 1, 1 with a width and height of 2
      args.lowrez.solids << { x: 1, y: 1, w: 2, h: 2, r: 255, g: 0, b: 0, a: 255 }
    
      # Render a red square at 3, 3 with a width and height of 3
      args.lowrez.solids << { x: 3, y: 3, w: 3, h: 3, r: 255, g: 0, b: 0 }
    
      # Render a red square at 6, 6 with a width and height of 4
      args.lowrez.solids << { x: 6, y: 6, w: 4, h: 4, r: 255, g: 0, b: 0 }
    end
    
    ## # =============================================================================
    ## # ==== HOW TO RENDER UNFILLED SQUARES (BORDERS) ===============================
    ## # =============================================================================
    def how_to_render_borders args
      # Render a red square at 0, 0 with a width and height of 3
      args.lowrez.borders << { x: 0, y: 0, w: 3, h: 3, r: 255, g: 0, b: 0, a: 255 }
    
      # Render a red square at 3, 3 with a width and height of 3
      args.lowrez.borders << { x: 3, y: 3, w: 4, h: 4, r: 255, g: 0, b: 0, a: 255 }
    
      # Render a red square at 5, 5 with a width and height of 4
      args.lowrez.borders << { x: 7, y: 7, w: 5, h: 5, r: 255, g: 0, b: 0, a: 255 }
    end
    
    ## # =============================================================================
    ## # ==== HOW TO RENDER A LINE ===================================================
    ## # =============================================================================
    def how_to_render_lines args
      # Render a horizontal line at the bottom
      args.lowrez.lines << { x: 0, y: 0, x2: 63, y2:  0, r: 255 }
    
      # Render a vertical line at the left
      args.lowrez.lines << { x: 0, y: 0, x2:  0, y2: 63, r: 255 }
    
      # Render a diagonal line starting from the bottom left and going to the top right
      args.lowrez.lines << { x: 0, y: 0, x2: 63, y2: 63, r: 255 }
    end
    
    ## # =============================================================================
    ## # == HOW TO RENDER A SPRITE ===================================================
    ## # =============================================================================
    def how_to_render_sprites args
      # Loop 10 times and create 10 sprites in 10 positions
      # Render a sprite at the bottom left with a width and height of 5 and a path of 'sprites/lowrez-ship-blue.png'
      10.times do |i|
        args.lowrez.sprites << {
          x: i * 5,
          y: i * 5,
          w: 5,
          h: 5,
          path: 'sprites/lowrez-ship-blue.png'
        }
      end
    
      # Given an array of positions create sprites
      positions = [
        { x: 10, y: 42 },
        { x: 15, y: 45 },
        { x: 22, y: 33 },
      ]
    
      positions.each do |position|
        # use Ruby's ~Hash#merge~ function to create a sprite
        args.lowrez.sprites << position.merge(path: 'sprites/lowrez-ship-red.png',
                                              w: 5,
                                              h: 5)
      end
    end
    
    ## # =============================================================================
    ## # ==== HOW TO ANIMATE A SPRITE (SEPERATE PNGS) ==========================
    ## # =============================================================================
    def how_to_animate_a_sprite args
      # STEP 1: Define when you want the animation to start. The animation in this case will start in 3 seconds
      start_animation_on_tick = 180
    
      # STEP 2: Get the frame_index given the start tick.
      sprite_index = start_animation_on_tick.frame_index count: 7,     # how many sprites?
                                                         hold_for: 4,  # how long to hold each sprite?
                                                         repeat: true  # should it repeat?
    
      # STEP 3: frame_index will return nil if the frame hasn't arrived yet
      if sprite_index
        # if the sprite_index is populated, use it to determine the sprite path and render it
        sprite_path  = "sprites/explosion-#{sprite_index}.png"
        args.lowrez.sprites << { x: 0, y: 0, w: 64, h: 64, path: sprite_path }
      else
        # if the sprite_index is nil, render a countdown instead
        countdown_in_seconds = ((start_animation_on_tick - Kernel.tick_count) / 60).round(1)
    
        args.lowrez.labels  << args.lowrez
                                   .default_label
                                   .merge(x: 32,
                                          y: 32,
                                          text: "Count Down: #{countdown_in_seconds}",
                                          alignment_enum: 1)
      end
    
      # render the current tick and the resolved sprite index
      args.lowrez.labels  << args.lowrez
                                   .default_label
                                   .merge(x: 0,
                                          y: 11,
                                          text: "Tick: #{Kernel.tick_count}")
      args.lowrez.labels  << args.lowrez
                                   .default_label
                                   .merge(x: 0,
                                          y: 5,
                                          text: "sprite_index: #{sprite_index}")
    end
    
    ## # =============================================================================
    ## # ==== HOW TO ANIMATE A SPRITE (SPRITE SHEET) =================================
    ## # =============================================================================
    def how_to_animate_a_sprite_sheet args
      # STEP 1: Define when you want the animation to start. The animation in this case will start in 3 seconds
      start_animation_on_tick = 180
    
      # STEP 2: Get the frame_index given the start tick.
      sprite_index = start_animation_on_tick.frame_index count: 7,     # how many sprites?
                                                         hold_for: 4,  # how long to hold each sprite?
                                                         repeat: true  # should it repeat?
    
      # STEP 3: frame_index will return nil if the frame hasn't arrived yet
      if sprite_index
        # if the sprite_index is populated, use it to determine the source rectangle and render it
        args.lowrez.sprites << {
          x: 0,
          y: 0,
          w: 64,
          h: 64,
          path:  "sprites/explosion-sheet.png",
          source_x: 32 * sprite_index,
          source_y: 0,
          source_w: 32,
          source_h: 32
        }
      else
        # if the sprite_index is nil, render a countdown instead
        countdown_in_seconds = ((start_animation_on_tick - Kernel.tick_count) / 60).round(1)
    
        args.lowrez.labels  << args.lowrez
                                   .default_label
                                   .merge(x: 32,
                                          y: 32,
                                          text: "Count Down: #{countdown_in_seconds}",
                                          alignment_enum: 1)
      end
    
      # render the current tick and the resolved sprite index
      args.lowrez.labels  << args.lowrez
                                   .default_label
                                   .merge(x: 0,
                                          y: 11,
                                          text: "tick: #{Kernel.tick_count}")
      args.lowrez.labels  << args.lowrez
                                   .default_label
                                   .merge(x: 0,
                                          y: 5,
                                          text: "sprite_index: #{sprite_index}")
    end
    
    ## # =============================================================================
    ## # ==== HOW TO STORE STATE, ACCEPT INPUT, AND RENDER SPRITE BASED OFF OF STATE =
    ## # =============================================================================
    def how_to_move_a_sprite args
      args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(x: 32,
                                       y: 62, text: "Use Arrow Keys",
                                       alignment_enum: 1)
    
      args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(x: 32,
                                       y: 56, text: "Use WASD",
                                       alignment_enum: 1)
    
      args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(x: 32,
                                       y: 50, text: "Or Click",
                                       alignment_enum: 1)
    
      # set the initial values for x and y using ||= ("or equal operator")
      args.state.ship.x ||= 0
      args.state.ship.y ||= 0
    
      # if a mouse click occurs, update the ship's x and y to be the location of the click
      if args.lowrez.mouse_click
        args.state.ship.x = args.lowrez.mouse_click.x
        args.state.ship.y = args.lowrez.mouse_click.y
      end
    
      # if a or left arrow is pressed/held, decrement the ships x position
      if args.lowrez.keyboard.left
        args.state.ship.x -= 1
      end
    
      # if d or right arrow is pressed/held, increment the ships x position
      if args.lowrez.keyboard.right
        args.state.ship.x += 1
      end
    
      # if s or down arrow is pressed/held, decrement the ships y position
      if args.lowrez.keyboard.down
        args.state.ship.y -= 1
      end
    
      # if w or up arrow is pressed/held, increment the ships y position
      if args.lowrez.keyboard.up
        args.state.ship.y += 1
      end
    
      # render the sprite to the screen using the position stored in args.state.ship
      args.lowrez.sprites << {
        x: args.state.ship.x,
        y: args.state.ship.y,
        w: 5,
        h: 5,
        path: 'sprites/lowrez-ship-blue.png',
        # parameters beyond this point are optional
        angle: 0, # Note: rotation angle is denoted in degrees NOT radians
        r: 255,
        g: 255,
        b: 255,
        a: 255
      }
    end
    
    # =======================================================================
    # ==== HOW TO DETERMINE COLLISION =======================================
    # =======================================================================
    def how_to_determine_collision args
      # Render the instructions
      args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(x: 32,
                                       y: 62, text: "Click Anywhere",
                                       alignment_enum: 1)
    
      # if a mouse click occurs:
      # - set ship_one if it isn't set
      # - set ship_two if it isn't set
      # - otherwise reset ship one and ship two
      if args.lowrez.mouse_click
        # is ship_one set?
        if !args.state.ship_one
          args.state.ship_one = { x: args.lowrez.mouse_click.x - 10,
                                  y: args.lowrez.mouse_click.y - 10,
                                  w: 20,
                                  h: 20 }
        # is ship_one set?
        elsif !args.state.ship_two
          args.state.ship_two = { x: args.lowrez.mouse_click.x - 10,
                                  y: args.lowrez.mouse_click.y - 10,
                                  w: 20,
                                  h: 20 }
        # should we reset?
        else
          args.state.ship_one = nil
          args.state.ship_two = nil
        end
      end
    
      # render ship one if it's set
      if args.state.ship_one
        # use Ruby's .merge method which is available on ~Hash~ to set the sprite and alpha
        # render ship one
        args.lowrez.sprites << args.state.ship_one.merge(path: 'sprites/lowrez-ship-blue.png', a: 100)
      end
    
      if args.state.ship_two
        # use Ruby's .merge method which is available on ~Hash~ to set the sprite and alpha
        # render ship two
        args.lowrez.sprites << args.state.ship_two.merge(path: 'sprites/lowrez-ship-red.png', a: 100)
      end
    
      # if both ship one and ship two are set, then determine collision
      if args.state.ship_one && args.state.ship_two
        # collision is determined using the intersect_rect? method
        if args.state.ship_one.intersect_rect? args.state.ship_two
          # if collision occurred, render the words collision!
          args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(x: 31,
                                       y: 5,
                                       text: "Collision!",
                                       alignment_enum: 1)
        else
          # if collision occurred, render the words no collision.
          args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(x: 31,
                                       y: 5,
                                       text: "No Collision.",
                                       alignment_enum: 1)
        end
      else
        # if both ship one and ship two aren't set, then render --
          args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(x: 31,
                                       y: 6,
                                       text: "--",
                                       alignment_enum: 1)
      end
    end
    
    ## # =============================================================================
    ## # ==== HOW TO CREATE BUTTONS ==================================================
    ## # =============================================================================
    def how_to_create_buttons args
      # Define a button style
      args.state.button_style = { w: 62, h: 10, r: 80, g: 80, b: 80 }
      args.state.label_style  = { r: 80, g: 80, b: 80 }
    
      # Render instructions
      args.state.button_message ||= "Press a Button!"
      args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(args.state.label_style)
                                .merge(x: 32,
                                       y: 62,
                                       text: args.state.button_message,
                                       alignment_enum: 1)
    
    
      # Creates button one using a border and a label
      args.state.button_one_border = args.state.button_style.merge( x: 1, y: 32)
      args.lowrez.borders << args.state.button_one_border
      args.lowrez.labels  << args.lowrez
                                 .default_label
                                 .merge(args.state.label_style)
                                 .merge(x: args.state.button_one_border.x + 2,
                                        y: args.state.button_one_border.y + LOWREZ_FONT_SM_HEIGHT + 2,
                                        text: "Button One")
    
      # Creates button two using a border and a label
      args.state.button_two_border = args.state.button_style.merge( x: 1, y: 20)
    
      args.lowrez.borders << args.state.button_two_border
      args.lowrez.labels << args.lowrez
                                .default_label
                                .merge(args.state.label_style)
                                .merge(x: args.state.button_two_border.x + 2,
                                       y: args.state.button_two_border.y + LOWREZ_FONT_SM_HEIGHT + 2,
                                       text: "Button Two")
    
      # Initialize the state variable that tracks which button was clicked to "" (empty stringI
      args.state.last_button_clicked ||= "--"
    
      # If a click occurs, check to see if either button one, or button two was clicked
      # using the inside_rect? method of the mouse
      # set args.state.last_button_clicked accordingly
      if args.lowrez.mouse_click
        if args.lowrez.mouse_click.inside_rect? args.state.button_one_border
          args.state.last_button_clicked = "One Clicked!"
        elsif args.lowrez.mouse_click.inside_rect? args.state.button_two_border
          args.state.last_button_clicked = "Two Clicked!"
        else
          args.state.last_button_clicked = "--"
        end
      end
    
      # Render the current value of args.state.last_button_clicked
      args.lowrez.labels << args.lowrez
                                 .default_label
                                 .merge(args.state.label_style)
                                 .merge(x: 32,
                                        y: 5,
                                        text: args.state.last_button_clicked,
                                        alignment_enum: 1)
    end
    
    
    def render_debug args
      if !args.state.grid_rendered
        65.map_with_index do |i|
          args.outputs.static_debug << {
            x:  LOWREZ_X_OFFSET,
            y:  LOWREZ_Y_OFFSET + (i * 10),
            x2: LOWREZ_X_OFFSET + LOWREZ_ZOOMED_SIZE,
            y2: LOWREZ_Y_OFFSET + (i * 10),
            r: 128,
            g: 128,
            b: 128,
            a: 80
          }.line!
    
          args.outputs.static_debug << {
            x:  LOWREZ_X_OFFSET + (i * 10),
            y:  LOWREZ_Y_OFFSET,
            x2: LOWREZ_X_OFFSET + (i * 10),
            y2: LOWREZ_Y_OFFSET + LOWREZ_ZOOMED_SIZE,
            r: 128,
            g: 128,
            b: 128,
            a: 80
          }.line!
        end
      end
    
      args.state.grid_rendered = true
    
      args.state.last_click ||= 0
      args.state.last_up    ||= 0
      args.state.last_click   = Kernel.tick_count if args.lowrez.mouse_down # you can also use args.lowrez.click
      args.state.last_up      = Kernel.tick_count if args.lowrez.mouse_up
      args.state.label_style  = { size_enum: -1.5 }
    
      args.state.watch_list = [
        "Kernel.tick_count is:           #{Kernel.tick_count}",
        "args.lowrez.mouse_position is:  #{args.lowrez.mouse_position.x}, #{args.lowrez.mouse_position.y}",
        "args.lowrez.mouse_down tick:    #{args.state.last_click || "never"}",
        "args.lowrez.mouse_up tick:      #{args.state.last_up || "false"}",
      ]
    
      args.outputs.debug << args.state
                                .watch_list
                                .map_with_index do |text, i|
        {
          x: 5,
          y: 720 - (i * 20),
          text: text,
          size_enum: -1.5
        }.label!
      end
    
      args.outputs.debug << {
        x: 640,
        y:  25,
        text: "INFO: dev mode is currently enabled. Comment out the invocation of ~render_debug~ within the ~tick~ method to hide the debug layer.",
        size_enum: -0.5,
        alignment_enum: 1
      }.label!
    end
    
    $gtk.reset
    
    

    Genre Mario link

    Jumping - main.rb link

    # ./samples/99_genre_mario/01_jumping/app/main.rb
    def tick args
      defaults args
      input args
      calc args
      render args
    end
    
    def defaults args
      args.state.player ||= {
        x: Grid.w / 2,
        y: 0,
        w: 100,
        h: 100,
        dy: 0,
        action: :standing
      }
    
      args.state.jump ||= {
        power: 20,
        increase_frames: 10,
        increase_power: 1
      }
    
      args.state.gravity ||= -1
    end
    
    def input args
      if args.inputs.keyboard.key_down.space
        if args.state.player.action == :standing
          args.state.player.action = :jumping
          args.state.player.dy = args.state.jump.power
    
          # record when the action took place
          current_frame = Kernel.tick_count
          args.state.player.action_at = current_frame
        end
      end
    
      # if the space bar is being held
      if args.inputs.keyboard.key_held.space
        # is the player jumping
        is_jumping = args.state.player.action == :jumping
    
        # when was the jump performed
        time_of_jump = args.state.player.action_at
    
        # how much time has passed since the jump
        jump_elapsed_time = time_of_jump.elapsed_time
    
        # how much time is allowed for increasing power
        time_allowed = args.state.jump.increase_frames
    
        # if the player is jumping
        # and the elapsed time is less than
        # the allowed time
        if is_jumping && jump_elapsed_time < time_allowed
           # increase the dy by the increase power
           power_to_add = args.state.jump.increase_power
           args.state.player.dy += power_to_add
        end
      end
    end
    
    def calc args
      if args.state.player.action == :jumping
        args.state.player.y  += args.state.player.dy
        args.state.player.dy += args.state.gravity
      end
    
      if args.state.player.y < 0
        args.state.player.y      = 0
        args.state.player.action = :standing
      end
    end
    
    def render args
      args.outputs.sprites << {
        x: args.state.player.x -
           args.state.player.w / 2,
        y: args.state.player.y,
        w: args.state.player.w,
        h: args.state.player.h,
        path: 'sprites/square/red.png'
      }
    end
    
    

    Jumping And Collisions - main.rb link

    # ./samples/99_genre_mario/02_jumping_and_collisions/app/main.rb
    class Game
      attr_gtk
    
      def tick
        defaults
        input
        calc
        render
      end
    
      def defaults
        state.gravity              ||= -1
    
        player.x                   ||= 64
        player.y                   ||= 800
        player.w                   ||= 50
        player.h                   ||= 50
        player.dx                  ||= 0
        player.dy                  ||= 0
        player.on_ground           ||= false
    
        player.max_speed           ||= 20
        player.jump_power          ||= 15
        player.jump_air_time       ||= 15
        player.jump_increase_power ||= 1
    
        state.tile_size            ||= 64
        if !state.tiles
          state.tiles                = [
            { ordinal_x:  0, ordinal_y: 0 },
            { ordinal_x:  1, ordinal_y: 0 },
            { ordinal_x:  2, ordinal_y: 0 },
            { ordinal_x:  3, ordinal_y: 0 },
            { ordinal_x:  4, ordinal_y: 0 },
            { ordinal_x:  5, ordinal_y: 0 },
            { ordinal_x:  6, ordinal_y: 0 },
            { ordinal_x:  7, ordinal_y: 0 },
            { ordinal_x:  8, ordinal_y: 0 },
            { ordinal_x:  9, ordinal_y: 0 },
            { ordinal_x: 10, ordinal_y: 0 },
            { ordinal_x: 11, ordinal_y: 0 },
            { ordinal_x: 12, ordinal_y: 0 },
            { ordinal_x: 13, ordinal_y: 0 },
    
            { ordinal_x:  3, ordinal_y: 1 },
            { ordinal_x:  3, ordinal_y: 2 },
            { ordinal_x:  6, ordinal_y: 1 },
            { ordinal_x:  6, ordinal_y: 2 },
    
            { ordinal_x:  9, ordinal_y: 3 },
            { ordinal_x: 10, ordinal_y: 3 },
            { ordinal_x: 11, ordinal_y: 3 },
    
            { ordinal_x: 10, ordinal_y: 4 },
            { ordinal_x: 11, ordinal_y: 4 },
    
            { ordinal_x: 11, ordinal_y: 5 },
    
            { ordinal_x: 12, ordinal_y: 2 },
          ]
    
          state.tiles.each do |t|
            t.rect = { x: t.ordinal_x * state.tile_size,
                       y: t.ordinal_y * state.tile_size,
                       w: state.tile_size,
                       h: state.tile_size }
          end
        end
      end
    
      def input
        input_jump
        input_move
      end
    
      def input_jump
        if inputs.keyboard.key_down.space
          player_jump
        end
    
        if inputs.keyboard.key_held.space
          player_jump_increase_air_time
        end
      end
    
      def input_move
        if inputs.keyboard.left
          player.dx -= 2
        elsif inputs.keyboard.right
          player.dx += 2
        end
        player.dx = player.dx.clamp(-player.max_speed, player.max_speed)
      end
    
      def calc
        calc_physics
        calc_game_over
      end
    
      def calc_physics
        player.x  += player.dx
        collision = state.tiles.find { |t| player.intersect_rect? t.rect }
        if collision
          if player.dx > 0
            player.x = collision.rect.x - player.w
          elsif player.dx < 0
            player.x = collision.rect.x + collision.rect.w
          end
        end
        player.dx *= 0.8
    
        player.y += player.dy
        collision = state.tiles.find { |t| player.intersect_rect? t.rect }
        if collision
          if player.dy > 0
            player.y = collision.rect.y - player.h
          elsif player.dy < 0
            player.y = collision.rect.y + collision.rect.h
          end
          player.dy = 0
          player.jump_at = nil
          player.on_ground = true
        else
          player.on_ground = false
        end
        player.dy = player.dy + state.gravity
        player.dy = player.dy.clamp(-state.tile_size, state.tile_size)
      end
    
      def calc_game_over
        if player.y < -64
          player.y = 800
          player.dx = 0
          player.dy = 0
        end
      end
    
      def render
        render_player
        render_tiles
        # render_grid
      end
    
      def render_player
        outputs.sprites << {
          x: player.x,
          y: player.y,
          w: player.w,
          h: player.h,
          path: 'sprites/square/red.png'
        }
      end
    
      def render_tiles
        outputs.sprites << state.tiles.map do |t|
          t.merge path: 'sprites/square/white.png',
                  x: t.ordinal_x * state.tile_size,
                  y: t.ordinal_y * state.tile_size,
                  w: state.tile_size,
                  h: state.tile_size
        end
      end
    
      def render_grid
        if Kernel.tick_count == 0
          outputs[:grid].background_color = [0, 0, 0, 0]
          outputs[:grid].borders << available_brick_locations
          outputs[:grid].labels  << available_brick_locations.map do |b|
            [
              b.merge(text: "#{b.ordinal_x},#{b.ordinal_y}",
                      x: b.x + 2,
                      y: b.y + 2,
                      size_enum: -3,
                      vertical_alignment_enum: 0,
                      blendmode_enum: 0),
              b.merge(text: "#{b.x},#{b.y}",
                      x: b.x + 2,
                      y: b.y + 2 + 20,
                      size_enum: -3,
                      vertical_alignment_enum: 0,
                      blendmode_enum: 0)
            ]
          end
        end
    
        outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: :grid }
      end
    
      def available_brick_locations
        (0..19).to_a
          .product(0..11)
          .map do |(ordinal_x, ordinal_y)|
          { ordinal_x: ordinal_x,
            ordinal_y: ordinal_y,
            x: ordinal_x * state.tile_size,
            y: ordinal_y * state.tile_size,
            w: state.tile_size,
            h: state.tile_size }
        end
      end
    
      def player
        state.player ||= args.state.new_entity :player
      end
    
      def player_jump
        return if !player.on_ground
        player.dy = state.player.jump_power
        player.jump_at = Kernel.tick_count
      end
    
      def player_jump_increase_air_time
        return if !player.jump_at
        return if player.jump_at.elapsed_time >= player.jump_air_time
        player.dy += player.jump_increase_power
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args
      $game.tick
    end
    
    $gtk.reset
    
    

    Genre Platformer link

    Clepto Frog - main.rb link

    # ./samples/99_genre_platformer/clepto_frog/app/main.rb
    class CleptoFrog
      attr_gtk
    
      def tick
        defaults
        render
        input
        calc
      end
    
      def defaults
        state.level_editor_rect_w ||= 32
        state.level_editor_rect_h     ||= 32
        state.target_camera_scale ||= 0.5
        state.camera_scale        ||= 1
        state.tongue_length       ||= 100
        state.action              ||= :aiming
        state.tongue_angle        ||= 90
        state.tile_size           ||= 32
        state.gravity             ||= -0.1
        state.drag                ||= -0.005
        state.player ||= {
          x: 2400,
          y: 200,
          w: 60,
          h: 60,
          dx: 0,
          dy: 0,
        }
        state.camera_x     ||= state.player.x - 640
        state.camera_y     ||= 0
        load_if_needed
        state.map_saved_at ||= 0
      end
    
      def player
        state.player
      end
    
      def render
        render_world
        render_player
        render_level_editor
        render_mini_map
        render_instructions
      end
    
      def to_camera_space rect
        rect.merge(x: to_camera_space_x(rect.x),
                   y: to_camera_space_y(rect.y),
                   w: to_camera_space_w(rect.w),
                   h: to_camera_space_h(rect.h))
      end
    
      def to_camera_space_x x
        return nil if !x
         (x * state.camera_scale) - state.camera_x
      end
    
      def to_camera_space_y y
        return nil if !y
        (y * state.camera_scale) - state.camera_y
      end
    
      def to_camera_space_w w
        return nil if !w
        w * state.camera_scale
      end
    
      def to_camera_space_h h
        return nil if !h
        h * state.camera_scale
      end
    
      def render_world
        viewport = {
          x: player.x - 1280 / state.camera_scale,
          y: player.y - 720 / state.camera_scale,
          w: 2560 / state.camera_scale,
          h: 1440 / state.camera_scale
        }
    
        outputs.sprites << geometry.find_all_intersect_rect(viewport, state.mugs).map do |rect|
          to_camera_space rect
        end
    
        outputs.sprites << geometry.find_all_intersect_rect(viewport, state.walls).map do |rect|
          to_camera_space(rect).merge!(path: :pixel, r: 128, g: 128, b: 128, a: 128)
        end
      end
    
      def render_player
        start_of_tongue_render = to_camera_space start_of_tongue
    
        if state.anchor_point
          anchor_point_render = to_camera_space state.anchor_point
          outputs.sprites << { x: start_of_tongue_render.x - 2,
                               y: start_of_tongue_render.y - 2,
                               w: to_camera_space_w(4),
                               h: geometry.distance(start_of_tongue_render, anchor_point_render),
                               path:  :pixel,
                               angle_anchor_y: 0,
                               r: 255, g: 128, b: 128,
                               angle: state.tongue_angle - 90 }
        else
          outputs.sprites << { x: to_camera_space_x(start_of_tongue.x) - 2,
                               y: to_camera_space_y(start_of_tongue.y) - 2,
                               w: to_camera_space_w(4),
                               h: to_camera_space_h(state.tongue_length),
                               path:  :pixel,
                               r: 255, g: 128, b: 128,
                               angle_anchor_y: 0,
                               angle: state.tongue_angle - 90 }
        end
    
        angle = 0
        if state.action == :aiming && !player.on_floor
          angle = state.tongue_angle - 90
        elsif state.action == :shooting && !player.on_floor
          angle = state.tongue_angle - 90
        elsif state.action == :anchored
          angle = state.tongue_angle - 90
        end
    
        outputs.sprites << to_camera_space(player).merge!(path: "sprites/square/green.png", angle: angle)
      end
    
      def render_mini_map
        x, y = 1170, 10
        outputs.primitives << { x: x,
                                y: y,
                                w: 100,
                                h: 58,
                                r: 0,
                                g: 0,
                                b: 0,
                                a: 200,
                                path: :pixel }
    
        outputs.primitives << { x: x + player.x.fdiv(100) - 1,
                                y: y + player.y.fdiv(100) - 1,
                                w: 2,
                                h: 2,
                                r: 0,
                                g: 255,
                                b: 0,
                                path: :pixel }
    
        t_start = start_of_tongue
        t_end = end_of_tongue
    
        outputs.primitives << {
          x: x + t_start.x.fdiv(100),
          y: y + t_start.y.fdiv(100),
          x2: x + t_end.x.fdiv(100),
          y2: y + t_end.y.fdiv(100),
          r: 255, g: 255, b: 255
        }
    
        outputs.primitives << state.mugs.map do |o|
          { x: x + o.x.fdiv(100) - 1,
            y: y + o.y.fdiv(100) - 1,
            w: 2,
            h: 2,
            r: 200,
            g: 200,
            b: 0,
            path: :pixel }
        end
      end
    
      def render_level_editor
        return if !state.level_editor_mode
        if state.map_saved_at > 0 && state.map_saved_at.elapsed_time < 120
          outputs.primitives << { x: 920, y: 670, text: 'Map has been exported!', size_enum: 1, r: 0, g: 50, b: 100, a: 50 }
        end
    
        outputs.primitives << { x: to_camera_space_x(((state.camera_x + inputs.mouse.x) / state.camera_scale).ifloor(state.tile_size)),
                                y: to_camera_space_y(((state.camera_y + inputs.mouse.y) / state.camera_scale).ifloor(state.tile_size)),
                                w: to_camera_space_w(state.level_editor_rect_w),
                                h: to_camera_space_h(state.level_editor_rect_h), path: :pixel, a: 200, r: 180, g: 80, b: 200 }
      end
    
      def render_instructions
        if state.level_editor_mode
          outputs.labels << { x: 640,
                              y: 10.from_top,
                              text: "Click to place wall. HJKL to change wall size. X + click to remove wall. M + click to place mug. Arrow keys to move around.",
                              size_enum: -1,
                              anchor_x: 0.5 }
          outputs.labels << { x: 640,
                              y: 35.from_top,
                              text: " - and + to zoom in and out. 0 to reset camera to default zoom. G to exit level editor mode.",
                              size_enum: -1,
                              anchor_x: 0.5 }
        else
          outputs.labels << { x: 640,
                              y: 10.from_top,
                              text: "Left and Right to aim tongue. Space to shoot or release tongue. G to enter level editor mode.",
                              size_enum: -1,
                              anchor_x: 0.5 }
    
          outputs.labels << { x: 640,
                              y: 35.from_top,
                              text: "Up and Down to change tongue length (when tongue is attached). Left and Right to swing (when tongue is attached).",
                              size_enum: -1,
                              anchor_x: 0.5 }
        end
      end
    
      def start_of_tongue
        {
          x: player.x + player.w / 2,
          y: player.y + player.h / 2
        }
      end
    
      def calc
        calc_camera
        calc_player
        calc_mug_collection
      end
    
      def calc_camera
        percentage = 0.2 * state.camera_scale
        target_scale = state.target_camera_scale
        distance_scale = target_scale - state.camera_scale
        state.camera_scale += distance_scale * percentage
    
        target_x = player.x * state.target_camera_scale
        target_y = player.y * state.target_camera_scale
    
        distance_x = target_x - (state.camera_x + 640)
        distance_y = target_y - (state.camera_y + 360)
        state.camera_x += distance_x * percentage if distance_x.abs > 1
        state.camera_y += distance_y * percentage if distance_y.abs > 1
        state.camera_x = 0 if state.camera_x < 0
        state.camera_y = 0 if state.camera_y < 0
      end
    
      def calc_player
        calc_shooting
        calc_swing
        calc_aabb_collision
        calc_tongue_angle
        calc_on_floor
      end
    
      def calc_shooting
        calc_shooting_step
        calc_shooting_step
        calc_shooting_step
        calc_shooting_step
        calc_shooting_step
        calc_shooting_step
      end
    
      def calc_shooting_step
        return unless state.action == :shooting
        state.tongue_length += 5
        potential_anchor = end_of_tongue
        anchor_rect = { x: potential_anchor.x - 5, y: potential_anchor.y - 5, w: 10, h: 10 }
        collision = state.walls.find_all do |v|
          v.intersect_rect?(anchor_rect)
        end.first
        if collision
          state.anchor_point = potential_anchor
          state.action = :anchored
        end
      end
    
      def calc_swing
        return if !state.anchor_point
        target_x = state.anchor_point.x - start_of_tongue.x
        target_y = state.anchor_point.y -
                   state.tongue_length - 5 - 20 - player.h
    
        diff_y = player.y - target_y
    
        distance = geometry.distance(player, state.anchor_point)
        pull_strength = if distance < 100
                          0
                        else
                          (distance / 800)
                        end
    
        vector = state.tongue_angle.to_vector
    
        player.dx += vector.x * pull_strength**2
        player.dy += vector.y * pull_strength**2
      end
    
      def calc_aabb_collision
        return if !state.walls
    
        player.dx = player.dx.clamp(-30, 30)
        player.dy = player.dy.clamp(-30, 30)
    
        player.dx += player.dx * state.drag
        player.x += player.dx
    
        collision = geometry.find_intersect_rect player, state.walls
    
        if collision
          if player.dx > 0
            player.x = collision.x - player.w
          elsif player.dx < 0
            player.x = collision.x + collision.w
          end
          player.dx *= -0.8
        end
    
        if !state.level_editor_mode
          player.dy += state.gravity  # Since acceleration is the change in velocity, the change in y (dy) increases every frame
          player.y += player.dy
        end
    
        collision = geometry.find_intersect_rect player, state.walls
    
        if collision
          if player.dy > 0
            player.y = collision.y - 60
          elsif player.dy < 0
            player.y = collision.y + collision.h
          end
    
          player.dy *= -0.8
        end
      end
    
      def calc_tongue_angle
        return unless state.anchor_point
        state.tongue_angle = geometry.angle_from state.anchor_point, start_of_tongue
        state.tongue_length = geometry.distance(start_of_tongue, state.anchor_point)
        state.tongue_length = state.tongue_length.greater(100)
      end
    
      def calc_on_floor
        if state.action == :anchored
          player.on_floor = false
          player.on_floor_debounce = 30
        else
          player.on_floor_debounce ||= 30
    
          if player.dy.round != 0
            player.on_floor_debounce = 30
            player.on_floor = false
          else
            player.on_floor_debounce -= 1
          end
    
          if player.on_floor_debounce <= 0
            player.on_floor_debounce = 0
            player.on_floor = true
          end
        end
      end
    
      def calc_mug_collection
        collected = state.mugs.find_all { |s| s.intersect_rect? player }
        state.mugs.reject! { |s| collected.include? s }
      end
    
      def set_camera_scale v = nil
        return if v < 0.1
        state.target_camera_scale = v
      end
    
      def input
        input_game
        input_level_editor
      end
    
      def input_up?
        inputs.keyboard.w || inputs.keyboard.up
      end
    
      def input_down?
        inputs.keyboard.s || inputs.keyboard.down
      end
    
      def input_left?
        inputs.keyboard.a || inputs.keyboard.left
      end
    
      def input_right?
        inputs.keyboard.d || inputs.keyboard.right
      end
    
      def input_game
        if inputs.keyboard.key_down.g
          state.level_editor_mode = !state.level_editor_mode
        end
    
        if player.on_floor
          if inputs.keyboard.q
            player.dx = -5
          elsif inputs.keyboard.e
            player.dx = 5
          end
        end
    
        if inputs.keyboard.key_down.space && !state.anchor_point
          state.tongue_length = 0
          state.action = :shooting
        elsif inputs.keyboard.key_down.space
          state.action = :aiming
          state.anchor_point  = nil
          state.tongue_length = 100
        end
    
        if state.anchor_point
          vector = state.tongue_angle.to_vector
    
          if input_up?
            state.tongue_length -= 5
            player.dy += vector.y
            player.dx += vector.x
          elsif input_down?
            state.tongue_length += 5
            player.dy -= vector.y
            player.dx -= vector.x
          end
    
          if input_left?
            player.dx -= 0.5
          elsif input_right?
            player.dx += 0.5
          end
        else
          if input_left?
            state.tongue_angle += 1.5
            state.tongue_angle = state.tongue_angle
          elsif input_right?
            state.tongue_angle -= 1.5
            state.tongue_angle = state.tongue_angle
          end
        end
      end
    
      def input_level_editor
        return unless state.level_editor_mode
    
        if Kernel.tick_count.mod_zero?(5)
          # zoom
          if inputs.keyboard.equal_sign || inputs.keyboard.plus
            set_camera_scale state.camera_scale + 0.1
          elsif inputs.keyboard.hyphen
            set_camera_scale state.camera_scale - 0.1
          elsif inputs.keyboard.zero
            set_camera_scale 0.5
          end
    
          # change wall width
          if inputs.keyboard.h
            state.level_editor_rect_w -= state.tile_size
          elsif inputs.keyboard.l
            state.level_editor_rect_w += state.tile_size
          end
    
          state.level_editor_rect_w = state.tile_size if state.level_editor_rect_w < state.tile_size
    
          # change wall height
          if inputs.keyboard.j
            state.level_editor_rect_h -= state.tile_size
          elsif inputs.keyboard.k
            state.level_editor_rect_h += state.tile_size
          end
    
          state.level_editor_rect_h = state.tile_size if state.level_editor_rect_h < state.tile_size
        end
    
        if inputs.mouse.click
          x = ((state.camera_x + inputs.mouse.x) / state.camera_scale).ifloor(state.tile_size)
          y = ((state.camera_y + inputs.mouse.y) / state.camera_scale).ifloor(state.tile_size)
          # place mug
          if inputs.keyboard.m
            w = 32
            h = 32
            candidate_rect = { x: x, y: y, w: w, h: h }
            if inputs.keyboard.x
              mouse_rect = { x: (state.camera_x + inputs.mouse.x) / state.camera_scale,
                             y: (state.camera_y + inputs.mouse.y) / state.camera_scale,
                             w: 10,
                             h: 10 }
              to_remove = state.mugs.find do |r|
                r.intersect_rect? mouse_rect
              end
              if to_remove
                state.mugs.reject! { |r| r == to_remove }
              end
            else
              exists = state.mugs.find { |r| r == candidate_rect }
              if !exists
                state.mugs << candidate_rect.merge(path: "sprites/square/orange.png")
              end
            end
          else
            # place wall
            w = state.level_editor_rect_w
            h = state.level_editor_rect_h
            candidate_rect = { x: x, y: y, w: w, h: h }
            if inputs.keyboard.x
              mouse_rect = { x: (state.camera_x + inputs.mouse.x) / state.camera_scale,
                             y: (state.camera_y + inputs.mouse.y) / state.camera_scale,
                             w: 10,
                             h: 10 }
              to_remove = state.walls.find do |r|
                r.intersect_rect? mouse_rect
              end
              if to_remove
                state.walls.reject! { |r| r == to_remove }
              end
            else
              exists = state.walls.find { |r| r == candidate_rect }
              if !exists
                state.walls << candidate_rect
              end
            end
          end
    
          save
        end
    
        if input_up?
          player.y += 10
          player.dy = 0
        elsif input_down?
          player.y -= 10
          player.dy = 0
        end
    
        if input_left?
          player.x -= 10
          player.dx = 0
        elsif input_right?
          player.x += 10
          player.dx = 0
        end
      end
    
      def end_of_tongue
        p = state.tongue_angle.to_vector
        { x: start_of_tongue.x + p.x * state.tongue_length,
          y: start_of_tongue.y + p.y * state.tongue_length }
      end
    
      def save
        $gtk.write_file("data/mugs.txt", "")
        state.mugs.each do |o|
          $gtk.append_file "data/mugs.txt", "#{o.x},#{o.y},#{o.w},#{o.h}\n"
        end
    
        $gtk.write_file("data/walls.txt", "")
        state.walls.map do |o|
          $gtk.append_file "data/walls.txt", "#{o.x},#{o.y},#{o.w},#{o.h}\n"
        end
      end
    
      def load_if_needed
        return if state.walls
        state.walls = []
        state.mugs = []
    
        contents = $gtk.read_file "data/mugs.txt"
        if contents
          contents.each_line do |l|
            x, y, w, h = l.split(',').map(&:to_i)
            state.mugs << { x: x.ifloor(state.tile_size),
                            y: y.ifloor(state.tile_size),
                            w: w,
                            h: h,
                            path: "sprites/square/orange.png" }
          end
        end
    
        contents = $gtk.read_file "data/walls.txt"
        if contents
          contents.each_line do |l|
            x, y, w, h = l.split(',').map(&:to_i)
            state.walls << { x: x.ifloor(state.tile_size),
                             y: y.ifloor(state.tile_size),
                             w: w,
                             h: h,
                             path: :pixel,
                             r: 128,
                             g: 128,
                             b: 128,
                             a: 128 }
          end
        end
      end
    end
    
    $game = CleptoFrog.new
    
    def tick args
      $game.args = args
      $game.tick
    end
    
    # $gtk.reset
    
    

    Clepto Frog - Data - mugs.txt link

    # ./samples/99_genre_platformer/clepto_frog/data/mugs.txt
    64,64,32,32
    928,1952,32,32
    3744,2464,32,32
    1536,3264,32,32
    7648,32,32,32
    9312,1120,32,32
    7296,1152,32,32
    5792,1824,32,32
    864,3744,32,32
    1024,4640,32,32
    800,5312,32,32
    3232,5216,32,32
    4736,5280,32,32
    9312,5152,32,32
    9632,4288,32,32
    7808,4096,32,32
    8640,1952,32,32
    6880,2016,32,32
    4608,3872,32,32
    4000,4544,32,32
    3200,3328,32,32
    5056,1056,32,32
    3424,608,32,32
    6496,288,32,32
    6080,288,32,32
    5600,288,32,32
    3424,608,32,32
    2656,704,32,32
    2208,224,32,32
    
    

    Clepto Frog - Data - walls.txt link

    # ./samples/99_genre_platformer/clepto_frog/data/walls.txt
    0,0,32,5664
    0,5664,10016,32
    0,0,10016,32
    10016,0,32,5696
    2112,192,704,32
    2112,672,704,32
    3328,576,224,32
    5504,256,256,32
    5984,256,256,32
    6400,256,256,32
    4928,1024,256,32
    7168,1120,256,32
    9216,1088,256,32
    8544,1920,256,32
    6752,1984,256,32
    5664,1792,256,32
    832,1920,256,32
    1440,3232,256,32
    736,3712,256,32
    896,4608,256,32
    672,5280,256,32
    3136,5184,256,32
    3872,4512,256,32
    4640,5248,256,32
    7680,4064,256,32
    9536,4256,256,32
    9184,5120,256,32
    3072,3296,256,32
    3616,2432,256,32
    4480,3840,256,32
    4704,1952,128,128
    6272,3328,128,128
    5248,4832,128,128
    2496,4320,128,128
    1536,5056,128,128
    7232,5024,128,128
    2208,2336,128,128
    1120,704,128,128
    8448,2944,128,128
    8576,4608,128,128
    7840,2176,128,128
    8640,416,128,128
    6048,1088,128,128
    4768,352,128,128
    3040,1600,128,128
    448,2720,128,128
    1568,4064,128,128
    256,4736,128,128
    3936,5312,128,128
    3872,3360,128,128
    7904,800,128,128
    6272,4320,128,128
    1728,1440,128,128
    96,768,128,128
    9120,3616,128,128
    6144,5184,128,128
    7168,3168,128,128
    5472,3712,128,128
    2592,5088,128,128
    2528,3328,128,128
    1376,2560,128,128
    4096,1344,128,128
    9344,2336,128,128
    5952,2656,128,128
    3360,4160,128,128
    224,1696,128,128
    352,4064,128,128
    8192,5248,128,128
    7168,448,128,128
    6624,2592,128,128
    4608,2848,128,128
    2336,1184,128,128
    640,224,128,128
    7264,4352,128,128
    
    

    Gorillas Basic - credits.txt link

    # ./samples/99_genre_platformer/gorillas_basic/CREDITS.txt
    code: Amir Rajan, https://twitter.com/amirrajan
    graphics: Nick Culbertson, https://twitter.com/MobyPixel
    
    
    

    Gorillas Basic - main.rb link

    # ./samples/99_genre_platformer/gorillas_basic/app/main.rb
    class YouSoBasicGorillas
      attr_accessor :outputs, :grid, :state, :inputs
    
      def tick
        defaults
        render
        calc
        process_inputs
      end
    
      def defaults
        outputs.background_color = [33, 32, 87]
        state.building_spacing       = 1
        state.building_room_spacing  = 15
        state.building_room_width    = 10
        state.building_room_height   = 15
        state.building_heights       = [4, 4, 6, 8, 15, 20, 18]
        state.building_room_sizes    = [5, 4, 6, 7]
        state.gravity                = 0.25
        state.first_strike         ||= :player_1
        state.buildings            ||= []
        state.holes                ||= []
        state.player_1_score       ||= 0
        state.player_2_score       ||= 0
        state.wind                 ||= 0
      end
    
      def render
        render_stage
        render_value_insertion
        render_gorillas
        render_holes
        render_banana
        render_game_over
        render_score
        render_wind
      end
    
      def render_score
        outputs.primitives << [0, 0, 1280, 31, fancy_white].solid
        outputs.primitives << [1, 1, 1279, 29].solid
        outputs.labels << [  10, 25, "Score: #{state.player_1_score}", 0, 0, fancy_white]
        outputs.labels << [1270, 25, "Score: #{state.player_2_score}", 0, 2, fancy_white]
      end
    
      def render_wind
        outputs.primitives << [640, 12, state.wind * 500 + state.wind * 10 * rand, 4, 35, 136, 162].solid
        outputs.lines     <<  [640, 30, 640, 0, fancy_white]
      end
    
      def render_game_over
        return unless state.over
        outputs.primitives << [grid.rect, 0, 0, 0, 200].solid
        outputs.primitives << [640, 370, "Game Over!!", 5, 1, fancy_white].label
        if state.winner == :player_1
          outputs.primitives << [640, 340, "Player 1 Wins!!", 5, 1, fancy_white].label
        else
          outputs.primitives << [640, 340, "Player 2 Wins!!", 5, 1, fancy_white].label
        end
      end
    
      def render_stage
        return unless state.stage_generated
        return if state.stage_rendered
    
        outputs.static_solids << [grid.rect, 33, 32, 87]
        outputs.static_solids << state.buildings.map(&:solids)
        state.stage_rendered = true
      end
    
      def render_gorilla gorilla, id
        return unless gorilla
        if state.banana && state.banana.owner == gorilla
          animation_index  = state.banana.created_at.frame_index(3, 5, false)
        end
        if !animation_index
          outputs.sprites << [gorilla.solid, "sprites/#{id}-idle.png"]
        else
          outputs.sprites << [gorilla.solid, "sprites/#{id}-#{animation_index}.png"]
        end
      end
    
      def render_gorillas
        render_gorilla state.player_1, :left
        render_gorilla state.player_2, :right
      end
    
      def render_value_insertion
        return if state.banana
        return if state.over
    
        if    state.current_turn == :player_1_angle
          outputs.labels << [  10, 710, "Angle:    #{state.player_1_angle}_",    fancy_white]
        elsif state.current_turn == :player_1_velocity
          outputs.labels << [  10, 710, "Angle:    #{state.player_1_angle}",     fancy_white]
          outputs.labels << [  10, 690, "Velocity: #{state.player_1_velocity}_", fancy_white]
        elsif state.current_turn == :player_2_angle
          outputs.labels << [1120, 710, "Angle:    #{state.player_2_angle}_",    fancy_white]
        elsif state.current_turn == :player_2_velocity
          outputs.labels << [1120, 710, "Angle:    #{state.player_2_angle}",     fancy_white]
          outputs.labels << [1120, 690, "Velocity: #{state.player_2_velocity}_", fancy_white]
        end
      end
    
      def render_banana
        return unless state.banana
        rotation = Kernel.tick_count.%(360) * 20
        rotation *= -1 if state.banana.dx > 0
        outputs.sprites << [state.banana.x, state.banana.y, 15, 15, 'sprites/banana.png', rotation]
      end
    
      def render_holes
        outputs.sprites << state.holes.map do |s|
          animation_index = s.created_at.frame_index(7, 3, false)
          if animation_index
            [s.sprite, [s.sprite.rect, "sprites/explosion#{animation_index}.png" ]]
          else
            s.sprite
          end
        end
      end
    
      def calc
        calc_generate_stage
        calc_current_turn
        calc_banana
      end
    
      def calc_current_turn
        return if state.current_turn
    
        state.current_turn = :player_1_angle
        state.current_turn = :player_2_angle if state.first_strike == :player_2
      end
    
      def calc_generate_stage
        return if state.stage_generated
    
        state.buildings << building_prefab(state.building_spacing + -20, *random_building_size)
        8.numbers.inject(state.buildings) do |buildings, i|
          buildings <<
            building_prefab(state.building_spacing +
                            state.buildings.last.right,
                            *random_building_size)
        end
    
        building_two = state.buildings[1]
        state.player_1 = new_player(building_two.x + building_two.w.fdiv(2),
                                   building_two.h)
    
        building_nine = state.buildings[-3]
        state.player_2 = new_player(building_nine.x + building_nine.w.fdiv(2),
                                   building_nine.h)
        state.stage_generated = true
        state.wind = 1.randomize(:ratio, :sign)
      end
    
      def new_player x, y
        state.new_entity(:gorilla) do |p|
          p.x = x - 25
          p.y = y
          p.solid = [p.x, p.y, 50, 50]
        end
      end
    
      def calc_banana
        return unless state.banana
    
        state.banana.x  += state.banana.dx
        state.banana.dx += state.wind.fdiv(50)
        state.banana.y  += state.banana.dy
        state.banana.dy -= state.gravity
        banana_collision = [state.banana.x, state.banana.y, 10, 10]
    
        if state.player_1 && banana_collision.intersect_rect?(state.player_1.solid)
          state.over = true
          if state.banana.owner == state.player_2
            state.winner = :player_2
          else
            state.winner = :player_1
          end
    
          state.player_2_score += 1
        elsif state.player_2 && banana_collision.intersect_rect?(state.player_2.solid)
          state.over = true
          if state.banana.owner == state.player_2
            state.winner = :player_1
          else
            state.winner = :player_2
          end
          state.player_1_score += 1
        end
    
        if state.over
          place_hole
          return
        end
    
        return if state.holes.any? do |h|
          h.sprite.scale_rect(0.8, 0.5, 0.5).intersect_rect? [state.banana.x, state.banana.y, 10, 10]
        end
    
        return unless state.banana.y < 0 || state.buildings.any? do |b|
          b.rect.intersect_rect? [state.banana.x, state.banana.y, 1, 1]
        end
    
        place_hole
      end
    
      def place_hole
        return unless state.banana
    
        state.holes << state.new_entity(:banana) do |b|
          b.sprite = [state.banana.x - 20, state.banana.y - 20, 40, 40, 'sprites/hole.png']
        end
    
        state.banana = nil
      end
    
      def process_inputs_main
        return if state.banana
        return if state.over
    
        if inputs.keyboard.key_down.enter
          input_execute_turn
        elsif inputs.keyboard.key_down.backspace
          state.as_hash[state.current_turn] ||= ""
          state.as_hash[state.current_turn]   = state.as_hash[state.current_turn][0..-2]
        elsif inputs.keyboard.key_down.char
          state.as_hash[state.current_turn] ||= ""
          state.as_hash[state.current_turn]  += inputs.keyboard.key_down.char
        end
      end
    
      def process_inputs_game_over
        return unless state.over
        return unless inputs.keyboard.key_down.truthy_keys.any?
        state.over = false
        outputs.static_solids.clear
        state.buildings.clear
        state.holes.clear
        state.stage_generated = false
        state.stage_rendered = false
        if state.first_strike == :player_1
          state.first_strike = :player_2
        else
          state.first_strike = :player_1
        end
      end
    
      def process_inputs
        process_inputs_main
        process_inputs_game_over
      end
    
      def input_execute_turn
        return if state.banana
    
        if state.current_turn == :player_1_angle && parse_or_clear!(:player_1_angle)
          state.current_turn = :player_1_velocity
        elsif state.current_turn == :player_1_velocity && parse_or_clear!(:player_1_velocity)
          state.current_turn = :player_2_angle
          state.banana =
            new_banana(state.player_1,
                       state.player_1.x + 25,
                       state.player_1.y + 60,
                       state.player_1_angle,
                       state.player_1_velocity)
        elsif state.current_turn == :player_2_angle && parse_or_clear!(:player_2_angle)
          state.current_turn = :player_2_velocity
        elsif state.current_turn == :player_2_velocity && parse_or_clear!(:player_2_velocity)
          state.current_turn = :player_1_angle
          state.banana =
            new_banana(state.player_2,
                       state.player_2.x + 25,
                       state.player_2.y + 60,
                       180 - state.player_2_angle,
                       state.player_2_velocity)
        end
    
        if state.banana
          state.player_1_angle = nil
          state.player_1_velocity = nil
          state.player_2_angle = nil
          state.player_2_velocity = nil
        end
      end
    
      def random_building_size
        [state.building_heights.sample, state.building_room_sizes.sample]
      end
    
      def int? v
        v.to_i.to_s == v.to_s
      end
    
      def random_building_color
        [[ 99,   0, 107],
         [ 35,  64, 124],
         [ 35, 136, 162],
         ].sample
      end
    
      def random_window_color
        [[ 88,  62, 104],
         [253, 224, 187]].sample
      end
    
      def windows_for_building starting_x, floors, rooms
        floors.-(1).combinations(rooms - 1).map do |floor, room|
          [starting_x +
           state.building_room_width.*(room) +
           state.building_room_spacing.*(room + 1),
           state.building_room_height.*(floor) +
           state.building_room_spacing.*(floor + 1),
           state.building_room_width,
           state.building_room_height,
           random_window_color]
        end
      end
    
      def building_prefab starting_x, floors, rooms
        state.new_entity(:building) do |b|
          b.x      = starting_x
          b.y      = 0
          b.w      = state.building_room_width.*(rooms) +
                     state.building_room_spacing.*(rooms + 1)
          b.h      = state.building_room_height.*(floors) +
                     state.building_room_spacing.*(floors + 1)
          b.right  = b.x + b.w
          b.rect   = [b.x, b.y, b.w, b.h]
          b.solids = [[b.x - 1, b.y, b.w + 2, b.h + 1, fancy_white],
                      [b.x, b.y, b.w, b.h, random_building_color],
                      windows_for_building(b.x, floors, rooms)]
        end
      end
    
      def parse_or_clear! game_prop
        if int? state.as_hash[game_prop]
          state.as_hash[game_prop] = state.as_hash[game_prop].to_i
          return true
        end
    
        state.as_hash[game_prop] = nil
        return false
      end
    
      def new_banana owner, x, y, angle, velocity
        state.new_entity(:banana) do |b|
          b.owner     = owner
          b.x         = x
          b.y         = y
          b.angle     = angle % 360
          b.velocity  = velocity / 5
          b.dx        = b.angle.vector_x(b.velocity)
          b.dy        = b.angle.vector_y(b.velocity)
        end
      end
    
      def fancy_white
        [253, 252, 253]
      end
    end
    
    $you_so_basic_gorillas = YouSoBasicGorillas.new
    
    def tick args
      $you_so_basic_gorillas.outputs = args.outputs
      $you_so_basic_gorillas.grid    = args.grid
      $you_so_basic_gorillas.state    = args.state
      $you_so_basic_gorillas.inputs  = args.inputs
      $you_so_basic_gorillas.tick
    end
    
    

    Map Editor - camera.rb link

    # ./samples/99_genre_platformer/map_editor/app/camera.rb
    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
        def to_world_space camera, rect
          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
    
        def to_screen_space camera, rect
          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
    
        def viewport
          {
            x: OFFSET_X,
            y: OFFSET_Y,
            w: 1500,
            h: 1500
          }
        end
    
        def viewport_world camera
          to_world_space camera, viewport
        end
    
        def find_all_intersect_viewport camera, os
          Geometry.find_all_intersect_rect viewport_world(camera), os
        end
      end
    end
    
    

    Map Editor - level_editor.rb link

    # ./samples/99_genre_platformer/map_editor/app/level_editor.rb
    class LevelEditor
      attr_gtk
      attr :mode, :hovered_tile, :selected_tile, :tilesheet_rect
    
      def initialize
        @tilesheet_rect = { x: 0, y: 0, w: 320, h: 320 }
        @mode = :add
      end
    
      def tick
        generate_tilesheet
        calc
        render
      end
    
      def calc
        if inputs.keyboard.x
          @mode = :remove
        else
          @mode = :add
        end
    
        if !@selected_tile
          @mode = :remove
        elsif @selected_tile.x_ordinal == 0 && @selected_tile.y_ordinal == 0
          @mode = :remove
        end
    
        if mouse.intersect_rect? @tilesheet_rect
          x_ordinal = mouse.x.idiv(16)
          y_ordinal = mouse.y.idiv(16)
          @hovered_tile = { x_ordinal: mouse.x.idiv(16),
                            x: mouse.x.idiv(16) * 16,
                            y_ordinal: mouse.x.idiv(16),
                            y: mouse.y.idiv(16) * 16,
                            row: 20 - y_ordinal - 1,
                            col: x_ordinal,
                            path: tile_path(20 - y_ordinal - 1, x_ordinal, 20),
                            w: 16,
                            h: 16 }
        else
          @hovered_tile = nil
        end
    
        if mouse.click && @hovered_tile
          @selected_tile = @hovered_tile
        end
    
        world_mouse = Camera.to_world_space state.camera, inputs.mouse
        ifloor_x = world_mouse.x.ifloor(16)
        ifloor_y = world_mouse.y.ifloor(16)
    
        @mouse_world_rect =  { x: ifloor_x,
                               y: ifloor_y,
                               w: 16,
                               h: 16 }
    
        if @selected_tile
          ifloor_x = world_mouse.x.ifloor(16)
          ifloor_y = world_mouse.y.ifloor(16)
          @selected_tile.x = @mouse_world_rect.x
          @selected_tile.y = @mouse_world_rect.y
        end
    
        if @mode == :remove && (mouse.click || (mouse.held && mouse.moved))
          state.terrain.reject! { |t| t.intersect_rect? @mouse_world_rect }
          save_terrain args
        elsif @selected_tile && (mouse.click || (mouse.held && mouse.moved))
          if @mode == :add
            state.terrain.reject! { |t| t.intersect_rect? @selected_tile }
            state.terrain << @selected_tile.copy
          else
            state.terrain.reject! { |t| t.intersect_rect? @selected_tile }
          end
          save_terrain args
        end
      end
    
      def render
        outputs.sprites << { x: 0, y: 0, w: 320, h: 320, path: :tilesheet }
    
        if @hovered_tile
          outputs.sprites << { x: @hovered_tile.x,
                               y: @hovered_tile.y,
                               w: 16,
                               h: 16,
                               path: :pixel,
                               r: 255, g: 0, b: 0, a: 128 }
        end
    
        if @selected_tile
          if @mode == :remove
            outputs[:scene].sprites << (Camera.to_screen_space state.camera, @selected_tile).merge(path: :pixel, r: 255, g: 0, b: 0, a: 64)
          elsif @selected_tile
            outputs[:scene].sprites << (Camera.to_screen_space state.camera, @selected_tile)
            outputs[:scene].sprites << (Camera.to_screen_space state.camera, @selected_tile).merge(path: :pixel, r: 0, g: 255, b: 255, a: 64)
          end
        end
      end
    
      def generate_tilesheet
        return if state.tick_count > 0
        results = []
        rows = 20
        cols = 20
        tile_size = 16
        height = rows * tile_size
        width = cols * tile_size
        rows.map_with_index do |row|
          cols.map_with_index do |col|
            results << {
              x: col * tile_size,
              y: height - row * tile_size - tile_size,
              w: tile_size,
              h: tile_size,
              path: tile_path(row, col, cols)
            }
          end
        end
    
        outputs[:tilesheet].w = width
        outputs[:tilesheet].h = height
        outputs[:tilesheet].sprites << { x: 0, y: 0, w: width, h: height, path: :pixel, r: 0, g: 0, b: 0 }
        outputs[:tilesheet].sprites << results
      end
    
      def mouse
        inputs.mouse
      end
    
      def tile_path row, col, cols
        file_name = (tile_index row, col, cols).to_s.rjust(4, "0")
        "sprites/1-bit-platformer/#{file_name}.png"
      end
    
      def tile_index row, col, cols
        row * cols + col
      end
    
      def save_terrain args
        contents = args.state.terrain.uniq.map do |terrain_element|
          "#{terrain_element.x.to_i},#{terrain_element.y.to_i},#{terrain_element.w.to_i},#{terrain_element.h.to_i},#{terrain_element.path}"
        end
        File.write "data/terrain.txt", contents.join("\n")
      end
    
      def load_terrain args
        args.state.terrain = []
        contents = File.read("data/terrain.txt")
        return if !contents
        args.state.terrain = contents.lines.map do |line|
          l = line.strip
          if l.empty?
            nil
          else
            x, y, w, h, path = l.split ","
            { x: x.to_f, y: y.to_f, w: w.to_f, h: h.to_f, path: path }
          end
        end.compact.to_a.uniq
      end
    end
    
    

    Map Editor - main.rb link

    # ./samples/99_genre_platformer/map_editor/app/main.rb
    require 'app/level_editor.rb'
    require 'app/root_scene.rb'
    require 'app/camera.rb'
    
    def tick args
      $root_scene ||= RootScene.new args
      $root_scene.args = args
      $root_scene.tick
    end
    
    def reset
      $root_scene = nil
    end
    
    $gtk.reset
    
    

    Map Editor - root_scene.rb link

    # ./samples/99_genre_platformer/map_editor/app/root_scene.rb
    class RootScene
      attr_gtk
    
      attr :level_editor
    
      def initialize args
        @level_editor = LevelEditor.new
      end
    
      def tick
        args.outputs.background_color = [0, 0, 0]
        args.state.terrain ||= []
        @level_editor.load_terrain args if args.state.tick_count == 0
    
        state.player ||= {
          x: 0,
          y: 750,
          w: 16,
          h: 16,
          dy: 0,
          dx: 0,
          on_ground: false,
          path: "sprites/1-bit-platformer/0280.png"
        }
    
        if inputs.keyboard.left
          player.dx = -3
        elsif inputs.keyboard.right
          player.dx = 3
        end
    
        if inputs.keyboard.key_down.space && player.on_ground
          player.dy = 10
          player.on_ground = false
        end
    
        if args.inputs.keyboard.key_down.equal_sign || args.inputs.keyboard.key_down.plus
          state.camera.target_scale += 0.25
        elsif args.inputs.keyboard.key_down.minus
          state.camera.target_scale -= 0.25
          state.camera.target_scale = 0.25 if state.camera.target_scale < 0.25
        elsif args.inputs.keyboard.zero
          state.camera.target_scale = 1
        end
    
        state.gravity ||= 0.25
        calc_camera
        calc_physics
        outputs[:scene].w = 1500
        outputs[:scene].h = 1500
        outputs[:scene].background_color = [0, 0, 0, 0]
        outputs[:scene].lines << { x: 0, y: 0, x2: 1500, y2: 1500, r: 255, g: 255, b: 255, a: 255 }
        outputs[:scene].lines << { x: 0, y: 1500, x2: 1500, y2: 0, r: 255, g: 255, b: 255, a: 255 }
        outputs[:scene].sprites << player_prefab
    
        terrain_to_render = Camera.find_all_intersect_viewport(state.camera, state.terrain)
        outputs[:scene].sprites << terrain_to_render.map do |m|
          Camera.to_screen_space(state.camera, m)
        end
    
        outputs.sprites << { **Camera.viewport, path: :scene }
    
        @level_editor.args = args
        @level_editor.tick
    
        outputs.labels << { x: 640,
                            y: 30.from_top,
                            anchor_x: 0.5,
                            text: "WASD: move around. SPACE: jump. +/-: Zoom in and out. MOUSE: select tile/edit map (hold X and CLICK to delete).",
                            r: 255,
                            g: 255,
                            b: 255 }
      end
    
      def calc_camera
        state.world_size ||= 1280
    
        if !state.camera
          state.camera = {
            x: 0,
            y: 0,
            target_x: 0,
            target_y: 0,
            target_scale: 2,
            scale: 1
          }
        end
    
        ease = 0.1
        state.camera.scale += (state.camera.target_scale - state.camera.scale) * ease
        state.camera.target_x = player.x
        state.camera.target_y = player.y
    
        state.camera.x += (state.camera.target_x - state.camera.x) * ease
        state.camera.y += (state.camera.target_y - state.camera.y) * ease
      end
    
      def calc_physics
        player.x += player.dx
        collision = state.terrain.find do |t|
          t.intersect_rect? player
        end
    
        if collision
          if player.dx > 0
            player.x = collision.x - player.w
          else
            player.x = collision.x + collision.w
          end
    
          player.dx = 0
        end
    
        player.dx *= 0.8
        if player.dx.abs < 0.5
          player.dx = 0
        end
    
        player.y += player.dy
        player.on_ground = false
    
        collision = state.terrain.find do |t|
          t.intersect_rect? player
        end
    
        if collision
          if player.dy > 0
            player.y = collision.y - player.h
          else
            player.y = collision.y + collision.h
            player.on_ground = true
          end
          player.dy = 0
        end
    
        player.dy -= state.gravity
    
        if (player.y + player.h) < -750
          player.y = 750
          player.dy = 0
        end
      end
    
      def player
        state.player
      end
    
      def player_prefab
        prefab = Camera.to_screen_space state.camera, (player.merge path: "sprites/1-bit-platformer/0280.png")
    
        if !player.on_ground
          prefab.merge! path: "sprites/1-bit-platformer/0284.png"
          if player.dx > 0
            prefab.merge! flip_horizontally: false
          elsif player.dx < 0
            prefab.merge! flip_horizontally: true
          end
        elsif player.dx > 0
          frame_index = 0.frame_index 3, 5, true
          prefab.merge! path: "sprites/1-bit-platformer/028#{frame_index + 1}.png"
        elsif player.dx < 0
          frame_index = 0.frame_index 3, 5, true
          prefab.merge! path: "sprites/1-bit-platformer/028#{frame_index + 1}.png", flip_horizontally: true
        end
    
        prefab
      end
    
      def camera
        state.camera
      end
    
      def should_update_matricies?
        player.dx != 0 || player.dy != 0
      end
    end
    
    

    Map Editor - Data - terrain.txt link

    # ./samples/99_genre_platformer/map_editor/data/terrain.txt
    0,368,16,16,sprites/1-bit-platformer/0088.png
    -16,368,16,16,sprites/1-bit-platformer/0088.png
    -32,368,16,16,sprites/1-bit-platformer/0088.png
    -48,368,16,16,sprites/1-bit-platformer/0088.png
    -64,368,16,16,sprites/1-bit-platformer/0088.png
    -80,368,16,16,sprites/1-bit-platformer/0088.png
    16,368,16,16,sprites/1-bit-platformer/0088.png
    32,368,16,16,sprites/1-bit-platformer/0088.png
    48,368,16,16,sprites/1-bit-platformer/0088.png
    64,368,16,16,sprites/1-bit-platformer/0088.png
    80,368,16,16,sprites/1-bit-platformer/0088.png
    -96,368,16,16,sprites/1-bit-platformer/0087.png
    96,368,16,16,sprites/1-bit-platformer/0089.png
    -96,352,16,16,sprites/1-bit-platformer/0127.png
    -80,352,16,16,sprites/1-bit-platformer/0128.png
    -64,352,16,16,sprites/1-bit-platformer/0128.png
    -48,352,16,16,sprites/1-bit-platformer/0128.png
    -32,352,16,16,sprites/1-bit-platformer/0128.png
    -16,352,16,16,sprites/1-bit-platformer/0128.png
    0,352,16,16,sprites/1-bit-platformer/0128.png
    16,352,16,16,sprites/1-bit-platformer/0128.png
    32,352,16,16,sprites/1-bit-platformer/0128.png
    48,352,16,16,sprites/1-bit-platformer/0128.png
    64,352,16,16,sprites/1-bit-platformer/0128.png
    80,352,16,16,sprites/1-bit-platformer/0128.png
    96,352,16,16,sprites/1-bit-platformer/0129.png
    
    

    Shadows - main.rb link

    # ./samples/99_genre_platformer/shadows/app/main.rb
    # demo gameplay here: https://youtu.be/wQknjYk_-dE
    # this is the core game class. the game is
    # pretty small so this is the only class that was created
    class Game
      # attr_gtk is a ruby class macro (mixin) that
      # adds the .args, .inputs, .outputs, and .state
      # properties to a class
      attr_gtk
    
      # this is the main tick method that
      # will be called every frame
      # the tick method is your standard game loop.
      # ie initialize game state, process input,
      #    perform simulation calculations, then render
      def tick
        defaults
        input
        calc
        render
      end
    
      # defaults method re-initializes the game to its
      # starting point if
      # 1. it hasn't already been initialized (state.clock is nil)
      # 2. or reinitializes the game if the player died (game_over)
      def defaults
        new_game if !state.clock || state.game_over == true
      end
    
      # this is where inputs are processed
      # we process inputs for the player via input_entity
      # and then process inputs for each enemy using the same
      # input_entity function
      def input
        input_entity player,
                     find_input_timeline(at: player.clock, key: :left_right),
                     find_input_timeline(at: player.clock, key: :space),
                     find_input_timeline(at: player.clock, key: :down)
    
        # an enemy could still be spawing
        shadows.find_all { |shadow| entity_active? shadow }
               .each do |shadow|
                 input_entity shadow,
                              find_input_timeline(at: shadow.clock, key: :left_right),
                              find_input_timeline(at: shadow.clock, key: :space),
                              find_input_timeline(at: shadow.clock, key: :down)
                 end
      end
    
      # this is the input_entity function that handles
      # the movement of the player (and the enemies)
      # it's essentially your state machine for player
      # movement
      def input_entity entity, left_right, jump, fall_through
        # guard clause that ignores input processing if
        # the entity is still spawning
        return if !entity_active? entity
    
        # increment the dx of the entity by the magnitude of
        # the left_right input value
        entity.dx += left_right
    
        # if the left_right input is zero...
        if left_right == 0
          # if the entity was originally running, then
          # set their "action" to standing
          # entity_set_action! updates the current action
          # of the entity and takes note of the frame that
          # the action occured on
          if (entity.action == :running)
            entity_set_action! entity, :standing
          end
        elsif entity.left_right != left_right && (entity_on_platform? entity)
          # if the entity is on a platform, and their current
          # left right value is different, mark them as running
          # this is done because we want to reset the run animation
          # if they changed directions
          entity_set_action! entity, :running
        end
    
        # capture the left_right input so that it can be
        # consulted on the next frame
        entity.left_right = left_right
    
        # capture the direction the player is facing
        # (this is used to determine the horizontal flip of the
        # sprite
        entity.orientation = if left_right == -1
                               :left
                             elsif left_right == 1
                               :right
                             else
                               entity.orientation
                             end
    
        # if the fall_through (down) input was requested,
        # and if they are on a platform...
        if fall_through && (entity_on_platform? entity)
          entity.jumped_at      = 0
          # set their jump_down value (falling through a platform)
          entity.jumped_down_at = entity.clock
          # and increment the number of times they jumped
          # (entities get three jumps before needing to touch the ground again)
          entity.jump_count    += 1
        end
    
        # if the jump input was requested
        # and if they haven't reached their jump limit
        if jump && entity.jump_count < 3
          # update the player's current action to the
          # corresponding jump number (used for rendering
          # the different jump animations)
          if entity.jump_count == 0
            entity_set_action! entity, :first_jump
          elsif entity.jump_count == 1
            entity_set_action! entity, :midair_jump
          elsif entity.jump_count == 2
            entity_set_action! entity, :midair_jump
          end
    
          # set the entity's dy value and take note
          # of when jump occured (also increment jump
          # count/eat one of their jumps)
          entity.dy             = entity.jump_power
          entity.jumped_at      = entity.clock
          entity.jumped_down_at = 0
          entity.jump_count    += 1
        end
      end
    
      # after inputs have been processed, we then
      # determine game over states, collision, win states
      # etc
      def calc
        # calculate the new values of the light meter
        # (if the light meter hits zero, it's game over)
        calc_light_meter
    
        # capture the actions that were taken this turn so
        # that they can be "replayed" for the enemies on future
        # ticks of the simulation
        calc_action_history
    
        # calculate collisions for the player
        calc_entity player
    
        # calculate collisions for the enemies
        calc_shadows
    
        # spawn a new light crystal
        calc_light_crystal
    
        # process "fire and forget" render queues
        # (eg particles and death animations)
        calc_render_queues
    
        # determine game over
        calc_game_over
    
        # increment the internal clocks for all entities
        # this internal clock is used to determine how
        # a player's past input is replayed. it's also
        # used to determine what animation frame the entity
        # should be performing when idle, running, and jumping
        calc_clock
      end
    
      # ease the light meters value up or down
      # every time the player captures a light crystal
      # the "target" light meter value is increased and
      # slowly spills over to the final light meter value
      # which is used to determine game over
      def calc_light_meter
        state.light_meter -= 1
        d = state.light_meter_queue * 0.1
        state.light_meter += d
        state.light_meter_queue -= d
      end
    
      def calc_action_history
        # keep track of the inputs the player has performed over time
        # as the inputs change for the player, mark the point in time
        # the specific input changed, and when the change occured.
        # when enemies replay the player's actions, this history (along
        # with the enemy's interal clock) is consulted to determine
        # what action should be performed
    
        # the three possible input events are captured and marked
        # within the input timeline if/when the value changes
    
        # left right input events
        state.curr_left_right     = inputs.left_right
        if state.prev_left_right != state.curr_left_right
          state.input_timeline.unshift({ at: state.clock, k: :left_right, v: state.curr_left_right })
        end
        state.prev_left_right = state.curr_left_right
    
        # jump input events
        state.curr_space     = inputs.keyboard.key_down.space    ||
                               inputs.controller_one.key_down.a  ||
                               inputs.keyboard.key_down.up       ||
                               inputs.controller_one.key_down.b
        if state.prev_space != state.curr_space
          state.input_timeline.unshift({ at: state.clock, k: :space, v: state.curr_space })
        end
        state.prev_space = state.curr_space
    
        # jump down (fall through platform)
        state.curr_down     = inputs.keyboard.down || inputs.controller_one.down
        if state.prev_down != state.curr_down
          state.input_timeline.unshift({ at: state.clock, k: :down, v: state.curr_down })
        end
        state.prev_down = state.curr_down
      end
    
      def calc_entity entity
        # process entity collision/simulation
        calc_entity_rect entity
    
        # return if the entity is still spawning
        return if !entity_active? entity
    
        # calc collisions
        calc_entity_collision entity
    
        # update the state machine of the entity based on the
        # collision results
        calc_entity_action entity
    
        # calc actions the entity should take based on
        # input timeline
        calc_entity_movement entity
      end
    
      def calc_entity_rect entity
        # this function calculates the entity's new
        # collision rect, render rect, hurt box, etc
        entity.render_rect = { x: entity.x, y: entity.y, w: entity.w, h: entity.h }
        entity.rect = entity.render_rect.merge x: entity.render_rect.x + entity.render_rect.w * 0.33,
                                               w: entity.render_rect.w * 0.33
        entity.next_rect = entity.rect.merge x: entity.x + entity.dx,
                                             y: entity.y + entity.dy
        entity.prev_rect = entity.rect.merge x: entity.x - entity.dx,
                                             y: entity.y - entity.dy
        orientation_shift = 0
        if entity.orientation == :right
          orientation_shift = entity.rect.w.half
        end
        entity.hurt_rect  = entity.rect.merge y: entity.rect.y + entity.h * 0.33,
                                              x: entity.rect.x - entity.rect.w.half + orientation_shift,
                                              h: entity.rect.h * 0.33
      end
    
      def calc_entity_collision entity
        # run of the mill AABB collision
        calc_entity_below entity
        calc_entity_left entity
        calc_entity_right entity
      end
    
      def calc_entity_below entity
        # exit ground collision detection if they aren't falling
        return unless entity.dy < 0
        tiles_below = find_tiles { |t| t.rect.top <= entity.prev_rect.y }
        collision = find_collision tiles_below, (entity.rect.merge y: entity.next_rect.y)
    
        # exit ground collision detection if no ground was found
        return unless collision
    
        # determine if the entity is allowed to fall through the platform
        # (you can only fall through a platform if you've been standing on it for 8 frames)
        can_drop = true
        if entity.last_standing_at && (entity.clock - entity.last_standing_at) < 8
          can_drop = false
        end
    
        # if the entity is allowed to fall through the platform,
        # and the entity requested the action, then clip them through the platform
        if can_drop && entity.jumped_down_at.elapsed_time(entity.clock) < 10 && !collision.impassable
          if (entity_on_platform? entity) && can_drop
            entity.dy = -1
          end
    
          entity.jump_count = 1
        else
          entity.y  = collision.rect.y + collision.rect.h
          entity.dy = 0
          entity.jump_count = 0
        end
      end
    
      def calc_entity_left entity
        # collision detection left side of screen
        return unless entity.dx < 0
        return if entity.next_rect.x > 8 - 32
        entity.x  = 8 - 32
        entity.dx = 0
      end
    
      def calc_entity_right entity
        # collision detection right side of screen
        return unless entity.dx > 0
        return if (entity.next_rect.x + entity.rect.w) < (1280 - 8 - 32)
        entity.x  = (1280 - 8 - entity.rect.w - 32)
        entity.dx = 0
      end
    
      def calc_entity_action entity
        # update the state machine of the entity
        # based on where they ended up after physics calculations
        if entity.dy < 0
          # mark the entity as falling after the jump animation frames
          # have been processed
          if entity.action == :midair_jump
            if entity_action_complete? entity, state.midair_jump_duration
              entity_set_action! entity, :falling
            end
          else
            entity_set_action! entity, :falling
          end
        elsif entity.dy == 0 && !(entity_on_platform? entity)
          # if the entity's dy is zero, determine if they should
          # be marked as standing or running
          if entity.left_right == 0
            entity_set_action! entity, :standing
          else
            entity_set_action! entity, :running
          end
        end
      end
    
      def calc_entity_movement entity
        # increment x and y positions of the entity
        # based on dy and dx
        calc_entity_dy entity
        calc_entity_dx entity
      end
    
      def calc_entity_dx entity
        # horizontal movement application and friction
        entity.dx  = entity.dx.clamp(-5,  5)
        entity.dx *= 0.9
        entity.x  += entity.dx
      end
    
      def calc_entity_dy entity
        # vertical movement application and gravity
        entity.y  += entity.dy
        entity.dy += state.gravity
        entity.dy += entity.dy * state.drag ** 2 * -1
      end
    
      def calc_shadows
        # every 5 seconds, add a new shadow enemy/increase difficult
        add_shadow! if state.clock.zmod?(300)
    
        # for each shadow, perform a simulation calculation
        shadows.each do |shadow|
          calc_entity shadow
    
          # decrement the spawn countdown which is used to determine if
          # the enemy is finally active
          shadow.spawn_countdown -= 1 if shadow.spawn_countdown > 0
        end
      end
    
      def calc_light_crystal
        # determine if the player has intersected with a light crystal
        light_rect = state.light_crystal
        if player.hurt_rect.intersect_rect? light_rect
          # if they have then queue up the partical animation of the
          # light crystal being collected
          state.jitter_fade_out_render_queue << { x:    state.light_crystal.x,
                                                  y:    state.light_crystal.y,
                                                  w:    state.light_crystal.w,
                                                  h:    state.light_crystal.h,
                                                  a:    255,
                                                  path: 'sprites/light.png' }
    
          # increment the light meter target value
          state.light_meter_queue += 600
    
          # spawn a new light cristal for the player to try to get
          state.light_crystal = new_light_crystal
        end
      end
    
      def calc_render_queues
        # render all the entries in the "fire and forget" render queues
        state.jitter_fade_out_render_queue.each do |s|
          new_w = s.w * 1.02 ** 5
          ds = new_w - s.w
          s.w = new_w
          s.h = new_w
          s.x -= ds.half
          s.y -= ds.half
          s.a = s.a * 0.97 ** 5
        end
    
        state.jitter_fade_out_render_queue.reject! { |s| s.a <= 1 }
    
        state.game_over_render_queue.each { |s| s.a = s.a * 0.95 }
        state.game_over_render_queue.reject! { |s| s.a <= 1 }
      end
    
      def calc_game_over
        # calcuate game over
        state.game_over = false
    
        # it's game over if the player intersects with any of the enemies
        state.game_over ||= shadows.find_all { |s| s.spawn_countdown <= 0 }
                                   .any? { |s| s.hurt_rect.intersect_rect? player.hurt_rect }
    
        # it's game over if the light_meter hits 0
        state.game_over ||= state.light_meter <= 1
    
        # debug to reset the game/prematurely
        if inputs.keyboard.key_down.r
          state.you_win = false
          state.game_over = true
        end
    
        # update game over states and win/loss
        if state.game_over
          state.you_win = false
          state.game_over = true
        end
    
        if state.light_meter >= 6000
          state.you_win = true
          state.game_over = true
        end
    
        # if it's a game over, fade out all current entities in play
        if state.game_over
          state.game_over_render_queue.concat shadows.map { |s| s.sprite.merge(a: 255) }
          state.game_over_render_queue << player.sprite.merge(a: 255)
          state.game_over_render_queue << state.light_crystal.merge(a: 255, path: 'sprites/light.png', b: 128)
        end
      end
    
      def calc_clock
        return if state.game_over
        state.clock += 1
        player.clock += 1
        shadows.each { |s| s.clock += 1 if entity_active? s }
      end
    
      def render
        # render the game
        render_stage
        render_light_meter
        render_instructions
        render_render_queues
        render_light_meter_warning
        render_light_crystal
        render_entities
      end
    
      def render_stage
        # the stage is a simple background
        outputs.background_color = [255, 255, 255]
        outputs.sprites << { x: 0,
                             y: 0,
                             w: 1280,
                             h: 720,
                             path: "sprites/stage.png",
                             a: 200 }
      end
    
      def render_light_meter
        # the light meter sprite is rendered across the top
        # how much of the light meter is light vs dark is based off
        # of what the current light meter value is (which increases
        # when a crystal is collected and decreses a little bit every
        # frame
        meter_perc = state.light_meter.fdiv(6000) + (0.002 * rand)
        light_w = (1280 * meter_perc).round
        dark_w  = 1280 - light_w
    
        # once the light and dark partitions have been computed
        # render the meter sprite and clip its width (source_w)
        outputs.sprites << { x: 0,
                             y: 64.from_top,
                             w: light_w,
                             source_x: 0,
                             source_y: 0,
                             source_w: light_w,
                             source_h: 128,
                             h: 64,
                             path: 'sprites/meter-light.png' }
    
        outputs.sprites << { x: 1280 * meter_perc,
                             y: 64.from_top,
                             w: dark_w,
                             source_x: light_w,
                             source_y: 0,
                             source_w: dark_w,
                             source_h: 128,
                             h: 64,
                             path: 'sprites/meter-dark.png' }
      end
    
      def render_instructions
        outputs.labels << { x: 640,
                            y: 40,
                            text: '[left/right] to move, [up/space] to jump, [down] to drop through platform',
                            alignment_enum: 1 }
    
        if state.you_win
          outputs.labels << { x: 640,
                              y: 40.from_top,
                              text: 'You win!',
                              size_enum: -1,
                              alignment_enum: 1 }
        end
      end
    
      def render_render_queues
        outputs.sprites << state.jitter_fade_out_render_queue
        outputs.sprites << state.game_over_render_queue
      end
    
      def render_light_meter_warning
        return if state.light_meter >= 255
    
        # the screen starts to dim if they are close to having
        # a game over because of a depleated light meter
        outputs.primitives << { x: 0,
                                y: 0,
                                w: 1280,
                                h: 720,
                                a: 255 - state.light_meter,
                                path: :pixel,
                                r: 0,
                                g: 0,
                                b: 0 }
    
        outputs.primitives << { x: state.light_crystal.x - 32,
                                y: state.light_crystal.y - 32,
                                w: 128,
                                h: 128,
                                a: 255 - state.light_meter,
                                path: 'sprites/spotlight.png' }
      end
    
      def render_light_crystal
        jitter_sprite = { x: state.light_crystal.x + 5 * rand,
                          y: state.light_crystal.y + 5 * rand,
                          w: state.light_crystal.w + 5 * rand,
                          h: state.light_crystal.h + 5 * rand,
                          path: 'sprites/light.png' }
        outputs.primitives << jitter_sprite
      end
    
      def render_entities
        render_entity player, r: 0, g: 0, b: 0
        shadows.each { |shadow| render_entity shadow, g: 0, b: 0 }
      end
    
      def render_entity entity, r: 255, g: 255, b: 255;
        # this is essentially the entity "prefab"
        # the current action of the entity is consulted to
        # determine what sprite should be rendered
        # the action_at time is consulted to determine which frame
        # of the sprite animation should be presented
        a = 255
    
        entity.sprite = nil
    
        if entity.activate_at
          activation_elapsed_time = state.clock - entity.activate_at
          if entity.activate_at > state.clock
            entity.sprite = { x: entity.initial_x + 5 * rand,
                              y: entity.initial_y + 5 * rand,
                              w: 64 + 5 * rand,
                              h: 64 + 5 * rand,
                              path: "sprites/light.png",
                              g: 0, b: 0,
                              a: a }
    
            outputs.sprites << entity.sprite
            return
          elsif !entity.activated
            entity.activated = true
            state.jitter_fade_out_render_queue << { x: entity.initial_x + 5 * rand,
                                                    y: entity.initial_y + 5 * rand,
                                                    w: 86 + 5 * rand, h: 86 + 5 * rand,
                                                    path: "sprites/light.png",
                                                    g: 0, b: 0, a: 255 }
          end
        end
    
        # this is the render outputs for an entities action state machine
        if entity.action == :standing
          path = "sprites/player/stand.png"
        elsif entity.action == :running
          sprint_index = entity.action_at
                               .frame_index count: 4,
                                            hold_for: 8,
                                            repeat: true,
                                            tick_count_override: entity.clock
          path = "sprites/player/run-#{sprint_index}.png"
        elsif entity.action == :first_jump
          sprint_index = entity.action_at
                               .frame_index count: 2,
                                            hold_for: 8,
                                            repeat: false,
                                            tick_count_override: entity.clock
          path = "sprites/player/jump-#{sprint_index || 1}.png"
        elsif entity.action == :midair_jump
          sprint_index = entity.action_at
                               .frame_index count: state.midair_jump_frame_count,
                                            hold_for: state.midair_jump_hold_for,
                                            repeat: false,
                                            tick_count_override: entity.clock
          path = "sprites/player/midair-jump-#{sprint_index || 8}.png"
        elsif entity.action == :falling
          path = "sprites/player/falling.png"
        end
    
        flip_horizontally = true if entity.orientation == :left
        entity.sprite = entity.render_rect.merge path: path,
                                                 a: a,
                                                 r: r,
                                                 g: g,
                                                 b: b,
                                                 flip_horizontally: flip_horizontally
        outputs.sprites << entity.sprite
      end
    
      def new_game
        state.clock                   = 0
        state.game_over               = false
        state.gravity                 = -0.4
        state.drag                    = 0.15
    
        state.activation_time         = 90
        state.light_meter             = 600
        state.light_meter_queue       = 0
    
        state.midair_jump_frame_count = 9
        state.midair_jump_hold_for    = 6
        state.midair_jump_duration    = state.midair_jump_frame_count * state.midair_jump_hold_for
    
        # hard coded collision tiles
        state.tiles                   = [
          { impassable: true, x: 0, y: 0, w: 1280, h: 8, path: :pixel, r: 0, g: 0, b: 0 },
          { impassable: true, x: 0, y: 0, w: 8, h: 1500, path: :pixel, r: 0, g: 0, b: 0 },
          { impassable: true, x: 1280 - 8, y: 0, w: 8, h: 1500, path: :pixel, r: 0, g: 0, b: 0 },
    
          { x: 80 + 320 + 80,            y: 128, w: 320, h: 8, path: :pixel, r: 0, g: 0, b: 0 },
          { x: 80 + 320 + 80 + 320 + 80, y: 192, w: 320, h: 8, path: :pixel, r: 0, g: 0, b: 0 },
    
          { x: 160,                      y: 320, w: 400, h: 8, path: :pixel, r: 0, g: 0, b: 0 },
          { x: 160 + 400 + 160,          y: 400, w: 400, h: 8, path: :pixel, r: 0, g: 0, b: 0 },
    
          { x: 320,                      y: 600, w: 320, h: 8, path: :pixel, r: 0, g: 0, b: 0 },
    
          { x: 8, y: 500, w: 100, h: 8, path: :pixel, r: 0, g: 0, b: 0 },
    
          { x: 8, y: 60, w: 100, h: 8, path: :pixel, r: 0, g: 0, b: 0 },
        ]
    
        state.player                = new_entity
        state.player.jump_count     = 1
        state.player.jumped_at      = state.player.clock
        state.player.jumped_down_at = 0
    
        state.shadows   = []
    
        state.input_timeline = [
          { at: 0, k: :left_right, v: inputs.left_right },
          { at: 0, k: :space,      v: false },
          { at: 0, k: :down,       v: false },
        ]
    
        state.jitter_fade_out_render_queue   = []
        state.game_over_render_queue       ||= []
    
        state.light_crystal = new_light_crystal
      end
    
      def new_light_crystal
        r = { x: 124 + rand(1000), y: 135 + rand(500), w: 64, h: 64 }
        return new_light_crystal if tiles.any? { |t| t.intersect_rect? r }
        return new_light_crystal if (player.x - r.x).abs < 200
        r
      end
    
      def entity_active? entity
        return true unless entity.activate_at
        return entity.activate_at <= state.clock
      end
    
      def add_shadow!
        s = new_entity(from_entity: player)
        s.activate_at = state.clock + state.activation_time * (shadows.length + 1)
        s.spawn_countdown = state.activation_time
        shadows << s
      end
    
      def find_input_timeline at:, key:;
        state.input_timeline.find { |t| t.at <= at && t.k == key }.v
      end
    
      def new_entity from_entity: nil
        # these are all the properties of an entity
        # an optional from_entity can be passed in
        # for "cloning" an entity/setting an entities
        # starting state
        pe = state.new_entity(:body)
        pe.w                  = 96
        pe.h                  = 96
        pe.jump_power         = 12
        pe.y                  = 500
        pe.x                  = 640 - 8
        pe.initial_x          = pe.x
        pe.initial_y          = pe.y
        pe.dy                 = 0
        pe.dx                 = 0
        pe.jumped_down_at     = 0
        pe.jumped_at          = 0
        pe.jump_count         = 0
        pe.clock              = state.clock
        pe.orientation        = :right
        pe.action             = :falling
        pe.action_at          = state.clock
        pe.left_right         = 0
        if from_entity
          pe.w              = from_entity.w
          pe.h              = from_entity.h
          pe.jump_power     = from_entity.jump_power
          pe.x              = from_entity.x
          pe.y              = from_entity.y
          pe.initial_x      = from_entity.x
          pe.initial_y      = from_entity.y
          pe.dy             = from_entity.dy
          pe.dx             = from_entity.dx
          pe.jumped_down_at = from_entity.jumped_down_at
          pe.jumped_at      = from_entity.jumped_at
          pe.orientation    = from_entity.orientation
          pe.action         = from_entity.action
          pe.action_at      = from_entity.action_at
          pe.jump_count     = from_entity.jump_count
          pe.left_right     = from_entity.left_right
        end
        pe
      end
    
      def entity_on_platform? entity
        entity.action == :standing || entity.action == :running
      end
    
      def entity_action_complete? entity, action_duration
        entity.action_at.elapsed_time(entity.clock) + 1 >= action_duration
      end
    
      def entity_set_action! entity, action
        entity.action = action
        entity.action_at = entity.clock
        entity.last_standing_at = entity.clock if action == :standing
      end
    
      def player
        state.player
      end
    
      def shadows
        state.shadows
      end
    
      def tiles
        state.tiles
      end
    
      def find_tiles &block
        tiles.find_all(&block)
      end
    
      def find_collision tiles, target
        tiles.find { |t| t.rect.intersect_rect? target }
      end
    end
    
    def boot args
      # initialize the game on boot
      $game = Game.new
    end
    
    def tick args
      # tick the game class after setting .args
      # (which is provided by the engine)
      $game.args = args
      $game.tick
    end
    
    # debug function for resetting the game if requested
    def reset args
      $game = Game.new
    end
    
    

    The Little Probe - main.rb link

    # ./samples/99_genre_platformer/the_little_probe/app/main.rb
    class FallingCircle
      attr_gtk
    
      def tick
        fiddle
        defaults
        render
        input
        calc
      end
    
      def fiddle
        state.gravity     = -0.02
        circle.radius     = 15
        circle.elasticity = 0.4
        camera.follow_speed = 0.4 * 0.4
      end
    
      def render
        render_stage_editor
        render_debug
        render_game
      end
    
      def defaults
        # if Kernel.tick_count == 0
        #   args.audio[:bg] = { input: "sounds/bg.ogg", looping: true }
        # end
    
        state.storyline ||= [
          { text: "<- -> to aim, hold space to charge",                            distance_gate: 0 },
          { text: "the little probe - by @amirrajan, made with DragonRuby Game Toolkit", distance_gate: 0 },
          { text: "mission control, this is sasha. landing on europa successful.", distance_gate: 0 },
          { text: "operation \"find earth 2.0\", initiated at 8-29-2036 14:00.",   distance_gate: 0 },
          { text: "jupiter's sure is beautiful...",   distance_gate: 4000 },
          { text: "hmm, it seems there's some kind of anomoly in the sky",   distance_gate: 7000 },
          { text: "dancing lights, i'll call them whisps.",   distance_gate: 8000 },
          { text: "#todo... look i ran out of time -_-",   distance_gate: 9000 },
          { text: "there's never enough time",   distance_gate: 9000 },
          { text: "the game jam was fun though ^_^",   distance_gate: 10000 },
        ]
    
        load_level force: Kernel.tick_count == 0
        state.line_mode            ||= :terrain
    
        state.sound_index          ||= 1
        circle.potential_lift      ||= 0
        circle.angle               ||= 90
        circle.check_point_at      ||= -1000
        circle.game_over_at        ||= -1000
        circle.x                   ||= -485
        circle.y                   ||= 12226
        circle.check_point_x       ||= circle.x
        circle.check_point_y       ||= circle.y
        circle.dy                  ||= 0
        circle.dx                  ||= 0
        circle.previous_dy         ||= 0
        circle.previous_dx         ||= 0
        circle.angle               ||= 0
        circle.after_images        ||= []
        circle.terrains_to_monitor ||= {}
        circle.impact_history      ||= []
    
        camera.x                   ||= 0
        camera.y                   ||= 0
        camera.target_x            ||= 0
        camera.target_y            ||= 0
        state.snaps                ||= { }
        state.snap_number            = 10
    
        args.state.storyline_x ||= -1000
        args.state.storyline_y ||= -1000
      end
    
      def render_game
        outputs.background_color = [0, 0, 0]
        outputs.sprites << { x: -circle.x + 1100,
                             y: -circle.y - 100,
                             w: 2416 * 4,
                             h: 3574 * 4,
                             path: 'sprites/jupiter.png' }
        outputs.sprites << { x: -circle.x,
                             y: -circle.y,
                             w: 2416 * 4,
                             h: 3574 * 4,
                             path: 'sprites/level.png' }
        outputs.sprites << state.whisp_queue
        render_aiming_retical
        render_circle
        render_notification
      end
    
      def render_notification
        toast_length = 500
        if circle.game_over_at.elapsed_time < toast_length
          label_text = "..."
        elsif circle.check_point_at.elapsed_time > toast_length
          args.state.current_storyline = nil
          return
        end
        if circle.check_point_at &&
           circle.check_point_at.elapsed_time == 1 &&
           !args.state.current_storyline
           if args.state.storyline.length > 0 && args.state.distance_traveled > args.state.storyline[0][:distance_gate]
             args.state.current_storyline = args.state.storyline.shift[:text]
             args.state.distance_traveled ||= 0
             args.state.storyline_x = circle.x
             args.state.storyline_y = circle.y
           end
          return unless args.state.current_storyline
        end
        label_text = args.state.current_storyline
        return unless label_text
        x = circle.x + camera.x
        y = circle.y + camera.y - 40
        w = 900
        h = 30
        outputs.primitives << { x: x - w.idiv(2), y: y - h, w: w, h: h, r: 255, g: 255, b: 255, a: 255, primitive_marker: :solid }
        outputs.primitives << { x: x - w.idiv(2), y: y - h, w: w, h: h, r: 0, g: 0, b: 0, a: 255, primitive_marker: :border }
        outputs.labels << { x: x, y: y - 4, text: label_text, size_enum: 1, alignment_enum: 1, r: 0, g: 0, b: 0, a: 255 }
      end
    
      def render_aiming_retical
        outputs.sprites << { x: state.camera.x + circle.x + circle.angle.vector_x(circle.potential_lift * 10) - 5,
                             y: state.camera.y + circle.y + circle.angle.vector_y(circle.potential_lift * 10) - 5,
                             w: 10, h: 10, path: 'sprites/circle-orange.png' }
        outputs.sprites << { x: state.camera.x + circle.x + circle.angle.vector_x(circle.radius * 3) - 5,
                             y: state.camera.y + circle.y + circle.angle.vector_y(circle.radius * 3) - 5,
                             w: 10, h: 10, path: 'sprites/circle-orange.png', angle: 0, a: 128 }
        if rand > 0.9
          outputs.sprites << { x: state.camera.x + circle.x + circle.angle.vector_x(circle.radius * 3) - 5,
                               y: state.camera.y + circle.y + circle.angle.vector_y(circle.radius * 3) - 5,
                               w: 10, h: 10, path: 'sprites/circle-white.png', angle: 0, a: 128 }
        end
      end
    
      def render_circle
        outputs.sprites << circle.after_images.map do |ai|
          ai.merge(x: ai.x + state.camera.x - circle.radius,
                   y: ai.y + state.camera.y - circle.radius,
                   w: circle.radius * 2,
                   h: circle.radius * 2,
                   path: 'sprites/circle-white.png')
        end
    
        outputs.sprites << { x: (circle.x - circle.radius) + state.camera.x,
                             y: (circle.y - circle.radius) + state.camera.y,
                             w: circle.radius * 2,
                             h: circle.radius * 2,
                             path: 'sprites/probe.png' }
      end
    
      def render_debug
        return unless state.debug_mode
    
        outputs.labels << { x: 10, y: 30, text: state.line_mode, size_enum: 0, alignment_enum: 0, r: 0, g: 0, b: 0 }
        outputs.labels << { x: 12, y: 32, text: state.line_mode, size_enum: 0, alignment_enum: 0, r: 255, g: 255, b: 255 }
    
        args.outputs.lines << trajectory(circle).to_line.to_hash.tap do |h|
          h[:x] += state.camera.x
          h[:y] += state.camera.y
          h[:x2] += state.camera.x
          h[:y2] += state.camera.y
        end
    
        outputs.primitives << state.terrain.find_all do |t|
          circle.x.between?(t.x - 640, t.x2 + 640) || circle.y.between?(t.y - 360, t.y2 + 360)
        end.map do |t|
          [
            t.to_line.merge(r: 0, g: 255, b: 0).then do |h|
              h.x  += state.camera.x
              h.y  += state.camera.y
              h.x2 += state.camera.x
              h.y2 += state.camera.y
              if circle.rect.intersect_rect? t[:rect]
                h[:r] = 255
                h[:g] = 0
              end
              h
            end,
            t[:rect].to_border.merge(r: 255, g: 0, b: 0).then do |h|
              h.x += state.camera.x
              h.y += state.camera.y
              h.b = 255 if line_near_rect? circle.rect, t
              h
            end
          ]
        end
    
        outputs.primitives << state.lava.find_all do |t|
          circle.x.between?(t.x - 640, t.x2 + 640) || circle.y.between?(t.y - 360, t.y2 + 360)
        end.map do |t|
          [
            t.to_line.merge(r: 0, g: 0, b: 255).then do |h|
              h.x  += state.camera.x
              h.y  += state.camera.y
              h.x2 += state.camera.x
              h.y2 += state.camera.y
              if circle.rect.intersect_rect? t[:rect]
                h[:r] = 255
                h[:b] = 0
              end
              h
            end,
            t[:rect].to_border.merge(r: 255, g: 0, b: 0).then do |h|
              h.x += state.camera.x
              h.y += state.camera.y
              h.b = 255 if line_near_rect? circle.rect, t
              h
            end
          ]
        end
    
        if state.god_mode
          border = circle.rect.merge(x: circle.rect.x + state.camera.x,
                                     y: circle.rect.y + state.camera.y,
                                     g: 255)
        else
          border = circle.rect.merge(x: circle.rect.x + state.camera.x,
                                     y: circle.rect.y + state.camera.y,
                                     b: 255)
        end
    
        outputs.borders << border
    
        overlapping ||= {}
    
        circle.impact_history.each do |h|
          label_mod = 300
          x = (h[:body][:x].-(150).idiv(label_mod)) * label_mod + camera.x
          y = (h[:body][:y].+(150).idiv(label_mod)) * label_mod + camera.y
          10.times do
            if overlapping[x] && overlapping[x][y]
              y -= 52
            else
              break
            end
          end
    
          overlapping[x] ||= {}
          overlapping[x][y] ||= true
          outputs.primitives << [x, y - 25, 300, 50, 0, 0, 0, 128].solid
          outputs.labels << [x + 10, y + 24, "dy: %.2f" % h[:body][:new_dy], -2, 0, 255, 255, 255]
          outputs.labels << [x + 10, y +  9, "dx: %.2f" % h[:body][:new_dx], -2, 0, 255, 255, 255]
          outputs.labels << [x + 10, y -  5, " ?: #{h[:body][:new_reason]}", -2, 0, 255, 255, 255]
    
          outputs.labels << [x + 100, y + 24, "angle: %.2f" % h[:impact][:angle], -2, 0, 255, 255, 255]
          outputs.labels << [x + 100, y + 9, "m(l): %.2f" % h[:terrain][:slope], -2, 0, 255, 255, 255]
          outputs.labels << [x + 100, y - 5, "m(c): %.2f" % h[:body][:slope], -2, 0, 255, 255, 255]
    
          outputs.labels << [x + 200, y + 24, "ray: #{h[:impact][:ray]}", -2, 0, 255, 255, 255]
          outputs.labels << [x + 200, y +  9, "nxt: #{h[:impact][:ray_next]}", -2, 0, 255, 255, 255]
          outputs.labels << [x + 200, y -  5, "typ: #{h[:impact][:type]}", -2, 0, 255, 255, 255]
        end
    
        if circle.floor
          outputs.labels << [circle.x + camera.x + 30, circle.y + camera.y + 100, "point: #{circle.floor_point.slice(:x, :y).values}", -2, 0]
          outputs.labels << [circle.x + camera.x + 31, circle.y + camera.y + 101, "point: #{circle.floor_point.slice(:x, :y).values}", -2, 0, 255, 255, 255]
          outputs.labels << [circle.x + camera.x + 30, circle.y + camera.y +  85, "circle: #{circle.as_hash.slice(:x, :y).values}", -2, 0]
          outputs.labels << [circle.x + camera.x + 31, circle.y + camera.y +  86, "circle: #{circle.as_hash.slice(:x, :y).values}", -2, 0, 255, 255, 255]
          outputs.labels << [circle.x + camera.x + 30, circle.y + camera.y +  70, "rel: #{circle.floor_relative_x} #{circle.floor_relative_y}", -2, 0]
          outputs.labels << [circle.x + camera.x + 31, circle.y + camera.y +  71, "rel: #{circle.floor_relative_x} #{circle.floor_relative_y}", -2, 0, 255, 255, 255]
        end
      end
    
      def render_stage_editor
        return unless state.god_mode
        return unless state.point_one
        args.lines << [state.point_one, inputs.mouse.point, 0, 255, 255]
      end
    
      def trajectory body
        { x: body.x + body.dx,
          y: body.y + body.dy,
          x2: body.x + body.dx * 1000,
          y2: body.y + body.dy * 1000,
          r: 0, g: 255, b: 255 }
      end
    
      def rect_for_line line
        if line.x > line.x2
          x  = line.x2
          y  = line.y2
          x2 = line.x
          y2 = line.y
        else
          x  = line.x
          y  = line.y
          x2 = line.x2
          y2 = line.y2
        end
    
        w = x2 - x
        h = y2 - y
    
        if h < 0
          y += h
          h = h.abs
        end
    
        if w < circle.radius
          x -= circle.radius
          w = circle.radius * 2
        end
    
        if h < circle.radius
          y -= circle.radius
          h = circle.radius * 2
        end
    
        { x: x, y: y, w: w, h: h }
      end
    
      def snap_to_grid x, y, snaps
        snap_number = 10
        x = x.to_i
        y = y.to_i
    
        x_floor = x.idiv(snap_number) * snap_number
        x_mod   = x % snap_number
        x_ceil  = (x.idiv(snap_number) + 1) * snap_number
    
        y_floor = y.idiv(snap_number) * snap_number
        y_mod   = y % snap_number
        y_ceil  = (y.idiv(snap_number) + 1) * snap_number
    
        if snaps[x_floor]
          x_result = x_floor
        elsif snaps[x_ceil]
          x_result = x_ceil
        elsif x_mod < snap_number.idiv(2)
          x_result = x_floor
        else
          x_result = x_ceil
        end
    
        snaps[x_result] ||= {}
    
        if snaps[x_result][y_floor]
          y_result = y_floor
        elsif snaps[x_result][y_ceil]
          y_result = y_ceil
        elsif y_mod < snap_number.idiv(2)
          y_result = y_floor
        else
          y_result = y_ceil
        end
    
        snaps[x_result][y_result] = true
        return [x_result, y_result]
    
      end
    
      def snap_line line
        x, y, x2, y2 = line
      end
    
      def string_to_line s
        x, y, x2, y2 = s.split(',').map(&:to_f)
    
        if x > x2
          x2, x = x, x2
          y2, y = y, y2
        end
    
        x, y = snap_to_grid x, y, state.snaps
        x2, y2 = snap_to_grid x2, y2, state.snaps
        [x, y, x2, y2].line.to_hash
      end
    
      def load_lines file
        return unless state.snaps
        data = gtk.read_file(file) || ""
        data.each_line
            .reject { |l| l.strip.length == 0 }
            .map { |l| string_to_line l }
            .map { |h| h.merge(rect: rect_for_line(h))  }
      end
    
      def load_terrain
        load_lines 'data/level.txt'
      end
    
      def load_lava
        load_lines 'data/level_lava.txt'
      end
    
      def load_level force: false
        if force
          state.snaps = {}
          state.terrain = load_terrain
          state.lava = load_lava
        else
          state.terrain ||= load_terrain
          state.lava ||= load_lava
        end
      end
    
      def save_lines lines, file
        s = lines.map do |l|
          "#{l.x},#{l.y},#{l.x2},#{l.y2}"
        end.join("\n")
        gtk.write_file(file, s)
      end
    
      def save_level
        save_lines(state.terrain, 'level.txt')
        save_lines(state.lava, 'level_lava.txt')
        load_level force: true
      end
    
      def line_near_rect? rect, terrain
        geometry.intersect_rect?(rect, terrain[:rect])
      end
    
      def point_within_line? point, line
        return false if !point
        return false if !line
        return true
      end
    
      def calc_impacts x, dx, y, dy, radius
        results = { }
        results[:x] = x
        results[:y] = y
        results[:dx] = x
        results[:dy] = y
        results[:point] = { x: x, y: y }
        results[:rect] = { x: x - radius, y: y - radius, w: radius * 2, h: radius * 2 }
        results[:trajectory] = trajectory(results)
        results[:impacts] = terrain.find_all { |t| t && (line_near_rect? results[:rect], t) }.map do |t|
          intersection = geometry.ray_intersect(results[:trajectory], t)
          {
            terrain: t,
            point: geometry.ray_intersect(results[:trajectory], t),
            type: :terrain
          }
        end
    
        results[:impacts] += lava.find_all { |t| line_near_rect? results[:rect], t }.map do |t|
          intersection = geometry.ray_intersect(results[:trajectory], t)
          {
            terrain: t,
            point: geometry.ray_intersect(results[:trajectory], t),
            type: :lava
          }
        end
    
        results
      end
    
      def calc_potential_impacts
        impact_results = calc_impacts circle.x, circle.dx, circle.y, circle.dy, circle.radius
        circle.rect = impact_results[:rect]
        circle.trajectory = impact_results[:trajectory]
        circle.impacts = impact_results[:impacts]
      end
    
      def calc_terrains_to_monitor
        return unless circle.impacts
        circle.impact = nil
        circle.impacts.each do |i|
          future_circle = { x: circle.x + circle.dx, y: circle.y + circle.dy }
          circle.terrains_to_monitor[i[:terrain]] ||= {
            ray_start: geometry.ray_test(future_circle, i[:terrain]),
          }
    
          circle.terrains_to_monitor[i[:terrain]][:ray_current] = geometry.ray_test(future_circle, i[:terrain])
          if circle.terrains_to_monitor[i[:terrain]][:ray_start] != circle.terrains_to_monitor[i[:terrain]][:ray_current]
            circle.impact = i
            circle.ray_current = circle.terrains_to_monitor[i[:terrain]][:ray_current]
          end
        end
      end
    
      def impact_result body, impact
        infinity_alias = 1000
        r = {
          body: {},
          terrain: {},
          impact: {}
        }
    
        r[:body][:line] = body.trajectory.dup
        r[:body][:slope] = geometry.line_slope(body.trajectory, replace_infinity: infinity_alias)
        r[:body][:slope_sign] = r[:body][:slope].sign
        r[:body][:x] = body.x
        r[:body][:y] = body.y
        r[:body][:dy] = body.dy
        r[:body][:dx] = body.dx
    
        r[:terrain][:line] = impact[:terrain].dup
        r[:terrain][:slope] = geometry.line_slope(impact[:terrain], replace_infinity: infinity_alias)
        r[:terrain][:slope_sign] = r[:terrain][:slope].sign
    
        r[:impact][:angle] = -geometry.angle_between_lines(body.trajectory, impact[:terrain], replace_infinity: infinity_alias)
        r[:impact][:point] = { x: impact[:point].x, y: impact[:point].y }
        r[:impact][:same_slope_sign] = r[:body][:slope_sign] == r[:terrain][:slope_sign]
        r[:impact][:ray] = body.ray_current
        r[:body][:new_on_floor] = body.on_floor
        r[:body][:new_floor] = r[:terrain][:line]
    
        if r[:impact][:angle].abs < 90 && r[:terrain][:slope].abs < 3
          play_sound
          r[:body][:new_dy] = r[:body][:dy] * circle.elasticity * -1
          r[:body][:new_dx] = r[:body][:dx] * circle.elasticity
          r[:impact][:type] = :horizontal
          r[:body][:new_reason] = "-"
        elsif r[:impact][:angle].abs < 90 && r[:terrain][:slope].abs > 3
          play_sound
          r[:body][:new_dy] = r[:body][:dy] * 1.1
          r[:body][:new_dx] = r[:body][:dx] * -circle.elasticity
          r[:impact][:type] = :vertical
          r[:body][:new_reason] = "|"
        else
          play_sound
          r[:body][:new_dx] = r[:body][:dx] * -circle.elasticity
          r[:body][:new_dy] = r[:body][:dy] * -circle.elasticity
          r[:impact][:type] = :slanted
          r[:body][:new_reason] = "/"
        end
    
        r[:impact][:energy] = r[:body][:new_dx].abs + r[:body][:new_dy].abs
    
        if r[:impact][:energy] <= 0.3 && r[:terrain][:slope].abs < 4
          r[:body][:new_dx] = 0
          r[:body][:new_dy] = 0
          r[:impact][:energy] = 0
          r[:body][:new_on_floor] = true if r[:impact][:point].y < body.y
          r[:body][:new_floor] = r[:terrain][:line]
          r[:body][:new_reason] = "0"
        end
    
        r[:impact][:ray_next] = geometry.ray_test({ x: r[:body][:x] - (r[:body][:dx] * 1.1) + r[:body][:new_dx],
                                                    y: r[:body][:y] - (r[:body][:dy] * 1.1) + r[:body][:new_dy] + state.gravity },
                                                  r[:terrain][:line])
    
        if r[:impact][:ray_next] == r[:impact][:ray]
          r[:body][:new_dx] *= -1
          r[:body][:new_dy] *= -1
          r[:body][:new_reason] = "clip"
        end
    
        r
      end
    
      def game_over!
        circle.x = circle.check_point_x
        circle.y = circle.check_point_y
        circle.dx = 0
        circle.dy = 0
        circle.game_over_at = Kernel.tick_count
      end
    
      def not_game_over!
        impact_history_entry = impact_result circle, circle.impact
        circle.impact_history << impact_history_entry
        circle.x -= circle.dx * 1.1
        circle.y -= circle.dy * 1.1
        circle.dx = impact_history_entry[:body][:new_dx]
        circle.dy = impact_history_entry[:body][:new_dy]
        circle.on_floor = impact_history_entry[:body][:new_on_floor]
    
        if circle.on_floor
          circle.check_point_at = Kernel.tick_count
          circle.check_point_x = circle.x
          circle.check_point_y = circle.y
        end
    
        circle.previous_floor = circle.floor || {}
        circle.floor = impact_history_entry[:body][:new_floor] || {}
        circle.floor_point = impact_history_entry[:impact][:point]
        if circle.floor.slice(:x, :y, :x2, :y2) != circle.previous_floor.slice(:x, :y, :x2, :y2)
          new_relative_x = if circle.dx > 0
                             :right
                           elsif circle.dx < 0
                             :left
                           else
                             nil
                           end
    
          new_relative_y = if circle.dy > 0
                             :above
                           elsif circle.dy < 0
                             :below
                           else
                             nil
                           end
    
          circle.floor_relative_x = new_relative_x
          circle.floor_relative_y = new_relative_y
        end
    
        circle.impact = nil
        circle.terrains_to_monitor.clear
      end
    
      def calc_physics
        if args.state.god_mode
          calc_potential_impacts
          calc_terrains_to_monitor
          return
        end
    
        if circle.y < -700
          game_over
          return
        end
    
        return if state.game_over
        return if circle.on_floor
        circle.previous_dy = circle.dy
        circle.previous_dx = circle.dx
        circle.x  += circle.dx
        circle.y  += circle.dy
        args.state.distance_traveled ||= 0
        args.state.distance_traveled += circle.dx.abs + circle.dy.abs
        circle.dy += state.gravity
        calc_potential_impacts
        calc_terrains_to_monitor
        return unless circle.impact
        if circle.impact && circle.impact[:type] == :lava
          game_over!
        else
          not_game_over!
        end
      end
    
      def input_god_mode
        state.debug_mode = !state.debug_mode if inputs.keyboard.key_down.forward_slash
    
        # toggle god mode
        if inputs.keyboard.key_down.g
          state.god_mode = !state.god_mode
          state.potential_lift = 0
          circle.floor = nil
          circle.floor_point = nil
          circle.floor_relative_x = nil
          circle.floor_relative_y = nil
          circle.impact = nil
          circle.terrains_to_monitor.clear
          return
        end
    
        return unless state.god_mode
    
        circle.x = circle.x.to_i
        circle.y = circle.y.to_i
    
        # move god circle
        if inputs.keyboard.left || inputs.keyboard.a
          circle.x -= 20
        elsif inputs.keyboard.right || inputs.keyboard.d || inputs.keyboard.f
          circle.x += 20
        end
    
        if inputs.keyboard.up || inputs.keyboard.w
          circle.y += 20
        elsif inputs.keyboard.down || inputs.keyboard.s
          circle.y -= 20
        end
    
        # delete terrain
        if inputs.keyboard.key_down.x
          calc_terrains_to_monitor
          state.terrain = state.terrain.reject do |t|
            t[:rect].intersect_rect? circle.rect
          end
    
          state.lava = state.lava.reject do |t|
            t[:rect].intersect_rect? circle.rect
          end
    
          calc_potential_impacts
          save_level
        end
    
        # change terrain type
        if inputs.keyboard.key_down.l
          if state.line_mode == :terrain
            state.line_mode = :lava
          else
            state.line_mode = :terrain
          end
        end
    
        if inputs.mouse.click && !state.point_one
          state.point_one = inputs.mouse.click.point
        elsif inputs.mouse.click && state.point_one
          l = [*state.point_one, *inputs.mouse.click.point]
          l = [l.x  - state.camera.x,
               l.y  - state.camera.y,
               l.x2 - state.camera.x,
               l.y2 - state.camera.y].line.to_hash
          l[:rect] = rect_for_line l
          if state.line_mode == :terrain
            state.terrain << l
          else
            state.lava << l
          end
          save_level
          next_x = inputs.mouse.click.point.x - 640
          next_y = inputs.mouse.click.point.y - 360
          circle.x += next_x
          circle.y += next_y
          state.point_one = nil
        elsif inputs.keyboard.one
          state.point_one = [circle.x + camera.x, circle.y+ camera.y]
        end
    
        # cancel chain lines
        if inputs.keyboard.key_down.nine || inputs.keyboard.key_down.escape || inputs.keyboard.key_up.six || inputs.keyboard.key_up.one
          state.point_one = nil
        end
      end
    
      def play_sound
        return if state.sound_debounce > 0
        state.sound_debounce = 5
        # outputs.sounds << "sounds/03#{"%02d" % state.sound_index}.wav"
        state.sound_index += 1
        if state.sound_index > 21
          state.sound_index = 1
        end
      end
    
      def input_game
        if inputs.keyboard.down || inputs.keyboard.space
          circle.potential_lift += 0.03
          circle.potential_lift = circle.potential_lift.lesser(10)
        elsif inputs.keyboard.key_up.down || inputs.keyboard.key_up.space
          play_sound
          circle.dy += circle.angle.vector_y circle.potential_lift
          circle.dx += circle.angle.vector_x circle.potential_lift
    
          if circle.on_floor
            if circle.floor_relative_y == :above
              circle.y += circle.potential_lift.abs * 2
            elsif circle.floor_relative_y == :below
              circle.y -= circle.potential_lift.abs * 2
            end
          end
    
          circle.on_floor = false
          circle.potential_lift = 0
          circle.terrains_to_monitor.clear
          circle.impact_history.clear
          circle.impact = nil
          calc_physics
        end
    
        # aim probe
        if inputs.keyboard.right || inputs.keyboard.a
          circle.angle -= 2
        elsif inputs.keyboard.left || inputs.keyboard.d
          circle.angle += 2
        end
      end
    
      def input
        input_god_mode
        input_game
      end
    
      def calc_camera
        state.camera.target_x = 640 - circle.x
        state.camera.target_y = 360 - circle.y
        xdiff = state.camera.target_x - state.camera.x
        ydiff = state.camera.target_y - state.camera.y
        state.camera.x += xdiff * camera.follow_speed
        state.camera.y += ydiff * camera.follow_speed
      end
    
      def calc
        state.sound_debounce ||= 0
        state.sound_debounce -= 1
        state.sound_debounce = 0 if state.sound_debounce < 0
        if state.god_mode
          circle.dy *= 0.1
          circle.dx *= 0.1
        end
        calc_camera
        state.whisp_queue ||= []
        if Kernel.tick_count.mod_zero?(4)
          state.whisp_queue << {
            x: -300,
            y: 1400 * rand,
            speed: 2.randomize(:ratio) + 3,
            w: 20,
            h: 20, path: 'sprites/whisp.png',
            a: 0,
            created_at: Kernel.tick_count,
            angle: 0,
            r: 100,
            g: 128 + 128 * rand,
            b: 128 + 128 * rand
          }
        end
    
        state.whisp_queue.each do |w|
          w.x += w[:speed] * 2
          w.x -= circle.dx * 0.3
          w.y -= w[:speed]
          w.y -= circle.dy * 0.3
          w.angle += w[:speed]
          w.a = w[:created_at].ease(30) * 255
        end
    
        state.whisp_queue = state.whisp_queue.reject { |w| w[:x] > 1280 }
    
        if Kernel.tick_count.mod_zero?(2) && (circle.dx != 0 || circle.dy != 0)
          circle.after_images << {
            x: circle.x,
            y: circle.y,
            w: circle.radius,
            h: circle.radius,
            a: 255,
            created_at: Kernel.tick_count
          }
        end
    
        circle.after_images.each do |ai|
          ai.a = ai[:created_at].ease(10, :flip) * 255
        end
    
        circle.after_images = circle.after_images.reject { |ai| ai[:created_at].elapsed_time > 10 }
        calc_physics
      end
    
      def circle
        state.circle
      end
    
      def camera
        state.camera
      end
    
      def terrain
        state.terrain
      end
    
      def lava
        state.lava
      end
    end
    
    # $gtk.reset
    
    def tick args
      args.outputs.background_color = [0, 0, 0]
      if args.inputs.keyboard.r
        args.gtk.reset
        return
      end
      # uncomment the line below to slow down the game so you
      # can see each tick as it passes
      # args.gtk.slowmo! 30
      $game ||= FallingCircle.new
      $game.args = args
      $game.tick
    end
    
    def reset
      $game = nil
    end
    
    

    The Little Probe - Data - level.txt link

    # ./samples/99_genre_platformer/the_little_probe/data/level.txt
    640,8840,1180,8840
    -60,10220,0,9960
    -60,10220,0,10500
    0,10500,0,10780
    0,10780,40,10900
    500,10920,760,10960
    300,10560,820,10600
    420,10320,700,10300
    820,10600,1500,10600
    1500,10600,1940,10600
    1940,10600,2380,10580
    2380,10580,2800,10620
    2240,11080,2480,11020
    2000,11120,2240,11080
    1760,11180,2000,11120
    1620,11180,1760,11180
    1500,11220,1620,11180
    1180,11280,1340,11220
    1040,11240,1180,11280
    840,11280,1040,11240
    640,11280,840,11280
    500,11220,640,11280
    420,11140,500,11220
    240,11100,420,11140
    100,11120,240,11100
    0,11180,100,11120
    -160,11220,0,11180
    -260,11240,-160,11220
    1340,11220,1500,11220
    960,13300,1280,13060
    1280,13060,1540,12860
    1540,12860,1820,12700
    1820,12700,2080,12520
    2080,12520,2240,12400
    2240,12400,2240,12240
    2240,12240,2400,12080
    2400,12080,2560,11920
    2560,11920,2640,11740
    2640,11740,2740,11580
    2740,11580,2800,11400
    2800,11400,2800,11240
    2740,11140,2800,11240
    2700,11040,2740,11140
    2700,11040,2740,10960
    2740,10960,2740,10920
    2700,10900,2740,10920
    2380,10900,2700,10900
    2040,10920,2380,10900
    1720,10940,2040,10920
    1380,11000,1720,10940
    1180,10980,1380,11000
    900,10980,1180,10980
    760,10960,900,10980
    240,10960,500,10920
    40,10900,240,10960
    0,9700,0,9960
    -60,9500,0,9700
    -60,9420,-60,9500
    -60,9420,-60,9340
    -60,9340,-60,9280
    -60,9120,-60,9280
    -60,8940,-60,9120
    -60,8940,-60,8780
    -60,8780,0,8700
    0,8700,40,8680
    40,8680,240,8700
    240,8700,360,8780
    360,8780,640,8840
    1420,8400,1540,8480
    1540,8480,1680,8500
    1680,8500,1940,8460
    1180,8840,1280,8880
    1280,8880,1340,8860
    1340,8860,1720,8860
    1720,8860,1820,8920
    1820,8920,1820,9140
    1820,9140,1820,9280
    1820,9460,1820,9280
    1760,9480,1820,9460
    1640,9480,1760,9480
    1540,9500,1640,9480
    1340,9500,1540,9500
    1100,9500,1340,9500
    1040,9540,1100,9500
    960,9540,1040,9540
    300,9420,360,9460
    240,9440,300,9420
    180,9600,240,9440
    120,9660,180,9600
    100,9820,120,9660
    100,9820,120,9860
    120,9860,140,9900
    140,9900,140,10000
    140,10440,180,10540
    100,10080,140,10000
    100,10080,140,10100
    140,10100,140,10440
    180,10540,300,10560
    2140,9560,2140,9640
    2140,9720,2140,9640
    1880,9780,2140,9720
    1720,9780,1880,9780
    1620,9740,1720,9780
    1500,9780,1620,9740
    1380,9780,1500,9780
    1340,9820,1380,9780
    1200,9820,1340,9820
    1100,9780,1200,9820
    900,9780,1100,9780
    820,9720,900,9780
    540,9720,820,9720
    360,9840,540,9720
    360,9840,360,9960
    360,9960,360,10080
    360,10140,360,10080
    360,10140,360,10240
    360,10240,420,10320
    700,10300,820,10280
    820,10280,820,10280
    820,10280,900,10320
    900,10320,1040,10300
    1040,10300,1200,10320
    1200,10320,1380,10280
    1380,10280,1500,10300
    1500,10300,1760,10300
    2800,10620,2840,10600
    2840,10600,2900,10600
    2900,10600,3000,10620
    3000,10620,3080,10620
    3080,10620,3140,10600
    3140,10540,3140,10600
    3140,10540,3140,10460
    3140,10460,3140,10360
    3140,10360,3140,10260
    3140,10260,3140,10140
    3140,10140,3140,10000
    3140,10000,3140,9860
    3140,9860,3160,9720
    3160,9720,3160,9580
    3160,9580,3160,9440
    3160,9300,3160,9440
    3160,9300,3160,9140
    3160,9140,3160,8980
    3160,8980,3160,8820
    3160,8820,3160,8680
    3160,8680,3160,8520
    1760,10300,1880,10300
    660,9500,960,9540
    640,9460,660,9500
    360,9460,640,9460
    -480,10760,-440,10880
    -480,11020,-440,10880
    -480,11160,-260,11240
    -480,11020,-480,11160
    -600,11420,-380,11320
    -380,11320,-200,11340
    -200,11340,0,11340
    0,11340,180,11340
    960,13420,960,13300
    960,13420,960,13520
    960,13520,1000,13560
    1000,13560,1040,13540
    1040,13540,1200,13440
    1200,13440,1380,13380
    1380,13380,1620,13300
    1620,13300,1820,13220
    1820,13220,2000,13200
    2000,13200,2240,13200
    2240,13200,2440,13160
    2440,13160,2640,13040
    -480,10760,-440,10620
    -440,10620,-360,10560
    -380,10460,-360,10560
    -380,10460,-360,10300
    -380,10140,-360,10300
    -380,10140,-380,10040
    -380,9880,-380,10040
    -380,9720,-380,9880
    -380,9720,-380,9540
    -380,9360,-380,9540
    -380,9180,-380,9360
    -380,9180,-380,9000
    -380,8840,-380,9000
    -380,8840,-380,8760
    -380,8760,-380,8620
    -380,8620,-380,8520
    -380,8520,-360,8400
    -360,8400,-100,8400
    -100,8400,-60,8420
    -60,8420,240,8440
    240,8440,240,8380
    240,8380,500,8440
    500,8440,760,8460
    760,8460,1000,8400
    1000,8400,1180,8420
    1180,8420,1420,8400
    1940,8460,2140,8420
    2140,8420,2200,8520
    2200,8680,2200,8520
    2140,8840,2200,8680
    2140,8840,2140,9020
    2140,9100,2140,9020
    2140,9200,2140,9100
    2140,9200,2200,9320
    2200,9320,2200,9440
    2140,9560,2200,9440
    1880,10300,2200,10280
    2200,10280,2480,10260
    2480,10260,2700,10240
    2700,10240,2840,10180
    2840,10180,2900,10060
    2900,9860,2900,10060
    2900,9640,2900,9860
    2900,9640,2900,9500
    2900,9460,2900,9500
    2740,9460,2900,9460
    2700,9460,2740,9460
    2700,9360,2700,9460
    2700,9320,2700,9360
    2600,9320,2700,9320
    2600,9260,2600,9320
    2600,9200,2600,9260
    2480,9120,2600,9200
    2440,9080,2480,9120
    2380,9080,2440,9080
    2320,9060,2380,9080
    2320,8860,2320,9060
    2320,8860,2380,8840
    2380,8840,2480,8860
    2480,8860,2600,8840
    2600,8840,2740,8840
    2740,8840,2840,8800
    2840,8800,2900,8700
    2900,8600,2900,8700
    2900,8480,2900,8600
    2900,8380,2900,8480
    2900,8380,2900,8260
    2900,8260,2900,8140
    2900,8140,2900,8020
    2900,8020,2900,7900
    2900,7820,2900,7900
    2900,7820,2900,7740
    2900,7660,2900,7740
    2900,7560,2900,7660
    2900,7460,2900,7560
    2900,7460,2900,7360
    2900,7260,2900,7360
    2840,7160,2900,7260
    2800,7080,2840,7160
    2700,7100,2800,7080
    2560,7120,2700,7100
    2400,7100,2560,7120
    2320,7100,2400,7100
    2140,7100,2320,7100
    2040,7080,2140,7100
    1940,7080,2040,7080
    1820,7140,1940,7080
    1680,7140,1820,7140
    1540,7140,1680,7140
    1420,7220,1540,7140
    1280,7220,1380,7220
    1140,7200,1280,7220
    1000,7220,1140,7200
    760,7280,900,7320
    540,7220,760,7280
    300,7180,540,7220
    180,7120,180,7160
    40,7140,180,7120
    -60,7160,40,7140
    -200,7120,-60,7160
    180,7160,300,7180
    -260,7060,-200,7120
    -260,6980,-260,7060
    -260,6880,-260,6980
    -260,6880,-260,6820
    -260,6820,-200,6760
    -200,6760,-100,6740
    -100,6740,-60,6740
    -60,6740,40,6740
    40,6740,300,6800
    300,6800,420,6760
    420,6760,500,6740
    500,6740,540,6760
    540,6760,540,6760
    540,6760,640,6780
    640,6660,640,6780
    580,6580,640,6660
    580,6440,580,6580
    580,6440,640,6320
    640,6320,640,6180
    580,6080,640,6180
    580,6080,640,5960
    640,5960,640,5840
    640,5840,640,5700
    640,5700,660,5560
    660,5560,660,5440
    660,5440,660,5300
    660,5140,660,5300
    660,5140,660,5000
    660,5000,660,4880
    660,4880,820,4860
    820,4860,1000,4840
    1000,4840,1100,4860
    1100,4860,1280,4860
    1280,4860,1420,4840
    1420,4840,1580,4860
    1580,4860,1720,4820
    1720,4820,1880,4860
    1880,4860,2000,4840
    2000,4840,2140,4840
    2140,4840,2320,4860
    2320,4860,2440,4880
    2440,4880,2600,4880
    2600,4880,2800,4880
    2800,4880,2900,4880
    2900,4880,2900,4820
    2900,4740,2900,4820
    2800,4700,2900,4740
    2520,4680,2800,4700
    2240,4660,2520,4680
    1940,4620,2240,4660
    1820,4580,1940,4620
    1820,4500,1820,4580
    1820,4500,1880,4420
    1880,4420,2000,4420
    2000,4420,2200,4420
    2200,4420,2400,4440
    2400,4440,2600,4440
    2600,4440,2840,4440
    2840,4440,2900,4400
    2740,4260,2900,4280
    2600,4240,2740,4260
    2480,4280,2600,4240
    2320,4240,2480,4280
    2140,4220,2320,4240
    1940,4220,2140,4220
    1880,4160,1940,4220
    1880,4160,1880,4080
    1880,4080,2040,4040
    2040,4040,2240,4060
    2240,4060,2400,4040
    2400,4040,2600,4060
    2600,4060,2740,4020
    2740,4020,2840,3940
    2840,3780,2840,3940
    2740,3660,2840,3780
    2700,3680,2740,3660
    2520,3700,2700,3680
    2380,3700,2520,3700
    2200,3720,2380,3700
    2040,3720,2200,3720
    1880,3700,2040,3720
    1820,3680,1880,3700
    1760,3600,1820,3680
    1760,3600,1820,3480
    1820,3480,1880,3440
    1880,3440,1960,3460
    1960,3460,2140,3460
    2140,3460,2380,3460
    2380,3460,2640,3440
    2640,3440,2900,3380
    2840,3280,2900,3380
    2840,3280,2900,3200
    2900,3200,2900,3140
    2840,3020,2900,3140
    2800,2960,2840,3020
    2700,3000,2800,2960
    2600,2980,2700,3000
    2380,3000,2600,2980
    2140,3000,2380,3000
    1880,3000,2140,3000
    1720,3040,1880,3000
    1640,2960,1720,3040
    1500,2940,1640,2960
    1340,3000,1500,2940
    1240,3000,1340,3000
    1140,3020,1240,3000
    1040,3000,1140,3020
    960,2960,1040,3000
    900,2960,960,2960
    840,2840,900,2960
    700,2820,840,2840
    540,2820,700,2820
    420,2820,540,2820
    180,2800,420,2820
    60,2780,180,2800
    -60,2800,60,2780
    -160,2760,-60,2800
    -260,2740,-160,2760
    -300,2640,-260,2740
    -360,2560,-300,2640
    -380,2460,-360,2560
    -380,2460,-300,2380
    -300,2300,-300,2380
    -300,2300,-300,2220
    -300,2100,-300,2220
    -300,2100,-300,2040
    -300,2040,-160,2040
    -160,2040,-60,2040
    -60,2040,60,2040
    60,2040,180,2040
    180,2040,360,2040
    360,2040,540,2040
    540,2040,700,2080
    660,2160,700,2080
    660,2160,700,2260
    660,2380,700,2260
    500,2340,660,2380
    360,2340,500,2340
    240,2340,360,2340
    40,2320,240,2340
    -60,2320,40,2320
    -100,2380,-60,2320
    -100,2380,-100,2460
    -100,2460,-100,2540
    -100,2540,0,2560
    0,2560,140,2600
    140,2600,300,2600
    300,2600,460,2600
    460,2600,640,2600
    640,2600,760,2580
    760,2580,820,2560
    820,2560,820,2500
    820,2500,820,2400
    820,2400,840,2320
    840,2320,840,2240
    820,2120,840,2240
    820,2020,820,2120
    820,1900,820,2020
    760,1840,820,1900
    640,1840,760,1840
    500,1840,640,1840
    300,1860,420,1880
    180,1840,300,1860
    420,1880,500,1840
    0,1840,180,1840
    -60,1860,0,1840
    -160,1840,-60,1860
    -200,1800,-160,1840
    -260,1760,-200,1800
    -260,1680,-260,1760
    -260,1620,-260,1680
    -260,1540,-260,1620
    -260,1540,-260,1460
    -300,1420,-260,1460
    -300,1420,-300,1340
    -300,1340,-260,1260
    -260,1260,-260,1160
    -260,1060,-260,1160
    -260,1060,-260,960
    -260,880,-260,960
    -260,880,-260,780
    -260,780,-260,680
    -300,580,-260,680
    -300,580,-300,480
    -300,480,-260,400
    -300,320,-260,400
    -300,320,-300,240
    -300,240,-200,220
    -200,220,-200,160
    -200,160,-100,140
    -100,140,0,120
    0,120,60,120
    60,120,180,120
    180,120,300,120
    300,120,420,140
    420,140,580,180
    580,180,760,180
    760,180,900,180
    960,180,1100,180
    1100,180,1340,200
    1340,200,1580,200
    1580,200,1720,180
    1720,180,2000,140
    2000,140,2240,140
    2240,140,2480,140
    2520,140,2800,160
    2800,160,3000,160
    3000,160,3140,160
    3140,260,3140,160
    3140,260,3140,380
    3080,500,3140,380
    3080,620,3080,500
    3080,620,3080,740
    3080,740,3080,840
    3080,960,3080,840
    3080,1080,3080,960
    3080,1080,3080,1200
    3080,1200,3080,1340
    3080,1340,3080,1460
    3080,1580,3080,1460
    3080,1700,3080,1580
    3080,1700,3080,1760
    3080,1760,3200,1760
    3200,1760,3320,1760
    3320,1760,3520,1760
    3520,1760,3680,1740
    3680,1740,3780,1700
    3780,1700,3840,1620
    3840,1620,3840,1520
    3840,1520,3840,1420
    3840,1320,3840,1420
    3840,1120,3840,1320
    3840,1120,3840,940
    3840,940,3840,760
    3780,600,3840,760
    3780,600,3780,440
    3780,320,3780,440
    3780,320,3780,160
    3780,60,3780,160
    3780,60,4020,60
    4020,60,4260,40
    4260,40,4500,40
    4500,40,4740,40
    4740,40,4840,20
    4840,20,4880,80
    4880,80,5080,40
    5080,40,5280,20
    5280,20,5500,0
    5500,0,5720,0
    5720,0,5940,60
    5940,60,6240,60
    6240,60,6540,20
    6540,20,6840,20
    6840,20,7040,0
    7040,0,7140,0
    7140,0,7400,20
    7400,20,7680,0
    7680,0,7940,0
    7940,0,8200,-20
    8200,-20,8360,20
    8360,20,8560,-40
    8560,-40,8760,0
    8760,0,8880,40
    8880,120,8880,40
    8840,220,8840,120
    8620,240,8840,220
    8420,260,8620,240
    8200,280,8420,260
    7940,280,8200,280
    7760,240,7940,280
    7560,220,7760,240
    7360,280,7560,220
    7140,260,7360,280
    6940,240,7140,260
    6720,220,6940,240
    6480,220,6720,220
    6360,300,6480,220
    6240,300,6360,300
    6200,500,6240,300
    6200,500,6360,540
    6360,540,6540,520
    6540,520,6720,480
    6720,480,6880,460
    6880,460,7080,500
    7080,500,7320,500
    7320,500,7680,500
    7680,620,7680,500
    7520,640,7680,620
    7360,640,7520,640
    7200,640,7360,640
    7040,660,7200,640
    6880,720,7040,660
    6720,700,6880,720
    6540,700,6720,700
    6420,760,6540,700
    6280,740,6420,760
    6240,760,6280,740
    6200,920,6240,760
    6200,920,6360,960
    6360,960,6540,960
    6540,960,6720,960
    6720,960,6760,980
    6760,980,6880,940
    6880,940,7080,940
    7080,940,7280,940
    7280,940,7520,920
    7520,920,7760,900
    7760,900,7980,860
    7980,860,8100,880
    8100,880,8280,900
    8280,900,8500,820
    8500,820,8700,820
    8700,820,8760,840
    8760,960,8760,840
    8700,1040,8760,960
    8560,1060,8700,1040
    8460,1080,8560,1060
    8360,1040,8460,1080
    8280,1080,8360,1040
    8160,1120,8280,1080
    8040,1120,8160,1120
    7940,1100,8040,1120
    7800,1120,7940,1100
    7680,1120,7800,1120
    7520,1100,7680,1120
    7360,1100,7520,1100
    7200,1120,7360,1100
    7040,1180,7200,1120
    6880,1160,7040,1180
    6720,1160,6880,1160
    6540,1160,6720,1160
    6360,1160,6540,1160
    6200,1160,6360,1160
    6040,1220,6200,1160
    6040,1220,6040,1400
    6040,1400,6200,1440
    6200,1440,6320,1440
    6320,1440,6440,1440
    6600,1440,6760,1440
    6760,1440,6940,1420
    6440,1440,6600,1440
    6940,1420,7280,1400
    7280,1400,7560,1400
    7560,1400,7760,1400
    7760,1400,7940,1360
    7940,1360,8100,1380
    8100,1380,8280,1340
    8280,1340,8460,1320
    8660,1300,8760,1360
    8460,1320,8660,1300
    8760,1360,8800,1500
    8800,1660,8800,1500
    8800,1660,8800,1820
    8700,1840,8800,1820
    8620,1860,8700,1840
    8560,1800,8620,1860
    8560,1800,8620,1680
    8500,1640,8620,1680
    8420,1680,8500,1640
    8280,1680,8420,1680
    8160,1680,8280,1680
    7900,1680,8160,1680
    7680,1680,7900,1680
    7400,1660,7680,1680
    7140,1680,7400,1660
    6880,1640,7140,1680
    6040,1820,6320,1780
    5900,1840,6040,1820
    6640,1700,6880,1640
    6320,1780,6640,1700
    5840,2040,5900,1840
    5840,2040,5840,2220
    5840,2220,5840,2320
    5840,2460,5840,2320
    5840,2560,5840,2460
    5840,2560,5960,2620
    5960,2620,6200,2620
    6200,2620,6380,2600
    6380,2600,6600,2580
    6600,2580,6800,2600
    6800,2600,7040,2580
    7040,2580,7280,2580
    7280,2580,7480,2560
    7760,2540,7980,2520
    7980,2520,8160,2500
    7480,2560,7760,2540
    8160,2500,8160,2420
    8160,2420,8160,2320
    8160,2180,8160,2320
    7980,2160,8160,2180
    7800,2180,7980,2160
    7600,2200,7800,2180
    7400,2200,7600,2200
    6960,2200,7200,2200
    7200,2200,7400,2200
    6720,2200,6960,2200
    6540,2180,6720,2200
    6320,2200,6540,2180
    6240,2160,6320,2200
    6240,2160,6240,2040
    6240,2040,6240,1940
    6240,1940,6440,1940
    6440,1940,6720,1940
    6720,1940,6940,1920
    7520,1920,7760,1920
    6940,1920,7280,1920
    7280,1920,7520,1920
    7760,1920,8100,1900
    8100,1900,8420,1900
    8420,1900,8460,1940
    8460,2120,8460,1940
    8460,2280,8460,2120
    8460,2280,8560,2420
    8560,2420,8660,2380
    8660,2380,8800,2340
    8800,2340,8840,2400
    8840,2520,8840,2400
    8800,2620,8840,2520
    8800,2740,8800,2620
    8800,2860,8800,2740
    8800,2940,8800,2860
    8760,2980,8800,2940
    8660,2980,8760,2980
    8620,2960,8660,2980
    8560,2880,8620,2960
    8560,2880,8560,2780
    8500,2740,8560,2780
    8420,2760,8500,2740
    8420,2840,8420,2760
    8420,2840,8420,2940
    8420,3040,8420,2940
    8420,3160,8420,3040
    8420,3280,8420,3380
    8420,3280,8420,3160
    8420,3380,8620,3460
    8620,3460,8760,3460
    8760,3460,8840,3400
    8840,3400,8960,3400
    8960,3400,9000,3500
    9000,3700,9000,3500
    9000,3900,9000,3700
    9000,4080,9000,3900
    9000,4280,9000,4080
    9000,4500,9000,4280
    9000,4620,9000,4500
    9000,4780,9000,4620
    9000,4780,9000,4960
    9000,5120,9000,4960
    9000,5120,9000,5300
    8960,5460,9000,5300
    8920,5620,8960,5460
    8920,5620,8920,5800
    8920,5800,8920,5960
    8920,5960,8920,6120
    8920,6120,8960,6300
    8960,6300,8960,6480
    8960,6660,8960,6480
    8960,6860,8960,6660
    8960,7040,8960,6860
    8920,7420,8920,7220
    8920,7420,8960,7620
    8960,7620,8960,7800
    8960,7800,8960,8000
    8960,8000,8960,8180
    8960,8180,8960,8380
    8960,8580,8960,8380
    8920,8800,8960,8580
    8880,9000,8920,8800
    8840,9180,8880,9000
    8800,9220,8840,9180
    8800,9220,8840,9340
    8760,9380,8840,9340
    8560,9340,8760,9380
    8360,9360,8560,9340
    8160,9360,8360,9360
    8040,9340,8160,9360
    7860,9360,8040,9340
    7680,9360,7860,9360
    7520,9360,7680,9360
    7420,9260,7520,9360
    7400,9080,7420,9260
    7400,9080,7420,8860
    7420,8860,7440,8720
    7440,8720,7480,8660
    7480,8660,7520,8540
    7520,8540,7600,8460
    7600,8460,7800,8480
    7800,8480,8040,8480
    8040,8480,8280,8480
    8280,8480,8500,8460
    8500,8460,8620,8440
    8620,8440,8660,8340
    8660,8340,8660,8220
    8660,8220,8700,8080
    8700,8080,8700,7920
    8700,7920,8700,7760
    8700,7760,8700,7620
    8700,7480,8700,7620
    8700,7480,8700,7320
    8700,7160,8700,7320
    8920,7220,8960,7040
    8660,7040,8700,7160
    8660,7040,8700,6880
    8660,6700,8700,6880
    8660,6700,8700,6580
    8700,6460,8700,6580
    8700,6460,8700,6320
    8700,6160,8700,6320
    8700,6160,8760,6020
    8760,6020,8760,5860
    8760,5860,8760,5700
    8760,5700,8760,5540
    8760,5540,8760,5360
    8760,5360,8760,5180
    8760,5000,8760,5180
    8700,4820,8760,5000
    8560,4740,8700,4820
    8420,4700,8560,4740
    8280,4700,8420,4700
    8100,4700,8280,4700
    7980,4700,8100,4700
    7820,4740,7980,4700
    7800,4920,7820,4740
    7800,4920,7900,4960
    7900,4960,8060,4980
    8060,4980,8220,5000
    8220,5000,8420,5040
    8420,5040,8460,5120
    8460,5180,8460,5120
    8360,5200,8460,5180
    8360,5280,8360,5200
    8160,5300,8360,5280
    8040,5260,8160,5300
    7860,5220,8040,5260
    7720,5160,7860,5220
    7640,5120,7720,5160
    7480,5120,7640,5120
    7240,5120,7480,5120
    7000,5120,7240,5120
    6800,5160,7000,5120
    6640,5220,6800,5160
    6600,5360,6640,5220
    6600,5460,6600,5360
    6480,5520,6600,5460
    6240,5540,6480,5520
    5980,5540,6240,5540
    5740,5540,5980,5540
    5500,5520,5740,5540
    5400,5520,5500,5520
    5280,5540,5400,5520
    5080,5540,5280,5540
    4940,5540,5080,5540
    4760,5540,4940,5540
    4600,5540,4760,5540
    4440,5560,4600,5540
    4040,5580,4120,5520
    4260,5540,4440,5560
    4120,5520,4260,5540
    4020,5720,4040,5580
    4020,5840,4020,5720
    4020,5840,4080,5940
    4080,5940,4120,6040
    4120,6040,4200,6080
    4200,6080,4340,6080
    4340,6080,4500,6060
    4500,6060,4700,6060
    4700,6060,4880,6060
    4880,6060,5080,6060
    5080,6060,5280,6080
    5280,6080,5440,6100
    5440,6100,5660,6100
    5660,6100,5900,6080
    5900,6080,6120,6080
    6120,6080,6360,6080
    6360,6080,6480,6100
    6480,6100,6540,6060
    6540,6060,6720,6060
    6720,6060,6940,6060
    6940,6060,7140,6060
    7400,6060,7600,6060
    7140,6060,7400,6060
    7600,6060,7800,6060
    7800,6060,7860,6080
    7860,6080,8060,6080
    8060,6080,8220,6080
    8220,6080,8320,6140
    8320,6140,8360,6300
    8320,6460,8360,6300
    8320,6620,8320,6460
    8320,6800,8320,6620
    8320,6960,8320,6800
    8320,6960,8360,7120
    8320,7280,8360,7120
    8320,7440,8320,7280
    8320,7600,8320,7440
    8100,7580,8220,7600
    8220,7600,8320,7600
    7900,7560,8100,7580
    7680,7560,7900,7560
    7480,7580,7680,7560
    7280,7580,7480,7580
    7080,7580,7280,7580
    7000,7600,7080,7580
    6880,7600,7000,7600
    6800,7580,6880,7600
    6640,7580,6800,7580
    6540,7580,6640,7580
    6380,7600,6540,7580
    6280,7620,6380,7600
    6240,7700,6280,7620
    6240,7700,6240,7800
    6240,7840,6240,7800
    6080,7840,6240,7840
    5960,7820,6080,7840
    5660,7840,5800,7840
    5500,7800,5660,7840
    5440,7700,5500,7800
    5800,7840,5960,7820
    5440,7540,5440,7700
    5440,7440,5440,7540
    5440,7320,5440,7440
    5400,7320,5440,7320
    5340,7400,5400,7320
    5340,7400,5340,7500
    5340,7600,5340,7500
    5340,7600,5340,7720
    5340,7720,5340,7860
    5340,7860,5340,7960
    5340,7960,5440,8020
    5440,8020,5560,8020
    5560,8020,5720,8040
    5720,8040,5900,8060
    5900,8060,6080,8060
    6080,8060,6240,8060
    6720,8040,6840,8060
    6240,8060,6480,8040
    6480,8040,6720,8040
    6840,8060,6940,8060
    6940,8060,7080,8120
    7080,8120,7140,8180
    7140,8460,7140,8320
    7140,8620,7140,8460
    7140,8620,7140,8740
    7140,8860,7140,8740
    7140,8960,7140,8860
    7140,8960,7200,9080
    7140,9200,7200,9080
    7140,9200,7200,9320
    7200,9320,7200,9460
    7200,9760,7200,9900
    7200,9620,7200,9460
    7200,9620,7200,9760
    7200,9900,7200,10060
    7200,10220,7200,10060
    7200,10360,7200,10220
    7140,10400,7200,10360
    6880,10400,7140,10400
    6640,10360,6880,10400
    6420,10360,6640,10360
    6160,10380,6420,10360
    5940,10340,6160,10380
    5720,10320,5940,10340
    5500,10340,5720,10320
    5280,10300,5500,10340
    5080,10300,5280,10300
    4840,10280,5080,10300
    4700,10280,4840,10280
    4540,10280,4700,10280
    4360,10280,4540,10280
    4200,10300,4360,10280
    4040,10380,4200,10300
    4020,10500,4040,10380
    3980,10640,4020,10500
    3980,10640,3980,10760
    3980,10760,4020,10920
    4020,10920,4080,11000
    4080,11000,4340,11020
    4340,11020,4600,11060
    4600,11060,4840,11040
    4840,11040,4880,10960
    4880,10740,4880,10960
    4880,10740,4880,10600
    4880,10600,5080,10560
    5080,10560,5340,10620
    5340,10620,5660,10620
    5660,10620,6040,10600
    6040,10600,6120,10620
    6120,10620,6240,10720
    6240,10720,6420,10740
    6420,10740,6640,10760
    6640,10760,6880,10780
    7140,10780,7400,10780
    6880,10780,7140,10780
    7400,10780,7680,10780
    7680,10780,8100,10760
    8100,10760,8460,10740
    8460,10740,8700,10760
    8800,10840,8800,10980
    8700,10760,8800,10840
    8760,11200,8800,10980
    8760,11200,8760,11380
    8760,11380,8800,11560
    8760,11680,8800,11560
    8760,11760,8760,11680
    8760,11760,8760,11920
    8760,11920,8800,12080
    8800,12200,8800,12080
    8700,12240,8800,12200
    8560,12220,8700,12240
    8360,12220,8560,12220
    8160,12240,8360,12220
    7720,12220,7980,12220
    7980,12220,8160,12240
    7400,12200,7720,12220
    7200,12180,7400,12200
    7000,12160,7200,12180
    6800,12160,7000,12160
    6280,12140,6380,12180
    6120,12180,6280,12140
    6540,12180,6800,12160
    6380,12180,6540,12180
    5900,12200,6120,12180
    5620,12180,5900,12200
    5340,12120,5620,12180
    5140,12100,5340,12120
    4980,12120,5140,12100
    4840,12120,4980,12120
    4700,12200,4840,12120
    4700,12380,4700,12200
    4740,12480,4940,12520
    4700,12380,4740,12480
    4940,12520,5160,12560
    5160,12560,5340,12600
    5340,12600,5400,12600
    5400,12600,5500,12600
    5500,12600,5620,12600
    5620,12600,5720,12560
    5720,12560,5800,12440
    5800,12440,5900,12380
    5900,12380,6120,12420
    6120,12420,6380,12440
    6380,12440,6600,12460
    6720,12460,6840,12520
    6840,12520,6960,12520
    6600,12460,6720,12460
    6960,12520,7040,12500
    7040,12500,7140,12440
    7200,12440,7360,12500
    7360,12500,7600,12560
    7600,12560,7860,12600
    7860,12600,8060,12500
    8100,12500,8200,12340
    8200,12340,8360,12360
    8360,12360,8560,12400
    8560,12400,8660,12420
    8660,12420,8840,12400
    8840,12400,9000,12360
    9000,12360,9000,12360
    2900,4400,2900,4280
    900,7320,1000,7220
    2640,13040,2900,12920
    2900,12920,3160,12840
    3480,12760,3780,12620
    3780,12620,4020,12460
    4300,12360,4440,12260
    4020,12460,4300,12360
    3160,12840,3480,12760
    4440,12080,4440,12260
    4440,12080,4440,11880
    4440,11880,4440,11720
    4440,11720,4600,11720
    4600,11720,4760,11740
    4760,11740,4980,11760
    4980,11760,5160,11760
    5160,11760,5340,11780
    6000,11860,6120,11820
    5340,11780,5620,11820
    5620,11820,6000,11860
    6120,11820,6360,11820
    6360,11820,6640,11860
    6940,11920,7240,11940
    7240,11940,7520,11960
    7520,11960,7860,11960
    7860,11960,8100,11920
    8100,11920,8420,11940
    8420,11940,8460,11960
    8460,11960,8500,11860
    8460,11760,8500,11860
    8320,11720,8460,11760
    8160,11720,8320,11720
    7940,11720,8160,11720
    7720,11700,7940,11720
    7520,11680,7720,11700
    7320,11680,7520,11680
    7200,11620,7320,11680
    7200,11620,7200,11500
    7200,11500,7280,11440
    7280,11440,7420,11440
    7420,11440,7600,11440
    7600,11440,7980,11460
    7980,11460,8160,11460
    8160,11460,8360,11460
    8360,11460,8460,11400
    8420,11060,8500,11200
    8280,11040,8420,11060
    8100,11060,8280,11040
    8460,11400,8500,11200
    7800,11060,8100,11060
    7520,11060,7800,11060
    7240,11060,7520,11060
    6940,11040,7240,11060
    6640,11000,6940,11040
    6420,10980,6640,11000
    6360,11060,6420,10980
    6360,11180,6360,11060
    6200,11280,6360,11180
    5960,11300,6200,11280
    5720,11280,5960,11300
    5500,11280,5720,11280
    4940,11300,5200,11280
    4660,11260,4940,11300
    4440,11280,4660,11260
    4260,11280,4440,11280
    4220,11220,4260,11280
    4080,11280,4220,11220
    3980,11420,4080,11280
    3980,11420,4040,11620
    4040,11620,4040,11820
    3980,11960,4040,11820
    3840,12000,3980,11960
    3720,11940,3840,12000
    3680,11800,3720,11940
    3680,11580,3680,11800
    3680,11360,3680,11580
    3680,11360,3680,11260
    3680,11080,3680,11260
    3680,11080,3680,10880
    3680,10700,3680,10880
    3680,10700,3680,10620
    3680,10480,3680,10620
    3680,10480,3680,10300
    3680,10300,3680,10100
    3680,10100,3680,9940
    3680,9940,3720,9860
    3720,9860,3920,9900
    3920,9900,4220,9880
    4980,9940,5340,9960
    4220,9880,4540,9900
    4540,9900,4980,9940
    5340,9960,5620,9960
    5620,9960,5900,9960
    5900,9960,6160,10000
    6160,10000,6480,10000
    6480,10000,6720,10000
    6720,10000,6880,9860
    6880,9860,6880,9520
    6880,9520,6940,9340
    6940,9120,6940,9340
    6940,9120,6940,8920
    6940,8700,6940,8920
    6880,8500,6940,8700
    6880,8320,6880,8500
    7140,8320,7140,8180
    6760,8260,6880,8320
    6540,8240,6760,8260
    6420,8180,6540,8240
    6280,8240,6420,8180
    6160,8300,6280,8240
    6120,8400,6160,8300
    6080,8520,6120,8400
    5840,8480,6080,8520
    5620,8500,5840,8480
    5500,8500,5620,8500
    5340,8560,5500,8500
    5160,8540,5340,8560
    4620,8520,4880,8520
    4360,8480,4620,8520
    4880,8520,5160,8540
    4140,8440,4360,8480
    3920,8460,4140,8440
    3720,8380,3920,8460
    3680,8160,3720,8380
    3680,8160,3720,7940
    3720,7720,3720,7940
    3680,7580,3720,7720
    3680,7580,3720,7440
    3720,7440,3720,7300
    3720,7160,3720,7300
    3720,7160,3720,7020
    3720,7020,3780,6900
    3780,6900,4080,6940
    4080,6940,4340,6980
    4340,6980,4600,6980
    4600,6980,4880,6980
    4880,6980,5160,6980
    5160,6980,5400,7000
    5400,7000,5560,7020
    5560,7020,5660,7080
    5660,7080,5660,7280
    5660,7280,5660,7440
    5660,7440,5740,7520
    5740,7520,5740,7600
    5740,7600,5900,7600
    5900,7600,6040,7540
    6040,7540,6040,7320
    6040,7320,6120,7200
    6120,7200,6120,7040
    6120,7040,6240,7000
    6240,7000,6480,7060
    6480,7060,6800,7060
    6800,7060,7080,7080
    7080,7080,7320,7100
    7940,7100,7980,6920
    7860,6860,7980,6920
    7640,6860,7860,6860
    7400,6840,7640,6860
    7320,7100,7560,7120
    7560,7120,7760,7120
    7760,7120,7940,7100
    7200,6820,7400,6840
    7040,6820,7200,6820
    6600,6840,6840,6840
    6380,6800,6600,6840
    6120,6800,6380,6800
    5900,6840,6120,6800
    5620,6820,5900,6840
    5400,6800,5620,6820
    5140,6800,5400,6800
    4880,6780,5140,6800
    4600,6760,4880,6780
    4340,6760,4600,6760
    4080,6760,4340,6760
    3840,6740,4080,6760
    3680,6720,3840,6740
    3680,6720,3680,6560
    3680,6560,3720,6400
    3720,6400,3720,6200
    3720,6200,3780,6000
    3720,5780,3780,6000
    3720,5580,3720,5780
    3720,5360,3720,5580
    3720,5360,3840,5240
    3840,5240,4200,5260
    4200,5260,4600,5280
    4600,5280,4880,5280
    4880,5280,5140,5200
    5140,5200,5220,5100
    5220,5100,5280,4900
    5280,4900,5340,4840
    5340,4840,5720,4880
    6120,4880,6480,4860
    6880,4840,7200,4860
    6480,4860,6880,4840
    7200,4860,7320,4860
    7320,4860,7360,4740
    7360,4600,7440,4520
    7360,4600,7360,4740
    7440,4520,7640,4520
    7640,4520,7800,4480
    7800,4480,7800,4280
    7800,4280,7800,4040
    7800,4040,7800,3780
    7800,3560,7800,3780
    7800,3560,7860,3440
    7860,3440,8060,3460
    8060,3460,8160,3340
    8160,3340,8160,3140
    8160,3140,8160,2960
    8000,2900,8160,2960
    7860,2900,8000,2900
    7640,2940,7860,2900
    7400,2980,7640,2940
    7100,2980,7400,2980
    6840,3000,7100,2980
    5620,2980,5840,2980
    5840,2980,6500,3000
    6500,3000,6840,3000
    5560,2780,5620,2980
    5560,2780,5560,2580
    5560,2580,5560,2380
    5560,2140,5560,2380
    5560,2140,5560,1900
    5560,1900,5620,1660
    5620,1660,5660,1460
    5660,1460,5660,1300
    5500,1260,5660,1300
    5340,1260,5500,1260
    4600,1220,4840,1240
    4440,1220,4600,1220
    4440,1080,4440,1220
    4440,1080,4600,1020
    5080,1260,5340,1260
    4840,1240,5080,1260
    4600,1020,4940,1020
    4940,1020,5220,1020
    5220,1020,5560,960
    5560,960,5660,860
    5660,740,5660,860
    5280,740,5660,740
    4940,780,5280,740
    4660,760,4940,780
    4500,700,4660,760
    4500,520,4500,700
    4500,520,4700,460
    4700,460,5080,440
    5440,420,5740,420
    5080,440,5440,420
    5740,420,5840,360
    5800,280,5840,360
    5560,280,5800,280
    4980,300,5280,320
    4360,320,4660,300
    4200,360,4360,320
    5280,320,5560,280
    4660,300,4980,300
    4140,480,4200,360
    4140,480,4140,640
    4140,640,4200,780
    4200,780,4200,980
    4200,980,4220,1180
    4220,1400,4220,1180
    4220,1400,4260,1540
    4260,1540,4500,1540
    4500,1540,4700,1520
    4700,1520,4980,1540
    5280,1560,5400,1560
    4980,1540,5280,1560
    5400,1560,5400,1700
    5400,1780,5400,1700
    5340,1900,5400,1780
    5340,2020,5340,1900
    5340,2220,5340,2020
    5340,2220,5340,2420
    5340,2420,5340,2520
    5080,2600,5220,2580
    5220,2580,5340,2520
    4900,2580,5080,2600
    4700,2540,4900,2580
    4500,2540,4700,2540
    4220,2580,4340,2540
    4200,2700,4220,2580
    4340,2540,4500,2540
    3980,2740,4200,2700
    3840,2740,3980,2740
    3780,2640,3840,2740
    3780,2640,3780,2460
    3780,2280,3780,2460
    3620,2020,3780,2100
    3780,2280,3780,2100
    3360,2040,3620,2020
    3080,2040,3360,2040
    2840,2020,3080,2040
    2740,1940,2840,2020
    2740,1940,2800,1800
    2800,1640,2800,1800
    2800,1640,2800,1460
    2800,1300,2800,1460
    2700,1180,2800,1300
    2480,1140,2700,1180
    1580,1200,1720,1200
    2240,1180,2480,1140
    1960,1180,2240,1180
    1720,1200,1960,1180
    1500,1320,1580,1200
    1500,1440,1500,1320
    1500,1440,1760,1480
    1760,1480,1940,1480
    1940,1480,2140,1500
    2140,1500,2320,1520
    2400,1560,2400,1700
    2280,1820,2380,1780
    2320,1520,2400,1560
    2380,1780,2400,1700
    2080,1840,2280,1820
    1720,1820,2080,1840
    1420,1800,1720,1820
    1280,1800,1420,1800
    1240,1720,1280,1800
    1240,1720,1240,1600
    1240,1600,1280,1480
    1280,1340,1280,1480
    1180,1280,1280,1340
    1000,1280,1180,1280
    760,1280,1000,1280
    360,1240,540,1260
    180,1220,360,1240
    540,1260,760,1280
    180,1080,180,1220
    180,1080,180,1000
    180,1000,360,940
    360,940,540,960
    540,960,820,980
    1100,980,1200,920
    820,980,1100,980
    6640,11860,6940,11920
    5200,11280,5500,11280
    4120,7330,4120,7230
    4120,7230,4660,7250
    4660,7250,4940,7250
    4940,7250,5050,7340
    5010,7400,5050,7340
    4680,7380,5010,7400
    4380,7370,4680,7380
    4120,7330,4360,7370
    4120,7670,4120,7760
    4120,7670,4280,7650
    4280,7650,4540,7660
    4550,7660,4820,7680
    4820,7680,4900,7730
    4880,7800,4900,7730
    4620,7820,4880,7800
    4360,7790,4620,7820
    4120,7760,4360,7790
    6840,6840,7040,6820
    5720,4880,6120,4880
    1200,920,1340,810
    1340,810,1520,790
    1520,790,1770,800
    2400,790,2600,750
    2600,750,2640,520
    2520,470,2640,520
    2140,470,2520,470
    1760,800,2090,800
    2080,800,2400,790
    1760,450,2140,470
    1420,450,1760,450
    1180,440,1420,450
    900,480,1180,440
    640,450,900,480
    360,440,620,450
    120,430,360,440
    0,520,120,430
    -20,780,0,520
    -20,780,-20,1020
    -20,1020,-20,1150
    -20,1150,0,1300
    0,1470,60,1530
    0,1300,0,1470
    60,1530,360,1530
    360,1530,660,1520
    660,1520,980,1520
    980,1520,1040,1520
    1040,1520,1070,1560
    1070,1770,1070,1560
    1070,1770,1100,2010
    1070,2230,1100,2010
    1070,2240,1180,2340
    1180,2340,1580,2340
    1580,2340,1940,2350
    1940,2350,2440,2350
    2440,2350,2560,2380
    2560,2380,2600,2540
    2810,2640,3140,2680
    2600,2540,2810,2640
    3140,2680,3230,2780
    3230,2780,3260,2970
    3230,3220,3260,2970
    3200,3470,3230,3220
    3200,3480,3210,3760
    3210,3760,3210,4040
    3200,4040,3230,4310
    3210,4530,3230,4310
    3210,4530,3230,4730
    3230,4960,3230,4730
    3230,4960,3260,5190
    3170,5330,3260,5190
    2920,5330,3170,5330
    2660,5360,2920,5330
    2420,5330,2660,5360
    2200,5280,2400,5330
    2020,5280,2200,5280
    1840,5260,2020,5280
    1660,5280,1840,5260
    1500,5300,1660,5280
    1360,5270,1500,5300
    1200,5290,1340,5270
    1070,5400,1200,5290
    1040,5630,1070,5400
    1000,5900,1040,5630
    980,6170,1000,5900
    980,6280,980,6170
    980,6540,980,6280
    980,6540,1040,6720
    1040,6720,1360,6730
    1360,6730,1760,6710
    2110,6720,2420,6730
    1760,6710,2110,6720
    2420,6730,2640,6720
    2640,6720,2970,6720
    2970,6720,3160,6700
    3160,6700,3240,6710
    3240,6710,3260,6890
    3260,7020,3260,6890
    3230,7180,3260,7020
    3230,7350,3230,7180
    3210,7510,3230,7350
    3210,7510,3210,7690
    3210,7870,3210,7690
    3210,7870,3210,7980
    3200,8120,3210,7980
    3200,8330,3200,8120
    3160,8520,3200,8330
    2460,11100,2480,11020
    2200,11180,2460,11100
    1260,11350,1600,11320
    600,11430,930,11400
    180,11340,620,11430
    1600,11320,1910,11280
    1910,11280,2200,11180
    923.0029599285435,11398.99893503157,1264.002959928544,11351.99893503157
    

    The Little Probe - Data - level_lava.txt link

    # ./samples/99_genre_platformer/the_little_probe/data/level_lava.txt
    100,10740,500,10780
    500,10780,960,10760
    960,10760,1340,10760
    1380,10760,1820,10780
    1820,10780,2240,10780
    2280,10780,2740,10740
    2740,10740,3000,10780
    3000,10780,3140,11020
    -520,8820,-480,9160
    -520,8480,-520,8820
    -520,8480,-480,8180
    -480,8180,-200,8120
    -200,8120,100,8220
    100,8220,420,8240
    420,8240,760,8260
    760,8260,1140,8280
    1140,8280,1500,8200
    1500,8200,1880,8240
    1880,8240,2240,8260
    2240,8260,2320,8480
    2320,8480,2380,8680
    2240,8860,2380,8680
    2240,9080,2240,8860
    2240,9080,2320,9260
    2320,9260,2480,9440
    2480,9440,2600,9640
    2480,9840,2600,9640
    2400,10020,2480,9840
    2240,10080,2400,10020
    1960,10080,2240,10080
    1720,10080,1960,10080
    1460,10080,1720,10080
    1180,10080,1420,10080
    900,10080,1180,10080
    640,10080,900,10080
    640,10080,640,9900
    60,10520,100,10740
    40,10240,60,10520
    40,10240,40,9960
    40,9960,40,9680
    40,9680,40,9360
    40,9360,60,9080
    60,9080,100,8860
    100,8860,460,9040
    460,9040,760,9220
    760,9220,1140,9220
    1140,9220,1720,9200
    -660,11580,-600,11420
    -660,11800,-660,11580
    -660,12000,-660,11800
    -660,12000,-600,12220
    -600,12220,-600,12440
    -600,12440,-600,12640
    -600,11240,-260,11280
    -260,11280,100,11240
    9000,12360,9020,12400
    9020,12620,9020,12400
    9020,12840,9020,12620
    9020,13060,9020,12840
    9020,13060,9020,13240
    9020,13240,9020,13420
    9020,13420,9020,13600
    9020,13600,9020,13780
    8880,13900,9020,13780
    8560,13800,8880,13900
    8220,13780,8560,13800
    7860,13760,8220,13780
    7640,13780,7860,13760
    7360,13800,7640,13780
    7100,13800,7360,13800
    6540,13760,6800,13780
    6800,13780,7100,13800
    6280,13760,6540,13760
    5760,13760,6280,13760
    5220,13780,5760,13760
    4700,13760,5220,13780
    4200,13740,4700,13760
    3680,13720,4200,13740
    3140,13700,3680,13720
    2600,13680,3140,13700
    2040,13940,2600,13680
    1640,13940,2040,13940
    1200,13960,1640,13940
    840,14000,1200,13960
    300,13960,840,14000
    -200,13900,300,13960
    -600,12840,-600,12640
    -600,13140,-600,12840
    -600,13140,-600,13420
    -600,13700,-600,13420
    -600,13700,-600,13820
    -600,13820,-200,13900
    -600,11240,-560,11000
    -560,11000,-480,10840
    -520,10660,-480,10840
    -520,10660,-520,10480
    -520,10480,-520,10300
    -520,10260,-480,10080
    -480,9880,-440,10060
    -520,9680,-480,9880
    -520,9680,-480,9400
    -480,9400,-480,9160
    1820,9880,2140,9800
    1540,9880,1820,9880
    1200,9920,1500,9880
    900,9880,1200,9920
    640,9900,840,9880
    2380,8760,2800,8760
    2800,8760,2840,8660
    2840,8660,2840,8420
    2840,8160,2840,8420
    2800,7900,2840,8160
    2800,7900,2800,7720
    2800,7540,2800,7720
    2800,7540,2800,7360
    2700,7220,2800,7360
    2400,7220,2700,7220
    2080,7240,2400,7220
    1760,7320,2080,7240
    1380,7360,1720,7320
    1040,7400,1340,7360
    640,7400,1000,7420
    300,7380,640,7400
    0,7300,240,7380
    -300,7180,-60,7300
    -380,6860,-360,7180
    -380,6880,-360,6700
    -360,6700,-260,6540
    -260,6540,0,6520
    0,6520,240,6640
    240,6640,460,6640
    460,6640,500,6480
    500,6260,500,6480
    460,6060,500,6260
    460,5860,460,6060
    460,5860,500,5640
    500,5640,540,5440
    540,5440,580,5220
    580,5220,580,5000
    580,4960,580,4740
    580,4740,960,4700
    960,4700,1140,4760
    1140,4760,1420,4740
    1420,4740,1720,4700
    1720,4700,2000,4740
    2000,4740,2380,4760
    2380,4760,2700,4800
    1720,4600,1760,4300
    1760,4300,2200,4340
    2200,4340,2560,4340
    2560,4340,2740,4340
    2160,12580,2440,12400
    1820,12840,2160,12580
    1500,13080,1820,12840
    1140,13340,1500,13080
    1140,13340,1580,13220
    2110,13080,2520,13000
    2520,13000,2900,12800
    1580,13220,2110,13080
    2900,12800,3200,12680
    3200,12680,3440,12640
    3440,12640,3720,12460
    3720,12460,4040,12320
    4040,12320,4360,12200
    4360,11940,4380,12180
    4360,11700,4360,11940
    4360,11700,4540,11500
    4540,11500,4880,11540
    6000,11660,6280,11640
    5440,11600,5720,11610
    5720,11610,6000,11660
    6280,11640,6760,11720
    6760,11720,7060,11780
    7060,11780,7360,11810
    7360,11810,7640,11840
    7640,11840,8000,11830
    8000,11830,8320,11850
    8320,11850,8390,11800
    8330,11760,8390,11800
    8160,11760,8330,11760
    7910,11750,8160,11760
    7660,11740,7900,11750
    7400,11730,7660,11740
    7160,11680,7400,11730
    7080,11570,7160,11680
    7080,11570,7100,11350
    7100,11350,7440,11280
    7440,11280,7940,11280
    7960,11280,8360,11280
    5840,11540,6650,11170
    4880,11540,5440,11600
    3410,11830,3420,11300
    3410,11260,3520,10920
    3520,10590,3520,10920
    3520,10590,3540,10260
    3520,9900,3540,10240
    3520,9900,3640,9590
    3640,9570,4120,9590
    4140,9590,4600,9680
    4620,9680,5030,9730
    5120,9750,5520,9800
    5620,9820,6080,9800
    6130,9810,6580,9820
    6640,9820,6800,9700
    6780,9400,6800,9700
    6780,9400,6840,9140
    6820,8860,6840,9120
    6780,8600,6820,8830
    6720,8350,6780,8570
    6480,8340,6720,8320
    6260,8400,6480,8340
    6050,8580,6240,8400
    5760,8630,6040,8590
    5520,8690,5740,8630
    5120,8690,5450,8700
    4570,8670,5080,8690
    4020,8610,4540,8670
    3540,8480,4020,8610
    3520,8230,3520,8480
    3520,7930,3520,8230
    3520,7930,3540,7630
    3480,7320,3540,7610
    3480,7280,3500,7010
    3500,6980,3680,6850
    3680,6850,4220,6840
    4230,6840,4760,6850
    4780,6850,5310,6860
    5310,6860,5720,6940
    5720,6940,5880,7250
    5880,7250,5900,7520
    100,11240,440,11300
    440,11300,760,11330
    1480,11280,1840,11230
    2200,11130,2360,11090
    1840,11230,2200,11130
    

    Genre Rpg Narrative link

    Choose Your Own Adventure - decision.rb link

    # ./samples/99_genre_rpg_narrative/choose_your_own_adventure/app/decision.rb
    # Hey there! Welcome to Four Decisions. Here is how you
    # create your decision tree. Remove =being and =end from the text to
    # enable the game (just save the file). Change stuff and see what happens!
    
    def game
      {
        starting_decision: :stormy_night,
        decisions: {
          stormy_night: {
            description: 'It was a dark and stormy night. (storyline located in decision.rb)',
            option_one: {
              description: 'Go to sleep.',
              decision: :nap
            },
            option_two: {
              description: 'Watch a movie.',
              decision: :movie
            },
            option_three: {
              description: 'Go outside.',
              decision: :go_outside
            },
            option_four: {
              description: 'Get a snack.',
              decision: :get_a_snack
            }
          },
          nap: {
            description: 'You took a nap. The end.',
            option_one: {
              description: 'Start over.',
              decision: :stormy_night
            }
          }
        }
      }
    end
    
    

    Choose Your Own Adventure - main.rb link

    # ./samples/99_genre_rpg_narrative/choose_your_own_adventure/app/main.rb
    =begin
    
     Reminders:
    
     - Hashes: Collection of unique keys and their corresponding values. The values can be found
       using their keys.
    
       In this sample app, the decisions needed for the game are stored in a hash. In fact, the
       decision.rb file contains hashes inside of other hashes!
    
       Each option is a key in the first hash, but also contains a hash (description and
       decision being its keys) as its value.
       Go into the decision.rb file and take a look before diving into the code below.
    
     - 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.keyboard.key_down.KEY: Determines if a key is in the down state or pressed down.
       For more information about the keyboard, go to mygame/documentation/06-keyboard.md.
    
     - 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
    
    # This sample app provides users with a story and multiple decisions that they can choose to make.
    # Users can make a decision using their keyboard, and the story will move forward based on user choices.
    
    # The decisions available to users are stored in the decision.rb file.
    # We must have access to it for the game to function properly.
    GAME_FILE = 'app/decision.rb' # found in app folder
    
    require GAME_FILE # require used to load another file, import class/method definitions
    
    # Instructions are given using labels to users if they have not yet set up their story in the decision.rb file.
    # Otherwise, the game is run.
    def tick args
      if !args.state.loaded && !respond_to?(:game) # if game is not loaded and not responding to game symbol's method
        args.labels << [640, 370, 'Hey there! Welcome to Four Decisions.', 0, 1] # a welcome label is shown
        args.labels << [640, 340, 'Go to the file called decision.rb and tell me your story.', 0, 1]
      elsif respond_to?(:game) # otherwise, if responds to game
        args.state.loaded = true
        tick_game args # calls tick_game method, runs game
      end
    
      if Kernel.tick_count.mod_zero? 60 # update every 60 frames
        t = args.gtk.ffi_file.mtime GAME_FILE # mtime returns modification time for named file
        if t != args.state.mtime
          args.state.mtime = t
          require GAME_FILE # require used to load file
          args.state.game_definition = nil # game definition and decision are empty
          args.state.decision_id = nil
        end
      end
    end
    
    # Runs methods needed for game to function properly
    # Creates a rectangular border around the screen
    def tick_game args
      defaults args
      args.borders << args.grid.rect
      render_decision args
      process_inputs args
    end
    
    # Sets default values and uses decision.rb file to define game and decision_id
    # variable using the starting decision
    def defaults args
      args.state.game_definition ||= game
      args.state.decision_id ||= args.state.game_definition[:starting_decision]
    end
    
    # Outputs the possible decision descriptions the user can choose onto the screen
    # as well as what key to press on their keyboard to make their decision
    def render_decision args
      decision = current_decision args
      # text is either the value of decision's description key or warning that no description exists
      args.labels << [640, 360, decision[:description] || "No definition found for #{args.state.decision_id}. Please update decision.rb.", 0, 1] # uses string interpolation
    
      # All decisions are stored in a hash
      # The descriptions output onto the screen are the values for the description keys of the hash.
      if decision[:option_one]
        args.labels << [10, 360, decision[:option_one][:description], 0, 0] # option one's description label
        args.labels << [10, 335, "(Press 'left' on the keyboard to select this decision)", -5, 0] # label of what key to press to select the decision
      end
    
      if decision[:option_two]
        args.labels << [1270, 360, decision[:option_two][:description], 0, 2] # option two's description
        args.labels << [1270, 335, "(Press 'right' on the keyboard to select this decision)", -5, 2]
      end
    
      if decision[:option_three]
        args.labels << [640, 45, decision[:option_three][:description], 0, 1] # option three's description
        args.labels << [640, 20, "(Press 'down' on the keyboard to select this decision)", -5, 1]
      end
    
      if decision[:option_four]
        args.labels << [640, 700, decision[:option_four][:description], 0, 1] # option four's description
        args.labels << [640, 675, "(Press 'up' on the keyboard to select this decision)", -5, 1]
      end
    end
    
    # Uses keyboard input from the user to make a decision
    # Assigns the decision as the value of the decision_id variable
    def process_inputs args
      decision = current_decision args # calls current_decision method
    
      if args.keyboard.key_down.left! && decision[:option_one] # if left key pressed and option one exists
        args.state.decision_id = decision[:option_one][:decision] # value of option one's decision hash key is set to decision_id
      end
    
      if args.keyboard.key_down.right! && decision[:option_two] # if right key pressed and option two exists
        args.state.decision_id = decision[:option_two][:decision] # value of option two's decision hash key is set to decision_id
      end
    
      if args.keyboard.key_down.down! && decision[:option_three] # if down key pressed and option three exists
        args.state.decision_id = decision[:option_three][:decision] # value of option three's decision hash key is set to decision_id
      end
    
      if args.keyboard.key_down.up! && decision[:option_four] # if up key pressed and option four exists
        args.state.decision_id = decision[:option_four][:decision] # value of option four's decision hash key is set to decision_id
      end
    end
    
    # Uses decision_id's value to keep track of current decision being made
    def current_decision args
      args.state.game_definition[:decisions][args.state.decision_id] || {} # either has value or is empty
    end
    
    # Resets the game.
    $gtk.reset
    
    

    Return Of Serenity - lowrez_simulator.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/lowrez_simulator.rb
    ###################################################################################
    # YOU CAN PLAY AROUND WITH THE CODE BELOW, BUT USE CAUTION AS THIS IS WHAT EMULATES
    # THE 64x64 CANVAS.
    ###################################################################################
    
    TINY_RESOLUTION       = 64
    TINY_SCALE            = 720.fdiv(TINY_RESOLUTION + 5)
    CENTER_OFFSET         = 10
    EMULATED_FONT_SIZE    = 20
    EMULATED_FONT_X_ZERO  = 0
    EMULATED_FONT_Y_ZERO  = 46
    
    def tick args
      sprites = []
      labels = []
      borders = []
      solids = []
      mouse = emulate_lowrez_mouse args
      args.state.show_gridlines = false
      lowrez_tick args, sprites, labels, borders, solids, mouse
      render_gridlines_if_needed args
      render_mouse_crosshairs args, mouse
      emulate_lowrez_scene args, sprites, labels, borders, solids, mouse
    end
    
    def emulate_lowrez_mouse args
      args.state.new_entity_strict(:lowrez_mouse) do |m|
        m.x = args.mouse.x.idiv(TINY_SCALE) - CENTER_OFFSET.idiv(TINY_SCALE) - 1
        m.y = args.mouse.y.idiv(TINY_SCALE)
        if args.mouse.click
          m.click = [
            args.mouse.click.point.x.idiv(TINY_SCALE) - CENTER_OFFSET.idiv(TINY_SCALE) - 1,
            args.mouse.click.point.y.idiv(TINY_SCALE)
          ]
          m.down = m.click
        else
          m.click = nil
          m.down = nil
        end
    
        if args.mouse.up
          m.up = [
            args.mouse.up.point.x.idiv(TINY_SCALE) - CENTER_OFFSET.idiv(TINY_SCALE) - 1,
            args.mouse.up.point.y.idiv(TINY_SCALE)
          ]
        else
          m.up = nil
        end
      end
    end
    
    def render_mouse_crosshairs args, mouse
      return unless args.state.show_gridlines
      args.labels << [10, 25, "mouse: #{mouse.x} #{mouse.y}", 255, 255, 255]
    end
    
    def emulate_lowrez_scene args, sprites, labels, borders, solids, mouse
      args.render_target(:lowrez).solids  << [0, 0, 1280, 720]
      args.render_target(:lowrez).sprites << sprites
      args.render_target(:lowrez).borders << borders
      args.render_target(:lowrez).solids  << solids
      args.outputs.primitives << labels.map do |l|
        as_label = l.label
        l.text.each_char.each_with_index.map do |char, i|
          [CENTER_OFFSET + EMULATED_FONT_X_ZERO + (as_label.x * TINY_SCALE) + i * 5 * TINY_SCALE,
           EMULATED_FONT_Y_ZERO + (as_label.y * TINY_SCALE), char,
           EMULATED_FONT_SIZE, 0, as_label.r, as_label.g, as_label.b, as_label.a, 'fonts/dragonruby-gtk-4x4.ttf'].label
        end
      end
    
      args.sprites    << [CENTER_OFFSET, 0, 1280 * TINY_SCALE, 720 * TINY_SCALE, :lowrez]
    end
    
    def render_gridlines_if_needed args
      if args.state.show_gridlines && args.static_lines.length == 0
        args.static_lines << 65.map do |i|
          [
            [CENTER_OFFSET + i * TINY_SCALE + 1,  0,
             CENTER_OFFSET + i * TINY_SCALE + 1,  720,                128, 128, 128],
            [CENTER_OFFSET + i * TINY_SCALE,      0,
             CENTER_OFFSET + i * TINY_SCALE,      720,                128, 128, 128],
            [CENTER_OFFSET,                       0 + i * TINY_SCALE,
             CENTER_OFFSET + 720,                 0 + i * TINY_SCALE, 128, 128, 128],
            [CENTER_OFFSET,                       1 + i * TINY_SCALE,
             CENTER_OFFSET + 720,                 1 + i * TINY_SCALE, 128, 128, 128]
          ]
        end
      elsif !args.state.show_gridlines
        args.static_lines.clear
      end
    end
    
    

    Return Of Serenity - main.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/main.rb
    require 'app/require.rb'
    
    def defaults args
      args.outputs.background_color = [0, 0, 0]
      args.state.last_story_line_text ||= ""
      args.state.scene_history ||= []
      args.state.storyline_history ||= []
      args.state.word_delay ||= 8
      if Kernel.tick_count == 0
        args.gtk.stop_music
        args.outputs.sounds << 'sounds/static-loop.ogg'
      end
    
      if args.state.last_story_line_text
        lines = args.state
                    .last_story_line_text
                    .gsub("-", "")
                    .gsub("~", "")
                    .wrapped_lines(50)
    
        args.outputs.labels << lines.map_with_index { |l, i| [690, 200 - (i * 25), l, 1, 0, 255, 255, 255] }
      elsif args.state.storyline_history[-1]
        lines = args.state
                    .storyline_history[-1]
                    .gsub("-", "")
                    .gsub("~", "")
                    .wrapped_lines(50)
    
        args.outputs.labels << lines.map_with_index { |l, i| [690, 200 - (i * 25), l, 1, 0, 255, 255, 255] }
      end
    
      return if args.state.current_scene
      set_scene(args, day_one_beginning(args))
    end
    
    def inputs_move_player args
      if args.state.scene_changed_at.elapsed_time > 5
        if args.keyboard.down  || args.keyboard.s || args.keyboard.j
          args.state.player.y -= 0.25
        elsif args.keyboard.up || args.keyboard.w || args.keyboard.k
          args.state.player.y += 0.25
        end
    
        if args.keyboard.left     || args.keyboard.a  || args.keyboard.h
          args.state.player.x -= 0.25
        elsif args.keyboard.right || args.keyboard.d  || args.keyboard.l
          args.state.player.x += 0.25
        end
    
        args.state.player.y = 60 if args.state.player.y > 63
        args.state.player.y =  0 if args.state.player.y < -3
        args.state.player.x = 60 if args.state.player.x > 63
        args.state.player.x =  0 if args.state.player.x < -3
      end
    end
    
    def null_or_empty? ary
      return true unless ary
      return true if ary.length == 0
      return false
    end
    
    def calc_storyline_hotspot args
      hotspots = args.state.storylines.find_all do |hs|
        args.state.player.inside_rect?(hs.shift_rect(-2, 0))
      end
    
      if !null_or_empty?(hotspots) && !args.state.inside_storyline_hotspot
        _, _, _, _, storyline = hotspots.first
        queue_storyline_text(args, storyline)
        args.state.inside_storyline_hotspot = true
      elsif null_or_empty?(hotspots)
        args.state.inside_storyline_hotspot = false
    
        args.state.storyline_queue_empty_at ||= Kernel.tick_count
        args.state.is_storyline_dialog_active = false
        args.state.scene_storyline_queue.clear
      end
    end
    
    def calc_scenes args
      hotspots = args.state.scenes.find_all do |hs|
        args.state.player.inside_rect?(hs.shift_rect(-2, 0))
      end
    
      if !null_or_empty?(hotspots) && !args.state.inside_scene_hotspot
        _, _, _, _, scene_method_or_hash = hotspots.first
        if scene_method_or_hash.is_a? Symbol
          set_scene(args, send(scene_method_or_hash, args))
          args.state.last_hotspot_scene = scene_method_or_hash
          args.state.scene_history << scene_method_or_hash
        else
          set_scene(args, scene_method_or_hash)
        end
        args.state.inside_scene_hotspot = true
      elsif null_or_empty?(hotspots)
        args.state.inside_scene_hotspot = false
      end
    end
    
    def null_or_whitespace? word
      return true if !word
      return true if word.strip.length == 0
      return false
    end
    
    def calc_storyline_presentation args
      return unless Kernel.tick_count > args.state.next_storyline
      return unless args.state.scene_storyline_queue
      next_storyline = args.state.scene_storyline_queue.shift
      if null_or_whitespace? next_storyline
        args.state.storyline_queue_empty_at ||= Kernel.tick_count
        args.state.is_storyline_dialog_active = false
        return
      end
      args.state.storyline_to_show = next_storyline
      args.state.is_storyline_dialog_active = true
      args.state.storyline_queue_empty_at = nil
      if next_storyline.end_with?(".") || next_storyline.end_with?("!") || next_storyline.end_with?("?") || next_storyline.end_with?("\"")
        args.state.next_storyline += 60
      elsif next_storyline.end_with?(",")
        args.state.next_storyline += 50
      elsif next_storyline.end_with?(":")
        args.state.next_storyline += 60
      else
        default_word_delay = 13 + args.state.word_delay - 8
        if next_storyline.gsub("-", "").gsub("~", "").length <= 4
          default_word_delay = 11 + args.state.word_delay - 8
        end
        number_of_syllabals = next_storyline.length - next_storyline.gsub("-", "").length
        args.state.next_storyline += default_word_delay + number_of_syllabals * (args.state.word_delay + 1)
      end
    end
    
    def inputs_reload_current_scene args
      return
      if args.inputs.keyboard.key_down.r!
        reload_current_scene
      end
    end
    
    def inputs_dismiss_current_storyline args
      if args.inputs.keyboard.key_down.x!
        args.state.scene_storyline_queue.clear
      end
    end
    
    def inputs_restart_game args
      if args.inputs.keyboard.exclamation_point
        args.gtk.reset_state
      end
    end
    
    def inputs_change_word_delay args
      if args.inputs.keyboard.key_down.plus || args.inputs.keyboard.key_down.equal_sign
        args.state.word_delay -= 2
        if args.state.word_delay < 0
          args.state.word_delay = 0
          # queue_storyline_text args, "Text speed at MAXIMUM. Geez, how fast do you read?"
        else
          # queue_storyline_text args, "Text speed INCREASED."
        end
      end
    
      if args.inputs.keyboard.key_down.hyphen || args.inputs.keyboard.key_down.underscore
        args.state.word_delay += 2
        # queue_storyline_text args, "Text speed DECREASED."
      end
    end
    
    def multiple_lines args, x, y, texts, size = 0, minimum_alpha = nil
      texts.each_with_index.map do |t, i|
        [x, y - i * (25 + size * 2), t, size, 0, 255, 255, 255, adornments_alpha(args, 255, minimum_alpha)]
      end
    end
    
    def lowrez_tick args, lowrez_sprites, lowrez_labels, lowrez_borders, lowrez_solids, lowrez_mouse
      # args.state.show_gridlines = true
      defaults args
      render_current_scene args, lowrez_sprites, lowrez_labels, lowrez_solids
      render_controller args, lowrez_borders
      lowrez_solids << [0, 0, 64, 64, 0, 0, 0]
      calc_storyline_presentation args
      calc_scenes args
      calc_storyline_hotspot args
      inputs_move_player args
      inputs_print_mouse_rect args, lowrez_mouse
      inputs_reload_current_scene args
      inputs_dismiss_current_storyline args
      inputs_change_word_delay args
      inputs_restart_game args
    end
    
    def render_controller args, lowrez_borders
      args.state.up_button    = [85, 40, 15, 15, 255, 255, 255]
      args.state.down_button  = [85, 20, 15, 15, 255, 255, 255]
      args.state.left_button  = [65, 20, 15, 15, 255, 255, 255]
      args.state.right_button = [105, 20, 15, 15, 255, 255, 255]
      lowrez_borders << args.state.up_button
      lowrez_borders << args.state.down_button
      lowrez_borders << args.state.left_button
      lowrez_borders << args.state.right_button
    end
    
    def inputs_print_mouse_rect args, lowrez_mouse
      if lowrez_mouse.up
        args.state.mouse_held = false
      elsif lowrez_mouse.click
        mouse_rect = [lowrez_mouse.x, lowrez_mouse.y, 1, 1]
        if args.state.up_button.intersect_rect? mouse_rect
          args.state.player.y += 1
        end
    
        if args.state.down_button.intersect_rect? mouse_rect
          args.state.player.y -= 1
        end
    
        if args.state.left_button.intersect_rect? mouse_rect
          args.state.player.x -= 1
        end
    
        if args.state.right_button.intersect_rect? mouse_rect
          args.state.player.x += 1
        end
        args.state.mouse_held = true
      elsif args.state.mouse_held
        mouse_rect = [lowrez_mouse.x, lowrez_mouse.y, 1, 1]
        if args.state.up_button.intersect_rect? mouse_rect
          args.state.player.y += 0.25
        end
    
        if args.state.down_button.intersect_rect? mouse_rect
          args.state.player.y -= 0.25
        end
    
        if args.state.left_button.intersect_rect? mouse_rect
          args.state.player.x -= 0.25
        end
    
        if args.state.right_button.intersect_rect? mouse_rect
          args.state.player.x += 0.25
        end
      end
    
      if lowrez_mouse.click
        dx = lowrez_mouse.click.x - args.state.previous_mouse_click.x
        dy = lowrez_mouse.click.y - args.state.previous_mouse_click.y
        x, y, w, h = args.state.previous_mouse_click.x, args.state.previous_mouse_click.y, dx, dy
        puts "x #{lowrez_mouse.click.x}, y: #{lowrez_mouse.click.y}"
        if args.state.previous_mouse_click
    
          if dx < 0 && dx < 0
            x = x + w
            w = w.abs
            y = y + h
            h = h.abs
          end
    
          w += 1
          h += 1
    
          args.state.previous_mouse_click = nil
        else
          args.state.previous_mouse_click = lowrez_mouse.click
          square_x, square_y = lowrez_mouse.click
        end
      end
    end
    
    def try_centering! word
      word ||= ""
      just_word = word.gsub("-", "").gsub(",", "").gsub(".", "").gsub("'", "").gsub('""', "\"-\"")
      return word if just_word.strip.length == 0
      return word if just_word.include? "~"
      return "~#{word}" if just_word.length <= 2
      if just_word.length.mod_zero? 2
        center_index = just_word.length.idiv(2) - 1
      else
        center_index = (just_word.length - 1).idiv(2)
      end
      return "#{word[0..center_index - 1]}~#{word[center_index]}#{word[center_index + 1..-1]}"
    end
    
    def queue_storyline args, scene
      queue_storyline_text args, scene[:storyline]
    end
    
    def queue_storyline_text args, text
      args.state.last_story_line_text = text
      args.state.storyline_history << text if text
      words = (text || "").split(" ")
      words = words.map { |w| try_centering! w }
      args.state.scene_storyline_queue = words
      if args.state.scene_storyline_queue.length != 0
        args.state.scene_storyline_queue.unshift "~$--"
        args.state.storyline_to_show = "~."
      else
        args.state.storyline_to_show = ""
      end
      args.state.scene_storyline_queue << ""
      args.state.next_storyline = Kernel.tick_count
    end
    
    def set_scene args, scene
      args.state.current_scene = scene
      args.state.background = scene[:background] ||  'sprites/todo.png'
      args.state.scene_fade = scene[:fade] || 0
      args.state.scenes = (scene[:scenes] || []).reject { |s| !s }
      args.state.scene_render_override = scene[:render_override]
      args.state.storylines = (scene[:storylines] || []).reject { |s| !s }
      args.state.scene_changed_at = Kernel.tick_count
      if scene[:player]
        args.state.player = scene[:player]
      end
      args.state.inside_scene_hotspot = false
      args.state.inside_storyline_hotspot = false
      queue_storyline args, scene
    end
    
    def replay_storyline_rect
      [26, -1, 7, 4]
    end
    
    def labels_for_word word
      left_side_of_word = ""
      center_letter = ""
      right_side_of_word = ""
    
      if word[0] == "~"
        left_side_of_word = ""
        center_letter = word[1]
        right_side_of_word = word[2..-1]
      elsif word.length > 0
        left_side_of_word, right_side_of_word = word.split("~")
        center_letter = right_side_of_word[0]
        right_side_of_word = right_side_of_word[1..-1]
      end
    
      right_side_of_word = right_side_of_word.gsub("-", "")
    
      {
        left:   [29 - left_side_of_word.length * 4 - 1 * left_side_of_word.length, 2, left_side_of_word],
        center: [29, 2, center_letter, 255, 0, 0],
        right:  [34, 2, right_side_of_word]
      }
    end
    
    def render_scenes args, lowrez_sprites
      lowrez_sprites << args.state.scenes.flat_map do |hs|
        hotspot_square args, hs.x, hs.y, hs.w, hs.h
      end
    end
    
    def render_storylines args, lowrez_sprites
      lowrez_sprites << args.state.storylines.flat_map do |hs|
        hotspot_square args, hs.x, hs.y, hs.w, hs.h
      end
    end
    
    def adornments_alpha args, target_alpha = nil, minimum_alpha = nil
      return (minimum_alpha || 80) unless args.state.storyline_queue_empty_at
      target_alpha ||= 255
      target_alpha * args.state.storyline_queue_empty_at.ease(60)
    end
    
    def hotspot_square args, x, y, w, h
      if w >= 3 && h >= 3
        [
          [x + w.idiv(2) + 1, y, w.idiv(2), h, 'sprites/label-background.png', 0, adornments_alpha(args, 50), 23, 23, 23],
          [x, y, w.idiv(2), h, 'sprites/label-background.png', 0, adornments_alpha(args, 100), 223, 223, 223],
          [x + 1, y + 1, w - 2, h - 2, 'sprites/label-background.png', 0, adornments_alpha(args, 200), 40, 140, 40],
        ]
      else
        [
          [x, y, w, h, 'sprites/label-background.png', 0, adornments_alpha(args, 200), 0, 140, 0],
        ]
      end
    end
    
    def render_storyline_dialog args, lowrez_labels, lowrez_sprites
      return unless args.state.is_storyline_dialog_active
      return unless args.state.storyline_to_show
      labels = labels_for_word args.state.storyline_to_show
      if true # high rez version
        scale = 8.88
        offset = 45
        size = 25
        args.outputs.labels << [offset + labels[:left].x.-(1) * scale,
                                labels[:left].y * TINY_SCALE + 55,
                                labels[:left].text, size, 0, 0, 0, 0, 255,
                                'fonts/manaspc.ttf']
        center_text = labels[:center].text
        center_text = "|" if center_text == "$"
        args.outputs.labels << [offset + labels[:center].x * scale,
                                labels[:center].y * TINY_SCALE + 55,
                                center_text, size, 0, 255, 0, 0, 255,
                                'fonts/manaspc.ttf']
        args.outputs.labels << [offset + labels[:right].x * scale,
                                labels[:right].y * TINY_SCALE + 55,
                                labels[:right].text, size, 0, 0, 0, 0, 255,
                                'fonts/manaspc.ttf']
      else
        lowrez_labels << labels[:left]
        lowrez_labels << labels[:center]
        lowrez_labels << labels[:right]
      end
      args.state.is_storyline_dialog_active = true
      render_player args, lowrez_sprites
      lowrez_sprites <<  [0, 0, 64, 8, 'sprites/label-background.png']
    end
    
    def render_player args, lowrez_sprites
      lowrez_sprites << player_md_down(args, *args.state.player)
    end
    
    def render_adornments args, lowrez_sprites
      render_scenes args, lowrez_sprites
      render_storylines args, lowrez_sprites
      return if args.state.is_storyline_dialog_active
      lowrez_sprites << player_md_down(args, *args.state.player)
    end
    
    def global_alpha_percentage args, max_alpha = 255
      return 255 unless args.state.scene_changed_at
      return 255 unless args.state.scene_fade
      return 255 unless args.state.scene_fade > 0
      return max_alpha * args.state.scene_changed_at.ease(args.state.scene_fade)
    end
    
    def render_current_scene args, lowrez_sprites, lowrez_labels, lowrez_solids
      lowrez_sprites << [0, 0, 64, 64, args.state.background, 0, (global_alpha_percentage args)]
      if args.state.scene_render_override
        send args.state.scene_render_override, args, lowrez_sprites, lowrez_labels, lowrez_solids
      end
      storyline_to_show = args.state.storyline_to_show || ""
      render_adornments args, lowrez_sprites
      render_storyline_dialog args, lowrez_labels, lowrez_sprites
    
      if args.state.background == 'sprites/tribute-game-over.png'
        lowrez_sprites << [0, 0, 64, 11, 'sprites/label-background.png', 0, adornments_alpha(args, 200), 0, 0, 0]
        lowrez_labels << [9, 6, 'Return of', 255, 255, 255]
        lowrez_labels << [9, 1, ' Serenity', 255, 255, 255]
        if !args.state.ended
          args.gtk.stop_music
          args.outputs.sounds << 'sounds/music-loop.ogg'
          args.state.ended = true
        end
      end
    end
    
    def player_md_right args, x, y
      [x, y, 4, 11, 'sprites/player-right.png', 0, (global_alpha_percentage args)]
    end
    
    def player_md_left args, x, y
      [x, y, 4, 11, 'sprites/player-left.png', 0, (global_alpha_percentage args)]
    end
    
    def player_md_up args, x, y
      [x, y, 4, 11, 'sprites/player-up.png', 0, (global_alpha_percentage args)]
    end
    
    def player_md_down args, x, y
      [x, y, 4, 11, 'sprites/player-down.png', 0, (global_alpha_percentage args)]
    end
    
    def player_sm args, x, y
      [x, y, 3, 7, 'sprites/player-zoomed-out.png', 0, (global_alpha_percentage args)]
    end
    
    def player_xs args, x, y
      [x, y, 1, 4, 'sprites/player-zoomed-out.png', 0, (global_alpha_percentage args)]
    end
    
    

    Return Of Serenity - require.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/require.rb
    require 'app/lowrez_simulator.rb'
    require 'app/storyline_day_one.rb'
    require 'app/storyline_blinking_light.rb'
    require 'app/storyline_serenity_introduction.rb'
    require 'app/storyline_speed_of_light.rb'
    require 'app/storyline_serenity_alive.rb'
    require 'app/storyline_serenity_bio.rb'
    require 'app/storyline_anka.rb'
    require 'app/storyline_final_message.rb'
    require 'app/storyline_final_decision.rb'
    require 'app/storyline.rb'
    
    

    Return Of Serenity - storyline.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline.rb
    def hotspot_top
      [4, 61, 56, 3]
    end
    
    def hotspot_bottom
      [4, 0, 56, 3]
    end
    
    def hotspot_top_right
      [62, 35, 3, 25]
    end
    
    def hotspot_bottom_right
      [62, 0, 3, 25]
    end
    
    def storyline_history_include? args, text
      args.state.storyline_history.any? { |s| s.gsub("-", "").gsub(" ", "").include? text.gsub("-", "").gsub(" ", "") }
    end
    
    def blinking_light_side_of_home_render args, lowrez_sprites, lowrez_labels, lowrez_solids
      lowrez_sprites << [48, 44, 5, 5, 'sprites/square.png', 0,  50 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [49, 45, 3, 3, 'sprites/square.png', 0, 100 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [50, 46, 1, 1, 'sprites/square.png', 0, 255 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
    end
    
    def blinking_light_mountain_pass_render args, lowrez_sprites, lowrez_labels, lowrez_solids
      lowrez_sprites << [18, 47, 5, 5, 'sprites/square.png', 0,  50 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [19, 48, 3, 3, 'sprites/square.png', 0, 100 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [20, 49, 1, 1, 'sprites/square.png', 0, 255 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
    end
    
    def blinking_light_path_to_observatory_render args, lowrez_sprites, lowrez_labels, lowrez_solids
      lowrez_sprites << [0, 26, 5, 5, 'sprites/square.png', 0,  50 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [1, 27, 3, 3, 'sprites/square.png', 0, 100 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [2, 28, 1, 1, 'sprites/square.png', 0, 255 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
    end
    
    def blinking_light_observatory_render args, lowrez_sprites, lowrez_labels, lowrez_solids
      lowrez_sprites << [23, 59, 5, 5, 'sprites/square.png', 0,  50 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [24, 60, 3, 3, 'sprites/square.png', 0, 100 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [25, 61, 1, 1, 'sprites/square.png', 0, 255 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
    end
    
    def blinking_light_inside_observatory_render args, lowrez_sprites, lowrez_labels, lowrez_solids
      lowrez_sprites << [30, 30, 5, 5, 'sprites/square.png', 0,  50 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [31, 31, 3, 3, 'sprites/square.png', 0, 100 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [32, 32, 1, 1, 'sprites/square.png', 0, 255 * (Kernel.tick_count % 50).fdiv(50), 0, 255, 0]
    end
    
    def decision_graph context_message, context_action, context_result_one, context_result_two, context_result_three = [], context_result_four = []
      result_one_scene, result_one_label, result_one_text = context_result_one
      result_two_scene, result_two_label, result_two_text = context_result_two
      result_three_scene, result_three_label, result_three_text = context_result_three
      result_four_scene, result_four_label, result_four_text = context_result_four
    
      top_level_hash = {
        background: 'sprites/decision.png',
        fade: 60,
        player: [20, 36],
        storylines: [ ],
        scenes: [ ]
      }
    
      confirmation_result_one_hash = {
        background: 'sprites/decision.png',
        scenes: [ ],
        storylines: [ ]
      }
    
      confirmation_result_two_hash = {
        background: 'sprites/decision.png',
        scenes: [ ],
        storylines: [ ]
      }
    
      confirmation_result_three_hash = {
        background: 'sprites/decision.png',
        scenes: [ ],
        storylines: [ ]
      }
    
      confirmation_result_four_hash = {
        background: 'sprites/decision.png',
        scenes: [ ],
        storylines: [ ]
      }
    
      top_level_hash[:storylines] << [ 5, 35, 4, 4, context_message]
      top_level_hash[:storylines] << [20, 35, 4, 4, context_action]
    
      confirmation_result_one_hash[:scenes]       << [20, 35, 4, 4, top_level_hash]
      confirmation_result_one_hash[:scenes]       << [60, 50, 4, 4, result_one_scene]
      confirmation_result_one_hash[:storylines]   << [40, 50, 4, 4, "#{result_one_label}: \"#{result_one_text}\""]
      confirmation_result_one_hash[:scenes]       << [40, 40, 4, 4, confirmation_result_four_hash] if result_four_scene
      confirmation_result_one_hash[:scenes]       << [40, 30, 4, 4, confirmation_result_three_hash] if result_three_scene
      confirmation_result_one_hash[:scenes]       << [40, 20, 4, 4, confirmation_result_two_hash]
    
      confirmation_result_two_hash[:scenes]       << [20, 35, 4, 4, top_level_hash]
      confirmation_result_two_hash[:scenes]       << [40, 50, 4, 4, confirmation_result_one_hash]
      confirmation_result_two_hash[:scenes]       << [40, 40, 4, 4, confirmation_result_four_hash] if result_four_scene
      confirmation_result_two_hash[:scenes]       << [40, 30, 4, 4, confirmation_result_three_hash] if result_three_scene
      confirmation_result_two_hash[:scenes]       << [60, 20, 4, 4, result_two_scene]
      confirmation_result_two_hash[:storylines]   << [40, 20, 4, 4, "#{result_two_label}: \"#{result_two_text}\""]
    
      confirmation_result_three_hash[:scenes]     << [20, 35, 4, 4, top_level_hash]
      confirmation_result_three_hash[:scenes]     << [40, 50, 4, 4, confirmation_result_one_hash]
      confirmation_result_three_hash[:scenes]     << [40, 40, 4, 4, confirmation_result_four_hash]
      confirmation_result_three_hash[:scenes]     << [60, 30, 4, 4, result_three_scene]
      confirmation_result_three_hash[:storylines] << [40, 30, 4, 4, "#{result_three_label}: \"#{result_three_text}\""]
      confirmation_result_three_hash[:scenes]     << [40, 20, 4, 4, confirmation_result_two_hash]
    
      confirmation_result_four_hash[:scenes]      << [20, 35, 4, 4, top_level_hash]
      confirmation_result_four_hash[:scenes]      << [40, 50, 4, 4, confirmation_result_one_hash]
      confirmation_result_four_hash[:scenes]      << [60, 40, 4, 4, result_four_scene]
      confirmation_result_four_hash[:storylines]  << [40, 40, 4, 4, "#{result_four_label}: \"#{result_four_text}\""]
      confirmation_result_four_hash[:scenes]      << [40, 30, 4, 4, confirmation_result_three_hash]
      confirmation_result_four_hash[:scenes]      << [40, 20, 4, 4, confirmation_result_two_hash]
    
      top_level_hash[:scenes]     << [40, 50, 4, 4, confirmation_result_one_hash]
      top_level_hash[:scenes]     << [40, 40, 4, 4, confirmation_result_four_hash] if result_four_scene
      top_level_hash[:scenes]     << [40, 30, 4, 4, confirmation_result_three_hash] if result_three_scene
      top_level_hash[:scenes]     << [40, 20, 4, 4, confirmation_result_two_hash]
    
      top_level_hash
    end
    
    def ship_control_hotspot offset_x, offset_y, a, b, c, d
      results = []
      results << [ 6 + offset_x, 0 + offset_y, 4, 4, a]  if a
      results << [ 1 + offset_x, 5 + offset_y, 4, 4, b]  if b
      results << [ 6 + offset_x, 5 + offset_y, 4, 4, c]  if c
      results << [ 11 + offset_x, 5 + offset_y, 4, 4, d] if d
      results
    end
    
    def reload_current_scene
      if $gtk.args.state.last_hotspot_scene
        set_scene $gtk.args, send($gtk.args.state.last_hotspot_scene, $gtk.args)
        tick $gtk.args
      elsif respond_to? :set_scene
        set_scene $gtk.args, (replied_to_serenity_alive_firmly $gtk.args)
        tick $gtk.args
      end
      $gtk.console.close
    end
    
    

    Return Of Serenity - storyline_anka.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_anka.rb
    def anka_inside_room args
      {
        background: 'sprites/inside-home.png',
        player: [34, 35],
        storylines: [
          [34, 34, 4, 4, "Ahhhh!!! Oh god, it was just- a nightmare."],
        ],
        scenes: [
          [32, -1, 8, 3, :anka_observatory]
        ]
      }
    end
    
    def anka_observatory args
      {
        background: 'sprites/inside-observatory.png',
        fade: 60,
        player: [51, 12],
        storylines: [
          [50, 10, 4, 4,   "Breathe, Hiro. Just see what's there... everything--- will- be okay."]
        ],
        scenes: [
          [30, 18, 5, 12, :anka_inside_mainframe]
        ],
        render_override: :blinking_light_inside_observatory_render
      }
    end
    
    def anka_inside_mainframe args
      {
        player: [32, 4],
        background: 'sprites/mainframe.png',
        fade: 60,
        storylines: [
          [22, 45, 17, 4, (anka_last_reply args)],
          [45, 45,  4, 4, (anka_current_reply args)],
        ],
        scenes: [
          [*hotspot_top_right, :reply_to_anka]
        ]
      }
    end
    
    def reply_to_anka args
      decision_graph anka_current_reply(args),
                     "Matthew's-- wife is doing-- well. What's-- even-- better-- is that he's-- a dad, and he didn't-- even-- know it. Should- I- leave- out the part about-- the crew- being-- in hibernation-- for 20-- years? They- should- enter-- statis-- on a high- note... Right?",
                     [:replied_with_whole_truth, "Whole-- Truth--", anka_reply_whole_truth],
                     [:replied_with_half_truth, "Half-- Truth--", anka_reply_half_truth]
    end
    
    def anka_last_reply args
      if args.state.scene_history.include? :replied_to_serenity_alive_firmly
        return "Buffer--: #{serenity_alive_firm_reply.quote}"
      else
        return "Buffer--: #{serenity_alive_sugarcoated_reply.quote}"
      end
    end
    
    def anka_reply_whole_truth
      "Matthew's wife is doing-- very-- well. In fact, she was pregnant. Matthew-- is a dad. He has a son. But, I need- all-- of-- you-- to brace-- yourselves. You've-- been in statis-- for 20 years. A lot has changed. Most of Earth's-- population--- didn't-- survive. Tell- Matthew-- that I'm-- sorry he didn't-- get to see- his- son grow- up."
    end
    
    def anka_reply_half_truth
      "Matthew's--- wife- is doing-- very-- well. In fact, she was pregnant. Matthew is a dad! It's a boy! Tell- Matthew-- congrats-- for me. Hope-- to see- all of you- soon."
    end
    
    def replied_with_whole_truth args
      {
        background: 'sprites/inside-observatory.png',
        fade: 60,
        player: [32, 21],
        scenes: [[60, 0, 4, 32, :replied_to_anka_back_home]],
        storylines: [
          [30, 18, 5, 12, "Buffer-- has been set to: #{anka_reply_whole_truth.quote}"],
          [30, 10, 5, 4, "I- hope- I- did the right- thing- by laying-- it all- out- there."],
        ]
      }
    end
    
    def replied_with_half_truth args
      {
        background: 'sprites/inside-observatory.png',
        fade: 60,
        player: [32, 21],
        scenes: [[60, 0, 4, 32, :replied_to_anka_back_home]],
        storylines: [
          [30, 18, 5, 12, "Buffer-- has been set to: #{anka_reply_half_truth.quote}"],
          [30, 10, 5, 4, "I- hope- I- did the right- thing- by not giving-- them- the whole- truth."],
        ]
      }
    end
    
    def anka_current_reply args
      if args.state.scene_history.include? :replied_to_serenity_alive_firmly
        return "Hello. This is, Aanka. Sasha-- is still- trying-- to gather-- her wits about-- her, given- the gravity--- of your- last- reply. Thank- you- for being-- honest, and thank- you- for the help- with the ship- diagnostics. I was able-- to retrieve-- all of the navigation--- information---- after-- the battery--- swap. We- are ready-- to head back to Earth. Before-- we go- back- into-- statis, Matthew--- wanted-- to know- how his- wife- is doing. Please- reply-- as soon- as you can. He's-- not going-- to get- into-- the statis-- chamber-- until-- he knows- his wife is okay."
      else
        return "Hello. This is, Aanka. Thank- you for the help- with the ship's-- diagnostics. I was able-- to retrieve-- all of the navigation--- information--- after-- the battery-- swap. I- know-- that- you didn't-- tell- the whole truth- about-- how far we are from- Earth. Don't-- worry. I understand-- why you did it. We- are ready-- to head back to Earth. Before-- we go- back- into-- statis, Matthew--- wanted-- to know- how his- wife- is doing. Please- reply-- as soon- as you can. He's-- not going-- to get- into-- the statis-- chamber-- until-- he knows- his wife is okay."
      end
    end
    
    def replied_to_anka_back_home args
      if args.state.scene_history.include? :replied_with_whole_truth
        return {
          fade: 60,
          background: 'sprites/inside-home.png',
          player: [34, 4],
          storylines: [
            [34, 4, 4, 4, "I- hope-- this pit in my stomach-- is gone-- by tomorrow---."],
          ],
          scenes: [
            [30, 38, 12, 13, :final_message_sad],
          ]
        }
      else
        return {
          fade: 60,
          background: 'sprites/inside-home.png',
          player: [34, 4],
          storylines: [
            [34, 4, 4, 4, "I- get the feeling-- I'm going-- to sleep real well tonight--."],
          ],
          scenes: [
            [30, 38, 12, 13, :final_message_happy],
          ]
        }
      end
    end
    
    

    Return Of Serenity - storyline_blinking_light.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_blinking_light.rb
    def the_blinking_light args
      {
        fade: 60,
        background: 'sprites/side-of-home.png',
        player: [16, 13],
        scenes: [
          [52, 24, 11, 5, :blinking_light_mountain_pass],
        ],
        render_override: :blinking_light_side_of_home_render
      }
    end
    
    def blinking_light_mountain_pass args
      {
        background: 'sprites/mountain-pass-zoomed-out.png',
        player: [4, 4],
        scenes: [
          [18, 47, 5, 5, :blinking_light_path_to_observatory]
        ],
        render_override: :blinking_light_mountain_pass_render
      }
    end
    
    def blinking_light_path_to_observatory args
      {
        background: 'sprites/path-to-observatory.png',
        player: [60, 4],
        scenes: [
          [0, 26, 5, 5, :blinking_light_observatory]
        ],
        render_override: :blinking_light_path_to_observatory_render
      }
    end
    
    def blinking_light_observatory args
      {
        background: 'sprites/observatory.png',
        player: [60, 2],
        scenes: [
          [28, 39, 4, 10, :blinking_light_inside_observatory]
        ],
        render_override: :blinking_light_observatory_render
      }
    end
    
    def blinking_light_inside_observatory args
      {
        background: 'sprites/inside-observatory.png',
        player: [60, 2],
        storylines: [
          [50, 2, 4, 8,   "That's weird. I thought- this- mainframe-- was broken--."]
        ],
        scenes: [
          [30, 18, 5, 12, :blinking_light_inside_mainframe]
        ],
        render_override: :blinking_light_inside_observatory_render
      }
    end
    
    def blinking_light_inside_mainframe args
      {
        background: 'sprites/mainframe.png',
        fade: 60,
        player: [30, 4],
        scenes: [
          [62, 32, 4, 32, :reply_to_introduction]
        ],
        storylines: [
          [43, 43,  8, 8, "\"Mission-- control--, your- main- comm-- channels-- seem-- to be down. My apologies-- for- using-- this low- level-- exploit--. What's-- going-- on down there? We are ready-- for reentry--.\" Message--- Timestamp---: 4- hours-- 23--- minutes-- ago--."],
          [30, 30,  4, 4, "There's-- a low- level-- message-- here... NANI.T.F?"],
          [14, 10, 24, 4, "Oh interesting---. This transistor--- needed-- to be activated--- for the- mainframe-- to work."],
          [14, 20, 24, 4, "What the heck activated--- this thing- though?"]
        ]
      }
    end
    
    

    Return Of Serenity - storyline_day_one.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_day_one.rb
    def day_one_beginning args
      {
        background: 'sprites/side-of-home.png',
        player: [16, 13],
        scenes: [
          [0, 0, 64, 2, :day_one_infront_of_home],
        ],
        storylines: [
          [35, 10, 6, 6,  "Man. Hard to believe- that today- is the 20th--- anniversary-- of The Impact."]
        ]
      }
    end
    
    def day_one_infront_of_home args
      {
        background: 'sprites/front-of-home.png',
        player: [56, 23],
        scenes: [
          [43, 34, 10, 16, :day_one_home],
          [62, 0,  3, 40, :day_one_beginning],
          [0, 4, 3, 20, :day_one_ceremony]
        ],
        storylines: [
          [40, 20, 4, 4, "It looks like everyone- is already- at the rememberance-- ceremony."],
        ]
      }
    end
    
    def day_one_home args
      {
        background: 'sprites/inside-home.png',
        player: [34, 3],
        scenes: [
          [28, 0, 12, 2, :day_one_infront_of_home]
        ],
        storylines: [
          [
            38, 4, 4, 4, "My mansion- in all its glory! Okay yea, it's just a shipping- container-. Apparently-, it's nothing- like the luxuries- of the 2040's. But it's- all we have- in- this day and age. And it'll suffice."
          ],
          [
            28, 7, 4, 7,
            "Ahhh. My reading- couch. It's so comfortable--."
          ],
          [
            38, 21, 4, 4,
            "I'm- lucky- to have a computer--. I'm- one of the few people- with- the skills to put this- thing to good use."
          ],
          [
            45, 37, 4, 8,
            "This corner- of my home- is always- warmer-. It's cause of the ref~lected-- light- from the solar-- panels--, just on the other- side- of this wall. It's hard- to believe- there was o~nce-- an unlimited- amount- of electricity--."
          ],
          [
            32, 40, 8, 10,
            "This isn't- a good time- to sleep. I- should probably- head to the ceremony-."
          ],
          [
            25, 21, 5, 12,
            "Fifteen-- years- of computer-- science-- notes, neatly-- organized. Compiler--- Theory--, Linear--- Algebra---, Game-- Development---... Every-- subject-- imaginable--."
          ]
        ]
      }
    end
    
    def day_one_ceremony args
      {
        background: 'sprites/tribute.png',
        player: [57, 21],
        scenes: [
          [62, 0, 2, 40, :day_one_infront_of_home],
          [0, 24, 2, 40, :day_one_infront_of_library]
        ],
        storylines: [
          [53, 12, 3,  8,  "It's- been twenty- years since The Impact. Twenty- years, since Halley's-- Comet-- set Earth's- blue- sky on fire."],
          [45, 12, 3,  8,  "The space mission- sent to prevent- Earth's- total- destruction--, was a success. Only- 99.9%------ of the world's- population-- died-- that day. Hey, it's- better-- than 100%---- of humanity-- dying."],
          [20, 12, 23, 4, "The monument--- reads:---- Here- stands- the tribute-- to Space- Mission-- Serenity--- and- its- crew. You- have- given-- humanity--- a second-- chance."],
          [15, 12, 3,  8, "Rest- in- peace--- Matthew----, Sasha----, Aanka----"],
        ]
      }
    end
    
    def day_one_infront_of_library args
      {
        background: 'sprites/outside-library.png',
        player: [57, 21],
        scenes: [
          [62, 0, 2, 40, :day_one_ceremony],
          [49, 39, 6, 9, :day_one_library]
        ],
        storylines: [
          [50, 20, 4, 8,  "Shipping- containers-- as far- as the eye- can see. It's- rather- beautiful-- if you ask me. Even- though-- this- view- represents-- all- that's-- left- of humanity-."]
        ]
      }
    end
    
    def day_one_library args
      {
        background: 'sprites/library.png',
        player: [27, 4],
        scenes: [
          [0, 0, 64, 2, :end_day_one_infront_of_library]
        ],
        storylines: [
          [28, 22, 8, 4,  "I grew- up- in this library. I've- read every- book- here. My favorites-- were- of course-- anything- computer-- related."],
          [6, 32, 10, 6, "My favorite-- area--- of the library. The Science-- Section."]
        ]
      }
    end
    
    def end_day_one_infront_of_library args
      {
        background: 'sprites/outside-library.png',
        player: [51, 33],
        scenes: [
          [49, 39, 6, 9, :day_one_library],
          [62, 0, 2, 40, :end_day_one_monument],
        ],
        storylines: [
          [50, 27, 4, 4, "It's getting late. Better get some sleep."]
        ]
      }
    end
    
    def end_day_one_monument args
      {
        background: 'sprites/tribute.png',
        player: [2, 36],
        scenes: [
          [62, 0, 2, 40, :end_day_one_infront_of_home],
        ],
        storylines: [
          [50, 27, 4, 4, "It's getting late. Better get some sleep."],
        ]
      }
    end
    
    def end_day_one_infront_of_home args
      {
        background: 'sprites/front-of-home.png',
        player: [1, 17],
        scenes: [
          [43, 34, 10, 16, :end_day_one_home],
        ],
        storylines: [
          [20, 10, 4, 4, "It's getting late. Better get some sleep."],
        ]
      }
    end
    
    def end_day_one_home args
      {
        background: 'sprites/inside-home.png',
        player: [34, 3],
        scenes: [
          [32, 40, 8, 10, :end_day_one_dream],
        ],
        storylines: [
          [38, 4, 4, 4, "It's getting late. Better get some sleep."],
        ]
      }
    end
    
    def end_day_one_dream args
      {
        background: 'sprites/dream.png',
        fade: 60,
        player: [4, 4],
        scenes: [
          [62, 0, 2, 64, :explaining_the_special_power]
        ],
        storylines: [
          [10, 10, 4, 4, "Why- does this- moment-- always- haunt- my dreams?"],
          [20, 10, 4, 4, "This kid- reads these computer--- science--- books- nonstop-. What's- wrong with him?"],
          [30, 10, 4, 4, "There- is nothing-- wrong- with him. This behavior-- should be encouraged---! In fact-, I think- he's- special---. Have- you seen- him use- a computer---? It's-- almost-- as if he can- speak-- to it."]
        ]
      }
    end
    
    def explaining_the_special_power args
      {
        fade: 60,
        background: 'sprites/inside-home.png',
        player: [32, 30],
        scenes: [
          [
            38, 21, 4, 4, :explaining_the_special_power_inside_computer
          ],
        ]
      }
    end
    
    def explaining_the_special_power_inside_computer args
      {
        background: 'sprites/pc.png',
        fade: 60,
        player: [34, 4],
        scenes: [
          [0, 62, 64, 3, :the_blinking_light]
        ],
        storylines: [
          [14, 20, 24, 4, "So... I have a special-- power--. I don't-- need a mouse-, keyboard--, or even-- a monitor--- to control-- a computer--."],
          [14, 25, 24, 4, "I only-- pretend-- to use peripherals---, so as not- to freak- anyone--- out."],
          [14, 30, 24, 4, "Inside-- this silicon--- Universe---, is the only-- place I- feel- at peace."],
          [14, 35, 24, 4, "It's-- the only-- place where I don't-- feel alone."]
        ]
      }
    end
    
    

    Return Of Serenity - storyline_final_decision.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_final_decision.rb
    def final_decision_side_of_home args
      {
        fade: 120,
        background: 'sprites/side-of-home.png',
        player: [16, 13],
        scenes: [
          [52, 24, 11, 5, :final_decision_mountain_pass],
        ],
        render_override: :blinking_light_side_of_home_render,
        storylines: [
          [28, 13, 8, 4,  "Man. Hard to believe- that today- is the 21st--- anniversary-- of The Impact. Serenity--- will- be- home- soon."]
        ]
      }
    end
    
    def final_decision_mountain_pass args
      {
        background: 'sprites/mountain-pass-zoomed-out.png',
        player: [4, 4],
        scenes: [
          [18, 47, 5, 5, :final_decision_path_to_observatory]
        ],
        render_override: :blinking_light_mountain_pass_render
      }
    end
    
    def final_decision_path_to_observatory args
      {
        background: 'sprites/path-to-observatory.png',
        player: [60, 4],
        scenes: [
          [0, 26, 5, 5, :final_decision_observatory]
        ],
        render_override: :blinking_light_path_to_observatory_render
      }
    end
    
    def final_decision_observatory args
      {
        background: 'sprites/observatory.png',
        player: [60, 2],
        scenes: [
          [28, 39, 4, 10, :final_decision_inside_observatory]
        ],
        render_override: :blinking_light_observatory_render
      }
    end
    
    def final_decision_inside_observatory args
      {
        background: 'sprites/inside-observatory.png',
        player: [60, 2],
        storylines: [],
        scenes: [
          [30, 18, 5, 12, :final_decision_inside_mainframe]
        ],
        render_override: :blinking_light_inside_observatory_render
      }
    end
    
    def final_decision_inside_mainframe args
      {
        player: [32, 4],
        background: 'sprites/mainframe.png',
        storylines: [],
        scenes: [
          [*hotspot_top, :final_decision_ship_status],
        ]
      }
    end
    
    def final_decision_ship_status args
      {
        background: 'sprites/serenity.png',
        fade: 60,
        player: [30, 10],
        scenes: [
          [*hotspot_top_right, :final_decision]
        ],
        storylines: [
          [30,  8, 4, 4, "????"],
          *final_decision_ship_status_shared(args)
        ]
      }
    end
    
    def final_decision args
      decision_graph  "Stasis-- Chambers--: UNDERPOWERED, Life- forms-- will be terminated---- unless-- equilibrium----- is reached.",
                      "I CAN'T DO THIS... But... If-- I-- don't--- bring-- the- chambers--- to- equilibrium-----, they all die...",
                      [:final_decision_game_over_noone, "Kill--- Everyone---", "DO--- NOTHING?"],
                      [:final_decision_game_over_matthew, "Kill--- Sasha---", "KILL--- SASHA?"],
                      [:final_decision_game_over_anka, "Kill--- Aanka---", "KILL--- AANKA?"],
                      [:final_decision_game_over_sasha, "Kill--- Matthew---", "KILL--- MATTHEW?"]
    end
    
    def final_decision_game_over_noone args
      {
        background: 'sprites/tribute-game-over.png',
        player: [53, 14],
        fade: 600
      }
    end
    
    def final_decision_game_over_matthew args
      {
        background: 'sprites/tribute-game-over.png',
        player: [53, 14],
        fade: 600
      }
    end
    
    def final_decision_game_over_anka args
      {
        background: 'sprites/tribute-game-over.png',
        player: [53, 14],
        fade: 600
      }
    end
    
    def final_decision_game_over_sasha args
      {
        background: 'sprites/tribute-game-over.png',
        player: [53, 14],
        fade: 600
      }
    end
    
    def final_decision_ship_status_shared args
      [
        *ship_control_hotspot(24, 22,
                               "Stasis-- Chambers--: UNDERPOWERED, Life- forms-- will be terminated---- unless-- equilibrium----- is reached. WHAT?! NO!",
                               "Matthew's--- Chamber--: UNDER-- THREAT-- OF-- TERMINATION. WHAT?! NO!",
                               "Aanka's--- Chamber--: UNDER-- THREAT-- OF-- TERMINATION.  WHAT?! NO!",
                               "Sasha's--- Chamber--: UNDER-- THREAT-- OF-- TERMINATION. WHAT?! NO!"),
      ]
    end
    
    

    Return Of Serenity - storyline_final_message.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_final_message.rb
    def final_message_sad args
      {
        fade: 60,
        background: 'sprites/inside-home.png',
        player: [34, 35],
        storylines: [
          [34, 34, 4, 4, "Another-- sleepless-- night..."],
        ],
        scenes: [
          [32, -1, 8, 3, :final_message_observatory]
        ]
      }
    end
    
    def final_message_happy args
      {
        fade: 60,
        background: 'sprites/inside-home.png',
        player: [34, 35],
        storylines: [
          [34, 34, 4, 4, "Oh man, I slept like rock!"],
        ],
        scenes: [
          [32, -1, 8, 3, :final_message_observatory]
        ]
      }
    end
    
    def final_message_side_of_home args
      {
        fade: 60,
        background: 'sprites/side-of-home.png',
        player: [16, 13],
        scenes: [
          [52, 24, 11, 5, :final_message_mountain_pass],
        ],
        render_override: :blinking_light_side_of_home_render
      }
    end
    
    def final_message_mountain_pass args
      {
        background: 'sprites/mountain-pass-zoomed-out.png',
        player: [4, 4],
        scenes: [
          [18, 47, 5, 5, :final_message_path_to_observatory],
        ],
        storylines: [
          [18, 13, 5, 5, "Hnnnnnnnggg. My legs-- are still sore- from yesterday."]
        ],
        render_override: :blinking_light_mountain_pass_render
      }
    end
    
    def final_message_path_to_observatory args
      {
        background: 'sprites/path-to-observatory.png',
        player: [60, 4],
        scenes: [
          [0, 26, 5, 5, :final_message_observatory]
        ],
        storylines: [
          [22, 20, 10, 10, "This spot--, on the mountain, right here, it's-- perfect. This- is where- I'll-- yeet-- the person-- who is playing-- this- prank- on me."]
        ],
        render_override: :blinking_light_path_to_observatory_render
      }
    end
    
    def final_message_observatory args
      if args.state.scene_history.include? :replied_with_whole_truth
        return {
          background: 'sprites/inside-observatory.png',
          fade: 60,
          player: [51, 12],
          storylines: [
            [50, 10, 4, 4, "Here-- we- go..."]
          ],
          scenes: [
            [30, 18, 5, 12, :final_message_inside_mainframe]
          ],
          render_override: :blinking_light_inside_observatory_render
        }
      else
        return {
          background: 'sprites/inside-observatory.png',
          fade: 60,
          player: [51, 12],
          storylines: [
            [50, 10, 4, 4, "I feel like I'm-- walking-- on sunshine!"]
          ],
          scenes: [
            [30, 18, 5, 12, :final_message_inside_mainframe]
          ],
          render_override: :blinking_light_inside_observatory_render
        }
      end
    end
    
    def final_message_inside_mainframe args
      {
        player: [32, 4],
        background: 'sprites/mainframe.png',
        fade: 60,
        scenes: [[45, 45,  4, 4, :final_message_check_ship_status]]
      }
    end
    
    def final_message_check_ship_status args
      {
        background: 'sprites/mainframe.png',
        storylines: [
          [45, 45, 4, 4, (final_message_current args)],
        ],
        scenes: [
          [*hotspot_top, :final_message_ship_status],
        ]
      }
    end
    
    def final_message_ship_status args
      {
        background: 'sprites/serenity.png',
        fade: 60,
        player: [30, 10],
        scenes: [
          [30, 50, 4, 4, :final_message_ship_status_reviewed]
        ],
        storylines: [
          [30,  8, 4, 4, "Let me make- sure- everything--- looks good. It'll-- give me peace- of mind."],
          *final_message_ship_status_shared(args)
        ]
      }
    end
    
    def final_message_ship_status_reviewed args
      {
        background: 'sprites/serenity.png',
        fade: 60,
        scenes: [
          [*hotspot_bottom, :final_message_summary]
        ],
        storylines: [
          [0, 62, 62, 3, "Whew. Everyone-- is in their- chambers. The engines-- are roaring-- and Serenity-- is coming-- home."],
        ]
      }
    end
    
    def final_message_ship_status_shared args
      [
        *ship_control_hotspot( 0, 50,
                               "Stasis-- Chambers--: Online, All chambers-- are powered. Battery--- Allocation---: 3--- of-- 3--.",
                               "Matthew's--- Chamber--: OCCUPIED----",
                               "Aanka's--- Chamber--: OCCUPIED----",
                               "Sasha's--- Chamber--: OCCUPIED----"),
        *ship_control_hotspot(12, 35,
                              "Life- Support--: Not-- Needed---",
                              "O2--- Production---: OFF---",
                              "CO2--- Scrubbers---: OFF---",
                              "H2O--- Production---: OFF---"),
        *ship_control_hotspot(24, 20,
                              "Navigation: Offline---",
                              "Sensor: OFF---",
                              "Heads- Up- Display: DAMAGED---",
                              "Arithmetic--- Unit: DAMAGED----"),
        *ship_control_hotspot(36, 35,
                              "COMM: Underpowered----",
                              "Text: ON---",
                              "Audio: SEGFAULT---",
                              "Video: DAMAGED---"),
        *ship_control_hotspot(48, 50,
                              "Engine: Online, Coordinates--- Set- for Earth. Battery--- Allocation---: 3--- of-- 3---",
                              "Engine I: ON---",
                              "Engine II: ON---",
                              "Engine III: ON---")
      ]
    end
    
    def final_message_last_reply args
      if args.state.scene_history.include? :replied_with_whole_truth
        return "Buffer--: #{anka_reply_whole_truth.quote}"
      else
        return "Buffer--: #{anka_reply_half_truth.quote}"
      end
    end
    
    def final_message_current args
      if args.state.scene_history.include? :replied_with_whole_truth
        return "Hey... It's-- me Sasha. Aanka-- is trying-- her best to comfort-- Matthew. This- is the first- time- I've-- ever-- seen-- Matthew-- cry. We'll-- probably-- be in stasis-- by the time you get this message--. Thank- you- again-- for all your help. I look forward-- to meeting-- you in person."
      else
        return "Hey! It's-- me Sasha! LOL! Aanka-- and Matthew-- are dancing-- around-- like- goofballs--! They- are both- so adorable! Only-- this- tiny-- little-- genius-- can make-- a battle-- hardened-- general--- put- on a tiara-- and dance- around-- like a fairy-- princess-- XD------ Anyways, we are heading-- back into-- the chambers--. I hope our welcome-- home- parade-- has fireworks!"
      end
    end
    
    def final_message_summary args
      if args.state.scene_history.include? :replied_with_whole_truth
        return {
          background: 'sprites/inside-observatory.png',
          fade: 60,
          player: [31, 11],
          scenes: [[60, 0, 4, 32, :final_decision_side_of_home]],
          storylines: [
            [30, 10, 5, 4, "I can't-- imagine-- what they are feeling-- right now. But at least- they- know everything---, and we can- concentrate-- on rebuilding--- this world-- right- off the bat. I can't-- wait to see the future-- they'll-- help- build."],
          ]
        }
      else
        return {
          background: 'sprites/inside-observatory.png',
          fade: 60,
          player: [31, 11],
          scenes: [[60, 0, 4, 32, :final_decision_side_of_home]],
          storylines: [
            [30, 10, 5, 4, "They all sounded-- so happy. I know- they'll-- be in for a tough- dose- of reality--- when they- arrive. But- at least- they'll-- be around-- all- of us. We'll-- help them- cope."],
          ]
        }
      end
    end
    
    

    Return Of Serenity - storyline_serenity_alive.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_serenity_alive.rb
    def serenity_alive_side_of_home args
      {
        fade: 60,
        background: 'sprites/side-of-home.png',
        player: [16, 13],
        scenes: [
          [52, 24, 11, 5, :serenity_alive_mountain_pass],
        ],
        render_override: :blinking_light_side_of_home_render
      }
    end
    
    def serenity_alive_mountain_pass args
      {
        background: 'sprites/mountain-pass-zoomed-out.png',
        player: [4, 4],
        scenes: [
          [18, 47, 5, 5, :serenity_alive_path_to_observatory],
        ],
        storylines: [
          [18, 13, 5, 5, "Hnnnnnnnggg. My legs-- are still sore- from yesterday."]
        ],
        render_override: :blinking_light_mountain_pass_render
      }
    end
    
    def serenity_alive_path_to_observatory args
      {
        background: 'sprites/path-to-observatory.png',
        player: [60, 4],
        scenes: [
          [0, 26, 5, 5, :serenity_alive_observatory]
        ],
        storylines: [
          [22, 20, 10, 10, "This spot--, on the mountain, right here, it's-- perfect. This- is where- I'll-- yeet-- the person-- who is playing-- this- prank- on me."]
        ],
        render_override: :blinking_light_path_to_observatory_render
      }
    end
    
    def serenity_alive_observatory args
      {
        background: 'sprites/observatory.png',
        player: [60, 2],
        scenes: [
          [28, 39, 4, 10, :serenity_alive_inside_observatory]
        ],
        render_override: :blinking_light_observatory_render
      }
    end
    
    def serenity_alive_inside_observatory args
      {
        background: 'sprites/inside-observatory.png',
        player: [60, 2],
        storylines: [],
        scenes: [
          [30, 18, 5, 12, :serenity_alive_inside_mainframe]
        ],
        render_override: :blinking_light_inside_observatory_render
      }
    end
    
    def serenity_alive_inside_mainframe args
      {
        background: 'sprites/mainframe.png',
        fade: 60,
        player: [30, 4],
        scenes: [
          [*hotspot_top, :serenity_alive_ship_status],
        ],
        storylines: [
          [22, 45, 17, 4, (serenity_alive_last_reply args)],
          [45, 45,  4, 4, (serenity_alive_current_message args)],
        ]
      }
    end
    
    def serenity_alive_ship_status args
      {
        background: 'sprites/serenity.png',
        fade: 60,
        player: [30, 10],
        scenes: [
          [30, 50, 4, 4, :serenity_alive_ship_status_reviewed]
        ],
        storylines: [
          [30,  8, 4, 4, "Serenity? THE--- Mission-- Serenity?! How is that possible? They- are supposed-- to be dead."],
          [30, 10, 4, 4, "I... can't-- believe-- it. I- can access-- Serenity's-- computer? I- guess my \"superpower----\" isn't limited-- by proximity-- to- a machine--."],
          *serenity_alive_shared_ship_status(args)
        ]
      }
    end
    
    def serenity_alive_ship_status_reviewed args
      {
        background: 'sprites/serenity.png',
        fade: 60,
        scenes: [
          [*hotspot_bottom, :serenity_alive_time_to_reply]
        ],
        storylines: [
          [0, 62, 62, 3, "Okay. Reviewing-- everything--, it looks- like- I- can- take- the batteries--- from the Stasis--- Chambers--- and- Engine--- to keep- the crew-- alive-- and-- their-- location--- pinpointed---."],
        ]
      }
    end
    
    def serenity_alive_time_to_reply args
      decision_graph serenity_alive_current_message(args),
                      "Okay... time to deliver the bad news...",
                      [:replied_to_serenity_alive_firmly, "Firm-- Reply", serenity_alive_firm_reply],
                      [:replied_to_serenity_alive_kindly, "Sugar-- Coated---- Reply", serenity_alive_sugarcoated_reply]
    end
    
    def serenity_alive_shared_ship_status args
      [
        *ship_control_hotspot( 0, 50,
                               "Stasis-- Chambers--: Online, All chambers-- are powered. Battery--- Allocation---: 3--- of-- 3--, Hmmm. They don't-- need this to be powered-- right- now. Everyone-- is awake.",
                               nil,
                               nil,
                               nil),
        *ship_control_hotspot(12, 35,
                              "Life- Support--: Offline, Unable--- to- Sustain-- Life. Battery--- Allocation---: 0--- of-- 3---, Okay. That is definitely---- not a good thing.",
                              nil,
                              nil,
                              nil),
        *ship_control_hotspot(24, 20,
                              "Navigation: Offline, Unable--- to- Calculate--- Location. Battery--- Allocation---: 0--- of-- 3---, Whelp. No wonder-- Sasha-- can't-- get- any-- readings. Their- Navigation--- is completely--- offline.",
                              nil,
                              nil,
                              nil),
        *ship_control_hotspot(36, 35,
                              "COMM: Underpowered----, Limited--- to- Text-- Based-- COMM. Battery--- Allocation---: 1--- of-- 3---, It's-- lucky- that- their- COMM---- system was able to survive-- twenty-- years--. Just- barely-- it seems.",
                              nil,
                              nil,
                              nil),
        *ship_control_hotspot(48, 50,
                              "Engine: Online, Full- Control-- Available. Battery--- Allocation---: 3--- of-- 3---, Hmmm. No point of having an engine-- online--, if you don't- know- where you're-- going.",
                              nil,
                              nil,
                              nil)
      ]
    end
    
    def serenity_alive_firm_reply
      "Serenity, you are at a distance-- farther-- than- Neptune. All- of the ship's-- systems-- are failing. Please- move the batteries---- from- the Stasis-- Chambers-- over- to- Life-- Support--. I also-- need- you to move-- the batteries---- from- the Engines--- to your Navigation---- System."
    end
    
    def serenity_alive_sugarcoated_reply
      "So... you- are- a teeny--- tiny--- bit--- farther-- from Earth- than you think. And you have a teeny--- tiny--- problem-- with your ship. Please-- move the batteries--- from the Stasis--- Chambers--- over to Life--- Support---. I also need you to move the batteries--- from the Engines--- to your- Navigation--- System. Don't-- worry-- Sasha. I'll-- get y'all-- home."
    end
    
    def replied_to_serenity_alive_firmly args
      {
        background: 'sprites/inside-observatory.png',
        fade: 60,
        player: [32, 21],
        scenes: [
          [*hotspot_bottom_right, :serenity_alive_path_from_observatory]
        ],
        storylines: [
          [30, 18, 5, 12, "Buffer-- has been set to: #{serenity_alive_firm_reply.quote}"],
          *serenity_alive_reply_completed_shared_hotspots(args),
        ]
      }
    end
    
    def replied_to_serenity_alive_kindly args
      {
        background: 'sprites/inside-observatory.png',
        fade: 60,
        player: [32, 21],
        scenes: [
          [*hotspot_bottom_right, :serenity_alive_path_from_observatory]
        ],
        storylines: [
          [30, 18, 5, 12, "Buffer-- has been set to: #{serenity_alive_sugarcoated_reply.quote}"],
          *serenity_alive_reply_completed_shared_hotspots(args),
        ]
      }
    end
    
    def serenity_alive_path_from_observatory args
      {
        fade: 60,
        background: 'sprites/path-to-observatory.png',
        player: [4, 21],
        scenes: [
          [*hotspot_bottom_right, :serenity_bio_infront_of_home]
        ],
        storylines: [
          [22, 20, 10, 10, "I'm not sure what's-- worse. Waiting-- for Sasha's-- reply. Or jumping-- off- from- right- here."]
        ]
      }
    end
    
    def serenity_alive_reply_completed_shared_hotspots args
      [
        [30, 10, 5, 4, "I guess it wasn't-- a joke- after-- all."],
        [40, 10, 5, 4, "I barely-- remember--- the- history----- of the crew."],
        [50, 10, 5, 4, "It probably--- wouldn't-- hurt- to- refresh-- my memory--."]
      ]
    end
    
    def serenity_alive_last_reply args
      if args.state.scene_history.include? :replied_to_introduction_seriously
        return "Buffer--: \"Hello, Who- is sending-- this message--?\""
      else
        return "Buffer--: \"New- phone. Who dis?\""
      end
    end
    
    def serenity_alive_current_message args
      if args.state.scene_history.include? :replied_to_introduction_seriously
        "This- is Sasha. The Serenity--- crew-- is out of hibernation---- and ready-- for Earth reentry--. But, it seems like we are having-- trouble-- with our Navigation---- systems. Please advise.".quote
      else
        "LOL! Thanks for the laugh. I needed that. This- is Sasha. The Serenity--- crew-- is out of hibernation---- and ready-- for Earth reentry--. But, it seems like we are having-- trouble-- with our Navigation---- systems. Can you help me out- babe?".quote
      end
    end
    
    

    Return Of Serenity - storyline_serenity_bio.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_serenity_bio.rb
    def serenity_bio_infront_of_home args
      {
        fade: 60,
        background: 'sprites/front-of-home.png',
        player: [54, 23],
        scenes: [
          [44, 34, 8, 14, :serenity_bio_inside_home],
          [0, 3, 3, 22, :serenity_bio_library]
        ]
      }
    end
    
    def serenity_bio_inside_home args
      {
        background: 'sprites/inside-home.png',
        player: [34, 4],
        storylines: [
          [34, 4, 4, 4, "I'm--- completely--- exhausted."],
        ],
        scenes: [
          [30, 38, 12, 13, :serenity_bio_restless_sleep],
          [32, 0, 8, 3, :serenity_bio_infront_of_home],
        ]
      }
    end
    
    def serenity_bio_restless_sleep args
      {
        fade: 60,
        background: 'sprites/inside-home.png',
        storylines: [
          [32, 38, 10, 13, "I can't-- seem to sleep. I know nothing-- about the- crew-. Maybe- I- should- go read- up- on- them."],
        ],
        scenes: [
          [32, 0, 8, 3, :serenity_bio_infront_of_home],
        ]
      }
    end
    
    def serenity_bio_library args
      {
        background: 'sprites/library.png',
        fade: 60,
        player: [30, 7],
        scenes: [
          [21, 35, 3, 18, :serenity_bio_book]
        ]
      }
    end
    
    def serenity_bio_book args
      {
        background: 'sprites/book.png',
        fade: 60,
        player: [6, 52],
        storylines: [
          [ 4, 50, 56, 4, "The Title-- Reads: Never-- Forget-- Mission-- Serenity---"],
    
          [ 4, 38,  8, 8, "Name: Matthew--- R. Sex: Male--- Age-- at-- Departure: 36-----"],
          [14, 38, 46, 8, "Tribute-- Text: Matthew graduated-- Magna-- Cum-- Laude-- from MIT--- with-- a- PHD---- in Aero-- Nautical--- Engineering. He was immensely--- competitive, and had an insatiable---- thirst- for aerial-- battle. From the age of twenty, he remained-- undefeated--- in the Israeli-- Air- Force- \"Blue Flag\" combat-- exercises. By the age of 29--- he had already-- risen through- the ranks, and became-- the Lieutenant--- General--- of Lufwaffe. Matthew-- volenteered-- to- pilot-- Mission-- Serenity. To- this day, his wife- and son- are pillars-- of strength- for us. Rest- in Peace- Matthew, we are sorry-- that- news of the pregancy-- never-- reached- you. Please forgive us."],
    
          [4,  26,  8, 8, "Name: Aanka--- P. Sex: Female--- Age-- at-- Departure: 9-----"],
          [14, 26, 46, 8, "Tribute-- Text: Aanka--- gratuated--- Magna-- Cum- Laude-- from MIT, at- the- age- of eight, with a- PHD---- in Astro-- Physics. Her-- IQ--- was over 390, the highest-- ever- recorded--- IQ-- in- human-- history. She changed- the landscape-- of Physics-- with her efforts- in- unravelling--- the mysteries--- of- Dark- Matter--. Anka discovered-- the threat- of Halley's-- Comet-- collision--- with Earth. She spear headed-- the global-- effort-- for Misson-- Serenity. Her- multilingual--- address-- to- the world-- brought- us all hope."],
    
          [4,  14,  8, 8, "Name: Sasha--- N. Sex: Female--- Age-- at-- Departure: 29-----"],
          [14, 14, 46, 8, "Tribute-- Text: Sasha gratuated-- Magna-- Cum- Laude-- from MIT--- with-- a- PHD---- in Computer---- Science----. She-- was-- brilliant--, strong- willed--, and-- a-- stunningly--- beautiful--- woman---. Sasha---- is- the- creator--- of the world's--- first- Ruby--- Quantum-- Machine---. After-- much- critical--- acclaim--, the Quantum-- Computer-- was placed in MIT's---- Museam-- next- to- Richard--- G. and Thomas--- K.'s---- Lisp-- Machine---. Her- engineering--- skills-- were-- paramount--- for Mission--- Serenity's--- success. Humanity-- misses-- you-- dearly,-- Sasha--. Life-- shines-- a dimmer-- light-- now- that- your- angelic- voice-- can never- be heard- again."],
        ],
        scenes: [
          [*hotspot_bottom, :serenity_bio_finally_to_bed]
        ]
      }
    end
    
    def serenity_bio_finally_to_bed args
      {
        fade: 60,
        background: 'sprites/inside-home.png',
        player: [35, 3],
        storylines: [
          [34, 4, 4, 4, "Maybe-- I'll-- be able-- to sleep- now..."],
        ],
        scenes: [
          [32, 38, 10, 13, :bad_dream],
        ]
      }
    end
    
    def bad_dream args
      {
        fade: 120,
        background: 'sprites/inside-home.png',
        player: [34, 35],
        storylines: [
          [34, 34, 4, 4, "Man. I did not- sleep- well- at all..."],
        ],
        scenes: [
          [32, -1, 8, 3, :bad_dream_observatory]
        ]
      }
    end
    
    def bad_dream_observatory args
      {
        background: 'sprites/inside-observatory.png',
        fade: 120,
        player: [51, 12],
        storylines: [
          [50, 10, 4, 4,   "Breathe, Hiro. Just see what's there... everything--- will- be okay."]
        ],
        scenes: [
          [30, 18, 5, 12, :bad_dream_inside_mainframe]
        ],
        render_override: :blinking_light_inside_observatory_render
      }
    end
    
    def bad_dream_inside_mainframe args
      {
        player: [32, 4],
        background: 'sprites/mainframe.png',
        fade: 120,
        storylines: [
          [22, 45, 17, 4, (bad_dream_last_reply args)],
        ],
        scenes: [
          [45, 45,  4, 4, :bad_dream_everyone_dead],
        ]
      }
    end
    
    def bad_dream_everyone_dead args
      {
        background: 'sprites/mainframe.png',
        storylines: [
          [22, 45, 17, 4, (bad_dream_last_reply args)],
          [45, 45,  4, 4, "Hi-- Hiro. This is Sasha. By the time- you get this- message, chances-- are we will- already-- be- dead. The batteries--- got- damaged-- during-- removal. And- we don't-- have enough-- power-- for Life-- Support. The air-- is- already--- starting-- to taste- bad. It... would- have been- nice... to go- on a date--- with- you-- when-- I- got- back- to Earth. Anyways, good-- bye-- Hiro-- XOXOXO----"],
          [22,  5, 17, 4, "Meh. Whatever, I didn't-- want to save them anyways. What- a pain- in my ass."],
        ],
        scenes: [
          [*hotspot_bottom, :anka_inside_room]
        ]
      }
    end
    
    def bad_dream_last_reply args
      if args.state.scene_history.include? :replied_to_serenity_alive_firmly
        return "Buffer--: #{serenity_alive_firm_reply.quote}"
      else
        return "Buffer--: #{serenity_alive_sugarcoated_reply.quote}"
      end
    end
    
    

    Return Of Serenity - storyline_serenity_introduction.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_serenity_introduction.rb
    # decision_graph "Message from Sasha",
    #                "I should reply.",
    #                [:replied_to_introduction_seriously,  "Reply Seriously", "Who is this?"],
    # [:replied_to_introduction_humorously, "Reply Humorously", "New phone who dis?"]
    def reply_to_introduction args
      decision_graph  "\"Mission-- control--, your- main- comm-- channels-- seem-- to be down. My apologies-- for- using-- this low- level-- exploit--. What's-- going-- on down there? We are ready-- for reentry--.\" Message--- Timestamp---: 4- hours-- 23--- minutes-- ago--.",
                      "Whoever-- pulled- off this exploit-- knows their stuff. I should reply--.",
                      [:replied_to_introduction_seriously,  "Serious Reply",  "Hello, Who- is sending-- this message--?"],
                      [:replied_to_introduction_humorously, "Humorous Reply", "New phone, who dis?"]
    end
    
    def replied_to_introduction_seriously args
      {
        background: 'sprites/inside-observatory.png',
        fade: 60,
        player: [32, 21],
        scenes: [
          *replied_to_introduction_shared_scenes(args)
        ],
        storylines: [
          [30, 18, 5, 12, "Buffer-- has been set to: \"Hello, Who- is sending-- this message--?\""],
          *replied_to_introduction_shared_storylines(args)
        ]
      }
    end
    
    def replied_to_introduction_humorously args
      {
        background: 'sprites/inside-observatory.png',
        fade: 60,
        player: [32, 21],
        scenes: [
          *replied_to_introduction_shared_scenes(args)
        ],
        storylines: [
          [30, 18, 5, 12, "Buffer-- has been set to: \"New- phone. Who dis?\""],
          *replied_to_introduction_shared_storylines(args)
        ]
      }
    end
    
    def replied_to_introduction_shared_storylines args
      [
        [30, 10, 5, 4, "It's-- going-- to take a while-- for this reply-- to make it's-- way back."],
        [40, 10, 5, 4, "4- hours-- to send a message-- at light speed?! How far away-- is the sender--?"],
        [50, 10, 5, 4, "I know- I've-- read about-- light- speed- travel-- before--. Maybe-- the library--- still has that- poster."]
      ]
    end
    
    def replied_to_introduction_shared_scenes args
      [[60, 0, 4, 32, :replied_to_introduction_observatory]]
    end
    
    def replied_to_introduction_observatory args
      {
        background: 'sprites/observatory.png',
        player: [28, 39],
        scenes: [
          [60, 0, 4, 32, :replied_to_introduction_path_to_observatory]
        ]
      }
    end
    
    def replied_to_introduction_path_to_observatory args
      {
        background: 'sprites/path-to-observatory.png',
        player: [0, 26],
        scenes: [
          [60, 0, 4, 20, :replied_to_introduction_mountain_pass]
        ],
      }
    end
    
    def replied_to_introduction_mountain_pass args
      {
        background: 'sprites/mountain-pass-zoomed-out.png',
        player: [21, 48],
        scenes: [
          [0, 0, 15, 4, :replied_to_introduction_side_of_home]
        ],
        storylines: [
          [15, 28, 5, 3, "At least I'm-- getting-- my- exercise-- in- for- today--."]
        ]
      }
    end
    
    def replied_to_introduction_side_of_home args
      {
        background: 'sprites/side-of-home.png',
        player: [58, 29],
        scenes: [
          [2, 0, 61, 2, :speed_of_light_front_of_home]
        ],
      }
    end
    
    

    Return Of Serenity - storyline_speed_of_light.rb link

    # ./samples/99_genre_rpg_narrative/return_of_serenity/app/storyline_speed_of_light.rb
    def speed_of_light_front_of_home args
      {
        background: 'sprites/front-of-home.png',
        player: [54, 23],
        scenes: [
          [44, 34, 8, 14, :speed_of_light_inside_home],
          [0, 3, 3, 22, :speed_of_light_outside_library]
        ]
      }
    end
    
    def speed_of_light_inside_home args
      {
        background: 'sprites/inside-home.png',
        player: [35, 4],
        storylines: [
          [30, 38, 12, 13, "Can't- sleep right now. I have to- find- out- why- it took- over-- 4- hours-- to receive-- that message."]
        ],
        scenes: [
          [32, 0, 8, 3, :speed_of_light_front_of_home],
        ]
      }
    end
    
    def speed_of_light_outside_library args
      {
        background: 'sprites/outside-library.png',
        player: [55, 19],
        scenes: [
          [49, 39, 6, 10, :speed_of_light_library],
          [61, 11, 3, 20, :speed_of_light_front_of_home]
        ]
      }
    end
    
    def speed_of_light_library args
      {
        background: 'sprites/library.png',
        player: [30, 7],
        scenes: [
          [3, 50, 10, 3, :speed_of_light_celestial_bodies_diagram]
        ]
      }
    end
    
    def speed_of_light_celestial_bodies_diagram args
      {
        background: 'sprites/planets.png',
        fade: 60,
        player: [30, 3],
        scenes: [
          [56 - 2, 10, 5, 5, :speed_of_light_distance_discovered]
        ],
        storylines: [
          [30, 2, 4, 4, "Here- it is! This is a diagram--- of the solar-- system--. It was printed-- over-- fifty-- years- ago. Geez-- that's-- old."],
    
          [ 0 - 2, 10, 5, 5, "The label- reads: Sun. The length- of the Astronomical-------- Unit-- (AU), is the distance-- from the Sun- to the Earth. Which is about 150--- million--- kilometers----."],
          [ 7 - 2, 10, 5, 5, "The label- reads: Mercury. Distance from Sun: 0.39AU------------ or- 3----- light-- minutes--."],
          [14 - 2, 10, 5, 5, "The label- reads: Venus. Distance from Sun: 0.72AU------------ or- 6----- light-- minutes--."],
          [21 - 2, 10, 5, 5, "The label- reads: Earth. Distance from Sun: 1.00AU------------ or- 8----- light-- minutes--."],
          [28 - 2, 10, 5, 5, "The label- reads: Mars. Distance from Sun: 1.52AU------------ or- 12----- light-- minutes--."],
          [35 - 2, 10, 5, 5, "The label- reads: Jupiter. Distance from Sun: 5.20AU------------ or- 45----- light-- minutes--."],
          [42 - 2, 10, 5, 5, "The label- reads: Saturn. Distance from Sun: 9.53AU------------ or- 79----- light-- minutes--."],
          [49 - 2, 10, 5, 5, "The label- reads: Uranus. Distance from Sun: 19.81AU------------ or- 159----- light-- minutes--."],
          # [56 - 2, 15, 4, 4, "The label- reads: Neptune. Distance from Sun: 30.05AU------------ or- 4.1----- light-- hours--."],
          [63 - 2, 10, 5, 5, "The label- reads: Pluto. Wait. WTF? Pluto-- isn't-- a planet."],
        ]
      }
    end
    
    def speed_of_light_distance_discovered args
      {
        background: 'sprites/planets.png',
        scenes: [
          [13, 0, 44, 3, :speed_of_light_end_of_day]
        ],
        storylines: [
          [ 0 - 2, 10, 5, 5, "The label- reads: Sun. The length- of the Astronomical-------- Unit-- (AU), is the distance-- from the Sun- to the Earth. Which is about 150--- million--- kilometers----."],
          [ 7 - 2, 10, 5, 5, "The label- reads: Mercury. Distance from Sun: 0.39AU------------ or- 3----- light-- minutes--."],
          [14 - 2, 10, 5, 5, "The label- reads: Venus. Distance from Sun: 0.72AU------------ or- 6----- light-- minutes--."],
          [21 - 2, 10, 5, 5, "The label- reads: Earth. Distance from Sun: 1.00AU------------ or- 8----- light-- minutes--."],
          [28 - 2, 10, 5, 5, "The label- reads: Mars. Distance from Sun: 1.52AU------------ or- 12----- light-- minutes--."],
          [35 - 2, 10, 5, 5, "The label- reads: Jupiter. Distance from Sun: 5.20AU------------ or- 45----- light-- minutes--."],
          [42 - 2, 10, 5, 5, "The label- reads: Saturn. Distance from Sun: 9.53AU------------ or- 79----- light-- minutes--."],
          [49 - 2, 10, 5, 5, "The label- reads: Uranus. Distance from Sun: 19.81AU------------ or- 159----- light-- minutes--."],
          [56 - 2, 10, 5, 5, "The label- reads: Neptune. Distance from Sun: 30.05AU------------ or- 4.1----- light-- hours--. What?! The message--- I received-- was from a source-- farther-- than-- Neptune?!"],
          [63 - 2, 10, 5, 5, "The label- reads: Pluto. Dista- Wait... Pluto-- isn't-- a planet. People-- thought- Pluto-- was a planet-- back- then?--"],
        ]
      }
    end
    
    def speed_of_light_end_of_day args
      {
        fade: 60,
        background: 'sprites/inside-home.png',
        player: [35, 0],
        storylines: [
          [35, 10, 4, 4, "Wonder-- what the reply-- will be. Who- the hell is contacting--- me from beyond-- Neptune? This- has to be some- kind- of- joke."]
        ],
        scenes: [
          [31, 38, 10, 12, :serenity_alive_side_of_home]
        ]
      }
    end
    
    

    Genre Rpg Roguelike link

    Roguelike Starting Point - constants.rb link

    # ./samples/99_genre_rpg_roguelike/01_roguelike_starting_point/app/constants.rb
    SHOW_LEGEND = true
    SOURCE_TILE_SIZE = 16
    DESTINATION_TILE_SIZE = 16
    TILE_SHEET_SIZE = 256
    TILE_R = 0
    TILE_G = 0
    TILE_B = 0
    TILE_A = 255
    
    

    Roguelike Starting Point - legend.rb link

    # ./samples/99_genre_rpg_roguelike/01_roguelike_starting_point/app/legend.rb
    def tick_legend args
      return unless SHOW_LEGEND
    
      legend_padding = 16
      legend_x = 1280 - TILE_SHEET_SIZE - legend_padding
      legend_y =  720 - TILE_SHEET_SIZE - legend_padding
      tile_sheet_sprite = [legend_x,
                           legend_y,
                           TILE_SHEET_SIZE,
                           TILE_SHEET_SIZE,
                           'sprites/simple-mood-16x16.png', 0,
                           TILE_A,
                           TILE_R,
                           TILE_G,
                           TILE_B]
    
      if args.inputs.mouse.point.inside_rect? tile_sheet_sprite
        mouse_row = args.inputs.mouse.point.y.idiv(SOURCE_TILE_SIZE)
        tile_row = 15 - (mouse_row - legend_y.idiv(SOURCE_TILE_SIZE))
    
        mouse_col = args.inputs.mouse.point.x.idiv(SOURCE_TILE_SIZE)
        tile_col = (mouse_col - legend_x.idiv(SOURCE_TILE_SIZE))
    
        args.outputs.primitives << [legend_x - legend_padding * 2,
                                    mouse_row * SOURCE_TILE_SIZE, 256 + legend_padding * 2, 16, 128, 128, 128, 64].solid
    
        args.outputs.primitives << [mouse_col * SOURCE_TILE_SIZE,
                                    legend_y - legend_padding * 2, 16, 256 + legend_padding * 2, 128, 128, 128, 64].solid
    
        sprite_key = sprite_lookup.find { |k, v| v == [tile_row, tile_col] }
        if sprite_key
          member_name, _ = sprite_key
          member_name = member_name_as_code member_name
          args.outputs.labels << [660, 70, "# CODE SAMPLE (place in the tick_game method located in main.rb)", -1, 0]
          args.outputs.labels << [660, 50, "#                                    GRID_X, GRID_Y, TILE_KEY", -1, 0]
          args.outputs.labels << [660, 30, "args.outputs.sprites << tile_in_game(     5,      6, #{member_name}    )", -1, 0]
        else
          args.outputs.labels << [660, 50, "Tile [#{tile_row}, #{tile_col}] not found. Add a key and value to app/sprite_lookup.rb:", -1, 0]
          args.outputs.labels << [660, 30, "{ \"some_string\" => [#{tile_row}, #{tile_col}] } OR { some_symbol: [#{tile_row}, #{tile_col}] }.", -1, 0]
        end
    
      end
    
      # render the sprite in the top right with a padding to the top and right so it's
      # not flush against the edge
      args.outputs.sprites << tile_sheet_sprite
    
      # carefully place some ascii arrows to show the legend labels
      args.outputs.labels  <<  [895, 707, "ROW --->"]
      args.outputs.labels  <<  [943, 412, "       ^"]
      args.outputs.labels  <<  [943, 412, "       |"]
      args.outputs.labels  <<  [943, 394, "COL ---+"]
    
      # use the tile sheet to print out row and column numbers
      args.outputs.sprites << 16.map_with_index do |i|
        sprite_key = i % 10
        [
          tile(1280 - TILE_SHEET_SIZE - legend_padding * 2 - SOURCE_TILE_SIZE,
                720 - legend_padding * 2 - (SOURCE_TILE_SIZE * i),
                sprite(sprite_key)),
          tile(1280 - TILE_SHEET_SIZE - SOURCE_TILE_SIZE + (SOURCE_TILE_SIZE * i),
                720 - TILE_SHEET_SIZE - legend_padding * 3, sprite(sprite_key))
        ]
      end
    end
    
    

    Roguelike Starting Point - main.rb link

    # ./samples/99_genre_rpg_roguelike/01_roguelike_starting_point/app/main.rb
    require 'app/constants.rb'
    require 'app/sprite_lookup.rb'
    require 'app/legend.rb'
    
    def tick args
      tick_game args
      tick_legend args
    end
    
    def tick_game args
      # setup the grid
      args.state.grid.padding = 104
      args.state.grid.size = 512
    
      # set up your game
      # initialize the game/game defaults. ||= means that you only initialize it if
      # the value isn't alread initialized
      args.state.player.x ||= 0
      args.state.player.y ||= 0
    
      args.state.enemies ||= [
        { x: 10, y: 10, type: :goblin, tile_key: :G },
        { x: 15, y: 30, type: :rat,    tile_key: :R }
      ]
    
      args.state.info_message ||= "Use arrow keys to move around."
    
      # handle keyboard input
      # keyboard input (arrow keys to move player)
      new_player_x = args.state.player.x
      new_player_y = args.state.player.y
      player_direction = ""
      player_moved = false
      if args.inputs.keyboard.key_down.up
        new_player_y += 1
        player_direction = "north"
        player_moved = true
      elsif args.inputs.keyboard.key_down.down
        new_player_y -= 1
        player_direction = "south"
        player_moved = true
      elsif args.inputs.keyboard.key_down.right
        new_player_x += 1
        player_direction = "east"
        player_moved = true
      elsif args.inputs.keyboard.key_down.left
        new_player_x -= 1
        player_direction = "west"
        player_moved = true
      end
    
      #handle game logic
      # determine if there is an enemy on that square,
      # if so, don't let the player move there
      if player_moved
        found_enemy = args.state.enemies.find do |e|
          e[:x] == new_player_x && e[:y] == new_player_y
        end
    
        if !found_enemy
          args.state.player.x = new_player_x
          args.state.player.y = new_player_y
          args.state.info_message = "You moved #{player_direction}."
        else
          args.state.info_message = "You cannot move into a square an enemy occupies."
        end
      end
    
      args.outputs.sprites << tile_in_game(args.state.player.x,
                                           args.state.player.y, '@')
    
      # render game
      # render enemies at locations
      args.outputs.sprites << args.state.enemies.map do |e|
        tile_in_game(e[:x], e[:y], e[:tile_key])
      end
    
      # render the border
      border_x = args.state.grid.padding - DESTINATION_TILE_SIZE
      border_y = args.state.grid.padding - DESTINATION_TILE_SIZE
      border_size = args.state.grid.size + DESTINATION_TILE_SIZE * 2
    
      args.outputs.borders << [border_x,
                               border_y,
                               border_size,
                               border_size]
    
      # render label stuff
      args.outputs.labels << [border_x, border_y - 10, "Current player location is: #{args.state.player.x}, #{args.state.player.y}"]
      args.outputs.labels << [border_x, border_y + 25 + border_size, args.state.info_message]
    end
    
    def tile_in_game x, y, tile_key
      tile($gtk.args.state.grid.padding + x * DESTINATION_TILE_SIZE,
           $gtk.args.state.grid.padding + y * DESTINATION_TILE_SIZE,
           tile_key)
    end
    
    

    Roguelike Starting Point - sprite_lookup.rb link

    # ./samples/99_genre_rpg_roguelike/01_roguelike_starting_point/app/sprite_lookup.rb
    def sprite_lookup
      {
        0 => [3, 0],
        1 => [3, 1],
        2 => [3, 2],
        3 => [3, 3],
        4 => [3, 4],
        5 => [3, 5],
        6 => [3, 6],
        7 => [3, 7],
        8 => [3, 8],
        9 => [3, 9],
        '@' => [4, 0],
        A: [ 4,  1],
        B: [ 4,  2],
        C: [ 4,  3],
        D: [ 4,  4],
        E: [ 4,  5],
        F: [ 4,  6],
        G: [ 4,  7],
        H: [ 4,  8],
        I: [ 4,  9],
        J: [ 4, 10],
        K: [ 4, 11],
        L: [ 4, 12],
        M: [ 4, 13],
        N: [ 4, 14],
        O: [ 4, 15],
        P: [ 5,  0],
        Q: [ 5,  1],
        R: [ 5,  2],
        S: [ 5,  3],
        T: [ 5,  4],
        U: [ 5,  5],
        V: [ 5,  6],
        W: [ 5,  7],
        X: [ 5,  8],
        Y: [ 5,  9],
        Z: [ 5, 10],
        a: [ 6,  1],
        b: [ 6,  2],
        c: [ 6,  3],
        d: [ 6,  4],
        e: [ 6,  5],
        f: [ 6,  6],
        g: [ 6,  7],
        h: [ 6,  8],
        i: [ 6,  9],
        j: [ 6, 10],
        k: [ 6, 11],
        l: [ 6, 12],
        m: [ 6, 13],
        n: [ 6, 14],
        o: [ 6, 15],
        p: [ 7,  0],
        q: [ 7,  1],
        r: [ 7,  2],
        s: [ 7,  3],
        t: [ 7,  4],
        u: [ 7,  5],
        v: [ 7,  6],
        w: [ 7,  7],
        x: [ 7,  8],
        y: [ 7,  9],
        z: [ 7, 10],
        '|' => [ 7, 12]
      }
    end
    
    def sprite key
      $gtk.args.state.reserved.sprite_lookup[key]
    end
    
    def member_name_as_code raw_member_name
      if raw_member_name.is_a? Symbol
        ":#{raw_member_name}"
      elsif raw_member_name.is_a? String
        "'#{raw_member_name}'"
      elsif raw_member_name.is_a? Fixnum
        "#{raw_member_name}"
      else
        "UNKNOWN: #{raw_member_name}"
      end
    end
    
    def tile x, y, tile_row_column_or_key
      tile_extended x, y, DESTINATION_TILE_SIZE, DESTINATION_TILE_SIZE, TILE_R, TILE_G, TILE_B, TILE_A, tile_row_column_or_key
    end
    
    def tile_extended x, y, w, h, r, g, b, a, tile_row_column_or_key
      row_or_key, column = tile_row_column_or_key
      if !column
        row, column = sprite row_or_key
      else
        row, column = row_or_key, column
      end
    
      if !row
        member_name = member_name_as_code tile_row_column_or_key
        raise "Unabled to find a sprite for #{member_name}. Make sure the value exists in app/sprite_lookup.rb."
      end
    
      # Sprite provided by Rogue Yun
      # http://www.bay12forums.com/smf/index.php?topic=144897.0
      # License: Public Domain
    
      {
        x: x,
        y: y,
        w: w,
        h: h,
        tile_x: column * 16,
        tile_y: (row * 16),
        tile_w: 16,
        tile_h: 16,
        r: r,
        g: g,
        b: b,
        a: a,
        path: 'sprites/simple-mood-16x16.png'
      }
    end
    
    $gtk.args.state.reserved.sprite_lookup = sprite_lookup
    
    

    Roguelike Line Of Sight - main.rb link

    # ./samples/99_genre_rpg_roguelike/02_roguelike_line_of_sight/app/main.rb
    =begin
    
     APIs listing that haven't been encountered in previous sample apps:
    
     - lambda: A way to define a block and its parameters with special syntax.
       For example, the syntax of lambda looks like this:
       my_lambda = -> { puts "This is my lambda" }
    
     Reminders:
     - 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.
    
     - ARRAY#inside_rect?: Returns whether or not the point is inside a rect.
    
     - product: Returns an array of all combinations of elements from all arrays.
    
     - find: Finds all elements of a collection that meet requirements.
    
     - abs: Returns the absolute value.
    
    =end
    
    # This sample app allows the player to move around in the dungeon, which becomes more or less visible
    # depending on the player's location, and also has enemies.
    
    class Game
      attr_accessor :args, :state, :inputs, :outputs, :grid
    
      # Calls all the methods needed for the game to run properly.
      def tick
        defaults
        render_canvas
        render_dungeon
        render_player
        render_enemies
        print_cell_coordinates
        calc_canvas
        input_move
        input_click_map
      end
    
      # Sets default values and initializes variables
      def defaults
        outputs.background_color = [0, 0, 0] # black background
    
        # Initializes empty canvas, dungeon, and enemies collections.
        state.canvas   ||= []
        state.dungeon  ||= []
        state.enemies  ||= []
    
        # If state.area doesn't have value, load_area_one and derive_dungeon_from_area methods are called
        if !state.area
          load_area_one
          derive_dungeon_from_area
    
          # Changing these values will change the position of player
          state.x = 7
          state.y = 5
    
          # Creates new enemies, sets their values, and adds them to the enemies collection.
          state.enemies << state.new_entity(:enemy) do |e| # declares each enemy as new entity
            e.x           = 13 # position
            e.y           = 5
            e.previous_hp = 3
            e.hp          = 3
            e.max_hp      = 3
            e.is_dead     = false # the enemy is alive
          end
    
          update_line_of_sight # updates line of sight by adding newly visible cells
        end
      end
    
      # Adds elements into the state.area collection
      # The dungeon is derived using the coordinates of this collection
      def load_area_one
        state.area ||= []
        state.area << [8, 6]
        state.area << [7, 6]
        state.area << [7, 7]
        state.area << [8, 9]
        state.area << [7, 8]
        state.area << [7, 9]
        state.area << [6, 4]
        state.area << [7, 3]
        state.area << [7, 4]
        state.area << [6, 5]
        state.area << [7, 5]
        state.area << [8, 5]
        state.area << [8, 4]
        state.area << [1, 1]
        state.area << [0, 1]
        state.area << [0, 2]
        state.area << [1, 2]
        state.area << [2, 2]
        state.area << [2, 1]
        state.area << [2, 3]
        state.area << [1, 3]
        state.area << [1, 4]
        state.area << [2, 4]
        state.area << [2, 5]
        state.area << [1, 5]
        state.area << [2, 6]
        state.area << [3, 6]
        state.area << [4, 6]
        state.area << [4, 7]
        state.area << [4, 8]
        state.area << [5, 8]
        state.area << [5, 9]
        state.area << [6, 9]
        state.area << [7, 10]
        state.area << [7, 11]
        state.area << [7, 12]
        state.area << [7, 12]
        state.area << [7, 13]
        state.area << [8, 13]
        state.area << [9, 13]
        state.area << [10, 13]
        state.area << [11, 13]
        state.area << [12, 13]
        state.area << [12, 12]
        state.area << [8, 12]
        state.area << [9, 12]
        state.area << [10, 12]
        state.area << [11, 12]
        state.area << [12, 11]
        state.area << [13, 11]
        state.area << [13, 10]
        state.area << [13, 9]
        state.area << [13, 8]
        state.area << [13, 7]
        state.area << [13, 6]
        state.area << [12, 6]
        state.area << [14, 6]
        state.area << [14, 5]
        state.area << [13, 5]
        state.area << [12, 5]
        state.area << [12, 4]
        state.area << [13, 4]
        state.area << [14, 4]
        state.area << [1, 6]
        state.area << [6, 6]
      end
    
      # Starts with an empty dungeon collection, and adds dungeon cells into it.
      def derive_dungeon_from_area
        state.dungeon = [] # starts as empty collection
    
        state.area.each do |a| # for each element of the area collection
          state.dungeon << state.new_entity(:dungeon_cell) do |d| # declares each dungeon cell as new entity
            d.x = a.x # dungeon cell position using coordinates from area
            d.y = a.y
            d.is_visible = false # cell is not visible
            d.alpha = 0 # not transparent at all
            d.border = [left_margin   + a.x * grid_size,
                        bottom_margin + a.y * grid_size,
                        grid_size,
                        grid_size,
                        *blue,
                        255] # sets border definition for dungeon cell
            d # returns dungeon cell
          end
        end
      end
    
      def left_margin
        40  # sets left margin
      end
    
      def bottom_margin
        60 # sets bottom margin
      end
    
      def grid_size
        40 # sets size of grid square
      end
    
      # Updates the line of sight by calling the thick_line_of_sight method and
      # adding dungeon cells to the newly_visible collection
      def update_line_of_sight
        variations = [-1, 0, 1]
        # creates collection of newly visible dungeon cells
        newly_visible = variations.product(variations).flat_map do |rise, run| # combo of all elements
          thick_line_of_sight state.x, state.y, rise, run, 15, # calls thick_line_of_sight method
                              lambda { |x, y| dungeon_cell_exists? x, y } # checks whether or not cell exists
        end.uniq# removes duplicates
    
        state.dungeon.each do |d| # perform action on each element of dungeons collection
          d.is_visible = newly_visible.find { |v| v.x == d.x && v.y == d.y } # finds match inside newly_visible collection
        end
      end
    
      #Returns a boolean value
      def dungeon_cell_exists? x, y
        # Finds cell coordinates inside dungeon collection to determine if dungeon cell exists
        state.dungeon.find { |d| d.x == x && d.y == y }
      end
    
      # Calls line_of_sight method to add elements to result collection
      def thick_line_of_sight start_x, start_y, rise, run, distance, cell_exists_lambda
        result = []
        result += line_of_sight start_x, start_y, rise, run, distance, cell_exists_lambda
        result += line_of_sight start_x - 1, start_y, rise, run, distance, cell_exists_lambda # one left
        result += line_of_sight start_x + 1, start_y, rise, run, distance, cell_exists_lambda # one right
        result
      end
    
      # Adds points to the result collection to create the player's line of sight
      def line_of_sight start_x, start_y, rise, run, distance, cell_exists_lambda
        result = [] # starts as empty collection
        points = points_on_line start_x, start_y, rise, run, distance # calls points_on_line method
        points.each do |p| # for each point in collection
          if cell_exists_lambda.call(p.x, p.y) # if the cell exists
            result << p # add it to result collection
          else # if cell does not exist
            return result # return result collection as it is
          end
        end
    
        result # return result collection
      end
    
      # Finds the coordinates of the points on the line by performing calculations
      def points_on_line start_x, start_y, rise, run, distance
        distance.map do |i| # perform an action
          [start_x + run * i, start_y + rise * i] # definition of point
        end
      end
    
      def render_canvas
        return
        outputs.borders << state.canvas.map do |c| # on each element of canvas collection
          c.border # outputs border
        end
      end
    
      # Outputs the dungeon cells.
      def render_dungeon
        outputs.solids << [0, 0, grid.w, grid.h] # outputs black background for grid
    
        # Sets the alpha value (opacity) for each dungeon cell and calls the cell_border method.
        outputs.borders << state.dungeon.map do |d| # for each element in dungeon collection
          d.alpha += if d.is_visible # if cell is visible
                     255.fdiv(30) # increment opacity (transparency)
                   else # if cell is not visible
                     255.fdiv(600) * -1 # decrease opacity
                   end
          d.alpha = d.alpha.cap_min_max(0, 255)
          cell_border d.x, d.y, [*blue, d.alpha] # sets blue border using alpha value
        end.reject_nil
      end
    
      # Sets definition of a cell border using the parameters
      def cell_border x, y, color = nil
        [left_margin   + x * grid_size,
        bottom_margin + y * grid_size,
        grid_size,
        grid_size,
        *color]
      end
    
      # Sets the values for the player and outputs it as a label
      def render_player
        outputs.labels << [grid_x(state.x) + 20, # positions "@" text in center of grid square
                         grid_y(state.y) + 35,
                         "@", # player is represented by a white "@" character
                         1, 1, *white]
      end
    
      def grid_x x
        left_margin + x * grid_size # positions horizontally on grid
      end
    
      def grid_y y
        bottom_margin + y * grid_size # positions vertically on grid
      end
    
      # Outputs enemies onto the screen.
      def render_enemies
        state.enemies.map do |e| # for each enemy in the collection
          alpha = 255 # set opacity (full transparency)
    
          # Outputs an enemy using a label.
          outputs.labels << [
                       left_margin + 20 +  e.x * grid_size, # positions enemy's "r" text in center of grid square
                       bottom_margin + 35 + e.y * grid_size,
                       "r", # enemy's text
                       1, 1, *white, alpha]
    
          # Creates a red border around an enemy.
          outputs.borders << [grid_x(e.x), grid_y(e.y), grid_size, grid_size, *red]
        end
      end
    
      #White labels are output for the cell coordinates of each element in the dungeon collection.
      def print_cell_coordinates
        return unless state.debug
        state.dungeon.each do |d|
          outputs.labels << [grid_x(d.x) + 2,
                             grid_y(d.y) - 2,
                             "#{d.x},#{d.y}",
                             -2, 0, *white]
        end
      end
    
      # Adds new elements into the canvas collection and sets their values.
      def calc_canvas
        return if state.canvas.length > 0 # return if canvas collection has at least one element
        15.times do |x| # 15 times perform an action
          15.times do |y|
            state.canvas << state.new_entity(:canvas) do |c| # declare canvas element as new entity
              c.x = x # set position
              c.y = y
              c.border = [left_margin   + x * grid_size,
                          bottom_margin + y * grid_size,
                          grid_size,
                          grid_size,
                          *white, 30] # sets border definition
            end
          end
        end
      end
    
      # Updates x and y values of the player, and updates player's line of sight
      def input_move
        x, y, x_diff, y_diff = input_target_cell
    
        return unless dungeon_cell_exists? x, y # player can't move there if a dungeon cell doesn't exist in that location
        return if enemy_at x, y # player can't move there if there is an enemy in that location
    
        state.x += x_diff # increments x by x_diff (so player moves left or right)
        state.y += y_diff # same with y and y_diff ( so player moves up or down)
        update_line_of_sight # updates visible cells
      end
    
      def enemy_at x, y
        # Finds if coordinates exist in enemies collection and enemy is not dead
        state.enemies.find { |e| e.x == x && e.y == y && !e.is_dead }
      end
    
      #M oves the user based on their keyboard input and sets values for target cell
      def input_target_cell
        if inputs.keyboard.key_down.up # if "up" key is in "down" state
          [state.x, state.y + 1,  0,  1] # user moves up
        elsif inputs.keyboard.key_down.down # if "down" key is pressed
          [state.x, state.y - 1,  0, -1] # user moves down
        elsif inputs.keyboard.key_down.left # if "left" key is pressed
          [state.x - 1, state.y, -1,  0] # user moves left
        elsif inputs.keyboard.key_down.right # if "right" key is pressed
          [state.x + 1, state.y,  1,  0] # user moves right
        else
          nil  # otherwise, empty
        end
      end
    
      # Goes through the canvas collection to find if the mouse was clicked inside of the borders of an element.
      def input_click_map
        return unless inputs.mouse.click # return unless the mouse is clicked
        canvas_entry = state.canvas.find do |c| # find element from canvas collection that meets requirements
          inputs.mouse.click.inside_rect? c.border # find border that mouse was clicked inside of
        end
        puts canvas_entry # prints canvas_entry value
      end
    
      # Sets the definition of a label using the parameters.
      def label text, x, y, color = nil
        color ||= white # color is initialized to white
        [x, y, text, 1, 1, *color] # sets label definition
      end
    
      def green
        [60, 200, 100] # sets color saturation to shade of green
      end
    
      def blue
        [50, 50, 210] # sets color saturation to shade of blue
      end
    
      def white
        [255, 255, 255] # sets color saturation to white
      end
    
      def red
        [230, 80, 80] # sets color saturation to shade of red
      end
    
      def orange
        [255, 80, 60] # sets color saturation to shade of orange
      end
    
      def pink
        [255, 0, 200] # sets color saturation to shade of pink
      end
    
      def gray
        [75, 75, 75] # sets color saturation to shade of gray
      end
    
      # Recolors the border using the parameters.
      def recolor_border border, r, g, b
        border[4] = r
        border[5] = g
        border[6] = b
        border
      end
    
      # Returns a boolean value.
      def visible? cell
        # finds cell's coordinates inside visible_cells collections to determine if cell is visible
        state.visible_cells.find { |c| c.x == cell.x && c.y == cell.y}
      end
    
      # Exports dungeon by printing dungeon cell coordinates
      def export_dungeon
        state.dungeon.each do |d| # on each element of dungeon collection
          puts "state.dungeon << [#{d.x}, #{d.y}]" # prints cell coordinates
        end
      end
    
      def distance_to_cell cell
        distance_to state.x, cell.x, state.y, cell.y # calls distance_to method
      end
    
      def distance_to from_x, x, from_y, y
        (from_x - x).abs + (from_y - y).abs # finds distance between two cells using coordinates
      end
    end
    
    $game = Game.new
    
    def tick args
      $game.args    = args
      $game.state   = args.state
      $game.inputs  = args.inputs
      $game.outputs = args.outputs
      $game.grid    = args.grid
      $game.tick
    end
    
    

    Genre Rpg Tactical link

    Hexagonal Grid - main.rb link

    # ./samples/99_genre_rpg_tactical/hexagonal_grid/app/main.rb
    class HexagonTileGame
      attr_gtk
    
      def defaults
        state.tile_scale      = 1.3
        state.tile_size       = 80
        state.tile_w          = Math.sqrt(3) * state.tile_size.half
        state.tile_h          = state.tile_size * 3/4
        state.tiles_x_count   = 1280.idiv(state.tile_w) - 1
        state.tiles_y_count   = 720.idiv(state.tile_h) - 1
        state.world_width_px  = state.tiles_x_count * state.tile_w
        state.world_height_px = state.tiles_y_count * state.tile_h
        state.world_x_offset  = (1280 - state.world_width_px).half
        state.world_y_offset  = (720 - state.world_height_px).half
        state.tiles         ||= state.tiles_x_count.map_with_ys(state.tiles_y_count) do |ordinal_x, ordinal_y|
          {
            ordinal_x: ordinal_x,
            ordinal_y: ordinal_y,
            offset_x: (ordinal_y.even?) ?
                      (state.world_x_offset + state.tile_w.half.half) :
                      (state.world_x_offset - state.tile_w.half.half),
            offset_y: state.world_y_offset,
            w: state.tile_w,
            h: state.tile_h,
            type: :blank,
            path: "sprites/hexagon-gray.png",
            a: 20
          }.associate do |h|
            h.merge(x: h[:offset_x] + h[:ordinal_x] * h[:w],
                    y: h[:offset_y] + h[:ordinal_y] * h[:h]).scale_rect(state.tile_scale)
          end.associate do |h|
            h.merge(center: {
                      x: h[:x] + h[:w].half,
                      y: h[:y] + h[:h].half
                    }, radius: [h[:w].half, h[:h].half].max)
          end
        end
      end
    
      def input
        if inputs.click
          tile = state.tiles.find { |t| inputs.click.point_inside_circle? t[:center], t[:radius] }
          if tile
            tile[:a] = 255
            tile[:path] = "sprites/hexagon-black.png"
          end
        end
      end
    
      def tick
        defaults
        input
        render
      end
    
      def render
        outputs.sprites << state.tiles
      end
    end
    
    $game = HexagonTileGame.new
    
    def tick args
      $game.args = args
      $game.tick
    end
    
    $gtk.reset
    
    

    Isometric Grid - main.rb link

    # ./samples/99_genre_rpg_tactical/isometric_grid/app/main.rb
    class Isometric
        attr_accessor :grid, :inputs, :state, :outputs
    
        def tick
            defaults
            render
            calc
            process_inputs
        end
    
        def defaults
            state.quantity              ||= 6                                                        #Size of grid
            state.tileSize              ||= [262 / 2, 194 / 2]                                       #width and heigth of orange tiles
            state.tileGrid              ||= []                                                       #Holds ordering of tiles
            state.currentSpriteLocation ||= -1                                                       #Current Sprite hovering location
            state.tileCords             ||= []                                                       #Physical, rendering cordinates
            state.initCords             ||= [640 - (state.quantity / 2 * state.tileSize[0]), 330]    #Location of tile (0, 0)
            state.sideSize              ||= [state.tileSize[0] / 2, 242 / 2]                         #Purple & green cube face size
            state.mode                  ||= :delete                                                  #Switches between :delete and :insert
            state.spriteSelection       ||= [['river',    0, 0, 262 / 2, 194 / 2],
                                             ['mountain', 0, 0, 262 / 2, 245 / 2],
                                             ['ocean',    0, 0, 262 / 2, 194 / 2]]             #Storage for sprite information
                                                                                               #['name', deltaX, deltaY, sizeW, sizeH]
                                                                                               #^delta refers to distance from tile cords
    
            #Orders tiles based on tile placement and fancy math. Very left: 0,0. Very bottom: quantity-1, 0, etc
            if state.tileGrid == []
                tempX = 0
                tempY = 0
                tempLeft = false
                tempRight = false
                count = 0
                (state.quantity * state.quantity).times do
                    if tempY == 0
                        tempLeft = true
                    end
                    if tempX == (state.quantity - 1)
                        tempRight = true
                    end
                    state.tileGrid.push([tempX, tempY, true, tempLeft, tempRight, count])
                        #orderX, orderY, exists?, leftSide, rightSide, order
                    tempX += 1
                    if tempX == state.quantity
                        tempX = 0
                        tempY += 1
                    end
                    tempLeft = false
                    tempRight = false
                    count += 1
                end
            end
    
            #Calculates physical cordinates for tiles
            if state.tileCords == []
                state.tileCords = state.tileGrid.map do
                    |val|
                    x = (state.initCords[0]) + ((val[0] + val[1]) * state.tileSize[0] / 2)
                    y = (state.initCords[1]) + (-1 * val[0] * state.tileSize[1] / 2) + (val[1] * state.tileSize[1] / 2)
                    [x, y, val[2], val[3], val[4], val[5], -1] #-1 represents sprite on top of tile. -1 for now
                end
            end
    
        end
    
        def render
            renderBackground
            renderLeft
            renderRight
            renderTiles
            renderObjects
            renderLabels
        end
    
        def renderBackground
            outputs.solids << [0, 0, 1280, 720, 0, 0, 0]   #Background color
        end
    
        def renderLeft
            #Shows the pink left cube face
            outputs.sprites << state.tileCords.map do
                |val|
                if val[2] == true && val[3] == true       #Checks if the tile exists and right face needs to be rendered
                    [val[0], val[1] + (state.tileSize[1] / 2) - state.sideSize[1], state.sideSize[0],
                    state.sideSize[1], 'sprites/leftSide.png']
                end
            end
        end
    
        def renderRight
            #Shows the green right cube face
            outputs.sprites << state.tileCords.map do
                |val|
                if val[2] == true && val[4] == true        #Checks if it exists & checks if right face needs to be rendered
                    [val[0] + state.tileSize[0] / 2, val[1] + (state.tileSize[1] / 2) - state.sideSize[1], state.sideSize[0],
                    state.sideSize[1], 'sprites/rightSide.png']
                end
            end
        end
    
        def renderTiles
            #Shows the tile itself. Important that it's rendered after the two above!
            outputs.sprites << state.tileCords.map do
                |val|
                if val[2] == true     #Chcekcs if tile needs to be rendered
                  if val[5] == state.currentSpriteLocation
                    [val[0], val[1], state.tileSize[0], state.tileSize[1], 'sprites/selectedTile.png']
                  else
                    [val[0], val[1], state.tileSize[0], state.tileSize[1], 'sprites/tile.png']
                  end
                end
            end
        end
    
        def renderObjects
            #Renders the sprites on top of the tiles. Order of rendering: top corner to right corner and cascade down until left corner
            #to bottom corner.
            a = (state.quantity * state.quantity) - state.quantity
            iter = 0
            loop do
                if state.tileCords[a][2] == true && state.tileCords[a][6] != -1
                    outputs.sprites << [state.tileCords[a][0] + state.spriteSelection[state.tileCords[a][6]][1],
                                        state.tileCords[a][1] + state.spriteSelection[state.tileCords[a][6]][2],
                                        state.spriteSelection[state.tileCords[a][6]][3], state.spriteSelection[state.tileCords[a][6]][4],
                                        'sprites/' + state.spriteSelection[state.tileCords[a][6]][0] + '.png']
                end
                iter += 1
                a    += 1
                a -= state.quantity * 2 if iter == state.quantity
                iter = 0                if iter == state.quantity
                break if a < 0
            end
        end
    
        def renderLabels
            #Labels
            outputs.labels << [50, 680, 'Click to delete!',             5, 0, 255, 255, 255, 255] if state.mode == :delete
            outputs.labels << [50, 640, 'Press \'i\' for insert mode!', 5, 0, 255, 255, 255, 255] if state.mode == :delete
            outputs.labels << [50, 680, 'Click to insert!',             5, 0, 255, 255, 255, 255] if state.mode == :insert
            outputs.labels << [50, 640, 'Press \'d\' for delete mode!', 5, 0, 255, 255, 255, 255] if state.mode == :insert
        end
    
        def calc
            calcCurrentHover
        end
    
        def calcCurrentHover
            #This determines what tile the mouse is hovering (or last hovering) over
            x = inputs.mouse.position.x
            y = inputs.mouse.position.y
            m = (state.tileSize[1] / state.tileSize[0])   #slope
            state.tileCords.map do
                |val|
                #Conditions that makes runtime faster. Checks if the mouse click was between tile dimensions (rectangle collision)
                next unless val[0] < x && x < val[0] + state.tileSize[0]
                next unless val[1] < y && y < val[1] + state.tileSize[1]
                next unless val[2] == true
                tempBool = false
                if x == val[0] + (state.tileSize[0] / 2)
                    #The height of a diamond is the height of the diamond, so if x equals that exact point, it must be inside the diamond
                    tempBool = true
                elsif x < state.tileSize[0] / 2 + val[0]
                    #Uses y = (m) * (x - x1) + y1 to determine the y values for the two diamond lines on the left half of diamond
                    tempY1 =      (m * (x - val[0])) + val[1] + (state.tileSize[1] / 2)
                    tempY2 = (-1 * m * (x - val[0])) + val[1] + (state.tileSize[1] / 2)
                    #Checks to see if the mouse click y value is between those temp y values
                    tempBool = true if y < tempY1 && y > tempY2
                elsif x > state.tileSize[0] / 2 + val[0]
                    #Uses y = (m) * (x - x1) + y1 to determine the y values for the two diamond lines on the right half of diamond
                    tempY1 =      (m * (x - val[0] - (state.tileSize[0] / 2))) + val[1]
                    tempY2 = (-1 * m * (x - val[0] - (state.tileSize[0] / 2))) + val[1] + state.tileSize[1]
                    #Checks to see if the mouse click y value is between those temp y values
                    tempBool = true if y > tempY1 && y < tempY2
                end
    
                if tempBool == true
                    state.currentSpriteLocation = val[5]         #Current sprite location set to the order value
                end
            end
        end
    
        def process_inputs
            #Makes development much faster and easier
            if inputs.keyboard.key_up.r
                $dragon.reset
            end
            checkTileSelected
            switchModes
        end
    
        def checkTileSelected
            if inputs.mouse.down
                x = inputs.mouse.down.point.x
                y = inputs.mouse.down.point.y
                m = (state.tileSize[1] / state.tileSize[0])   #slope
                state.tileCords.map do
                    |val|
                    #Conditions that makes runtime faster. Checks if the mouse click was between tile dimensions (rectangle collision)
                    next unless val[0] < x && x < val[0] + state.tileSize[0]
                    next unless val[1] < y && y < val[1] + state.tileSize[1]
                    next unless val[2] == true
                    tempBool = false
                    if x == val[0] + (state.tileSize[0] / 2)
                        #The height of a diamond is the height of the diamond, so if x equals that exact point, it must be inside the diamond
                        tempBool = true
                    elsif x < state.tileSize[0] / 2 + val[0]
                        #Uses y = (m) * (x - x1) + y1 to determine the y values for the two diamond lines on the left half of diamond
                        tempY1 =      (m * (x - val[0])) + val[1] + (state.tileSize[1] / 2)
                        tempY2 = (-1 * m * (x - val[0])) + val[1] + (state.tileSize[1] / 2)
                        #Checks to see if the mouse click y value is between those temp y values
                        tempBool = true if y < tempY1 && y > tempY2
                    elsif x > state.tileSize[0] / 2 + val[0]
                        #Uses y = (m) * (x - x1) + y1 to determine the y values for the two diamond lines on the right half of diamond
                        tempY1 =      (m * (x - val[0] - (state.tileSize[0] / 2))) + val[1]
                        tempY2 = (-1 * m * (x - val[0] - (state.tileSize[0] / 2))) + val[1] + state.tileSize[1]
                        #Checks to see if the mouse click y value is between those temp y values
                        tempBool = true if y > tempY1 && y < tempY2
                    end
    
                    if tempBool == true
                        if state.mode == :delete
                            val[2] = false
                            state.tileGrid[val[5]][2]  = false      #Unnecessary because never used again but eh, I like consistency
                            state.tileCords[val[5]][2] = false      #Ensures that the tile isn't rendered
                            unless state.tileGrid[val[5]][0] == 0   #If tile is the left most tile in the row, right doesn't get rendered
                                state.tileGrid[val[5] - 1][4] = true            #Why the order value is amazing
                                state.tileCords[val[5] - 1][4] = true
                            end
                            unless state.tileGrid[val[5]][1] == state.quantity - 1     #Same but left side
                                state.tileGrid[val[5] + state.quantity][3] = true
                                state.tileCords[val[5] + state.quantity][3] = true
                            end
                        elsif state.mode == :insert
                            #adds the current sprite value selected to tileCords. (changes from the -1 earlier)
                            val[6] = rand(state.spriteSelection.length)
                        end
                    end
                end
            end
        end
    
        def switchModes
            #Switches between insert and delete modes
            if inputs.keyboard.key_up.i && state.mode == :delete
                state.mode = :insert
                inputs.keyboard.clear
            elsif inputs.keyboard.key_up.d && state.mode == :insert
                state.mode = :delete
                inputs.keyboard.clear
            end
        end
    
    end
    
    $isometric = Isometric.new
    
    def tick args
        $isometric.grid    = args.grid
        $isometric.inputs  = args.inputs
        $isometric.state   = args.state
        $isometric.outputs = args.outputs
        $isometric.tick
    end
    
    

    Genre Rpg Topdown link

    Topdown Casino - main.rb link

    # ./samples/99_genre_rpg_topdown/topdown_casino/app/main.rb
    $gtk.reset
    
    def coinflip
      rand < 0.5
    end
    
    class Game
      attr_accessor :args
    
      def text_font
        return nil #"rpg.ttf"
      end
    
      def text_color
        [ 255, 255, 255, 255 ]
      end
    
      def set_gem_values
        @args.state.gem0 = ((coinflip) ?  100 : 20)
        @args.state.gem1 = ((coinflip) ? -10 : -50)
        @args.state.gem2 = ((coinflip) ? -10 : -30)
        if coinflip
          tmp = @args.state.gem0
          @args.state.gem0 = @args.state.gem1
          @args.state.gem1 = tmp
        end
        if coinflip
          tmp = @args.state.gem1
          @args.state.gem1 = @args.state.gem2
          @args.state.gem2 = tmp
        end
        if coinflip
          tmp = @args.state.gem0
          @args.state.gem0 = @args.state.gem2
          @args.state.gem2 = tmp
        end
      end
    
      def initialize args
        @args = args
        @args.state.animticks = 0
        @args.state.score = 0
        @args.state.gem_chosen = false
        @args.state.round_finished = false
        @args.state.gem0_x = 197
        @args.state.gem0_y = 720-274
        @args.state.gem1_x = 623
        @args.state.gem1_y = 720-274
        @args.state.gem2_x = 1049
        @args.state.gem2_y = 720-274
        @args.state.hero_sprite = "sprites/herodown100.png"
        @args.state.hero_x = 608
        @args.state.hero_y = 720-656
        @args.state.hero.sprite ||= []
        set_gem_values
      end
    
      def render_gem_value x, y, gem
        if @args.state.gem_chosen
          @args.outputs.labels << [ x, y + 96, gem.to_s, 1, 1, *text_color, text_font ]
        end
      end
    
      def render
        gemsprite = ((@args.state.animticks % 400) < 200) ? 'sprites/gem200.png' : 'sprites/gem400.png'
        @args.outputs.background_color = [ 0, 0, 0, 255 ]
        @args.outputs.sprites << [608, 720-150, 64, 64, 'sprites/oldman.png']
        @args.outputs.sprites << [300, 720-150, 64, 64, 'sprites/fire.png']
        @args.outputs.sprites << [900, 720-150, 64, 64, 'sprites/fire.png']
        @args.outputs.sprites << [@args.state.gem0_x, @args.state.gem0_y, 32, 64, gemsprite]
        @args.outputs.sprites << [@args.state.gem1_x, @args.state.gem1_y, 32, 64, gemsprite]
        @args.outputs.sprites << [@args.state.gem2_x, @args.state.gem2_y, 32, 64, gemsprite]
        @args.outputs.sprites << [@args.state.hero_x, @args.state.hero_y, 64, 64, @args.state.hero_sprite]
    
        @args.outputs.labels << [ 630, 720-30, "IT'S A SECRET TO EVERYONE.", 1, 1, *text_color, text_font ]
        @args.outputs.labels << [ 50, 720-85, @args.state.score.to_s, 1, 1, *text_color, text_font ]
        render_gem_value @args.state.gem0_x, @args.state.gem0_y, @args.state.gem0
        render_gem_value @args.state.gem1_x, @args.state.gem1_y, @args.state.gem1
        render_gem_value @args.state.gem2_x, @args.state.gem2_y, @args.state.gem2
      end
    
      def calc
        @args.state.animticks += 16
    
        return unless @args.state.gem_chosen
        @args.state.round_finished_debounce ||= 60 * 3
        @args.state.round_finished_debounce -= 1
        return if @args.state.round_finished_debounce > 0
    
        @args.state.gem_chosen = false
        @args.state.hero.sprite[0] = 'sprites/herodown100.png'
        @args.state.hero.sprite[1] = 608
        @args.state.hero.sprite[2] = 656
        @args.state.round_finished_debounce = nil
        set_gem_values
      end
    
      def walk xdir, ydir, anim
        @args.state.hero_sprite = "sprites/#{anim}#{(((@args.state.animticks % 200) < 100) ? '100' : '200')}.png"
        @args.state.hero_x += 5 * xdir
        @args.state.hero_y += 5 * ydir
      end
    
      def check_gem_touching gem_x, gem_y, gem
        return if @args.state.gem_chosen
        herorect = [ @args.state.hero_x, @args.state.hero_y, 64, 64 ]
        return if !herorect.intersect_rect?([gem_x, gem_y, 32, 64])
        @args.state.gem_chosen = true
        @args.state.score += gem
        @args.outputs.sounds << ((gem < 0) ? 'sounds/lose.wav' : 'sounds/win.wav')
      end
    
      def input
        if @args.inputs.keyboard.key_held.left
          walk(-1.0, 0.0, 'heroleft')
        elsif @args.inputs.keyboard.key_held.right
          walk(1.0, 0.0, 'heroright')
        elsif @args.inputs.keyboard.key_held.up
          walk(0.0, 1.0, 'heroup')
        elsif @args.inputs.keyboard.key_held.down
          walk(0.0, -1.0, 'herodown')
        end
    
        check_gem_touching(@args.state.gem0_x, @args.state.gem0_y, @args.state.gem0)
        check_gem_touching(@args.state.gem1_x, @args.state.gem1_y, @args.state.gem1)
        check_gem_touching(@args.state.gem2_x, @args.state.gem2_y, @args.state.gem2)
      end
    
      def tick
        input
        calc
        render
      end
    end
    
    def tick args
        args.state.game ||= Game.new args
        args.state.game.args = args
        args.state.game.tick
    end
    
    

    Topdown Starting Point - main.rb link

    # ./samples/99_genre_rpg_topdown/topdown_starting_point/app/main.rb
    =begin
     APIs listing that haven't been encountered in previous sample apps:
    
     - reverse: Returns a new string with the characters from original string in reverse order.
       For example, the command "dragonruby".reverse would return the string "yburnogard".
       Reverse is not only limited to strings, but can be applied to arrays and other collections.
    
     Reminders:
    
     - HASH#intersect_rect?: Returns true or false depending on if two rectangles intersect.
    
     - args.outputs.labels: Added a hash to this collection will generate a label.
       The parameters are:
       {
         x: X,
         y: y,
         text: TEXT,
         size_px: 22 (optional),
         anchor_x: 0 (optional),
         anchor_y: 0 (optional),
         r: RED (optional),
         g: GREEN (optional),
         b: BLUE (optional),
         a: ALPHA (optional),
         font: PATH_TO_TTF (optional)
       }
    =end
    
    # This code shows a maze and uses input from the keyboard to move the user around the screen.
    # The objective is to reach the goal.
    
    # Sets values of tile size and player's movement speed
    # Also creates tile or box for player and generates map
    def tick args
      args.state.tile_size     = 80
      args.state.player_speed  = 4
      args.state.player      ||= tile(args, 7, 3, 0, 128, 180)
      generate_map args
    
      # Adds walls, goal, and player to args.outputs.solids so they appear on screen
      args.outputs.sprites << args.state.walls
      args.outputs.sprites << args.state.goal
      args.outputs.sprites << args.state.player
    
      # If player's box intersects with goal, a label is output onto the screen
      if args.state.player.intersect_rect? args.state.goal
        args.outputs.labels << { x: 30, y: 720 - 30, text: "You're a wizard Harry!!" } # 30 pixels lower than top of screen
      end
    
      move_player args, -1,  0 if args.inputs.keyboard.left # x position decreases by 1 if left key is pressed
      move_player args,  1,  0 if args.inputs.keyboard.right # x position increases by 1 if right key is pressed
      move_player args,  0,  1 if args.inputs.keyboard.up # y position increases by 1 if up is pressed
      move_player args,  0, -1 if args.inputs.keyboard.down # y position decreases by 1 if down is pressed
    end
    
    # Sets position, size, and color of the tile
    def tile args, x, y, r, g, b
      {
        x: x * args.state.tile_size, # sets definition for array using method parameters
        y: y * args.state.tile_size, # multiplying by tile_size sets x and y to correct position using pixel values
        w: args.state.tile_size,
        h: args.state.tile_size,
        path: :pixel,
        r: r,
        g: g,
        b: b
      }
    end
    
    # Creates map by adding tiles to the wall, as well as a goal (that the player needs to reach)
    def generate_map args
      return if args.state.area
    
      # Creates the area of the map. There are 9 rows running horizontally across the screen
      # and 16 columns running vertically on the screen. Any spot with a "1" is not
      # open for the player to move into (and is green), and any spot with a "0" is available
      # for the player to move in.
      args.state.area = [
        [1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1,],
        [1, 1, 1, 2, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1,], # the "2" represents the goal
        [1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1,],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1,],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,],
        [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
      ].reverse # reverses the order of the area collection
    
      # By reversing the order, the way that the area appears above is how it appears
      # on the screen in the game. If we did not reverse, the map would appear inverted.
    
      #The wall starts off with no tiles.
      args.state.walls = []
    
      # If v is 1, a green tile is added to args.state.walls.
      # If v is 2, a black tile is created as the goal.
      args.state.area.map_2d do |y, x, v|
        if    v == 1
          args.state.walls << tile(args, x, y, 0, 255, 0) # green tile
        elsif v == 2 # notice there is only one "2" above because there is only one single goal
          args.state.goal   = tile(args, x, y, 0,   0, 0) # black tile
        end
      end
    end
    
    # Allows the player to move their box around the screen
    def move_player args, vector_x, vector_y
      player = args.state.player
      next_x = player.x + vector_x * args.state.player_speed
      next_y = player.y + vector_y * args.state.player_speed
      next_position = args.state.player.merge x: next_x, y: next_y
    
      # If the player's box hits a wall, it is not able to move further in that direction
      return if next_x < 0 || (next_x + player.w) > 1280
      return if next_y < 0 || (next_y + player.h) > 720
      return if args.state.walls.any_intersect_rect? next_position
    
      # Player's box is able to move at angles (not just the four general directions) fast
      args.state.player.x = next_x
      args.state.player.y = next_y
    end
    
    

    Genre Rpg Turn Based link

    Turn Based Battle - main.rb link

    # ./samples/99_genre_rpg_turn_based/turn_based_battle/app/main.rb
    def tick args
      args.state.phase ||= :selecting_top_level_action
      args.state.potential_action ||= :attack
      args.state.currently_acting_hero_index ||= 0
      args.state.enemies ||= [
        { name: "Goblin A" },
        { name: "Goblin B" },
        { name: "Goblin C" }
      ]
    
      args.state.heroes ||= [
        { name: "Hero A" },
        { name: "Hero B" },
        { name: "Hero C" }
      ]
    
      args.state.potential_enemy_index ||= 0
    
      if args.state.phase == :selecting_top_level_action
        if args.inputs.keyboard.key_down.down
          case args.state.potential_action
          when :attack
            args.state.potential_action = :special
          when :special
            args.state.potential_action = :magic
          when :magic
            args.state.potential_action = :items
          when :items
            args.state.potential_action = :items
          end
        elsif args.inputs.keyboard.key_down.up
          case args.state.potential_action
          when :attack
            args.state.potential_action = :attack
          when :special
            args.state.potential_action = :attack
          when :magic
            args.state.potential_action = :special
          when :items
            args.state.potential_action = :magic
          end
        end
    
        if args.inputs.keyboard.key_down.enter
          args.state.selected_action = args.state.potential_action
          args.state.next_phase = :selecting_target
        end
      end
    
      if args.state.phase == :selecting_target
        if args.inputs.keyboard.key_down.left
          select_previous_live_enemy args
        elsif args.inputs.keyboard.key_down.right
          select_next_live_enemy args
        end
    
        args.state.potential_enemy_index = args.state.potential_enemy_index.clamp(0, args.state.enemies.length - 1)
    
        if args.inputs.keyboard.key_down.enter
          args.state.enemies[args.state.potential_enemy_index].dead = true
          args.state.potential_enemy_index = args.state.enemies.find_index { |e| !e.dead }
          args.state.selected_action = nil
          args.state.potential_action = :attack
          args.state.next_phase = :selecting_top_level_action
          args.state.currently_acting_hero_index += 1
          if args.state.currently_acting_hero_index >= args.state.heroes.length
            args.state.currently_acting_hero_index = 0
          end
        end
      end
    
      if args.state.next_phase
        args.state.phase = args.state.next_phase
        args.state.next_phase = nil
      end
    
      render_actions_menu args
      render_enemies args
      render_heroes args
      render_hero_statuses args
    end
    
    def select_next_live_enemy args
      next_target_index = args.state.enemies.find_index.with_index { |e, i| !e.dead && i > args.state.potential_enemy_index }
      if next_target_index
        args.state.potential_enemy_index = next_target_index
      end
    end
    
    def select_previous_live_enemy args
      args.state.potential_enemy_index -= 1
      if args.state.potential_enemy_index < 0
        args.state.potential_enemy_index = 0
      elsif args.state.enemies[args.state.potential_enemy_index].dead
        select_previous_live_enemy args
      end
    end
    
    def render_actions_menu args
      args.outputs.borders << args.layout.rect(row:  8, col: 0, w: 4, h: 4, include_row_gutter: true, include_col_gutter: true)
      if !args.state.selected_action
        selected_rect = if args.state.potential_action == :attack
                          args.layout.rect(row:  8, col: 0, w: 4, h: 1)
                        elsif args.state.potential_action == :special
                          args.layout.rect(row:  9, col: 0, w: 4, h: 1)
                        elsif args.state.potential_action == :magic
                          args.layout.rect(row: 10, col: 0, w: 4, h: 1)
                        elsif args.state.potential_action == :items
                          args.layout.rect(row: 11, col: 0, w: 4, h: 1)
                        end
    
        args.outputs.solids  << selected_rect.merge(r: 200, g: 200, b: 200)
      end
    
      args.outputs.borders << args.layout.rect(row:  8, col: 0, w: 4, h: 1)
      args.outputs.labels  << args.layout.rect(row:  8, col: 0, w: 4, h: 1).center.merge(text: "Attack", vertical_alignment_enum: 1, alignment_enum: 1)
    
      args.outputs.borders << args.layout.rect(row:  9, col: 0, w: 4, h: 1)
      args.outputs.labels  << args.layout.rect(row:  9, col: 0, w: 4, h: 1).center.merge(text: "Special", vertical_alignment_enum: 1, alignment_enum: 1)
    
      args.outputs.borders << args.layout.rect(row: 10, col: 0, w: 4, h: 1)
      args.outputs.labels  << args.layout.rect(row: 10, col: 0, w: 4, h: 1).center.merge(text: "Magic", vertical_alignment_enum: 1, alignment_enum: 1)
    
      args.outputs.borders << args.layout.rect(row: 11, col: 0, w: 4, h: 1)
      args.outputs.labels  << args.layout.rect(row: 11, col: 0, w: 4, h: 1).center.merge(text: "Items", vertical_alignment_enum: 1, alignment_enum: 1)
    end
    
    def render_enemies args
      args.outputs.primitives << args.state.enemies.map_with_index do |e, i|
        if e.dead
          nil
        elsif i == args.state.potential_enemy_index && args.state.phase == :selecting_target
          [
            args.layout.rect(row: 1, col: 9 + i * 2, w: 2, h: 2).solid!(r: 200, g: 200, b: 200),
            args.layout.rect(row: 1, col: 9 + i * 2, w: 2, h: 2).border!,
            args.layout.rect(row: 1, col: 9 + i * 2, w: 2, h: 2).center.label!(text: "#{e.name}", vertical_alignment_enum: 1, alignment_enum: 1)
          ]
        else
          [
            args.layout.rect(row: 1, col: 9 + i * 2, w: 2, h: 2).border!,
            args.layout.rect(row: 1, col: 9 + i * 2, w: 2, h: 2).center.label!(text: "#{e.name}", vertical_alignment_enum: 1, alignment_enum: 1)
          ]
        end
      end
    end
    
    def render_heroes args
      args.outputs.primitives << args.state.heroes.map_with_index do |h, i|
        if i == args.state.currently_acting_hero_index
          [
            args.layout.rect(row: 5, col: 9 + i * 2, w: 2, h: 2).solid!(r: 200, g: 200, b: 200),
            args.layout.rect(row: 5, col: 9 + i * 2, w: 2, h: 2).border!,
            args.layout.rect(row: 5, col: 9 + i * 2, w: 2, h: 2).center.label!(text: "#{h.name}", vertical_alignment_enum: 1, alignment_enum: 1)
          ]
        else
          [
            args.layout.rect(row: 5, col: 9 + i * 2, w: 2, h: 2).border!,
            args.layout.rect(row: 5, col: 9 + i * 2, w: 2, h: 2).center.label!(text: "#{h.name}", vertical_alignment_enum: 1, alignment_enum: 1)
          ]
        end
      end
    end
    
    def render_hero_statuses args
      args.outputs.borders << args.layout.rect(row: 8, col: 4, w: 20, h: 4, include_col_gutter: true, include_row_gutter: true)
    end
    
    $gtk.reset
    
    

    Genre Simulation link

    Sand main.rb link

    # ./samples/99_genre_simulation/sand_simulation/app/main.rb
    class Elements
      def initialize size
        @size = size
        @max_x_ordinal = 1280.idiv size
        @element_lookup = {}
        @elements = []
      end
    
      def add_element x_ordinal, y_ordinal
        return nil if @element_lookup.dig x_ordinal, y_ordinal
        element = Element.new x_ordinal, y_ordinal, @size
        @elements << element
        rehash_elements
        element
      end
    
      def tick
        fn.each_send @elements, self, :move_element
        rehash_elements
      end
    
      def move_element element
        if below_empty?(element) && element.y_ordinal != 0
          element.move  0, -1
        elsif below_left_empty?(element) && element.y_ordinal != 0 && element.x_ordinal != 0
          element.move -1, -1
        elsif below_right_empty?(element) && element.y_ordinal != 0 && element.x_ordinal != @max_x_ordinal
          element.move  1, -1
        end
      end
    
      def element_count
        @elements.length
      end
    
      def rehash_elements
        @element_lookup.clear
        fn.each_send @elements, self, :rehash_element
      end
    
      def rehash_element element
        @element_lookup[element.x_ordinal] ||= {}
        @element_lookup[element.x_ordinal][element.y_ordinal] = element
      end
    
      def below_empty? e
        return false if e.y_ordinal == 0
        return true  if !@element_lookup[e.x_ordinal]
        return true  if !@element_lookup[e.x_ordinal][e.y_ordinal - 1]
        return false if  @element_lookup[e.x_ordinal][e.y_ordinal - 1]
        return true
      end
    
      def below_left_empty? e
        return false if e.y_ordinal == 0
        return false if e.x_ordinal == 0
        return true  if !@element_lookup[e.x_ordinal - 1]
        return true  if !@element_lookup[e.x_ordinal - 1][e.y_ordinal - 1]
        return false if  @element_lookup[e.x_ordinal - 1][e.y_ordinal - 1]
        return true
      end
    
      def below_right_empty? e
        return false if e.y_ordinal == 0
        return false if e.x_ordinal == 256
        return true  if !@element_lookup[e.x_ordinal + 1]
        return true  if !@element_lookup[e.x_ordinal + 1][e.y_ordinal - 1]
        return false if  @element_lookup[e.x_ordinal + 1][e.y_ordinal - 1]
        return true
      end
    end
    
    class Element
      attr_sprite
      attr :x_ordinal, :y_ordinal
    
      def initialize x_ordinal, y_ordinal, s
        @x_ordinal     = x_ordinal
        @y_ordinal     = y_ordinal
        @s             = s
        @x             = x_ordinal * s
        @y             = y_ordinal * s
        @w             = s
        @h             = s
        @path          = "sprites/sand-element.png"
      end
    
      def draw_override ffi
        ffi.draw_sprite @x, @y, @w, @h, @path
      end
    
      def move dx, dy
        @y_ordinal += dy
        @x_ordinal += dx
        @y = @y_ordinal * @s
        @x = @x_ordinal * @s
      end
    end
    
    def tick args
      args.state.size        ||= 10
      args.state.mouse_state ||= :up
      @elements              ||= Elements.new args.state.size
    
      if args.inputs.mouse.down
        args.state.mouse_state = :held
      elsif args.inputs.mouse.up
        args.state.mouse_state = :released
      end
    
      if args.state.mouse_state == :held
        added = @elements.add_element args.inputs.mouse.x.idiv(args.state.size), args.inputs.mouse.y.idiv(args.state.size)
        args.outputs.static_sprites << added if added
      end
    
      @elements.tick
    
      args.outputs.labels << { x: 30, y: 30.from_top, text: "#{args.gtk.current_framerate.to_sf}" }
      args.outputs.labels << { x: 30, y: 60.from_top, text: "#{@elements.element_count}" }
    end
    
    $gtk.reset
    @elements = nil
    
    

    Genre Twenty Second Games link

    Twenty Second Starting Point - main.rb link

    # ./samples/99_genre_twenty_second_games/twenty_second_starting_point/app/main.rb
    # full documenation is at http://docs.dragonruby.org
    # be sure to come to the discord if you hit any snags: http://discord.dragonruby.org
    def tick args
      # ====================================================
      # initialize default variables
      # ====================================================
    
      # ruby has an operator called ||= which means "only initialize this if it's nil"
      args.state.count_down   ||= 20 * 60 # set the count down to 20 seconds
      # set the initial position of the target
      args.state.target       ||= { x: args.grid.w.half,
                                    y: args.grid.h.half,
                                    w: 20,
                                    h: 20 }
    
      # set the initial position of the player
      args.state.player       ||= { x: 50,
                                    y: 50,
                                    w: 20,
                                    h: 20 }
    
      # set the player movement speed
      args.state.player_speed ||= 5
    
      # set the score
      args.state.score        ||= 0
      args.state.teleports    ||= 3
    
      # set the instructions
      args.state.instructions ||= "Get to the red goal! Use arrow keys to move. Spacebar to teleport (use them carefully)!"
    
      # ====================================================
      # render the game
      # ====================================================
      args.outputs.labels  << { x: args.grid.w.half, y: args.grid.h - 10,
                                text: args.state.instructions,
                                alignment_enum: 1 }
    
      # check if it's game over. if so, then render game over
      # otherwise render the current time left
      if game_over? args
        args.outputs.labels  << { x: args.grid.w.half,
                                  y: args.grid.h - 40,
                                  text: "game over! (press r to start over)",
                                  alignment_enum: 1 }
      else
        args.outputs.labels  << { x: args.grid.w.half,
                                  y: args.grid.h - 40,
                                  text: "time left: #{(args.state.count_down.idiv 60) + 1}",
                                  alignment_enum: 1 }
      end
    
      # render the score
      args.outputs.labels  << { x: args.grid.w.half,
                                y: args.grid.h - 70,
                                text: "score: #{args.state.score}",
                                alignment_enum: 1 }
    
      # render the player with teleport count
      args.outputs.sprites << { x: args.state.player.x,
                                y: args.state.player.y,
                                w: args.state.player.w,
                                h: args.state.player.h,
                                path: 'sprites/square-green.png' }
    
      args.outputs.labels << { x: args.state.player.x + 10,
                               y: args.state.player.y + 40,
                               text: "teleports: #{args.state.teleports}",
                               alignment_enum: 1, size_enum: -2 }
    
      # render the target
      args.outputs.sprites << { x: args.state.target.x,
                                y: args.state.target.y,
                                w: args.state.target.w,
                                h: args.state.target.h,
                                path: 'sprites/square-red.png' }
    
      # ====================================================
      # run simulation
      # ====================================================
    
      # count down calculation
      args.state.count_down -= 1
      args.state.count_down = -1 if args.state.count_down < -1
    
      # ====================================================
      # process player input
      # ====================================================
      # if it isn't game over let them move
      if !game_over? args
        dir_y = 0
        dir_x = 0
    
        # determine the change horizontally
        if args.inputs.keyboard.up
          dir_y += args.state.player_speed
        elsif args.inputs.keyboard.down
          dir_y -= args.state.player_speed
        end
    
        # determine the change vertically
        if args.inputs.keyboard.left
          dir_x -= args.state.player_speed
        elsif args.inputs.keyboard.right
          dir_x += args.state.player_speed
        end
    
        # determine if teleport can be used
        if args.inputs.keyboard.key_down.space && args.state.teleports > 0
          args.state.teleports -= 1
          dir_x *= 20
          dir_y *= 20
        end
    
        # apply change to player
        args.state.player.x += dir_x
        args.state.player.y += dir_y
      else
        # if r is pressed, reset the game
        if args.inputs.keyboard.key_down.r
          $gtk.reset
          return
        end
      end
    
      # ====================================================
      # determine score
      # ====================================================
    
      # calculate new score if the player is at goal
      if !game_over? args
    
        # if the player is at the goal, then move the goal
        if args.state.player.intersect_rect? args.state.target
          # increment the goal
          args.state.score += 1
    
          # move the goal to a random location
          args.state.target = { x: (rand args.grid.w), y: (rand args.grid.h), w: 20, h: 20 }
    
          # make sure the goal is inside the view area
          if args.state.target.x < 0
            args.state.target.x += 20
          elsif args.state.target.x > 1280
            args.state.target.x -= 20
          end
    
          # make sure the goal is inside the view area
          if args.state.target.y < 0
            args.state.target.y += 20
          elsif args.state.target.y > 720
            args.state.target.y -= 20
          end
        end
      end
    end
    
    def game_over? args
      args.state.count_down < 0
    end
    
    $gtk.reset