mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-04-14 19:47:03 +00:00
scoring function refactoring & testing
This commit is contained in:
parent
163395829a
commit
baf689a8d1
5 changed files with 105 additions and 84 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue