• Docs and Samples
  • Docs
  • Samples
  • Table Of Contents

    Welcome link

    Welcome to DragonRuby Game Toolkit!

    The information contained here is all available in your the zip file at ./docs/docs.html. You can browse the docs in a local website by starting up DragonRuby and going to http://localhost:9001.

    Tips for Learning DragonRuby Game Toolkit link

    The following tips will help you learn the DragonRuby quickly.

    Tip #1: Join the Community link

    Our Discord server is extremely supportive and helpful. It's the best place to get answers to your questions. The developers of DragonRuby are also on this server if you have any feedback or bug reports.

    The Link to Our Discord Server is: http://discord.dragonruby.org.

    The News Letter will keep you in the loop with regards to current DragonRuby Events: http://dragonrubydispatch.com.

    Tip #2: Read the Book link

    Brett Chalupa (one of our community members) has written a book to help you get started: https://book.dragonriders.community/

    Tip #3: Watch the Tutorial Video link

    Here are some videos to help you get the lay of the land.

    1. Building Tetris - Part 1: https://youtu.be/xZMwRSbC4rY
    2. Building Tetris - Part 2: https://youtu.be/C3LLzDUDgz4

    Tip #4: Go Through the Sample Apps in Order link

    The sample apps are located in the ./samples directory. The samples are ordered by increasing difficulty and cover all aspects of the game engine.

    Getting Started Tutorial link

    This is a tutorial written by Ryan C Gordon (a Juggernaut in the industry who has contracted to Valve, Epic, Activision, and EA... check out his Wikipedia page: https://en.wikipedia.org/wiki/Ryan_C._Gordon).

    Introduction link

    Welcome!

    Here's just a little push to get you started if you're new to programming or game development.

    If you want to write a game, it's no different than writing any other program for any other framework: there are a few simple rules that might be new to you, but more or less programming is programming no matter what you are building.

    Did you not know that? Did you think you couldn't write a game because you're a "web guy" or you're writing Java at a desk job? Stop letting people tell you that you can't, because you already have everything you need.

    Here, we're going to be programming in a language called "Ruby." In the interest of full disclosure, I (Ryan Gordon) wrote the C parts of this toolkit and Ruby looks a little strange to me (Amir Rajan wrote the Ruby parts, discounting the parts I mangled), but I'm going to walk you through the basics because we're all learning together, and if you mostly think of yourself as someone that writes C (or C++, C#, Objective-C), PHP, or Java, then you're only a step behind me right now.

    Prerequisites link

    Here's the most important thing you should know: Ruby lets you do some complicated things really easily, and you can learn that stuff later. I'm going to show you one or two cool tricks, but that's all.

    Do you know what an if statement is? A for-loop? An array? That's all you'll need to start.

    The Game Loop link

    Ok, here are few rules with regards to game development with GTK:

    That's an entire video game in one run-on sentence.

    Here's that function. You're going to want to put this in mygame/app/main.rb, because that's where we'll look for it by default. Load it up in your favorite text editor.

    def tick args
      args.outputs.labels << [580, 400, 'Hello World!']
    end
    

    Now run dragonruby ...did you get a window with "Hello World!" written in it? Good, you're officially a game developer!

    Breakdown Of The tick Method link

    mygame/app/main.rb, is where the Ruby source code is located. This looks a little strange, so I'll break it down line by line. In Ruby, a '#' character starts a single-line comment, so I'll talk about this inline.

    # This "def"ines a function, named "tick," which takes a single argument
    # named "args". DragonRuby looks for this function and calls it every
    # frame, 60 times a second. "args" is a magic structure with lots of
    # information in it. You can set variables in there for your own game state,
    # and every frame it will updated if keys are pressed, joysticks moved,
    # mice clicked, etc.
    def tick args
    
      # One of the things in "args" is the "outputs" object that your game uses
      # to draw things. Afraid of rendering APIs? No problem. In DragonRuby,
      # you use arrays to draw things and we figure out the details.
      # If you want to draw text on the screen, you give it an array (the thing
      # in the [ brackets ]), with an X and Y coordinate and the text to draw.
      # The "<<" thing says "append this hash onto the list of them at
      # args.outputs.labels)
      args.outputs.labels << { x: 580, y: 400, text: 'Hello World!' }
    end
    

    Once your tick function finishes, we look at all the arrays you made and figure out how to draw it. You don't need to know about graphics APIs. You're just setting up some arrays! DragonRuby clears out these arrays every frame, so you just need to add what you need _right now_ each time.

    Rendering A Sprite link

    Now let's spice this up a little.

    We're going to add some graphics. Each 2D image in DragonRuby is called a "sprite," and to use them, you just make sure they exist in a reasonable file format (png, jpg, gif, bmp, etc) and specify them by filename. The first time you use one, DragonRuby will load it and keep it in video memory for fast access in the future. If you use a filename that doesn't exist, you get a fun checkerboard pattern!

    There's a "dragonruby.png" file included, just to get you started. Let's have it draw every frame with our text:

    def tick args
      args.outputs.labels  << { x: 580, y: 400, text: 'Hello World!' }
      args.outputs.sprites << { x: 576, y: 100, w: 128, h: 101, path: 'dragonruby.png' }
    end
    

    (Pro Tip: you don't have to restart DragonRuby to test your changes; when you save main.rb, DragonRuby will notice and reload your program.)

    That .sprites line says "add a sprite to the list of sprites we're drawing, and draw it at position (576, 100) at a size of 128x101 pixels". You can find the image to draw at dragonruby.png.

    Coordinate System and Virtual Canvas link

    Quick note about coordinates: (0, 0) is the bottom left corner of the screen, and positive numbers go up and to the right. This is more "geometrically correct," even if it's not how you remember doing 2D graphics, but we chose this for a simpler reason: when you're making Super Mario Brothers and you want Mario to jump, you should be able to add to Mario's y position as he goes up and subtract as he falls. It makes things easier to understand.

    Also: your game screen is _always_ 1280x720 pixels. If you resize the window, we will scale and letterbox everything appropriately, so you never have to worry about different resolutions.

    Ok, now we have an image on the screen, let's animate it:

    def tick args
      args.state.rotation  ||= 0
    
      args.state.rotation  -= 1
    
      args.outputs.labels  << { x: 580, y: 400, text: 'Hello World!' }
      args.outputs.sprites << { x: 576,
                                y: 100,
                                w: 128,
                                h: 101,
                                path: 'dragonruby.png',
                                angle: args.state.rotation }
    end
    

    Now you can see that this function is getting called a lot!

    Game State link

    Here's a fun Ruby thing: args.state.rotation ||= 0 is shorthand for "if args.state.rotation isn't initialized, set it to zero." It's a nice way to embed your initialization code right next to where you need the variable.

    args.state is a place you can hang your own data. It's an open data structure that allows you to define properties that are arbitrarily nested. You don't need to define any kind of class.

    In this case, the current rotation of our sprite, which is happily spinning at 60 frames per second. If you don't specify rotation (or alpha, or color modulation, or a source rectangle, etc), DragonRuby picks a reasonable default, and the array is ordered by the most likely things you need to tell us: position, size, name.

    There Is No Delta Time link

    One thing we decided to do in DragonRuby is not make you worry about delta time: your function runs at 60 frames per second (about 16 milliseconds) and that's that. Having to worry about framerate is something massive triple-AAA games do, but for fun little 2D games? You'd have to work really hard to not hit 60fps. All your drawing is happening on a GPU designed to run Fortnite quickly; it can definitely handle this.

    Since we didn't make you worry about delta time, you can just move the rotation by 1 every time and it works without you having to keep track of time and math. Want it to move faster? Subtract 2.

    Handling User Input link

    Now, let's move that image around.

    def tick args
      args.state.rotation ||= 0
      args.state.x ||= 576
      args.state.y ||= 100
    
      if args.inputs.mouse.click
        args.state.x = args.inputs.mouse.x - 64
        args.state.y = args.inputs.mouse.y - 50
      end
    
      args.outputs.labels  << { x: 580, y: 400, text: 'Hello World!' }
      args.outputs.sprites << { x: args.state.x,
                                y: args.state.y,
                                w: 128,
                                h: 101,
                                path: 'dragonruby.png',
                                angle: args.state.rotation }
    
      args.state.rotation -= 1
    end
    

    Everywhere you click your mouse, the image moves there. We set a default location for it with args.state.x ||= 576, and then we change those variables when we see the mouse button in action. You can get at the keyboard and game controllers in similar ways.

    Coding On A Raspberry Pi link

    We have only tested DragonRuby on a Raspberry Pi 3, Models B and B+, but we believe it _should_ work on any model with comparable specs.

    If you're running DragonRuby Game Toolkit on a Raspberry Pi, or trying to run a game made with the Toolkit on a Raspberry Pi, and it's really really slow-- like one frame every few seconds--then there's likely a simple fix.

    You're probably running a desktop environment: menus, apps, web browsers, etc. This is okay! Launch the terminal app and type:

    sudo raspi-config
    

    It'll ask you for your password (if you don't know, try "raspberry"), and then give you a menu of options. Find your way to "Advanced Options", then "GL Driver", and change this to "GL (Full KMS)" ... not "fake KMS," which is also listed there. Save and reboot. In theory, this should fix the problem.

    If you're _still_ having problems and have a Raspberry Pi 2 or better, go back to raspi-config and head over to "Advanced Options", "Memory split," and give the GPU 256 megabytes. You might be able to avoid this for simple games, as this takes RAM away from the system and reserves it for graphics. You can also try 128 megabytes as a gentler option.

    Note that you can also run DragonRuby without X11 at all: if you run it from a virtual terminal it will render fullscreen and won't need the "Full KMS" option. This might be attractive if you want to use it as a game console sort of thing, or develop over ssh, or launch it from RetroPie, etc.

    Conclusion link

    There is a lot more you can do with DragonRuby, but now you've already got just about everything you need to make a simple game. After all, even the most fancy games are just creating objects and moving them around. Experiment a little. Add a few more things and have them interact in small ways. Want something to go away? Just don't add it to args.output anymore.

    Starting a New DragonRuby Project link

    The DragonRuby zip that contains the engine is a complete, self contained project structure. To create a new project, unzip the zip file again in its entirety and use that as a starting point for another game. This is the recommended approach to starting a new project.

    The DragonRuby binary/package is designed to be committed in its entirety with your source code (it’s why we keep it small). This protects the “shelf life” for commercial games. 3 years from now, we might be on a vastly different version of the engine. But you know that the code you’ve written will definitely work with the version that was committed to source control.

    It's strongly recommended that you do NOT keep DragonRuby Game Toolkit in a shared location and instead unzip a clean copy for every game (and commit everything to source control).

    File access functions are sandoxed and assume that the dragonruby binary lives alongside the game you are building. Do not expect file access functions to return correct values if you are attempting to run the dragonruby binary from a shared location. It's recommended that the directory structure contained in the zip is not altered and games are built using that starting directory structure.

    Considerations For Public Git Repositories link

    You can open source your game's code given the following options.

    Option 1 (Recommended) link

    Your public repository needs only to contain the contents of ./mygame. This approach is the cleanest and doesn't require your .gitignore to be polluted with DragonRuby specific files.

    Option 2 (Restrictions Apply) link

    IMPORTANT: Do NOT commit dragonruby-publish(.exe), or dragonruby-bind(.exe).

    dragonruby
    dragonruby.exe
    dragonruby-publish
    dragonruby-publish.exe
    dragonruby-bind
    dragonruby-bind.exe
    /tmp/
    /builds/
    /logs/
    /samples/
    /docs/
    /.dragonruby/
    

    If you'd like people who do not own a DragonRuby license to run your game, you may include the dragonruby(.exe) binary within the repo. This permission is granted in good-faith and can be revoked if abused.

    Considerations For Private Git Repos link

    The following .gitignore should be used for private repositories (commercial games).

    /tmp/
    /logs/
    

    You'll notice that everything else is committed to source control (even the ./samples, ./docs, and ./builds directory).

    Deploying To Itch.io link

    Once you've built your game, you're all set to deploy! Good luck in your game dev journey and if you get stuck, come to the Discord channel!

    Creating Your Game Landing Page link

    Log into Itch.io and go to https://itch.io/game/new.

    You can fill out all the other options later.

    Update Your Game's Metadata link

    Point your text editor at mygame/metadata/game_metadata.txt and make it look like this:

    NOTE: Remove the # at the beginning of each line.

    devid=bob
    devtitle=Bob The Game Developer
    gameid=mygame
    gametitle=My Game
    version=0.1
    

    The devid property is the username you use to log into Itch.io. The devtitle is your name or company name (it can contain spaces). The gameid is the Project URL value. The gametitle is the name of your game (it can contain spaces). The version can be any major.minor number format.

    Building Your Game For Distribution link

    Open up the terminal and run this from the command line:

    ./dragonruby-publish --only-package mygame
    

    (if you're on Windows, don't put the "./" on the front. That's a Mac and Linux thing.)

    A directory called ./build will be created that contains your binaries. You can upload this to Itch.io manually.

    Browser Game Settings link

    For the HTML version of your game, the following configuration is required for your game to run correctly:

    For subsequent updates you can use an automated deployment to Itch.io:

    ./dragonruby-publish mygame
    

    DragonRuby will package _and publish_ your game to itch.io! Tell your friends to go to your game's very own webpage and buy it!

    If you make changes to your game, just re-run dragonruby-publish and it'll update the downloads for you.

    Consider Adding Pause When Game is In Background link

    It's a good idea to pause the game if it doesn't have focus. Here's an example of how to do that

    def tick args
      # if the keyboard doesn't have focus, and the game is in production mode, and it isn't the first tick
      if !args.inputs.keyboard.has_focus && args.gtk.production && args.state.tick_count != 0
        args.outputs.background_color = [0, 0, 0]
        args.outputs.labels << { x: 640,
                                 y: 360,
                                 text: "Game Paused (click to resume).",
                                 alignment_enum: 1,
                                 r: 255, g: 255, b: 255 }
        # consider setting all audio volume to 0.0
      else
        # perform your regular tick function
      end
    end
    

    If you want your game to run at full speed even when it's in the background, add the following line to mygame/metadata/cvars.txt:

    renderer.background_sleep=0
    

    Consider Adding a Request to Review Your Game In-Game link

    Getting reviews of your game are extremely important and it's recommended that you put an option to review within the game itself. You can use args.gtk.open_url plus a review URL. Here's an example:

    def tick args
      # render the review button
      args.state.review_button ||= { x: 640 - 50,
                                     y: 360 - 25,
                                     w: 100,
                                     h: 50,
                                     path: :pixel,
                                     r: 0,
                                     g: 0,
                                     b: 0 }
      args.outputs.sprites << args.state.review_button
      args.outputs.labels << { x: 640, y: 360, anchor_x: 0.5, anchor_y: 0.5, text: "Review" }
    
      # check to see if the review button was clicked
      if args.inputs.mouse.intersect_rect? args.state.review_button
        # open platform specific review urls
        if args.gtk.platform? :ios
          # your app id is provided at Apple's Developer Portal (numeric value)
          args.gtk.openurl "itms-apps://itunes.apple.com/app/idYOURGAMEID?action=write-review"
        elsif args.gtk.platform? :android
          # your app id is the name of your android package
          args.gtk.openurl "https://play.google.com/store/apps/details?id=YOURGAMEID"
        elsif args.gtk.platform? :web
          # if they are playing the web version of the game, take them to the purchase page on itch
          args.gtk.openurl "https://amirrajan.itch.io/YOURGAMEID/purchase"
        else
          # if they are playing the desktop version of the game, take them to itch's rating page
          args.gtk.openurl "https://amirrajan.itch.io/YOURGAMEID/rate?source=game"
        end
      end
    end
    

    Deploying To Mobile Devices link

    If you have a Pro subscription, you also have the capability to deploy to mobile devices.

    Deploying to iOS link

    To deploy to iOS, you need to have a Mac running MacOS Catalina, an iOS device, and an active/paid Developer Account with Apple. From the Console type: $wizards.ios.start and you will be guided through the deployment process.

    Deploying to Android link

    To deploy to Android, you need to have an Android emulator/device, and an environment that is able to run Android SDK. dragonruby-publish will create an APK for you. From there, you can sign the APK and install it to your device. The signing and installation procedure varies from OS to OS. Here's an example of what the command might look like:

    # generating a keystore
    keytool -genkey -v -keystore APP.keystore -alias mygame -keyalg RSA -keysize 2048 -validity 10000
    
    # deploying to a local device/emulator
    apksigner sign --min-sdk-version 21 --ks ./profiles/mygame.keystore ./builds/APP-android.apk
    adb install ./builds/APP-android.apk
    # read logs of device
    adb logcat -e mygame
    
    # signing for Google Play
    apksigner sign --min-sdk-version 33 --ks ./profiles/APP.keystore ./builds/APP-googleplay.aab
    

    Deploying To Steam link

    If you have a Indie or Pro subscription, you also get streamlined deployment to Steam via dragonruby-publish. Please note that games developed using the Standard license can deploy to Steam using the Steamworks toolchain https://partner.steamgames.com/doc/store/releasing.

    Testing on Your Steam Deck link

    Easy Setup link

    1. Run dragonruby-publish --only-package.
    2. Find the Linux build of your game under the ./builds directory and load it onto an SD Card.
    3. Restart the Steam Deck in Desktop Mode.
    4. Copy your game binary onto an SD card.
    5. Find the game on the SD card and double click binary.

    Advanced Setup link

    1. Restart the Steam Deck in Desktop Mode.
    2. Open up Konsole and set an admin password via passwd.
    3. Disable readonly mode: sudo steamos-readonly disable.
    4. Update pacman sudo pacman-key --populate archlinux.
    5. Update sshd_config sudo vim /etc/ssh/sshd_config and uncomment the PubkeyAuthentication yes line.
    6. Enable ssh: sudo systemctl enable sshd.
    7. Start ssh: sudo systemctl start sshd.
    8. Run dragonruby-publish --only-package.
    9. Use scp to copy the game over from your dev machine without needing an SD Card: scp -R ./builds/SOURCE.bin deck@IP_ADDRESS:/home/deck/Downloads

    Note: Steps 2 through 7 need only be done once.

    Note: scp comes pre-installed on Mac and Linux. You can download the tool for Windows from https://winscp.net/eng/index.php

    Setting up the game on the Partner Site link

    Getting your App ID link

    You'll need to create a product on Steam. This is unfortunately manual and requires identity verification for taxation purposes. Valve offers pretty robust documentation on all this, though. Eventually, you'll have an App ID for your game.

    Go to https://partner.steamgames.com/apps/view/$APPID, where $APPID is your game's App ID.

    Specifing Supported Operating Systems for your game link

    Find the "Supported Operating Systems" section and make sure these things are checked:

    Click the "Save" button below it.

    Setting up SteamPipe Depots link

    Click the "SteamPipe" tab at the top of the page, click on "depots"

    Click the "Add a new depot" button. Give it a name like "My Game Name Linux Depot" and take whatever depot ID it offers you.

    You'll see this new depot is listed on the page now. Fix its settings:

    Do this again, make a "My Game Name Windows Depot", set it to the same things, except "Operating System," which should be "Windows," of course.

    Do this again, make a "My Game Name Mac Depot", set it to the same things, except "Operating System," which should be "macOS," of course.

    Push the big green "Save" button on the page. Now we have a place to upload platform-specific builds of your game.

    Setting up Launch Options link

    Click on the "Installation" tab near the top of the page, then "General Installation".

    Under "Launch Options," click the "Add new launch option" button, edit the new section that just popped up, and set it like this:

    (Whenever you see "mygamename" in here, this should be whatever your game_metadata's "gameid" value is set to. If you see "My Game Name", it's whatever your game_metadata's "gametitle" value is set to, but you'll have to check in case we mangled it to work as a filename.)

    Click the "Update" button on that section.

    Add another launch option, as before:

    Add another launch option, as before:

    Publish Changes link

    Go to the "Publish" tab at near the top of the page. Click the "View Diffs" button and make sure it looks sane (it should just be the things we've changed in here), then click "Prepare for Publishing", then "Publish to Steam" and follow the instructions to publish these changes.

    Go to https://partner.steamgames.com/apps/associated/$APPID For each package, make sure all three depots are included.

    Configuring dragonruby-publish link

    You only have to do this part once when first setting up your game. Note that this capability is only available for Indie and Pro license tiers. If you have a Standard DragonRuby License, you'll need to use the Steamworks toolchains directly.

    Go add a text file to your game's metadata directory called steam_metadata.txt ... note that this file will be filtered out dragonruby-publish packages the game and will not be distributed with the published game.

    steam.publish=true
    steam.branch=public
    steam.username=AAA
    steam.appid=BBB
    steam.linux_depotid=CCC
    steam.windows_depotid=DDD
    steam.mac_depotid=EEE
    

    If steam.publish is set to false then dragonruby-publish will not attempt to upload to Steam. false is the default if this file, or this setting, is missing.

    Where "AAA" is the login name on the Steamworks Partner Site to use for publishing builds, "BBB" is your game-specific AppID provided by Steam, "CCC", "DDD", and "EEE" are the DepotIDs you created for Linux, Windows, and macOS builds, respectively.

    Setting a branch live link

    Once your build is uploaded, you can assign it to a specific branch through the interface on the Partner site. You can make arbitrary branches here, like "beta" or "nightly" or "fixing-weird-bug" or whatever. The one that goes to the end users without them switching branches, is "default" and you should assume this is where paying customers live, so be careful before you set a build live there.

    You can have dragonruby-publish set the builds it publishes live on a branch immediately, if you prefer. Simply add...

    steam.branch=XXX
    

    ...to steam_metadata.txt, where "XXX" is the branch name from the partner website. If this is blank or unspecified, it will _not_ set the build live on _any_ branch. Setting the value to public will push to production.

    A reasonable strategy is to create a (possibly passworded) branch called "staging" and have dragonruby-publish always push to there automatically. Then you can test from a Steam install, pushing as often as you like, and when you are satisfied, manually set the latest build live on default for the general public to download.

    If you are feeling brave, you can always just set all published builds live on default, too. After all, if you break it, you can always just push a fix right away. :) (or use the Partner Site to roll back to a known-good build, you know.)

    Publishing Build link

    Run dragonuby-publish as you normally would. When it is time to publish to Steam, it will set up any tools it needs, attempt to log you into Steam, and upload the latest version of your game.

    Steam login is handled by Valve's steamcmd command line program, not dragonruby-publish. DragonRuby does not ever have access to your login credentials. You may need to take steps to get an authorization token in place if necessary, so you don't have to deal with Steam Guard in automated build processes (documentation on how to do this is forthcoming, or read Valve's SteamCMD manual for details).

    You (currently) have to set the new build live on the partner site before users will receive it. Optionally automating this step is coming soon!

    Questions/Need Help? link

    You probably have several. Please come visit the Discord and ask questions, and we'll do our best to help, and update this document.

    DragonRuby's Philosophy link

    The following tenants of DragonRuby are what set us apart from other game engines. Given that Game Toolkit is a relatively new engine, there are definitely features that are missing. So having a big check list of "all the cool things" is not this engine's forte. This is compensated with a strong commitment to the following principles.

    Challenge The Status Quo link

    Game engines of today are in a local maximum and don't take into consideration the challenges of this day and age. Unity and GameMaker specifically rot your brain. It's not sufficient to say:

    But that's how we've always done it.

    It's a hard pill to swallow, but forget blindly accepted best practices and try to figure out the underlying motivation for a specific approach to game development. Collaborate with us.

    Continuity of Design link

    There is a programming idiom in software called "The Pit of Success". The term normalizes upfront pain as a necessity/requirement in the hopes that the investment will yield dividends "when you become successful" or "when the code becomes more complicated". This approach to development is strongly discouraged by us. It leads to over-architected and unnecessary code; creates barriers to rapid prototyping and shipping a game; and overwhelms beginners who are new to the engine or programming in general.

    DragonRuby's philosophy is to provide multiple options across the "make it fast" vs "make it right" spectrum, with incremental/intuitive transitions between the options provided. A concrete example of this philosophy would be render primitives: the spectrum of options allows renderable constructs that take the form of tuples/arrays (easy to pickup, simple, and fast to code/prototype with), hashes (a little more work, but gives you the ability to add additional properties), open and strict entities (more work than hashes, but yields cleaner apis), and finally - if you really need full power/flexibility in rendering - classes (which take the most amount of code and programming knowledge to create).

    Release Early and Often link

    The biggest mistake game devs make is spending too much time in isolation building their game. Release something, however small, and release it soon.

    Stop worrying about everything being pixel perfect. Don't wait until your game is 100% complete. Build your game publicly and iterate. Post in the #show-and-tell channel in the community Discord. You'll find a lot of support and encouragement there.

    Real artists ship. Remember that.

    Sustainable And Ethical Monetization link

    We all aspire to put food on the table doing what we love. Whether it is building games, writing tools to support game development, or anything in between.

    Charge a fair amount of money for the things you create. It's expected and encouraged within the community. Give what you create away for free to those that can't afford it.

    If you are gainfully employed, pay full price for the things you use. If you do end up getting something at a discount, pay the difference "forward" to someone else.

    Sustainable And Ethical Open Source link

    This goes hand in hand with sustainable and ethical monetization. The current state of open source is not sustainable. There is an immense amount of contributor burnout. Users of open source expect everything to be free, and few give back. This is a problem we want to fix (we're still trying to figure out the best solution).

    So, don't be "that guy" in the Discord that says "DragonRuby should be free and open source!" You will be personally flogged by Amir.

    People Over Entities link

    We prioritize the endorsement of real people over faceless entities. This game engine, and other products we create, are not insignificant line items of a large company. And you aren't a generic "commodity" or "corporate resource". So be active in the community Discord and you'll reap the benefits as more devs use DragonRuby.

    Building A Game Should Be Fun And Bring Happiness link

    We will prioritize the removal of pain. The aesthetics of Ruby make it such a joy to work with, and we want to capture that within the engine.

    Real World Application Drives Features link

    We are bombarded by marketing speak day in and day out. We don't do that here. There are things that are really great in the engine, and things that need a lot of work. Collaborate with us so we can help you reach your goals. Ask for features you actually need as opposed to anything speculative.

    We want DragonRuby to *actually* help you build the game you want to build (as opposed to sell you something piece of demoware that doesn't work).

    Frequently Asked Questions, Comments, and Concerns link

    Here are questions, comments, and concerns that frequently come up.

    Frequently Asked Questions link

    What is DragonRuby LLP? link

    DragonRuby LLP is a partnership of four devs who came together with the goal of bringing the aesthetics and joy of Ruby, everywhere possible.

    Under DragonRuby LLP, we offer a number of products (with more on the way):

    All of the products above leverage a shared core called DragonRuby.

    NOTE: From an official branding standpoint each one of the products is suffixed with "A DragonRuby LLP Product" tagline. Also, DragonRuby is _one word, title cased_.

    NOTE: We leave the "A DragonRuby LLP Product" off of this one because that just sounds really weird.

    NOTE: Devs who use DragonRuby are "Dragon Riders/Riders of Dragons". That's a bad ass identifier huh?

    What is DragonRuby? link

    The response to this question requires a few subparts. First we need to clarify some terms. Specifically _language specification_ vs _runtime_.

    Okay... so what is the difference between a language specification and a runtime?

    A runtime is an _implementation_ of a language specification. When people say "Ruby," they are usually referring to "the Ruby 3.0+ language specification implemented via the CRuby/MRI Runtime."

    But, there are many Ruby Runtimes: CRuby/MRI, JRuby, Truffle, Rubinius, Artichoke, and (last but certainly not least) DragonRuby.

    Okay... what language specification does DragonRuby use then?

    DragonRuby's goal is to be compliant with the ISO/IEC 30170:2012 standard. It's syntax is Ruby 2.x compatible, but also contains semantic changes that help it natively interface with platform specific libraries.

    So... why another runtime?

    The elevator pitch is:

    DragonRuby is a Multilevel Cross-platform Runtime. The "multiple levels" within the runtime allows us to target platforms no other Ruby can target: PC, Mac, Linux, Raspberry Pi, WASM, iOS, Android, Nintendo Switch, PS4, Xbox, and Stadia.

    What does Multilevel Cross-platform mean?

    There are complexities associated with targeting all the platforms we support. Because of this, the runtime had to be architected in such a way that new platforms could be easily added (which lead to us partitioning the runtime internally):

    Levels 1 through 3 are fairly commonplace in many runtime implementations (with level 1 being the most portable, and level 3 being the fastest). But the DragonRuby Runtime has taken things a bit further:

    These levels allow us to stay up to date with open source implementations of Ruby; provide fast, native code execution on proprietary platforms; ensure good separation between these two worlds; and provides a means to add new platforms without going insane.

    Cool cool. So given that I understand everything to this point, can we answer the original question? What is DragonRuby?

    DragonRuby is a Ruby runtime implementation that takes all the lessons we've learned from MRI/CRuby, and merges it with the latest and greatest compiler and OSS technologies.

    How is DragonRuby different than MRI? link

    DragonRuby supports a subset of MRI apis. Our target is to support all of mRuby's standard lib. There are challenges to this given the number of platforms we are trying to support (specifically console).

    Does DragonRuby support Gems?

    DragonRuby does not support gems because that requires the installation of MRI Ruby on the developer's machine (which is a non-starter given that we want DragonRuby to be a zero dependency runtime). While this seems easy for Mac and Linux, it is much harder on Windows and Raspberry Pi. mRuby has taken the approach of having a git repository for compatible gems and we will most likely follow suite: https://github.com/mruby/mgem-list.

    Does DragonRuby have a REPL/IRB?

    You can use DragonRuby's Console within the game to inspect object and execute small pieces of code. For more complex pieces of code create a file called repl.rb and put it in mygame/app/repl.rb:

    repl do
      puts "hello world"
      puts 1 + 1
    end
    

    4. To ignore code in repl.rb, instead of commenting it out, prefix repl with the letter x and it'll be ignored.

    xrepl do # <------- line is prefixed with an "x"
      puts "hello world"
      puts 1 + 1
    end
    
    # This code will be executed when you save the file.
    repl do
      puts "Hello"
    end
    
    repl do
      puts "This code will also be executed."
    end
    
    # use xrepl to "comment out" code
    xrepl do
      puts "This code will not be executed because of the x in front of repl".
    end
    

    Does DragonRuby support pry or have any other debugging facilities?

    pry is a gem that assumes you are using the MRI Runtime (which is incompatible with DragonRuby). Eventually DragonRuby will have a pry based experience that is compatible with a debugging infrastructure called LLDB. Take the time to read about LLDB as it shows the challenges in creating something that is compatible.

    You can use DragonRuby's replay capabilities to troubleshoot:

    1. DragonRuby is hot loaded which gives you a very fast feedback loop (if the game throws an exception, it's because of the code you just added).
    2. Use ./dragonruby mygame --record to create a game play recording that you can use to find the exception (you can replay a recording by executing ./dragonruby mygame --replay last_replay.txt or through the DragonRuby Console using $gtk.recording.start_replay "last_replay.txt".
    3. DragonRuby also ships with a unit testing facility. You can invoke the following command to run a test: ./dragonruby mygame --test tests/some_ruby_file.rb.
    4. Get into the habit of adding debugging facilities within the game itself. You can add drawing primitives to args.outputs.debug that will render on top of your game but will be ignored in a production release.
    5. Debugging something that runs at 60fps is (imo) not that helpful. The exception you are seeing could have been because of a change that occurred many frames ago.

    Frequent Comments About Ruby as a Language Choice link

    But Ruby is dead. link

    Let's check the official source for the answer to this question: isrubydead.com: https://isrubydead.com/.

    On a more serious note, Ruby's _quantity_ levels aren't what they used to be. And that's totally fine. Everyone chases the new and shiny.

    What really matters is _quality/maturity_. Here's a StackOverflow Survey sorted by highest paid developers: https://insights.stackoverflow.com/survey/2021#section-top-paying-technologies-top-paying-technologies.

    Let's stop making this comment shall we?

    But Ruby is slow. link

    That doesn't make any sense. A language specification can't be slow... it's a language spec. Sure, an _implementation/runtime_ can be slow though, but then we'd have to talk about which runtime.

    Here's a some quick demonstrations of how well DragonRuby Game Toolkit Performs:

    Dynamic languages are slow. link

    They are certainly slower than statically compiled languages. With the processing power and compiler optimizations we have today, dynamic languages like Ruby are _fast enough_.

    Unless you are writing in some form of intermediate representation by hand, your language of choice also suffers this same fallacy of slow. Like, nothing is faster than a low level assembly-like language. So unless you're writing in that, let's stop making this comment.

    NOTE: If you _are_ hand writing LLVM IR, we are always open to bringing on new partners with such a skill set. Email us ^_^.

    Frequent Concerns link

    DragonRuby is not open source. That's not right. link

    The current state of open source is unsustainable. Contributors work for free, most all open source repositories are severely under-staffed, and burnout from core members is rampant.

    We believe in open source very strongly. Parts of DragonRuby are in fact, open source. Just not all of it (for legal reasons, and because the IP we've created has value). And we promise that we are looking for (or creating) ways to _sustainably_ open source everything we do.

    If you have ideas on how we can do this, email us!

    If the reason above isn't sufficient, then definitely use something else.

    All this being said, we do have parts of the engine open sourced on GitHub: https://github.com/dragonruby/dragonruby-game-toolkit-contrib/

    DragonRuby is for pay. You should offer a free version. link

    If you can afford to pay for DragonRuby, you should (and will). We don't tell authors that they should give us their books for free, and only require payment if we read the entire thing. It's time we stop asking that of software products.

    That being said, we will _never_ put someone out financially. We have income assistance for anyone that can't afford a license to any one of our products.

    You qualify for a free, unrestricted license to DragonRuby products if any of the following items pertain to you:

    Just contact Amir at [email protected] with a short explanation of your current situation and he'll set you up. No questions asked.

    But still, you should offer a free version. So I can try it out and see if I like it. link

    You can try our web-based sandbox environment at http://fiddle.dragonruby.org. But it won't do the runtime justice. Or just come to our Discord Channel at http://discord.dragonruby.org and ask questions. We'd be happy to have a one on one video chat with you and show off all the cool stuff we're doing.

    Seriously just buy it. Get a refund if you don't like it. We make it stupid easy to do so.

    I still think you should do a free version. Think of all people who would give it a shot. link

    Free isn't a sustainable financial model. We don't want to spam your email. We don't want to collect usage data off of you either. We just want to provide quality toolchains to quality developers (as opposed to a large quantity of developers).

    The people that pay for DragonRuby and make an effort to understand it are the ones we want to build a community around, partner with, and collaborate with. So having that small monetary wall deters entitled individuals that don't value the same things we do.

    What if I build something with DragonRuby, but DragonRuby LLP becomes insolvent. link

    We want to be able to work on the stuff we love, every day of our lives. And we'll go to great lengths to make that continues.

    But, in the event that sad day comes, our partnership bylaws state that _all_ DragonRuby IP that can be legally open sourced, will be released under a permissive license.

    RECIPIES: link

    How To Determine What Frame You Are On link

    There is a property on state called tick_count that is incremented by DragonRuby every time the tick method is called. The following code renders a label that displays the current tick_count.

    def tick args
      args.outputs.labels << [10, 670, "#{args.state.tick_count}"]
    end
    

    How To Get Current Framerate link

    Current framerate is a top level property on the Game Toolkit Runtime and is accessible via args.gtk.current_framerate.

    def tick args
      args.outputs.labels << [10, 710, "framerate: #{args.gtk.current_framerate.round}"]
    end
    

    How To Render A Sprite Using An Array link

    All file paths should use the forward slash / *not* backslash . Game Toolkit includes a number of sprites in the sprites folder (everything about your game is located in the mygame directory).

    The following code renders a sprite with a width and height of 100 in the center of the screen.

    args.outputs.sprites is used to render a sprite.

    NOTE: Rendering using an Array is "quick and dirty". It's generally recommended that you render using Hashes long term.

    def tick args
      args.outputs.sprites << [
        640 - 50,                 # X
        360 - 50,                 # Y
        100,                      # W
        100,                      # H
        'sprites/square-blue.png' # PATH
     ]
    end
    

    Rendering a Sprite Using a Hash link

    Using ordinal positioning can get a little unruly given so many properties you have control over.

    You can represent a sprite as a Hash:

    def tick args
      args.outputs.sprites << {
        x: 640 - 50,
        y: 360 - 50,
        w: 100,
        h: 100,
    
        path: 'sprites/square-blue.png',
        angle: 0,
    
        a: 255,
        r: 255,
        g: 255,
        b: 255,
    
        # source_ properties have origin of bottom left
        source_x:  0,
        source_y:  0,
        source_w: -1,
        source_h: -1,
    
        # tile_ properties have origin of top left
        tile_x:  0,
        tile_y:  0,
        tile_w: -1,
        tile_h: -1,
    
        flip_vertically: false,
        flip_horizontally: false,
    
        angle_anchor_x: 0.5,
        angle_anchor_y: 1.0,
    
        blendmode_enum: 1
    
        # sprites anchor/alignment (default is nil)
        anchor_x: 0.5,
        anchor_y: 0.5
      }
    end
    

    The blendmode_enum value can be set to 0 (no blending), 1 (alpha blending), 2 (additive blending), 3 (modulo blending), 4 (multiply blending).

    How To Render A Label link

    args.outputs.labels is used to render labels.

    Labels are how you display text. This code will go directly inside of the def tick args method.

    NOTE: Rendering using an Array is "quick and dirty". It's generally recommended that you render using Hashes long term.

    Here is the minimum code:

    def tick args
      #                       X    Y    TEXT
      args.outputs.labels << [640, 360, "I am a black label."]
    end
    

    A Colored Label link

    def tick args
      # A colored label
      #                       X    Y    TEXT,                   RED    GREEN  BLUE  ALPHA
      args.outputs.labels << [640, 360, "I am a redish label.", 255,     128,  128,   255]
    end
    

    Extended Label Properties link

    def tick args
      # A colored label
      #                       X    Y     TEXT           SIZE  ALIGNMENT  RED  GREEN  BLUE  ALPHA  FONT FILE
      args.outputs.labels << [
        640,                   # X
        360,                   # Y
        "Hello world",         # TEXT
        0,                     # SIZE_ENUM
        1,                     # ALIGNMENT_ENUM
        0,                     # RED
        0,                     # GREEN
        0,                     # BLUE
        255,                   # ALPHA
        "fonts/coolfont.ttf"   # FONT
      ]
    end
    

    A SIZE_ENUM of 0 represents "default size". A negative value will decrease the label size. A positive value will increase the label's size.

    An ALIGNMENT_ENUM of 0 represents "left aligned". 1 represents "center aligned". 2 represents "right aligned".

    Rendering A Label As A Hash link

    You can add additional metadata about your game within a label, which requires you to use a `Hash` instead.

    If you use a Hash to render a label, you can set the label's size using either SIZE_ENUM or SIZE_PX. If both options are provided, SIZE_PX will be used.

    def tick args
      args.outputs.labels << {
        x:                       200,
        y:                       550,
        text:                    "dragonruby",
        # size specification can be either size_enum or size_px
        size_enum:               2,
        size_px:                 22,
        alignment_enum:          1,
        r:                       155,
        g:                       50,
        b:                       50,
        a:                       255,
        font:                    "fonts/manaspc.ttf",
        vertical_alignment_enum: 0, # 0 is bottom, 1 is middle, 2 is top
        anchor_x: 0.5,
        anchor_y: 0.5
        # You can add any properties you like (this will be ignored/won't cause errors)
        game_data_one:  "Something",
        game_data_two: {
           value_1: "value",
           value_2: "value two",
           a_number: 15
        }
      }
    end
    

    Getting The Size Of A Piece Of Text link

    You can get the render size of any string using args.gtk.calcstringbox.

    def tick args
      #                             TEXT           SIZE_ENUM  FONT
      w, h = args.gtk.calcstringbox("some string",         0, "font.ttf")
    
      # NOTE: The SIZE_ENUM and FONT are optional arguments.
    
      # Render a label showing the w and h of the text:
      args.outputs.labels << [
        10,
        710,
        # This string uses Ruby's string interpolation literal: #{}
        "'some string' has width: #{w}, and height: #{h}."
      ]
    end
    

    Rendering Labels With New Line Characters And Wrapping link

    You can use a strategy like the following to create multiple labels from a String.

    def tick args
      long_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elitteger dolor velit, ultricies vitae libero vel, aliquam imperdiet enim."
      max_character_length = 30
      long_strings_split = args.string.wrapped_lines long_string, max_character_length
      args.outputs.labels << long_strings_split.map_with_index do |s, i|
        { x: 10, y: 600 - (i * 20), text: s }
      end
    end
    

    How To Play A Sound link

    Sounds that end .wav will play once:

    def tick args
      # Play a sound every second
      if (args.state.tick_count % 60) == 0
        args.outputs.sounds << 'something.wav'
      end
    end
    

    Sounds that end .ogg is considered background music and will loop:

    def tick args
      # Start a sound loop at the beginning of the game
      if args.state.tick_count == 0
        args.outputs.sounds << 'background_music.ogg'
      end
    end
    

    If you want to play a .ogg once as if it were a sound effect, you can do:

    def tick args
      # Play a sound every second
      if (args.state.tick_count % 60) == 0
        args.gtk.queue_sound 'some-ogg.ogg'
      end
    end
    

    Using args.state To Store Your Game State link

    args.state is a open data structure that allows you to define properties that are arbitrarily nested. You don't need to define any kind of class.

    To initialize your game state, use the ||= operator. Any value on the right side of ||= will only be assigned _once_.

    To assign a value every frame, just use the = operator, but _make sure_ you've initialized a default value.

    def tick args
      # initialize your game state ONCE
      args.state.player.x  ||= 0
      args.state.player.y  ||= 0
      args.state.player.hp ||= 100
    
      # increment the x position of the character by one every frame
      args.state.player.x += 1
    
      # Render a sprite with a label above the sprite
      args.outputs.sprites << [
        args.state.player.x,
        args.state.player.y,
        32, 32,
        "player.png"
      ]
    
      args.outputs.labels << [
        args.state.player.x,
        args.state.player.y - 50,
        args.state.player.hp
      ]
    end
    

    Accessing files link

    DragonRuby uses a sandboxed filesystem which will automatically read from and write to a location appropriate for your platform so you don't have to worry about theses details in your code. You can just use gtk.read_file, gtk.write_file, and gtk.append_file with a relative path and the engine will take care of the rest.

    The data directories that will be written to in a production build are:

    The values in square brackets are the values you set in your app/metadata/game_metadata.txt file.

    When reading files, the engine will first look in the game's data directory and then in the game directory itself. This means that if you write a file to the data directory that already exists in your game directory, the file in the data directory will be used instead of the one that is in your game.

    When running a development build you will directly write to your game directory (and thus overwrite existing files). This can be useful for built-in development tools like level editors.

    For more details on the implementation of the sandboxed filesystem, see Ryan C. Gordon's PhysicsFS documentation: https://icculus.org/physfs/

    IMPORTANT: File access functions are sandoxed and assume that the dragonruby binary lives alongside the game you are building. Do not expect file access functions to return correct values if you are attempting to run the dragonruby binary from a shared location. It's recommended that the directory structure contained in the zip is not altered and games are built using that starter template.

    Troubleshoot Performance link

    args.state.bullets.each do |bullet|
      args.outputs.sprites << bullet.sprite
    end
    

    do

    args.outputs.sprites << args.state.bullets.map do |b|
      b.sprite
    end
    
    args.state.fx_queue |fx|
      fx.count_down ||= 255
      fx.countdown -= 5
      if fx.countdown < 0
        args.state.fx_queue.delete fx
      end
    end
    

    Do:

    args.state.fx_queue |fx|
      fx.count_down ||= 255
      fx.countdown -= 5
    end
    
    args.state.fx_queue.reject! { |fx| fx.countdown < 0 }
    

    Outputs (args.outputs) link

    Outputs is how you render primitives to the screen. The minimal setup for rendering something to the screen is via a tick method defined in mygame/app/main.rb

    def tick args
      args.outputs.solids     << { x: 0, y: 0, w: 100, h: 100 }
      args.outputs.sprites    << { x: 100, y: 100, w: 100, h: 100, path: "sprites/square/blue.png" }
      args.outputs.labels     << { x: 200, y: 200, text: "Hello World" }
      args.outputs.borders    << { x: 0, y: 0, w: 100, h: 100 }
      args.outputs.lines      << { x: 300, y: 300, x2: 400, y2: 400 }
    end
    

    Collection Render Orders link

    Primitives are rendered first-in, first-out. The rendering order (sorted by bottom-most to top-most):

    Primitives Collection (args.outputs.primitives) link

    args.outputs.primitives can take in any primitive and will render first in, first out.

    For example, you can render a solid above a sprite:

    def tick args
      args.outputs.primitives << { x: 100, y: 100,
                                   w: 100, h: 100,
                                   path: "sprites/square/blue.png" }
      args.outputs.primitives << { x: 0, y: 0, w: 100, h: 100, primitive_marker: :solid }
      args.outputs.primitives << { x: 0, y: 0, w: 100, h: 100, primitive_marker: :border }
    end
    

    Debug Collection (args.outputs.debug) link

    args.outputs.debug will not render in production mode and behaves like args.outputs.primitives. Objects in this collection are rendered above everything.

    Additionally, args.outputs.debug allows you to pass in a String as a primitive type. This is helpful for quickly showing the value of a variable on the screen. A label with black text and a white background will be created for each String sent in. The labels will be automatically stacked vertically for you.

    Example:

    def tick args
      args.state.player ||= { x: 100, y: 100 }
      args.state.player.x += 1
      args.state.player.x = 0 if args.state.player.x > 1280
    
      # the following string values will generate labels with backgrounds
      # and will auto stack vertically
      args.outputs.debug << "current tick: #{args.state.tick_count}"
      args.outputs.debug << "player x: #{args.state.player.x}"
    end
    

    solids link

    Add primitives to this collection to render a solid to the screen.

    Rendering a solid using an Array link

    Creates a solid black rectangle located at 100, 100. 160 pixels wide and 90 pixels tall.

    def tick args
      #                         X    Y  WIDTH  HEIGHT
      args.outputs.solids << [100, 100,   160,     90]
    end
    

    Rendering a solid using an Array with colors and alpha link

    The value for the color and alpha is a number between 0 and 255. The alpha property is optional and will be set to 255 if not specified.

    Creates a green solid rectangle with an opacity of 50%.

    def tick args
      #                         X    Y  WIDTH  HEIGHT  RED  GREEN  BLUE  ALPHA
      args.outputs.solids << [100, 100,   160,     90,   0,   255,    0,   128]
    end
    

    Rendering a solid using a Hash link

    If you want a more readable invocation. You can use the following hash to create a solid. Any parameters that are not specified will be given a default value. The keys of the hash can be provided in any order.

    def tick args
      args.outputs.solids << {
        x:    0,
        y:    0,
        w:  100,
        h:  100,
        r:    0,
        g:  255,
        b:    0,
        a:  255,
        anchor_x: 0,
        anchor_y: 0,
        blendmode_enum: 1
      }
    end
    

    Rendering a solid using a Class link

    You can also create a class with solid properties and render it as a primitive. ALL properties must be on the class. *Additionally*, a method called primitive_marker must be defined on the class.

    Here is an example:

    # Create type with ALL solid properties AND primitive_marker
    class Solid
      attr_accessor :x, :y, :w, :h, :r, :g, :b, :a, :anchor_x, :anchor_y, :blendmode_enum
    
      def primitive_marker
        :solid # or :border
      end
    end
    
    # Inherit from type
    class Square < Solid
      # constructor
      def initialize x, y, size
        self.x = x
        self.y = y
        self.w = size
        self.h = size
      end
    end
    
    def tick args
      # render solid/border
      args.outputs.solids  << Square.new(10, 10, 32)
    end
    

    borders link

    Add primitives to this collection to render an unfilled solid to the screen. Take a look at the documentation for Outputs#solids.

    The only difference between the two primitives is where they are added.

    Instead of using args.outputs.solids:

    def tick args
      #                         X    Y  WIDTH  HEIGHT
      args.outputs.solids << [100, 100,   160,     90]
    end
    

    You have to use args.outputs.borders:

    def tick args
      #                           X    Y  WIDTH  HEIGHT
      args.outputs.borders << [100, 100,   160,     90]
    end
    

    sprites link

    Add primitives to this collection to render a sprite to the screen.

    Rendering a sprite using an Array link

    Creates a sprite of a white circle located at 100, 100. 160 pixels wide and 90 pixels tall.

    def tick args
      #                         X    Y   WIDTH   HEIGHT                      PATH
      args.outputs.sprites << [100, 100,   160,     90, "sprites/circle/white.png"]
    end
    

    Rendering a sprite using a Hash link

    If you want a more readable (and faster) invocation, you can use the following hash to create a sprite. Any parameters that are not specified will be given a default value. The keys of the hash can be provided in any order.

    def tick args
      args.outputs.sprites << {
        x:                             0,
        y:                             0,
        w:                           100,
        h:                           100,
        path: "sprites/circle/white.png",
        angle:                         0,
        a:                           255,
        r:                             0,
        g:                           255,
        b:                             0
      }
    end
    

    Here are all the properties that you can set on a sprite. The only required ones are x, y, w, h, and path.

    Required properties

    Anchors and Rotations

    Here's an example of rendering a 80x80 pixel sprite in the center of the screen:

    def tick args
      args.outputs.sprites << {
        x: 640 - 40, # the logical center of the screen horizontally is 640, minus half the width of the sprite
        y: 360 - 40, # the logical center of the screen vertically is 360, minus half the height of the sprite
        w: 80,
        h: 80,
        path: "sprites/square/blue.png"
     }
    end
    

    Instead of computing the offset, you can use anchor_x, and anchor_y to center the sprite. The following is equivalent to the code above:

    def tick args
      args.outputs.sprites << {
        x: 640,
        y: 360,
        w: 80,
        h: 80,
        path: "sprites/square/blue.png",
        anchor_x: 0.5, # position horizontally at 0.5 of the sprite's width
        anchor_y: 0.5  # position vertically at 0.5 of the sprite's height
     }
    end
    

    Cropping Properties

    See the sample apps under ./samples/03_rendering_sprites for examples of how to use this properties non-trivially.

    Blending Options

    The following sample apps show how blendmode_enum can be leveraged to create coloring and lighting effects:

    Triagles (Indie, Pro Feature)

    Sprites can be rendered as triangles at the Indie and Pro License Tiers. To rendering using triangles, instead of providing a w, h property, provide x2, y2, x3, y3. This applies for positioning and cropping.

    Here is an example:

    def tick args
      args.outputs.sprites << {
        x: 0,
        y: 0,
        x2: 80,
        y2: 0,
        x3: 0,
        y3: 80,
        source_x: 0,
        source_y: 0,
        source_x2: 80,
        source_y2: 0,
        source_x3: 0,
        source_y3: 80,
        path: "sprites/square/blue.png"
      }
    end
    

    For more example of rendering using triangles see:

    Rendering a sprite using a Class link

    You can also create a class with solid/border properties and render it as a primitive. ALL properties must be on the class. *Additionally*, a method called primitive_marker must be defined on the class.

    Here is an example:

    # Create type with ALL sprite properties AND primitive_marker
    class Sprite
      attr_accessor :x, :y, :w, :h, :path, :angle, :a, :r, :g, :b, :tile_x,
                    :tile_y, :tile_w, :tile_h, :flip_horizontally,
                    :flip_vertically, :angle_anchor_x, :angle_anchor_y, :id,
                    :angle_x, :angle_y, :z,
                    :source_x, :source_y, :source_w, :source_h, :blendmode_enum,
                    :source_x2, :source_y2, :source_x3, :source_y3, :x2, :y2, :x3, :y3,
                    :anchor_x, :anchor_y
    
      def primitive_marker
        :sprite
      end
    end
    
    # Inherit from type
    class Circle < Sprite
      # constructor
      def initialize x, y, size, path
        self.x = x
        self.y = y
        self.w = size
        self.h = size
        self.path = path
      end
    
      def serialize
        {x:self.x, y:self.y, w:self.w, h:self.h, path:self.path}
      end
    
      def inspect
        serialize.to_s
      end
    
      def to_s
        serialize.to_s
      end
    end
    
    def tick args
      # render circle sprite
      args.outputs.sprites  << Circle.new(10, 10, 32,"sprites/circle/white.png")
    end
    

    attr_sprite link

    The attr_sprite class macro adds all properties needed to render a sprite to a class. This removes the need to manually define all sprites properties that DragonRuby offers for rendering.

    Instead of manually defining the properties, you can represent a sprite using the attr_sprite class macro:

    class BlueSquare
      # invoke the helper function at the class level for
      # anything you want to represent as a sprite
      attr_sprite
    
      def initialize(x: 0, y: 0, w: 0, h: 0k
        @x = x
        @y = y
        @w = w
        @h = h
        @path = 'sprites/square-blue.png'
      end
    end
    
    def tick args
      args.outputs.sprites << BlueSquare.new(x: 640 - 50,
                                             y: 360 - 50,
                                             w: 50,
                                             h: 50)
    end
    

    labels link

    Add primitives to this collection to render a label.

    Rendering a label using an Array link

    Labels represented as Arrays/Tuples:

    def tick args
                             #        X         Y              TEXT   SIZE_ENUM
      args.outputs.labels << [175 + 150, 610 - 50, "Smaller label.",         0]
    end
    

    Here are all the properties that you can set with a label represented as an Array. It's recommended to move over to using Hashes once you've specified a lot of properties.

    def tick args
      args.outputs.labels << [
        640,                   # X
        360,                   # Y
        "Hello world",         # TEXT
        0,                     # SIZE_ENUM
        1,                     # ALIGNMENT_ENUM
        0,                     # RED
        0,                     # GREEN
        0,                     # BLUE
        255,                   # ALPHA
        "fonts/coolfont.ttf"   # FONT
      ]
    end
    d
    

    Rendering a label using a Hash link

    def tick args
      args.outputs.labels << {
          x:                       200,
          y:                       550,
          text:                    "dragonruby",
          size_enum:               2,
          alignment_enum:          1, # 0 = left, 1 = center, 2 = right
          r:                       155,
          g:                       50,
          b:                       50,
          a:                       255,
          font:                    "fonts/manaspc.ttf",
          vertical_alignment_enum: 0  # 0 = bottom, 1 = center, 2 = top
      }
    end
    

    Screenshots link

    Add a hash to this collection to take a screenshot and save as png file. The keys of the hash can be provided in any order.

    def tick args
      args.outputs.screenshots << {
        x: 0, y: 0, w: 100, h: 100,    # Which portion of the screen should be captured
        path: 'screenshot.png',        # Output path of PNG file (inside game directory)
        r: 255, g: 255, b: 255, a: 0   # Optional chroma key
      }
    end
    

    Chroma key (Making a color transparent) link

    By specifying the r, g, b and a keys of the hash you change the transparency of a color in the resulting PNG file. This can be useful if you want to create files with transparent background like spritesheets. The transparency of the color specified by r, g, b will be set to the transparency specified by a.

    The example above sets the color white (255, 255, 255) as transparent.

    Inputs (args.inputs) link

    Access using input using args.inputs.

    last_active link

    This function returns the last active input which will be set to either :keyboard, :mouse, or :controller. The function is helpful when you need to present on screen instructions based on the input the player chose to play with.

    def tick args
      if args.inputs.last_active == :controller
        args.outputs.labels << { x: 60, y: 60, text: "Use the D-Pad to move around." }
      else
        args.outputs.labels << { x: 60, y: 60, text: "Use the arrow keys to move around." }
      end
    end
    

    :mouse, or :controller. The function is helpful when you need to present on screen instructions based on the input the player chose to play with.

    locale link

    Returns the ISO 639-1 two-letter langauge code based on OS preferences. Refer to the following link for locale strings: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).

    Defaults to "en" if locale can't be retrieved (args.inputs.locale_raw will be nil in this case).

    up link

    Returns true if: the up arrow or w key is pressed or held on the keyboard; or if up is pressed or held on controller_one; or if the left_analog on controller_one is tilted upwards.

    down link

    Returns true if: the down arrow or s key is pressed or held on the keyboard; or if down is pressed or held on controller_one; or if the left_analog on controller_one is tilted downwards.

    left link

    Returns true if: the left arrow or a key is pressed or held on the keyboard; or if left is pressed or held on controller_one; or if the left_analog on controller_one is tilted to the left.

    right link

    Returns true if: the right arrow or d key is pressed or held on the keyboard; or if right is pressed or held on controller_one; or if the left_analog on controller_one is tilted to the right.

    left_right link

    Returns -1 (left), 0 (neutral), or +1 (right). This method is aliased to args.inputs.left_right_with_wasd.

    The following inputs are inspected to determine the result:

    left_right_perc link

    Returns a floating point value between -1 and 1. This method is aliased to args.inputs.left_right_perc_with_wasd

    The following inputs are inspected to dermine the result:

    left_right_directional link

    Returns -1 (left), 0 (neutral), or +1 (right). This method is aliased to args.inputs.left_right_arrow.

    The following inputs are inspected to determine the result:

    left_right_directional_perc link

    Returns a floating point value between -1 and 1. The following inputs are inspected to dermine the result:

    Here is some sample code to help visualize the left_right functions.

    def tick args
      args.outputs.debug << "* Variations of args.inputs.left_right"
      args.outputs.debug << "  args.inputs.left_right(_with_wasd) #{args.inputs.left_right}"
      args.outputs.debug << "  args.inputs.left_right_perc(_with_wasd) #{args.inputs.left_right_perc}"
      args.outputs.debug << "  args.inputs.left_right_directional #{args.inputs.left_right_directional}"
      args.outputs.debug << "  args.inputs.left_right_directional_perc #{args.inputs.left_right_directional_perc}"
      args.outputs.debug << "** Keyboard"
      args.outputs.debug << "   args.inputs.keyboard.a #{args.inputs.keyboard.a}"
      args.outputs.debug << "   args.inputs.keyboard.d #{args.inputs.keyboard.d}"
      args.outputs.debug << "   args.inputs.keyboard.left_arrow #{args.inputs.keyboard.left_arrow}"
      args.outputs.debug << "   args.inputs.keyboard.right_arrow #{args.inputs.keyboard.right_arrow}"
      args.outputs.debug << "** Controller"
      args.outputs.debug << "   args.inputs.controller_one.dpad_left #{args.inputs.controller_one.dpad_left}"
      args.outputs.debug << "   args.inputs.controller_one.dpad_right #{args.inputs.controller_one.dpad_right}"
      args.outputs.debug << "   args.inputs.controller_one.left_analog_x_perc #{args.inputs.controller_one.left_analog_x_perc}"
    end
    

    up_down link

    Returns -1 (down), 0 (neutral), or +1 (up). This method is aliased to args.inputs.up_down_with_wasd.

    The following inputs are inspected to determine the result:

    up_down_directional link

    Returns -1 (down), 0 (neutral), or +1 (up). This method is aliased to args.inputs.up_down_arrow.

    The following inputs are inspected to determine the result:

    up_down_perc link

    Returns a floating point value between -1 and 1. The following inputs are inspected to dermine the result:

    Here is some sample code to help visualize the up_down functions.

    def tick args
      args.outputs.debug << "* Variations of args.inputs.up_down"
      args.outputs.debug << "  args.inputs.up_down #{args.inputs.up_down}"
      args.outputs.debug << "  args.inputs.up_down_directional #{args.inputs.up_down_directional}"
      args.outputs.debug << "  args.inputs.up_down_perc #{args.inputs.up_down_perc}"
      args.outputs.debug << "** Keyboard"
      args.outputs.debug << "   args.inputs.keyboard.a #{args.inputs.keyboard.a}"
      args.outputs.debug << "   args.inputs.keyboard.d #{args.inputs.keyboard.d}"
      args.outputs.debug << "   args.inputs.keyboard.up_arrow #{args.inputs.keyboard.up_arrow}"
      args.outputs.debug << "   args.inputs.keyboard.down_arrow #{args.inputs.keyboard.down_arrow}"
      args.outputs.debug << "** Controller"
      args.outputs.debug << "   args.inputs.controller_one.dpad_up #{args.inputs.controller_one.dpad_up}"
      args.outputs.debug << "   args.inputs.controller_one.dpad_down #{args.inputs.controller_one.dpad_down}"
      args.outputs.debug << "   args.inputs.controller_one.up_analog_x_perc #{args.inputs.controller_one.up_analog_x_perc}"
    end
    

    text link

    Returns a string that represents the last key that was pressed on the keyboard.

    Mouse (args.inputs.mouse) link

    Represents the user's mouse.

    has_focus link

    Return's true if the game has mouse focus.

    x link

    Returns the current x location of the mouse.

    y link

    Returns the current y location of the mouse.

    inside_rect? rect link

    Return. args.inputs.mouse.inside_rect? takes in any primitive that responds to x, y, w, h:

    inside_circle? center_point, radius link

    Returns true if the mouse is inside of a specified circle. args.inputs.mouse.inside_circle? takes in any primitive that responds to x, y (which represents the circle's center), and takes in a radius:

    moved link

    Returns true if the mouse has moved on the current frame.

    button_left link

    Returns true if the left mouse button is down.

    button_middle link

    Returns true if the middle mouse button is down.

    button_right link

    Returns true if the right mouse button is down.

    button_bits link

    Returns a bitmask for all buttons on the mouse: 1 for a button in the down state, 0 for a button in the up state.

    wheel link

    Represents the mouse wheel. Returns nil if no mouse wheel actions occurred. Otherwise args.inputs.mouse.wheel will return a Hash with x, and y (representing movement on each axis).

    click OR down, previous_click, up link

    The properties args.inputs.mouse.(click|down|previous_click|up) each return nil if the mouse button event didn't occur. And return an Entity that has an x, y properties along with helper functions to determine collision: inside_rect?, inside_circle. This value will be true if any of the mouse's buttons caused these events. To scope to a specific button use .button_left, .button_middle, .button_right, or .button_bits.

    Touch link

    The following touch apis are available on touch devices (iOS, Android, Mobile Web, Surface).

    args.inputs.touch link

    Returns a Hash representing all touch points on a touch device.

    args.inputs.finger_left link

    Returns a Hash with x and y denoting a touch point that is on the left side of the screen.

    args.inputs.finger_right link

    Returns a Hash with x and y denoting a touch point that is on the right side of the screen.

    Controller (args.inputs.controller_(one-four)) link

    Represents controllers connected to the usb ports.

    active link

    Returns true if any of the controller's buttons were used.

    up link

    Returns true if up is pressed or held on the directional or left analog.

    down link

    Returns true if down is pressed or held on the directional or left analog.

    left link

    Returns true if left is pressed or held on the directional or left analog.

    right link

    Returns true if right is pressed or held on the directional or left analog.

    left_right link

    Returns -1 (left), 0 (neutral), or +1 (right) depending on results of args.inputs.controller_(one-four).left and args.inputs.controller_(one-four).right.

    up_down link

    Returns -1 (down), 0 (neutral), or +1 (up) depending on results of args.inputs.controller_(one-four).up and args.inputs.controller_(one-four).down.

    (left|right)_analog_x_raw link

    Returns the raw integer value for the analog's horizontal movement (-32,767 to +32,767).

    (left|right)_analog_y_raw link

    Returns the raw integer value for the analog's vertical movement (-32,767 to +32,767).

    (left|right)_analog_x_perc link

    Returns a number between -1 and 1 which represents the percentage the analog is moved horizontally as a ratio of the maximum horizontal movement.

    (left|right)_analog_y_perc link

    Returns a number between -1 and 1 which represents the percentage the analog is moved vertically as a ratio of the maximum vertical movement.

    dpad_up, directional_up link

    Returns true if up is pressed or held on the dpad.

    dpad_down, directional_down link

    Returns true if down is pressed or held on the dpad.

    dpad_left, directional_left link

    Returns true if left is pressed or held on the dpad.

    dpad_right, directional_right link

    Returns true if right is pressed or held on the dpad.

    (a|b|x|y|l1|r1|l2|r2|l3|r3|start|select) link

    Returns true if the specific button is pressed or held. Note: For PS4 and PS5 controllers a maps to Cross, b maps to Circle, x maps to Square, and y maps to Triangle.

    truthy_keys link

    Returns a collection of Symbols that represent all keys that are in the pressed or held state.

    key_down link

    Returns true if the specific button was pressed on this frame. args.inputs.controller_(one-four).key_down.BUTTON will only be true on the frame it was pressed.

    key_held link

    Returns true if the specific button is being held. args.inputs.controller_(one-four).key_held.BUTTON will be true for all frames after key_down (until released).

    key_up link

    Returns true if the specific button was released. args.inputs.controller_(one-four).key_up.BUTTON will be true only on the frame the button was released.

    Keyboard (args.inputs.keyboard) link

    Represents the user's keyboard.

    active link

    Returns Kernel.tick_count (args.state.tick_count) if any keys on the keyboard were pressed.

    has_focus link

    Returns true if the game has keyboard focus.

    up link

    Returns true if up or w is pressed or held on the keyboard.

    down link

    Returns true if down or s is pressed or held on the keyboard.

    left link

    Returns true if left or a is pressed or held on the keyboard.

    right link

    Returns true if right or d is pressed or held on the keyboard.

    left_right link

    Returns -1 (left), 0 (neutral), or +1 (right) depending on results of args.inputs.keyboard.left and args.inputs.keyboard.right.

    up_down link

    Returns -1 (left), 0 (neutral), or +1 (right) depending on results of args.inputs.keyboard.up and args.inputs.keyboard.up.

    keyboard properties link

    The following properties represent keys on the keyboard and are available on args.inputs.keyboard.KEY, args.inputs.keyboard.key_down.KEY, args.inputs.keyboard.key_held.KEY, and args.inputs.keyboard.key_up.KEY:

    char link

    Method is available under inputs.key_down, inputs.key_held, and inputs.key_up. Take note that

    args.inputs.keyboard.key_held.char will only return the ascii value of the last key that was held. Use args.inputs.keyboard.key_held.truthy_keys to get an Array of Symbols representing all keys being held.

    To get a picture of all key states args.inputs.keyboard.keys returns a Hash with the following keys: :down, :held, :down_or_held, :up.

    NOTE: args.inputs.keyboard.key_down.char will be set in line with key repeat behavior of your OS.

    This is a demonstration of the behavior (see ./samples/02_input_basics/01_keyboard for a more detailed example):

    def tick args
      # uncomment the line below to see the value changes at a slower rate
      # $gtk.slowmo! 30
    
      keyboard = args.inputs.keyboard
    
      args.outputs.labels << { x: 30,
                               y: 720,
                               text: "use the J key to test" }
    
      args.outputs.labels << { x: 30,
                               y: 720 - 30,
                               text: "key_down.char: #{keyboard.key_down.char.inspect}" }
    
      args.outputs.labels << { x: 30,
                               y: 720 - 60,
                               text: "key_down.j:    #{keyboard.key_down.j}" }
    
      args.outputs.labels << { x: 30,
                               y: 720 - 30,
                               text: "key_held.char: #{keyboard.key_held.char.inspect}" }
    
      args.outputs.labels << { x: 30,
                               y: 720 - 60,
                               text: "key_held.j:    #{keyboard.key_held.j}" }
    
      args.outputs.labels << { x: 30,
                               y: 720 - 30,
                               text: "key_up.char:   #{keyboard.key_up.char.inspect}" }
    
      args.outputs.labels << { x: 30,
                               y: 720 - 60,
                               text: "key_up.j:      #{keyboard.key_up.j}" }
    end
    

    keys link

    Returns a Hash with all keys on the keyboard in their respective state. The Hash contains the following keys

    Runtime (args.gtk) link

    The GTK::Runtime class is the core of DragonRuby. It is globally accessible via $gtk or inside of the tick method through args.

    def tick args
      args.gtk # accessible like this
      $gtk # or like this
    end
    

    Class Macros link

    The following class macros are available within DragonRuby.

    attr link

    The attr class macro is an alias to attr_accessor.

    Instead of:

    class Player
      attr_accessor :hp, :armor
    end
    

    You can do:

    class Player
      attr :hp, :armor
    end
    

    attr_gtk link

    As the size/complexity of your game increases. You may want to create classes to organize everything. The attr_gtk class macro adds DragonRuby's environment methods (such as args.state, args.inputs, args.outputs, args.audio, etc) to your class so you don't have to pass args around everwhere.

    Instead of:

    class Game
      def tick args
        defaults args
        calc args
        render args
      end
    
      def defaults args
        args.state.space_pressed_at ||= 0
      end
    
      def calc args
        if args.inputs.keyboard.key_down.space
          args.state.space_pressed_at = args.state.tick_count
        end
      end
    
      def render args
        if args.state.space_pressed_at == 0
          args.outputs.labels << { x: 100, y: 100,
                                   text: "press space" }
        else
          args.outputs.labels << { x: 100, y: 100,
                                   text: "space was pressed at: #{args.state.space_pressed_at}" }
        end
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.tick args
    end
    

    You can do:

    class Game
      attr_gtk # attr_gtk class macro
    
      def tick
        defaults
        calc
        render
      end
    
      def defaults
        state.space_pressed_at ||= 0
      end
    
      def calc
        if inputs.keyboard.key_down.space
          state.space_pressed_at = state.tick_count
        end
      end
    
      def render
        if state.space_pressed_at == 0
          outputs.labels << { x: 100, y: 100,
                              text: "press space" }
        else
          outputs.labels << { x: 100, y: 100,
                              text: "space was pressed at: #{state.space_pressed_at}" }
        end
      end
    end
    
    def tick args
      $game ||= Game.new
      $game.args = args # set args property on game
      $game.tick        # call tick without passing in args
    end
    
    $game = nil
    

    Indie and Pro Functions link

    The following functions are only available at the Indie and Pro License tiers.

    get_pixels link

    Given a file_path to a sprite, this function returns a one dimensional array of hexadecimal values representing the ARGB of each pixel in a sprite.

    See the following sample app for a full demonstration of how to use this function: ./samples/07_advanced_rendering/06_pixel_arrays_from_file

    dlopen link

    Loads a precompiled C Extension into your game.

    See the sample apps at ./samples/12_c_extensions for detailed walkthroughs of creating C extensions.

    Environment and Utility Functions link

    The following functions will help in interacting with the OS and rendering pipeline.

    calcstringbox link

    Returns the render width and render height as a tuple for a piece of text. The parameters this method takes are:

    def tick args
      text = "a piece of text"
      size_enum = 5 # "large font size"
    
      # path is relative to your game directory (eg mygame/fonts/courier-new.ttf)
      font = "fonts/courier-new.ttf"
    
      # get the render width and height
      string_w, string_h = args.gtk.calcstringbox text, size_enum, font
    
      # render the label
      args.outputs.labels << {
        x: 100,
        y: 100,
        text: text,
        size_enum: size_enum,
        font: font
      }
    
      # render a border around the label based on the results from calcstringbox
      args.outputs.borders << {
        x: 100,
        y: 100,
        w: string_w,
        h: string_h,
        r: 0,
        g: 0,
        b: 0
      }
    end
    

    request_quit link

    Call this function to exit your game. You will be given one additional tick if you need to perform any housekeeping before that game closes.

    def tick args
      # exit the game after 600 frames (10 seconds)
      if args.state.tick_count == 600
        args.gtk.request_quit
      end
    end
    

    quit_requested? link

    This function will return true if the game is about to exit (either from the user closing the game or if request_quit was invoked).

    set_window_fullscreen link

    This function takes in a single boolean parameter. true to make the game fullscreen, false to return the game back to windowed mode.

    def tick args
      # make the game full screen after 600 frames (10 seconds)
      if args.state.tick_count == 600
        args.gtk.set_window_fullscreen true
      end
    
      # return the game to windowed mode after 20 seconds
      if args.state.tick_count == 1200
        args.gtk.set_window_fullscreen false
      end
    end
    

    window_fullscreen? link

    Returns true if the window is currently in fullscreen mode.

    set_window_scale link

    This function takes in a float value and uses that to resize the game window to a percentage of 1280x720 (or 720x1280 in portrait mode). The valid scale options are 0.1, 0.25, 0.5, 0.75, 1.25, 1.5, 2.0, 2.5, 3.0, and 4.0. The float value you pass in will be floored to the nearest valid scale option.

    platform? link

    You can ask DragonRuby which platform your game is currently being run on. This can be useful if you want to perform different pieces of logic based on where the game is running.

    The raw platform string value is available via args.gtk.platform which takes in a symbol representing the platform's categorization/mapping.

    You can see all available platform categorizations via the args.gtk.platform_mappings function.

    Here's an example of how to use args.gtk.platform? category_symbol:

    def tick args
      label_style = { x: 640, y: 360, anchor_x: 0.5, anchor_y: 0.5 }
      if    args.gtk.platform? :macos
        args.outputs.labels << { text: "I am running on MacOS.", **label_style }
      elsif args.gtk.platform? :win
        args.outputs.labels << { text: "I am running on Windows.", **label_style }
      elsif args.gtk.platform? :linux
        args.outputs.labels << { text: "I am running on Linux.", **label_style }
      elsif args.gtk.platform? :web
        args.outputs.labels << { text: "I am running on a web page.", **label_style }
      elsif args.gtk.platform? :android
        args.outputs.labels << { text: "I am running on Android.", **label_style }
      elsif args.gtk.platform? :ios
        args.outputs.labels << { text: "I am running on iOS.", **label_style }
      elsif args.gtk.platform? :touch
        args.outputs.labels << { text: "I am running on a device that supports touch (either iOS/Android native or mobile web).", **label_style }
      elsif args.gtk.platform? :steam
        args.outputs.labels << { text: "I am running via steam (covers both desktop and steamdeck).", **label_style }
      elsif args.gtk.platform? :steam_deck
        args.outputs.labels << { text: "I am running via steam on the Steam Deck (not steam desktop).", **label_style }
      elsif args.gtk.platform? :steam_desktop
        args.outputs.labels << { text: "I am running via steam on desktop (not steam deck).", **label_style }
      end
    end
    

    production? link

    Returns true if the game is being run in a released/shipped state.

    If you want to simulate a production build. Add an empty file called dragonruby_production_build inside of the metadata folder. This will turn of all logging and all creation of temp files used for development purposes.

    platform_mappings link

    These are the current platform categorizations (args.gtk.platform_mappings):

    {
      "Mac OS X"   => [:desktop, :macos, :osx, :mac, :macosx], # may also include :steam and :steam_desktop run via steam
      "Windows"    => [:desktop, :windows, :win],              # may also include :steam and :steam_desktop run via steam
      "Linux"      => [:desktop, :linux, :nix],                # may also include :steam and :steam_desktop run via steam
      "Emscripten" => [:web, :wasm, :html, :emscripten],       # may also include :touch if running in the web browser on mobile
      "iOS"        => [:mobile, :ios, :touch],
      "Android"    => [:mobile, :android, :touch],
      "Steam Deck" => [:steamdeck, :steam_deck, :steam],
    }
    

    Given the mappings above, args.gtk.platform? :desktop would return true if the game is running on a player's computer irrespective of OS (MacOS, Linux, and Windows are all categorized as :desktop platforms).

    open_url link

    Given a uri represented as a string. This fuction will open the uri in the user's default browser.

    def tick args
      # open a url after 600 frames (10 seconds)
      if args.state.tick_count == 600
        args.gtk.open_url "http://dragonruby.org"
      end
    end
    

    system link

    Given an OS dependent cli command represented as a string, this function executes the command and puts the results to the DragonRuby Console (returns nil).

    def tick args
      # execute ls on the current directory in 10 seconds
      if args.state.tick_count == 600
        args.gtk.system "ls ."
      end
    end
    

    exec link

    Given an OS dependent cli command represented as a string, this function executes the command and returns a string representing the results.

    def tick args
      # execute ls on the current directory in 10 seconds
      if args.state.tick_count == 600
        results = args.gtk.exec "ls ."
        puts "The results of the command are:"
        puts results
      end
    end
    

    show_cursor link

    Shows the mouse cursor.

    hide_cursor link

    Hides the mouse cursor.

    cursor_shown? link

    Returns true if the mouse cursor is visible.

    set_mouse_grab link

    Takes in a numeric parameter representing the mouse grab mode.

    set_system_cursor link

    Takes in a string value of "arrow", "ibeam", "wait", or "hand" and sets the mouse curosor to the corresponding system cursor (if available on the OS).

    set_cursor link

    Replaces the mouse cursor with a sprite. Takes in a path to the sprite, and optionally an x and y value representing the realtive positioning the sprite will have to the mouse cursor.

    def tick args
      if args.state.tick_count == 0
        # assumes a sprite of size 80x80 and centers the sprite
        # relative to the cursor position.
        args.gtk.set_cursor "sprites/square/blue.png", 40, 40
      end
    end
    

    File IO Functions link

    The following functions give you the ability to interact with the file system.

    IMPORTANT: File access functions are sandoxed and assume that the dragonruby binary lives alongside the game you are building. Do not expect these functions to return correct values if you are attempting to run the dragonruby binary from a shared location. It's recommended that the directory structure contained in the zip is not altered and games are built using that starter template.

    list_files link

    This function takes in one parameter. The parameter is the directory path and assumes the the game directory is the root. The method returns an Array of String representing all files within the directory. Use stat_file to determine whether a specific path is a file or a directory.

    stat_file link

    This function takes in one parameter. The parameter is the file path and assumes the the game directory is the root. The method returns nil if the file doesn't exist otherwise it returns a Hash with the following information:

    # {
    #   path: String,
    #   file_size: Int,
    #   mod_time: Int,
    #   create_time: Int,
    #   access_time: Int,
    #   readonly: Boolean,
    #   file_type: Symbol (:regular, :directory, :symlink, :other),
    # }
    
    def tick args
      if args.inputs.mouse.click
        args.gtk.write_file "last-mouse-click.txt", "Mouse was clicked at #{args.state.tick_count}."
      end
    
      file_info = args.gtk.stat_file "last-mouse-click.txt"
    
      if file_info
        args.outputs.labels << {
          x: 30,
          y: 30.from_top,
          text: file_info.to_s,
          size_enum: -3
        }
      else
        args.outputs.labels << {
          x: 30,
          y: 30.from_top,
          text: "file does not exist, click to create file",
          size_enum: -3
        }
      end
    end
    

    read_file link

    Given a file path, a string will be returned representing the contents of the file. nil will be returned if the file does not exist. You can use stat_file to get additional information of a file.

    write_file link

    This function takes in two parameters. The first parameter is the file path and assumes the the game directory is the root. The second parameter is the string that will be written. The method **overwrites** whatever is currently in the file. Use append_file to append to the file as opposed to overwriting.

    def tick args
      if args.inputs.mouse.click
        args.gtk.write_file "last-mouse-click.txt", "Mouse was clicked at #{args.state.tick_count}."
      end
    end
    

    append_file link

    This function takes in two parameters. The first parameter is the file path and assumes the the game directory is the root. The second parameter is the string that will be written. The method appends to whatever is currently in the file (a new file is created if one does not alread exist). Use write_file to overwrite the file's contents as opposed to appending.

    def tick args
      if args.inputs.mouse.click
        args.gtk.write_file "last-mouse-click.txt", "Mouse was clicked at #{args.state.tick_count}."
      end
    end
    

    delete_file link

    This function takes in a single parameters. The parameter is the file path that should be deleted. This function will raise an exception if the path requesting to be deleted does not exist.

    Notes:

    Here is a list of reasons an exception could be raised:

    - If the path is not found. - If the path is still open (for reading or writing). - If the path is not a file or directory. - If the path is a circular symlink. - If you do not have permissions to delete the path. - If the directory attempting to be deleted is not empty.

    def tick args
      if args.inputs.mouse.click
        args.gtk.write_file "last-mouse-click.txt", "Mouse was clicked at #{args.state.tick_count}."
      end
    end
    

    delete_file_if_exist link

    Has the same behavior as delete_file except this function does not throw an exception.

    XML and JSON link

    The following functions help with parsing xml and json.

    parse_json link

    Given a json string, this function returns a hash representing the json data.

    hash = args.gtk.parse_json '{ "name": "John Doe", "aliases": ["JD"] }'
    # structure of hash: { "name"=>"John Doe", "aliases"=>["JD"] }
    

    parse_json_file link

    Same behavior as parse_json_file except a file path is read for the json string.

    parse_xml link

    Given xml data as a string, this function will return a hash that represents the xml data in the following recursive structure:

    
    type: :element,
    name: "Person",
    children: [...]
    
    

    parse_xml_file link

    Function has the same behavior as parse_xml except that the parameter must be a file path that contains xml contents.

    Network IO Functions link

    The following functions help with interacting with the network.

    http_get link

    Returns an object that represents an http response which will eventually have a value. This http_get method is invoked asynchronously. Check for completion before attempting to read results.

    def tick args
      # perform an http get and print the response when available
      args.state.result ||= args.gtk.http_get "https://httpbin.org/html"
    
      if args.state.result && args.state.result[:complete] && !args.state.printed
        if args.state.result[:http_response_code] == 200
          puts "The response was successful. The body is:"
          puts args.state.result[:response_data]
        else
          puts "The response failed. Status code:"
          puts args.state.result[:http_response_code]
        end
        # set a flag denoting that the response has been printed
        args.state.printed = true
    
        # show the console
        args.gtk.show_console
      end
    end
    

    http_post link

    Returns an object that represents an http response which will eventually have a value. This http_post method is invoked asynchronously. Check for completion before attempting to read results.

    def tick args
      # perform an http get and print the response when available
    
      args.state.form_fields ||= { "userId" => "1705280157" }
      args.state.result ||= args.gtk.http_post "http://httpbin.org/post",
                                               args.state.form_fields,
                                               ["Content-Type: application/x-www-form-urlencoded"]
    
    
      if args.state.result && args.state.result[:complete] && !args.state.printed
        if args.state.result[:http_response_code] == 200
          puts "The response was successful. The body is:"
          puts args.state.result[:response_data]
        else
          puts "The response failed. Status code:"
          puts args.state.result[:http_response_code]
        end
        # set a flag denoting that the response has been printed
        args.state.printed = true
    
        # show the console
        args.gtk.show_console
      end
    end
    

    http_post_body link

    Returns an object that represents an http response which will eventually have a value. This http_post_body method is invoked asynchronously. Check for completion before attempting to read results.

    def tick args
      # perform an http get and print the response when available
    
      args.state.json ||= "{ "userId": "#{Time.now.to_i}"}"
      args.state.result ||= args.gtk.http_post_body "http://httpbin.org/post",
                                                    args.state.json,
                                                    ["Content-Type: application/json", "Content-Length: #{args.state.json.length}"]
    
    
      if args.state.result && args.state.result[:complete] && !args.state.printed
        if args.state.result[:http_response_code] == 200
          puts "The response was successful. The body is:"
          puts args.state.result[:response_data]
        else
          puts "The response failed. Status code:"
          puts args.state.result[:http_response_code]
        end
        # set a flag denoting that the response has been printed
        args.state.printed = true
    
        # show the console
        args.gtk.show_console
      end
    end
    

    start_server! link

    Starts a in-game http server that can be process http requests. When your game is running in development mode. A dev server is started at http://localhost:9001

    You can start an in-game http server in production via:

    def tick args
      # server explicitly enabled in production
      args.gtk.start_server! port: 9001, enable_in_prod: true
    end
    

    Here's how you would responde to http requests:

    def tick args
      # server explicitly enabled in production
      args.gtk.start_server! port: 9001, enable_in_prod: true
    
      # loop through pending requests and respond to them
      args.inputs.http_requests.each do |request|
        puts "#{request}"
        request.respond 200, "ok"
      end
    end
    

    Developer Support Functions link

    The following functions help support the development process. It is not recommended to use this functions in "production" game logic.

    version link

    Returns a string representing the version of DragonRuby you are running.

    version_pro? link

    Returns true if the version of DragonRuby is NOT Standard Edition.

    reset link

    Resets DragonRuby's internal state as if it were just started. args.state.tick_count is set to 0 and args.state is cleared of any values. This function is helpful when you are developing your game and want to reset everything as if the game just booted up.

    def tick args
    end
    
    # reset the game if this file is hotloaded/required
    # (removes the need to press "r" when I file is updated)
    $gtk.reset
    

    Resetting iVars (advanced)

    NOTE: args.gtk.reset does not reset global variables or instance of classes you have have constructed. If you want to also reset global variables or instances of classes when $gtk.reset is called. Define a reset method. Here's an example:

    class Game
      def initialize
        puts "Game initialize called"
      end
    end
    
    def tick args
      $game ||= Game.new
    
      if args.state.tick_count == 0
        puts "tick_count is 0"
      end
    
      # if r is pressed on the keyboard, reset the game
      if args.inputs.keyboard.key_down.r
        args.gtk.reset
      end
    end
    
    # custom reset function
    def reset
      puts "Custom reset function was called."
      $game = nil
    end
    

    seed and RNG (advanced)

    Optionally, $gtk.reset (args.gtk.reset) can take in a named parameter for RNG called seed:. Passing in seed: will reset RNG so that rand returns a repeatable set of random numbers. This seed value is initialized with the start time of your game ($gtk.started_at). Having this option is is helpful for replays and unit tests.

    Don't worry about this capability if you aren't using DragonRuby's unit testing, or replay capabilities.

    Here is the behavior of $gtk.reset when given a seed:

    def tick args
      if args.state.tick_count == 0
        puts rand
        puts rand
        puts rand
        puts rand
      end
    end
    
    puts "Started at (RNG seed inital value)"
    puts $gtk.started_at # Time as an integer that your game was started at
    
    puts "Seed value that will be used on reset"
    puts $gtk.seed # current value that RNG was seeded with
    
    # reset the game and use the last seed to reset RNG
    $gtk.reset
    
    # === OR ===
    # sets the seed value to predefined value
    # subsequent resets will use the new predefined value
    # $gtk.reset seed: 100
    # (or shorthand)
    # $gtk.reset 100
    
    # sets the seed back to its original value
    # $gtk.reset seed: $gtk.started_at
    

    If you want to set RNG without resetting your game state, you can use $gtk.set_rng VALUE.

    reset_next_tick link

    Has the same behavior as reset except the reset occurs before tick is executed again. reset resets the environment immediately (while the tick method is inflight). It's recommended that reset should be called outside of the tick method (invoked when a file is saved/hotloaded), and reset_next_tick be used inside of the tick method so you don't accidentally blow away state the your game depends on to complete the current tick without exceptions.

    def tick args
      # reset the game if "r" is pressed on the keyboard
      if args.inputs.keyboard.key_down.r
        args.gtk.reset_next_tick # use reset_next_tick instead of reset
      end
    end
    
    # reset the game if this file is hotloaded/required
    # (removes the need to press "r" when I file is updated)
    $gtk.reset
    

    reset_sprite link

    Sprites when loaded are cached. Given a string parameter, this method invalidates the cache record of a sprite so that updates on from the disk can be loaded.

    reset_sprites link

    Sprites when loaded are cached. This method invalidates the cache record of all sprites so that updates on from the disk can be loaded. This function is automatically called when args.gtk.reset ($gtk.reset) is invoked.

    calcspritebox link

    Given a path to a sprite, this method returns the width and height of a sprite as a tuple.

    NOTE: This method should be used for development purposes only and is expensive to call every frame. Do not use this method to set the size of sprite when rendering (hard code those values since you know what they are beforehand).

    current_framerate link

    Returns a float value representing the framerate of your game. This is an approximation/moving average of your framerate and should eventually settle to 60fps.

    def tick args
      # render a label to the screen that shows the current framerate
      # formatted as a floating point number with two decimal places
      args.outputs.labels << { x: 30, y: 30.from_top, text: "#{args.gtk.current_framerate.to_sf}" }
    end
    

    framerate_diagnostics_primitives link

    Returns a set of primitives that can be rendered to the screen which provide more detailed information about the speed of your simulation (framerate, draw call count, mouse position, etc).

    def tick args
      args.outputs.primitives << args.gtk.framerate_diagnostics_primitives
    end
    

    warn_array_primitives! link

    This function helps you audit your game of usages of array-based primitives. While array-based primitives are simple to create and use, they are slower to process than Hash or Class based primitives.

    def tick args
      # enable array based primitives warnings
      args.gtk.warn_array_primitives!
    
      # array-based primitive elsewhere in code
      # an log message will be posted giving the location of the array
      # based primitive usage
      args.outputs.sprites << [100, 100, 200, 200, "sprites/square/blue.png"]
    
      # instead of using array based primitives, migrate to hashes as needed
      args.outputs.sprites << {
        x: 100,
        y: 100,
        w: 200,
        h: 200, path:
        "sprites/square/blue.png"
      }
    end
    

    benchmark link

    You can use this function to compare the relative performance of blocks of code.

    def tick args
      # press r to run benchmark
      if args.inputs.keyboard.key_down.r
        args.gtk.console.show
        args.gtk.benchmark iterations: 1000, # number of iterations
                           # label for experiment
                           using_numeric_map: -> () {
                             # experiment body
                             v = 100.map_with_index do |i|
                               i * 100
                             end
                           },
                           # label for experiment
                           using_numeric_times: -> () {
                             # experiment body
                             v = []
                             100.times do |i|
                               v << i * 100
                             end
                           }
      end
    end
    

    notify! link

    Given a string, this function will present a message at the bottom of your game. This method is only invoked in dev mode and is useful for debugging.

    An optional parameter of duration (number value representing ticks) can also be passed in. The default value if 300 ticks (5 seconds).

    def tick args
      if args.inputs.mouse.click
        args.gtk.notify! "Mouse was clicked!"
      end
    
      if args.inputs.keyboard.key_down.r
        # optional duration parameter
        args.gtk.notify! "R key was pressed!", 600 # present message for 10 seconds/600 frames
      end
    end
    

    notify_extended! link

    Has similar behavior as notify! except you have additional options to show messages in a production environment.

    def tick args
      if args.inputs.mouse.click
        args.gtk.notify_extended! message: "message",
                                  duration: 300,
                                  env: :prod
      end
    end
    

    slowmo! link

    Given a numeric value representing the factor of 60fps. This function will bring your simulation loop down to slower rate. This method is intended to be used for debugging purposes.

    def tick args
      # set your simulation speed to (15 fps): args.gtk.slowmo! 4
      # set your simulation speed to (1 fps): args.gtk.slowmo! 60
      # set your simulation speed to (30 fps):
      args.gtk.slowmo! 2
    end
    

    Remove this line from your tick method will automatically set your simulation speed back to 60 fps.

    show_console link

    Shows the DragonRuby console. Useful when debugging/customizing an in-game dev workflow.

    hide_console link

    Shows the DragonRuby console. Useful when debugging/customizing an in-game dev workflow.

    enable_console link

    Enables the DragonRuby Console so that it can be presented by pressing the tilde key (the key next to the number 1 key).

    disable_console link

    Disables the DragonRuby Console so that it won't show up even if you press the tilde key or call args.gtk.show_console.

    disable_reset_via_ctrl_r link

    By default, pressing CTRL+R invokes $gtk.reset_next_tick (safely resetting your game with a convenient key combo).

    If you want to disable this behavior, add the following to the main.rb:

    def tick args
      ...
    end
    
    $gtk.disable_reset_via_ctrl_r
    

    NOTE: $gtk.disable_console will also disable the CTRL+R reset behavior.

    start_recording link

    Resets the game to tick 0 and starts recording gameplay. Useful for visual regression tests/verification.

    stop_recording link

    Function takes in a destination file for the currently recording gameplay. This file can be used to replay a recording.

    cancel_recording link

    Function cancels a gameplay recording session and discards the replay.

    start_replay link

    Given a file that represents a recording, this method will run the recording against the current codebase.

    You can start a replay from the command line also:

    # first argument: the game directory
    # --replay switch is the file path relative to the game directory
    # --speed switch is optional. a value of 4 will run the replay and game at 4x speed
    # cli command example is in the context of Linux and Mac, for Windows the binary would be ./dragonruby.exe
    ./dragonruby ./mygame --replay ./replay.txt --speed 4
    

    stop_replay link

    Function stops a replay that is currently executing.

    get_base_dir link

    Returns the path to the location of the dragonruby binary. In production mode, this value will be the same as the value returned by get_game_dir. Function should only be used for debugging/development workflows.

    get_game_dir link

    Returns the location within sandbox storage that the game is running. When developing your game, this value will be your mygame directory. In production, it'll return a value that is OS specific (eg the Roaming directory on Windows or the Application Support directory on Mac).

    Invocations of ~(write|append)_file will write to this sandboxed directory.

    get_game_dir_url link

    Returns a url encoded string representing the sandbox location for game data.

    open_game_dir link

    Opens the game directory in the OS's file explorer. This should be used for debugging purposes only.

    write_file_root link

    Given a file path and contents, the contents will be written to a directory outside of the game directory. This method should be used for development purposes only. In production this method will write to the same sandboxed location as write_file.

    append_file_root link

    Has the same behavior as write_file_root except that it appends the contents as opposed to overwriting them.

    argv link

    Returns a string representing the command line arguments passed to the DragonRuby binary. This should be used for development/debugging purposes only.

    cli_arguments link

    Returns a Hash for command line arguments in the format of --switch value (two hyphens preceding the switch flag with the value seperated by a space). This should be used for development/debugging purposes only.

    download_stb_rb(_raw) link

    These two functions can help facilitate the integration of external code files. OSS contributors are encouraged to create libraries that all fit in one file (lowering the barrier to entry for adoption).

    Examples:

    def tick args
    end
    
    # option 1:
    # source code will be downloaded from the specified GitHub url, and saved locally with a
    # predefined folder convension.
    $gtk.download_stb_rb "https://github.com/xenobrain/ruby_vectormath/blob/main/vectormath_2d.rb"
    
    # option 2:
    # source code will be downloaded from the specified GitHub username, repository, and file.
    # code will be saved locally with a predefined folder convension.
    $gtk.download_stb_rb "xenobrain", "ruby_vectormath", "vectormath_2d.rb"
    
    # option 3:
    # source code will be downloaded from a direct/raw url and saved to a direct/raw local path.
    $gtk.download_stb_rb_raw "https://raw.githubusercontent.com/xenobrain/ruby_vectormath/main/vectormath_2d.rb",
                             "lib/xenobrain/ruby_vectionmath/vectormath_2d.rb"
    

    reload_history link

    Returns a Hash representing the code files that have be loaded for your game along with timings for the events. This should be used for development/debugging purposes only.

    reload_history_pending link

    Returns a Hash for files that have been queued for reload, but haven't been processed yet. This should be used for development/debugging purposes only.

    reload_if_needed link

    Given a file name, this function will queue the file for reload if it's been modified. An optional second parameter can be passed in to signify if the file should be forced loaded regardless of modified time (true means to force load, false means to load only if the file has been modified). This function should be used for development/debugging purposes only.

    State (args.state) link

    Store your game state inside of this state. Properties with arbitrary nesting is allowed and a backing Entity will be created on your behalf.

    def tick args
      args.state.player.x ||= 0
      args.state.player.y ||= 0
    end
    

    entity_id link

    Entities automatically receive an entity_id of type Fixnum.

    entity_type link

    Entities can have an entity_type which is represented as a Symbol.

    created_at link

    Entities have created_at set to args.state.tick_count when they are created.

    created_at_elapsed link

    Returns the elapsed number of ticks since creation.

    global_created_at link

    Entities have global_created_at set to Kernel.global_tick_count when they are created.

    global_created_at_elapsed link

    Returns the elapsed number of global ticks since creation.

    as_hash link

    Entity cast to a Hash so you can update values as if you were updating a Hash.

    new_entity link

    Creates a new Entity with a type, and initial properties. An option block can be passed to change the newly created entity:

    def tick args
      args.state.player ||= args.state.new_entity :player, x: 0, y: 0 do |e|
        e.max_hp = 100
        e.hp     = e.max_hp * rand
      end
    end
    

    new_entity_strict link

    Creates a new Strict Entity. While Entities created via args.state.new_entity can have new properties added later on, Entities created using args.state.new_entity_strict must define all properties that are allowed during its initialization. Attempting to add new properties after initialization will result in an exception.

    args.state.tick_count link

    Returns the current tick of the game. args.state.tick_count is 0 when the game is first started or if the game is reset via $gtk.reset.

    add_caller_to_puts! link

    If you need to hund down rogue puts statements in your code do:

    def tick args
      # adding the following line to the TOP of your tick method
      # will print ~caller~ along side each ~puts~ statement
      $gtk.add_caller_to_puts!
    end
    

    Geometry (args.geometry) link

    The Geometry module contains methods for calculations that are frequently used in game development.

    The following functions of Geometry are mixed into Hash, Array, and DragonRuby's Entity class:

    You can invoke the functions above using either the mixin variant or the module variant. Example:

    def tick args
      # define to rectangles
      rect_1 = { x: 0, y: 0, w: 100, h: 100 }
      rect_2 = { x: 50, y: 50, w: 100, h: 100 }
    
      # mixin variant
      # call geometry method function from instance of a Hash class
      puts rect_1.intersect_rect?(rect_2)
    
      # OR
    
      # module variants
      puts args.geometry.intersect_rect?(rect_1, rect_2)
      puts Geometry.intersect_rect?(rect_1, rect_2)
    end
    

    intersect_rect? link

    Invocation variants:

    Given two rectangle primitives this function will return true or false depending on if the two rectangles intersect or not. An optional final parameter can be passed in representing the tolerence of overlap needed to be considered a true intersection. The default value of tolerance is 0.1 which keeps the function from returning true if only the edges of the rectangles overlap.

    :anchor_x, and anchor_y is taken into consideration if the objects respond to these methods.

    Here is an example where one rectangle is stationary, and another rectangle is controlled using directional input. The rectangles change color from blue to read if they intersect.

    def tick args
      # define a rectangle in state and position it
      # at the center of the screen with a color of blue
      args.state.box_1 ||= {
        x: 640 - 20,
        y: 360 - 20,
        w: 40,
        h: 40,
        r: 0,
        g: 0,
        b: 255
      }
    
      # create another rectangle in state and position it
      # at the far left center
      args.state.box_2 ||= {
        x: 0,
        y: 360 - 20,
        w: 40,
        h: 40,
        r: 0,
        g: 0,
        b: 255
      }
    
      # take the directional input and use that to move the second rectangle around
      # increase or decrease the x value based on if left or right is held
      args.state.box_2.x += args.inputs.left_right * 5
      # increase or decrease the y value based on if up or down is held
      args.state.box_2.y += args.inputs.up_down * 5
    
      # change the colors of the rectangles based on whether they
      # intersect or not
      if args.state.box_1.intersect_rect? args.state.box_2
        args.state.box_1.r = 255
        args.state.box_1.g = 0
        args.state.box_1.b = 0
    
        args.state.box_2.r = 255
        args.state.box_2.g = 0
        args.state.box_2.b = 0
      else
        args.state.box_1.r = 0
        args.state.box_1.g = 0
        args.state.box_1.b = 255
    
        args.state.box_2.r = 0
        args.state.box_2.g = 0
        args.state.box_2.b = 255
      end
    
      # render the rectangles as border primitives on the screen
      args.outputs.borders << args.state.box_1
      args.outputs.borders << args.state.box_2
    end
    

    inside_rect? link

    Invocation variants:

    Given two rectangle primitives this function will return true or false depending on if the first rectangle (or self) is inside of the second rectangle.

    Here is an example where one rectangle is stationary, and another rectangle is controlled using directional input. The rectangles change color from blue to read if the movable rectangle is entirely inside the stationary rectangle.

    :anchor_x, and anchor_y is taken into consideration if the objects respond to these methods.

    def tick args
      # define a rectangle in state and position it
      # at the center of the screen with a color of blue
      args.state.box_1 ||= {
        x: 640 - 40,
        y: 360 - 40,
        w: 80,
        h: 80,
        r: 0,
        g: 0,
        b: 255
      }
    
      # create another rectangle in state and position it
      # at the far left center
      args.state.box_2 ||= {
        x: 0,
        y: 360 - 10,
        w: 20,
        h: 20,
        r: 0,
        g: 0,
        b: 255
      }
    
      # take the directional input and use that to move the second rectangle around
      # increase or decrease the x value based on if left or right is held
      args.state.box_2.x += args.inputs.left_right * 5
      # increase or decrease the y value based on if up or down is held
      args.state.box_2.y += args.inputs.up_down * 5
    
      # change the colors of the rectangles based on whether they
      # intersect or not
      if args.state.box_2.inside_rect? args.state.box_1
        args.state.box_1.r = 255
        args.state.box_1.g = 0
        args.state.box_1.b = 0
    
        args.state.box_2.r = 255
        args.state.box_2.g = 0
        args.state.box_2.b = 0
      else
        args.state.box_1.r = 0
        args.state.box_1.g = 0
        args.state.box_1.b = 255
    
        args.state.box_2.r = 0
        args.state.box_2.g = 0
        args.state.box_2.b = 255
      end
    
      # render the rectangles as border primitives on the screen
      args.outputs.borders << args.state.box_1
      args.outputs.borders << args.state.box_2
    end
    

    scale_rect link

    Given a Rectangle this function returns a new rectangle with a scaled size.

    def tick args
      # a rect at the center of the screen
      args.state.rect_1 ||= { x: 640 - 20, y: 360 - 20, w: 40, h: 40 }
    
      # render the rect
      args.outputs.borders << args.state.rect_1
    
      # the rect half the size with the x and y position unchanged
      args.outputs.borders << args.state.rect_1.scale_rect(0.5)
    
      # the rect double the size, repositioned in the center given anchor optional arguments
      args.outputs.borders << args.state.rect_1.scale_rect(2, 0.5, 0.5)
    end
    

    scale_rect_extended link

    The behavior is similar to scale_rect except that you can independently control the scale of each axis. The parameters are all named:

    def tick args
      baseline_rect = { x: 640 - 20, y: 360 - 20, w: 40, h: 40 }
      args.state.rect_1 ||= baseline_rect
      args.state.rect_2 ||= baseline_rect.scale_rect_extended(percentage_x: 2,
                                                              percentage_y: 0.5,
                                                              anchor_x: 0.5,
                                                              anchor_y: 1.0)
      args.outputs.borders << args.state.rect_1
      args.outputs.borders << args.state.rect_2
    end
    

    anchor_rect link

    Returns a new rect that is anchored by an anchor_x and anchor_y value. The width and height of the rectangle is taken into consideration when determining the anchor position:

    def tick args
      args.state.rect ||= {
        x: 640,
        y: 360,
        w: 100,
        h: 100
      }
    
      # rect's center: 640 + 50, 360 + 50
      args.outputs.borders << args.state.rect.anchor_rect(0, 0)
    
      # rect's center: 640, 360
      args.outputs.borders << args.state.rect.anchor_rect(0.5, 0.5)
    
      # rect's center: 640, 360
      args.outputs.borders << args.state.rect.anchor_rect(0.5, 0)
    end
    

    angle_from link

    Invocation variants:

    Returns an angle in degrees from the end_point to the start_point (if you want the value in radians, you can call .to_radians on the value returned):

    def tick args
      rect_1 ||= {
        x: 0,
        y: 0,
      }
    
      rect_2 ||= {
        x: 100,
        y: 100,
      }
    
      angle = rect_1.angle_from rect_2 # returns 225 degrees
      angle_radians = angle.to_radians
      args.outputs.labels << { x: 30, y: 30.from_top, text: "#{angle}, #{angle_radians}" }
    
      angle = args.geometry.angle_from rect_1, rect_2 # returns 225 degrees
      angle_radians = angle.to_radians
      args.outputs.labels << { x: 30, y: 60.from_top, text: "#{angle}, #{angle_radians}" }
    end
    

    angle_to link

    Invocation variants:

    Returns an angle in degrees to the end_point from the start_point (if you want the value in radians, you can call .to_radians on the value returned):

    def tick args
      rect_1 ||= {
        x: 0,
        y: 0,
      }
    
      rect_2 ||= {
        x: 100,
        y: 100,
      }
    
      angle = rect_1.angle_to rect_2 # returns 45 degrees
      angle_radians = angle.to_radians
      args.outputs.labels << { x: 30, y: 30.from_top, text: "#{angle}, #{angle_radians}" }
    
      angle = args.geometry.angle_to rect_1, rect_2 # returns 45 degrees
      angle_radians = angle.to_radians
      args.outputs.labels << { x: 30, y: 60.from_top, text: "#{angle}, #{angle_radians}" }
    end
    

    distance link

    Returns the distance between two points;

    def tick args
      rect_1 ||= {
        x: 0,
        y: 0,
      }
    
      rect_2 ||= {
        x: 100,
        y: 100,
      }
    
      distance = args.geometry.distance rect_1, rect_2
      args.outputs.labels << {
        x: 30,
        y: 30.from_top,
        text: "#{distance}"
      }
    
      args.outputs.lines << {
        x: rect_1.x,
        y: rect_1.y,
        x2: rect_2.x,
        y2: rect_2.y
      }
    end
    

    point_inside_circle? link

    Invocation variants:

    circle_center can also contain the radius value (instead of passing it as a separate argument).

    Returns true if a point is inside of a circle defined as a center point and radius.

    def tick args
      # define circle center
      args.state.circle_center ||= {
        x: 640,
        y: 360
      }
    
      # define circle radius
      args.state.circle_radius ||= 100
    
      # define point
      args.state.point_1 ||= {
        x: 100,
        y: 100
      }
    
      # allow point to be moved using keyboard
      args.state.point_1.x += args.inputs.left_right * 5
      args.state.point_1.y += args.inputs.up_down * 5
    
      # determine if point is inside of circle
      intersection = args.geometry.point_inside_circle? args.state.point_1,
                                                        args.state.circle_center,
                                                        args.state.circle_radius
    
      # render point as a square
      args.outputs.sprites << {
        x: args.state.point_1.x - 20,
        y: args.state.point_1.y - 20,
        w: 40,
        h: 40,
        path: "sprites/square/blue.png"
      }
    
      # if there is an intersection, render a red circle
      # otherwise render a blue circle
      if intersection
        args.outputs.sprites << {
          x: args.state.circle_center.x - args.state.circle_radius,
          y: args.state.circle_center.y - args.state.circle_radius,
          w: args.state.circle_radius * 2,
          h: args.state.circle_radius * 2,
          path: "sprites/circle/red.png",
          a: 128
        }
      else
        args.outputs.sprites << {
          x: args.state.circle_center.x - args.state.circle_radius,
          y: args.state.circle_center.y - args.state.circle_radius,
          w: args.state.circle_radius * 2,
          h: args.state.circle_radius * 2,
          path: "sprites/circle/blue.png",
          a: 128
        }
      end
    end
    

    center_inside_rect link

    Invocation variants:

    Given a target rect and a reference rect, the target rect is centered inside the reference rect (a new rect is returned).

    def tick args
      rect_1 = {
        x: 0,
        y: 0,
        w: 100,
        h: 100
      }
    
      rect_2 = {
        x: 640 - 100,
        y: 360 - 100,
        w: 200,
        h: 200
      }
    
      centered_rect = args.geometry.center_inside_rect rect_1, rect_2
      # OR
      # centered_rect = rect_1.center_inside_rect rect_2
    
      args.outputs.solids << rect_1.merge(r: 255)
      args.outputs.solids << rect_2.merge(b: 255)
      args.outputs.solids << centered_rect.merge(g: 255)
    end
    

    ray_test link

    Given a point and a line, ray_test returns one of the following symbols based on the location of the point relative to the line: :left, :right, :on

    def tick args
      # create a point based off of the mouse location
      point = {
        x: args.inputs.mouse.x,
        y: args.inputs.mouse.y
      }
    
      # draw a line from the bottom left to the top right
      line = {
        x: 0,
        y: 0,
        x2: 1280,
        y2: 720
      }
    
      # perform ray_test on point and line
      ray = args.geometry.ray_test point, line
    
      # output the results of ray test at mouse location
      args.outputs.labels << {
        x: point.x,
        y: point.y + 25,
        text: "#{ray}",
        alignment_enum: 1,
        vertical_alignment_enum: 1,
      }
    
      # render line
      args.outputs.lines << line
    
      # render point
      args.outputs.solids << {
        x: point.x - 5,
        y: point.y - 5,
        w: 10,
        h: 10
      }
    end
    

    line_rise_run link

    Given a line, this function returns a Hash with x and y keys representing a normalized representation of the rise and run of the line.

    def tick args
      # draw a line from the bottom left to the top right
      line = {
        x: 0,
        y: 0,
        x2: 1280,
        y2: 720
      }
    
      # get rise and run of line
      rise_run = args.geometry.line_rise_run line
    
      # output the rise and run of line
      args.outputs.labels << {
        x: 640,
        y: 360,
        text: "#{rise_run}",
        alignment_enum: 1,
        vertical_alignment_enum: 1,
      }
    
      # render the line
      args.outputs.lines << line
    end
    

    rotate_point link

    Given a point and an angle in degrees, a new point is returned that is rotated around the origin by the degrees amount. An optional third argument can be provided to rotate the angle around a point other than the origin.

    def tick args
      args.state.rotate_amount ||= 0
      args.state.rotate_amount  += 1
    
      if args.state.rotate_amount >= 360
        args.state.rotate_amount = 0
      end
    
      point_1 = {
        x: 100,
        y: 100
      }
    
      # rotate point around 0, 0
      rotated_point_1 = args.geometry.rotate_point point_1,
                                                   args.state.rotate_amount
    
      args.outputs.solids << {
        x: rotated_point_1.x - 5,
        y: rotated_point_1.y - 5,
        w: 10,
        h: 10
      }
    
      point_2 = {
        x: 640 + 100,
        y: 360 + 100
      }
    
      # rotate point around center screen
      rotated_point_2 = args.geometry.rotate_point point_2,
                                                   args.state.rotate_amount,
                                                   x: 640, y: 360
    
      args.outputs.solids << {
        x: rotated_point_2.x - 5,
        y: rotated_point_2.y - 5,
        w: 10,
        h: 10
      }
    end
    

    find_intersect_rect link

    Given a rect and a collection of rects, find_intersect_rect returns the first rect that intersects with the the first parameter.

    :anchor_x, and anchor_y is taken into consideration if the objects respond to these methods.

    If you find yourself doing this:

    collision = args.state.terrain.find { |t| t.intersect_rect? args.state.player }
    

    Consider using find_intersect_rect instead (it's more descriptive and faster):

    collision = args.geometry.find_intersect_rect args.state.player, args.state.terrain
    

    find_all_intersect_rect link

    Given a rect and a collection of rects, find_all_intersect_rect returns all rects that intersects with the the first parameter.

    :anchor_x, and anchor_y is taken into consideration if the objects respond to these methods.

    If you find yourself doing this:

    collisions = args.state.terrain.find_all { |t| t.intersect_rect? args.state.player }
    

    Consider using find_all_intersect_rect instead (it's more descriptive and faster):

    collisions = args.geometry.find_all_intersect_rect args.state.player, args.state.terrain
    

    find_intersect_rect_quad_tree link

    This is a faster collision algorithm for determining if a rectangle intersects any rectangle in an array. In order to use find_intersect_rect_quad_tree, you must first generate a quad tree data structure using create_quad_tree. Use this function if find_intersect_rect isn't fast enough.

    def tick args
      # create a player
      args.state.player ||= {
        x: 640 - 10,
        y: 360 - 10,
        w: 20,
        h: 20
      }
    
      # allow control of player movement using arrow keys
      args.state.player.x += args.inputs.left_right * 5
      args.state.player.y += args.inputs.up_down * 5
    
      # generate 40 random rectangles
      args.state.boxes ||= 40.map do
        {
          x: 1180 * rand + 50,
          y: 620 * rand + 50,
          w: 100,
          h: 100
        }
      end
    
      # generate a quad tree based off of rectangles.
      # the quad tree should only be generated once for
      # a given array of rectangles. if the rectangles
      # change, then the quad tree must be regenerated
      args.state.quad_tree ||= args.geometry.quad_tree_create args.state.boxes
    
      # use quad tree and find_intersect_rect_quad_tree to determine collision with player
      collision = args.geometry.find_intersect_rect_quad_tree args.state.player,
                                                              args.state.quad_tree
    
      # if there is a collision render a red box
      if collision
        args.outputs.solids << collision.merge(r: 255)
      end
    
      # render player as green
      args.outputs.solids << args.state.player.merge(g: 255)
    
      # render boxes as borders
      args.outputs.borders << args.state.boxes
    end
    

    find_all_intersect_rect_quad_tree link

    This is a faster collision algorithm for determining if a rectangle intersects other rectangles in an array. In order to use find_all_intersect_rect_quad_tree, you must first generate a quad tree data structure using create_quad_tree. Use this function if find_all_intersect_rect isn't fast enough.

    def tick args
      # create a player
      args.state.player ||= {
        x: 640 - 10,
        y: 360 - 10,
        w: 20,
        h: 20
      }
    
      # allow control of player movement using arrow keys
      args.state.player.x += args.inputs.left_right * 5
      args.state.player.y += args.inputs.up_down * 5
    
      # generate 40 random rectangles
      args.state.boxes ||= 40.map do
        {
          x: 1180 * rand + 50,
          y: 620 * rand + 50,
          w: 100,
          h: 100
        }
      end
    
      # generate a quad tree based off of rectangles.
      # the quad tree should only be generated once for
      # a given array of rectangles. if the rectangles
      # change, then the quad tree must be regenerated
      args.state.quad_tree ||= args.geometry.quad_tree_create args.state.boxes
    
      # use quad tree and find_intersect_rect_quad_tree to determine collision with player
      collisions = args.geometry.find_all_intersect_rect_quad_tree args.state.player,
                                                                  args.state.quad_tree
    
      # if there is a collision render a red box
      args.outputs.solids << collisions.map { |c| c.merge(r: 255) }
    
      # render player as green
      args.outputs.solids << args.state.player.merge(g: 255)
    
      # render boxes as borders
      args.outputs.borders << args.state.boxes
    end
    

    line_angle link

    Given a line, this function will return the angle of the line in degrees.

    vec2_dot_product link

    Given two Hashes with x and y keys (or Objects that respond to x and y), this function will return the dot product of the two vectors.

    Note: Take a look at this sample app for a non-trivial example of how to use this function: ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/

    vec2_normalize link

    Given a Hash with x and y keys (or an Object that responds to x and y), this function will return a Hash with x and y keys that represents the vector normalized.

    Note: Take a look at this sample app for a non-trivial example of how to use this function: ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/

    line_vec2 link

    Given a line, this function will return a Hash with x and y keys that represents the vector of the line.

    Note: Take a look at this sample app for a non-trivial example of how to use this function: ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/

    vec2_magnitude link

    Given a Hash with x and y keys (or an Object that responds to x and y), this function will return the magnitude of the vector.

    Note: Take a look at this sample app for a non-trivial example of how to use this function: ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/

    distance_squared link

    Given two Hashes with x and y keys (or Objects that respond to x and y), this function will return the distance squared between the two points. This is useful when you only want to compare distances, and don't need the actual distance.

    Note: Take a look at this sample app for a non-trivial example of how to use this function: ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/

    vec2_normal link

    Given a Hash with x and y keys (or an Object that responds to x and y), this function will return a Hash with x and y keys that represents the normal of the vector.

    Note: Take a look at this sample app for a non-trivial example of how to use this function: ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/

    circle_intersect_line? link

    The first parameters is a Hash with x, y, and radius keys (or an Object that responds to x, y, and radius).

    The second parameter is a Hash with x1, y1, x2, and y2 keys (or an Object that responds to x1, y1, x2, and y2).

    This function will return true if the circle intersects the line, and false if it does not.

    Note: Take a look at this sample app for a non-trivial example of how to use this function: ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/

    line_normal link

    The first parameter is a line (a Hash with x1, y1, x2, and y2 keys, or an Object that responds to x1, y1, x2, and y2).

    The second parameter is a Hash with x and y keys (or an Object that responds to x and y).

    This function will return a Hash with x and y keys that represents the normal of the line relative to the point provided.

    Note: Take a look at this sample app for a non-trivial example of how to use this function: ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/

    point_on_line? link

    The first parameter is a point (a Hash with x and y keys, or an Object that responds to x and y).

    The second parameter is a line (a Hash with x1, y1, x2, and y2 keys, or an Object that responds to x1, y1, x2, and y2).

    This function will return true if the point is on the line, and false if it is not.

    Note: Take a look at this sample app for a non-trivial example of how to use this function: ./samples/04_physics_and_collisions/11_bouncing_ball_with_gravity/

    find_collisions link

    Given an Array of rects, returns a Hash of collisions. Each entry in the return Hash maps two rects from the input Array that intersect.

    Note that in the event of an intersection of rects A and B, the returned Hash will contain two entries: {A=>B,B=>A}

    def tick(args)
      args.state.squares ||= []
      args.state.alphabet ||= ('A'..'Z').to_a
    
      # reset the squares if the user presses 'R'
      args.state.squares = [] if args.inputs.keyboard.r
    
      # add a new square every 4 ticks until we get to 26
      # the last part of the condition is to make sure we always have at least 1 square before
      # we start checking for collisions, otherwise #find_collisions will throw an error
      if (args.state.tick_count % 4 == 0 && args.state.squares.size < 26) || args.state.squares.size == 0
    
        # add a new square to the array with a random position, with some padding
        # so that the squares don't spawn too close to the edge of the screen
        # we also set the text to a random letter from the alphabet so we can re-use
        # the same hash for both the squares and their labels
        args.state.squares << {
          x: rand(1280 - 200) + 100, y: rand(720 - 300) + 100,
          w: 50, h: 50,
          text: args.state.alphabet[args.state.squares.size],
          alignment_enum: 1, # center the text
          r: 0, g: 255, b: 0, a: 128
        }
      end
    
      # check for collisions between the squares. this returns a hash of the
      # colliding squares, with the key being the first square and the value
      # being the second square
      collisions = args.geometry.find_collisions(args.state.squares)
      collisions.each do |key, value|
        key.merge!(r: 255, g: 0, b: 0)
        value.merge!(r: 0, g: 0, b: 255)
      end
    
      # render instructions and collision info
      args.outputs.labels << {x: 30, y: 20.from_top, text: "Press 'R' to reset" }
      args.outputs.labels << {x: 30, y: 45.from_top, text: "#{args.state.squares.size} squares, #{collisions.size} collisions" }
      args.outputs.labels << {x: 30, y: 70.from_top, text: "#{collisions.map { |k, v| "{#{k.text}=>#{v.text}}" }.join(', ')}" }
    
      # render the squares and their labels
      args.outputs.solids << args.state.squares
      args.outputs.labels << args.state.squares.map_with_index do |square, i|
        square.merge(
          x: square.x + 25, y: square.y + 35, # center the text in the square
          r: 0, g: 0, b: 0                    # make it black
        )
      end
    end
    

    create_quad_tree link

    Generates a quad tree from an array of rectangles. See find_intersect_rect_quad_tree for usage.

    Audio (args.audio) link

    Hash that contains audio sources that are playing.

    Sounds that don't specify looping: true will be removed automatically from the hash after the playback ends. Looping sounds or sounds that should stop early must be removed manually.

    When you assign a hash to an audio output, a :length key will be added to the hash on the following tick. This will tell you the duration of the audio file in seconds (float).

    volume link

    You can globally control the volume for all audio using args.audio.volume. Example:

    def tick args
      if args.inputs.down
        args.audio.volume -= 0.01
      elsif args.inputs.up
        args.audio.volume += 0.01
      end
    end
    

    One-Time Sounds link

    Here's how to play audio one-time (does not loop).

    def tick args
      # play a one-time non-looping sound every second
      if (args.state.tick_count % 60) == 0
        args.audio[:coin] = { input: "sounds/coin.wav" }
        # OR
        args.outputs.sounds << "sounds/coin.wav"
      end
    end
    

    Looping Audio link

    Here's how to play audio that loops (eg background music), and how to stop the sound.

    def tick args
      if args.state.tick_count == 0
        args.audio[:bg_music] = { input: "sounds/bg-music.ogg", looping: true }
      end
    
      # stop sound if space key is pressed
      if args.inputs.keyboard.key_down.space
        args.audio[:bg_music] = nil
        # OR
        args.audio.delete :bg_music
      end
    end
    

    Setting Additional Audio Properties link

    Here are additional properties that can be set.

    def tick args
      # The values below (except for input of course) are the default values that apply if you don't
      # specify the value in the hash.
      args.audio[:my_audio] ||= {
        input: 'sound/boom.wav',  # file path relative to mygame directory
        gain:    1.0,             # Volume (float value 0.0 to 1.0)
        pitch:   1.0,             # Pitch of the sound (1.0 = original pitch)
        paused:  false,           # Set to true to pause the sound at the current playback position
        looping: true,            # Set to true to loop the sound/music until you stop it
        foobar:  :baz,            # additional keys/values can be safely added to help with context/game logic (ie metadata)
        x: 0.0, y: 0.0, z: 0.0    # Relative position to the listener, x, y, z from -1.0 to 1.0
      }
    end
    

    IMPORTANT: Please take note that gain and pitch must be given float values (eg gain: 1.0, not gain: 1 or game: 0).

    Advanced Audio Manipulation (Crossfade) link

    Take a look at the Audio Mixer sample app for a non-trival example of how to use these properties. The sample app is located within the DragonRuby zip file at ./samples/07_advanced_audio/01_audio_mixer.

    Here's an example of crossfading two bg music tracks.

    def tick args
      # start bg-1.ogg at the start
      if args.state.tick_count == 0
        args.audio[:bg_music] = { input: "sounds/bg-1.ogg", looping: true, gain: 0.0 }
      end
    
      # if space is pressed cross fade to new bg music
      if args.inputs.keyboard.key_down.space
        # get the current bg music and create a new audio entry that represents the crossfade
        current_bg_music = args.audio[:bg_music]
    
        # cross fade audio entry
        args.audio[:bg_music_fade] = {
          input:    current_bg_music[:input],
          looping:  true,
          gain:     current_bg_music[:gain],
          pitch:    current_bg_music[:pitch],
          paused:   false,
          playtime: current_bg_music[:playtime]
        }
    
        # replace the current playing background music (toggling between bg-1.ogg and bg-2.ogg)
        # set the gain/volume to 0.0 (this will be increased to 1.0 accross ticks)
        new_background_music = { looping: true, gain: 0.0 }
    
        # determine track to play (swap between bg-1 and bg-2)
        new_background_music[:input] = if current_bg_music.input == "sounds/bg-1.ogg"
                                         "sounds/bg-2.ogg"
                                       else
                                         "sounds/bg-2.ogg"
                                       end
    
        # bg music audio entry
        args.audio[:bg_music] = new_background_music
      end
    
      # process cross fade (happens every tick)
      # increase the volume of bg_music every tick until it's at 100%
      if args.audio[:bg_music] && args.audio[:bg_music].gain < 1.0
        # increase the gain 1% every tick until we are at 100%
        args.audio[:bg_music].gain += 0.01
        # clamp value to 1.0 max value
        args.audio[:bg_music].gain = 1.0 if args.audio[:bg_music].gain > 1.0
      end
    
      # decrease the volume of cross fade bg music until it's 0.0, then delete it
      if args.audio[:bg_music_fade] && args.audio[:bg_music_fade].gain > 0.0
        # decrease by 1% every frame
        args.audio[:bg_music_fade].gain -= 0.01
        # delete audio when it's at 0%
        if args.audio[:bg_music_fade].gain <= 0.0
          args.audio[:bg_music_fade] = nil
        end
      end
    end
    

    Audio encoding trouble shooting link

    If audio doesn't seem to be working, try re-encoding it via ffmpeg:

    # re-encode ogg
    ffmpeg -i ./mygame/sounds/SOUND.ogg -ac 2 -b:a 160k -ar 44100 -acodec libvorbis ./mygame/sounds/SOUND-converted.ogg
    
    # convert wav to ogg
    ffmpeg -i ./mygame/sounds/SOUND.wav -ac 2 -b:a 160k -ar 44100 -acodec libvorbis ./mygame/sounds/SOUND-converted.ogg
    

    Audio synthesis link

    Instead of a path to an audio file you can specify an array [channels, sample_rate, sound_source] for input to procedurally generate sound. You do this by providing an array of float values between -1.0 and 1.0 that describe the waveform you want to play.

    Sound source link

    A sound source can be one of two things:

    When you specify 2 for channels, then the generated sample array will be played back in an interleaved manner. The first element is the first sample for the left channel, the second element is the first sample for the right channel, the third element is the second sample for the left channel etc.

    Example: link

    def tick args
      sample_rate = 48000
    
      generate_sine_wave = lambda do
        frequency = 440.0 # A5
        samples_per_period = (sample_rate / frequency).ceil
        one_period = samples_per_period.map_with_index { |i|
          Math.sin((2 * Math::PI) * (i / samples_per_period))
        }
        one_period * frequency # Generate 1 second worth of sound
      end
    
      args.audio[:my_audio] ||= {
        input: [1, sample_rate, generate_sine_wave]
      }
    end
    

    Easing (args.easing) link

    A set of functions that allow you to determine the current progression of an easing function.

    ease link

    This function will give you a float value between 0 and 1 that represents a percentage. You need to give the funcation a start_tick, current_tick, duration, and easing definitions.

    This YouTube video is a fantastic introduction to easing functions: https://www.youtube.com/watch?v=mr5xkf6zSzk

    Examples link

    This example shows how to fade in a label at frame 60 over two seconds (120 ticks). The :identity definition implies a linear fade: f(x) -> x.

    def tick args
      fade_in_at   = 60
      current_tick = args.state.tick_count
      duration     = 120
      percentage   = args.easing.ease fade_in_at,
                                      current_tick,
                                      duration,
                                      :identity
      alpha = 255 * percentage
      args.outputs.labels << { x: 640,
                               y: 320, text: "#{percentage.to_sf}",
                               alignment_enum: 1,
                               a: alpha }
    end
    

    This example will move a box at a linear speed from 0 to 1280.

    def tick args
      start_time = 10
      duration = 60
      current_progress = args.easing.ease start_time,
                                          args.state.tick_count,
                                          duration,
                                          :identity
      args.outputs.solids << { x: 1280 * current_progress, y: 360, w: 10, h: 10 }
    end
    

    Easing Definitions link

    There are a number of easing definitions availble to you:

    :identity

    The easing definition for :identity is f(x) = x. For example, if start_tick is 0, current_tick is 50, and duration is 100, then args.easing.ease 0, 50, 100, :identity will return 0.5 (since tick 50 is half way between 0 and 100).

    :flip

    The easing definition for :flip is f(x) = 1 - x. For example, if start_tick is 0, current_tick is 10, and duration is 100, then args.easing.ease 0, 10, 100, :flip will return 0.9 (since tick 10 means 100% - 10%).

    :quad, :cube, :quart, :quint

    These are the power easing definitions. :quad is f(x) = x * x (x squared), :cube is f(x) = x * x * x (x cubed), etc.

    The power easing definitions represent Smooth Start easing (the percentage changes slow at first and speeds up at the end).

    Example

    Here is an example of Smooth Start (the percentage changes slow at first and speeds up at the end).

    def tick args
      start_tick   = 60
      current_tick = args.state.tick_count
      duration     = 120
      percentage   = args.easing.ease start_tick,
                                      current_tick,
                                      duration,
                                      :quad
      start_x      = 100
      end_x        = 1180
      distance_x   = end_x - start_x
      final_x      = start_x + (distance_x * percentage)
    
      start_y      = 100
      end_y        = 620
      distance_y   = end_y - start_y
      final_y      = start_y + (distance_y * percentage)
    
      args.outputs.labels << { x: final_x,
                               y: final_y,
                               text: "#{percentage.to_sf}",
                               alignment_enum: 1 }
    end
    

    Combining Easing Definitions

    The base easing definitions can be combined to create common easing functions.

    Example

    Here is an example of Smooth Stop (the percentage changes fast at first and slows down at the end).

    def tick args
      start_tick   = 60
      current_tick = args.state.tick_count
      duration     = 120
    
      # :flip, :quad, :flip is Smooth Stop
      percentage   = args.easing.ease start_tick,
                                      current_tick,
                                      duration,
                                      :flip, :quad, :flip
      start_x      = 100
      end_x        = 1180
      distance_x   = end_x - start_x
      final_x      = start_x + (distance_x * percentage)
    
      start_y      = 100
      end_y        = 620
      distance_y   = end_y - start_y
      final_y      = start_y + (distance_y * percentage)
    
      args.outputs.labels << { x: final_x,
                               y: final_y,
                               text: "#{percentage.to_sf}",
                               alignment_enum: 1 }
    end
    

    Custom Easing Functions

    You can define your own easing functions by passing in a lambda as a definition or extending the Easing module.

    Example - Using Lambdas

    This easing function goes from 0 to 1 for the first half of the ease, then 1 to 0 for the second half of the ease.

    def tick args
      fade_in_at    = 60
      current_tick  = args.state.tick_count
      duration      = 600
      easing_lambda = lambda do |percentage, start_tick, duration|
                        fx = percentage
                        if fx < 0.5
                          fx = percentage * 2
                        else
                          fx = 1 - (percentage - 0.5) * 2
                        end
                        fx
                      end
    
      percentage    = args.easing.ease fade_in_at,
                                       current_tick,
                                       duration,
                                       easing_lambda
    
      alpha = 255 * percentage
      args.outputs.labels << { x: 640,
                               y: 320,
                               a: alpha,
                               text: "#{percentage.to_sf}",
                               alignment_enum: 1 }
    end
    
    Example - Extending Easing Definitions

    If you don't want to create a lambda, you can register an easing definition like so:

    # 1. Extend the Easing module
    module Easing
      def self.saw_tooth x
        if x < 0.5
          x * 2
        else
          1 - (x - 0.5) * 2
        end
      end
    end
    
    def tick args
      fade_in_at    = 60
      current_tick  = args.state.tick_count
      duration      = 600
    
      # 2. Reference easing definition by name
      percentage    = args.easing.ease fade_in_at,
                                       current_tick,
                                       duration,
                                       :saw_tooth
    
      alpha = 255 * percentage
      args.outputs.labels << { x: 640,
                               y: 320,
                               a: alpha,
                               text: "#{percentage.to_sf}",
                               alignment_enum: 1 }
    
    end
    

    easing.ease_spline start_tick, current_tick, duration, spline link

    Given a start, current, duration, and a multiple bezier values, this function returns a number between 0 and 1 that represents the progress of an easing function.

    This example will move a box at a linear speed from 0 to 1280 and then back to 0 using two bezier definitions (represented as an array with four values).

    def tick args
      start_time = 10
      duration = 60
      spline = [
        [  0, 0.25, 0.75, 1.0],
        [1.0, 0.75, 0.25,   0]
      ]
      current_progress = args.easing.ease_spline start_time,
                                                 args.state.tick_count,
                                                 duration,
                                                 spline
      args.outputs.solids << { x: 1280 * current_progress, y: 360, w: 10, h: 10 }
    end
    

    Pixel Arrays (args.pixel_arrays) link

    A PixelArray object with a width, height and an Array of pixels which are hexadecimal color values in ABGR format.

    You can create a pixel array like this:

    w = 200
    h = 100
    args.pixel_array(:my_pixel_array).w = w
    args.pixel_array(:my_pixel_array).h = h
    

    You'll also need to fill the pixels with values, if they are nil, the array will render with the checkerboard texture. You can use #00000000 to fill with transparent pixels if desired.

    args.pixel_array(:my_pixel_array).pixels.fill #FF00FF00, 0, w * h
    

    Note: To convert from rgb hex (like skyblue #87CEEB) to abgr hex, you split it in pairs pair (eg 87 CE EB) and reverse the order (eg EB CE 87) add join them again: #EBCE87. Then add the alpha component in front ie: FF for full opacity: #FFEBCE87.

    You can draw it by using the symbol for :path

    args.outputs.sprites << { x: 500, y: 300, w: 200, h: 100, path: :my_pixel_array) }
    

    If you want access a specific x, y position, you can do it like this for a bottom-left coordinate system:

    x = 150
    y = 33
    args.pixel_array(:my_pixel_array).pixels[(height - y) * width + x] = 0xFFFFFFFF
    

    CVars (args.cvars) link

    Hash contains metadata pulled from the files under the ./metadata directory. To get the keys that are available type $args.cvars.keys in the Console. Here is an example of how to retrieve the game version number:

    def tick args
      args.outputs.labels << {
        x: 640,
        y: 360,
        text: args.cvars["game_metadata.version"].value.to_s
      }
    end
    

    Each CVar has the following properties value, name, description, type, locked.

    Layout (args.layout) link

    Layout provides apis for placing primitives on a virtual grid that's within the "safe area" accross all platforms. This virtual grid is useful for rendering static controls (buttons, menu items, configuration screens, etc).

    For reference implementations, take a look at the following sample apps:

    The following example creates two menu items and updates a label with the button that was clicked:

    def tick args
      # render debug_primitives of args.layout for help with placement
      # args.outputs.primitives << args.layout.debug_primitives
    
      # capture the location for a label centered at the top
      args.state.label_rect ||= args.layout.rect(row: 0, col: 10, w: 4, h: 1)
      # state variable to hold the current click status
      args.state.label_message ||= "click a menu item"
    
      # capture the location of two menu items positioned in the center
      # with a cell width of 4 and cell height of 2
      args.state.menu_item_1_rect ||= args.layout.rect(row: 1, col: 10, w: 4, h: 2)
      args.state.menu_item_2_rect ||= args.layout.rect(row: 3, col: 10, w: 4, h: 2)
    
      # render the label at the center of the label_rect
      args.outputs.labels << args.state.label_rect.center.merge(text: args.state.label_message,
                                                                anchor_x: 0.5,
                                                                anchor_y: 0.5)
    
      # render menu items
      args.outputs.sprites << args.state.menu_item_1_rect.merge(path: :solid,
                                                                r: 100,
                                                                g: 100,
                                                                b: 200)
      args.outputs.labels << args.state.menu_item_1_rect.center.merge(text: "item 1",
                                                                      r: 255,
                                                                      g: 255,
                                                                      b: 255,
                                                                      anchor_x: 0.5,
                                                                      anchor_y: 0.5)
    
      args.outputs.sprites << args.state.menu_item_2_rect.merge(path: :solid,
                                                                r: 100,
                                                                g: 100,
                                                                b: 200)
      args.outputs.labels << args.state.menu_item_2_rect.center.merge(text: "item 2",
                                                                      r: 255,
                                                                      g: 255,
                                                                      b: 255,
                                                                      anchor_x: 0.5,
                                                                      anchor_y: 0.5)
    
      # if click occurs, then determine which menu item was clicked
      if args.inputs.mouse.click
        if args.inputs.mouse.intersect_rect?(args.state.menu_item_1_rect)
          args.state.label_message = "menu item 1 clicked"
        elsif args.inputs.mouse.intersect_rect?(args.state.menu_item_2_rect)
          args.state.label_message = "menu item 2 clicked"
        else
          args.state.label_message = "click a menu item"
        end
      end
    end
    

    rect link

    Given a row:, col:, w:, h:, returns a Hash with properties x, y, w, h, and center (which contains a Hash with x, y). The virtual grid is 12 rows by 24 columns (or 24 columns by 12 rows in portrait mode).

    debug_primitives link

    Function returns an array of primities that can be rendered to the screen to help you place items within the grid.

    Example:

    def tick args
      args.outputs.primitives << args.layout.debug_primitives
    end
    

    Array link

    The Array class has been extend to provide methods that will help in common game development tasks. Array is one of the most powerful classes in Ruby and a very fundamental component of Game Toolkit.

    map_2d link

    Assuming the array is an array of arrays, Given a block, each 2D array index invoked against the block. A 2D array is a common way to store data/layout for a stage.

    repl do
      stage = [
        [:enemy, :empty, :player],
        [:empty, :empty,  :empty],
        [:enemy, :empty,  :enemy],
      ]
    
      occupied_tiles = stage.map_2d do |row, col, tile|
        if tile == :empty
          nil
        else
          [row, col, tile]
        end
      end.reject_nil
    
      puts "Stage:"
      puts stage
    
      puts "Occupied Tiles"
      puts occupied_tiles
    end
    

    include_any? link

    Given a collection of items, the function will return true if any of self's items exists in the collection of items passed in:

    any_intersect_rect? link

    Assuming the array contains objects that respond to left, right, top, bottom, this method returns true if any of the elements within the array intersect the object being passed in. You are given an optional parameter called tolerance which informs how close to the other rectangles the elements need to be for it to be considered intersecting.

    The default tolerance is set to 0.1, which means that the primitives are not considered intersecting unless they are overlapping by more than 0.1.

    repl do
      # Here is a player class that has position and implement
      # the ~attr_rect~ contract.
      class Player
        attr_rect
        attr_accessor :x, :y, :w, :h
    
        def initialize x, y, w, h
          @x = x
          @y = y
          @w = w
          @h = h
        end
    
        def serialize
          { x: @x, y: @y, w: @w, h: @h }
        end
    
        def inspect
          "#{serialize}"
        end
    
        def to_s
          "#{serialize}"
        end
      end
    
      # Here is a definition of two walls.
      walls = [
         [10, 10, 10, 10],
         { x: 20, y: 20, w: 10, h: 10 },
       ]
    
      # Display the walls.
      puts "Walls."
      puts walls
      puts ""
    
      # Check any_intersect_rect? on player
      player = Player.new 30, 20, 10, 10
      puts "Is Player #{player} touching wall?"
      puts (walls.any_intersect_rect? player)
      # => false
      # The value is false because of the default tolerance is 0.1.
      # The overlap of the player rect and any of the wall rects is
      # less than 0.1 (for those that intersect).
      puts ""
    
      player = Player.new 9, 10, 10, 10
      puts "Is Player #{player} touching wall?"
      puts (walls.any_intersect_rect? player)
      # => true
      puts ""
    end
    

    map link

    The function given a block returns a new Enumerable of values.

    Example of using Array#map in conjunction with args.state and args.outputs.sprites to render sprites to the screen.

    def tick args
      # define the colors of the rainbow in ~args.state~
      # as an ~Array~ of ~Hash~es with :order and :name.
      # :order will be used to determine render location
      #  and :name will be used to determine sprite path.
      args.state.rainbow_colors ||= [
        { order: 0, name: :red    },
        { order: 1, name: :orange },
        { order: 2, name: :yellow },
        { order: 3, name: :green  },
        { order: 4, name: :blue   },
        { order: 5, name: :indigo },
        { order: 6, name: :violet },
      ]
    
      # render sprites diagonally to the screen
      # with a width and height of 50.
      args.outputs
          .sprites << args.state
                          .rainbow_colors
                          .map do |color| # <-- ~Array#map~ usage
                            [
                              color[:order] * 50,
                              color[:order] * 50,
                              50,
                              50,
                              "sprites/square-#{color[:name]}.png"
                            ]
                          end
    end
    

    each link

    The function, given a block, invokes the block for each item in the Array. Array#each is synonymous to foreach constructs in other languages.

    Example of using Array#each in conjunction with args.state and args.outputs.sprites to render sprites to the screen:

    def tick args
      # define the colors of the rainbow in ~args.state~
      # as an ~Array~ of ~Hash~es with :order and :name.
      # :order will be used to determine render location
      #  and :name will be used to determine sprite path.
      args.state.rainbow_colors ||= [
        { order: 0, name: :red    },
        { order: 1, name: :orange },
        { order: 2, name: :yellow },
        { order: 3, name: :green  },
        { order: 4, name: :blue   },
        { order: 5, name: :indigo },
        { order: 6, name: :violet },
      ]
    
      # render sprites diagonally to the screen
      # with a width and height of 50.
      args.state
          .rainbow_colors
          .map do |color| # <-- ~Array#each~ usage
            args.outputs.sprites << [
              color[:order] * 50,
              color[:order] * 50,
              50,
              50,
              "sprites/square-#{color[:name]}.png"
            ]
          end
    end
    

    reject_nil link

    Returns an Enumerable rejecting items that are nil, this is an alias for Array#compact:

    repl do
      a = [1, nil, 4, false, :a]
      puts a.reject_nil
      # => [1, 4, false, :a]
      puts a.compact
      # => [1, 4, false, :a]
    end
    

    reject_false link

    Returns an `Enumerable` rejecting items that are `nil` or `false`.

    repl do
      a = [1, nil, 4, false, :a]
      puts a.reject_false
      # => [1, 4, :a]
    end
    

    product link

    Returns all combinations of values between two arrays.

    Here are some examples of using product. Paste the following code at the bottom of main.rb and save the file to see the results:

    repl do
      a = [0, 1]
      puts a.product
      # => [[0, 0], [0, 1], [1, 0], [1, 1]]
    end
    
    repl do
      a = [ 0,  1]
      b = [:a, :b]
      puts a.product b
      # => [[0, :a], [0, :b], [1, :a], [1, :b]]
    end
    

    Numeric link

    The Numeric class has been extend to provide methods that will help in common game development tasks.

    frame_index link

    This function is helpful for determining the index of frame-by-frame sprite animation. The numeric value self represents the moment the animation started.

    frame_index takes three additional parameters:

    frame_index will return nil if the time for the animation is out of bounds of the parameter specification.

    Example using variables:

    def tick args
      start_looping_at = 0
      number_of_sprites = 6
      number_of_frames_to_show_each_sprite = 4
      does_sprite_loop = true
    
      sprite_index =
        start_looping_at.frame_index number_of_sprites,
                                     number_of_frames_to_show_each_sprite,
                                     does_sprite_loop
    
      sprite_index ||= 0
    
      args.outputs.sprites << [
        640 - 50,
        360 - 50,
        100,
        100,
        "sprites/dragon-#{sprite_index}.png"
      ]
    end
    

    Example using named parameters. The named parameters version allows you to also specify a repeat_index which is useful if your animation has starting frames that shouldn't be considered when looped:

    def tick args
      start_looping_at = 0
    
      sprite_index =
        start_looping_at.frame_index count: 6,
                                     hold_for: 4,
                                     repeat: true,
                                     repeat_index: 0,
                                     tick_count_override: args.state.tick_count
    
      sprite_index ||= 0
    
      args.outputs.sprites << [
        640 - 50,
        360 - 50,
        100,
        100,
        "sprites/dragon-#{sprite_index}.png"
      ]
    end
    

    The named parameter variant of frame_index is also available on Numeric:

    def tick args
      sprite_index =
        Numeric.frame_index start_at: 0,
                            count: 6,
                            hold_for: 4,
                            repeat: true,
                            repeat_index: 0,
                            tick_count_override: args.state.tick_count
    
      sprite_index ||= 0
    
      args.outputs.sprites << [
        640 - 50,
        360 - 50,
        100,
        100,
        "sprites/dragon-#{sprite_index}.png"
      ]
    end
    

    elapsed_time link

    For a given number, the elapsed frames since that number is returned. `Kernel.tick_count` is used to determine how many frames have elapsed. An optional numeric argument can be passed in which will be used instead of `Kernel.tick_count`.

    Here is an example of how elapsed_time can be used.

    def tick args
      args.state.last_click_at ||= 0
    
      # record when a mouse click occurs
      if args.inputs.mouse.click
        args.state.last_click_at = args.state.tick_count
      end
    
      # Use Numeric#elapsed_time to determine how long it's been
      if args.state.last_click_at.elapsed_time > 120
        args.outputs.labels << [10, 710, "It has been over 2 seconds since the mouse was clicked."]
      end
    end
    

    And here is an example where the override parameter is passed in:

    def tick args
      args.state.last_click_at ||= 0
    
      # create a state variable that tracks time at half the speed of args.state.tick_count
      args.state.simulation_tick = args.state.tick_count.idiv 2
    
      # record when a mouse click occurs
      if args.inputs.mouse.click
        args.state.last_click_at = args.state.simulation_tick
      end
    
      # Use Numeric#elapsed_time to determine how long it's been
      if (args.state.last_click_at.elapsed_time args.state.simulation_tick) > 120
        args.outputs.labels << [10, 710, "It has been over 4 seconds since the mouse was clicked."]
      end
    end
    

    elapsed? link

    Returns true if Numeric#elapsed_time is greater than the number. An optional parameter can be passed into elapsed? which is added to the number before evaluating whether elapsed? is true.

    Example usage (no optional parameter):

    def tick args
      args.state.box_queue ||= []
    
      if args.state.box_queue.empty?
        args.state.box_queue << { name: :red,
                                  destroy_at: args.state.tick_count + 60 }
        args.state.box_queue << { name: :green,
                                  destroy_at: args.state.tick_count + 60 }
        args.state.box_queue << { name: :blue,
                                  destroy_at: args.state.tick_count + 120 }
      end
    
      boxes_to_destroy = args.state
                             .box_queue
                             .find_all { |b| b[:destroy_at].elapsed? }
    
      if !boxes_to_destroy.empty?
        puts "boxes to destroy count: #{boxes_to_destroy.length}"
      end
    
      boxes_to_destroy.each { |b| puts "box #{b} was elapsed? on #{args.state.tick_count}." }
    
      args.state.box_queue -= boxes_to_destroy
    end
    

    Example usage (with optional parameter):

    def tick args
      args.state.box_queue ||= []
    
      if args.state.box_queue.empty?
        args.state.box_queue << { name: :red,
                                  create_at: args.state.tick_count + 120,
                                  lifespan: 60 }
        args.state.box_queue << { name: :green,
                                  create_at: args.state.tick_count + 120,
                                  lifespan: 60 }
        args.state.box_queue << { name: :blue,
                                  create_at: args.state.tick_count + 120,
                                  lifespan: 120 }
      end
    
      # lifespan is passed in as a parameter to ~elapsed?~
      boxes_to_destroy = args.state
                             .box_queue
                             .find_all { |b| b[:create_at].elapsed? b[:lifespan] }
    
      if !boxes_to_destroy.empty?
        puts "boxes to destroy count: #{boxes_to_destroy.length}"
      end
    
      boxes_to_destroy.each { |b| puts "box #{b} was elapsed? on #{args.state.tick_count}." }
    
      args.state.box_queue -= boxes_to_destroy
    end
    

    new? link

    Returns true if Numeric#elapsed_time == 0. Essentially communicating that number is equal to the current frame.

    Example usage:

    def tick args
      args.state.box_queue ||= []
    
      if args.state.box_queue.empty?
        args.state.box_queue << { name: :red,
                                  create_at: args.state.tick_count + 60 }
      end
    
      boxes_to_spawn_this_frame = args.state
                                      .box_queue
                                      .find_all { |b| b[:create_at].new? }
    
      boxes_to_spawn_this_frame.each { |b| puts "box #{b} was new? on #{args.state.tick_count}." }
    
      args.state.box_queue -= boxes_to_spawn_this_frame
    end
    

    Kernel link

    Kernel in the DragonRuby Runtime has patches for how standard out is handled and also contains a unit of time in games called a tick.

    tick_count link

    Returns the current tick of the game. This value is reset if you call $gtk.reset.

    global_tick_count link

    Returns the current tick of the application from the point it was started. This value is never reset.

    Grid (args.grid) link

    Returns the virtual grid for the game.

    name link

    Returns either :origin_bottom_left or :origin_center.

    bottom link

    Returns the y value that represents the bottom of the grid.

    top link

    Returns the y value that represents the top of the grid.

    left link

    Returns the x value that represents the left of the grid.

    right link

    Returns the x value that represents the right of the grid.

    rect link

    Returns a rectangle Primitive that represents the grid.

    origin_bottom_left! link

    Change the grids coordinate system to 0, 0 at the bottom left corner.

    origin_center! link

    Change the grids coordinate system to 0, 0 at the center of the screen.

    orientation link

    Returns either :portrait or :landscape. The orientation of your game is set within ./mygame/metadata/game_metadata.txt.

    w link

    Returns the grid's width (value is 1280 if orientation :landscape, and 720 if orientation is :portrait).

    h link

    Returns the grid's width (value is 720 if orientation :landscape, and 1280 if orientation is :portrait).

    Grid HD Properties link

    The following properties are available to Pro license holders. Setting hd=true and hd=true in ./mygame/metadata/game_metadata.txt will enable All Screen Mode.

    Please review the sample app located at ./samples/07_advanced_rendering_hd/03_allscreen_properties.

    When All Screen mode is enabled, you can render outside of the 1280x720 safe area. The 1280x720 logical canvas will be centered within the screen and scaled to one of the following closest/bess-fit resolutions.

    Regardless of the rendering resolution, your logical canvas will always be 1280x720 and all hd_* values will be at this same logical scale.

    hd_left link

    Returns the position of the left edge of the screen at the logical scale of 1280x720. For example, if the window's width is 1290x720, then hd_left will be -5.

    hd_right link

    Returns the position of the right edge of the screen at the logical scale of 1280x720. For example, if the window's width is 1290x720, then hd_right will be 1285.

    hd_bottom link

    Returns the position of the bottom edge of the screen at the logical scale of 1280x720. For example, if the window's width is 1280x730, then hd_bottom will be -5.

    hd_top link

    Returns the position of the top edge of the screen at the logical scale of 1280x720. For example, if the window's width is 1280x730, then hd_top will be 725.

    hd_offset_x link

    Returns the number of pixels that the 1280x720 canvas is offset from the left so that it's centered in the screen.

    hd_offset_y link

    Returns the number of pixels that the 1280x720 canvas is offset from the bottom so that it's centered in the screen.

    window_width link

    Returns the true width of the window. High DPI settings are not taken into consideration.

    window_height link

    Returns the true height of the window. High DPI settings are not taken into consideration.

    native_width link

    Returns the true width of the window. High DPI settings (macOS, iOS, Android) are taken into consideration.

    native_height link

    Returns the true height of the window. High DPI settings (macOS, iOS, Android) are taken into consideration.

    native_scale link

    Returns a decimal value representing the rendering scale of the game.

    native_scale_enum link

    Returns an integer value representing the rendering scale of the game.

    The enum value is taken into consideration when rendering a sprite through texture atlases.

    Given the following code:

    def tick args
      args.outputs.sprites << { x: 0, y: 0, w: 100, h: 100, path: "sprites/player.png" }
    end
    

    The sprite path of sprites/player.png will be replaced according to the following naming conventions (fallback to a lower resolution is automatically handled if a sprite with naming convention isn't found):

    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.

    Learn Ruby Optional link

    Beginner Ruby Primer - automation.rb link

    # ./samples/00_learn_ruby_optional/00_beginner_ruby_primer/app/automation.rb
    # ==========================================================================
    #  _    _ ________     __  _      _____  _____ _______ ______ _   _ _ _ _ _
    # | |  | |  ____\ \   / / | |    |_   _|/ ____|__   __|  ____| \ | | | | | |
    # | |__| | |__   \ \_/ /  | |      | | | (___    | |  | |__  |  \| | | | | |
    # |  __  |  __|   \   /   | |      | |  \___ \   | |  |  __| | . ` | | | | |
    # | |  | | |____   | |    | |____ _| |_ ____) |  | |  | |____| |\  |_|_|_|_|
    # |_|  |_|______|  |_|    |______|_____|_____/   |_|  |______|_| \_(_|_|_|_)
    #
    #
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                \  |  /
    #                                 \ | /
    #                                   +
    #
    # If you are new to the programming language Ruby, then you may find the
    # following code a bit overwhelming. Come back to this file when you have
    # a better grasp of Ruby and Game Toolkit.
    #
    # What follows is an automations script # that can be run via terminal:
    # ./samples/00_beginner_ruby_primer $ ../../dragonruby . --eval app/automation.rb
    # ==========================================================================
    
    $gtk.reset
    $gtk.scheduled_callbacks.clear
    $gtk.schedule_callback 10 do
      $gtk.console.set_command 'puts "Hello DragonRuby!"'
    end
    
    $gtk.schedule_callback 20 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 30 do
      $gtk.console.set_command 'outputs.solids << [910, 200, 100, 100, 255, 0, 0]'
    end
    
    $gtk.schedule_callback 40 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 50 do
      $gtk.console.set_command 'outputs.solids << [1010, 200, 100, 100, 0, 0, 255]'
    end
    
    $gtk.schedule_callback 60 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 70 do
      $gtk.console.set_command 'outputs.sprites << [1110, 200, 100, 100, "sprites/dragon_fly_0.png"]'
    end
    
    $gtk.schedule_callback 80 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 90 do
      $gtk.console.set_command "outputs.labels << [1210, 200, state.tick_count, 0, 255, 0]"
    end
    
    $gtk.schedule_callback 100 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 110 do
      $gtk.console.set_command "state.sprite_frame = state.tick_count.idiv(4).mod(6)"
    end
    
    $gtk.schedule_callback 120 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 130 do
      $gtk.console.set_command "outputs.labels << [1210, 170, state.sprite_frame, 0, 255, 0]"
    end
    
    $gtk.schedule_callback 140 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 150 do
      $gtk.console.set_command "state.sprite_path =  \"sprites/dragon_fly_\#{state.sprite_frame}.png\""
    end
    
    $gtk.schedule_callback 160 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 170 do
      $gtk.console.set_command "outputs.labels    << [910, 330, \"path: \#{state.sprite_path}\", 0, 255, 0]"
    end
    
    $gtk.schedule_callback 180 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 190 do
      $gtk.console.set_command "outputs.sprites   << [910, 330, 370, 370, state.sprite_path]"
    end
    
    $gtk.schedule_callback 200 do
      $gtk.console.eval_the_set_command
    end
    
    $gtk.schedule_callback 300 do
      $gtk.console.set_command ":wq"
    end
    
    $gtk.schedule_callback 400 do
      $gtk.console.eval_the_set_command
    end
    
    

    Beginner Ruby Primer - main.rb link

    # ./samples/00_learn_ruby_optional/00_beginner_ruby_primer/app/main.rb
    # ==========================================================================
    #  _    _ ________     __  _      _____  _____ _______ ______ _   _ _ _ _ _
    # | |  | |  ____\ \   / / | |    |_   _|/ ____|__   __|  ____| \ | | | | | |
    # | |__| | |__   \ \_/ /  | |      | | | (___    | |  | |__  |  \| | | | | |
    # |  __  |  __|   \   /   | |      | |  \___ \   | |  |  __| | . ` | | | | |
    # | |  | | |____   | |    | |____ _| |_ ____) |  | |  | |____| |\  |_|_|_|_|
    # |_|  |_|______|  |_|    |______|_____|_____/   |_|  |______|_| \_(_|_|_|_)
    #
    #
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                   |
    #                                \  |  /
    #                                 \ | /
    #                                   +
    #
    # If you are new to the programming language Ruby, then you may find the
    # following code a bit overwhelming. This sample is only designed to be
    # run interactively (as opposed to being manipulated via source code).
    #
    # Start up this sample and follow along by visiting:
    # https://s3.amazonaws.com/s3.dragonruby.org/dragonruby-gtk-primer.mp4
    #
    # It is STRONGLY recommended that you work through all the samples before
    # looking at the code in this file.
    # ==========================================================================
    
    class TutorialOutputs
      attr_accessor :solids, :sprites, :labels, :lines, :borders
    
      def initialize
        @solids  = []
        @sprites = []
        @labels  = []
        @lines   = []
        @borders = []
      end
    
      def tick
        @solids  ||= []
        @sprites ||= []
        @labels  ||= []
        @lines   ||= []
        @borders ||= []
        @solids.each  { |p| $gtk.args.outputs.reserved << p.solid  }
        @sprites.each { |p| $gtk.args.outputs.reserved << p.sprite }
        @labels.each  { |p| $gtk.args.outputs.reserved << p.label  }
        @lines.each   { |p| $gtk.args.outputs.reserved << p.line   }
        @borders.each { |p| $gtk.args.outputs.reserved << p.border }
      end
    
      def clear
        @solids.clear
        @sprites.clear
        @labels.clear
        @borders.clear
      end
    end
    
    def defaults
      state.reset_button ||=
        state.new_entity(
          :button,
          label:  [1190, 68, "RESTART", -2, 0, 0, 0, 0].label,
          background: [1160, 38, 120, 50, 255, 255, 255].solid
        )
      $gtk.log_level = :off
    end
    
    def tick_reset_button
      return unless state.hello_dragonruby_confirmed
      $gtk.args.outputs.reserved << state.reset_button.background
      $gtk.args.outputs.reserved << state.reset_button.label
      if inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.reset_button.background)
        restart_tutorial
      end
    end
    
    def seperator
      @seperator = "=" * 80
    end
    
    def tick_intro
      queue_message "Welcome to the DragonRuby GTK primer! Try typing the
    code below and press ENTER:
    
        puts \"Hello DragonRuby!\"
    "
    end
    
    def tick_hello_dragonruby
      return unless console_has? "Hello DragonRuby!", "puts "
    
      $gtk.args.state.hello_dragonruby_confirmed = true
    
      queue_message "Well HELLO to you too!
    
    If you ever want to RESTART the tutorial, just click the \"RESTART\"
    button in the bottom right-hand corner.
    
    Let's continue shall we? Type the code below and press ENTER:
    
        outputs.solids << [910, 200, 100, 100, 255, 0, 0]
    "
    
    end
    
    def tick_explain_solid
      return unless $tutorial_outputs.solids.any? {|s| s == [910, 200, 100, 100, 255, 0, 0]}
    
      queue_message "Sweet!
    
    The code: outputs.solids << [910, 200, 100, 100, 255, 0, 0]
    Does the following:
    1. GET the place where SOLIDS go: outputs.solids
    2. Request that a new SOLID be ADDED: <<
    3. The DEFINITION of a SOLID is the ARRAY:
       [910, 200, 100, 100, 255, 0, 0]
    
          GET       ADD     X      Y    WIDTH  HEIGHT RED  GREEN  BLUE
           |         |      |      |      |      |     |     |     |
           |         |      |      |      |      |     |     |     |
    outputs.solids  <<    [910,   200,   100,   100,  255,   0,    0]
                          |_________________________________________|
                                               |
                                               |
                                             ARRAY
    
    Now let's create a blue SOLID. Type:
    
        outputs.solids << [1010, 200, 100, 100, 0, 0, 255]
    "
    
      state.explain_solid_confirmed = true
    end
    
    def tick_explain_solid_blue
      return unless state.explain_solid_confirmed
      return unless $tutorial_outputs.solids.any? {|s| s == [1010, 200, 100, 100, 0, 0, 255]}
      state.explain_solid_blue_confirmed = true
    
      queue_message "And there is our blue SOLID!
    
    The ARRAY is the MOST important thing in DragonRuby GTK.
    
    Let's create a SPRITE using an ARRAY:
    
      outputs.sprites << [1110, 200, 100, 100, 'sprites/dragon_fly_0.png']
    "
    end
    
    def tick_explain_tick_count
      return unless $tutorial_outputs.sprites.any? {|s| s == [1110, 200, 100, 100, 'sprites/dragon_fly_0.png']}
      return if $tutorial_outputs.labels.any? {|l| l == [1210, 200, state.tick_count, 255, 255, 255]}
      state.explain_tick_count_confirmed = true
    
      queue_message "Look at the cute little dragon!
    
    We can create a LABEL with ARRAYS too. Let's create a LABEL showing
    THE PASSAGE OF TIME, which is called TICK_COUNT.
    
      outputs.labels << [1210, 200, state.tick_count, 0, 255, 0]
    "
    end
    
    def tick_explain_mod
      return unless $tutorial_outputs.labels.any? {|l| l == [1210, 200, state.tick_count, 0, 255, 0]}
      state.explain_mod_confirmed = true
      queue_message "
    The code: outputs.labels << [1210, 200, state.tick_count, 0, 255, 0]
    Does the following:
    1. GET the place where labels go: outputs.labels
    2. Request that a new label be ADDED: <<
    3. The DEFINITION of a LABEL is the ARRAY:
       [1210, 200, state.tick_count, 0, 255, 0]
    
          GET       ADD     X      Y          TEXT         RED  GREEN  BLUE
           |         |      |      |            |           |     |     |
           |         |      |      |            |           |     |     |
    outputs.labels  <<    [1210,  200,   state.tick_count,  0,   255,   0]
                          |______________________________________________|
                                                  |
                                                  |
                                                ARRAY
    
    Now let's do some MATH, save the result to STATE, and create a LABEL:
    
        state.sprite_frame = state.tick_count.idiv(4).mod(6)
        outputs.labels << [1210, 170, state.sprite_frame, 0, 255, 0]
    
    Type the lines above (pressing ENTER after each line).
    "
    end
    
    def tick_explain_string_interpolation
      return unless state.explain_mod_confirmed
      return unless state.sprite_frame == state.tick_count.idiv(4).mod(6)
      return unless $tutorial_outputs.labels.any? {|l| l == [1210, 170, state.sprite_frame, 0, 255, 0]}
    
      queue_message "Here is what the mathematical computation you just typed does:
    
    1. Create an item of STATE named SPRITE_FRAME: state.sprite_frame =
    2. Set this SPRITE_FRAME to the PASSAGE OF TIME (tick_count),
       DIVIDED EVENLY (idiv) into 4,
       and then compute the REMAINDER (mod) of 6.
    
       STATE   SPRITE_FRAME    PASSAGE OF      HOW LONG   HOW MANY
         |          |             TIME         TO SHOW    IMAGES
         |          |              |           AN IMAGE   TO FLIP THROUGH
         |          |              |               |      |
    state.sprite_frame =     state.tick_count.idiv(4).mod(6)
                                               |       |
                                               |       +- REMAINDER OF DIVIDE
                                        DIVIDE EVENLY
                                        (NO DECIMALS)
    
    With the information above, we can animate a SPRITE
    using STRING INTERPOLATION: \#{}
    which creates a unique SPRITE_PATH:
    
      state.sprite_path =  \"sprites/dragon_fly_\#{state.sprite_frame}.png\"
      outputs.labels    << [910, 330, \"path: \#{state.sprite_path}\", 0, 255, 0]
      outputs.sprites   << [910, 330, 370, 370, state.sprite_path]
    
    Type the lines above (pressing ENTER after each line).
    "
    end
    
    def tick_reprint_on_error
      return unless console.last_command_errored
      puts $gtk.state.messages.last
      puts "\nWhoops! Try again."
      console.last_command_errored = false
    end
    
    def tick_evals
      state.evals ||= []
      if console.last_command && (console.last_command.start_with?("outputs.") || console.last_command.start_with?("state."))
        state.evals << console.last_command
        console.last_command = nil
      end
    
      state.evals.each do |l|
        Kernel.eval l
      end
    rescue Exception => e
      state.evals = state.evals[0..-2]
    end
    
    $tutorial_outputs ||= TutorialOutputs.new
    
    def tick args
      $gtk.log_level = :off
      defaults
      console.show
      $tutorial_outputs.clear
      $tutorial_outputs.solids  << [900, 37, 480, 700,   0,   0,   0, 255]
      $tutorial_outputs.borders << [900, 37, 380, 683, 255, 255, 255]
      tick_evals
      $tutorial_outputs.tick
      tick_intro
      tick_hello_dragonruby
      tick_reset_button
      tick_explain_solid
      tick_explain_solid_blue
      tick_reprint_on_error
      tick_explain_tick_count
      tick_explain_mod
      tick_explain_string_interpolation
    end
    
    def console
      $gtk.console
    end
    
    def queue_message message
      $gtk.args.state.messages ||= []
      return if $gtk.args.state.messages.include? message
      $gtk.args.state.messages << message
      last_three = [$gtk.console.log[-3], $gtk.console.log[-2], $gtk.console.log[-1]].reject_nil
      $gtk.console.log.clear
      puts seperator
      $gtk.console.log += last_three
      puts seperator
      puts message
      puts seperator
    end
    
    def console_has? message, not_message = nil
      console.log
             .map(&:upcase)
             .reject { |s| not_message && s.include?(not_message.upcase) }
             .any?   { |s| s.include?("#{message.upcase}") }
    end
    
    def restart_tutorial
      $tutorial_outputs.clear
      $gtk.console.log.clear
      $gtk.reset
      puts "Starting the tutorial over!"
    end
    
    def state
      $gtk.args.state
    end
    
    def inputs
      $gtk.args.inputs
    end
    
    def outputs
      $tutorial_outputs
    end
    
    

    Intermediate Ruby Primer - printing.txt link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/01_printing.txt
    # ====================================================================================
    # Commenting Code
    # ====================================================================================
    #
    # Prefixing text with a pound sign (#) is how you comment code in Ruby. Example:
    #
    # I am commented code. And so are the lines above.
    #
    # I you want more than a quick primer on Ruby, check out https://poignant.guide/. It's
    # an entertaining read. Otherwise, go to the next txt file.
    #
    # Follow along by visiting:
    # https://s3.amazonaws.com/s3.dragonruby.org/dragonruby-gtk-intermediate.mp4
    
    # ====================================================================================
    #  Printing to the Console:
    # ====================================================================================
    #
    # Every time you save repl.rb file, DragonRuby runs the code within it. Copy this text
    # to repl.rb and save to see Hello World printed to the console.
    
    repl do
      puts '* RUBY PRIMER: Printing to the console using the ~puts~ function.'
      puts '===='
      puts '======'
      puts '================================'
      puts 'Hello World'
      puts '================================'
      puts '======'
      puts '===='
    end
    
    

    Intermediate Ruby Primer - strings.txt link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/02_strings.txt
    # ====================================================================================
    #  Strings
    # ====================================================================================
    #
    # Here is how you work with strings in Ruby. Take the text
    # in this file and paste it into repl.rb and save:
    
    repl do
      puts '* RUBY PRIMER: strings'
      message = "Hello World"
      puts "The value of message is: " + message
      puts "Any value can be interpolated within a string using \#{}."
      puts "Interpolated message: #{message}."
      puts 'This #{message} is not interpolated because the string uses single quotes.'
    end
    
    

    Intermediate Ruby Primer - numbers.txt link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/03_numbers.txt
    # ====================================================================================
    #  Numerics
    # ====================================================================================
    #
    # Here is how you work with numbers in Ruby. Take the text
    # in this file and paste it into repl.rb and save:
    
    repl do
      puts '* RUBY PRIMER: Fixnum and Floats'
      a = 10
      puts "The value of a is: #{a}"
      puts "a + 1 is: #{a + 1}"
      puts "a / 3 is: #{a / 3}"
      puts ''
    
      b = 10.12
      puts "The value of b is: #{b}"
      puts "b + 1 is: #{b + 1}"
      puts "b as an integer is: #{b.to_i}"
      puts ''
    end
    
    

    Intermediate Ruby Primer - booleans.txt link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/04_booleans.txt
    # ====================================================================================
    #  Booleans
    # ====================================================================================
    #
    # Here is how you work with numbers in Ruby. Take the text
    # in this file and paste it into repl.rb and save:
    
    repl do
      puts '* RUBY PRIMER: TrueClass, FalseClass, NilClass (truthy / falsey values)'
      puts "Anything that *isn't* false or nil is true."
    
      c = 30
      puts "The value of c is #{c}."
    
      if c
        puts "This if statement ran because c is truthy."
      end
    
      d = false
      puts "The value if d is #{d}. The type for d is #{d.class}."
    
      if !d
        puts "This if statement ran because d is falsey, using the not operator (!)."
      end
    
      e = nil
      puts "Nil is also considered falsey. The value of e is: #{e} (a blank string when printed). Which is of type #{e.class}."
    
      if !e
        puts "This if statement ran because e is nil and the if statement applied the NOT operator. !e yields a type of #{(!e).class}."
      end
    end
    
    

    Intermediate Ruby Primer - conditionals.txt link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/05_conditionals.txt
    # ====================================================================================
    #  Conditionals
    # ====================================================================================
    #
    # Here is how you create conditionals in Ruby. Take the text
    # in this file and paste it into repl.rb and save:
    
    repl do
      puts "* RUBY PRIMER: Conditionals"
    end
    
    # ====================================================================================
    #  if
    # ====================================================================================
    
    repl do
      puts "** INFO: if statement"
      i_am_one = 1
      if i_am_one
        puts "This was printed because i_am_one is truthy."
      end
    end
    
    # ====================================================================================
    #  if/else
    # ====================================================================================
    
    repl do
      puts "** INFO: if/else statement"
      i_am_false = false
      if i_am_false
        puts "This will NOT get printed because i_am_false is false."
      else
        puts "This was printed because i_am_false is false."
      end
    end
    
    
    # ====================================================================================
    #  if/elsif/else
    # ====================================================================================
    
    repl do
      puts "** INFO: if/elsif/else statement"
      i_am_false = false
      i_am_true  = true
      if i_am_false
        puts "This will NOT get printed because i_am_false is false."
      elsif i_am_true
        puts "This was printed because i_am_true is true."
      else
        puts "This will NOT get printed i_am_true was true."
      end
    end
    
    # ====================================================================================
    #  case
    # ====================================================================================
    
    repl do
      puts "** INFO case statement"
      i_am_one = 1 # change this value to see different results
    
      case i_am_one
      when 10
        puts "the value of i_am_one is 10"
      when 9
        puts "the value of i_am_one is 9"
      when 5
        puts "the value of i_am_one is 5"
      when 1
        puts "the value of i_am_one is 1"
      else
        puts "Value wasn't cased."
      end
    end
    
    # ====================================================================================
    #  comparison operators
    # ====================================================================================
    
    repl do
      puts "** INFO: Different types of comparisons"
      if 4 == 4
        puts "4 equals 4 (==)"
      end
    
      if 4 != 3
        puts "4 does not equal 3 (!=)"
      end
    
      if 3 < 4
        puts "3 is less than 4 (<)"
      end
    
      if 4 > 3
        puts "4 is greater than 3 (>)"
      end
    end
    
    # ====================================================================================
    #  and/or conditionals
    # ====================================================================================
    
    repl do
      puts "** INFO: AND, OR operator (&&, ||)"
      if (4 > 3) || (3 < 4) || false
        puts "print this if 4 is greater than 3 OR 3 is less than 4 OR false is true (||)"
      end
    
      if (4 > 3) && (3 < 4)
        puts "print this if 4 is greater than 3 AND 3 is less than 4 (&&)"
      end
    end
    
    

    Intermediate Ruby Primer - looping.txt link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/06_looping.txt
    # ====================================================================================
    #  Looping
    # ====================================================================================
    #
    # Looping looks a whole lot different than other languages.
    # But it's pretty awesome when you get used to it.
    
    repl do
      puts "* RUBY PRIMER: Loops"
    end
    
    # ====================================================================================
    #  times
    # ====================================================================================
    
    repl do
      puts "** INFO: ~Numeric#times~ (for loop)"
      3.times do |i|
        puts i
      end
    end
    
    # ====================================================================================
    #  foreach
    # ====================================================================================
    
    repl do
      puts "** INFO: ~Array#each~ (for each loop)"
      array = ["a", "b", "c", "d"]
      array.each do |char|
        puts char
      end
    
      puts "** INFO: ~Array#each_with_index~ (for each loop)"
      array = ["a", "b", "c", "d"]
      array.each do |char, i|
        puts "index #{i}: #{char}"
      end
    end
    
    # ====================================================================================
    #  ranges
    # ====================================================================================
    
    repl do
      puts "** INFO: range block exclusive (three dots)"
      (0...3).each do |i|
        puts i
      end
    
      puts "** INFO: range block inclusive (two dots)"
      (0..3).each do |i|
        puts i
      end
    end
    
    

    Intermediate Ruby Primer - functions.txt link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/07_functions.txt
    # ====================================================================================
    # Functions
    # ====================================================================================
    
    # The last statement of a function is implictly returned. Parenthesis for functions
    # are optional as long as the statement can be envaluated disambiguously.
    
    repl do
      puts "* RUBY PRIMER: Functions"
    end
    
    # ====================================================================================
    # Functions single parameter
    # ====================================================================================
    
    repl do
      puts "* INFO: Function with one parameter"
    
      # function definition
      def add_one_to n
        n + 1
      end
    
      # Parenthesis are optional in Ruby as long as the
      # parsing is disambiguous. Here are a couple of variations.
      # Generally speaking, don't put parenthesis is you don't have to.
    
      # Conventional Usage of Parenthesis.
      puts add_one_to(3)
    
      # DragonRuby's recommended use of parenthesis (inner function has parenthesis).
      puts (add_one_to 3)
    
      # Full parens.
      puts(add_one_to(3))
    
      # Outer function has parenthesis
      puts(add_one_to 3)
    end
    
    # ====================================================================================
    # Functions with default parameter values
    # ====================================================================================
    
    repl do
      puts "* INFO: Function with default value"
      def function_with_default_value v = 10
        v * 10
      end
    
      puts "Passing the argument three yields: #{function_with_default_value 3}"
      puts "Passing no argument yields: #{function_with_default_value}"
    end
    
    # ====================================================================================
    # Nil default parameter value and ||= operator.
    # ====================================================================================
    
    repl do
      puts "* INFO: Using the OR EQUAL operator (||=)"
      def function_with_nil_default_with_local a = nil
        result   = a
        result ||= "DEFAULT_VALUE_OF_A_IS_NIL_OR_FALSE"
        "value is #{result}."
      end
    
      puts "Passing 'hi' as the argument yields: #{function_with_nil_default_with_local 'hi'}"
      puts "Passing nil: #{function_with_nil_default_with_local}"
    end
    
    

    Intermediate Ruby Primer - arrays.txt link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/08_arrays.txt
    # ====================================================================================
    # Arrays
    # ====================================================================================
    
    # Arrays are incredibly powerful in Ruby. Learn to use them well.
    
    repl do
      puts "* RUBY PRIMER: ARRAYS"
    end
    
    # ====================================================================================
    # Enumerable ranges and .to_a
    # ====================================================================================
    
    repl do
      puts "** INFO: Create an array with the numbers 1 to 10."
      one_to_ten = (1..10).to_a
      puts one_to_ten
    end
    
    # ====================================================================================
    # Finding elements
    # ====================================================================================
    
    repl do
      puts "** INFO: Finding elements in an array using ~Array#find_all~."
      puts "Create a new array that only contains even numbers from the previous array."
    
      one_to_ten = (1..10).to_a
      evens = one_to_ten.find_all do |number|
        number % 2 == 0
      end
    
      puts evens
    end
    
    # ====================================================================================
    # Rejecting elements
    # ====================================================================================
    
    repl do
      puts "** INFO: Removing elements in an array using ~Array#reject~."
      puts "Create a new array that rejects odd numbers."
    
      one_to_ten = (1..10).to_a
      also_even = one_to_ten.reject do |number|
        number % 2 != 0
      end
    
      puts also_even
    end
    
    # ====================================================================================
    # Array transform using the map function.
    # ====================================================================================
    
    repl do
      puts "** INFO: Creating new derived values from an array using ~Array#map~."
      puts "Create an array that doubles every number."
    
      one_to_ten = (1..10).to_a
      doubled = one_to_ten.map do |number|
        number * 2
      end
    
      puts doubled
    end
    
    # ====================================================================================
    # Combining array functions.
    # ====================================================================================
    
    repl do
      puts "** INFO: Combining ~Array#find_all~ along with ~Array#map~."
      puts "Create an array that selects only odd numbers and then multiply those by 10."
    
      one_to_ten = (1..10).to_a
      odd_doubled = one_to_ten.find_all do |number|
        number % 2 != 0
      end.map do |odd_number|
        odd_number * 10
      end
    
      puts odd_doubled
    end
    
    # ====================================================================================
    # Product function.
    # ====================================================================================
    
    repl do
      puts "** INFO: Create all combinations of array values using ~Array#product~."
      puts "All two-item pairs of numbers 1 to 10."
      one_to_ten = (1..10).to_a
      all_combinations = one_to_ten.product(one_to_ten)
      puts all_combinations
    end
    
    # ====================================================================================
    # Uniq and sort function.
    # ====================================================================================
    
    repl do
      puts "** INFO: Providing uniq values using ~Array#uniq~ and ~Array#sort~."
      puts "All uniq combinations of numbers regardless of order."
      puts "For example: [1, 2] is the same as [2, 1]."
      one_to_ten = (1..10).to_a
      uniq_combinations =
        one_to_ten.product(one_to_ten)
                  .map do |unsorted_number|
                    unsorted_number.sort
                  end.uniq
      puts uniq_combinations
    end
    
    # ====================================================================================
    # Example of an advanced array transform.
    # ====================================================================================
    
    repl do
      puts "** INFO: Advanced chaining. Combining ~Array's ~map~, ~find_all~, ~sort~, and ~sort_by~."
      puts "All unique Pythagorean Triples between 1 and 100 sorted by area of the triangle."
    
      one_to_hundred = (1..100).to_a
    
      triples =
        one_to_hundred.product(one_to_hundred).map do |width, height|
                    [width, height, Math.sqrt(width ** 2 + height ** 2)]
                  end.find_all do |_, _, hypotenuse|
                    hypotenuse.to_i == hypotenuse
                  end.map do |triangle|
                    triangle.map(&:to_i)
                  end.uniq do |triangle|
                    triangle.sort
                  end.map do |width, height, hypotenuse|
                    [width, height, hypotenuse, (width * height) / 2]
                  end.sort_by do |_, _, _, area|
                    area
                  end
    
      triples.each do |width, height, hypotenuse, _|
        puts "(#{width}, #{height}, #{hypotenuse})"
      end
    end
    
    # ====================================================================================
    # Example of an sorting.
    # ====================================================================================
    
    repl do
      puts "** INFO: Implementing a custom sort function that operates on the ~Hash~ datatype."
    
      things_to_sort = [
        { type: :background, order: 1 },
        { type: :foreground, order: 1 },
        { type: :foreground, order: 2 }
      ]
      puts "*** Original array."
      puts things_to_sort
    
      puts "*** Simple sort using key."
      # For a simple sort, you can use sort_by
      results = things_to_sort.sort_by do |hash|
        hash[:order]
      end
    
      puts results
    
      puts "*** Custom sort."
      puts "**** Sorting process."
      # for a more complicated sort, you can provide a block that returns
      # -1, 0, 1 for a left and right operand
      results = things_to_sort.sort do |l, r|
        sort_result = 0
        puts "here is l: #{l}"
        puts "here is r: #{r || "nil"}"
        # if either value is nil/false return 0
        if !l || !r
          sort_result = 0
        # if the type of "left" is background and the
        # type of "right" is foreground, then return
        # -1 (which means "left" is less than "right"
        elsif l[:type] == :background && r[:type] == :foreground
          sort_result = -1
        # if the type of "left" is foreground and the
        # type of "right" is background, then return
        #  1 (which means "left" is greater than "right"
        elsif l[:type] == :foreground && r[:type] == :background
          sort_result = 1
        # if "left" and "right"'s type are the same, then
        # use the order as the tie breaker
        elsif l[:order] < r[:order]
          sort_result = -1
        elsif l[:order] > r[:order]
          sort_result = 1
        # returning 0 means both values are equal
        else
          sort_result = 0
        end
        sort_result
      end.to_a
    
      puts "**** Sort result."
      puts results
    end
    
    # ====================================================================================
    # Api documention for Array that is worth commiting to memory because arrays are so
    # awesome in Ruby: https://docs.ruby-lang.org/en/2.0.0/Array.html
    # ====================================================================================
    
    

    Intermediate Ruby Primer - main.rb link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/main.rb
    def tick args
      args.outputs.labels << [640, 380, "Open repl.rb in the text editor of your choice and follow the document.", 0, 1]
    end
    
    

    Intermediate Ruby Primer - repl.rb link

    # ./samples/00_learn_ruby_optional/00_intermediate_ruby_primer/app/repl.rb
    # Copy and paste the code inside of the txt files here.
    
    

    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 << [580.5, 650, "frame: #{args.state.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: #{args.state.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: 610 - 50, text: "Smaller label.",  size_enum: -2 }
    
      # size_enum of -1 is equivalent to using size_px: 20
      args.outputs.labels << { x: 175 + 150, y: 580 - 50, text: "Small label.",    size_enum: -1 }
    
      # 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
      args.outputs.labels << { x: 175 + 150, y: 345 - 50, text: "Left aligned.",   alignment_enum: 0 }
    
      # 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 }
    
      # 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" }
    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
      args.state.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."
      args.state.really_long_string += "\n"
      args.state.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
    
      # line height
      line_height = 25
    
      long_string = args.state.really_long_string
    
      # API: args.string.wrapped_lines string, max_character_length
      long_strings_split = args.string.wrapped_lines long_string, max_character_length
    
      # render a label for each line and offset by the line_height
      args.outputs.labels << long_strings_split.map_with_index do |s, i|
        {
          x: 60,
          y: 60.from_top - (i * line_height),
          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.
    - args.state.tick_count: This property contains an integer value that
      represents the current frame. DragonRuby renders at 60 FPS. A value of 0
      for args.state.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 args.state.tick_count to change the length of the lines over time
      # args.state.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 args.state.tick_count
      args.outputs.lines  << { x:  380,
                               y:  370,
                               x2: 875,
                               y2: 370,
                               r:  args.state.tick_count % 255,
                               g:  0,
                               b:  0,
                               a:  255 }
    
      args.outputs.lines  << { x:  380,
                               y:  330 - args.state.tick_count % 25,
                               x2: 875,
                               y2: 330,
                               r:  0,
                               g:  0,
                               b:  0,
                               a:  255 }
    
      args.outputs.lines  << { x:  380 + args.state.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
      # This 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]
    
      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]
    
      # using Hashes
      args.outputs.solids << { x: 710,
                               y: 520,
                               w: 50,
                               h: 50,
                               r: 0,
                               g: 80,
                               b: 40,
                               a: args.state.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: args.state.tick_count % 255,
                                   primitive_marker: :solid }
    
      args.outputs.borders << { x: 710,
                                y: 320,
                                w: 50,
                                h: 50,
                                r: 0,
                                g: 80,
                                b: 40,
                                a: args.state.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: args.state.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: args.state.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: args.state.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: args.state.tick_count % 360 }
    
      # rotation anchor using angle_anchor_x
      args.outputs.sprites << { x: 760,
                                y: 270,
                                w: 128,
                                h: 101,
                                path: 'dragonruby.png',
                                angle: args.state.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 args.state.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: #{args.state.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 = args.state.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 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 = args.state.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: #{args.state.tick_count}"
    
        # And MOST IMPORTANTLY set the point it time to start the animation,
        # equal to "now" which is represented as args.state.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 = args.state.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 ||= args.state.tick_count
      elsif args.inputs.keyboard.left
        args.state.player.x -= 3
        args.state.player.direction = -1
        args.state.player.started_running_at ||= args.state.tick_count
      end
    
      if args.inputs.keyboard.up
        args.state.player.y += 1
        args.state.player.started_running_at ||= args.state.tick_count
      elsif args.inputs.keyboard.down
        args.state.player.y -= 1
        args.state.player.started_running_at ||= args.state.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 ||= args.state.tick_count
      elsif args.state.player.x < -64
        args.state.player.x = 1280
        args.state.player.started_running_at ||= args.state.tick_count
      end
    
      if args.state.player.y > 720
        args.state.player.y = -64
        args.state.player.started_running_at ||= args.state.tick_count
      elsif args.state.player.y < -64
        args.state.player.y = 720
        args.state.player.started_running_at ||= args.state.tick_count
      end
    
      # 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 state.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)) != state.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 = args.state.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 = args.state.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 = args.state.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 = args.state.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 ||= state.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].transient!
        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 state.tick_count == key_0
            should_update_facing = true
          elsif state.tick_count == key_1
            should_update_facing = true
          elsif key_0 && key_1 && state.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     = state.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 > state.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[state.tick_count] = :slash_0
            state.player.next_action_queue[state.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 = state.tick_count if queue_at < state.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 !state.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,
                                                   state.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[state.tick_count]
    
        state.player.previous_action = state.player.action
        state.player.previous_action_at = state.player.action_at
        state.player.previous_action_ended_at = state.tick_count
        state.player.action = state.player.next_action_queue[state.tick_count]
        state.player.action_at = state.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
    
    

    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 args.state.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 ||= []
      args.state.tick_count = args.state.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 args.state.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 args.state.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: args.state.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] < args.state.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] < args.state.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 = args.state.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 ||= args.state.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 = state.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 = state.tick_count
        elsif inputs.keyboard.key_down.space
          player.dy             = player.jump_power
          player.jumped_at      = state.tick_count
          player.jumped_down_at = 0
        end
      end
    
      def calc_player
        calc_player_rect
        calc_below
        calc_left
        calc_right
        calc_above
        calc_player_dy
        calc_player_dx
        reset_player if player_off_stage?
      end
    
      def calc_player_rect
        player.rect      = current_player_rect
        player.next_rect = player.rect.merge x: player.x + player.dx,
                                             y: player.y + player.dy
        player.prev_rect = player.rect.merge x: player.x - player.dx,
                                             y: player.y - player.dy
      end
    
      def calc_below
        return unless player.dy <= 0
        tiles_below = find_tiles { |t| t.rect.top <= player.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  = args.state.tick_count
        s.gravity     = -0.3 # what goes up must come down because of gravity
        s.player.platforms_cleared ||= 0 # counts how many platforms the player has successfully cleared
        s.player.x  ||= 0           # sets player values
        s.player.y  ||= 100
        s.player.w  ||= 64
        s.player.h  ||= 64
        s.player.dy ||= 0           # change in position
        s.player.dx ||= 0
        s.player_jump_power           = 15
        s.player_jump_power_duration  = 10
        s.player_max_run_speed        = 5
        s.player_speed_slowdown_rate  = 0.9
        s.player_acceleration         = 1
        s.camera ||= { y: -100 } # shows view on screen (as the player moves upward, the camera does too)
      end
    
      # 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, args.state.tick_count])
    
      end
      i=0
      while i < shape.damageCount.length
        if shape.damageCount[i][0] <= 0
          shape.damageCount.delete_at(i)
          i-=1
        elsif shape.damageCount[i][1].elapsed_time > BALL_DISTANCE and shape.damageCount[i][0] > 1
          shape.count-=1
          shape.damageCount[i][0]-=1
          shape.damageCount[i][1] = args.state.tick_count
        end
        i+=1
      end
    end
    
    
    class Square
       attr_accessor :count, :x, :y, :home, :bold, :squareColliders, :colliders, :damageCount
       def initialize(args, x, y, block_size, orientation, block_offset)
            @x = x * block_size
            @y = y * block_size
            @block_size = block_size
            @block_offset = block_offset
            @orientation = orientation
            @damageCount = []
            @home = 'squares'
    
    
            Kernel.srand()
            @r = rand(255)
            @g = rand(255)
            @b = rand(255)
    
            @count = rand(MAX_COUNT)+1
    
            x_offset = (args.state.board_width + args.grid.w / 8) + @block_offset / 2
            @x_adjusted = @x + x_offset
            @y_adjusted = @y
            @size_adjusted = @block_size * 2 - @block_offset
    
            hypotenuse=args.state.ball_hypotenuse
            @bold = [(@x_adjusted-hypotenuse/2)-1, (@y_adjusted-hypotenuse/2)-1, @size_adjusted + hypotenuse + 2, @size_adjusted + hypotenuse + 2]
    
            @points = [
              {x:@x_adjusted, y:@y_adjusted},
              {x:@x_adjusted+@size_adjusted, y:@y_adjusted},
              {x:@x_adjusted+@size_adjusted, y:@y_adjusted+@size_adjusted},
              {x:@x_adjusted, y:@y_adjusted+@size_adjusted}
            ]
            @squareColliders = [
              SquareCollider.new(@points[0].x,@points[0].y,{x:-1,y:-1}),
              SquareCollider.new(@points[1].x-COLLISIONWIDTH,@points[1].y,{x:1,y:-1}),
              SquareCollider.new(@points[2].x-COLLISIONWIDTH,@points[2].y-COLLISIONWIDTH,{x:1,y:1}),
              SquareCollider.new(@points[3].x,@points[3].y-COLLISIONWIDTH,{x:-1,y:1}),
            ]
            @colliders = [
              LinearCollider.new(@points[0],@points[1], :neg),
              LinearCollider.new(@points[1],@points[2], :neg),
              LinearCollider.new(@points[2],@points[3], :pos),
              LinearCollider.new(@points[0],@points[3], :pos)
            ]
       end
    
       def draw(args)
        #Offset the coordinates to the edge of the game area
        x_offset = (args.state.board_width + args.grid.w / 8) + @block_offset / 2
        #args.outputs.solids << [@x + x_offset, @y, @block_size * 2 - @block_offset, @block_size * 2 - @block_offset, @r, @g, @b]
        args.outputs.solids <<{x: (@x + x_offset), y: (@y), w: (@block_size * 2 - @block_offset), h: (@block_size * 2 - @block_offset), r: @r , g: @g , b: @b }
        #args.outputs.solids << @bold.append([255,0,0])
        args.outputs.labels << [@x + x_offset + (@block_size * 2 - @block_offset)/2, (@y) + (@block_size * 2 - @block_offset)/2, @count.to_s]
    
       end
    
       def update args
         universalUpdate args, self
       end
    end
    
    class TShape
        attr_accessor :count, :x, :y, :home, :bold, :squareColliders, :colliders, :damageCount
        def initialize(args, x, y, block_size, orientation, block_offset)
            @x = x * block_size
            @y = y * block_size
            @block_size = block_size
            @block_offset = block_offset
            @orientation = orientation
            @damageCount = []
            @home = "tshapes"
    
            Kernel.srand()
            @r = rand(255)
            @g = rand(255)
            @b = rand(255)
    
            @count = rand(MAX_COUNT)+1
    
    
            @shapePoints = getShapePoints(args)
            minX={x:INFINITY, y:0}
            minY={x:0, y:INFINITY}
            maxX={x:-INFINITY, y:0}
            maxY={x:0, y:-INFINITY}
            for p in @shapePoints
              if p.x < minX.x
                minX = p
              end
              if p.x > maxX.x
                maxX = p
              end
              if p.y < minY.y
                minY = p
              end
              if p.y > maxY.y
                maxY = p
              end
            end
    
    
            hypotenuse=args.state.ball_hypotenuse
    
            @bold = [(minX.x-hypotenuse/2)-1, (minY.y-hypotenuse/2)-1, -((minX.x-hypotenuse/2)-1)+(maxX.x + hypotenuse + 2), -((minY.y-hypotenuse/2)-1)+(maxY.y + hypotenuse + 2)]
        end
        def getShapePoints(args)
          points=[]
          x_offset = (args.state.board_width + args.grid.w / 8) + (@block_offset / 2)
    
          if @orientation == :right
              #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
              #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2, @block_size, @r, @g, @b]
              points = [
                {x:@x + x_offset, y:@y},
                {x:(@x + x_offset)+(@block_size - @block_offset), y:@y},
                {x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size},
                {x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size},
                {x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size+@block_size},
                {x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size+@block_size},
                {x:(@x + x_offset)+(@block_size - @block_offset), y:@y+ @block_size * 3 - @block_offset},
                {x:@x + x_offset , y:@y+ @block_size * 3 - @block_offset}
              ]
              @squareColliders = [
                SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}),
                SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}),
                SquareCollider.new(points[2].x,points[2].y-COLLISIONWIDTH,{x:1,y:-1}),
                SquareCollider.new(points[3].x-COLLISIONWIDTH,points[3].y,{x:1,y:-1}),
                SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y-COLLISIONWIDTH,{x:1,y:1}),
                SquareCollider.new(points[5].x,points[5].y,{x:1,y:1}),
                SquareCollider.new(points[6].x-COLLISIONWIDTH,points[6].y-COLLISIONWIDTH,{x:1,y:1}),
                SquareCollider.new(points[7].x,points[7].y-COLLISIONWIDTH,{x:-1,y:1}),
              ]
              @colliders = [
                LinearCollider.new(points[0],points[1], :neg),
                LinearCollider.new(points[1],points[2], :neg),
                LinearCollider.new(points[2],points[3], :neg),
                LinearCollider.new(points[3],points[4], :neg),
                LinearCollider.new(points[4],points[5], :pos),
                LinearCollider.new(points[5],points[6], :neg),
                LinearCollider.new(points[6],points[7], :pos),
                LinearCollider.new(points[0],points[7], :pos)
              ]
          elsif @orientation == :up
              #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
              #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size, @block_size * 2, @r, @g, @b]
              points = [
                {x:@x + x_offset, y:@y},
                {x:(@x + x_offset)+(@block_size * 3 - @block_offset), y:@y},
                {x:(@x + x_offset)+(@block_size * 3 - @block_offset), y:@y+(@block_size - @block_offset)},
                {x:@x + x_offset + @block_size + @block_size, y:@y+(@block_size - @block_offset)},
                {x:@x + x_offset + @block_size + @block_size, y:@y+@block_size*2},
                {x:@x + x_offset + @block_size, y:@y+@block_size*2},
                {x:@x + x_offset + @block_size, y:@y+(@block_size - @block_offset)},
                {x:@x + x_offset, y:@y+(@block_size - @block_offset)}
              ]
              @squareColliders = [
                SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}),
                SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}),
                SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y-COLLISIONWIDTH,{x:1,y:1}),
                SquareCollider.new(points[3].x,points[3].y,{x:1,y:1}),
                SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y-COLLISIONWIDTH,{x:1,y:1}),
                SquareCollider.new(points[5].x,points[5].y-COLLISIONWIDTH,{x:-1,y:1}),
                SquareCollider.new(points[6].x-COLLISIONWIDTH,points[6].y,{x:-1,y:1}),
                SquareCollider.new(points[7].x,points[7].y-COLLISIONWIDTH,{x:-1,y:1}),
              ]
              @colliders = [
                LinearCollider.new(points[0],points[1], :neg),
                LinearCollider.new(points[1],points[2], :neg),
                LinearCollider.new(points[2],points[3], :pos),
                LinearCollider.new(points[3],points[4], :neg),
                LinearCollider.new(points[4],points[5], :pos),
                LinearCollider.new(points[5],points[6], :neg),
                LinearCollider.new(points[6],points[7], :pos),
                LinearCollider.new(points[0],points[7], :pos)
              ]
          elsif @orientation == :left
              #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
              #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2 - @block_offset, @block_size - @block_offset, @r, @g, @b]
              xh = @x + x_offset
              #points = [
                #{x:@x + x_offset, y:@y},
                #{x:(@x + x_offset)+(@block_size - @block_offset), y:@y},
                #{x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size},
                #{x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size},
                #{x:(@x + x_offset)+ @block_size * 2,y:@y + @block_size+@block_size},
                #{x:(@x + x_offset)+(@block_size - @block_offset),y:@y + @block_size+@block_size},
                #{x:(@x + x_offset)+(@block_size - @block_offset), y:@y+ @block_size * 3 - @block_offset},
                #{x:@x + x_offset , y:@y+ @block_size * 3 - @block_offset}
              #]
              points = [
                {x:@x + x_offset + @block_size, y:@y},
                {x:@x + x_offset + @block_size + (@block_size - @block_offset), y:@y},
                {x:@x + x_offset + @block_size + (@block_size - @block_offset),y:@y+@block_size*3- @block_offset},
                {x:@x + x_offset + @block_size, y:@y+@block_size*3- @block_offset},
                {x:@x + x_offset+@block_size, y:@y+@block_size*2- @block_offset},
                {x:@x + x_offset, y:@y+@block_size*2- @block_offset},
                {x:@x + x_offset, y:@y+@block_size},
                {x:@x + x_offset+@block_size, y:@y+@block_size}
              ]
              @squareColliders = [
                SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}),
                SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}),
                SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y-COLLISIONWIDTH,{x:1,y:1}),
                SquareCollider.new(points[3].x,points[3].y-COLLISIONWIDTH,{x:-1,y:1}),
                SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y,{x:-1,y:1}),
                SquareCollider.new(points[5].x,points[5].y-COLLISIONWIDTH,{x:-1,y:1}),
                SquareCollider.new(points[6].x,points[6].y,{x:-1,y:-1}),
                SquareCollider.new(points[7].x-COLLISIONWIDTH,points[7].y-COLLISIONWIDTH,{x:-1,y:-1}),
              ]
              @colliders = [
                LinearCollider.new(points[0],points[1], :neg),
                LinearCollider.new(points[1],points[2], :neg),
                LinearCollider.new(points[2],points[3], :pos),
                LinearCollider.new(points[3],points[4], :neg),
                LinearCollider.new(points[4],points[5], :pos),
                LinearCollider.new(points[5],points[6], :neg),
                LinearCollider.new(points[6],points[7], :neg),
                LinearCollider.new(points[0],points[7], :pos)
              ]
          elsif @orientation == :down
              #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
              #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 2 - @block_offset, @r, @g, @b]
    
              points = [
                {x:@x + x_offset, y:@y+(@block_size*2)-@block_offset},
                {x:@x + x_offset+ @block_size*3-@block_offset, y:@y+(@block_size*2)-@block_offset},
                {x:@x + x_offset+ @block_size*3-@block_offset, y:@y+(@block_size)},
                {x:@x + x_offset+ @block_size*2-@block_offset, y:@y+(@block_size)},
                {x:@x + x_offset+ @block_size*2-@block_offset, y:@y},#
                {x:@x + x_offset+ @block_size, y:@y},#
                {x:@x + x_offset + @block_size, y:@y+(@block_size)},
                {x:@x + x_offset, y:@y+(@block_size)}
              ]
              @squareColliders = [
                SquareCollider.new(points[0].x,points[0].y-COLLISIONWIDTH,{x:-1,y:1}),
                SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y-COLLISIONWIDTH,{x:1,y:1}),
                SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y,{x:1,y:-1}),
                SquareCollider.new(points[3].x,points[3].y-COLLISIONWIDTH,{x:1,y:-1}),
                SquareCollider.new(points[4].x-COLLISIONWIDTH,points[4].y,{x:1,y:-1}),
                SquareCollider.new(points[5].x,points[5].y,{x:-1,y:-1}),
                SquareCollider.new(points[6].x-COLLISIONWIDTH,points[6].y-COLLISIONWIDTH,{x:-1,y:-1}),
                SquareCollider.new(points[7].x,points[7].y,{x:-1,y:-1}),
              ]
              @colliders = [
                LinearCollider.new(points[0],points[1], :pos),
                LinearCollider.new(points[1],points[2], :pos),
                LinearCollider.new(points[2],points[3], :neg),
                LinearCollider.new(points[3],points[4], :pos),
                LinearCollider.new(points[4],points[5], :neg),
                LinearCollider.new(points[5],points[6], :pos),
                LinearCollider.new(points[6],points[7], :neg),
                LinearCollider.new(points[0],points[7], :neg)
              ]
          end
          return points
        end
    
        def draw(args)
            #Offset the coordinates to the edge of the game area
            x_offset = (args.state.board_width + args.grid.w / 8) + (@block_offset / 2)
    
            if @orientation == :right
                #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
                args.outputs.solids << {x: (@x + x_offset), y: @y, w: @block_size - @block_offset, h: (@block_size * 3 - @block_offset), r: @r , g: @g, b: @b}
                #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2, @block_size, @r, @g, @b]
                args.outputs.solids << {x: (@x + x_offset), y: (@y + @block_size), w: (@block_size * 2), h: (@block_size), r: @r , g: @g, b: @b }
            elsif @orientation == :up
                #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
                args.outputs.solids << {x: (@x + x_offset), y: (@y), w: (@block_size * 3 - @block_offset), h: (@block_size - @block_offset), r: @r , g: @g, b: @b}
                #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size, @block_size * 2, @r, @g, @b]
                args.outputs.solids << {x: (@x + x_offset + @block_size), y: (@y), w: (@block_size), h: (@block_size * 2), r: @r , g: @g, b: @b}
            elsif @orientation == :left
                #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
                args.outputs.solids << {x: (@x + x_offset + @block_size), y: (@y), w: (@block_size - @block_offset), h: (@block_size * 3 - @block_offset), r: @r , g: @g, b: @b}
                #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 2 - @block_offset, @block_size - @block_offset, @r, @g, @b]
                args.outputs.solids << {x: (@x + x_offset), y: (@y + @block_size), w: (@block_size * 2 - @block_offset), h: (@block_size - @block_offset), r: @r , g: @g, b: @b}
            elsif @orientation == :down
                #args.outputs.solids << [@x + x_offset, @y + @block_size, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
                args.outputs.solids << {x: (@x + x_offset), y: (@y + @block_size), w: (@block_size * 3 - @block_offset), h: (@block_size - @block_offset), r: @r , g: @g, b: @b}
                #args.outputs.solids << [@x + x_offset + @block_size, @y, @block_size - @block_offset, @block_size * 2 - @block_offset, @r, @g, @b]
                args.outputs.solids << {x: (@x + x_offset + @block_size), y: (@y), w: (@block_size - @block_offset), h: ( @block_size * 2 - @block_offset), r: @r , g: @g, b: @b}
            end
    
            #psize = 5.0
            #for p in @shapePoints
              #args.outputs.solids << [p.x-psize/2, p.y-psize/2, psize, psize, 0, 0, 0]
            #end
            args.outputs.labels << [@x + x_offset + (@block_size * 2 - @block_offset)/2, (@y) + (@block_size * 2 - @block_offset)/2, @count.to_s]
    
        end
    
        def updateOne_old args
          didHit = false
          hitter = nil
          toCollide = nil
          for b in args.state.balls
            if [b.x, b.y, b.width, b.height].intersect_rect?(@bold)
              didSquare = false
              for s in @squareColliders
                if (s.collision?(args, b))
                  didSquare = true
                  didHit = true
                  #s.collide(args, b)
                  toCollide = s
                  hitter = b
                  break
                end
              end
              if (didSquare == false)
                for c in @colliders
                  #puts args.state.ball.velocity
                  if c.collision?(args, b.getPoints(args),b)
                    #c.collide args, b
                    toCollide = c
                    didHit = true
                    hitter = b
                    break
                  end
                end
              end
            end
            if didHit
              break
            end
          end
          if (didHit)
            @count=0
            hitter.makeLeader args
            #toCollide.collide(args, hitter)
            args.state.tshapes.delete(self)
            #puts "HIT!" + hitter.number
          end
        end
    
        def update_old args
          if (@count == 1)
            updateOne args
            return
          end
          didHit = false
          hitter = nil
          for b in args.state.ballParents
            if [b.x, b.y, b.width, b.height].intersect_rect?(@bold)
              didSquare = false
              for s in @squareColliders
                if (s.collision?(args, b))
                  didSquare = true
                  didHit=true
                  s.collide(args, b)
                  hitter = b
                end
              end
              if (didSquare == false)
                for c in @colliders
                  #puts args.state.ball.velocity
                  if c.collision?(args, b.getPoints(args), b)
                    c.collide args, b
                    didHit=true
                    hitter = b
                  end
                end
              end
            end
          end
          if (didHit)
            @count=@count-1
            @damageCount.append([(hitter.leastChain+1 - hitter.number)-1, args.state.tick_count])
    
            if (@count == 0)
              args.state.tshapes.delete(self)
              return
            end
          end
          i=0
    
          while i < @damageCount.length
            if @damageCount[i][0] <= 0
              @damageCount.delete_at(i)
              i-=1
            elsif @damageCount[i][1].elapsed_time > BALL_DISTANCE
              @count-=1
              @damageCount[i][0]-=1
            end
            if (@count == 0)
              args.state.tshapes.delete(self)
              return
            end
            i+=1
          end
        end #end update
    
        def update args
          universalUpdate args, self
        end
    
    end
    
    class Line
        attr_accessor :count, :x, :y, :home, :bold, :squareColliders, :colliders, :damageCount
        def initialize(args, x, y, block_size, orientation, block_offset)
            @x = x * block_size
            @y = y * block_size
            @block_size = block_size
            @block_offset = block_offset
            @orientation = orientation
            @damageCount = []
            @home = "lines"
    
            Kernel.srand()
            @r = rand(255)
            @g = rand(255)
            @b = rand(255)
    
            @count = rand(MAX_COUNT)+1
    
            @shapePoints = getShapePoints(args)
            minX={x:INFINITY, y:0}
            minY={x:0, y:INFINITY}
            maxX={x:-INFINITY, y:0}
            maxY={x:0, y:-INFINITY}
            for p in @shapePoints
              if p.x < minX.x
                minX = p
              end
              if p.x > maxX.x
                maxX = p
              end
              if p.y < minY.y
                minY = p
              end
              if p.y > maxY.y
                maxY = p
              end
            end
    
    
            hypotenuse=args.state.ball_hypotenuse
    
            @bold = [(minX.x-hypotenuse/2)-1, (minY.y-hypotenuse/2)-1, -((minX.x-hypotenuse/2)-1)+(maxX.x + hypotenuse + 2), -((minY.y-hypotenuse/2)-1)+(maxY.y + hypotenuse + 2)]
        end
    
        def getShapePoints(args)
          points=[]
          x_offset = (args.state.board_width + args.grid.w / 8) + (@block_offset / 2)
    
          if @orientation == :right
            #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
            xa =@x + x_offset
            ya =@y
            wa =@block_size * 3 - @block_offset
            ha =(@block_size - @block_offset)
          elsif @orientation == :up
            #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
            xa =@x + x_offset
            ya =@y
            wa =@block_size - @block_offset
            ha =@block_size * 3 - @block_offset
    
          elsif @orientation == :left
            #args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
            xa =@x + x_offset
            ya =@y
            wa =@block_size * 3 - @block_offset
            ha =@block_size - @block_offset
          elsif @orientation == :down
            #args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
            xa =@x + x_offset
            ya =@y
            wa =@block_size - @block_offset
            ha =@block_size * 3 - @block_offset
          end
          points = [
            {x: xa, y:ya},
            {x: xa + wa,y:ya},
            {x: xa + wa,y:ya+ha},
            {x: xa, y:ya+ha},
          ]
          @squareColliders = [
            SquareCollider.new(points[0].x,points[0].y,{x:-1,y:-1}),
            SquareCollider.new(points[1].x-COLLISIONWIDTH,points[1].y,{x:1,y:-1}),
            SquareCollider.new(points[2].x-COLLISIONWIDTH,points[2].y-COLLISIONWIDTH,{x:1,y:1}),
            SquareCollider.new(points[3].x,points[3].y-COLLISIONWIDTH,{x:-1,y:1}),
          ]
          @colliders = [
            LinearCollider.new(points[0],points[1], :neg),
            LinearCollider.new(points[1],points[2], :neg),
            LinearCollider.new(points[2],points[3], :pos),
            LinearCollider.new(points[0],points[3], :pos),
          ]
          return points
        end
    
        def update args
          universalUpdate args, self
        end
    
        def draw(args)
            x_offset = (args.state.board_width + args.grid.w / 8) + @block_offset / 2
    
            if @orientation == :right
                args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
            elsif @orientation == :up
                args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
            elsif @orientation == :left
                args.outputs.solids << [@x + x_offset, @y, @block_size * 3 - @block_offset, @block_size - @block_offset, @r, @g, @b]
            elsif @orientation == :down
                args.outputs.solids << [@x + x_offset, @y, @block_size - @block_offset, @block_size * 3 - @block_offset, @r, @g, @b]
            end
    
            args.outputs.labels << [@x + x_offset + (@block_size * 2 - @block_offset)/2, (@y) + (@block_size * 2 - @block_offset)/2, @count.to_s]
    
        end
    end
    
    

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

    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 = args.state.tick_count
        end
    
        if args.state.game_over_at.elapsed_time < 60 && args.state.tick_count > 60 && args.state.bricks.count != 0
          #Display a "Game over" message
          args.outputs.labels << [100, 100, "GAME OVER", 10]
        end
    
        #If 60 frames have passed since the game ended, restart the game
        if args.state.game_over_at != 0 && args.state.game_over_at.elapsed_time == 60
          # FIXME: only put value types in state
          args.state.ball = Ball.new
    
          # FIXME: only put value types in state
          args.state.paddle = Paddle.new
    
          args.state.bricks = []
          args.state.num_bricks = 0
        end
      end
    
      def calc_collision args
        #Remove the brick if it is hit with the ball
        ball = args.state.ball
        ball_rect = [ball.xy.x, ball.xy.y, 20, 20]
    
        #Loop through each brick to see if the ball is colliding with it
        args.state.bricks.each do |b|
          if b.rect.intersect_rect?(ball_rect)
            #Run the linear collider for the brick if there is a collision
            b[:collider_bottom].update args
            b[:collider_right].update args
            b[:collider_left].update args
            b[:collider_top].update args
    
            b.broken = true
          end
        end
    
        args.state.bricks = args.state.bricks.reject(&:broken)
      end
    
      def win_game args
        if args.state.bricks.count == 0 && args.state.game_over_at.elapsed_time > 60
          #Freeze the ball
          args.state.ball.velocity.x = 0
          args.state.ball.velocity.y = 0
          #Freeze the paddle
          args.state.paddle.enabled = false
    
          args.state.game_over_at = args.state.tick_count
        end
    
        if args.state.game_over_at.elapsed_time < 60 && args.state.tick_count > 60 && args.state.bricks.count == 0
          #Display a "Game over" message
          args.outputs.labels << [100, 100, "CONGRATULATIONS!", 10]
        end
      end
    
    end
    
    def tick args
      defaults args
      render args
      calc args
    
      #args.outputs.lines << [0, 0, args.grid.w, args.grid.h]
    
      #$tc+=1
      #if $tc == 5
        #$train << [args.state.ball.xy.x, args.state.ball.xy.y]
        #$tc = 0
      #end
      #for t in $train
    
        #args.outputs.solids << [t[0],t[1],5,5,255,0,0];
      #end
    end
    
    

    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.
    
    # toolbar interaction is in a seperate file
    require 'app/toolbar.rb'
    
    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
    
    $gtk.reset_and_replay "replay.txt", speed: 2
    
    

    Ramp Collision - toolbar.rb link

    # ./samples/04_physics_and_collisions/12_ramp_collision/app/toolbar.rb
    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
    
    

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

    Reading Writing Files - main.rb link

    # ./samples/06_save_load/00_reading_writing_files/app/main.rb
    # APIs covered:
    #   args.gtk.write_file "file-1.txt", args.state.tick_count.to_s
    #   args.gtk.append_file "file-1.txt", args.state.tick_count.to_s
    
    #   stat = args.gtk.stat_file "file-1.txt"
    
    #   contents = args.gtk.read_file "file-1.txt"
    
    #   args.gtk.delete_file "file-1.txt"
    #   args.gtk.delete_file_if_exist "file-1.txt"
    
    #   root_files = args.gtk.list_files ""
    #   app_files  = args.gtk.list_files "app"
    
    def tick args
      # create buttons
      args.state.buttons ||= [
        create_button(args, id: :write_file_1,  row: 0, col: 0, text: "write file-1.txt"),
        create_button(args, id: :append_file_1, row: 1, col: 0, text: "append file-1.txt"),
        create_button(args, id: :delete_file_1, row: 2, col: 0, text: "delete file-1.txt"),
    
        create_button(args, id: :read_file_1,   row: 0, col: 3, text: "read file-1.txt"),
        create_button(args, id: :stat_file_1,   row: 1, col: 3, text: "stat file-1.txt"),
        create_button(args, id: :list_files,    row: 2, col: 3, text: "list files"),
      ]
    
      # 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
        long_string = args.state.center_label_text
        max_character_length = 80
        long_strings_split = args.string.wrapped_lines long_string, max_character_length
        line_height = 23
        offset = (long_strings_split.length / 2) * line_height
        args.outputs.labels << long_strings_split.map_with_index do |s, i|
          {
            x: 400,
            y: 60.from_top - (i * line_height),
            text: s
          }
        end
      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 :write_file_1
          args.gtk.write_file("file-1.txt", args.state.tick_count.to_s + "\n")
    
          args.state.center_label_text = ""
          args.state.center_label_text += "* Success (#{args.state.tick_count}):\n"
          args.state.center_label_text += "  Click \"read file-1.txt\" to see the contents.\n"
          args.state.center_label_text += "\n"
          args.state.center_label_text += "** Sample Code\n"
          args.state.center_label_text += "   args.gtk.write_file(\"file-1.txt\", args.state.tick_count.to_s + \"\\n\")\n"
        when :append_file_1
          args.gtk.append_file("file-1.txt", args.state.tick_count.to_s + "\n")
    
          args.state.center_label_text = ""
          args.state.center_label_text += "* Success (#{args.state.tick_count}):\n"
          args.state.center_label_text += "  Click \"read file-1.txt\" to see the contents.\n"
          args.state.center_label_text += "\n"
          args.state.center_label_text += "** Sample Code\n"
          args.state.center_label_text += "   args.gtk.append_file(\"file-1.txt\", args.state.tick_count.to_s + \"\\n\")\n"
        when :stat_file_1
          stat = args.gtk.stat_file "file-1.txt"
    
          args.state.center_label_text = ""
          args.state.center_label_text += "* Stat File (#{args.state.tick_count})\n"
          args.state.center_label_text += "#{stat || "nil (file does not exist)"}"
          args.state.center_label_text += "\n"
          args.state.center_label_text += "\n"
          args.state.center_label_text += "** Sample Code\n"
          args.state.center_label_text += "   args.gtk.stat_files(\"file-1.txt\")\n"
        when :read_file_1
          contents = args.gtk.read_file("file-1.txt")
    
          args.state.center_label_text = ""
          if contents
            args.state.center_label_text += "* Contents (#{args.state.tick_count}):\n"
            args.state.center_label_text += contents
            args.state.center_label_text += "\n"
            args.state.center_label_text += "** Sample Code\n"
            args.state.center_label_text += "   contents = args.gtk.read_file(\"file-1.txt\")\n"
          else
            args.state.center_label_text += "* Contents (#{args.state.tick_count}):\n"
            args.state.center_label_text += "Contents of file was nil. Click stat file-1.txt for file information."
            args.state.center_label_text += "\n"
            args.state.center_label_text += "** Sample Code\n"
            args.state.center_label_text += "   contents = args.gtk.read_file(\"file-1.txt\")\n"
          end
        when :delete_file_1
          args.state.center_label_text = ""
    
          if args.gtk.stat_file "file-1.txt"
            args.gtk.delete_file "file-1.txt"
            args.state.center_label_text += "* Delete File\n"
            args.state.center_label_text += "file-1.txt was deleted. Click \"list files\" or \"stat file-1.txt\" for more info."
            args.state.center_label_text += "\n"
            args.state.center_label_text += "\n"
            args.state.center_label_text += "** Sample Code\n"
            args.state.center_label_text += "   args.gtk.delete_file(\"file-1.txt\")\n"
          else
            args.state.center_label_text = ""
            args.state.center_label_text += "* Delete File\n"
            args.state.center_label_text += "File does not exist. Click \"write file-1.txt\" or \"append file-1.txt\" to create file."
            args.state.center_label_text += "\n"
            args.state.center_label_text += "\n"
            args.state.center_label_text += "** Sample Code\n"
            args.state.center_label_text += "   if args.gtk.stat_file(\"file-1.txt\") ...\n"
          end
        when :list_files
          root_files = args.gtk.list_files ""
          app_files  = args.gtk.list_files "app"
    
          args.state.center_label_text = ""
          args.state.center_label_text += "** Root Files (#{args.state.tick_count}):\n"
          args.state.center_label_text += root_files.join "\n"
          args.state.center_label_text += "\n"
          args.state.center_label_text += "\n"
          args.state.center_label_text += "** App Files (#{args.state.tick_count}):\n"
          args.state.center_label_text += app_files.join "\n"
          args.state.center_label_text += "\n"
          args.state.center_label_text += "\n"
          args.state.center_label_text += "** Sample Code\n"
          args.state.center_label_text += "   root_files = args.gtk.list_files(\"\")\n"
          args.state.center_label_text += "   app_files = args.gtk.list_files(\"app\")\n"
        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: -2,
            alignment_enum: 1,
            vertical_alignment_enum: 1,
            primitive_marker: :label
          }
        ]
      }
    end
    
    $gtk.reset
    
    

    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?
        state.tick_count.mod_zero?(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, state.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] && args.state.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:         args.state.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 args.state.tick_count != args.state.graphed_at
            args.outputs.static_lines.clear
            args.outputs.static_sprites.clear
          end
    
          wave = wave
    
          r, g, b = frequency.to_i % 85,
                    frequency.to_i % 170,
                    frequency.to_i % 255
    
          starting_rect = args.layout.rect(row: 5, col: 13)
          x_scale    = 10
          y_scale    = 100
          max_points = 25
    
          points = wave
          if wave.length > max_points
            resolution = wave.length.idiv max_points
            points = wave.find_all.with_index { |y, i| (i % resolution == 0) }
          end
    
          args.outputs.static_lines << points.map_with_index do |y, x|
            next_y = points[x + 1]
    
            if next_y
              {
                x:  starting_rect.x + (x * x_scale),
                y:  starting_rect.y + starting_rect.h.half + y_scale * y,
                x2: starting_rect.x + ((x + 1) * x_scale),
                y2: starting_rect.y + starting_rect.h.half + y_scale * next_y,
                r:  r,
                g:  g,
                b:  b
              }
            end
          end
    
          args.outputs.static_sprites << points.map_with_index do |y, x|
            {
              x:  (starting_rect.x + (x * x_scale)) - 2,
              y:  (starting_rect.y + starting_rect.h.half + y_scale * y) - 2,
              w:  4,
              h:  4,
              path: 'sprites/square-white.png',
              r: r,
              g: g,
              b: b
            }
          end
    
          args.state.graphed_at = args.state.tick_count
        end
      end
    
      begin # region: musical note mapping
        def defaults_frequency_for
          { note: :a, octave: 5, sharp:  false, flat:   false }
        end
    
        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
    
    

    Advanced Rendering link

    Labels With Wrapped Text - main.rb link

    # ./samples/07_advanced_rendering/00_labels_with_wrapped_text/app/main.rb
    def tick args
      # defaults
      args.state.scroll_location  ||= 0
      args.state.textbox.messages ||= []
      args.state.textbox.scroll   ||= 0
    
      # render
      args.outputs.background_color = [0, 0, 0, 255]
      render_messages args
      render_instructions args
    
      # inputs
      if args.inputs.keyboard.key_down.one
        queue_message args, "Hello there neighbour! my name is mark, how is your day today?"
      end
    
      if args.inputs.keyboard.key_down.two
        queue_message args, "I'm doing great sir, actually I'm having a picnic today"
      end
    
      if args.inputs.keyboard.key_down.three
        queue_message args, "Well that sounds wonderful!"
      end
    
      if args.inputs.keyboard.key_down.home
        args.state.scroll_location = 1
      end
    
      if args.inputs.keyboard.key_down.delete
        clear_message_queue args
      end
    end
    
    def queue_message args, msg
      args.state.textbox.messages.concat msg.wrapped_lines 50
    end
    
    def clear_message_queue args
      args.state.textbox.messages = nil
      args.state.textbox.scroll = 0
    end
    
    def render_messages args
      args.outputs[:textbox].transient!
      args.outputs[:textbox].w = 400
      args.outputs[:textbox].h = 720
    
      args.outputs.primitives << args.state.textbox.messages.each_with_index.map do |s, idx|
        {
          x: 0,
          y: 20 * (args.state.textbox.messages.size - idx) + args.state.textbox.scroll * 20,
          text: s,
          size_enum: -3,
          alignment_enum: 0,
          r: 255, g:255, b: 255, a: 255
        }
      end
    
      args.outputs[:textbox].labels << args.state.textbox.messages.each_with_index.map do |s, idx|
        {
          x: 0,
          y: 20 * (args.state.textbox.messages.size - idx) + args.state.textbox.scroll * 20,
          text: s,
          size_enum: -3,
          alignment_enum: 0,
          r: 255, g:255, b: 255, a: 255
        }
      end
    
      args.outputs[:textbox].borders << [0, 0, args.outputs[:textbox].w, 720]
    
      args.state.textbox.scroll += args.inputs.mouse.wheel.y unless args.inputs.mouse.wheel.nil?
    
      if args.state.scroll_location > 0
        args.state.textbox.scroll = 0
        args.state.scroll_location = 0
      end
    
      args.outputs.sprites << [900, 0, args.outputs[:textbox].w, 720, :textbox]
    end
    
    def render_instructions args
      args.outputs.labels << [30,
                              30.from_top,
                              "press 1, 2, 3 to display messages, MOUSE WHEEL to scroll, HOME to go to top, BACKSPACE to delete.",
                              0, 255, 255]
    
      args.outputs.primitives << [0, 55.from_top, 1280, 30, :pixel, 0, 255, 0, 0, 0].sprite
    end
    
    

    Rotating Label - main.rb link

    # ./samples/07_advanced_rendering/00_rotating_label/app/main.rb
    def tick args
      # set the render target width and height to match the label
      args.outputs[:scene].transient!
      args.outputs[:scene].w = 220
      args.outputs[:scene].h = 30
    
    
      # make the background transparent
      args.outputs[:scene].background_color = [255, 255, 255, 0]
    
      # set the blendmode of the label to 0 (no blending)
      # center it inside of the scene
      # set the vertical_alignment_enum to 1 (center)
      args.outputs[:scene].labels  << { x: 0,
                                        y: 15,
                                        text: "label in render target",
                                        blendmode_enum: 0,
                                        vertical_alignment_enum: 1 }
    
      # add a border to the render target
      args.outputs[:scene].borders << { x: 0,
                                        y: 0,
                                        w: args.outputs[:scene].w,
                                        h: args.outputs[:scene].h }
    
      # add the rendertarget to the main output as a sprite
      args.outputs.sprites << { x: 640 - args.outputs[:scene].w.half,
                                y: 360 - args.outputs[:scene].h.half,
                                w: args.outputs[:scene].w,
                                h: args.outputs[:scene].h,
                                angle: args.state.tick_count,
                                path: :scene }
    end
    
    

    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
    
      # mark it as transient so that the render target
      # isn't cached (since we are going to be changing it every frame)
      args.outputs[:clipped_area].transient!
    
      # 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: args.state.tick_count % 1280,
                                y: 0,
                                w: 80,
                                h: 80,
                                angle: args.state.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 args.state.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].transient!
      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 args.state.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 args.state.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].transient!
      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].transient!
      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.y1, line.x2 - line.x1) * 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 = args.state.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 args.state.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 state.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, state.tick_count % 255, 0, 0, 255] # red saturation changes
        outputs.lines  << [5, 390 - state.tick_count % 25, 300, 390, 0, 0, 0, 255] # y position changes
        outputs.lines  << [5 + state.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 + state.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 + state.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', state.tick_count % 360] # angle changes
        outputs.sprites << [ 300, 40, 128, 101, 'dragonruby.png', 0, state.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: #{state.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 = state.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.outputs.transient = true
      $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 [X1, Y1, 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: args.state.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 args.state.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].transient!
        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].transient!
      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 && args.state.tick_count.zmod?(3)
        args.state.camera.scale += 0.05
      elsif args.inputs.keyboard.hyphen && args.state.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].transient!
      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 = args.state.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 && args.state.tick_count.zmod?(3)
        args.state.camera.scale += 0.05
      elsif args.inputs.keyboard.hyphen && args.state.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,
                                         args.state.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].transient!
        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].transient!
         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].transient!
        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].transient!
        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].transient!
        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
    
    

    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
    
    

    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 = (args.state.tick_count == 0) || (args.inputs.keyboard.key_down.space)
      args.render_target(:accumulation).transient = true
      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 args.state.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 = args.state.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].transient!
      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].transient!
      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].transient!
      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
      args.outputs.labels << {
        x: 640,
        y: 30.from_top,
        text: "Triangle rendering is available in Indie and Pro versions (ignored in Standard Edition).",
        alignment_enum: 1
      }
    
      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,
        args.state.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: args.state.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(args.state.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(args.state.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(args.state.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
      args.outputs.labels << {
        x: 640,
        y: 30.from_top,
        text: "Triangle rendering is available in Indie and Pro versions (ignored in Standard Edition).",
        alignment_enum: 1
      }
    
      transform_scale = ((args.state.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].transient!
      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].transient!
      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 args.state.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(args.state.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(args.state.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. Triangles is a Indie/Pro Feature and will be ignored in Standard." }
    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. Triangles is a Indie/Pro Feature and will be ignored in Standard.",
                               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 args.state.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 args.state.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 args.state.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 args.state.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 args.state.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 args.state.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 state.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].transient!
        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. Triangles is a Indie/Pro Feature and will be ignored in Standard.",
                               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
        # fn.each_send pass.solids,            self, :draw_solid
        # fn.each_send pass.static_solids,     self, :draw_solid
        # fn.each_send pass.sprites,           self, :draw_sprite
        # fn.each_send pass.static_sprites,    self, :draw_sprite
        # fn.each_send pass.primitives,        self, :draw_primitive
        # fn.each_send pass.static_primitives, self, :draw_primitive
        fn.each_send pass.labels,            self, :draw_label
        fn.each_send pass.static_labels,     self, :draw_label
        # fn.each_send pass.lines,             self, :draw_line
        # fn.each_send pass.static_lines,      self, :draw_line
        # fn.each_send pass.borders,           self, :draw_border
        # fn.each_send pass.static_borders,    self, :draw_border
    
        # if !self.production
        #   fn.each_send pass.debug,           self, :draw_primitive
        #   fn.each_send pass.static_debug,    self, :draw_primitive
        # end
    
        # fn.each_send pass.reserved,          self, :draw_primitive
        # fn.each_send pass.static_reserved,   self, :draw_primitive
      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
    
    

    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 args.state.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].transient!
      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: "native_scale:       #{args.grid.native_scale}", **label_style }
      args.outputs.labels << { x: 10, y:  40.from_top, text: "native_scale_enum:  #{args.grid.native_scale_enum}",  **label_style }
      args.outputs.labels << { x: 10, y:  70.from_top, text: "hd_offset_x:        #{args.grid.hd_offset_x}", **label_style }
      args.outputs.labels << { x: 10, y: 100.from_top, text: "hd_offset_y:        #{args.grid.hd_offset_y}", **label_style }
    
      if (args.state.tick_count % 500) < 250
        args.outputs.labels << { x: 10, y: 130.from_top, text: "cropped to:         grid", **label_style }
    
        args.outputs.sprites << { x:        0,
                                  y:        0,
                                  w:        1280,
                                  h:        720,
                                  source_x: 2000 - 640,
                                  source_y: 2000 - 320,
                                  source_w: 1280,
                                  source_h: 720,
                                  path: "sprites/world.png" }
      else
        args.outputs.labels << { x: 10, y: 130.from_top, text: "cropped to:         allscreen", **label_style }
    
        args.outputs.sprites << { x:        0    - args.grid.hd_offset_x,
                                  y:        0    - args.grid.hd_offset_y,
                                  w:        1280 + args.grid.hd_offset_x * 2,
                                  h:        720  + args.grid.hd_offset_y * 2,
                                  source_x: 2000 - 640 - args.grid.hd_offset_x,
                                  source_y: 2000 - 320 - args.grid.hd_offset_y,
                                  source_w: 1280 + args.grid.hd_offset_x * 2,
                                  source_h: 720  + args.grid.hd_offset_y * 2,
                                  path:     "sprites/world.png" }
    
        args.outputs.sprites << { x:        0    - args.grid.hd_offset_x,
                                  y:        0    - args.grid.hd_offset_y,
                                  w:        1280 + args.grid.hd_offset_x * 2,
                                  h:        720  + args.grid.hd_offset_y * 2,
                                  source_x: 2000 - 640 - args.grid.hd_offset_x,
                                  source_y: 2000 - 320 - args.grid.hd_offset_y,
                                  source_w: 1280 + args.grid.hd_offset_x * 2,
                                  source_h: 720  + args.grid.hd_offset_y * 2,
                                  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, :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]
      # 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 - args.state.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 x1, y1, x2, y2, x3, y3, x4, y4, step
      step ||= 0
      color = [200, 200, 200]
      points = points_for_bezier [x1, y1], [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 = args.state.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 state.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].transient!
        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].transient!
        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 state.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 = state.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 = state.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 !args.state.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: args.state.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 state.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 state.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 state.tick_count + 255 do |args, entry|
          $gtk.notify "Cutscene complete!"
        end
    
        # start the fade to black
        @tick_queue.queue_tick state.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 args.state.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[args.state.tick_count] && @queued_ticks[args.state.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 args.state.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 args.state.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 args.state.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 args.state.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 args.state.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
      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 args.state.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 args.state.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].transient!
      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    = rand 1280
        @y    = rand 720
        @w    = 15
        @h    = 15
        @path = 'sprites/square/blue.png'
        @dir = 1
      end
    
      def mark_collisions all
        @path = if all[self]
                  'sprites/square/red.png'
                else
                  'sprites/square/blue.png'
                end
      end
    
      def move
        @dir  = -1 if (@x + @w >= 1280) && @dir ==  1
        @dir  =  1 if (@x      <=    0) && @dir == -1
        @x   += @dir
      end
    end
    
    def reset_if_needed args
      if args.state.tick_count == 0 || args.inputs.mouse.click
        args.state.star_count = 1500
        args.state.stars = args.state.star_count.map { |i| Square.new }.to_a
        args.outputs.static_sprites.clear
        args.outputs.static_sprites << args.state.stars
      end
    end
    
    def tick args
      reset_if_needed args
    
      Fn.each args.state.stars do |s| s.move end
    
      all = GTK::Geometry.find_collisions args.state.stars
      Fn.each args.state.stars do |s| s.mark_collisions all end
    
      args.outputs.background_color = [0, 0, 0]
      args.outputs.primitives << args.gtk.current_framerate_primitives
    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 = args.state.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, args.state.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, args.state.tick_count, 30, :smooth_stop_quint, :flip)
    
          [
            label,
            prefab.merge(primitive_marker: :solid, a: a),
            prefab.merge(primitive_marker: :border)
          ]
        end
      end
    end
    
    

    Advanced Debugging link

    Logging - main.rb link

    # ./samples/10_advanced_debugging/00_logging/app/main.rb
    def tick args
      args.outputs.background_color = [255, 255, 255, 0]
      if args.state.tick_count == 0
        args.gtk.log_spam "log level spam"
        args.gtk.log_debug "log level debug"
        args.gtk.log_info "log level info"
        args.gtk.log_warn "log level warn"
        args.gtk.log_error "log level error"
        args.gtk.log_unfiltered "log level unfiltered"
        puts "This is a puts call"
        args.gtk.console.show
      end
    
      if args.state.tick_count == 60
        puts "This is a puts call on tick 60"
      elsif args.state.tick_count == 120
        puts "This is a puts call on tick 120"
      end
    end
    
    

    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 - 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.port ||= 3000
      args.state.reqnum ||= 0
      # by default the embedded webserver runs on port 9001 (the port number is over 9000) and is disabled in a production build
      # to enable the http server in a production build, you need to manually start
      # the server up:
      args.gtk.start_server! port: args.state.port, enable_in_prod: true
      args.outputs.background_color = [0, 0, 0]
      args.outputs.labels << [640, 600, "Point your web browser at http://localhost:#{args.state.port}/", 10, 1, 255, 255, 255]
    
      if args.state.tick_count == 1
        $gtk.openurl "http://localhost:3000"
      end
    
      args.inputs.http_requests.each { |req|
        puts("METHOD: #{req.method}");
        puts("URI: #{req.uri}");
        puts("HEADERS:");
        req.headers.each { |k,v| puts("  #{k}: #{v}") }
    
        if (req.uri == '/')
          # headers and body can be nil if you don't care about them.
          # If you don't set the Content-Type, it will default to
          #  "text/html; charset=utf-8".
          # Don't set Content-Length; we'll ignore it and calculate it for you
          args.state.reqnum += 1
          req.respond 200, "<html><head><title>hello</title></head><body><h1>This #{req.method} was request number #{args.state.reqnum}!</h1></body></html>\n", { 'X-DRGTK-header' => 'Powered by DragonRuby!' }
        else
          req.reject
        end
      }
    end
    
    

    In Game Web Server Http Post - main.rb link

    # ./samples/11_http/03_in_game_web_server_http_post/app/main.rb
    def tick args
      # 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
    
    

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

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

    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 args.state.tick_count),
                                      (rotate_y args.state.tick_count),
                                      (rotate_z args.state.tick_count)
    
      args.state.front = mul_triangles args,
                                       args.state.a,
                                       (translate -0.05, -0.05, 0),
                                       (translate 0, 0, 0.05),
                                       (rotate_x args.state.tick_count),
                                       (rotate_y args.state.tick_count),
                                       (rotate_z args.state.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 args.state.tick_count),
                                      (rotate_y args.state.tick_count),
                                      (rotate_z args.state.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 args.state.tick_count),
                                       (rotate_y args.state.tick_count),
                                       (rotate_z args.state.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 args.state.tick_count),
                                     (rotate_y args.state.tick_count),
                                     (rotate_z args.state.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 args.state.tick_count),
                                        (rotate_y args.state.tick_count),
                                        (rotate_z args.state.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 = state.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 state.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   = state.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 = state.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 = state.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 = state.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 = state.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: state.tick_count,
                            angle_y: state.tick_count,
                            angle_z: state.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(args.state.tick_count / SPINDURATION) * Math.tan(args.state.tick_count / SPINDURATION)
      s = Math.sin(a)
      c = Math.cos(a)
      x = STARTX
      y = STARTY
      offset_x = (1280 - (ENDX - STARTX)) / 2
      offset_y =  (360 - (ENDY - STARTY)) / 2
    
      srand(1)
      while y < ENDY do
        while x < ENDX do
          if (y == STARTY ||
              y == (ENDY / 0.5) * 2 ||
              y == (ENDY / 0.5) * 2 + 0.5 ||
              y == ENDY - 0.5 ||
              x == STARTX ||
              x == ENDX - 0.5)
            z = rand(BOXDEPTH)
            z *= Math.sin(a / 2)
            x -= SPINPOINT
            u = (x * c) - (z * s)
            v = (x * s) + (z * c)
            k = DISTANCE.fdiv(100) + (v / 500 * YAW)
            u = u / k
            v = y / k
            w = POINTSIZE / 10 / k
            args.outputs.sprites << { x: offset_x + u - w, y: offset_y + v - w, w: w, h: w, path: 'sprites/square-blue.png'}
            x += SPINPOINT
          end
          x += 0.5
        end
        y += 0.5
        x = STARTX
      end
    end
    
    $gtk.reset
    
    

    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].transient!
      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].transient!
      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 Hell - main.rb link

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

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

    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 state.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,
                                                     state.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 ||= state.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 = state.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, state.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 state.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 = state.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 = state.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)) != state.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

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

    Farming Game Starting Point - repl.rb link

    # ./samples/99_genre_crafting/farming_game_starting_point/app/repl.rb
    # ===============================================================
    # Welcome to repl.rb
    # ===============================================================
    # You can experiement with code within this file. Code in this
    # file is only executed when you save (and only excecuted ONCE).
    # ===============================================================
    
    # ===============================================================
    # REMOVE the "x" from the word "xrepl" and save the file to RUN
    # the code in between the do/end block delimiters.
    # ===============================================================
    
    # ===============================================================
    # ADD the "x" to the word "repl" (make it xrepl) and save the
    # file to IGNORE the code in between the do/end block delimiters.
    # ===============================================================
    
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      puts "The result of 1 + 2 is: #{1 + 2}"
    end
    
    # ====================================================================================
    # Ruby Crash Course:
    # Strings, Numeric, Booleans, Conditionals, Looping, Enumerables, Arrays
    # ====================================================================================
    
    # ====================================================================================
    #  Strings
    # ====================================================================================
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      message = "Hello World"
      puts "The value of message is: " + message
      puts "Any value can be interpolated within a string using \#{}."
      puts "Interpolated message: #{message}."
      puts 'This #{message} is not interpolated because the string uses single quotes.'
    end
    
    # ====================================================================================
    #  Numerics
    # ====================================================================================
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      a = 10
      puts "The value of a is: #{a}"
      puts "a + 1 is: #{a + 1}"
      puts "a / 3 is: #{a / 3}"
    end
    
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      b = 10.12
      puts "The value of b is: #{b}"
      puts "b + 1 is: #{b + 1}"
      puts "b as an integer is: #{b.to_i}"
      puts ''
    end
    
    # ====================================================================================
    #  Booleans
    # ====================================================================================
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      c = 30
      puts "The value of c is #{c}."
    
      if c
        puts "This if statement ran because c is truthy."
      end
    end
    
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      d = false
      puts "The value of d is #{d}."
    
      if !d
        puts "This if statement ran because d is falsey, using the not operator (!) makes d evaluate to true."
      end
    
      e = nil
      puts "Nil is also considered falsey. The value of e is: #{e}."
    
      if !e
        puts "This if statement ran because e is nil (a falsey value)."
      end
    end
    
    # ====================================================================================
    #  Conditionals
    # ====================================================================================
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      i_am_true  = true
      i_am_nil   = nil
      i_am_false = false
      i_am_hi    = "hi"
    
      puts "======== if statement"
      i_am_one = 1
      if i_am_one
        puts "This was printed because i_am_one is truthy."
      end
    
      puts "======== if/else statement"
      if i_am_false
        puts "This will NOT get printed because i_am_false is false."
      else
        puts "This was printed because i_am_false is false."
      end
    
      puts "======== if/elsif/else statement"
      if i_am_false
        puts "This will NOT get printed because i_am_false is false."
      elsif i_am_true
        puts "This was printed because i_am_true is true."
      else
        puts "This will NOT get printed i_am_true was true."
      end
    
      puts "======== case statement "
      i_am_one = 1
      case i_am_one
      when 10
        puts "case equaled: 10"
      when 9
        puts "case equaled: 9"
      when 5
        puts "case equaled: 5"
      when 1
        puts "case equaled: 1"
      else
        puts "Value wasn't cased."
      end
    
      puts "======== different types of comparisons"
      if 4 == 4
        puts "equal (4 == 4)"
      end
    
      if 4 != 3
        puts "not equal (4 != 3)"
      end
    
      if 3 < 4
        puts "less than (3 < 4)"
      end
    
      if 4 > 3
        puts "greater than (4 > 3)"
      end
    
      if ((4 > 3) || (3 < 4) || false)
        puts "or statement ((4 > 3) || (3 < 4) || false)"
      end
    
      if ((4 > 3) && (3 < 4))
        puts "and statement ((4 > 3) && (3 < 4))"
      end
    end
    
    # ====================================================================================
    # Looping
    # ====================================================================================
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      puts "======== times block"
      3.times do |i|
        puts i
      end
      puts "======== range block exclusive"
      (0...3).each do |i|
        puts i
      end
      puts "======== range block inclusive"
      (0..3).each do |i|
        puts i
      end
    end
    
    # ====================================================================================
    #  Enumerables
    # ====================================================================================
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      puts "======== array each"
      colors = ["red", "blue", "yellow"]
      colors.each do |color|
        puts color
      end
    
      puts '======== array each_with_index'
      colors = ["red", "blue", "yellow"]
      colors.each_with_index do |color, i|
        puts "#{color} at index #{i}"
      end
    end
    
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      puts "======== single parameter function"
      def add_one_to n
        n + 5
      end
    
      puts add_one_to(3)
    
      puts "======== function with default value"
      def function_with_default_value v = 10
        v * 10
      end
    
      puts "passing three: #{function_with_default_value(3)}"
      puts "passing nil: #{function_with_default_value}"
    
      puts "======== Or Equal (||=) operator for nil values"
      def function_with_nil_default_with_local a = nil
        result   = a
        result ||= "or equal operator was exected and set a default value"
      end
    
      puts "passing 'hi': #{function_with_nil_default_with_local 'hi'}"
      puts "passing nil: #{function_with_nil_default_with_local}"
    end
    
    # ====================================================================================
    #  Arrays
    # ====================================================================================
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      puts "======== Create an array with the numbers 1 to 10."
      one_to_ten = (1..10).to_a
      puts one_to_ten
    
      puts "======== Create a new array that only contains even numbers from the previous array."
      one_to_ten = (1..10).to_a
      evens = one_to_ten.find_all do |number|
        number % 2 == 0
      end
      puts evens
    
      puts "======== Create a new array that rejects odd numbers."
      one_to_ten = (1..10).to_a
      also_even = one_to_ten.reject do |number|
        number % 2 != 0
      end
      puts also_even
    
      puts "======== Create an array that doubles every number."
      one_to_ten = (1..10).to_a
      doubled = one_to_ten.map do |number|
        number * 2
      end
      puts doubled
    
      puts "======== Create an array that selects only odd numbers and then multiply those by 10."
      one_to_ten = (1..10).to_a
      odd_doubled = one_to_ten.find_all do |number|
        number % 2 != 0
      end.map do |odd_number|
        odd_number * 10
      end
      puts odd_doubled
    
      puts "======== All combination of numbers 1 to 10."
      one_to_ten = (1..10).to_a
      all_combinations = one_to_ten.product(one_to_ten)
      puts all_combinations
    
      puts "======== All uniq combinations of numbers. For example: [1, 2] is the same as [2, 1]."
      one_to_ten = (1..10).to_a
      uniq_combinations =
        one_to_ten.product(one_to_ten)
          .map do |unsorted_number|
        unsorted_number.sort
      end.uniq
      puts uniq_combinations
    end
    
    # ====================================================================================
    #  Advanced Arrays
    # ====================================================================================
    # Remove the x from xrepl to run the code. Add the x back to ignore to code.
    xrepl do
      puts "======== All unique Pythagorean Triples between 1 and 40 sorted by area of the triangle."
    
      one_to_hundred = (1..40).to_a
      triples =
        one_to_hundred.product(one_to_hundred).map do |width, height|
        [width, height, Math.sqrt(width ** 2 + height ** 2)]
      end.find_all do |_, _, hypotenuse|
        hypotenuse.to_i == hypotenuse
      end.map do |triangle|
        triangle.map(&:to_i)
      end.uniq do |triangle|
        triangle.sort
      end.map do |width, height, hypotenuse|
        [width, height, hypotenuse, (width * height) / 2]
      end.sort_by do |_, _, _, area|
        area
      end
    
      triples.each do |width, height, hypotenuse, area|
        puts "(#{width}, #{height}, #{hypotenuse}) = #{area}"
      end
    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: 3,
                  # column for button
                  col: 10,
                  # text
                  text: "I AM CUSTOM",
                  # when clicked call the custom_button_clicked function
                  method: :custom_button_clicked),
    
          (button id: :yay,
                  # row for button
                  row: 3,
                  # column for button
                  col: 9,
                  # 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 state.tick_count == 1
    
        outputs[:rt_canvas].transient!
        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 state.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 state.tick_count.zero?
    
        outputs[:current_animation_frame].transient!
        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 = state.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 = state.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: state.tick_count + i,
                                                  queue_sprite_creation: true }
            queues.create_sprite << { index: i,
                                      at: state.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: state.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 == (state.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: state.tick_count }
          queues.create_sprite << { index: state.animation_frames_selected_index + 1,
                                    at: state.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: state.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: state.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: state.tick_count + 1 }
          end
        end
      end
    
      def update_animation_frame_render_target animation_frame
        return if !animation_frame
    
        outputs[animation_frame[:rt_name]].transient = true
        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: state.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: state.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: state.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: state.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 [X1, Y1, 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 = state.tick_count
        end
      end
    
      def calc
        calc_player
        calc_projectiles
        calc_enemies
        calc_spawn_locations
      end
    
      def calc_player
        if player.attacked_at == state.tick_count
          player.projectiles << { at: state.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
    
      args.state.tick_count = args.state.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 ((args.state.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 = args.state.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") && ((args.state.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] == "<") &&
              ((args.state.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 = args.state.tick_count
        elsif (args.inputs.controller_one.key_down.right || args.inputs.keyboard.key_down.d)
          args.state.inputlist.unshift(">")
        end
    
        args.state.lastpush = args.state.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 ||= args.state.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) #&& ((args.state.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: args.state.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 - args.state.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: #{args.state.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 - args.state.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: #{args.state.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   = args.state.tick_count if args.nokia.mouse_down # you can also use args.nokia.click
      args.state.last_up      = args.state.tick_count if args.nokia.mouse_up
      args.state.label_style  = { size_enum: -1.5 }
    
      args.state.watch_list = [
        "args.state.tick_count is:      #{args.state.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 @args.state.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 @args.state.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.y1 != l.y2
            l.x2 += 1 if l.x1 != 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 @args.state.tick_count <= 0
        return @args.outputs[:lowrez].transient!
      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 @args.state.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.y1 != l.y2
            l.x2 += 1 if l.x1 != 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: args.state.tick_count % 255,
        angle: args.state.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 - args.state.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: #{args.state.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 - args.state.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: #{args.state.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   = args.state.tick_count if args.lowrez.mouse_down # you can also use args.lowrez.click
      args.state.last_up      = args.state.tick_count if args.lowrez.mouse_up
      args.state.label_style  = { size_enum: -1.5 }
    
      args.state.watch_list = [
        "args.state.tick_count is:       #{args.state.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
      render args
      input args
      calc args
    end
    
    def defaults args
      args.state.player.x      ||= args.grid.w.half
      args.state.player.y      ||= 0
      args.state.player.size   ||= 100
      args.state.player.dy     ||= 0
      args.state.player.action ||= :jumping
      args.state.jump.power           = 20
      args.state.jump.increase_frames = 10
      args.state.jump.increase_power  = 1
      args.state.gravity              = -1
    end
    
    def render args
      args.outputs.sprites << {
        x: args.state.player.x -
           args.state.player.size.half,
        y: args.state.player.y,
        w: args.state.player.size,
        h: args.state.player.size,
        path: 'sprites/square/red.png'
      }
    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 = args.state.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
    
    

    Jumping And Collisions - main.rb link

    # ./samples/99_genre_mario/02_jumping_and_collisions/app/main.rb
    class Game
      attr_gtk
    
      def tick
        defaults
        render
        input
        calc
      end
    
      def defaults
        return if state.tick_count != 0
    
        player.x                     = 64
        player.y                     = 800
        player.size                  = 50
        player.dx                    = 0
        player.dy                    = 0
        player.action                = :falling
    
        player.max_speed             = 20
        player.jump_power            = 15
        player.jump_air_time         = 15
        player.jump_increase_power   = 1
    
        state.gravity                = -1
        state.drag                   = 0.001
        state.tile_size              = 64
        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:  9, ordinal_y: 3 },
          { ordinal_x: 10, ordinal_y: 3 },
          { ordinal_x: 11, ordinal_y: 3 },
        ]
    
        tiles.each do |t|
          t.rect = { x: t.ordinal_x * 64,
                     y: t.ordinal_y * 64,
                     w: 64,
                     h: 64 }
        end
      end
    
      def render
        render_player
        render_tiles
        # render_grid
      end
    
      def input
        input_jump
        input_move
      end
    
      def calc
        calc_player_rect
        calc_left
        calc_right
        calc_below
        calc_above
        calc_player_dy
        calc_player_dx
        calc_game_over
      end
    
      def render_player
        outputs.sprites << {
          x: player.x,
          y: player.y,
          w: player.size,
          h: player.size,
          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 * 64,
                  y: t.ordinal_y * 64,
                  w: 64,
                  h: 64
        end
      end
    
      def render_grid
        if state.tick_count == 0
          outputs[:grid].transient!
          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 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 player.dx.abs < 20
          if inputs.keyboard.left
            player.dx -= 2
          elsif inputs.keyboard.right
            player.dx += 2
          end
        end
      end
    
      def calc_game_over
        if player.y < -64
          player.x = 64
          player.y = 800
          player.dx = 0
          player.dy = 0
        end
      end
    
      def calc_player_rect
        player.rect      = player_current_rect
        player.next_rect = player_next_rect
        player.prev_rect = player_prev_rect
      end
    
      def calc_player_dx
        player.dx  = player_next_dx
        player.x  += player.dx
      end
    
      def calc_player_dy
        player.y  += player.dy
        player.dy  = player_next_dy
      end
    
      def calc_below
        return unless player.dy < 0
        tiles_below = tiles_find { |t| t.rect.top <= player.prev_rect.y }
        collision = tiles_find_colliding tiles_below, (player.rect.merge y: player.next_rect.y)
        if collision
          player.y  = collision.rect.y + state.tile_size
          player.dy = 0
          player.action = :standing
        else
          player.action = :falling
        end
      end
    
      def calc_left
        return unless player.dx < 0 && player_next_dx < 0
        tiles_left = tiles_find { |t| t.rect.right <= player.prev_rect.left }
        collision = tiles_find_colliding 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 && player_next_dx > 0
        tiles_right = tiles_find { |t| t.rect.left >= player.prev_rect.right }
        collision = tiles_find_colliding 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 = tiles_find { |t| t.rect.y >= player.prev_rect.y }
        collision = tiles_find_colliding tiles_above, (player.rect.merge y: player.next_rect.y)
        return unless collision
        player.dy = 0
        player.y  = collision.rect.bottom - player.rect.h
      end
    
      def player_current_rect
        { x: player.x, y: player.y, w: player.size, h: player.size }
      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 * 64,
            y: ordinal_y * 64,
            w: 64,
            h: 64 }
        end
      end
    
      def player
        state.player ||= args.state.new_entity :player
      end
    
      def player_next_dy
        player.dy + state.gravity + state.drag ** 2 * -1
      end
    
      def player_next_dx
        player.dx * 0.8
      end
    
      def player_next_rect
        player.rect.merge x: player.x + player_next_dx,
                          y: player.y + player_next_dy
      end
    
      def player_prev_rect
        player.rect.merge x: player.x - player.dx,
                          y: player.y - player.dy
      end
    
      def player_jump
        return if player.action != :standing
        player.action = :jumping
        player.dy = state.player.jump_power
        current_frame = state.tick_count
        player.action_at = current_frame
      end
    
      def player_jump_increase_air_time
        return if player.action != :jumping
        return if player.action_at.elapsed_time >= player.jump_air_time
        player.dy += player.jump_increase_power
      end
    
      def tiles
        state.tiles
      end
    
      def tiles_find_colliding tiles, target
        tiles.find { |t| t.rect.intersect_rect? target }
      end
    
      def tiles_find &block
        tiles.find_all(&block)
      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 state.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 = state.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
    
    

    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 state.tick_count == 0
          outputs.sounds << "sounds/bg.ogg"
        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: args.state.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 << [-circle.x + 1100,
                            -circle.y - 100,
                            2416 * 4,
                            3574 * 4,
                            'sprites/jupiter.png']
        outputs.sprites << [-circle.x,
                            -circle.y,
                            2416 * 4,
                            3574 * 4,
                            '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 - w.idiv(2), y - h, w, h, 255, 255, 255, 255].solid
        outputs.primitives << [x - w.idiv(2), y - h, w, h, 0, 0, 0, 255].border
        outputs.labels << [x, y - 4, label_text, 1, 1, 0, 0, 0, 255]
      end
    
      def render_aiming_retical
        outputs.sprites << [state.camera.x + circle.x + circle.angle.vector_x(circle.potential_lift * 10) - 5,
                            state.camera.y + circle.y + circle.angle.vector_y(circle.potential_lift * 10) - 5,
                            10, 10, 'sprites/circle-orange.png']
        outputs.sprites << [state.camera.x + circle.x + circle.angle.vector_x(circle.radius * 3) - 5,
                            state.camera.y + circle.y + circle.angle.vector_y(circle.radius * 3) - 5,
                            10, 10, 'sprites/circle-orange.png', 0, 128]
        if rand > 0.9
          outputs.sprites << [state.camera.x + circle.x + circle.angle.vector_x(circle.radius * 3) - 5,
                              state.camera.y + circle.y + circle.angle.vector_y(circle.radius * 3) - 5,
                              10, 10, 'sprites/circle-white.png', 0, 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 << [(circle.x - circle.radius) + state.camera.x,
                            (circle.y - circle.radius) + state.camera.y,
                            circle.radius * 2,
                            circle.radius * 2,
                            'sprites/probe.png']
      end
    
      def render_debug
        return unless state.debug_mode
    
        outputs.labels << [10, 30, state.line_mode, 0, 0, 0, 0, 0]
        outputs.labels << [12, 32, state.line_mode, 0, 0, 255, 255, 255]
    
        args.outputs.lines << trajectory(circle).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.line.associate(r: 0, g: 255, b: 0) 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].border.associate(r: 255, g: 0, b: 0) 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.line.associate(r: 0, g: 0, b: 255) 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].border.associate(r: 255, g: 0, b: 0) 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
        [body.x + body.dx,
         body.y + body.dy,
         body.x + body.dx * 1000,
         body.y + body.dy * 1000,
         0, 255, 255]
      end
    
      def lengthen_line line, num
        line = normalize_line(line)
        slope = geometry.line_slope(line, replace_infinity: 10).abs
        if slope < 2
          [line.x - num, line.y, line.x2 + num, line.y2].line.to_hash
        else
          [line.x, line.y, line.x2, line.y2].line.to_hash
        end
      end
    
      def normalize_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
        [x, y, x2, y2]
      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.x1},#{l.y1},#{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.line_intersect(results[:trajectory], t)
          {
            terrain: t,
            point: geometry.line_intersect(results[:trajectory], t),
            type: :terrain
          }
        end
    
        results[:impacts] += lava.find_all { |t| line_near_rect? results[:rect], t }.map do |t|
          intersection = geometry.line_intersect(results[:trajectory], t)
          {
            terrain: t,
            point: geometry.line_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 = state.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 = state.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 state.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: state.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 state.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: state.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 args.state.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).transient!
      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.times.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 args.state.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 ||= args.state.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 args.state.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 ||= args.state.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 = args.state.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 = args.state.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 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [49, 45, 3, 3, 'sprites/square.png', 0, 100 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [50, 46, 1, 1, 'sprites/square.png', 0, 255 * (args.state.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 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [19, 48, 3, 3, 'sprites/square.png', 0, 100 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [20, 49, 1, 1, 'sprites/square.png', 0, 255 * (args.state.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 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [1, 27, 3, 3, 'sprites/square.png', 0, 100 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [2, 28, 1, 1, 'sprites/square.png', 0, 255 * (args.state.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 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [24, 60, 3, 3, 'sprites/square.png', 0, 100 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [25, 61, 1, 1, 'sprites/square.png', 0, 255 * (args.state.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 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [31, 31, 3, 3, 'sprites/square.png', 0, 100 * (args.state.tick_count % 50).fdiv(50), 0, 255, 0]
      lowrez_sprites << [32, 32, 1, 1, 'sprites/square.png', 0, 255 * (args.state.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.times.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