Since Set Theory, Ruby and Upgrades I've been learning a lot more ruby. I decided that in order to progress from the theoretical underpinnings that my previous reading had laid, I needed to actually do something productive and practical. I like gaming so I thought why not create a game. So that's pretty much what I did this weekend.

The key aspects of the game were that it obviously needed to be in ruby so I could practise the various features in the languages. I wanted to create an interactive game somewhat like my mazer prototype in Time shifting, CS algorithms and Game Architecture.

One thing I did not get around to doing in the original mazer prototype was actually creating real, solvable mazes. The mazes in Mazer weren't based on any maze algorithm like prim's (they could not be solved) - they were just randomly generated rooms that I then decided to furthermore remove arbitrary walls from to create an impression of a sort of broken maze that served as the game world. The initial mockup was created a while back in Mazer Game Design and Network Security.

It worked because the game didn't need to solve the maze, but this time I wanted to generate new mazes each time, that was solvable and then base the gameplay on a need to solve the maze.

I ended up implementing prim's algorithm to generate the maze dynamically and used the procedure to solve the maze. Both strategies (generating and solving) are implemented in the ruby code here

module Algorithms
class Prims

  # Visits room that don't have any neighbours yet and links them
  def self.on(maze, start_at)
    room_queue = []
    room_queue << start_at
    while room_queue.any?
      room = room_queue.sample
      available_neighbours = room.neighbours.select {|k,v| v.links.empty? }
      if available_neighbours.any?
        side = available_neighbours.keys.sample
        neighbour = available_neighbours[side]
        room.link_with(side)
        case side
          when :top
            neighbour.link_with(:bottom)
          when :bottom
            neighbour.link_with(:top)
          when :left
            neighbour.link_with(:right)
          when :right
            neighbour.link_with(:left)
        end

        room_queue << neighbour 
      else
        room_queue.delete(room)
      end
    end

    # print_links(maze)

    # return the maze
    maze
  end

  def self.print_links(maze)
    maze.each { |r| 
      puts "room #{r.number} has #{r.neighbours.size} neighbours and #{r.links.size} links configured" 
      puts "links"
      r.links.each {|l| puts l}
    }

  end
end

class Maze
  # Solves the maze by visiting every room from the starting point
  # to the ending point
  def self.solve(rooms, player_room_n, exit_room_n)
    queue = []
    queue << rooms[player_room_n]
    found = player_room_n == exit_room_n
    while queue.any? && !found
      room = queue.sample
      room.visit
      found = room.number == exit_room_n
      queue += room.links.map { |k,v| v } if !found
      queue.delete(room) 
    end 
    found    
  end
end

end

The principle of the algorithm is simple: visit a room that has not been visited before and when you get there, visit one of its neighbouring rooms randomly until you've run out of rooms. This also works for the solving the maze, only you go until you find the maze room you're looking for - in my case the room with the 'exit' in it. 

The next thing I wanted to do was implement collision detection (because I quite like physics) which I did using basic math to determine the intersection between two rectangles (all my characters were bounded by a rectangle as seen here).

There is no native class in ruby for a rectangle and I wanted to use my own calculations so I created a model of a rectangle that corresponds to the 4 points A-B would be the two top points of the rectangle and C-D would be the bottom lower points (going clockwise around).

require_relative 'point'
require_relative 'player' 

# The basis of collision detection in the game
# Also represents the dimensions of any rectangle using the below model A->B->D->C
class Rect

  #  A-----B
  #  |     |
  #  |     |
  #  |     |
  #  C-----D
  attr_accessor :x, :y, :w, :h, :a, :b, :c, :d

  def initialize(x, y, w, h)
    @x, @y, @w, @h = x, y, w, h
    set_points(@x, @y, @w, @h)
  end


  # Each point in the rectangle is represented by a Point class 
  def set_points(x, y, w,h)
    ax = x
    ay = y
    bx = ax + w
    by = ay
    dx = bx
    dy = by + h
    cx = ax
    cy = ay + h
   
    # Update or set the points for this rect - useful when updating for movement of points(moving characters) 
    if @a
      @a.set(ax,ay)
    else
      @a = Point.new(ax,ay)
    end

    if @b
      @b.set(bx,by)
    else
      @b = Point.new(bx,by)
    end
    if @d
      @d.set(dx,dy)
    else
      @d = Point.new(dx,dy)
    end
    if @c
      @c.set(cx,cy)
    else
      @c = Point.new(cx,cy)
    end
  end

  # Main collision detection routine - check if two rectangles intersect
  def collides_with_rect?(rect_b)
    ax1 = @a.x
    ax2 = @d.x
    ay1 = @a.y
    ay2 = @d.y
    
    b = rect_b

    bx1 = b.a.x
    bx2 = b.d.x
    by1 = b.a.y
    by2 = b.d.y

    collision =  ax1 < bx2 &&
      ax2 > bx1 &&
      ay1 < by2 && 
      ay2 > by1
    collision
  end

  def eql?(other)
    self == other
  end

end

So then a rectangle is made up of these 4 points (still is presumably!) which I further abstracted as 4 Point instances(proves access to simple cartesian coordinate for a point).

The long and short of it is that I needed a way to model the pictures that I scribbled down on the piece of paper next to me into concepts that I could control and model in the game.

This became crucial for collision detection of two rectangles. The overall result was a simple routine using these points to calculate an intersection as shown above(collides_with_rect? function): 

ax1 < bx2 && ax2 > bx1 && ay1 < by2 && ay2 > by1

I also modelled the maze in a similar way that I've done before where each wall of the room is an independent entity/side that can be collided with and independently removed - for example, if the player wants to destroy it(like in the original) or if the maze algorithm determines that two neighbouring cells should be linked - we remove the wall to form a passage.

Ultimately I didn't have enough time to implement the physics that would use it but decided to rather use the collision detection routine in another way: if you touched the walls(ie intersected) then your game points are reduced by the duration that you touch the wall. That's a game designer under time constraints for you...I wanted to be done by the end of the long weekend.

 

This is what it looks like.

The overall framework that controlled the game loop, much like MonoGame was a ruby gem called Gosu, which is a 2D library written in Ruby. It provided the hooks/methods for implementing the drawing and logic routines and manages the input control. This allowed me to focus on the gameplay. Other interesting aspects afforded by Gosu is support for png/jpg and sound assets in the form of Gosu::Image and Gosu::Music/Sound. (The game features background music and backdrop image)

Gosu also provides some drawing primitives to draw a rectangle using simple cartesian co-ordinates but surprisingly no way of modelling a rectangle as a class, such as SDL_Rect in SDL.

This is the entry point of the game and you can see how it all falls together, creating the maze, drawing the characters, handling input etc:

require './lib/player'
require './lib/utils'
require './lib/algorithms'
require './lib/room_builder'
require './lib/player_builder'
require './lib/audio_building'
require './lib/options'

class Game < Gosu::Window
  include RoomBuilding
  include PlayerBuilding
  include AudioBuilding
  extend Utils
  
  BACKGROUND = Utils.media_path('background.jpg') 
  @@points = 0
  @@level = 1
  
  # Initial game initialization and setup
  def initialize(width=800, height=600, options = { :fullscreen => false })
    super
    self.caption = 'Mazer Platformer in Ruby!'
    
    GameOptions::set_options(:show_solution => false)
    
    @background = Gosu::Image.new(BACKGROUND, {:tileable => false} )
    @success_sound = Gosu::Sample.new(Utils.media_path('complete.mp3'))
    @edge_sound = Gosu::Sample.new(Utils.media_path('buzzer.mp3'))
    @room_width = 50
    @room_height = 50
    
    set_cols_rows

    # -- Diagnostics -- 
    # @room_width = 100
    # @room_height = 100
    # @rows = 4
    # @cols = 4

    play_music
    create_level
  end

  def set_cols_rows
    @rows = height/@room_height
    @cols = width/@room_width
  end

  def create_level 
    # We only want to generate solvable mazes
    solved = false
    until solved do
      create_rooms(@rows, @cols, @room_width, @room_height)

      player_start_room_n = @rooms.sample.number
      exit_room_n = @rooms.sample.number

      @player = create_player :cube, @room_width, @room_height, @rows, @cols, player_start_room_n
      @exit = create_player :exit, @room_width, @room_height, @rows, @cols, exit_room_n
      
      # Generate a maze (randomly links rooms together i.e removes walls to make those links)
      Algorithms::Prims.on(@rooms, @rooms.sample)

      # Solve it to make sure its solvable, otherwise do it it again
      solved = Algorithms::Maze.solve(@rooms, player_start_room_n, exit_room_n)
    end
  end

  # Updates the game every frame
  def update

    # Get input from the player
    move_player(:up) if button_down?(Gosu::KbUp)
    move_player(:down) if button_down?(Gosu::KbDown)
    move_player(:left) if button_down?(Gosu::KbLeft)
    move_player(:right) if button_down?(Gosu::KbRight)

    # Update player and NPCs

    @player.update
    @exit.update

    # Check for player/room collisions 
    # reduce points on collision with walls
    @rooms.each { |room|
      if room.collides_with_rect?(@player.Rect)
        @@points -= 1
      end
    }

    # Check for win condition
    # spawn a new level if you've found the exit point
    if @player.Rect.collides_with_rect?(@exit.Rect)
      generate_next_level
    end

  end

  def generate_next_level
      @@level += 1
      @@points += 1000
      @success_sound.play
      @room_width -= 1 
      @room_height -= 1
      set_cols_rows
      create_level
  end


  # Draws the game every frame
  def draw
    @background.draw(0,0,0)
    
    @rooms.each  { |room| room.draw }
    @player.draw
    @exit.draw

    @hud = Gosu::Image.from_text(self, stats, Gosu::default_font_name, 30) 
    @hud.draw(10, 10, 0)
  end

  # Input Control
  def button_down(id)

    # Cheat - press 'r' to skip to next level
    if id == Gosu::KbR
      generate_next_level
    end

    options = GameOptions::get_options

    # Show/hide maze solution
    if id == Gosu::KbS
      show_solution = options[:show_solution]
      options.merge!({ :show_solution =>!show_solution })
      GameOptions::set_options(options)
    end 
    close if id == Gosu::KbEscape
  end

  def move_player(direction)
    case direction
    when :up 
      @player.move_up
    when :down
      @player.move_down
    when :left
      @player.move_left
    when :right
      @player.move_right
    end
  end

  # Called before update() if button is released
  def button_up(id)

  end

  # Determines if its needed to draw an new frame
  def needs_redraw?
    true
  end

  def stats
    "fps:#{Gosu.fps} Level: #{@@level} Points: #{@@points}"
    # "Level: #{@@level} Points: #{@@points}"
  end

end

puts "Gosu version=#{Gosu::VERSION}"
puts "License=#{Gosu::LICENSES}"
game = Game.new

# Enters the main game loop
game.show

I was able to animate the player sprite, however, ran out of time to get the logic in place to control the drawing of the appropriate frame for the player's direction (something like I here in this) so in the interest of time - I also cut this feature and instead implemented a non-animated player abstraction rendering simple coloured cubes (white for the player and red for the exit point). 

The premise of the game evolved such that you need to connect objects on either side of the maze, you can obviously move in order to get to the other object however you've got the maze to content with. If you touch the walls, it will zap points from you, and a clear path exists to the other exit point by traversing through the maze(you just need to find it). Once you capture the object, you get 1000 points and you then progress to the next maze which is incrementally smaller. As you level up, the need to pay attention to your surrounding so it's more difficult.

The player and the exit point are randomly chosen and then the maze algorithm will build a maze and then we'll try and solve it between the two random points. If we can't(not all mazes are solvable), then we re-create the maze and place the characters again until we come across a solvable maze and then that becomes the level.

If you don't believe that maze is solvable - I didn't in the beginning, you can press a cheat key and show the maze solution on-screen - this retraces the prior solving algorithm and shows all rooms that were inspected for the exit object until it was found. That's how I could be sure that the reason I couldn't solve the mazes was that I really couldn't solve the mazes(not that they were unsolvable!)

In theory, the game never ends - it just keeps generating smaller and smaller mazes. You quit when you want to - I didn't actually implement a die condition! So, in theory, you'd just get into negative points if you keep touching walls, infinity otherwise. A simple objective would be to go for as long as possible and record which level you ended up at and your total accumulated points, you could then compare it with other's results to see who's better!

I wanted to actually finish this game so I decided that this was it, no more features - get it playable, presentable and go to bed. There are of course other things I could add to it like new backgrounds and music on each level, animated characters as well as walls that you could not go through. The list continues...

I also learnt how RSpec works. I implemented some tests to ensure the correctness of my room generation. For example, I wanted to ensure that my rows were indexed at row 0 and my columns started at col 0. How do you test this behaviour...

 example 'every row reports consequtive columns' do
    for r in 0..ROWS-1 do
      for c in 0..COLS-1 do
        expect(subject[r*COLS+c].col).to eq(c)
      end
    end
  end

  example 'every row is consequative' do
    for r in 0..ROWS-1 do
      for c in 0..COLS-1 do
        expect(subject[r*COLS+c].row).to eq(r)
      end
    end
  end

I also decided to model my rooms as a grid of ROWSxCOLS. Did I use a 2-dimensional array like a normal person? No

In hindsight, it would have probably been easier, however working out the logic to represent a 2D array as a 1D array was fun and in actual fact, this is how 2D arrays are laid out in memory anyway. It's just the language that nicely provides accessors to get at rows and cols in the familiar array[row][col] notation.

I remember my lecturer going through that in my C programming course. Something must have stuck.