Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement an achievement system in RoR

I'm attempting, poorly, to implement an achievement system into my Ruby on Rails application.

I have a long list of achievements I'd like to check for. All are triggered by some create action in various controllers.

I've had the idea that I'll have a achievement model, which includes the controller and action it responds to. Then do a before filter for the create and check for applicable achievements. I get stuck when it comes to actually defining/executing the achievements. Each achievement may require different data. For example one will want to know how many questions a user has answered, another how many comments they've made, and a third how many people the user invited have responded.

IS the best thing to do to actually just embed all the necessary ruby code straight into the DB? I could see doing a self contained block that does all the active record finding, etc and returns true/false, though there we are still some issues about knowing what is setup in advance (i.e. current_user, etc).

Any reasonable best practices out there that don't make me feel dirty? I could see a full on policy/rules engine being one path, though that may scare me more than plan a.

thanks! Oren

like image 717
teich Avatar asked May 19 '09 22:05

teich


2 Answers

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.

like image 68
molf Avatar answered Sep 30 '22 03:09

molf


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!

like image 26
Paul Campbell Avatar answered Sep 30 '22 05:09

Paul Campbell