A Simple Combat System (Ruby on Rails)

Introduction

This entry is based on a Building Browsergames blog entry: A Simple Combat System (PHP)

It pays to at least skim over the original entry before going over the Ruby on Rails version below.

Monsters and Hitpoints and Forest, Oh My!

The PHP version of this entry starts off by creating a way to store monsters within the game and adding some new stats (maxhp and currenthp). First off, we’ll use the generator to create the new model we want:

> ruby script/generate model Monster name:string attack:integer defense:integer
magic:integer gold:integer max_hp:integer cur_hp:integer

All by itself the generator did a fine job with the creation of the migration for Monster. We provided enough information about the fields to put in each one when we called the generator from the command line. But the migration to add the new stats to the user and add example monsters isn’t just adding a table with some fields, it’s performing a whole set of operations so we’ll edit the migration file to add some more to it:

class CreateMonsters < ActiveRecord::Migration
  def self.up
    create_table :monsters do |t|
      t.string :name
      t.integer :attack
      t.integer :defense
      t.integer :magic
      t.integer :gold
      t.integer :max_hp
      t.integer :cur_hp
 
      t.timestamps
    end
 
    # Add the maximum hitpoints and current hitpoints stats to the user.
    # Note: Setting the defaults here will set default values for any existing
    # users already in the database as well as for any new ones created. So we
    # won't have to update the before_create method in the User model to set 
    # defaults for these values.
    add_column :users, :max_hp, :integer, :default => 10
    add_column :users, :cur_hp, :integer, :default => 10
 
    # Create some example monsters.
    Monster.create(:name => 'Crazy Eric', :attack => 2, :defense => 2, 
      :max_hp => 8, :cur_hp => 8, :gold => 5)
    Monster.create(:name => 'Lazy Russell', :attack => 1, :defense => 0, 
      :max_hp => 4, :cur_hp => 4, :gold => 20)
    Monster.create(:name => 'Hard Hitting Louis', :attack => 4, :defense => 3, 
      :max_hp => 10, :cur_hp => 10, :gold => 5)
  end
 
  def self.down
    remove_column :users, :cur_hp
    remove_column :users, :max_hp
 
    drop_table :monsters
  end
end

This is one of our best examples of migration so far because you can see new tables being created, existing ones being revised, and additional code that exists just to give us a starting point in our application. Next we’ll add the two new stats to the welcome page so they are visible when a user is logged in.

<h1>Welcome To The Game</h1>
 
<% if logged_in? %>
  <h2><%= @current_user.login %></h2>
 
  <ul>
    <li>Attack: <strong><%= @current_user.attack %></strong></li>
    <li>Defense: <strong><%= @current_user.defense %></strong></li>
    <li>Magic: <strong><%= @current_user.magic %></strong></li>
    <li>Gold in hand: <strong><%= @current_user.gold %></strong></li>
    <li>Current HP: <strong><%= @current_user.cur_hp %>/<%= @current_user.max_hp %></strong></li>
  </ul>
 
  <%= link_to "The Forest", :controller => "forest" %>
<% end %>

Even though I haven’t created the controller to handle the forest yet, I’ve gone ahead and added a link to it above.

The Forest

> ruby script/generate controller Forest index

Now it’s time to fill in the code for the forest so we can actually encounter something there. First we’ll take a crack at the view in app/views/forest/index.html.erb:

<h1>The Forest</h1>
 
<p>You've encountered a <%= @monster.name %>!</p>
 
<%= link_to "Attack", :controller => "forest", :action => "attack" %>
<%= link_to "Run Away", :controller => "forest", :action => "run_away" %>

One thing you might note in the upcoming code is that I’ve added a “before_filter” to the code. That’s a callback that Rails provides you in your controller, much like ActiveRecord had callbacks for certain points within the lifetime of a record, a controller has callbacks that can get called before and after each action. That makes it easy to have something called before each and every action within this controller. In this case we just tell it to run the login_required function which restful_authentication provided. It will check to see if the user is logged in before any action on the page and if not, redirect to the root page for the application.

class ForestController < ApplicationController
  # We don't want anyone other than logged in users going to the forest.
  before_filter :login_required
 
  def index
    current_user
 
    @monster = Monster.random_encounter
  end
end

In addition we have to add the random_encounter method to the Monster model so we can call it to get a random monster. Note again that I’ve put game logic down in the model and kept the controller clean of all database interaction and logic. It’s just glue between the views which display things/take input and the models which handle all the data access and game logic.

def self.random_encounter
  # For a single random record I'm using the technique from here:
  # http://robzon.aenima.pl/2007/12/selecting-random-row-from-table.html
 
  random_monster = self.find(:first, :offset => (self.count * rand).to_i)
 
  # We'll set the number of hitpoints for the monster to the max before unleashing
  # the freshly retrieved creature out on the world. Note, this is just a copy
  # of the database record. There could be dozens of these in the system all at
  # the same time. As long as none of them is saved back to the database each one
  # can be changed independently (e.g. taking damage), because you're only changing
  # the in-memory copy.
  random_monster.cur_hp = random_monster.max_hp
 
  random_monster
end

At this point we’ve added some new stuff to our app and we can try it out. Don’t forget, if you haven’t already run the migration we created earlier to add the monsters table to the database then we need to run it now before we hit the pages that require the new data and fields:

> rake db:migrate

Here’s what our welcome page looks like now when the user is logged in:

And here’s the new forest page we see after hitting the link to go there:

One thing worth trying if you want to see the restful_authentication system in action is logging out of the game and then trying to go directly to the http://localhost:3000/forest page. When you do you’ll immediately find yourself redirected to the home page for the application thanks to the before_filter we added earlier.

Since the links for user actions (i.e. attack, run away) aren’t hooked up yet, clicking on them just gives you a Rails error. We’ll fix that by expanding the Forest controller to handle the user’s actions and by adding the ability to conduct combat to the User model:

class ForestController < ApplicationController
  # We don't want anyone other than logged in users going to the forest or executing
  # actions inside the forest.
  before_filter :login_required
 
  def index
    current_user
 
    # Get a random monster for us to encounter in the forest.
    @monster = Monster.random_encounter
 
    # Save the monster in our session.
    session[:monster_id] = @monster.id
  end
 
  def attack
    current_user
 
    # Pull the monster we were fighting from the session.
    @monster = Monster.find(session[:monster_id])
 
    # Fight the monster and get the transcript of the fight.
    @combat = @current_user.fight(@monster)
  end
 
  # Running away couldn't be much easier, just send the user back to the welcome page.
  def run_away
    session[:monster_id] = nil
 
    redirect_to :controller => "welcome"
  end
end

You might notice that in addition to adding the attack and run_away actions, I also changed the index action. It still asks for a random monster for the encounter in the forest, but now it saves that monster in the session. That way we aren’t sending the monster ID to the user with the page (the PHP code sends it as a hidden field) and then using it after we get it back. Remember, you can never send something to the user and implicitly trust what you get back. It could have been modified (e.g. the user might have swapped out the monster you picked for an easier or harder one). By storing the monster in the session we avoid that security hole. But remember how long your session lasts, if the session times out, is it OK to lose the information in it or is it something that needs to be saved to the database instead?

We wrote the fight function into the controller without really knowing how it works, which is a good thing because details of things like combat shouldn’t be up at the level of a controller. Instead it belongs down in the model layer. Specifically, I’ve put it into the User model:

def alive?
  self.cur_hp > 0
end
 
def fight(monster)
  combat = Array.new
 
  turn_number = 0
 
  # This is a small workaround for the fact that we aren't getting the user's name yet
  # when they signup. All we have for the user is the login.
  if (self.name.empty?)
    self.name = self.login
  end
 
  while (self.alive? and monster.alive?)
    turn_number += 1
 
    # Switch the roles of attacker and defender back and forth.
    if (turn_number % 2 != 0)
      attacker = monster
      defender = self
    else
      attacker = self
      defender = monster
    end
 
    if (attacker.attack > defender.defense)
      damage = attacker.attack - defender.defense
 
      # We allow damage to take you below zero hitpoints. Presumably the funeral is
      # closed casket in those cases.
      defender.cur_hp -= damage
 
      # We'll only make a turn record for those cases where something actually happens.
      turn = Hash.new
      turn["attacker"] = attacker.name
      turn["defender"] = defender.name
      turn["damage"] = damage
 
      # Save this turn in the combat transcript.
      combat << turn
    end
  end
 
  # Somebody is dead, let's see who and take appropriate action.
  if (self.alive?)
    # Yay, you lived. You get gold.
    self.gold += monster.gold
  else
    # There aren't any penalties for losing at the moment.
  end
 
  # We only save the user's record back to the database. The monster needs to
  # stay unchanged in the database no matter how much damage we did to it so
  # the next one we make will still be pristine.
  self.save
 
  # Return the results of combat.
  combat
end

We’re done with the User model, but we need to tweak the Monster model one more time to add a function to determine if the monster is alive too:

def alive?
  self.cur_hp > 0
end

Finally we have to have a page that displays the results of combat. Rather than wedging it into the index page for the forest, I felt it was easier to just have another page for it (app/views/forest/attack.html.erb):

<h2>Combat Results</h2>
 
<p>
<% @combat.each do |t| %>
  <%= "#{t['attacker']} hit #{t['defender']} for #{t['damage']} damage." %><br/>
<% end %>
</p>
 
<% if @current_user.alive? %>
<p>You killed <strong><%= @monster.name %></strong>! You gained <strong><%= @monster.gold %></strong> gold.</p>
 
<%= link_to "Explore Again", :controller => "forest" %>
<%= link_to "Home", :controller => "welcome" %>
<% else %>
<p>You were killed by <strong><%= @monster.name %></strong>.</p>
<% end %>

Now we can go ahead and click the “Attack” and “Run Away” links on the forest page to see what happens. Here’s an example of clicking the Attack link:

Extra Credit

  1. Download it. If you’ve only been following along so far by just reading the text about how to build all the pieces, it’s time for you to go get the code from the Subversion repository on Google and download it so you can see what I’ve done. The thing I think you should notice in particular is just how few files we are talking about and how little is in each file. Here’s the contents of the app directory (where almost all of you code goes):

    See the recent post Now On Google Code! for instructions on downloading the code.

  2. I talked about “sessions” in the code above but I didn’t really go into what they are. For a little more info about sessions, read something like Understanding Sessions or for a more in depth look at what sessions are in general Session @ Wikipedia.

Wish there was more?

I'm considering writing an ebook - click here.

.

John Munsch is a professional software developer with over 20 years experience. He created a series of game development sites (XPlus and DevGames.com) on his own before co-founding GameDev.net in 1999. The blog for his PBBG work is located at MadGamesLab.com.

Tuesday, September 16th, 2008 buildingbrowsergames, code, rubyonrails
blog comments powered by Disqus

About

Building Browsergames is a blog about browsergames(also known as PBBG's). It's geared towards the beginner to intermediate developer who has an interest in building their own browsergame.

Sponsors

Got Something to Say?

Send an e-mail to luke@buildingbrowsergames.com, or get in touch through Twitter at http://twitter.com/bbrowsergames