ROFLBALT
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 = 120SCREEN_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 endendGame, 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 endendWorld 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 "