views:

87

answers:

1

I'm planning an achievement system for an ASP.NET MVC site. I want the achievement "rules" be stored in a database - to be easier to add new achievements and a central place to manage existing rules. Users will have no access to change the rules.

When a user performs an action that could potentially earn an achievement, the db rules will be queried and if there are matches, give them the achievements (stored in a lookup table, (userId, achievementId, dateAwarded).

At the moment I'm planning to put the "triggers" on certain actions in the controller, but the code that does the work will be in the model.

Is there standard DB schema for an achievement system that accomplishes this? No need to reinvent the wheel if not necessary. If not, what sorts of issues do you think would pop up, what to look out for?

+3  A: 

You may find this answer helpful.

Speaking from experience, building a database-based rules engine for reacting to user actions is a very time-consuming, error-prone, painful exercise.

Instead, you can write any number of individual classes, each one responsible for knowing how to award one particular achievement. Use a base class or interface to give them all a common contract:

public abstract class AchievementAwarder
{
    //override to provide specific badge logic
    public abstract void Award();
}

You can foreach over each AchievementAwarder and call Award() on a recurring schedule. For example, you could have a "100 visits" achievement:

public class 100VisitsAwarder : AchievementAwarder
{
    public override void Award()
    {
        //get a visit count from the db for all users
        //award achievements
    }
}

This solves two problems:

  1. It's orders of magnitude simpler, but much more flexible, than a database-based rules engine. As you can see, the individual units are extremely small and easy to change without affecting the larger system.

  2. It can run asynchronously, so for achievements that require some heavy lifting to determine if they should be awarded, the user's normal activities are not impacted by the achievements engine.

Rex M
@Rex M - That sounds great! For the example you gave it would be a cron job type of look up. How would you recommend hooking into the actions users take? For example, they post new "thing" and it's the 100th, so give them an achievement. Also assume there could be any number of achievements for posting a new "thing".
Chad
@Chad sorry for the late reply - to answer your question, as you said this works best as a cron job or, in the other answer I linked to, a self-inserting cache object initialized at app startup. Your best bet all around, for simplicity and performance' sake, is to avoid connecting it directly to user actions at all where possible. in many cases it's much better to live with a few minutes delay between the action that pushed you over the edge and the award being given.
Rex M
@ Rex M - thanks... how does Foursquare do it? I don't use it, but when a user unlocks a badge, do they get it immediately? Or is it awarded later when the cron job rolls around?I definitely like the idea of keeping the site performant! Just got the home page to load in .48 sec. :D
Chad
@Chad Not sure about Foursquare, I use Gowalla. Gowalla does indeed award badges almost right away, but Gowalla also deals with a *much* smaller data set - my entire visit history only has a few hundred items in it, each with a few attributes - something that can be computed locally very quickly. In a multi-user web application, you may have to deal with much larger data sets.
Rex M
@Rex M - Hmmm, I believe my data set would be even smaller than Gowalla's... here's my site: http://www.apoads.com (choose Yokota for the base, the others aren't busy yet). It tracks what users do and gives them rep for each action. I want to add a new layer of "rewards", the achievements. Debating on the "on cron job" or "instant gratification" scenarios... hmm.
Chad
@Chad if you can keep the entire dataset you need to do the operations in memory and it's less than several thousand objects total then it might make sense. Multiply the number of pieces of information you need to calculate each achievement by the number of users and that's your set.
Rex M
@Rex M - Since my app is asp.net mvc 2 based, what is your opinion on using the async controller to fire up awarding achievements? This would make them appear more quickly for the user - not in real-time, but would also not have any blocking issues.
Chad
@Chad that could work. Without knowing the details of your application architecture or the kind of data you're working with, I can't say for sure though.
Rex M
@Rex M - I just thought of something, about the 100 visits award example. Let's say there was a visit field on the db for the user. What would the logic look like for the awarder here? It wouldn't work just checking if (visits == 100) : give award. Do you do a range? if > 100 but < 200? Then do you have to run a check to see if the user already got the 100 visit award? Am I completely on the wrong path here?
Chad
@Chad in this case it would likely want to filter where visits is greater than 100 and the badge hasn't already been awarded. That makes it more accurate and consistent with other awards which can be earned multiple times - the only difference is the presence or absence of this extra check. Doing it that way does make a case for running these as recurring jobs instead of unpredictable triggers based on user activity.
Rex M
@Rex M - I agree, recurring jobs is the way to go, I appreciate your help. One last question. I've never written a foreach over a class like you mentioned (you said "foreach over each AchievementAwarder and call Award()"). Let's say I had a bunch of achievement classes based off the AchievementAwarder. Do I pile them all into a collection then iterate over it? or is there a better way? Thank you so much for your help, I truly appreciate it!
Chad
@Chad see http://stackoverflow.com/questions/3162446/ - each type is responsible for queuing itself for recurring runs, so in something like Application_Start you can just call `new Awarder1(); new Awarder2(); new Awarder3()` and add more as you build them.
Rex M
@Rex M - I think I'll just throw it into a method that gets called by a scheduled task, but the concept is the same. Thanks a ton!
Chad