Site Design: How to award users tasks with achievements? - php

So I'm wanting to setup an achievements system on my site. People perform tasks and upload this information which is then stored in a database (think 'time', 'date', 'task', etc.). What would be the best method of checking their information and awarding achievements? Would I just have like an achievement.php that once information is uploaded it would trigger this document to run through all the checks to determine if the user needs to be awarded an achievement? Or is there something server side I should set up to award the user?
Thanks for any help or suggestions, comments, etc. :D
EDIT: I currently have the achievements listed in the database, (id, name, class)
Tasks are stored as ('date_time','time','device','user_id[fk]')
EDIT 2: Also many achievements will be calculated based on not only the tasks the user is currently submitting but takes into account previous tasks in addition to the newly added task. EX: If the user has completed 3 tasks within 3 consecutive days, then they will be awarded for it

Your best bet is probably to create a table of point values for the tasks, and then create a stored procedure that can fetch the appropriate counts from the appropriate tables and multiply them by the point values. That's what I've done in the past - it allows you modify point values on the fly from the DB as well.

it really depends on where your preference for business logic placement lies, and how real time you want acheivements to be. if you're looking to offload a bunch of business logic on you sql server, put it in a stored procedure, otherwise, class out the calculations into a class in php, and use that class to determine what new achievements have been.
i would definitely suggest doing the processing outside of the normal page response. perhaps kick off a server-side call to the php cli, or set up a cron job to run all individuals through a check for achievements at a certain interval.
edit:
as for the actual methods of awarding achievements, i would think you're most flexible and simple implementation (you will find more simple/less flexible and more flexible/less simple options i'm sure) would be to create an AwardRunner class, an IAward interface and a bunch of individual implementations of IAward for each award you have. the basic idea would be something like:
<?php
class AwardRunner {
var $UserId = 0;
function AwardRunner($userId) {
$this->UserId = $userId;
$dir = "/path/to/your/folder/full/of/IAwards/";
$includes = read_dir($dir);
//include all files that exist
foreach($includes as $include)
{
if (is_file($include))
{
require($include);
}
}
}
public function Run() {
$classList = get_declared_classes();
foreach($classList as $key => $className)
{
if (in_array('IAward', class_implements($className))) {
$award = $className();
$award->UserId = $this->UserId;
$award->GrantIfUserQualifies();
}
}
}
//function for reading all files in a directory.
//this is recursive, so any files in subfolders will also make it in
function read_dir($dir)
{
$array = array();
$d = dir($dir);
while (false !== ($entry = $d->read())) {
if($entry!='.' && $entry!='..') {
$entry = $dir.'/'.$entry;
if(is_dir($entry)) {
$array = array_merge($array, read_dir($entry));
} else {
$array[] = $entry;
}
}
}
$d->close();
return $array;
}
}
?>
i would think the idea of what the IAward interface would look like would be pretty clear from the usage, though you'd probably add to it the Id field from your table so it would be able to insert itself into the database, as would the way to call the AwardRunner class.
this idea should work whether you have something batching the awards process looping through all your users, or just fire it off after every task submission.

How about you create a trigger on the task submission proc (or however you insert the data when the user completes a task), that then performs the necessary actions for that user to determine if he/she is awarded an achievement, and then updates the achievements table accordingly.
Then, every-time you load up the information for the user on the front end, the data will already be in for him/her in the achievements table, and you can directly access it (which I'm sure you already do).

Related

DDD - how to deal with get-or-create logic in Application Layer?

I have an DailyReport Entity in my Domain Layer. There are some fields in this object:
reportId
userId
date
tasks - Collection of things that user did in given day;
mood - how does the user felt during the whole day;
Also, there are some methods in my Application Service:
DailyReportService::addTaskToDailyReport
DailyReportService::setUserMoodInDailyReport
The thing is that both of these methods require DailyReport to be created earlier or created during function execution. How to deal with this situation?
I have found 2 solutions:
1 Create new DailyReport object before method dispatching, and after that pass reportId to them:
//PHP, simplified
public function __invoke() {
$taskData = getTaskData();
/** #var $dailyReport DailyReport|null **/
$dailyReport = $dailyReportRepository->getOneByDateAndUser('1234-12-12', $user);
//there were no report created today, create new one
if($dailyReport === null) {
$dailyReport = new DailyReport('1234-12-12', $user);
$dailyReportRepository->store($dailyReport);
}
$result = $dailyReportService->addTaskToDailyReport($taskData, $dailyReport->reportId);
//[...]
}
This one requires to put a more business logic to my Controller which i want to avoid.
2: Verify in method that DailyReport exists, and create new one if needed:
//my controller method
public function __invoke() {
$taskData = getTaskData();
$result = $dailyReportService->addTaskToDailyReport($taskData, '1234-12-12', $user);
//[...]
}
//in my service:
public function addTaskToDailyReport($taskData, $date, $user) {
//Ensure that daily report for given day and user exists:
/** #var $dailyReport DailyReport|null **/
$dailyReport = $dailyReportRepository->getOneByDateAndUser();
//there were no report created today, create new one
if($dailyReport === null) {
$dailyReport = new DailyReport($date, $user);
$dailyReportRepository->store($dailyReport);
}
//perform rest of domain logic here
}
This one reduces complexity of my UI layer and does not expose business logic above the Application Layer.
Maybe these example is more CRUD-ish than DDD, but i wanted to expose one of my use-case in simpler way.
Which solution should be used when in these case? Is there any better way to handle get-or-create logic in DDD?
EDIT 2020-03-05 16:21:
a 3 example, this is what i am talking about in my first comment to Savvas Answer:
//a method that listens to new requests
public function onKernelRequest() {
//assume that user is logged in
$dailyReportService->ensureThereIsAUserReportForGivenDay(
$userObject,
$currentDateObject
);
}
// in my dailyReportService:
public function ensureThereIsAUserReportForGivenDay($user, $date) {
$report = getReportFromDB();
if($report === null) {
$report = createNewReport();
storeNewReport();
}
return $report;
}
//in my controllers
public function __invoke() {
$taskData = getTaskData();
//addTaskToDailyReport() only adds the data to summary, does not creates a new one
$result = $dailyReportService->addTaskToDailyReport($taskData, '1234-12-12', $user);
//[...]
}
This will be executed only when user will log in for the first time/user were logged in yesterday but this is his first request during the new day.
There will be less complexity in my business logic, i do not need to constantly checking in services/controllers if there is a report created because this has been executed
previously in the day.
I'm not sure if this is the answer you want to hear, but basically I think you're dealing with accidental complexity, and you're trying to solve the wrong problem.
Before continuing I'd strongly suggest you consider the following questions:
What happens if someone submits the same report twice
What happens if someone submits a report two different times, but in the second one, it's slightly different?
What is the impact of actually storing the same report from the same person twice?
The answers to the above questions should guide your decision.
IMPORTANT: Also, please note that both of your methods above have a small window where two concurrent requests to store the rerport would succeed.
From personal experience I would suggest:
If having duplicates isn't that big a problem (for example you may have a script that you run manually or automatically every so often that clears duplicates), then follow your option 1. It's not that bad, and for human scale errors should work OK.
If duplicates are somewhat of a problem, have a process that runs asynchronously after reports are submited, and tries to find duplicates. Then deal with them according to how your domain experts want (for example maybe duplicates are deleted, if one is newer either the old is deleted or flagged for human decision)
If this is part of an invariant-level constraint in the business (although I highly doubt it given that we're speaking about reports), and at no point in time should there ever be two reports, then there should be an aggregate in place to enforce this. Maybe this is UserMonthlyReport or whatever, and you can enforce this during runtime. Of course this is more complicated and potentially a lot more work, but if there is a business case for an invariant, then this is what you should do. (again, I doubt it's needed for reports, but I write it here in the care reports were used as an example, or for future readers).

PHP OOP Efficient DB Read Reduction

Six years ago I started a new PHP OOP project without having any experience so I just made it up as I went along. Anyway, I noticed that my rather powerful mySQL server sometimes gets bogged down far too easily and was wondering what the best way to limit some db activity, when I came up with this, as an example...
private $q_enghours;
public function gEngHours() {
if ( isset($this->q_enghours) ) {
} else {
$q = "SELECT q_eh FROM " . quQUOTES . " WHERE id = " . $this->id;
if ($r = $this->_dblink->query($q)) {
$row = $r->fetch_row();
$r->free();
$this->q_enghours = $row[0];
}
else {
$this->q_enghours = 0;
}
}
return $this->q_enghours;
}
This seems like it should be effective in greatly reducing the necessary reads to the db. If the object property is populated, no need to access the db. Note that there are almost two dozen classes all with the same db access routines for the "getter". I've only implemented this change in one place and was wondering if there is a "best practice" for this that I may have missed before I re-write all the classes.
I'd say that this question is rather based on wrong premises.
If you want to deal with "easily bogged down" database, then you have to dig up the particular reason, instead of just making guesses. These trifle reads you are so concerned with, in reality won't make any difference. You have to profile your whole application and find the real cause.
If you want to reduce number of reads, then make your object to map certain database record, by reading that record and populating all the properties once, at object creation. Constructors are made for it.
As a side note, you really need a good database wrapper, just to reduce the amount of code you have to write for each database call, so, this code can be written as
public function gEngHours() {
if ( !isset($this->q_enghours) ) {
$this->q_enghours = $this->db->getOne("SELECT q_eh FROM ?n WHERE id = ?", quQUOTES, $this->id);
}
return $this->q_enghours;
}
where getOne() method is doing all the job of running the query, fetching row, getting first result from it and many other thinks like proper error handling and making query safe.

CakePHP: authorizing actions based on belongsTo relationships

Let's keep it simple.
A project has just two models:
User (hasMany Project)
Project (belongsTo User)
Users are only allowed to perform actions on the projects which they own. No one else's.
I know how to manually check who the logged in user is and whether or not he/she owns a specific project, but is there a better, more global way to do this? I'm looking for a more D.R.Y. way that doesn't require repeating the same validation inside multiple actions. For example, is there a config setting like maybe...
Configure::write('Enforce_belongs_to', true);
...or maybe a setting/option on the Auth component.
Maybe this is crazy, but I figured I'd ask.
Adding to Nunser's answer, here would be a general concept of how the behavior would be. You can then attach it to the applicable model.
class StrongBelongBehavior extends ModelBehavior
{
public function beforeFind( Model $Model, $query = array() ) {
$query['conditions'] = array_merge( (array)$query['conditions'], array( $Model->alias.'.user_id' => CakeSession::read("Auth.User.id" ) );
return $query;
}
public function beforeSave( Model $Model ) {
$projectId = Hash::get( $Model->data, 'Poject.id' );
if( $projectId ) {
$Model->loadModel('UserProject'); // UserProject is a custom model
$canEdit = $Model->UserProject->projectIDExists( $projectId ); // returns true if projectId belongs to the current user
if ( ! $canEdit ) {
return false;
}
}
return true;
}
}
I'm not sure if what I'm answering is the best-utermost-dry-it's-almost-dehydrating approach, but is the simplest thing I could think of.
In the Project model, create a function that return an array of project ids associated to an user.
class Project extends AppModel {
public function getByUserId($userId) {
$projectsArray = array();
if ($userId != "valid")
//do all the checks, if it's not null, numeric, if the id exists, etc
$projects = $this->Project->find('all', array('conditions'=>
array('user_id'=>$userId)));
if (!empty($projects)) {
foreach($projects as $i => $project)
$projectsArray[] = $project['Project']['id'];
}
return $projectsArray;
}
}
You mention a find('first') in your comment, but I'm assuming you want all the projects related to the user instead of just the first. If not, it's a simple modification of that function. Also, I'm just getting the ids, but you may want an $id=>$name_project array, up to you.
Now, I don't know what you mean by "only allowed to perform actions", is it just edits that are restricted? Or lists or views should be restricted and not even shown to the user if the project is not his/hers?
For the first case, restrict editing, modify beforeSave.
public function beforeSave($options = array()) {
if(!$this->id && !isset($this->data[$this->alias][$this->primaryKey])) {
//INSERT
//not doing anything
} else {
//UPDATE
//check if project inside allowed projects array
$allowed = $this->getByUserId(CakeSession::read("Auth.User.id"));
if (!in_array($this->id, $allowed))
return false; //or throw error and catch it in the controller
}
return true;
}
The code is untested, but in general terms, you prevent the edit of a project that is not "the user's" just before the update of the record. I assume the insert of new projects is free for everyone. According to this post, all saving functions except saveAll pass through this filter first, so you will need to overwrite the saveAll function and add a validation similar to the one in beforeSave (as explained in the answer there).
And for the second part, filtering results so the user isn't even aware that there are other projects instead of his/hers, change beforeFind. The docs talk about restricting results based on user's roles, so I guess we're on the right track.
public function beforeFind($queryData) {
//force the condition
$allowed = $this->getByUserId(CakeSession::read("Auth.User.id"));
$queryData['conditions'][$this->alias.'.user_id'] = $allowed;
return $queryData;
}
Since the $allowed array has just id values, it'll work like an IN clause, but if you change that array structure, be sure to modify these functions accordingly.
And that's it. I'm thinking about the more basic cases here, edit, view, delete... Ups, delete... change the beforeDelete function also, to avoid any evil users who want to delete others property. The logic remains the same (check if project id is in allowed array, if not, return false or throw error), so I won't add the example of that function here. But that's the basic stuff. If for some reason you want to have the allowed projects in the controller, call the getByUserId function in beforeFilter and handle that ids array there. You can even store it in session, but you'll have to have in mind maintaining that session when adding or deleting projects.
If you want a superadmin that can see and edit everything, just add a condition in getByUserId that checks the role of the user, and if it is an admin, return all projects.
Also, keep in mind: maybe Project has many... subprojects (not much imagination), and so, the user related to the project can add subprojects, but the same evil user as before modifies the hidden project_id that subproject has and edits it. In that case, I recommend you also add a validation in Subproject to avoid actions on models related to Project that are not his. If you have the Security component in place and the edit and delete actions can just be reached by forms, this is a minor thing because Security Component well used avoids form tampering. But give it a thought to see if you need to validate "Subproject" instances also.
As Ayo Akinyemi mentioned, you can use all this as a behavior. I haven't personally done so, but it meets the requirements, all the callbacks modified here are what you modify in a behaviour. You'll have to encapsulate the logic, column names (need to be variable an not set hardcoded, like user_id), etc, but it will be reusable in any other cake project you'll have. Something like StrongBelongBehavior or MoreDRYBehavior. And share it if you do it :)
I'm not sure if Auth component has some way of doing what you want, but that would be the best option I guess. Until some enlightens me (I haven't investigate much this issue), this is the solution I'd use.

storing permissions into multi dimensional array php

OK, I am just trying to get better at making more loosely coupled classes etc in PHP just to improve my skills. I have a local test database on my computer and for the user table I have a column named "role". I am trying to build a function that is a general function for getting permissions for a user so it doesn't depend on a specific task they are trying to do.
When a user tries to do something such as create a new forum topic etc, I want to query the database and if "role" is a certain value, store permissions in a multidimensional array like the following:
$permissions = array(
'forums' => array("create", "delete", "edit", "lock"),
'users' => array("edit", "lock")
);
Then I want to be able to search that array for a specific permission without typing the following at the top of every PHP file after a user posts a form by checking isset($var). So if the user is trying to edit a user I want to be able to do something like the following via a class method if possible
if (Class::get_permissions($userID),array($permissionType=>$permission))) {
// do query
} else {
// return error message
}
How would be a good way to have a loosely coupled permission checking function that will be able to do something like this? It doesn't have to be laid out exactly like this but just be loosely coupled so it can be reused and not be bound to a certain task. But I want to be able to have an array of permissions instead of just "admin","user", etc for reusability and so it doesn't restrict my options down the road. Because I have a bunch of code that is like this right now in the top of my php script files.
if (Class::get_permissions($userID) == "admin") {
// allow query
} else {
// return error
}
Thanks for any input to help me get this to where I don't keep writing the same stuff over and over.
Your question is a little vague, but I will do my best. You said you're storing their permissions in an array $permissions.
public static $permissions = array();
public static function setPermissions($perms)
{
if (!is_array($perms)) {
throw new Exception('$perms must be an array.');
}
self::$permissions = $perms;
}
public static function hasPermission($section, $action)
{
if (array_key_exists($section, self::$permissions)
&& in_array($action, self::$permissions[$section])
) {
return true;
}
return false;
}
Using that logic, when you read a user's permissions from the DB, then set the Class::$permissions static var like so:
Class::setPermissions($permissions);
// ...
if (Class::hasPermissions($page, $action)) {
// user has permission
}
Note, my code is pretty generic and will have to remain that way until I have more information. For now, I'm assuming your permissions array is using a page section as the index and the array is a list of actions within that page section that the user has access to. So, assuming $page has been set to something like "forums" and the user is currently trying to perform an edit (so $action = 'edit'), the Class::hasPermission() function would return true.
I ran out of characters in the comments... But this is to your comment.
#corey instead of having a static object, I include a function that sets my permissions in the user's session. It as part of my LoginCommand class that gets called whenever the user logs in, obviously.
The permissions are then stored from view to view and I don't have to keep querying. The permissions check for most things only happen when the user logs in. However, certain sensitive things I'll run another query to double check. This has the disadvantage that, if the user's permissions change while the user has an active session, these changes won't be pushed to the user.
Remember to exercise good session security.
PHP Session Security
The only reason you wouldn't store data in your session size is because your session got too big. But unless you sessions are megabyte's, you probably don't need to worry about this.

Best way to code Achievements system

I'm thinking of the best way to design an achievements system for use on my site. The database structure can be found at Best way to tell 3 or more consecutive records missing and this thread is really an extension to get the ideas from developers.
The problem I have with lots of talk about badges/achievement systems on this website is just that -- it's all talk and no code. Where's the actual code implemention examples?
I propose here a design that I hope people could contribute to and hopefully create a good design for coding extensible achievement systems. I'm not saying this is the best, far from it, but it's a possible starting block.
Please feel free to contribute your ideas.
my system design idea
It seems the general consensus is to create an "event based system" -- whenever a known event occurs like a post is created, deleted, etc it calls the event class like so..
$event->trigger('POST_CREATED', array('id' => 8));
The event class then finds out what badges are "listening" for this event, then it requires that file, and creates an instance of that class, like so:
require '/badges/' . $file;
$badge = new $class;
It then calls the default event passing the data received when trigger was called;
$badge->default_event($data);
the badges
This is then where the real magic happens. each badge has its own query/logic to determine if a badge should be awarded. Each badge is set out in e.g. this format:
class Badge_Name extends Badge
{
const _BADGE_500 = 'POST_500';
const _BADGE_300 = 'POST_300';
const _BADGE_100 = 'POST_100';
function get_user_post_count()
{
$escaped_user_id = mysql_real_escape_string($this->user_id);
$r = mysql_query("SELECT COUNT(*) FROM posts
WHERE userid='$escaped_user_id'");
if ($row = mysql_fetch_row($r))
{
return $row[0];
}
return 0;
}
function default_event($data)
{
$post_count = $this->get_user_post_count();
$this->try_award($post_count);
}
function try_award($post_count)
{
if ($post_count > 500)
{
$this->award(self::_BADGE_500);
}
else if ($post_count > 300)
{
$this->award(self::_BADGE_300);
}
else if ($post_count > 100)
{
$this->award(self::_BADGE_100);
}
}
}
award function comes from an extended class Badge which basically checks to see if the user has already be awarded that badge, if not, will update the badge db table. The badge class also takes care of retrieving all badges for a user and returning it in an array, etc (so badges can be e.g. displayed on the user profile)
what about when the system is very first implemented on an already live site?
There is also a "cron" job query that can be added to each badge. The reason for this is because when the badge system is very first implemented and initilaised, the badges that should have already been earned have not yet be awarded because this is an event based system. So a CRON job is run on demand for each badge to award anything that needs to be. For example the CRON job for the above would look like:
class Badge_Name_Cron extends Badge_Name
{
function cron_job()
{
$r = mysql_query('SELECT COUNT(*) as post_count, user_id FROM posts');
while ($obj = mysql_fetch_object($r))
{
$this->user_id = $obj->user_id; //make sure we're operating on the right user
$this->try_award($obj->post_count);
}
}
}
As the above cron class extends the main badge class, it can re-use the logic function try_award
The reason why I create a specialised query for this is although we could "simulate" previous events, i.e. go through every user post and trigger the event class like $event->trigger() it would be very slow, especially for many badges. So we instead create an optimized query.
what user gets the award? all about awarding other users based on event
The Badge class award function acts on user_id -- they will always be given the award. By default the badge is awarded to the person who CAUSED the event to happen i.e. the session user id (this is true for the default_event function, although the CRON job obviously loops through all users and awards seperate users)
So let's take an example, on a coding challenge website users submit their coding entry. The admin then judges the entries and when complete, posts the results to the challenge page for all to see. When this happens, a POSTED_RESULTS event is called.
If you want to award badges for users for all the entries posted, lets say, if they were ranked within the top 5, you should use the cron job (although bare in mind this will update for all users, not just for that challenge the results were posted for)
If you want to target a more specific area to update with the cron job, let's see if there is a way to add filtering parameters into the cron job object, and get the cron_job function to use them. For example:
class Badge_Top5 extends Badge
{
const _BADGE_NAME = 'top5';
function try_award($position)
{
if ($position <= 5)
{
$this->award(self::_BADGE_NAME);
}
}
}
class Badge_Top5_Cron extends Badge_Top5
{
function cron_job($challenge_id = 0)
{
$where = '';
if ($challenge_id)
{
$escaped_challenge_id = mysql_real_escape_string($challenge_id);
$where = "WHERE challenge_id = '$escaped_challenge_id'";
}
$r = mysql_query("SELECT position, user_id
FROM challenge_entries
$where");
while ($obj = mysql_fetch_object($r))
{
$this->user_id = $obj->user_id; //award the correct user!
$this->try_award($obj->position);
}
}
The cron function will still work even if the parameter is not supplied.
I've implemented a reward system once in what you would call a document oriented database (this was a mud for players). Some highlights from my implementation, translated to PHP and MySQL:
Every detail about the badge is stored in the users data. If you use MySQL I would have made sure that this data is in one record per user in the database for performance.
Every time the person in question does something, the code triggers the badge code with a given flag, for instance flag('POST_MESSAGE').
One event could also trigger a counter, for instance a count of number of posts. increase_count('POST_MESSAGE'). In here you could have a check (either by a hook, or just having a test in this method) that if the POST_MESSAGE count is > 300 then you should have reward a badge, for instance: flag("300_POST").
In the flag method, I'd put the code to reward badges. For instance, if the Flag 300_POST is sent, then the badge reward_badge("300_POST") should be called.
In the flag method, you should also have the users previous flags present. so you could say when the user has FIRST_COMMENT, FIRST_POST, FIRST_READ you grant badge("NEW USER"), and when you get 100_COMMENT, 100_POST, 300_READ you can grant badge("EXPERIENCED_USER")
All of these flags and badges need to be stored somehow. Use some way where you think of the flags as bits. If you want this to be stored really efficiently, you think of them as bits and use the code below: (Or you could just use a bare string "000000001111000" if you don't want this complexity.
$achievments = 0;
$bits = sprintf("%032b", $achievements);
/* Set bit 10 */
$bits[10] = 1;
$achievements = bindec($bits);
print "Bits: $bits\n";
print "Achievements: $achievements\n";
/* Reload */
$bits = sprintf("%032b", $achievments);
/* Set bit 5 */
$bits[5] = 1;
$achievements = bindec($bits);
print "Bits: $bits\n";
print "Achievements: $achievements\n";
A nice way of storing a document for the user is to use json and store the users data in a single text column. Use json_encode and json_decode to store/retrieve the data.
For tracking activity on some of the users data manipulated by some other user, add a data structure on the item and use counters there as well. For instance read count. Use the same technique as described above for awarding badges, but the update should of course go into the owning users post. (For instance article read 1000 times badge).
UserInfuser is an open source gamification platform which implements a badging/points service. You can check out its API here:
http://code.google.com/p/userinfuser/wiki/API_Documentation
I implemented it and tried to keep the number of functions minimal. Here is the API for a php client:
class UserInfuser($account, $api_key)
{
public function get_user_data($user_id);
public function update_user($user_id);
public function award_badge($badge_id, $user_id);
public function remove_badge($badge_id, $user_id);
public function award_points($user_id, $points_awarded);
public function award_badge_points($badge_id, $user_id, $points_awarded, $points_required);
public function get_widget($user_id, $widget_type);
}
The end result is to show the data in a meaningful way through the use of widgets. These widgets include: trophy case, leaderboard, milestones, live notifications, rank and points.
The implementation of the API can be found here: http://code.google.com/p/userinfuser/source/browse/trunk/serverside/api/api.py
Achievements can be burdensome and even more so if you have to add them in later, unless you have a well-formed Event class.
This segues into my technique of implementing achievements.
I like to split them first into 'categories' and within those have tiers of accomplishment. i.e. a kills category in a game may have an award at 1 for first kill, 10 ten kills, 1000 thousand kills etc.
Then to the spine of any good application, the class handling your events. Again imagining a game with kills; when a player kills something, stuff happens. The kill is noted, etc and that is best handled in a centralized location, like and Events class that can dispatch info to other places involved.
It falls perfectly into place there, that in the proper method, instantiate your Achievements class and check it the player is due one.
As building the Achievements class it is trivial, just something that checks the database to see if the player has as many kills as are required for the next achievement.
I like to store user's achievements in a BitField using Redis but the same technique can be used in MySQL. That is, you can store the player's achievements as an int and then and that int with the bit you have defined as that achievement to see if they have already gained it. That way it uses only a single int column in the database.
The downside to this is you have to have them organized well and you will likely need to make some comments in your code so you will remember what 2^14 corresponds to later. If your achievements are enumerated in their own table then you can just do 2^pk where pk is the primary key of the achievements table. That makes the check something like
if(((2**$pk) & ($usersAchInt)) > 0){
// fire off the giveAchievement() event
}
This way you can add achievements later and it will dovetail fine, just NEVER change the primary key of the achievements already awarded.

Categories