setup for async cron

This commit is contained in:
Tyler Renelle 2012-09-24 16:19:27 -04:00
parent baf689a8d1
commit 13a7019413
4 changed files with 200 additions and 127 deletions

View file

@ -1,5 +1,7 @@
// Generated by CoffeeScript 1.3.3
var MODIFIER, content, cron, expModifier, helpers, hpModifier, model, moment, score, setModel, setupNotifications, tally, updateStats, user;
var MODIFIER, async, content, cron, expModifier, helpers, hpModifier, model, moment, score, setModel, setupNotifications, updateStats, user;
async = require('async');
moment = require('moment');
@ -122,17 +124,20 @@ updateStats = function(stats) {
}
};
score = function(taskId, direction, cron) {
var adjustvalue, delta, exp, hp, lvl, modified, money, num, sign, task, taskObj, taskPath, type, userObj, value, _ref, _ref1, _ref2, _ref3;
if (cron == null) {
cron = false;
score = function(taskId, direction, options) {
var adjustvalue, delta, exp, hp, lvl, modified, money, num, sign, task, taskObj, taskPath, type, userObj, value, _ref, _ref1, _ref2;
if (options == null) {
options = {
cron: false,
times: 1
};
}
taskPath = "_user.tasks." + taskId;
_ref = [model.at(taskPath), model.get(taskPath)], task = _ref[0], taskObj = _ref[1];
_ref1 = [taskObj.type, taskObj.value], type = _ref1[0], value = _ref1[1];
type = taskObj.type, value = taskObj.value;
userObj = user.get();
if (!task) {
_ref2 = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp], money = _ref2[0], hp = _ref2[1], exp = _ref2[2];
_ref1 = userObj.stats, money = _ref1.money, hp = _ref1.hp, exp = _ref1.exp;
if (direction === "up") {
modified = expModifier(1);
money += modified;
@ -157,6 +162,7 @@ score = function(taskId, direction, cron) {
if (adjustvalue) {
value += delta;
}
value *= options.times;
if (type === 'habit') {
if (taskObj.value !== value) {
task.push('history', {
@ -166,7 +172,7 @@ score = function(taskId, direction, cron) {
}
}
task.set('value', value);
_ref3 = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp, userObj.stats.lvl], money = _ref3[0], hp = _ref3[1], exp = _ref3[2], lvl = _ref3[3];
_ref2 = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp, userObj.stats.lvl], money = _ref2[0], hp = _ref2[1], exp = _ref2[2], lvl = _ref2[3];
if (type === 'reward') {
money -= task.get('value');
num = parseFloat(task.get('value')).toFixed(2);
@ -175,7 +181,7 @@ score = function(taskId, direction, cron) {
money = 0;
}
}
if ((delta > 0 || (type === 'daily' || type === 'todo')) && !cron) {
if ((delta > 0 || (type === 'daily' || type === 'todo')) && !options.cron) {
modified = expModifier(delta);
exp += modified;
money += modified;
@ -192,78 +198,79 @@ score = function(taskId, direction, cron) {
};
cron = function() {
var daysPassed, lastCron, today;
var daysPassed, lastCron, tallyTask, tasks, today, todoTally;
today = moment().sod();
user.setNull('lastCron', today.toDate());
lastCron = moment(user.get('lastCron'));
daysPassed = today.diff(lastCron, 'days');
if (daysPassed > 0) {
user.set('lastCron', today.toDate());
return _.times(daysPassed, function(n) {
var tallyFor;
tallyFor = lastCron.add('d', n);
return tally(tallyFor);
});
}
};
tally = function(momentDate) {
var expTally, lvl, todoTally;
todoTally = 0;
_.each(user.get('tasks'), function(taskObj, taskId, list) {
var absVal, completed, dayMapping, dueToday, repeat, task, type, value, _ref;
if (taskObj.id == null) {
return;
}
_ref = [taskObj.type, taskObj.value, taskObj.completed, taskObj.repeat], type = _ref[0], value = _ref[1], completed = _ref[2], repeat = _ref[3];
task = user.at("tasks." + taskId);
if (type === 'todo' || type === 'daily') {
if (!completed) {
dayMapping = {
0: 'su',
1: 'm',
2: 't',
3: 'w',
4: 'th',
5: 'f',
6: 's',
7: 'su'
};
dueToday = repeat && repeat[dayMapping[momentDate.day()]] === true;
if (dueToday || type === 'todo') {
score(taskId, 'down', true);
todoTally = 0;
tallyTask = function(taskObj, next) {
var absVal, completed, dayMapping, daysFailed, dueToday, id, repeat, task, type, value;
id = taskObj.id, type = taskObj.type, completed = taskObj.completed, repeat = taskObj.repeat;
if (id == null) {
return;
}
task = user.at("tasks." + id);
if (type === 'todo' || type === 'daily') {
if (!completed) {
daysFailed = daysPassed;
if (type === 'daily' && repeat) {
dayMapping = {
0: 'su',
1: 'm',
2: 't',
3: 'w',
4: 'th',
5: 'f',
6: 's',
7: 'su'
};
dueToday = repeat && repeat[dayMapping[momentDate.day()]] === true;
}
score(taskId, 'down', {
cron: true,
times: daysFailed
});
}
value = task.get('value');
if (type === 'daily') {
task.push("history", {
date: today.toDate(),
value: value
});
} else {
absVal = completed ? Math.abs(value) : value;
todoTally += absVal;
}
if (type === 'daily') {
task.pass({
cron: true
}).set('completed', false);
}
}
if (type === 'daily') {
task.push("history", {
date: new Date(momentDate),
value: value
});
} else {
absVal = completed ? Math.abs(value) : value;
todoTally += absVal;
return next();
};
tasks = _.toArray(user.get('tasks'));
return async.forEach(tasks, tallyTask, function(err) {
var expTally, lvl;
user.push('history.todos', {
date: today.toDate(),
value: todoTally
});
expTally = user.get('stats.exp');
lvl = 0;
while (lvl < (user.get('stats.lvl') - 1)) {
lvl++;
expTally += (lvl * 100) / 5;
}
if (type === 'daily') {
return task.pass({
cron: true
}).set('completed', false);
}
}
});
user.push('history.todos', {
date: new Date(momentDate),
value: todoTally
});
expTally = user.get('stats.exp');
lvl = 0;
while (lvl < (user.get('stats.lvl') - 1)) {
lvl++;
expTally += (lvl * 100) / 5;
user.push('history.exp', {
date: today.toDate(),
value: expTally
});
return user.set('lastCron', today.toDate());
});
}
return user.push('history.exp', {
date: new Date(),
value: expTally
});
};
module.exports = {

View file

@ -14,7 +14,9 @@
"guid": "*",
"node.extend": "*",
"moment": "*",
"stripe": "*"
"stripe": "*",
"async": "*",
"lodash": "*"
},
"private": true,
"devDependencies": {

View file

@ -1,3 +1,4 @@
async = require 'async'
moment = require 'moment'
content = require './content'
helpers = require './helpers'
@ -107,17 +108,17 @@ updateStats = (stats) ->
# {taskId} task you want to score
# {direction} 'up' or 'down'
# {cron} is this function being called by cron? (this will usually be false)
score = (taskId, direction, cron=false) ->
score = (taskId, direction, options={cron:false, times:1}) ->
taskPath = "_user.tasks.#{taskId}"
[task, taskObj] = [model.at(taskPath), model.get(taskPath)]
[type, value] = [taskObj.type, taskObj.value]
{type, value} = taskObj
userObj = user.get()
# up / down was called by itself, probably as REST from 3rd party service
#FIXME handle this
if !task
[money, hp, exp] = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp]
{money, hp, exp} = userObj.stats
if (direction == "up")
modified = expModifier(1)
money += modified
@ -140,6 +141,9 @@ score = (taskId, direction, cron=false) ->
if (type == 'habit') and (taskObj.up==false or taskObj.down==false)
adjustvalue = false
value += delta if adjustvalue
# If multiple days have passed, multiply times days missed
value *= options.times
if type == 'habit'
# Add habit value to habit-history (if different)
@ -160,7 +164,7 @@ score = (taskId, direction, cron=false) ->
# Add points to exp & money if positive delta
# Only take away mony if it was a mistake (aka, a checkbox)
if (delta > 0 or (type in ['daily', 'todo'])) and !cron
if (delta > 0 or (type in ['daily', 'todo'])) and !options.cron
modified = expModifier(delta)
exp += modified
money += modified
@ -172,50 +176,58 @@ score = (taskId, direction, cron=false) ->
updateStats({hp: hp, exp: exp, money: money})
return delta
# At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
# For incomplete Dailys, deduct experience
cron = ->
today = moment().sod() # start of day
user.setNull 'lastCron', today.toDate()
lastCron = moment(user.get('lastCron'))
daysPassed = today.diff(lastCron, 'days')
if daysPassed > 0
user.set('lastCron', today.toDate()) # reset cron
_.times daysPassed, (n) ->
tallyFor = lastCron.add('d',n)
tally(tallyFor)
# Tally function, which is called asyncronously below - but function is defined here.
# We need access to some closure variables above
todoTally = 0
tallyTask = (taskObj, next) ->
{id, type, completed, repeat} = taskObj
return unless id? #this shouldn't be happening, some tasks seem to be corrupted
task = user.at("tasks.#{id}")
if type in ['todo', 'daily']
# Deduct experience for missed Daily tasks,
# but not for Todos (just increase todo's value)
unless completed
# for todos & typical dailies, these are equivalent
daysFailed = daysPassed
# however, for dailys which have repeat dates, need
# to calculate how many they've missed according to their own schedule
if type=='daily' && repeat
dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s',7:'su'}
dueToday = (repeat && repeat[dayMapping[momentDate.day()]]==true)
score(taskId, 'down', {cron:true, times: daysFailed})
# At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
# For incomplete Dailys, deduct experience
tally = (momentDate) ->
todoTally = 0
_.each user.get('tasks'), (taskObj, taskId, list) ->
#FIXME is it hiccuping here? taskId == "$_65255f4e-3728-4d50-bade-3b05633639af_2", & taskObj.id = undefined
return unless taskObj.id? #this shouldn't be happening, some tasks seem to be corrupted
[type, value, completed, repeat] = [taskObj.type, taskObj.value, taskObj.completed, taskObj.repeat]
task = user.at("tasks.#{taskId}")
if type in ['todo', 'daily']
# Deduct experience for missed Daily tasks,
# but not for Todos (just increase todo's value)
unless completed
dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s',7:'su'}
dueToday = (repeat && repeat[dayMapping[momentDate.day()]]==true)
if dueToday or type=='todo'
score(taskId, 'down', true)
if type == 'daily'
task.push "history", { date: new Date(momentDate), value: value }
else
absVal = if (completed) then Math.abs(value) else value
todoTally += absVal
task.pass({cron:true}).set('completed', false) if type == 'daily'
user.push 'history.todos', { date: new Date(momentDate), value: todoTally }
# tally experience
expTally = user.get 'stats.exp'
lvl = 0 #iterator
while lvl < (user.get('stats.lvl')-1)
lvl++
expTally += (lvl*100)/5
user.push 'history.exp', { date: new Date(), value: expTally }
value = task.get('value') #get updated value
if type == 'daily'
task.push "history", { date: today.toDate(), value: value }
else
absVal = if (completed) then Math.abs(value) else value
todoTally += absVal
task.pass({cron:true}).set('completed', false) if type == 'daily'
next()
# Tally each task
tasks = _.toArray(user.get('tasks'))
async.forEach tasks, tallyTask, (err) ->
# Finished tallying, this is the 'completed' callback
user.push 'history.todos', { date: today.toDate(), value: todoTally }
# tally experience
expTally = user.get 'stats.exp'
lvl = 0 #iterator
while lvl < (user.get('stats.lvl')-1)
lvl++
expTally += (lvl*100)/5
user.push 'history.exp', { date: today.toDate(), value: expTally }
user.set('lastCron', today.toDate()) # reset cron
module.exports = {

View file

@ -5,14 +5,23 @@ derby = require 'derby'
# Custom modules
scoring = require '../src/app/scoring'
schema = require '../src/app/schema'
_ = require '../public/js/underscore-min'
_ = require 'lodash'
moment = require 'moment'
###### Helper Functions ######
modifictionLookup = (value, direction) ->
#TODO implement a lookup table to test if user stats & task value has been modified properly
###### Specs ######
describe 'User', ->
model = null
## Helper which clones the content at a path so tests can compare before/after values
pathSnapshots = (paths) ->
_.map paths, (path) -> _.clone(model.get(path))
beforeEach ->
model = new Model
model.set '_user', schema.newUserObject()
@ -35,11 +44,18 @@ describe 'User', ->
uuid = null
taskPath = null
before ->
# Reset tasks
model.set '_user.tasks', {}
model.set '_user.habitIds', []
model.set '_user.dailyIds', []
model.set '_user.todoIds', []
model.set '_user.rewardIds', []
describe 'Habits', ->
beforeEach ->
# create a test task
user = model.get('_user')
uuid = derby.uuid()
taskPath = "_user.tasks.#{uuid}"
model.refList "_habitList", "_user.tasks", "_user.habitIds"
@ -106,27 +122,63 @@ describe 'User', ->
it 'should show "undo" notification if user unchecks completed daily'
describe 'Dailies', ->
describe 'Dailies', ->
beforeEach ->
# create a test task
uuid = derby.uuid()
taskPath = "_user.tasks.#{uuid}"
model.refList "_dailyList", "_user.tasks", "_user.dailyIds"
model.at('_dailyList').push {type: 'daily', text: 'Daily', value: 0, completed: false, id: uuid }
it 'created the daily', ->
task = model.get(taskPath)
expect(task.text).to.eql 'Daily'
expect(task.value).to.eql 0
it 'does proper calculations when daily is complete'
it 'calculates user.stats & task.value properly on cron', ->
[statsBefore, taskBefore] = pathSnapshots(['_user.stats', taskPath])
# Set lastCron to yesterday
today = moment()
model.set '_user.lastCron', today.subtract('days',1).toDate()
# Run run
scoring.cron()
[statsAfter, taskAfter] = pathSnapshots(['_user.stats', taskPath])
# Should have updated cron to today
lastCron = moment(model.get('_user.lastCron'))
expect(today.diff(lastCron, 'days')).to.eql 0
# Should have updated points properly
expect(statsBefore.hp).to.be.lessThan statsAfter.hp
expect(taskBefore.value).to.eql 0
expect(taskAfter.value).to.eql -1
#TODO clicking repeat dates on newly-created item doesn't refresh until you refresh the page
#TODO dates on dailies is having issues, possibility: date cusps? my saturday exempts were set to exempt at 8pm friday
#TODO refactor as user->habits, user->dailys, user->todos, user->rewards
describe 'Todos', ->
describe 'Cron', ->
it 'should calculate user.stats & task.value properly on cron'
it 'should calculate cron based on difference between start-of-days, and not run in the middle of the day'
it 'should only run set operations once per task, even when daysPassed > 1'
# pass in daysPassed to score, multiply modification values by daysPassed before running set
it 'should only push a history point for lastCron, not each day in between'
# stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score()
it 'should defer saving user modifications until, save as aggregate values'
# pass in commit parameter to scoring func, if true save right away, otherwise return aggregated array so can save in the end (so total hp loss, etc)
describe 'Rewards', ->
describe 'Lvl & Items', ->
it 'modified damage based on lvl & armor'
it 'always decreases hp with damage, regardless of stats/items'
it 'always increases exp/gp with gain, regardless of stats/items'
describe 'Cron', ->
it 'should calculate user.stats & task.value properly on cron'
it 'should calculate cron based on difference between start-of-days, and not run in the middle of the day'
it 'should only run set operations once per task, even when daysPassed > 1'
# pass in daysPassed to score, multiply modification values by daysPassed before running set
it 'should only push a history point for lastCron, not each day in between'
# stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score()
it 'should defer saving user modifications until, save as aggregate values'
# pass in commit parameter to scoring func, if true save right away, otherwise return aggregated array so can save in the end (so total hp loss, etc)
#### Require.js stuff, might be necessary to place in casper.coffee
it "doesn't setup dependent functions until their modules are loaded, require.js callback"
# sortable, stripe, etc
#TODO refactor as user->habits, user->dailys, user->todos, user->rewards