How to implement an achievement system in RoR
Solution 1:
I agree with your idea to use an Achievement
model.
You should probably not implement the triggers in your controllers, though. Imagine that you have two ways to post a comment; you will inevitably get code duplication. This sort of behaviour belongs in a model.
Suppose you want to track the number of comments that a user makes, and award an achievement for 100 comments. You could have the following models:
class User < ActiveRecord::Base
has_many :comments
has_many :achievements
def award(achievement)
achievements << achievement.new
end
def awarded?(achievement)
achievements.count(:conditions => { :type => achievement }) > 0
end
end
class Achievement < ActiveRecord::Base
belongs_to :user
end
class Comment < ActiveRecord::Base
belongs_to :user
end
class CommentAchievement < Achievement
def self.check_conditions_for(user)
# Check if achievement is already awarded before doing possibly expensive
# operations to see if the achievement conditions are met.
if !user.awarded?(self) and user.comments.size > 100
user.award(self)
end
end
end
The different achievements are all subclasses of Achievement
model, and use single table inheritance so that they are stored in just one table. The subclasses can contain all logic required for each individual achievement. You can also store additional information in this model, such as the date on which the achievement was awarded. To make sure that the database rejects duplicate achievements, you could create a UNIQUE
index on the type
and user_id
columns.
CommentAchievement.check_conditions_for(user)
can be called whenever you wish to. You may create a background job that runs every now and then, or you could create an observer:
# app/models/comment_achievement_observer.rb
class CommentAchievementObserver < ActiveRecord::Observer
observe :comment
def after_create(comment)
CommentAchievement.check_conditions_for(comment.user)
end
end
# config/environment.rb
config.active_record.observers = :comment_achievement_observer
The above is just one idea of how to do it, of course there may be others. The code is just an example, I haven't actually tested it. Hopefully it's of some help to you.
Solution 2:
Really nice solution, molf.
I rolled this in to a plugin / gem with generators for new achievements:
http://github.com/paulca/paths_of_glory
Happy achieving!
Solution 3:
I wrote a Rails 3 gem for this task, which works for badges, points and rankings. You can find the source code in https://github.com/tute/merit.