scoring function refactoring & testing

This commit is contained in:
Tyler Renelle 2012-09-24 12:52:44 -04:00
parent 163395829a
commit baf689a8d1
5 changed files with 105 additions and 84 deletions

View file

@ -39,14 +39,14 @@ get('/:uidParam?', function(page, model, _arg, next) {
potion: content.items.potion,
reroll: content.items.reroll
});
model.fn('_user._tnl', '_user.stats.lvl', function(lvl) {
return (lvl * 100) / 5;
});
model.refList("_habitList", "_user.tasks", "_user.habitIds");
model.refList("_dailyList", "_user.tasks", "_user.dailyIds");
model.refList("_todoList", "_user.tasks", "_user.todoIds");
model.refList("_completedList", "_user.tasks", "_user.completedIds");
model.refList("_rewardList", "_user.tasks", "_user.rewardIds");
model.fn('_user._tnl', '_user.stats.lvl', function(lvl) {
return (lvl * 100) / 5;
});
return page.render();
});
});
@ -102,7 +102,7 @@ ready(function(model) {
}
tour.start();
model.on('set', '_user.tasks.*.completed', function(i, completed, previous, isLocal, passed) {
var direction, from, fromIds, task, to, toIds, _ref3, _ref4;
var direction, from, fromIds, to, toIds, _ref3, _ref4;
if ((passed != null) && passed.cron) {
return;
}
@ -115,11 +115,7 @@ ready(function(model) {
}
throw new Error("Direction neither 'up' nor 'down' on checkbox set.");
};
task = model.at("_user.tasks." + i);
scoring.score({
task: task,
direction: direction()
});
scoring.score(i, direction());
if (task.get('type') === 'todo') {
_ref3 = direction() === 'up' ? ['todo', 'completed'] : ['completed', 'todo'], from = _ref3[0], to = _ref3[1];
_ref4 = ["_user." + from + "Ids", "_user." + to + "Ids"], from = _ref4[0], to = _ref4[1];
@ -195,8 +191,7 @@ ready(function(model) {
return;
} else {
task.set('type', 'habit');
scoring.score({
task: task,
scoring.score(task.get('id'), {
direction: 'down'
});
}
@ -290,10 +285,7 @@ ready(function(model) {
direction = 'down';
}
task = model.at($(el).parents('li')[0]);
return scoring.score({
task: task,
direction: direction
});
return scoring.score(task.get('id'), direction);
};
exports.revive = function(e, el) {
var stats;

View file

@ -1,5 +1,7 @@
// Generated by CoffeeScript 1.3.3
var MODIFIER, content, cron, expModifier, helpers, hpModifier, model, score, setModel, setupNotifications, tally, updateStats, user;
var MODIFIER, content, cron, expModifier, helpers, hpModifier, model, moment, score, setModel, setupNotifications, tally, updateStats, user;
moment = require('moment');
content = require('./content');
@ -120,18 +122,17 @@ updateStats = function(stats) {
}
};
score = function(spec) {
var adjustvalue, cron, delta, direction, exp, hp, lvl, modified, money, num, sign, task, type, value, _ref, _ref1, _ref2;
if (spec == null) {
spec = {
task: null,
direction: null,
cron: null
};
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;
}
_ref = [spec.task, spec.direction, spec.cron], task = _ref[0], direction = _ref[1], cron = _ref[2];
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];
userObj = user.get();
if (!task) {
_ref1 = [user.get('stats.money'), user.get('stats.hp'), user.get('stats.exp')], money = _ref1[0], hp = _ref1[1], exp = _ref1[2];
_ref2 = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp], money = _ref2[0], hp = _ref2[1], exp = _ref2[2];
if (direction === "up") {
modified = expModifier(1);
money += modified;
@ -148,26 +149,24 @@ score = function(spec) {
return;
}
sign = direction === "up" ? 1 : -1;
value = task.get('value');
delta = value < 0 ? (-0.1 * value + 1) * sign : (Math.pow(0.9, value)) * sign;
type = task.get('type');
adjustvalue = type !== 'reward';
if ((type === 'habit') && (task.get("up") === false || task.get("down") === false)) {
if ((type === 'habit') && (taskObj.up === false || taskObj.down === false)) {
adjustvalue = false;
}
if (adjustvalue) {
value += delta;
}
if (type === 'habit') {
if (task.get('value') !== value) {
if (taskObj.value !== value) {
task.push('history', {
date: new Date(),
date: moment().sod().toDate(),
value: value
});
}
}
task.set('value', value);
_ref2 = [user.get('stats.money'), user.get('stats.hp'), user.get('stats.exp'), user.get('stats.lvl')], money = _ref2[0], hp = _ref2[1], exp = _ref2[2], lvl = _ref2[3];
_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];
if (type === 'reward') {
money -= task.get('value');
num = parseFloat(task.get('value')).toFixed(2);
@ -232,11 +231,7 @@ tally = function(momentDate) {
};
dueToday = repeat && repeat[dayMapping[momentDate.day()]] === true;
if (dueToday || type === 'todo') {
score({
task: task,
direction: 'down',
cron: true
});
score(taskId, 'down', true);
}
}
if (type === 'daily') {

View file

@ -12,6 +12,7 @@ helpers.viewHelpers(view)
# $ = require('jQuery')
# und = require('underscore') # node.js uses _
# ========== ROUTES ==========
get '/:uidParam?', (page, model, {uidParam}, next) ->
@ -23,36 +24,41 @@ get '/:uidParam?', (page, model, {uidParam}, next) ->
model.set('_facebookAuthenticated', true)
model.set '_userId', sess.userId
model.subscribe "users.#{sess.userId}", (err, user) ->
# Set variables which are passed from the controller to the view
model.ref '_user', user
#FIXME remove this eventually, part of user schema
user.setNull 'balance', 2
# Store
# Setup Item Store
model.set '_items'
armor: content.items.armor[parseInt(user.get('items.armor')) + 1]
weapon: content.items.weapon[parseInt(user.get('items.weapon')) + 1]
potion: content.items.potion
reroll: content.items.reroll
model.fn '_user._tnl', '_user.stats.lvl', (lvl) ->
# see https://github.com/lefnire/habitrpg/issues/4
# also update in scoring.coffee. TODO create a function accessible in both locations
(lvl*100)/5
# Default Tasks
# Setup Task Lists
model.refList "_habitList", "_user.tasks", "_user.habitIds"
model.refList "_dailyList", "_user.tasks", "_user.dailyIds"
model.refList "_todoList", "_user.tasks", "_user.todoIds"
model.refList "_completedList", "_user.tasks", "_user.completedIds"
model.refList "_rewardList", "_user.tasks", "_user.rewardIds"
# Setup Model Functions
model.fn '_user._tnl', '_user.stats.lvl', (lvl) ->
# see https://github.com/lefnire/habitrpg/issues/4
# also update in scoring.coffee. TODO create a function accessible in both locations
(lvl*100)/5
# Render Page
page.render()
# ========== CONTROLLER FUNCTIONS ==========
ready (model) ->
# Setup model in scoring functions
scoring.setModel(model)
$('[rel=tooltip]').tooltip()
$('[rel=popover]').popover()
# FIXME: this isn't very efficient, do model.on set for specific attrs for popover
@ -101,8 +107,8 @@ ready (model) ->
throw new Error("Direction neither 'up' nor 'down' on checkbox set.")
# Score the user based on todo task
task = model.at("_user.tasks.#{i}")
scoring.score({task:task, direction:direction()})
# task = model.at("_user.tasks.#{i}")
scoring.score(i, direction())
# Then move the todos to/from _todoList/_completedList
if task.get('type') == 'todo'
@ -155,7 +161,7 @@ ready (model) ->
return # Cancel. Don't delete, don't hurt user
else
task.set('type','habit') # hack to make sure it hits HP, instead of performing "undo checkbox"
scoring.score({task:task, direction:'down'})
scoring.score(task.get('id'), direction:'down')
# prevent accidently deleting long-standing tasks
else
@ -236,7 +242,7 @@ ready (model) ->
direction = 'up' if direction == 'true/'
direction = 'down' if direction == 'false/'
task = model.at $(el).parents('li')[0]
scoring.score({task:task, direction:direction})
scoring.score(task.get('id'), direction)
exports.revive = (e, el) ->
stats = model.at '_user.stats'

View file

@ -1,5 +1,6 @@
content = require('./content')
helpers = require('./helpers')
moment = require 'moment'
content = require './content'
helpers = require './helpers'
MODIFIER = .03 # each new level, armor, weapon add 3% modifier (this number may change)
user = undefined
model = undefined
@ -103,12 +104,20 @@ updateStats = (stats) ->
money = 0.0 if (!money? or money<0)
user.set 'stats.money', stats.money
score = (spec = {task:null, direction:null, cron:null}) ->
[task, direction, cron] = [spec.task, spec.direction, spec.cron]
# {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) ->
taskPath = "_user.tasks.#{taskId}"
[task, taskObj] = [model.at(taskPath), model.get(taskPath)]
[type, value] = [taskObj.type, taskObj.value]
userObj = user.get()
# up / down was called by itself, probably as REST from 3rd party service
#FIXME handle this
if !task
[money, hp, exp] = [user.get('stats.money'), user.get('stats.hp'), user.get('stats.exp')]
[money, hp, exp] = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp]
if (direction == "up")
modified = expModifier(1)
money += modified
@ -124,24 +133,21 @@ score = (spec = {task:null, direction:null, cron:null}) ->
# For positibe values, taper off with inverse log: y=.9^x
# Would love to use inverse log for the whole thing, but after 13 fails it hits infinity
sign = if (direction == "up") then 1 else -1
value = task.get('value')
delta = if (value < 0) then (( -0.1 * value + 1 ) * sign) else (( Math.pow(0.9,value) ) * sign)
type = task.get('type')
# Don't adjust values for rewards, or for habits that don't have both + and -
adjustvalue = (type != 'reward')
if (type == 'habit') and (task.get("up")==false or task.get("down")==false)
if (type == 'habit') and (taskObj.up==false or taskObj.down==false)
adjustvalue = false
value += delta if adjustvalue
if type == 'habit'
# Add habit value to habit-history (if different)
task.push 'history', { date: new Date(), value: value } if task.get('value') != value
task.push 'history', { date: moment().sod().toDate(), value: value } if taskObj.value != value
task.set('value', value)
# Update the user's status
[money, hp, exp, lvl] = [user.get('stats.money'), user.get('stats.hp'), user.get('stats.exp'), user.get('stats.lvl')]
[money, hp, exp, lvl] = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp, userObj.stats.lvl]
if type == 'reward'
# purchase item
@ -154,7 +160,7 @@ score = (spec = {task:null, direction:null, cron:null}) ->
# 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 !cron
modified = expModifier(delta)
exp += modified
money += modified
@ -194,7 +200,7 @@ tally = (momentDate) ->
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({task:task, direction:'down', cron:true})
score(taskId, 'down', true)
if type == 'daily'
task.push "history", { date: new Date(momentDate), value: value }
else

View file

@ -7,6 +7,9 @@ scoring = require '../src/app/scoring'
schema = require '../src/app/schema'
_ = require '../public/js/underscore-min'
modifictionLookup = (value, direction) ->
#TODO implement a lookup table to test if user stats & task value has been modified properly
describe 'User', ->
model = null
@ -48,33 +51,54 @@ describe 'User', ->
expect(task.value).to.eql 0
it 'made proper modifications when down-scored', ->
# Down-score the habit
[userBefore, taskBefore] = [model.get('_user'), model.get(taskPath)]
scoring.score({taskId:uuid, direction:'down'})
[userAfter, taskAfter] = [model.get('_user'), model.get(taskPath)]
# Setup 'before' objects for before/after comparisons
statsBefore = _.clone(model.get('_user.stats'))
taskBefore = _.clone(model.get(taskPath))
## Trial 1
scoring.score(uuid, 'down')
statsAfter = _.clone(model.get('_user.stats'))
taskAfter = _.clone(model.get(taskPath))
# User should have lost HP
expect(userAfter.stats.hp).to.be.lessThan userBefore.stats.hp
expect(statsAfter.hp).to.be.lessThan statsBefore.hp
# Exp, GP should stay the same
expect(userAfter.stats.money).to.eql userBefore.stats.money
expect(userAfter.stats.exp).to.eql userBefore.stats.exp
# Task should have gained in value
expect(taskAfter.value).to.be.greaterThan taskBefore.value
expect(statsAfter.money).to.eql statsBefore.money
expect(statsAfter.exp).to.eql statsBefore.exp
# Task should have gained in value (we're going down, so think Math.abs(task.value))
expect(taskBefore.value).to.eql 0
expect(taskAfter.value).to.eql -1
## Trial 2
taskBefore = _.clone(taskAfter)
scoring.score(uuid, 'down')
taskAfter = _.clone(model.get(taskPath))
# Should have gained in value
expect(taskAfter.value).to.be < taskBefore.value
# And gained more than trial 1
expect(Math.abs(taskAfter.value) - Math.abs(taskBefore.value)).to.be.greaterThan 1
it 'made proper modifications when up-scored', ->
# Up-score the habit
[userBefore, taskBefore] = [model.get('_user'), model.get(taskPath)]
statsBefore = _.clone(model.get('_user.stats'))
taskBefore = _.clone(model.get(taskPath))
scoring.score(uuid, 'up')
[userAfter, taskAfter] = [model.get('_user'), model.get(taskPath)]
statsAfter = _.clone(model.get('_user.stats'))
taskAfter = _.clone(model.get(taskPath))
# User should have gained Exp, GP
expect(userAfter.stats.exp).to.be.greaterThan userBefore.stats.exp
expect(userAfter.stats.money).to.be.greaterThan userBefore.stats.money
expect(statsAfter.exp).to.be.greaterThan statsBefore.exp
expect(statsAfter.money).to.be.greaterThan statsBefore.money
# HP should not change
expect(userAfter.stats.hp).to.eql userBefore.stats.hp
expect(statsAfter.hp).to.eql statsBefore.hp
# Task should have lost value
expect(taskAfter.value).to.be.lessThan taskBefore.value
expect(taskBefore.value).to.eql 0
expect(taskAfter.value).to.be.lessThan 1
it 'makes history entry for habit'
it 'makes proper modifications each time when clicking + / - in rapid succession'
# saw an issue here once, so test that it wasn't a fluke
it 'should not modify certain attributes given certain conditions'
# non up+down habits
@ -82,26 +106,24 @@ describe 'User', ->
it 'should show "undo" notification if user unchecks completed daily'
describe 'Dailies', ->
describe 'Dailies', ->
#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 'Lvl & Items', ->
it 'modified damage based on lvl & armor'
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)