Files
habitica/tests/algos.mocha.coffee
T
Cole Gleason 393bbb1885 Modify runCron tests to use hoursAgo instead of daysAgo.
This commit also sets hoursAgo for many of the cases and restructures
how cronMatrix is parsed so that more specific settings may override
defaults higher up on the tree.
2014-01-08 23:42:31 -06:00

505 lines
19 KiB
CoffeeScript

_ = require 'lodash'
expect = require 'expect.js'
sinon = require 'sinon'
moment = require 'moment'
shared = require '../script/index.coffee'
### Helper Functions ####
newUser = (addTasks=true)->
buffs = {per:0, int:0, con:0, str:0, stealth: 0, streaks: false}
user =
auth:
timestamps: {}
stats: {str:1, con:1, per:1, int:1, mp: 32, class: 'warrior', buffs: buffs}
items:
lastDrop:
count: 0
hatchingPotions: {}
eggs: {}
food: {}
gear:
equipped: {}
costume: {}
party:
quest:
progress:
down: 0
preferences: {}
dailys: []
todos: []
rewards: []
flags: {}
achievements: {}
shared.wrap(user)
user.ops.reset(null, ->)
if addTasks
_.each ['habit', 'todo', 'daily'], (task)->
user.ops.addTask {body: {type: task, id: shared.uuid()}}
user
rewrapUser = (user)->
user._wrapped = false
shared.wrap(user)
user
expectStrings = (obj, paths) ->
_.each paths, (path) -> expect(obj[path]).to.be.ok()
# options.hoursAgo: hours since the last cron was executed
beforeAfter = (options={}) ->
user = newUser()
[before, after] = [user, _.cloneDeep(user)]
# avoid closure on the original user
rewrapUser(after)
before.preferences.dayStart = after.preferences.dayStart = options.dayStart if options.dayStart
before.preferences.timezoneOffset = after.preferences.timezoneOffset = (options.timezoneOffset or moment().zone())
if options.limitOne
before["#{options.limitOne}s"] = [before["#{options.limitOne}s"][0]]
after["#{options.limitOne}s"] = [after["#{options.limitOne}s"][0]]
lastCron = +(moment(options.now || +new Date).subtract('hours', options.hoursAgo)) unless options.hoursAgo == undefined
_.each [before,after], (obj) ->
obj.lastCron = lastCron unless options.hoursAgo == undefined
{before:before, after:after}
#TODO calculate actual poins
expectLostPoints = (before, after, taskType) ->
if taskType in ['daily','habit']
expect(after.stats.hp).to.be.lessThan before.stats.hp
expect(after["#{taskType}s"][0].history).to.have.length(1)
else expect(after.history.todos).to.have.length(1)
expect(after.stats.exp).to.be 0
expect(after.stats.gp).to.be 0
expect(after["#{taskType}s"][0].value).to.be.lessThan before["#{taskType}s"][0].value
expectGainedPoints = (before, after, taskType) ->
expect(after.stats.hp).to.be 50
expect(after.stats.exp).to.be.greaterThan before.stats.exp
expect(after.stats.gp).to.be.greaterThan before.stats.gp
expect(after["#{taskType}s"][0].value).to.be.greaterThan before["#{taskType}s"][0].value
expect(after["#{taskType}s"][0].history).to.have.length(1) if taskType is 'habit'
# daily & todo histories handled on cron
expectNoChange = (before,after) ->
_.each ['stats', 'items', 'gear', 'dailys', 'todos', 'rewards', 'flags', 'preferences'], (attr)->
expect(after[attr]).to.eql before[attr]
expectDayResetNoDamage = (b,a) ->
[before,after] = [_.cloneDeep(b); _.cloneDeep(a)]
_.each after.dailys, (task,i) ->
expect(task.completed).to.be false
expect(before.dailys[i].value).to.be task.value
expect(before.dailys[i].streak).to.be task.streak
expect(task.history).to.have.length(1)
_.each after.todos, (task,i) ->
expect(task.completed).to.be false
expect(before.todos[i].value).to.be.greaterThan task.value
expect(after.history.todos).to.have.length(1)
# hack so we can compare user before/after obj equality sans effected paths
_.each ['dailys','todos','history','lastCron'], (path) ->
_.each [before,after], (obj) -> delete obj[path]
delete after._tmp
expectNoChange(before, after)
cycle = (array)->
n = -1
(seed=0)->
n++
return array[n % array.length]
repeatWithoutLastWeekday = ()->
repeat = {su:1,m:1,t:1,w:1,th:1,f:1,s:1}
if shared.startOfWeek(moment().zone(0)).isoWeekday() == 1 # Monday
repeat.su = false
else
repeat.s = false
{repeat: repeat}
###### Specs ######
describe 'User', ->
it 'sets correct user defaults', ->
user = newUser()
base_gear = { armor: 'armor_base_0', weapon: 'weapon_base_0', head: 'head_base_0', shield: 'shield_base_0' }
buffs = {per:0, int:0, con:0, str:0, stealth: 0, streaks: false}
expect(user.stats).to.eql { str: 1, con: 1, per: 1, int: 1, hp: 50, mp: 32, lvl: 1, exp: 0, gp: 0, class: 'warrior', buffs: buffs }
expect(user.items.gear).to.eql { equipped: base_gear, costume: base_gear, owned: {weapon_warrior_0: true} }
expect(user.preferences).to.eql { costume: false }
it 'calculates max MP', ->
user = newUser()
expect(user._statsComputed.maxMP).to.eql 32
user.stats.int = 10
expect(user._statsComputed.maxMP).to.eql 50
user.stats.lvl = 5
expect(user._statsComputed.maxMP).to.eql 54
user.stats.class = 'wizard'
user.items.gear.equipped.weapon = 'weapon_wizard_1'
expect(user._statsComputed.maxMP).to.eql 63
describe 'Death', ->
user = undefined
it 'revives correctly', ->
user = newUser()
user.stats = { gp: 10, exp: 100, lvl: 2, hp: 1 }
user.ops.revive()
expect(user.stats.gp).to.eql 0
expect(user.stats.exp).to.eql 0
expect(user.stats.lvl).to.eql 1
expect(user.stats.hp).to.eql 50
expect(user.items.gear.owned).to.eql { weapon_warrior_0: false }
it "doesn't break unbreakables", ->
ce = shared.countExists
user = newUser()
user.items.gear.owned['shield_rogue_1'] = true
user.items.gear.owned['head_special_nye'] = true
expect(ce user.items.gear.owned).to.be 3
user.ops.revive()
expect(ce(user.items.gear.owned)).to.be 2
user.ops.revive()
expect(ce(user.items.gear.owned)).to.be 1
user.ops.revive()
expect(ce(user.items.gear.owned)).to.be 0
it "handles event items", ->
shared.content.gear.flat.head_special_nye.event.start = '2012-01-01'
shared.content.gear.flat.head_special_nye.event.end = '2012-02-01'
expect(shared.content.gear.flat.head_special_nye.canOwn(user)).to.be true
delete user.items.gear.owned['head_special_nye']
expect(shared.content.gear.flat.head_special_nye.canOwn(user)).to.be false
shared.content.gear.flat.head_special_nye.event.start = moment().subtract('days',5)
shared.content.gear.flat.head_special_nye.event.end = moment().add('days',5)
expect(shared.content.gear.flat.head_special_nye.canOwn(user)).to.be true
describe 'store', ->
it 'recovers hp buying potions', ->
user = newUser()
user.stats.hp = 30
user.stats.gp = 50
user.ops.buy {params: {key: 'potion'}}
expect(user.stats.hp).to.eql 45
expect(user.stats.gp).to.eql 25
user.ops.buy {params: {key: 'potion'}}
expect(user.stats.hp).to.eql 50 # don't exceed max hp
expect(user.stats.gp).to.eql 0
it 'buys equipment', ->
user = newUser()
user.stats.gp = 31
user.ops.buy {params: {key: 'armor_warrior_1'}}
expect(user.items.gear.owned).to.eql { weapon_warrior_0: true, armor_warrior_1: true }
expect(user.items.gear.equipped).to.eql { armor: 'armor_warrior_1', weapon: 'weapon_base_0', head: 'head_base_0', shield: 'shield_base_0' }
expect(user.stats.gp).to.eql 1
it 'do not buy equipment without enough money', ->
user = newUser()
user.stats.gp = 1
user.ops.buy {params: {key: 'armor_warrior_1'}}
expect(user.items.gear.equipped).to.eql { armor: 'armor_base_0', weapon: 'weapon_base_0', head: 'head_base_0', shield: 'shield_base_0' }
expect(user.stats.gp).to.eql 1
describe 'drop system', ->
user = null
beforeEach ->
user = newUser()
user.flags.dropsEnabled = true
# too many predictableRandom calls to stub, let's return the last element
sinon.stub(user.fns, 'randomVal', (obj)->
result = undefined
for key, val of obj
result = val
result
)
@task_id = shared.uuid()
user.ops.addTask({body: {type: 'daily', id: @task_id}})
it 'gets a golden potion', ->
sinon.stub(user.fns, 'predictableRandom').returns 0
user.ops.score {params: { id: @task_id, direction: 'up'}}
expect(user.items.eggs).to.eql {}
expect(user.items.hatchingPotions).to.eql {'Golden': 1}
expect(user.items.food).to.eql {}
it 'gets a bear cub egg', ->
sinon.stub(user.fns, 'predictableRandom', cycle [0, 0, 0.55])
user.ops.score {params: { id: @task_id, direction: 'up'}}
expect(user.items.eggs).to.eql {'BearCub': 1}
expect(user.items.hatchingPotions).to.eql {}
expect(user.items.food).to.eql {}
it 'gets honey', ->
sinon.stub(user.fns, 'predictableRandom', cycle [0, 0, 0.9])
user.ops.score {params: { id: @task_id, direction: 'up'}}
expect(user.items.eggs).to.eql {}
expect(user.items.hatchingPotions).to.eql {}
expect(user.items.food).to.eql {'Honey': 1}
it 'does not get a drop', ->
sinon.stub(user.fns, 'predictableRandom').returns 0.5
user.ops.score {params: { id: @task_id, direction: 'up'}}
expect(user.items.eggs).to.eql {}
expect(user.items.hatchingPotions).to.eql {}
expect(user.items.food).to.eql {}
afterEach ->
user.fns.randomVal.restore()
user.fns.predictableRandom.restore()
describe 'Simple Scoring', ->
beforeEach ->
{@before, @after} = beforeAfter()
it 'Habits : Up', ->
@after.ops.score {params: {id: @after.habits[0].id, direction: 'down'}, query: {times: 5}}
expectLostPoints(@before, @after,'habit')
it 'Habits : Down', ->
@after.ops.score {params: {id: @after.habits[0].id, direction: 'up'}, query: {times: 5}}
expectGainedPoints(@before, @after,'habit')
it 'Dailys : Up', ->
@after.ops.score {params: {id: @after.dailys[0].id, direction: 'up'}}
expectGainedPoints(@before, @after,'daily')
it 'Todos : Up', ->
@after.ops.score {params: {id: @after.todos[0].id, direction: 'up'}}
expectGainedPoints(@before, @after,'todo')
describe 'Cron', ->
it 'computes shouldCron', ->
user = newUser()
paths = {};user.fns.cron {paths}
expect(user.lastCron).to.not.be.ok # it setup the cron property now
user.lastCron = +moment().subtract('days',1)
paths = {};user.fns.cron {paths}
expect(user.lastCron).to.be.greaterThan 0
# user.lastCron = +moment().add('days',1)
# paths = {};algos.cron user, {paths}
# expect(paths.lastCron).to.be true # busted cron (was set to after today's date)
it 'only dailies & todos are effected', ->
{before,after} = beforeAfter({hoursAgo:24})
before.dailys = before.todos = after.dailys = after.todos = []
after.fns.cron()
expect(after.lastCron).to.not.be before.lastCron # make sure cron was run
expect(before.stats).to.eql after.stats
beforeTasks = before.habits.concat(before.dailys).concat(before.todos).concat(before.rewards)
afterTasks = after.habits.concat(after.dailys).concat(after.todos).concat(after.rewards)
expect(beforeTasks).to.eql afterTasks
describe 'preening', ->
beforeEach ->
@clock = sinon.useFakeTimers(Date.parse("2013-11-20"), "Date");
afterEach ->
@clock.restore()
it 'should preen user history', ->
{before,after} = beforeAfter({hoursAgo:24})
history = [
# Last year should be condensed to one entry, avg: 1
{date:'09/01/2012', value: 0}
{date:'10/01/2012', value: 0}
{date:'11/01/2012', value: 2}
{date:'12/01/2012', value: 2}
# Each month of this year should be condensed to 1/mo, averages follow
{date:'01/01/2013', value: 1} #2
{date:'01/15/2013', value: 3}
{date:'02/01/2013', value: 2} #3
{date:'02/15/2013', value: 4}
{date:'03/01/2013', value: 3} #4
{date:'03/15/2013', value: 5}
{date:'04/01/2013', value: 4} #5
{date:'04/15/2013', value: 6}
{date:'05/01/2013', value: 5} #6
{date:'05/15/2013', value: 7}
{date:'06/01/2013', value: 6} #7
{date:'06/15/2013', value: 8}
{date:'07/01/2013', value: 7} #8
{date:'07/15/2013', value: 9}
{date:'08/01/2013', value: 8} #9
{date:'08/15/2013', value: 10}
{date:'09/01/2013', value: 9} #10
{date:'09/15/2013', value: 11}
{date:'010/01/2013', value: 10} #11
{date:'010/15/2013', value: 12}
# This month should condense each week
{date:'011/01/2013', value: 12}
{date:'011/02/2013', value: 13}
{date:'011/03/2013', value: 14}
{date:'011/04/2013', value: 15}
]
after.history = {exp: _.cloneDeep(history), todos: _.cloneDeep(history)}
after.habits[0].history = _.cloneDeep(history)
after.fns.cron()
# remove history entries created by cron
after.history.exp.pop()
after.history.todos.pop()
_.each [after.history.exp, after.history.todos, after.habits[0].history], (arr) ->
expect(_.map(arr, (x)->x.value)).to.eql [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
describe 'Todos', ->
it '1 day missed', ->
{before,after} = beforeAfter({hoursAgo:24})
before.dailys = after.dailys = []
after.fns.cron()
# todos don't effect stats
expect(after.stats.hp).to.be 50
expect(after.stats.exp).to.be 0
expect(after.stats.gp).to.be 0
# but they devalue
expect(after.todos[0].value).to.be.lessThan before.todos[0].value
expect(after.history.todos).to.have.length 1
describe 'dailies', ->
describe 'new day', ->
###
This section runs through a "cron matrix" of all permutations (that I can easily account for). It sets
task due days, user custom day start, timezoneOffset, etc - then runs cron, jumps to tomorrow and runs cron,
and so on - testing each possible outcome along the way
###
runCron = (options) ->
_.each [480, 240, 0, -120], (timezoneOffset) -> # test different timezones
now = shared.startOfWeek({timezoneOffset}).add('hours', options.currentHour||0)
{before,after} = beforeAfter({now, timezoneOffset, hoursAgo:options.hoursAgo||0, dayStart:options.dayStart||0, limitOne:'daily'})
before.dailys[0].repeat = after.dailys[0].repeat = options.repeat if options.repeat
before.dailys[0].streak = after.dailys[0].streak = 10
before.dailys[0].completed = after.dailys[0].completed = true if options.checked
if options.shouldDo
expect(shared.shouldDo(now, options.repeat, {timezoneOffset, dayStart:options.dayStart, now})).to.be.ok()
after.fns.cron {now}
switch options.expect
when 'losePoints' then expectLostPoints(before,after,'daily')
when 'noChange' then expectNoChange(before,after)
when 'noDamage' then expectDayResetNoDamage(before,after)
{before,after}
cronMatrix =
steps:
'due yesterday':
defaults: {hoursAgo:24, limitOne: 'daily'}
steps:
'(simple)': {expect:'losePoints'}
'due today':
# NOTE: a strange thing here, moment().startOf('week') is Sunday, but moment.zone(myTimeZone).startOf('week') is Monday.
defaults: {repeat:{su:1,m:true,t:1,w:1,th:1,f:1,s:1}}
steps:
'pre-dayStart':
defaults: {hoursAgo:2,currentHour:3, dayStart:4, shouldDo:true}
steps:
'checked': {checked: true, expect:'noChange'}
'un-checked': {checked: false, expect:'noChange'}
'post-dayStart':
defaults: {currentHour:5, dayStart:4, shouldDo:true}
steps:
'checked': {checked:true, expect:'noDamage'}
'unchecked': {checked:false, expect: 'losePoints'}
'NOT due today':
defaults: {repeat:{su:1,m:false,t:1,w:1,th:1,f:1,s:1}}
steps:
'pre-dayStart':
defaults: {hoursAgo:23, currentHour:3, dayStart:4, shouldDo:true}
steps:
'checked': {checked: true, expect:'noChange'}
'un-checked': {checked: false, expect:'noChange'}
'post-dayStart':
defaults: {currentHour:5, dayStart:4, shouldDo:false}
steps:
'checked': {checked:true, expect:'noDamage'}
'unchecked': {checked:false, expect: 'losePoints'}
'not due yesterday':
defaults: repeatWithoutLastWeekday()
steps:
'(simple)': {hoursAgo:24, expect:'noDamage'}
'post-dayStart': {hoursAgo:23,currentHour:5,dayStart:4, expect:'noDamage'}
'pre-dayStart': {hoursAgo:21, currentHour:3, dayStart:4, expect:'noChange'}
recurseCronMatrix = (obj, options={}) ->
if obj.steps
_.each obj.steps, (step, text) ->
o = _.cloneDeep options
o.text ?= ''; o.text += " #{text} "
recurseCronMatrix step, _.assign(o, obj.defaults)
else
it "#{options.text}", -> runCron(_.assign(options, obj))
recurseCronMatrix(cronMatrix)
it 'calculates day differences with dayStart properly', ->
dayStart = 4
yesterday = shared.startOfDay {now: moment().subtract('d', 1), dayStart}
now = shared.startOfDay {dayStart: dayStart-1}
expect(shared.daysSince(yesterday, {now, dayStart})).to.eql 0
now = moment().startOf('day').add('h', dayStart).add('m', 1)
expect(shared.daysSince(yesterday, {now, dayStart})).to.eql 1
describe 'Helper', ->
it 'calculates gold coins', ->
expect(shared.gold(10)).to.eql 10
expect(shared.gold(1.957)).to.eql 1
expect(shared.gold()).to.eql 0
it 'calculates silver coins', ->
expect(shared.silver(10)).to.eql 0
expect(shared.silver(1.957)).to.eql 95
expect(shared.silver(0.01)).to.eql "01"
expect(shared.silver()).to.eql "00"
it 'calculates experience to next level', ->
expect(shared.tnl 1).to.eql 150
expect(shared.tnl 2).to.eql 160
expect(shared.tnl 10).to.eql 260
expect(shared.tnl 99).to.eql 3580
expect(shared.tnl 100).to.eql 0
it 'calculates the start of the day', ->
expect(shared.startOfDay({now: new Date(2013, 0, 1, 0)}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 00:00'
expect(shared.startOfDay({now: new Date(2013, 0, 1, 5)}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 00:00'
expect(shared.startOfDay({now: new Date(2013, 0, 1, 23, 59, 59)}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 00:00'
# between midnight and custom day start
expect(shared.startOfDay({now: new Date(2013, 0, 2, 2), dayStart: 4}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 04:00'
it 'counts pets', ->
pets = {}
expect(shared.countPets(null, pets)).to.eql 0
expect(shared.countPets(1, pets)).to.eql 1
pets = { "Dragon-Red": 1, "Wolf-Base": 2 }
expect(shared.countPets(null, pets)).to.eql 2
expect(shared.countPets(2, pets)).to.eql 2
pets = { "Wolf-Base": 2, "Wolf-Veteran": 1, "Wolf-Cerberus": 1, "Dragon-Hydra": 1}
expect(shared.countPets(null, pets)).to.eql 1
expect(shared.countPets(_.size(pets), pets)).to.eql 1