Modern Tools for Ruby/Rails for Building an Achievement System

Modern tools for Ruby/Rails for building an achievement system

An achievement system seems simple at first glance but can quickly become quite complex.

First, you have to identify what kind of achievements you want to provide. You can award :

  1. Badges
  2. Points
  3. Ranks

Of course, you'll also want to make various combinations of those.
Non-obvious but frequently asked features are :

  • the ability to know its progress towards a specific rank or badge.
  • the ability to hide some badges

In RoR world, I have found 3 third-party libraries freely available. As often, there's no magic bullet and you have to choose one according to your needs.

Badgeable

Badgeable is a simple DSL which implements only a badge system. It's dynamic and simple to understand. This example is from the official documentation :

badge "Fancy Pants" do
thing Meal
subject :person
count
Meal.where(:price_cents.gte => 10000).count >= 12
end
conditions do |meal|
meal.restaurant.city != meal.eater.city
end
end

It would award the Fancy Pants badge to the diner who has eaten 12 expensive meals where the awarding meal was out of town. It includes interesting features like unseen badge, but cannot award the same badge multiples times. By default, Badgeable add hooks after creation of the observed record. In the above example, badge condition is executed after each Meal creation.

It supports both ActiveRecord and Mongoid.

Paths of Glory

Paths of Glory is quite different of Badgeable. This gem is more towards Points and Ranks. It separates logic to compute badges (observer) from logic to describe badges (Achivement class). Maybe it would be more natural for you if you already use Observer pattern. Take note that it's pure Ruby, there's no DSL in Paths of Glory.

In the Achievement class, you describe your levels, what you count and how to award achievements :

level 1, :quota => 2
level 2, :quota => 4
level 3, :quota => 6

set_thing_to_check { |user| user.posts.count }

def self.award_achievements_for(user)
return unless user
return if user.has_achievement?(self)
user.award_achievement(self)
end

Observer part is very classic :

  observe :post

def after_save(post)
Teacher.award_achievements_for(post.user) unless post.new_record?
end

It's not well documented but you can find a sample application using it here.

It includes helpers in order to follow progress to the next rank. Since it uses classic rails features, it should be compatible with every rails ORM available.

Merit

Merit seems to be the more complete gem about this subject, ATM. It allows to define badges, points and rules with a DSL.

For badges, it looks like :

grant_on ['users#create', 'users#update'], :badge => 'autobiographer', :temporary => true do |user|
user.name.present? && user.address.present?
end

It will check on both creation and update if user has put its address. It will remove the badge if user removes its address.

For points, it's capable to count a score based on multiple models :

score 20, :on => [ 'comments#create', 'photos#create' ]

For ranks, it's quite similar to badges. Difference is mainly in the level :

set_rank :stars, :level => 2, :to => Commiter.active do |commiter|
commiter.branches > 1 && commiter.followers >= 10
end

set_rank :stars, :level => 3, :to => Commiter.active do |commiter|
commiter.branches > 2 && commiter.followers >= 20
end

This gem also provides means to compute badges or ranks in cron jobs and not after each writes on objects :

task :cron => :environment do
MeritRankRules.new.check_rank_rules
end

Under the hood, Merit uses Ambry to store badges information. It should help to both reduces it's noise on your datastore and make it a little faster.
There's an experimental support available for MongoMapper. I haven't found any means to have unseen badges or to follow progress towards a badge.

Achievement model

Assuming you are using the accepted answer way of doing this, then yes, you would have a user_id in your achievements table. You can tell this because in the Achievements model, it has:

belongs_to :user

If you were generating the achievements model through a rails generator, it would look like this:

rails g model Achievement user:references ........

Where ..... is the other fields in the achievement model.

Rails Badge / Achievements System Dynamic Master List

To iterate through the subclasses, you should be able to do something like this:

#Get the subclasses as class objects
Achievement.subclasses

#Get just the subclass names
Achievement.subclasses.map(&:name)

And then for the show URLs, probably make a route like 'achievements/:badge' and, in your controller, do

@badges = Achievement.where(:type => params[:badge]).all

#or, depending on how you've named everything
@badge = params[:badge].camelize.constantize.all

Rails Badge type plugin / tutorial?

You might also want to try the achievements gem: https://github.com/mrb/achievements

It's based on Redis, so you'll need to get that working first. Basically, you define a bunch of achievement contexts (pages viewed, messages sent, etc.) along with multiple levels if necessary. Then, you increment your value appropriately upon certain events, and you can then check if the achievement has been reached.

This link also has a relatively detailed explanation of the thinking behind a badge/achievement system: RoR Achievement System - Polymorphic Association & Design Issues

Implementation of achievement systems in modern, complex games

Achievement systems are really just a form of logging. For a system like this, publish/subscribe is a good approach. In this case, players publish information about themselves, and interested software components (that handle individual achievements) can subscribe. This allows you to watch public values with specialised logging code, without affecting any core game logic.

Take your 'player walked x miles' example. I would implement the distance walked as a field in the player object, since this is a simple value to increment and does not require increasing space over time. An achievement that rewards players that walk 10 miles is then a subscriber of that field. If there were many players then it would make sense to aggregate this value with one or more intermediate broker levels. For example, if 1 million players exist in the game, then you might aggregate the values with 1000 brokers, each responsible for tracking 1000 individual players. The achievement then subscribes to these brokers, rather than to all the players directly. Of course, the optimal hierarchy and number of subscribers is implementation-specific.

In the case of your fight example, players could publish details of their last fight in exactly the same way. An achievement that monitors jumping in fights would subscribe to this info, and check the number of jumps. Since no historical state is required, this does not grow with time either. Again, no core code need be modified; you only need to be able to access some values.

Note also that most rewards do not need to be instantaneous. This allows you some leeway in managing your traffic. In the previous example, you might not update the broker's published distance travelled until a player has walked a total of one more mile, or a day has passed since last update (incrementing internally until then). This is really just a form of caching; the exact parameters will depend on your problem.

Using OpenFeint to get current Achievement progression

you can do this by calling :

[OFAchievementService getPercentComplete:achievementId forUser:[OpenFeint lastLoggedInUserId]];

RenderPartialForEachT(..) HtmlHelper for asp.net mvc?

You need to declare the method in a class. It's not obvious that you are doing that, but it would certainly cause the type of error that you are seeing.

  public static class CustomHtmlHelperExtensions
{
public static void RenderPartialForEach<T>(
this HtmlHelper helper,
...
}

EDIT: In retrospect, given the text of the error, I suspect that the error lies elsewhere in your markup. Perhaps, you're missing a parenthesis around an if statement or foreach clause.

which were your achievements in programming in 2008?

  1. I wrote 2 VB.NET language features that will ship as part of VS 2010.

  2. I designed a programing language called Liberty,

    However, I've only implemented a small fraction of it. I stopped working on it so that I could concentrate on building a profitable software company. My original intent was to market the language (actually an IDE for it) as my first product, but the economics of programing languages being as they are, I decided to pick something else for my company's first product. I've been thinking about turning it into an open source project. If the statement "A programing language that feels like LISP, but looks like C#..." has any appeal to you, and you are interested in working on an open source .NET compiler, let me know.

  3. I started my own software company

  4. I've designed and implemented most of my company's first product "Transactor Code Agent", which should be shipping in Q1 2009. I've been billing it as a "Disaster Recovery Tool for Programmers".

    It's a tool that provides automatic local version history for source code. You point it at the folders that contain your source, and then anytime you make a change to file it automatically creates a backup for you. It's meant to be a compliment to existing source control setups, by protecting all the "broken", "in-progress" work that you usually don't check into source control.

    By the way, we are looking for beta-testers. If you are interested let me know.



Related Topics



Leave a reply



Submit