Usually achievement systems boil down to a simple pattern of:
values -> conditions -> achievements
- Define a list of values that might influence achievements (e.g. Highscore, Highest-Completed-Level, Number-Of-Hours-Player, Cash-Earned)
- Every time one of these values changes have the "owner" of the value (e.g. the "Player-Class" for "Cash-Earned") send a valueChanged( value_id, new_value ) event which is routed to the AchievementManager.
- The AchievementManager checks his list of conditions (for incomplete achievements depending on that value) and updates the achievements (sending an event if needed).
Don't forget the load/save/reset edge-cases, but that's it.
My current implementation has around 70 lines of code, not including the "Achievement.config to achievement.cfg" converter.
The configuration currently looks like this:
VALUE_PLAY_TIME_IN_MINUTES > 3600,
VALUE_CASH > 10k
The created .cfg, which is loaded by the game, then looks something like this
[Number of Achievements]
for each achievement
[CRC32 of the achievement string, e.g. ACHIEVEMENT_PLAYED_VERY_LONG_AND_EARNED_LOTS_OF_CASH]
[Number of Conditions]
for each condition
[CRC32 of the value name string, e.g. VALUE_CASH]
[condition type, e.g. 0 => less than, 1 => greater than]
[value, e.g. 10000]
The converter is actually a little smarter, extending "10K" to "10000", warning about duplicates, detecting of conditions are conflicting, etc.
You could use an enum for the achievement and value id, but I prefer CRC32s, since you don't have to update a list for new values and still get the benefit of numeric keys and even can map them back to the original name in debug builds.
(But ids are a completely different topic ;) )
The system once had a quite complexe boolean logic for conditions, but in the end all we ever used was "and", so we removed it.