Search code examples
ruby-on-railsrubyachievements

Modern tools for Ruby/Rails for building an achievement system


I am interested in building a robust achievement system for my Ruby on Rails application.

The achievements can happen in a myriad of ways. Normally, we'd have some criteria that can happen to several models, some required, some optional (mix and match) that triggers an achievement.

I would say this is similar to the achievement system on here or Foursquare badges. Ideally if there is a modern tool out there that can help abstract a lot of the work in some DSL, that would be fantastic.

I'm interested in seeing how others solved this issue using Ruby or Ruby on Rails. Building this from scratch sounds unreasonable as I'm sure it's been solved many times repeatedly.


Solution

  • 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.