From 65b3561fb04127c571082522924c8a0db7d3409b Mon Sep 17 00:00:00 2001 From: Brandon Reid Date: Wed, 1 Oct 2014 17:13:51 -0600 Subject: [PATCH 1/2] Changed calculateDelta() so when unchecking a task it removes the right amount of gold and exp --- dist/habitrpg-shared.js | 83 +++++++++++++++++++++++++-------------- script/index.coffee | 86 +++++++++++++++++++++++++++++------------ 2 files changed, 116 insertions(+), 53 deletions(-) diff --git a/dist/habitrpg-shared.js b/dist/habitrpg-shared.js index 21da91e0b0..95526bfe55 100644 --- a/dist/habitrpg-shared.js +++ b/dist/habitrpg-shared.js @@ -12738,7 +12738,6 @@ api.userDefaults = { }; - },{"./i18n.coffee":6,"lodash":3,"moment":4}],6:[function(require,module,exports){ var _; @@ -12783,7 +12782,6 @@ module.exports = { }; - },{"lodash":3}],7:[function(require,module,exports){ (function (process){ var $w, api, content, i18n, moment, preenHistory, sanitizeOptions, sortOrder, _, @@ -14137,7 +14135,7 @@ api.wrap = function(user, main) { return typeof cb === "function" ? cb(null, 'items.special') : void 0; }, score: function(req, cb) { - var addPoints, calculateDelta, delta, direction, id, mpDelta, multiplier, num, options, stats, subtractPoints, task, th, _ref; + var addPoints, calculateDelta, calculateReverseDelta, changeTaskValue, delta, direction, id, mpDelta, multiplier, num, options, stats, subtractPoints, task, th, _ref; _ref = req.params, id = _ref.id, direction = _ref.direction; task = user.tasks[id]; options = req.query || {}; @@ -14164,29 +14162,53 @@ api.wrap = function(user, main) { } delta = 0; calculateDelta = function() { - return _.times(options.times, function() { - var currVal, nextDelta, _ref1, _ref2; - currVal = task.value < -47.27 ? -47.27 : task.value > 21.27 ? 21.27 : task.value; - nextDelta = Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1); - if (((_ref1 = task.checklist) != null ? _ref1.length : void 0) > 0) { - if (direction === 'down' && task.type === 'daily' && options.cron) { - nextDelta *= 1 - _.reduce(task.checklist, (function(m, i) { - return m + (i.completed ? 1 : 0); - }), 0) / task.checklist.length; - } - if (task.type === 'todo') { - nextDelta *= 1 + _.reduce(task.checklist, (function(m, i) { - return m + (i.completed ? 1 : 0); - }), 0); - } + var currVal, nextDelta, _ref1; + currVal = task.value < -47.27 ? -47.27 : task.value > 21.27 ? 21.27 : task.value; + nextDelta = Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1); + if (((_ref1 = task.checklist) != null ? _ref1.length : void 0) > 0) { + if (direction === 'down' && task.type === 'daily' && options.cron) { + nextDelta *= 1 - _.reduce(task.checklist, (function(m, i) { + return m + (i.completed ? 1 : 0); + }), 0) / task.checklist.length; } + if (task.type === 'todo') { + nextDelta *= 1 + _.reduce(task.checklist, (function(m, i) { + return m + (i.completed ? 1 : 0); + }), 0); + } + } + return nextDelta; + }; + calculateReverseDelta = function() { + var calc, closeEnough, currVal, diff, testVal; + currVal = task.value < -47.27 ? -47.27 : task.value > 21.27 ? 21.27 : task.value; + testVal = currVal + Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1); + closeEnough = 0.0001; + while (true) { + calc = testVal + Math.pow(0.9747, testVal); + diff = currVal - calc; + if (Math.abs(diff) < closeEnough) { + break; + } + if (diff > 0) { + testVal -= diff; + } else { + testVal += diff; + } + } + return testVal - currVal; + }; + changeTaskValue = function(reverse) { + return _.times(options.times, function() { + var nextDelta, _ref1; + nextDelta = reverse ? calculateReverseDelta() : calculateDelta(); if (task.type !== 'reward') { if (user.preferences.automaticAllocation === true && user.preferences.allocationMode === 'taskbased' && !(task.type === 'todo' && direction === 'down')) { user.stats.training[task.attribute] += nextDelta; } if (direction === 'up' && !(task.type === 'habit' && !task.down)) { user.party.quest.progress.up = user.party.quest.progress.up || 0; - if ((_ref2 = task.type) === 'daily' || _ref2 === 'todo') { + if ((_ref1 = task.type) === 'daily' || _ref1 === 'todo') { user.party.quest.progress.up += nextDelta * (1 + (user._statsComputed.str / 200)); } } @@ -14218,7 +14240,7 @@ api.wrap = function(user, main) { }; switch (task.type) { case 'habit': - calculateDelta(); + changeTaskValue(); if (delta > 0) { addPoints(); } else { @@ -14241,13 +14263,16 @@ api.wrap = function(user, main) { break; case 'daily': if (options.cron) { - calculateDelta(); + changeTaskValue(); subtractPoints(); if (!user.stats.buffs.streaks) { task.streak = 0; } } else { - calculateDelta(); + changeTaskValue(delta < 0); + if (delta < 0) { + delta = calculateDelta(); + } addPoints(); if (direction === 'up') { task.streak = task.streak ? task.streak + 1 : 1; @@ -14264,10 +14289,13 @@ api.wrap = function(user, main) { break; case 'todo': if (options.cron) { - calculateDelta(); + changeTaskValue(); } else { task.dateCompleted = direction === 'up' ? new Date : void 0; - calculateDelta(); + changeTaskValue(delta < 0); + if (delta < 0) { + delta = calculateDelta(); + } addPoints(); multiplier = _.max([ _.reduce(task.checklist, (function(m, i) { @@ -14289,7 +14317,7 @@ api.wrap = function(user, main) { } break; case 'reward': - calculateDelta(); + changeTaskValue(); stats.gp -= Math.abs(task.value); num = parseFloat(task.value).toFixed(2); if (stats.gp < 0) { @@ -14843,6 +14871,5 @@ api.wrap = function(user, main) { }; - -}).call(this,require("/Users/lefnire/Dropbox/Sites/habitrpg/modules/habitrpg-shared/node_modules/browserify/node_modules/insert-module-globals/node_modules/process/browser.js")) -},{"./content.coffee":5,"./i18n.coffee":6,"/Users/lefnire/Dropbox/Sites/habitrpg/modules/habitrpg-shared/node_modules/browserify/node_modules/insert-module-globals/node_modules/process/browser.js":2,"lodash":3,"moment":4}]},{},[1]) \ No newline at end of file +}).call(this,require("/Users/brando37/Documents/Projects/habitrpg/node_modules/habitrpg-shared/node_modules/browserify/node_modules/insert-module-globals/node_modules/process/browser.js")) +},{"./content.coffee":5,"./i18n.coffee":6,"/Users/brando37/Documents/Projects/habitrpg/node_modules/habitrpg-shared/node_modules/browserify/node_modules/insert-module-globals/node_modules/process/browser.js":2,"lodash":3,"moment":4}]},{},[1]) \ No newline at end of file diff --git a/script/index.coffee b/script/index.coffee index 8d8e573e32..6d8b7c595f 100644 --- a/script/index.coffee +++ b/script/index.coffee @@ -845,28 +845,60 @@ api.wrap = (user, main=true) -> delta = 0 calculateDelta = -> + # Calculates the next task.value based on direction + # Uses a capped inverse log y=.95^x, y>= -5 + + # Min/max on task redness + currVal = + if task.value < -47.27 then -47.27 + else if task.value > 21.27 then 21.27 + else task.value + nextDelta = Math.pow(0.9747, currVal) * (if direction is 'down' then -1 else 1) + + # Checklists + if task.checklist?.length > 0 + # If the Daily, only dock them them a portion based on their checklist completion + if direction is 'down' and task.type is 'daily' and options.cron + nextDelta *= (1 - _.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),0) / task.checklist.length) + # If To-Do, point-match the TD per checklist item completed + if task.type is 'todo' + nextDelta *= (1 + _.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),0)) + nextDelta + + calculateReverseDelta = -> + # Approximates the reverse delta for the task value + # This is meant to return the task value to its original value when unchecking a task. + # First, calculate the the value using the normal way for our first guess although + # it will be a bit off + currVal = + if task.value < -47.27 then -47.27 + else if task.value > 21.27 then 21.27 + else task.value + testVal = currVal + Math.pow(0.9747, currVal) * (if direction is 'down' then -1 else 1) + + # Now keep moving closer to the original value until we get "close enough" + closeEnough = 0.0001 + while true + # Check how close we are to the original value by computing the delta off our guess + # and looking at the difference between that and our current value. + calc = (testVal) + Math.pow(0.9747, testVal) + diff = currVal-calc + if Math.abs(diff) < closeEnough + break + if diff > 0 + testVal -= diff + else + testVal += diff + # When we get close enough, return the difference between our approximated value + # and the current value. This will be the delta calculated from the original value + # before the task was checked. + testVal - currVal + + changeTaskValue = (reverse) -> # If multiple days have passed, multiply times days missed _.times options.times, -> # Each iteration calculate the nextDelta, which is then accumulated in the total delta. - # Calculates the next task.value based on direction - # Uses a capped inverse log y=.95^x, y>= -5 - - # Min/max on task redness - currVal = - if task.value < -47.27 then -47.27 - else if task.value > 21.27 then 21.27 - else task.value - nextDelta = Math.pow(0.9747, currVal) * (if direction is 'down' then -1 else 1) - - # Checklists - if task.checklist?.length > 0 - # If the Daily, only dock them them a portion based on their checklist completion - if direction is 'down' and task.type is 'daily' and options.cron - nextDelta *= (1 - _.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),0) / task.checklist.length) - # If To-Do, point-match the TD per checklist item completed - if task.type is 'todo' - nextDelta *= (1 + _.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),0)) - + nextDelta = if reverse then calculateReverseDelta() else calculateDelta() unless task.type is 'reward' if (user.preferences.automaticAllocation is true and user.preferences.allocationMode is 'taskbased' and !(task.type is 'todo' and direction is 'down')) then user.stats.training[task.attribute] += nextDelta # ===== STRENGTH ===== @@ -915,7 +947,7 @@ api.wrap = (user, main=true) -> switch task.type when 'habit' - calculateDelta() + changeTaskValue() # Add habit value to habit-history (if different) if (delta > 0) then addPoints() else subtractPoints() @@ -929,11 +961,13 @@ api.wrap = (user, main=true) -> when 'daily' if options.cron - calculateDelta() + changeTaskValue() subtractPoints() task.streak = 0 unless user.stats.buffs.streaks else - calculateDelta() + changeTaskValue(delta < 0) + if delta < 0 + delta = calculateDelta() addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes if direction is 'up' task.streak = if task.streak then task.streak + 1 else 1 @@ -948,11 +982,13 @@ api.wrap = (user, main=true) -> when 'todo' if options.cron - calculateDelta() + changeTaskValue() #don't touch stats on cron else task.dateCompleted = if direction is 'up' then new Date else undefined - calculateDelta() + changeTaskValue(delta < 0) + if delta < 0 + delta = calculateDelta() addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes # MP++ per checklist item in ToDo, bonus per CLI multiplier = _.max([(_.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),1)),1]) @@ -965,7 +1001,7 @@ api.wrap = (user, main=true) -> when 'reward' # Don't adjust values for rewards - calculateDelta() + changeTaskValue() # purchase item stats.gp -= Math.abs(task.value) num = parseFloat(task.value).toFixed(2) From d0cebce60507d54697562321b6be270fcf73f91e Mon Sep 17 00:00:00 2001 From: Brandon Reid Date: Mon, 6 Oct 2014 13:33:43 -0600 Subject: [PATCH 2/2] Fixed bug when calculating the reverse delta. Added in a case for a todo with checklists so the gp and exp for those are reversed correctly. Added a couple test cases for checking then unchecking tasks. --- dist/habitrpg-shared.js | 30 +++++++++++++++++++----------- script/index.coffee | 35 +++++++++++++++++++++++------------ test/algos.mocha.coffee | 15 +++++++++++++++ 3 files changed, 57 insertions(+), 23 deletions(-) diff --git a/dist/habitrpg-shared.js b/dist/habitrpg-shared.js index 95526bfe55..eb3dea7521 100644 --- a/dist/habitrpg-shared.js +++ b/dist/habitrpg-shared.js @@ -14180,10 +14180,10 @@ api.wrap = function(user, main) { return nextDelta; }; calculateReverseDelta = function() { - var calc, closeEnough, currVal, diff, testVal; + var calc, closeEnough, currVal, diff, nextDelta, testVal, _ref1; currVal = task.value < -47.27 ? -47.27 : task.value > 21.27 ? 21.27 : task.value; testVal = currVal + Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1); - closeEnough = 0.0001; + closeEnough = 0.00001; while (true) { calc = testVal + Math.pow(0.9747, testVal); diff = currVal - calc; @@ -14196,12 +14196,20 @@ api.wrap = function(user, main) { testVal += diff; } } - return testVal - currVal; + nextDelta = testVal - currVal; + if (((_ref1 = task.checklist) != null ? _ref1.length : void 0) > 0) { + if (task.type === 'todo') { + nextDelta *= 1 + _.reduce(task.checklist, (function(m, i) { + return m + (i.completed ? 1 : 0); + }), 0); + } + } + return nextDelta; }; - changeTaskValue = function(reverse) { + changeTaskValue = function() { return _.times(options.times, function() { var nextDelta, _ref1; - nextDelta = reverse ? calculateReverseDelta() : calculateDelta(); + nextDelta = !options.cron && direction === 'down' ? calculateReverseDelta() : calculateDelta(); if (task.type !== 'reward') { if (user.preferences.automaticAllocation === true && user.preferences.allocationMode === 'taskbased' && !(task.type === 'todo' && direction === 'down')) { user.stats.training[task.attribute] += nextDelta; @@ -14218,7 +14226,7 @@ api.wrap = function(user, main) { }); }; addPoints = function() { - var afterStreak, gpMod, intBonus, perBonus, streakBonus, _crit; + var afterStreak, currStreak, gpMod, intBonus, perBonus, streakBonus, _crit; _crit = (delta > 0 ? user.fns.crit() : 1); if (_crit > 1) { user._tmp.crit = _crit; @@ -14227,7 +14235,7 @@ api.wrap = function(user, main) { stats.exp += Math.round(delta * intBonus * task.priority * _crit * 6); perBonus = 1 + user._statsComputed.per * .02; gpMod = delta * task.priority * _crit * perBonus; - return stats.gp += task.streak ? (streakBonus = task.streak / 100 + 1, afterStreak = gpMod * streakBonus, gpMod > 0 ? user._tmp.streakBonus = afterStreak - gpMod : void 0, afterStreak) : gpMod; + return stats.gp += task.streak ? (currStreak = direction === 'down' ? task.streak - 1 : task.streak, streakBonus = currStreak / 100 + 1, afterStreak = gpMod * streakBonus, currStreak > 0 ? gpMod > 0 ? user._tmp.streakBonus = afterStreak - gpMod : void 0 : void 0, afterStreak) : gpMod; }; subtractPoints = function() { var conBonus, hpMod; @@ -14269,8 +14277,8 @@ api.wrap = function(user, main) { task.streak = 0; } } else { - changeTaskValue(delta < 0); - if (delta < 0) { + changeTaskValue(); + if (direction === 'down') { delta = calculateDelta(); } addPoints(); @@ -14292,8 +14300,8 @@ api.wrap = function(user, main) { changeTaskValue(); } else { task.dateCompleted = direction === 'up' ? new Date : void 0; - changeTaskValue(delta < 0); - if (delta < 0) { + changeTaskValue(); + if (direction === 'down') { delta = calculateDelta(); } addPoints(); diff --git a/script/index.coffee b/script/index.coffee index 6d8b7c595f..cd86afd909 100644 --- a/script/index.coffee +++ b/script/index.coffee @@ -877,7 +877,7 @@ api.wrap = (user, main=true) -> testVal = currVal + Math.pow(0.9747, currVal) * (if direction is 'down' then -1 else 1) # Now keep moving closer to the original value until we get "close enough" - closeEnough = 0.0001 + closeEnough = 0.00001 while true # Check how close we are to the original value by computing the delta off our guess # and looking at the difference between that and our current value. @@ -889,16 +889,25 @@ api.wrap = (user, main=true) -> testVal -= diff else testVal += diff + # When we get close enough, return the difference between our approximated value # and the current value. This will be the delta calculated from the original value # before the task was checked. - testVal - currVal + nextDelta = testVal - currVal - changeTaskValue = (reverse) -> + # Checklists + if task.checklist?.length > 0 + # If To-Do, point-match the TD per checklist item completed + if task.type is 'todo' + nextDelta *= (1 + _.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),0)) + nextDelta + + + changeTaskValue = -> # If multiple days have passed, multiply times days missed _.times options.times, -> # Each iteration calculate the nextDelta, which is then accumulated in the total delta. - nextDelta = if reverse then calculateReverseDelta() else calculateDelta() + nextDelta = if not options.cron and direction is 'down' then calculateReverseDelta() else calculateDelta() unless task.type is 'reward' if (user.preferences.automaticAllocation is true and user.preferences.allocationMode is 'taskbased' and !(task.type is 'todo' and direction is 'down')) then user.stats.training[task.attribute] += nextDelta # ===== STRENGTH ===== @@ -930,9 +939,11 @@ api.wrap = (user, main=true) -> gpMod = (delta * task.priority * _crit * perBonus) stats.gp += if task.streak - streakBonus = task.streak / 100 + 1 # eg, 1-day streak is 1.1, 2-day is 1.2, etc + currStreak = if direction is 'down' then task.streak-1 else task.streak + streakBonus = currStreak / 100 + 1 # eg, 1-day streak is 1.01, 2-day is 1.02, etc afterStreak = gpMod * streakBonus - user._tmp.streakBonus = afterStreak - gpMod if (gpMod > 0) # keep this on-hand for later, so we can notify streak-bonus + if currStreak > 0 + user._tmp.streakBonus = afterStreak - gpMod if (gpMod > 0) # keep this on-hand for later, so we can notify streak-bonus afterStreak else gpMod @@ -965,9 +976,9 @@ api.wrap = (user, main=true) -> subtractPoints() task.streak = 0 unless user.stats.buffs.streaks else - changeTaskValue(delta < 0) - if delta < 0 - delta = calculateDelta() + changeTaskValue() + if direction is 'down' + delta = calculateDelta() # recalculate delta for unchecking so the gp and exp come out correctly addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes if direction is 'up' task.streak = if task.streak then task.streak + 1 else 1 @@ -986,9 +997,9 @@ api.wrap = (user, main=true) -> #don't touch stats on cron else task.dateCompleted = if direction is 'up' then new Date else undefined - changeTaskValue(delta < 0) - if delta < 0 - delta = calculateDelta() + changeTaskValue() + if direction is 'down' + delta = calculateDelta() # recalculate delta for unchecking so the gp and exp come out correctly addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes # MP++ per checklist item in ToDo, bonus per CLI multiplier = _.max([(_.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),1)),1]) diff --git a/test/algos.mocha.coffee b/test/algos.mocha.coffee index 199c5eb887..fb5ccdef6b 100644 --- a/test/algos.mocha.coffee +++ b/test/algos.mocha.coffee @@ -93,6 +93,11 @@ expectNoChange = (before,after) -> _.each $w('stats items gear dailys todos rewards flags preferences'), (attr)-> expect(after[attr]).to.eql before[attr] +expectClosePoints = (before, after, taskType) -> + expect( Math.abs(after.stats.exp - before.stats.exp) ).to.be.lessThan 0.0001 + expect( Math.abs(after.stats.gp - before.stats.gp) ).to.be.lessThan 0.0001 + expect( Math.abs(after["#{taskType}s"][0].value - before["#{taskType}s"][0].value) ).to.be.lessThan 0.0001 + expectDayResetNoDamage = (b,a) -> [before,after] = [_.cloneDeep(b), _.cloneDeep(a)] _.each after.dailys, (task,i) -> @@ -379,10 +384,20 @@ describe 'Simple Scoring', -> @after.ops.score {params: {id: @after.dailys[0].id, direction: 'up'}} expectGainedPoints(@before, @after,'daily') + it 'Dailys : Up, Down', -> + @after.ops.score {params: {id: @after.dailys[0].id, direction: 'up'}} + @after.ops.score {params: {id: @after.dailys[0].id, direction: 'down'}} + expectClosePoints(@before, @after, 'daily') + it 'Todos : Up', -> @after.ops.score {params: {id: @after.todos[0].id, direction: 'up'}} expectGainedPoints(@before, @after,'todo') + it 'Todos : Up, Down', -> + @after.ops.score {params: {id: @after.todos[0].id, direction: 'up'}} + @after.ops.score {params: {id: @after.todos[0].id, direction: 'down'}} + expectClosePoints(@before, @after, 'todo') + describe 'Cron', -> it 'computes shouldCron', ->