Using Metaprogramming for a RPG Battle Engine

written by Evil Trout on June 7th, 2008 @ 05:24 PM

Ever wonder what happens behind the scenes when you click on an action on your toolbar in Forumwarz?

The action might perform an attack. It might heal you. It might even do both! Some attacks are stronger when you're weaker, others spawn buffs that protect (or harm you) for multiple turns.

How does the game know what to do?

Putting Effects in the Database

When I was first prototyping Forumwarz, I stored the information for each action as rows in a database table. An Action would have multiple Effects.

An Effect would have a target, an attribute and a value.

Here's a (simplied) example of an attack:

Target: Enemy, Attribute: Pwnage, Value: 6
Target: Player, Attribute: Douchebaggery, Value: 1

This worked pretty well at first. However, I quickly found myself wanting to put in exceptions to the structure. What if you only want an effect to apply when a certain condition is met?

For example, Life After Death brings you back to life if you die in the next few turns after you've used it.

You could add a new column for that, like "only_on_death" as a boolean, but that's kind of lame. Are you going to add a new column every time an action does anything interesting?

And what if you want your attack to do some cool stuff in the future? Let's say it's stronger against certain things, weaker against others.

Scripting Attacks

Forumwarz is coded in Ruby, so I decided to make use of its scripting capabilities.

I came up with a framework where each action in the game had its own class. The class would be instantiated with a reference to the player performing it, and the enemy they were currently attacking. Then it would call an execute method which would do whatever the action was meant to do:

action = AsciiArtAttack.new(character, enemy)
action.execute

The implementation for AsciiArtAttack would look something like this:

class AsciiArtAttack < Action
  def execute
    @enemy.update_attribute :life, @enemy.life - 6
    @character.update_attribute :douchebaggery, @character.douchebaggery - 1
  end
end

The beauty of this approach is you're free to use any Ruby code you want in the execute method. You can check any conditions you want, you can calculate anything you want, really, the sky is the limit!

It's also faster to execute, since it doesn't involve querying the database each time you perform an action. It just runs through the Ruby code!

One obvious disadvantage is that you can't change the balance of the game without access to its Ruby code. Another is that you cannot "query" your attacks without a parser that can read Ruby code which is a non-trivial problem. So if you wanted, say, a list of all the attacks that effect Ego, your best tool is probably a text editor's search tool.

Metaprogramming

Although the above code is how the Forumwarz battle engine works under the hood, I rarely write the code that way. As it turns out, exceptions like "Only execute this action when you're dead" are quite exceptional. Most of the attacks in the game can be written as the simple list of their effects.

To make my life easier, and to avoid repeating myself, I have used the metaprogramming capabilities of Ruby to build the classes for the actions.

Instead of implementing an execute method, I use class methods to build it for me. This is similar to how putting in Validation class methods in ActiveRecord will write the validations for you.

Here's the actual code for the Ascii Art Attack that's running on Forumwarz right now:

class EventHandlers::Abilities::Troll::Ascii < EventHandlers::ActionBase
  decrease :enemy, :pwnage, :value => 7..9
  decrease :player, :douchebaggery, :value => 1
  inactivate_action :turns => 1
  minimum :douchebaggery, :value => 1
end

As you can see it does a little more than the above example did. It inactivates itself on your toolbar for 1 turn. It also enforces a minimum of 1 douchebaggery before it runs.

However, I think it's quite a bit more readable. You can just look at it and understand what it's doing.

This is how most of the actions in the game are written. If I need more flexibility, I just leave those class methods out and write an execute method instead. Pretty neat eh?

A secondary advantage is that the class methods can be implemented differently depending on what you're doing with them. Those same methods are used to generate the descriptions of an action, for example. Instead of calling "execute" I can call "describe" and get back the text for the tooltips you see in battle.

I won't go into how the metaprogramming works since it's outside the scope of this article (and also, other people can explain it much better than I can), but it's really worth looking into if you're coding something similar to this.

save to del.icio.us | digg this | submit to reddit

Flood Detection, Rails and Memcached.

written by Evil Trout on April 16th, 2008 @ 01:56 PM

Note: The article is about the code behind Forumwarz for those who are interested in such things. If people like it I might write more like it!

Flooding is what happens when a user submits data repeatedly to your server. A good example would be a user who repeatedly posts comments on your message board.

Sometimes it's done innocently: a user hits the submit button 5 times impatiently during a bit of server lag, only to find their post went through 5 times.

Other times it's nefarious and deliberate: a user creates a wget script to just post garbage over and over.

Not only can flooding be taxing on your server, but it can fill up your site with so much garbage that it will put other users off. Obviously it's something you want to get rid of!

A simple and effective strategy: the cool down period

You can assign a "cool down" period, where a user is barred from submitting data until the period ends. The length of the cool down is largely up to you, based on what you consider normal submission processes. I've found 30 seconds seems to work in most cases.

An obvious way to do this would be to add a timestamp column to the row you insert (which is always a good idea anyway!). Then, when posting, query the table you're about to insert into for anything from that user within that cooldown period. If any rows are found, don't allow the insert.

For most sites, implementing something like this will work great. However, and I cannot stress this enough, make sure you have a good database index on your timestamp column. If you do not, every insert will result in a table scan and your site performance will be terrible.

A memcached implementation

I got this idea from the Memcached FAQ and it works splendidly!

Memcached allows you to set an expiry for any key you set. So, instead of using the timestamp column in the database, you can simply set a key in memcached with an expiry of your cool down period. Then, when you are about to insert, check to see if the key exists in memcached. If it does, don't insert. If it doesn't, insert your row and then add the key there.

It is incredibly fast, and it doesn't matter if your tables are indexed on the date column or not. In fact, since it's not tied to your database at all, you can do flood prevention on anything you want (sending emails, real time chat, etc)!

Adding it to ActiveRecord

I have created a custom validation method in ActiveRecord for flood protection. It can be attached to any model using the following simple syntax:

prevent_flood 30.seconds, :user_id

The first parameter is the length of the cooldown period. The second parameter is the column in the model that uniquely identifies the user. In this case, it's a user_id column.

The implementation looks like this:

      def prevent_flood(cooldown, field)
        validates_each(field, :on => :create) do |record, attr_name, value|
          cache_key = "flood:#{record.class.name}_#{record[field]}"       

          unless CACHE_ME.get(cache_key).nil?
            record.errors.add_to_base("You're posting too often. Slow down!")
          end
        end
        
        after_create do |record|
          CACHE_ME.put("flood:#{record.class.name}_#{record[field]}", "F", cooldown.to_i)
        end
      end

CACHE_ME is an abstraction I wrote to use Memcached from ruby. It it initialized to connect to memcached when rails starts up, and can be replaced with however you personally connect to Memcached fairly easily. The get method returns nil if the key isn't there, and the put method sets a key to be the value "F" with an expiry of cooldown seconds. It doesn't really matter what value you put in the cache, I just chose F for flood, and because it's one character long.

I know personally flood protection is something that I never really implement until it becomes a problem, and one of the reasons was that it was a pain in the butt to code for every model. However, with this interface, I am now using it on all new code I'm writing from the beginning. The overhead is minimal, and it can really save your butt down the line!

save to del.icio.us | digg this | submit to reddit