Today, we’re going to dissect a project that a couple of Rubyists we are fond of wrote for RailsCamp last month. Paul Annesley (twitter) and Dennis Hotson (twitter) joined forces at RailsCamp Australia and the result was ASCII-based fireworks.

First, though, if you are unfamiliar with RailsCamps, here is a blurb from their site:

Imagine yourself and a posse of like-minded ruby hackers on a country retreat with zero internet for a weekend of fun. You’ll laugh, hack, learn, cry (well, you probably won’t cry… but you know… it felt poetic) and most likely play a crap-load of Guitar Hero.

The point of a RailsCamp, from what I can tell, is to disconnect with everything except Ruby and the other attendees. It is a programming fest where they encourage hackery and creativity, two things that live in abundance in the Ruby community. Everyone that I know who has attended a RailsCamp has come back saying it was a gamechanger for their career. I need to get one scheduled near Charlotte.

The Project

The project that Dennis and Paul settled on was entitled ROFLBALT. It is a Ruby port of the somewhat famous Canabalt game that you can play online or download to your mobile device. The goal of Canabalt (and all -balt type games) is to run and jump from building to building, going for as long as you can. Your score is directly proportional to how long you go before you die. It’s just a bit of brain candy in simple gaming form, the kind of candy my brain likes best.

Paul and Dennis aimed to complete ROFLBALT using less than 500 lines of Ruby, and they pulled it off. Let’s see how.

How They Did It

I am going to attempt to breakdown the code for ROFLBALT. I am warning you now that Paul and Dennis are, seemingly and unsurprisingly, much smarter than I am. In some parts of this code, I had no idea how they 1) came up with what they did or 2) what the heck the code does. This is, in no way, a reflection on the code or project, but more on the author, who is, um, “special.”

You can find the code for the project on github.

ROFLBALT is broken into the following classes.

  • Game
  • Screen
  • Pixel
  • BAckground
  • WindowColor
  • FrameBuffer
  • World
  • BuildingGenerator
  • (module)Renderable
  • Building
  • Player
  • Blood
  • Scoreboard
  • GameOverBanner
  • RoflCoptor

The executable (in the bin dir) simple runs Game.new.run, so we’ll start there.

Game

SCREEN_WIDTH = 120
SCREEN_HEIGHT = 40

class Game
  def initialize
    reset
  end
  def reset
    @run = true
    @world = World.new(SCREEN_WIDTH)
    @screen = Screen.new(SCREEN_WIDTH, SCREEN_HEIGHT, @world)
  end
  def run
    Signal.trap(:INT) do
      @run = false
    end
    while @run
      start_time = Time.new.to_f
      unless @world.tick
        reset
      end
      render start_time
    end
    on_exit
  end
  def render start_time
    @world.buildings.each do |building|
      @screen.draw(building)
    end
    @screen.draw(@world.player)
    @world.misc.each do |object|
      @screen.draw(object)
    end
    @screen.render start_time
  end
  def on_exit
    @screen.on_exit
  end
end
view raw game.rb This Gist is brought to you using Simple Gist Embed.

Game, as you may have guessed, puts all the bits in place to start the game, as well as handles exiting of the game. It depends on a World and a Screen. In the Game methods, things are pretty high-level, as we’re dealing with a World object and Screen object, both abstractions created by this project. The game starts the fun, basically, in the render method. It draws the buildings, the player, and the rest of the world objects. Note that “drawing” an object means passing it to the screen’s draw method, something we’ll cover when we get to Screen. Other than that, the Game listens for a signal interrupt (Ctrl-C in this case) and kills the game when that happens.

World

class World
  def initialize horizon
    @ticks = 0
    @horizon = horizon
    @building_generator = BuildingGenerator.new(self, WindowColor.new)
    @background = Background.new(self)
    @player = Player.new(25, @background)
    @buildings = [ @building_generator.build(-10, 30, 120) ]
    @misc = [ Scoreboard.new(self), RoflCopter.new(50, 4, @background) ]
    @speed = 4
    @distance = 0
  end
  attr_reader :buildings, :player, :horizon, :speed, :misc, :ticks, :distance, :background
  def tick
    # TODO: this, but less often.
    if @ticks % 20 == 0
      @building_generator.generate_if_necessary
      @building_generator.destroy_if_necessary
    end

    @distance += speed

    buildings.each do |b|
      b.move_left speed
    end

    if b = building_under_player
      if player.bottom_y > b.y
        b.move_left(-speed)
        @speed = 0
        @misc << Blood.new(player.x, player.y)
        @misc << GameOverBanner.new
        player.die!
      end
    end

    begin
      if STDIN.read_nonblock(1)
        if player.dead?
          return false
        else
          player.jump
        end
      end
    rescue Errno::EAGAIN
    end

    player.tick

    if b = building_under_player
      player.walk_on_building b if player.bottom_y >= b.y
    end

    @ticks += 1
  end
  def building_under_player
    buildings.detect do |b|
      b.x <= player.x && b.right_x >= player.right_x
    end
  end
end
view raw world.rb This Gist is brought to you using Simple Gist Embed.

World holds all of the renderable items that the game can contain. The Buildings, the background, scoreboard, and a player. it also has a couple of non-visible items, like a Building Generator and a horizon (screen width). The world has a lot of dependencies:

  • BuildingGenerator
  • Background
  • Player
  • Building
  • Scoreboard

The world also controls the speed of the game and the distance travelled, which is how scoring is calculated. The tick method is each “move” of the world. As the player moves right (or, more appropriately, the buildings move left) the tick method checks to make sure that a building is still under the player, otherwise it’s time to RENDER BLOOD and call PLAYER.DIE (Sorry, just feels like I should capitalize those) If you aren’t dead, it adds one to ticks

Screen

class Screen
  OFFSET = -20
  def initialize width, height, world
    @width = width
    @height = height
    @world = world
    @background = world.background
    create_frame_buffer
    %x{stty -icanon -echo}
    print "33[0m" # reset
    print "33[2J" # clear screen
    print "x1B[?25l" # disable cursor
  end
  attr_reader :width, :height, :world
  def create_frame_buffer
    @fb = Framebuffer.new @background
  end
  def draw renderable
    renderable.each_pixel(world.ticks) do |x, y, pixel|
      @fb.set x, y, pixel
    end
  end
  def render start_time
    print "e[H"
    buffer = ''
    previous_pixel = nil
    (0...height).each do |y|
      (OFFSET...(width + OFFSET)).each do |x|
        pixel = @fb.get(x, y)
        if Pixel === previous_pixel && Pixel === pixel && pixel.color_equal?(previous_pixel)
          buffer << pixel.char
        else
          buffer << pixel.to_s
        end
        previous_pixel = pixel
      end
      buffer << "n"
    end
    print "33[0m"

    dt = Time.new.to_f - start_time;
    target_time = 0.04
    sleep target_time - dt if dt < target_time

    print buffer
    create_frame_buffer
  end
  def on_exit
    print "33[0m" # reset colours
    print "x1B[?25h" # re-enable cursor
    print "n"
  end
end
view raw screen.rb This Gist is brought to you using Simple Gist Embed.

Screen is initialized with the width and height of our game “canvas”, along with the game world. If you look at the initialize method, it does some funky stty magic to clear the screen and disable the cursor. It’s things like this that make me love reading other people’s code. I always see and learn things that I would miss in my everyday coding routine. The Screen is responsible for rendering the initial game “canvas” and drawing Renderables onto the canvas. Renderable are classes that include the Renderable module, which we will get to soon.

Pixel

class Pixel
  def initialize char = " ", fg = nil, bg = nil
    @char = char
    @fg, @bg = fg, bg
  end
  attr_reader :char
  def fg; @fg || 255 end
  def bg; @bg || 0 end
  def to_s
    "33[48;5;%dm33[38;5;%dm%s" % [ bg, fg, @char ]
  end
  def color_equal? other
    fg == other.fg && bg == other.bg
  end
end
view raw pixel.rb This Gist is brought to you using Simple Gist Embed.

The Pixel class represents a game pixel, which is an ASCII character in ROLFBALT. A Pixel instance consists of a foreground color, a background color, and a character. The cool thing I learned from the Pixel class is how to mess with my terminal colors. The to_s method shows this, using a fancy string interpolation method (http://apidock.com/ruby/String/%25). I was switching my terminal from black-on-white to white-on-black and giggling when my wife walked by and asked what I was giggling about. All I could say was “um, nerd stuff” and realize that it’s situations like this that separate us from the “normals.” Anyway, I digress… back to the code…

Background

class Background
  PALETTE = [ 16, 232, 233 ]
  PERIOD = 16.0
  SPEED = 0.5
  BLOCKINESS = 10.0
  def initialize world
    @world = world
  end
  def pixel x, y, char = " "
    Pixel.new char, 0, color(x, y)
  end
  def color x, y
    y = (y / BLOCKINESS).round * BLOCKINESS
    sin = Math.sin((x + @world.distance.to_f * SPEED) / PERIOD + y / PERIOD)
    PALETTE[(0.9 * sin + 0.9).round]
  end
end

Background takes a world as a dependency and is mainly responsible for returning the color behind everything else. For example, when the player is rendered, it passes backround.color() as the background color to the players Pixels. Nice little, focused class. I like it.

WindowColor

class WindowColor
  PALETTE = [ 16, 60 ]
  PERIOD = 6.0
  def pixel x, y, char = " "
    Pixel.new char, 0, color(x, y)
  end
  def color x, y
    sin = Math.sin(x / PERIOD + y / (PERIOD * 0.5))
    PALETTE[(0.256 * sin + 0.256).round]
  end
end

The color of the windows on the buildings. BuildingGenerator uses it. That is all

Framebuffer

class Framebuffer
  def initialize background
    @pixels = Hash.new { |h, k| h[k] = {} }
    @background = background
  end
  def set x, y, pixel
    @pixels[x][y] = pixel
  end
  def get x, y
    @pixels[x][y] || @background.pixel(x, y)
  end
  def size
    @pixels.values.reduce(0) { |a, v| a + v.size }
  end
end

Framebuffer is instantiated by the Screen class and takes a background. The initialize method reminded me of the cool way to pass a block to the Hash intializer to get a hash that has a default value for any member that is accessed. Rendered pixels are fed into the framebuffer, and then other things (like the screen) can grab them later. It also uses Background, passing back background pixels as a default when the Framebuffer does not have a pixel for a requested x,y.

BuildingGenerator

class BuildingGenerator
  def initialize world, background
    @world = world
    @background = background
  end
  def destroy_if_necessary
    while @world.buildings.any? && @world.buildings.first.x < -100
      @world.buildings.shift
    end
  end
  def generate_if_necessary
    while (b = @world.buildings.last).x < @world.horizon
      @world.buildings << build(
        b.right_x + minimium_gap + rand(24),
        next_y(b),
        rand(40) + 40
      )
    end
  end
  def minimium_gap; 16 end
  def maximum_height_delta; 10 end
  def minimum_height_clearance; 12; end
  def next_y previous_building
    p = previous_building
    delta = 0
    while delta.abs <= 1
      delta = maximum_height_delta * -1 + rand(2 * maximum_height_delta + 1)
    end
    [25, [previous_building.y - delta, minimum_height_clearance].max].min
  end
  def build x, y, width
    Building.new x, y, width, @background
  end
end

BuildingGenerator takes a World and a Background and, as you may have guessed, it creates the buildings. The methods here focus on destroying building objects (by taking them out of the world.buildings property) and creating builidings as the world turns moves. The generate_if_necessary method is a loop that keeps creating builidings while the last building’s x coord is less than the worlds Horizon (which is the width of the screen, if you remember.) It calls the build method which builds the building. The next_y function takes the previous building into account and makes sure the new building isn’t too high for our player to jump. This is the kind of domain problem you have to solve when creating a video game. :)

Renderable

module Renderable
  def each_pixel ticks
    (y...(y + height)).each do |y|
      (x...(x + width)).each do |x|
        rx = x - self.x
        ry = y - self.y
        yield x, y, pixel(x, y, rx, ry, ticks)
      end
    end
  end
  def right_x; x + width end
end

This brings us to the Renderable module, which is included in:

  • Building
  • Player
  • Blood (hee)
  • Scoreboard
  • GameOverBanner
  • RoflCopter

It defines two methods: each_pixel and right_x. Also, it expects the included class to define a pixel method, along with x, y, height, and width properties (or methods.) In a nutshell, the each_pixel method loops over every pixel for the object, and returns the x, y of that pixel, along with the character that represents that pixel for the given Renderable. In a way, it works just like your TV, scanning through the images and rendering them, pixel by pixel. I like how Renderable delegates the pixel method, making a clear contract with things that include Renderable as well as things that use Renderables.

Building

class Building
  include Renderable
  def initialize x, y, width, background
    @x, @y = x, y
    @width = width
    @background = background
    @period = rand(4) + 6
    @window_width = @period - rand(2) - 1
    @color = (235..238).to_a.shuffle.first # Ruby 1.8
    @top_color = @color + 4
    @left_color = @color + 2
  end
  attr_reader :x, :y, :width
  def move_left distance
    @x -= distance
  end
  def height; SCREEN_HEIGHT - @y end
  def pixel x, y, rx, ry, ticks
    if ry == 0
      if rx == width - 1
        Pixel.new " "
      else
        Pixel.new "=", 234, @top_color
      end
    elsif rx == 0 || rx == 1
      Pixel.new ":", @left_color + 1, @left_color
    elsif rx == 2
      Pixel.new ":", 236, 236
    elsif rx == width - 1
      Pixel.new ":", 236, 236
    else
      if rx % @period >= @period - @window_width && ry % 5 >= 2
        Pixel.new(" ", 255, @background.color(rx + x/2, ry))
      else
        Pixel.new(":", 235, @color)
      end
    end
  end
end

Looking at our first Renderable, Building, it takes an x,y, which is the left most x and the highest y, a width, and a background object. Moving it left is as simple as decrementing its x property. The complexity happens in the pixel method expected by Renderable. The goal is to figure out, for the given x/y, what character and color to render. Looking at the start of the if statement, if the distance between the y of the current pixel and the y of the building is zero, then we are at the top of the building, so draw a “=”. The rest of the logic is the same, figuring out which character and color to return. Pretty smart.

Player

class Player
  include Renderable
  def initialize y, background
    @y = y
    @background = background
    @velocity = 1
    @walking = false
  end
  def x; 0; end
  def width; 3 end
  def height; 3 end
  def pixel x, y, rx, ry, ticks
    Pixel.new char(rx, ry, ticks), 255, @background.color(x, y)
  end

  def char rx, ry, ticks
    if dead?
      [
        ' @ ',
        '+/',
        ' ',
      ][ry][rx]
    elsif !@walking
      [
        ' O/',
        '/| ',
        '/ >',
      ][ry][rx]
    else
      [
        [
          ' O ',
          '/|v',
          '/ >',
        ],
        [
          ' 0 ',
          ',|',
          ' >',
        ],
      ][ticks / 4 % 2][ry][rx]
    end
  end
  def acceleration
    if @dead
      0.05
    else
      0.35
    end
  end
  def tick
    @y += @velocity
    @velocity += acceleration
    @walking = false
  end
  def y; @y.round end
  def bottom_y; y + height end
  def walk_on_building b
    @y = b.y - height
    @velocity = 0
    @walking = true
  end
  def jump
    jump! if @walking
  end
  def jump!
    @velocity = -2.5
  end
  def die!
    @dead = true
    @velocity = 0
  end
  def dead?
    @dead
  end
end


Player is another renderable, but it handles what character to draw differently than building. Looking at the code, it uses the delta in between the current x,y and the player x,y to figure out which character to return based on one of three states: dead, walking, or other (jumping). Each state is made up of a two-dimensional array that holds the actual characters, which you can literally see. The code here is very clever in its structure, forming the player in the state, then using the rx/ry indices to grab the character. Pretty ingenious.

Blood

class Blood < Struct.new(:x, :y)
  include Renderable
  def height; 4 end
  def width; 2 end
  def x; super + 2 end
  def pixel x, y, rx, ry, ticks
    Pixel.new ":", 124, 52
  end
end

The Blood class always uses the same character and color, but I had to give it its own section. I just LOVE that there is a Blood class.

Scoreboard and GameOverBanner

class Scoreboard
  include Renderable
  def initialize world
    @world = world
  end
  def height; 3 end
  def width; 20 end
  def x; -18 end
  def y; 1 end
  def template
    [
      ' ',
      ' Score: %9s ' % [ @world.distance],
      ' '
    ]
  end
  def pixel x, y, rx, ry, ticks
    Pixel.new template[ry][rx], 244, 234
  end
end

class GameOverBanner
  FG = 16
  BG = 244
  include Renderable
  def x; 28 end
  def y; 14 end
  def width; 28 end
  def height; 3 end
  def template
    [
      ' ',
      ' YOU DIED. LOL. ',
      ' ',
    ]
  end
  def pixel x, y, rx, ry, ticks
    Pixel.new template[ry][rx], FG, BG
  end
end


Both of these renderables use a template method to find the character to render. Again, the structure of the code is awesome, as it reads like it looks in the game. This makes it easy to figure out what the code is doing. If only you could make all code this visually obvious.

RoflCoptor

class RoflCopter
  include Renderable
  def initialize x, y, background
    @x, @y = x, y
    @background = background
    @frames = [
      [
        ' :LoL:ROFL:ROFL',
        ' L ____|__ ',
        ' O ===` [] ',
        ' L ________] ',
        ' .__|____|__/ ',
      ],
      [
        ' ROFL:ROFL:LoL: ',
        ' ____|__ ',
        ' LOL===` [] ',
        ' ________] ',
        ' .__|____|__/ ',
      ],
    ]
  end
  def width; 24 end
  def height; 5 end
  def y
    range = 1.5
    @y + (range * Math.sin(Time.new.to_f * 1.5)).round
  end
  def x
    range = 20
    @x + (range * Math.sin(Time.new.to_f * 0.7)).round
  end
  def pixel x, y, rx, ry, ticks
    Pixel.new char(rx, ry, ticks), 246, @background.color(x, y)
  end
  def char rx, ry, ticks
    @frames[ticks % 2][ry][rx] || " "
  rescue
    " " # Roflcopter crashes from time to time..
  end
end

The coup de grace, our ROFLCopter. The approach here is, again, similar to the other template-based renderables, but here we have multiple templates or frames. The x and y of the coptor change based on current time, using a formula I don’t really understand. I think my favorite bit is the rescue, where it claims that “RoflCopter” crashes from time to time.

Time to Play

Well, Paul and Dennis pulled it off. A port of Canabalt to Ruby that is highly playable, as well as being comprised of interesting code. When I asked the gents for any anecdotes about it, they came back with “Ask the readers to make it work in Ruby 1.8 as a challenge.” So, anyone up for the that? In the meantime, I am going to try and beat my record in ROFLBALT.