mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Added tests for repeatable achievements, including complicated xp
Intermediate
This commit is contained in:
parent
faf02d8e4b
commit
1fe2c67ffe
9 changed files with 120 additions and 28 deletions
|
@ -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
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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', ->
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue