Implementing A Flexible Stats System (Ruby on Rails)

Introduction

Before you tackle this one, you probably need to take a quick peek at the blog entry where the design of this system is described: A Flexible Stats System

The contents of this tutorial is based on the follow-up to that entry, entitled: Implementing A Flexible Stats System (PHP)

The gist of the design is that there are “stats” and each one can be fairly generic. It only holds text so it could be a number or something else entirely and there is a many-to-many relationship between these stats and users. Each user may have many stats and each stat may appear on many users. In addition, each user’s instance of a particular stat (e.g. “Magic”) may have a different value from the same stat on a different user. Problem is, I can’t see a thing it buys us under Rails.

One of the stated goals was flexibility but the updating of existing records in tables can be done naturally in the Rails migration when we’re adding the new fields. Likewise, since we’re object oriented and using Model-View-Controller (MVC) we can handle setting the default values for users (and later monsters, items, etc.) as part of the object initialization when we create a new instance. So, first we diverged from the PHP version by using restful_authentication plugin, now we’re going to do it again by ignoring the stats table and the matrix table which tied it to the user.

Adding Stats Directly To Users

So, what’s the alternative? Since we already have a User model we will add our stats to that model:

> ruby script/generate migration AddStatsToUser attack:int defense:int magic:int

It’s worth looking at the migration generated by running this generation because we want to make a small adjustment to it:

class AddStatsToUser < ActiveRecord::Migration
  def self.up
    add_column :users, :attack, :int, :default => 10
    add_column :users, :defense, :int, :default => 9
    add_column :users, :magic, :int, :default => 8
  end
 
  def self.down
    remove_column :users, :magic
    remove_column :users, :defense
    remove_column :users, :attack
  end
end

If you look at your migration vs. the one above, you’ll notice that we’ve added “, :default => [some number]” to each of the fields we added. That is the default value and as it adds all the new fields it will set those as the default values. Add the same code onto the end of your three add_column lines and you’re done with the migration. Now we’ll run it to add the fields to the table:

> rake db:migrate

Let’s Play With What We Built

OK, this is where Rails starts to shine just a little bit brighter. That’s because it uses a built in ORM to handle database access. At this point we’re actually done and we can try out our code using the Rails console:

> ruby script/console
 
Loading development environment (Rails 2.1.0)
>> users = User.find(:all)
=> [#<User id: 1, login: "John", name: "", email: "john.munsch@gmail.com", 
crypted_password: "74c61eed256854719d00ce1ce35bc9f7bbc584c8", 
salt: "8a879696f0390696a5b6329d4a678378ab5bb878", 
created_at: "2008-08-17 19:22:33", updated_at: "2008-08-25 02:56:18", 
remember_token: "6522cde53ae8c60b7217ea5e40b1a75df285532b", 
remember_token_expires_at: "2008-09-07 19:11:43", 
attack: 10, defense: 9, magic: 8>]
>> y users[0]
--- !ruby/object:User 
attributes: 
  defense: "9"
  salt: 8a879696f0390696a5b6329d4a678378ab5bb878
  name: ""
  updated_at: 2008-08-25 02:56:18
  crypted_password: 74c61eed256854719d00ce1ce35bc9f7bbc584c8
  remember_token_expires_at: 2008-09-07 19:11:43
  id: "1"
  magic: "8"
  remember_token: 6522cde53ae8c60b7217ea5e40b1a75df285532b
  attack: "10"
  login: John
  created_at: 2008-08-17 19:22:33
  email: john.munsch@gmail.com
attributes_cache: {}
 
=> nil

We didn’t write the code in User.find(:all), ActiveRecord took care of that for us. We call it here and it returns a list of all the users we created using the registration system we added with restful_authentication. Any it brings back will have the new attack, defense, and magic stats on them and they will be set to the default values we set in the migration (if you don’t have any users, go to the index page and use the registration to create a couple). I used the function “y” to convert the object to YAML format and dump it to the screen to make it a little easier to read.

>> john = User.find(1)
=> #<User id: 1, login: "John", name: "", email: "john.munsch@gmail.com", 
crypted_password: "74c61eed256854719d00ce1ce35bc9f7bbc584c8", 
salt: "8a879696f0390696a5b6329d4a678378ab5bb878", 
created_at: "2008-08-17 19:22:33", updated_at: "2008-08-25 02:56:18", 
remember_token: "6522cde53ae8c60b7217ea5e40b1a75df285532b", 
remember_token_expires_at: "2008-09-07 19:11:43", 
attack: 10, defense: 9, magic: 8>
>> john = User.find_by_name("John")
=> nil
>> john = User.find_by_login("John")
=> #<User id: 1, login: "John", name: "", email: "john.munsch@gmail.com", 
crypted_password: "74c61eed256854719d00ce1ce35bc9f7bbc584c8", 
salt: "8a879696f0390696a5b6329d4a678378ab5bb878", 
created_at: "2008-08-17 19:22:33", updated_at: "2008-08-25 02:56:18", 
remember_token: "6522cde53ae8c60b7217ea5e40b1a75df285532b", 
remember_token_expires_at: "2008-09-07 19:11:43", 
attack: 10, defense: 9, magic: 8>

Just to demonstrate some more of what ActiveRecord provides I went ahead and found a user by user ID (the default way of finding any object in ActiveRecord is by ID), by name, and by login. That was easy. ActiveRecord supplies simple find_by_something functions automatically whenever you call one. In this case there were no users named “John” so it returned nothing. But when I looked for a login of “John” it found the right record. Let’s try out the getters and setters ActiveRecord is providing us for our newly minted stats.

>> john.attack
=> 10
>> john.attack = 11
=> 11
>> john.save
=> true
>> john.reload
=> #<User id: 1, login: "John", name: "", email: "john.munsch@gmail.com", 
crypted_password: "74c61eed256854719d00ce1ce35bc9f7bbc584c8", 
salt: "8a879696f0390696a5b6329d4a678378ab5bb878", 
created_at: "2008-08-17 19:22:33", updated_at: "2008-08-25 04:48:01", 
remember_token: "6522cde53ae8c60b7217ea5e40b1a75df285532b", 
remember_token_expires_at: "2008-09-07 19:11:43", 
attack: 11, defense: 9, magic: 8>

john.attack gets us the current value of that field, then we can assign a new value to it, and save it back to the database. The john.reload command isn’t really necessary in this case, it’s just a command to reload the object using it’s ID. The fact that the newly reloaded object has the updated attack value tells us that saving to the database worked.

Extra Credit

Don’t assume for a second that because I didn’t use a user_stats matrix table and a stats table linked together in the Rails code that it’s incapable of doing it. It’s not hard at all to do by creating the necessary tables in a migration and adding “has_many :through” and “belongs_to” to our models. Read a little bit about ActiveRecord’s ability to link models together through tables. Don’t worry if you don’t understand everything on that page yet. Just the idea that ActiveRecord can link models together to match how they are linked in your database tables and how that can make data access easier is enough for now.
Take a second to contemplate “flexibility” in your code. There’s a mantra that goes “You aren’t gonna need it” that I think you should take very seriously. Follow the link to learn in a very short paragraph or two why trying to anticipate your needs far ahead of time and code for those anticipated needs is a bad idea.