mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 08:50:58 -05:00
Achievement recalculation is now covered with tests
This commit is contained in:
parent
a367082cc4
commit
e904a0a8f7
6 changed files with 132 additions and 71 deletions
|
@ -64,6 +64,6 @@ AchievementSchema.post 'save', -> @constructor.loadAchievements()
|
|||
AchievementSchema.plugin(plugins.NamedPlugin)
|
||||
AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']})
|
||||
|
||||
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema)
|
||||
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema, 'achievements')
|
||||
|
||||
AchievementSchema.statics.loadAchievements()
|
||||
|
|
|
@ -23,14 +23,13 @@ class EarnedAchievementHandler extends Handler
|
|||
EarnedAchievementHandler.recalculate onSuccess
|
||||
@sendAccepted res, {}
|
||||
|
||||
# Returns success: boolean
|
||||
# TODO call onFinished
|
||||
@recalculate: (callbackOrSlugsOrIDs, onFinished) ->
|
||||
if _.isArray callbackOrSlugsOrIDs
|
||||
@recalculate: (callbackOrSlugsOrIDs, callback) ->
|
||||
if _.isArray callbackOrSlugsOrIDs # slugs or ids
|
||||
achievementSlugs = (thing for thing in callbackOrSlugsOrIDs when not Handler.isID(thing))
|
||||
achievementIDs = (thing for thing in callbackOrSlugsOrIDs when Handler.isID(thing))
|
||||
else
|
||||
onFinished = callbackOrSlugsOrIDs
|
||||
else # just a callback
|
||||
callback = callbackOrSlugsOrIDs
|
||||
onFinished = -> callback arguments... if callback?
|
||||
|
||||
filter = {}
|
||||
filter.$or = [
|
||||
|
@ -40,11 +39,11 @@ class EarnedAchievementHandler extends Handler
|
|||
|
||||
# Fetch all relevant achievements
|
||||
Achievement.find filter, (err, achievements) ->
|
||||
return log.error err if err?
|
||||
log.error err if err?
|
||||
|
||||
# Fetch every single user
|
||||
User.find {}, (err, users) ->
|
||||
_.each users, (user) ->
|
||||
async.each users, ((user, doneWithUser) ->
|
||||
# Keep track of a user's already achieved in order to set the notified values correctly
|
||||
userID = user.get('_id').toHexString()
|
||||
|
||||
|
@ -52,64 +51,70 @@ class EarnedAchievementHandler extends Handler
|
|||
EarnedAchievement.find {user: userID}, (err, alreadyEarned) ->
|
||||
alreadyEarnedIDs = []
|
||||
previousPoints = 0
|
||||
_.each alreadyEarned, (earned) ->
|
||||
if (_.find achievements, (single) -> earned.get('achievement') is single.get('_id').toHexString())
|
||||
async.each alreadyEarned, ((earned, doneWithEarned) ->
|
||||
if (_.find achievements, (single) -> earned.get('achievement') is single.get('_id').toHexString()) # if already earned
|
||||
alreadyEarnedIDs.push earned.get('achievement')
|
||||
previousPoints += earned.get 'earnedPoints'
|
||||
doneWithEarned()
|
||||
), -> # After checking already achieved
|
||||
# TODO maybe also delete earned? Make sure you don't delete too many
|
||||
|
||||
# TODO maybe also delete earned? Make sure you don't delete too many
|
||||
newTotalPoints = 0
|
||||
|
||||
newTotalPoints = 0
|
||||
async.each achievements, ((achievement, doneWithAchievement) ->
|
||||
isRepeatable = achievement.get('proportionalTo')?
|
||||
model = mongoose.modelNameByCollection(achievement.get('collection'))
|
||||
if not model?
|
||||
log.error "Model with collection '#{achievement.get 'collection'}' doesn't exist."
|
||||
return doneWithAchievement()
|
||||
|
||||
earnedAchievementSaverGenerator = (achievement) -> (callback) ->
|
||||
isRepeatable = achievement.get('proportionalTo')?
|
||||
model = mongoose.model(achievement.get('collection'))
|
||||
if not model?
|
||||
log.error "Model #{achievement.get 'collection'} doesn't even exist."
|
||||
return callback()
|
||||
finalQuery = _.clone achievement.get 'query'
|
||||
finalQuery.$or = [{}, {}] # Allow both ObjectIDs or hexa string IDs
|
||||
finalQuery.$or[0][achievement.userField] = userID
|
||||
finalQuery.$or[1][achievement.userField] = ObjectId userID
|
||||
|
||||
model.findOne achievement.query, (err, something) ->
|
||||
return callback() unless something
|
||||
model.findOne finalQuery, (err, something) ->
|
||||
return doneWithAchievement() if _.isEmpty something
|
||||
|
||||
log.debug "Matched an achievement: #{achievement.get 'name'}"
|
||||
log.debug "Matched an achievement: #{achievement.get 'name'} for #{user.get 'name'}"
|
||||
|
||||
earned =
|
||||
user: userID
|
||||
achievement: achievement._id.toHexString()
|
||||
achievementName: achievement.get 'name'
|
||||
notified: achievement._id in alreadyEarnedIDs
|
||||
earned =
|
||||
user: userID
|
||||
achievement: achievement._id.toHexString()
|
||||
achievementName: achievement.get 'name'
|
||||
notified: achievement._id in alreadyEarnedIDs
|
||||
|
||||
if isRepeatable
|
||||
earned.achievedAmount = something.get(achievement.get 'proportionalTo')
|
||||
earned.previouslyAchievedAmount = 0
|
||||
if isRepeatable
|
||||
earned.achievedAmount = something.get(achievement.get 'proportionalTo')
|
||||
earned.previouslyAchievedAmount = 0
|
||||
|
||||
expFunction = achievement.getExpFunction()
|
||||
newPoints = expFunction(earned.achievedAmount) * achievement.get('worth')
|
||||
expFunction = achievement.getExpFunction()
|
||||
newPoints = expFunction(earned.achievedAmount) * achievement.get('worth')
|
||||
else
|
||||
newPoints = achievement.get 'worth'
|
||||
|
||||
earned.earnedPoints = newPoints
|
||||
newTotalPoints += newPoints
|
||||
|
||||
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
||||
log.error err if err?
|
||||
doneWithAchievement()
|
||||
), saveUserPoints = ->
|
||||
# In principle it is enough to deduct the old amount of points and add the new amount,
|
||||
# but just to be entirely safe let's start from 0 in case we're updating all of a user's achievements
|
||||
return doneWithUser() unless newTotalPoints
|
||||
log.debug "Matched a total of #{newTotalPoints} new points"
|
||||
if _.isEmpty filter # Completely clean
|
||||
log.debug "Setting this user's score to #{newTotalPoints}"
|
||||
User.update {_id: userID}, {$set: points: newTotalPoints}, {}, (err) ->
|
||||
log.error err if err?
|
||||
doneWithUser()
|
||||
else
|
||||
newPoints = achievement.get 'worth'
|
||||
|
||||
earned.earnedPoints = newPoints
|
||||
newTotalPoints += newPoints
|
||||
|
||||
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
||||
log.error err if err?
|
||||
callback()
|
||||
|
||||
saveUserPoints = (callback) ->
|
||||
# In principle it is enough to deduct the old amount of points and add the new amount,
|
||||
# but just to be entirely safe let's start from 0 in case we're updating all of a user's achievements
|
||||
log.debug "Matched a total of #{newTotalPoints} new points"
|
||||
if _.isEmpty filter # Completely clean
|
||||
User.update {_id: userID}, {$set: points: newTotalPoints}, {}, (err) -> log.error err if err?
|
||||
else
|
||||
log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}"
|
||||
User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) -> log.error err if err?
|
||||
|
||||
earnedAchievementSavers = (earnedAchievementSaverGenerator(achievement) for achievement in achievements)
|
||||
earnedAchievementSavers.push saveUserPoints
|
||||
|
||||
# We need to have all these database updates chained so we know the final score
|
||||
async.series earnedAchievementSavers
|
||||
log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}"
|
||||
User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) ->
|
||||
log.error err if err?
|
||||
doneWithUser()
|
||||
), onFinished
|
||||
|
||||
|
||||
module.exports = new EarnedAchievementHandler()
|
||||
|
|
|
@ -26,3 +26,12 @@ module.exports.routes =
|
|||
'routes/queue'
|
||||
'routes/stacklead'
|
||||
]
|
||||
|
||||
mongoose = require 'mongoose'
|
||||
module.exports.modules = modules = # by collection name
|
||||
'achievements': 'Achievement'
|
||||
'level.sessions': 'level.session'
|
||||
'users': 'User'
|
||||
|
||||
mongoose.modelNameByCollection = (collection) ->
|
||||
mongoose.model modules[collection] if collection of modules
|
||||
|
|
|
@ -42,4 +42,4 @@ LevelSessionSchema.pre 'save', (next) ->
|
|||
delete previous[id] if initd
|
||||
next()
|
||||
|
||||
module.exports = LevelSession = mongoose.model('level.session', LevelSessionSchema)
|
||||
module.exports = LevelSession = mongoose.model('level.session', LevelSessionSchema, 'level.sessions')
|
||||
|
|
|
@ -14,10 +14,12 @@ AchievablePlugin = (schema, options) ->
|
|||
|
||||
before = {}
|
||||
|
||||
# Keep track the document before it's saved
|
||||
schema.post 'init', (doc) ->
|
||||
before[doc.id] = doc.toObject()
|
||||
# TODO check out how many objects go unreleased
|
||||
|
||||
# Check if an achievement has been earned
|
||||
schema.post 'save', (doc) ->
|
||||
isNew = not doc.isInit('_id') or not (doc.id of before)
|
||||
originalDocObj = before[doc.id] unless isNew
|
||||
|
@ -36,27 +38,25 @@ AchievablePlugin = (schema, options) ->
|
|||
isRepeatable = achievement.get('proportionalTo')?
|
||||
alreadyAchieved = if isNew then false else LocalMongo.matchesQuery originalDocObj, query
|
||||
newlyAchieved = LocalMongo.matchesQuery(docObj, query)
|
||||
log.debug 'isRepeatable: ' + isRepeatable
|
||||
log.debug 'alreadyAchieved: ' + alreadyAchieved
|
||||
log.debug 'newlyAchieved: ' + newlyAchieved
|
||||
#log.debug 'isRepeatable: ' + isRepeatable
|
||||
#log.debug 'alreadyAchieved: ' + alreadyAchieved
|
||||
#log.debug 'newlyAchieved: ' + newlyAchieved
|
||||
|
||||
userObjectID = doc.get(achievement.get('userField'))
|
||||
userID = if _.isObject userObjectID then userObjectID.toHexString() else userObjectID # Standardize! Use strings, not ObjectId's
|
||||
|
||||
if newlyAchieved and (not alreadyAchieved or isRepeatable)
|
||||
earned = {
|
||||
earned =
|
||||
user: userID
|
||||
achievement: achievement._id.toHexString()
|
||||
achievementName: achievement.get 'name'
|
||||
}
|
||||
|
||||
worth = achievement.get('worth')
|
||||
earnedPoints = 0
|
||||
wrapUp = ->
|
||||
# Update user's experience points
|
||||
User.update({_id: userID}, {$inc: {points: earnedPoints}}, {}, (err, count) ->
|
||||
User.update {_id: userID}, {$inc: {points: earnedPoints}}, {}, (err, count) ->
|
||||
log.error err if err?
|
||||
)
|
||||
|
||||
if isRepeatable
|
||||
log.debug 'Upserting repeatable achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
||||
|
@ -81,7 +81,7 @@ AchievablePlugin = (schema, options) ->
|
|||
log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
||||
earned.earnedPoints = worth
|
||||
(new EarnedAchievement(earned)).save (err, doc) ->
|
||||
return log.debug err if err?
|
||||
return log.error err if err?
|
||||
earnedPoints = worth
|
||||
wrapUp()
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ repeatable =
|
|||
description: 'Simulated Games.'
|
||||
worth: 1
|
||||
collection: 'users'
|
||||
query: "{\"simulatedBy\":{\"$gt\":\"0\"}}"
|
||||
query: "{\"simulatedBy\":{\"$gt\":0}}"
|
||||
userField: '_id'
|
||||
proportionalTo: 'simulatedBy'
|
||||
|
||||
|
@ -21,7 +21,7 @@ diminishing =
|
|||
name: 'Simulated2'
|
||||
worth: 1.5
|
||||
collection: 'users'
|
||||
query: "{\"simulatedBy\":{\"$gt\":\"0\"}}"
|
||||
query: "{\"simulatedBy\":{\"$gt\":0}}"
|
||||
userField: '_id'
|
||||
proportionalTo: 'simulatedBy'
|
||||
function:
|
||||
|
@ -106,7 +106,6 @@ describe 'Achievement', ->
|
|||
expect(body.type).toBeDefined()
|
||||
done()
|
||||
|
||||
|
||||
describe 'Achieving Achievements', ->
|
||||
it 'wait for achievements to be loaded', (done) ->
|
||||
Achievement.loadAchievements (achievements) ->
|
||||
|
@ -120,11 +119,10 @@ describe 'Achieving Achievements', ->
|
|||
|
||||
it 'saving an object that should trigger an unlockable achievement', (done) ->
|
||||
unittest.getNormalJoe (joe) ->
|
||||
session = new LevelSession(
|
||||
session = new LevelSession
|
||||
permissions: simplePermissions
|
||||
creator: joe._id
|
||||
level: original: 'dungeon-arena'
|
||||
)
|
||||
|
||||
session.save (err, doc) ->
|
||||
expect(err).toBeNull()
|
||||
|
@ -139,6 +137,7 @@ describe 'Achieving Achievements', ->
|
|||
expect(err).toBeNull()
|
||||
expect(docs.length).toBe(1)
|
||||
achievement = docs[0]
|
||||
expect(achievement).toBeDefined()
|
||||
|
||||
expect(achievement.get 'achievement').toBe unlockable._id
|
||||
expect(achievement.get 'user').toBe joe._id.toHexString()
|
||||
|
@ -185,7 +184,55 @@ describe 'Achieving Achievements', ->
|
|||
|
||||
done()
|
||||
|
||||
it 'cleaning up test: deleting all Achievements and relates', (done) ->
|
||||
describe 'Recalculate Achievements', ->
|
||||
EarnedAchievementHandler = require '../../../server/achievements/earned_achievement_handler'
|
||||
|
||||
it 'remove earned achievements', (done) ->
|
||||
clearModels [EarnedAchievement], (err) ->
|
||||
expect(err).toBeNull()
|
||||
EarnedAchievement.find {}, (err, earned) ->
|
||||
expect(earned.length).toBe 0
|
||||
|
||||
User.update {}, {$set: {points: 0}}, {multi:true}, (err) ->
|
||||
expect(err).toBeNull()
|
||||
done()
|
||||
|
||||
it 'can not be accessed by regular users', (done) ->
|
||||
loginJoe -> request.post {uri:getURL '/admin/earned_achievement/recalculate'}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe 403
|
||||
done()
|
||||
|
||||
it 'can recalculate a selection of achievements', (done) ->
|
||||
loginAdmin ->
|
||||
EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
|
||||
EarnedAchievement.find {}, (err, earnedAchievements) ->
|
||||
expect(earnedAchievements.length).toBe 1
|
||||
|
||||
# Recalculate again, doesn't change a thing
|
||||
EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
|
||||
EarnedAchievement.find {}, (err, earnedAchievements) ->
|
||||
expect(earnedAchievements.length).toBe 1
|
||||
|
||||
unittest.getNormalJoe (joe) ->
|
||||
User.findById joe.get('id'), (err, guy) ->
|
||||
expect(err).toBeNull()
|
||||
expect(guy.get 'points').toBe unlockable.worth
|
||||
done()
|
||||
|
||||
it 'can recalculate all achievements', (done) ->
|
||||
loginAdmin ->
|
||||
Achievement.count {}, (err, count) ->
|
||||
expect(count).toBe 3
|
||||
EarnedAchievementHandler.constructor.recalculate ->
|
||||
EarnedAchievement.find {}, (err, earnedAchievements) ->
|
||||
expect(earnedAchievements.length).toBe 3
|
||||
unittest.getNormalJoe (joe) ->
|
||||
User.findById joe.get('id'), (err, guy) ->
|
||||
expect(err).toBeNull()
|
||||
expect(guy.get 'points').toBe unlockable.worth + 2 * repeatable.worth + (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
|
||||
done()
|
||||
|
||||
it 'cleaning up test: deleting all Achievements and related', (done) ->
|
||||
clearModels [Achievement, EarnedAchievement, LevelSession], (err) ->
|
||||
expect(err).toBeNull()
|
||||
|
||||
|
|
Loading…
Reference in a new issue