Added tests for repeatable achievements, including complicated xp

Intermediate
This commit is contained in:
Ruben Vereecken 2014-06-14 20:12:17 +02:00
parent faf02d8e4b
commit 1fe2c67ffe
9 changed files with 120 additions and 28 deletions

View file

@ -24,6 +24,7 @@ doQuerySelector = (value, operatorObj) ->
matchesQuery = (target, queryObj) ->
return true unless queryObj
throw new Error 'Expected an object to match a query against, instead got null' unless target
for prop, query of queryObj
if prop[0] == '$'
switch prop

View file

@ -69,6 +69,7 @@ module.exports.i18n = (say, target, language=me.lang(), fallback='en') ->
null
module.exports.getByPath = (target, path) ->
throw new Error 'Expected an object to match a query against, instead got null' unless target
pieces = path.split('.')
obj = target
for piece in pieces
@ -79,7 +80,7 @@ module.exports.getByPath = (target, path) ->
module.exports.round = _.curry (digits, n) ->
n = +n.toFixed(digits)
positify = (func) -> (x) -> if x > 0 then func(x) else 0
positify = (func) -> (params) -> (x) -> if x > 0 then func(params)(x) else 0
# f(x) = ax + b
createLinearFunc = (params) ->

View file

@ -11,6 +11,6 @@ module.exports = class Achievement extends CocoModel
# TODO logic is duplicated in Mongoose Achievement schema
getExpFunction: ->
kind = @get('function')?.kind or @schema.function.default.kind
parameters = @get('function')?.parameters or @schema.function.default.parameters
kind = @get('function')?.kind or jsonschema.properties.function.default.kind
parameters = @get('function')?.parameters or jsonschema.properties.function.default.parameters
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators

View file

@ -293,6 +293,7 @@ class CocoModel extends Backbone.Model
@pollAchievements: ->
achievements = new NewAchievementCollection
console.log 'ohai'
achievements.fetch(
success: (collection) ->
me.fetch (success: -> Backbone.Mediator.publish('achievements:new', collection)) unless _.isEmpty(collection.models)

View file

@ -53,13 +53,14 @@ _.extend(AchievementSchema.properties,
function:
type: 'object'
properties:
kind: {enum: ['linear', 'logarithmic'], default: 'linear'}
kind: {enum: ['linear', 'logarithmic', 'quadratic'], default: 'linear'}
parameters:
type: 'object'
properties:
a: {type: 'number', default: 1}
b: {type: 'number', default: 1}
c: {type: 'number', default: 1}
additionalProperties: true
default: {kind: 'linear', parameters: a: 1}
required: ['kind', 'parameters']
additionalProperties: false

View file

@ -1,7 +1,7 @@
mongoose = require('mongoose')
jsonschema = require('../../app/schemas/models/achievement')
log = require 'winston'
util = require '../../app/lib/utils'
utils = require '../../app/lib/utils'
plugins = require('../plugins/plugins')
AchievablePlugin = require '../plugins/achievements'
@ -25,10 +25,11 @@ AchievementSchema.methods.stringifyQuery = ->
@set('query', JSON.stringify(@get('query'))) if typeof @get('query') != "string"
AchievementSchema.methods.getExpFunction = ->
kind = @get('function')?.kind or jsonschema.function.default.kind
parameters = @get('function')?.parameters or jsonschema.function.default.parameters
kind = @get('function')?.kind or jsonschema.properties.function.default.kind
parameters = @get('function')?.parameters or jsonschema.properties.function.default.parameters
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
AchievementSchema.statics.jsonschema = jsonschema
AchievementSchema.statics.achievements = {}
AchievementSchema.statics.loadAchievements = (done) ->

View file

@ -4,7 +4,10 @@ LocalMongo = require '../../app/lib/LocalMongo'
util = require '../../app/lib/utils'
log = require 'winston'
# Warning: To ensure proper functioning one must always `find` documents before saving them.
# Otherwise the schema's `post init` won't be triggered and the plugin can't keep track of changes
# TODO if this is still a common scenario I could implement a database hit after all, but only
# on the condition that it's necessary and still not too frequent in occurrence
AchievablePlugin = (schema, options) ->
User = require '../users/User' # Avoid mutual inclusion cycles
Achievement = require('../achievements/Achievement')
@ -12,18 +15,19 @@ AchievablePlugin = (schema, options) ->
before = {}
schema.post 'init', (doc) ->
#log.debug 'initd'
#log.debug doc.toObject()
before[doc.id] = doc.toObject()
# TODO check out how many objects go unreleased
schema.post 'save', (doc) ->
#log.debug 'waiting in init: ' + Object.keys(before).length
isNew = not doc.isInit('_id')
isNew = not doc.isInit('_id') or not (doc.id of before)
originalDocObj = before[doc.id] unless isNew
if doc.isInit('_id') and not doc.id of before
log.warn 'document was already initialized but did not go through `init` and is therefore treated as new while it might not be'
category = doc.constructor.modelName
loadedAchievements = Achievement.getLoadedAchievements()
log.debug 'about to save ' + category + ', number of achievements is ' + Object.keys(loadedAchievements).length
#log.debug 'about to save ' + category + ', number of achievements is ' + Object.keys(loadedAchievements).length
if category of loadedAchievements
docObj = doc.toObject()
@ -57,7 +61,7 @@ AchievablePlugin = (schema, options) ->
if isRepeatable
log.debug 'Upserting repeatable achievement called \'' + (achievement.get 'name') + '\' for ' + userID
proportionalTo = achievement.get 'proportionalTo'
originalAmount = util.getByPath(originalDocObj, proportionalTo) or 0
originalAmount = if originalDocObj then util.getByPath(originalDocObj, proportionalTo) or 0 else 0
newAmount = docObj[proportionalTo]
if originalAmount isnt newAmount
@ -81,7 +85,6 @@ AchievablePlugin = (schema, options) ->
earnedPoints = worth
wrapUp()
delete before[doc.id] unless isNew # This assumes everything we patch has a _id
return
delete before[doc.id] if doc.id of before
module.exports = AchievablePlugin

View file

@ -12,7 +12,7 @@ class BlandClass extends CocoModel
_id: {type: 'string'}
}
urlRoot: '/db/bland'
describe 'CocoModel', ->
describe 'save', ->
@ -82,3 +82,23 @@ describe 'CocoModel', ->
b.patch()
request = jasmine.Ajax.requests.mostRecent()
expect(request).toBeUndefined()
describe 'Achievement polling', ->
it 'achievements are polled upon saving a model', (done) ->
#spyOn(CocoModel, 'pollAchievements')
b = new BlandClass({})
res = b.save()
request = jasmine.Ajax.requests.mostRecent()
request.response({status: 200, responseText: {}})
jasmine.Ajax.requests.reset()
#expect(CocoModel.pollAchievements).toHaveBeenCalled()
console.log jasmine.Ajax.requests.mostRecent()
request = jasmine.Ajax.requests.mostRecent()
#expect(request.url).toBe("")
done()

View file

@ -17,6 +17,17 @@ repeatable =
userField: '_id'
proportionalTo: 'simulatedBy'
diminishing =
name: 'Simulated2'
worth: 1.5
collection: 'User'
query: "{\"simulatedBy\":{\"$gt\":\"0\"}}"
userField: '_id'
proportionalTo: 'simulatedBy'
function:
kind: 'logarithmic'
parameters: {a: 1, b: .5, c: .5, d: 1}
url = getURL('/db/achievement')
describe 'Achievement', ->
@ -52,15 +63,19 @@ describe 'Achievement', ->
expect(res.statusCode).toBe(200)
repeatable._id = body._id
Achievement.find {}, (err, docs) ->
expect(docs.length).toBe(2)
done()
request.post {uri: url, json: diminishing}, (err, res, body) ->
expect(res.statusCode).toBe(200)
diminishing._id = body._id
Achievement.find {}, (err, docs) ->
expect(docs.length).toBe 3
done()
it 'can get all for ordinary users', (done) ->
loginJoe ->
request.get {uri: url, json: unlockable}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.length).toBe(2)
expect(body.length).toBe 3
done()
it 'can be read by ordinary users', (done) ->
@ -103,8 +118,8 @@ describe 'Achieving Achievements', ->
done()
it 'allows users to unlock one-time Achievements', (done) ->
loginJoe (joe) ->
it 'saving an object that should trigger an unlockable achievement', (done) ->
unittest.getNormalJoe (joe) ->
session = new LevelSession(
permissions: simplePermissions
creator: joe._id
@ -117,16 +132,64 @@ describe 'Achieving Achievements', ->
expect(doc.creator).toBe(session.creator)
done()
it 'check if the earned achievement was already saved', (done) ->
EarnedAchievement.find {}, (err, docs) ->
expect(err).toBeNull()
expect(docs.length).toBe(1)
done()
it 'verify that an unlockable achievement has been earned', (done) ->
unittest.getNormalJoe (joe) ->
EarnedAchievement.find {}, (err, docs) ->
expect(err).toBeNull()
expect(docs.length).toBe(1)
achievement = docs[0]
expect(achievement.get 'achievement').toBe unlockable._id
expect(achievement.get 'user').toBe joe._id.toHexString()
expect(achievement.get 'notified').toBeFalsy()
expect(achievement.get 'earnedPoints').toBe unlockable.worth
expect(achievement.get 'achievedAmount').toBeUndefined()
expect(achievement.get 'previouslyAchievedAmount').toBeUndefined()
done()
it 'saving an object that should trigger a repeatable achievement', (done) ->
unittest.getNormalJoe (joe) ->
expect(joe.get 'simulatedBy').toBeFalsy()
joe.set('simulatedBy', 2)
joe.save (err, doc) ->
expect(err).toBeNull()
done()
it 'verify that a repeatable achievement has been earned', (done) ->
unittest.getNormalJoe (joe) ->
EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
expect(err).toBeNull()
expect(docs.length).toBe(1)
achievement = docs[0]
expect(achievement.get 'achievement').toBe repeatable._id
expect(achievement.get 'user').toBe joe._id.toHexString()
expect(achievement.get 'notified').toBeFalsy()
expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
expect(achievement.get 'achievedAmount').toBe 2
expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
done()
it 'verify that the repeatable achievement with complex exp has been earned', (done) ->
unittest.getNormalJoe (joe) ->
EarnedAchievement.find {achievementName: diminishing.name}, (err, docs) ->
expect(err).toBeNull()
expect(docs.length).toBe 1
achievement = docs[0]
expect(achievement.get 'achievedAmount').toBe 2
expect(achievement.get 'earnedPoints').toBe (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
done()
it 'cleaning up test: deleting all Achievements and relates', (done) ->
clearModels [Achievement, EarnedAchievement, LevelSession], (err) ->
expect(err).toBeNull()
# reset achievements in memory as well
Achievement.resetAchievements()
loadedAchievements = Achievement.getLoadedAchievements()
expect(Object.keys(loadedAchievements).length).toBe(0)
@ -138,3 +201,4 @@ describe 'Achieving Achievements', ->