shared-code-user-score-task

This commit is contained in:
Victor Piousbox
2016-04-01 15:54:47 +00:00
parent 3089658cc7
commit e6e2c26a12
6 changed files with 226 additions and 9 deletions

View File

@@ -11,7 +11,7 @@ function cloneDropItem (drop) {
}); });
} }
module.exports = function(user, modifiers, req) { module.exports = function randomDrop (user, modifiers, req) {
var acceptableDrops, base, base1, base2, chance, drop, dropK, dropMultiplier, name, name1, name2, quest, rarity, ref, ref1, ref2, ref3, task; var acceptableDrops, base, base1, base2, chance, drop, dropK, dropMultiplier, name, name1, name2, quest, rarity, ref, ref1, ref2, ref3, task;
task = modifiers.task; task = modifiers.task;
chance = _.min([Math.abs(task.value - 21.27), 37.5]) / 150 + .02; chance = _.min([Math.abs(task.value - 21.27), 37.5]) / 150 + .02;

View File

@@ -4,7 +4,8 @@ import {
MAX_STAT_POINTS MAX_STAT_POINTS
} from '../constants'; } from '../constants';
import { toNextLevel } from '../statHelpers'; import { toNextLevel } from '../statHelpers';
module.exports = function (user, stats, req, analytics) {
module.exports = function updateStats (user, stats, req, analytics) {
let allocatedStatPoints; let allocatedStatPoints;
let totalStatPoints; let totalStatPoints;
let experienceToNextLevel; let experienceToNextLevel;

View File

@@ -27,7 +27,7 @@ function _calculateDelta (task, direction, cron) {
// Checklists // Checklists
if (task.checklist && task.checklist.length > 0) { if (task.checklist && task.checklist.length > 0) {
// If the Daily, only dock them them a portion based on their checklist completion // If the Daily, only dock them a portion based on their checklist completion
if (direction === 'down' && task.type === 'daily' && cron) { if (direction === 'down' && task.type === 'daily' && cron) {
nextDelta *= 1 - _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0) / task.checklist.length; nextDelta *= 1 - _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0) / task.checklist.length;
} }
@@ -43,7 +43,7 @@ function _calculateDelta (task, direction, cron) {
// Approximates the reverse delta for the task value // Approximates the reverse delta for the task value
// This is meant to return the task value to its original value when unchecking a task. // 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 // First, calculate the value using the normal way for our first guess although
// it will be a bit off // it will be a bit off
function _calculateReverseDelta (task, direction) { function _calculateReverseDelta (task, direction) {
let currVal = _getTaskValue(task.value); let currVal = _getTaskValue(task.value);
@@ -185,6 +185,7 @@ module.exports = function scoreTask (options = {}, req = {}) {
// ===== starting to actually do stuff, most of above was definitions ===== // ===== starting to actually do stuff, most of above was definitions =====
if (task.type === 'habit') { if (task.type === 'habit') {
delta += _changeTaskValue(user, task, direction, times, cron); delta += _changeTaskValue(user, task, direction, times, cron);
// Add habit value to habit-history (if different) // Add habit value to habit-history (if different)
if (delta > 0) { if (delta > 0) {
_addPoints(user, task, stats, direction, delta); _addPoints(user, task, stats, direction, delta);
@@ -213,17 +214,25 @@ module.exports = function scoreTask (options = {}, req = {}) {
task.streak += 1; task.streak += 1;
// Give a streak achievement when the streak is a multiple of 21 // Give a streak achievement when the streak is a multiple of 21
if (task.streak % 21 === 0) user.achievements.streak = user.achievements.streak ? user.achievements.streak + 1 : 1; if (task.streak % 21 === 0) user.achievements.streak = user.achievements.streak ? user.achievements.streak + 1 : 1;
} else { task.completed = true;
} else if (direction === 'down') {
// Remove a streak achievement if streak was a multiple of 21 and the daily was undone // Remove a streak achievement if streak was a multiple of 21 and the daily was undone
if (task.streak % 21 === 0) user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0; if (task.streak % 21 === 0) user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0;
task.streak -= 1; task.streak -= 1;
task.completed = false;
} }
} }
} else if (task.type === 'todo') { } else if (task.type === 'todo') {
if (cron) { // don't touch stats on cron if (cron) { // don't touch stats on cron
delta += _changeTaskValue(user, task, direction, times, cron); delta += _changeTaskValue(user, task, direction, times, cron);
} else { } else {
task.dateCompleted = direction === 'up' ? new Date() : undefined; if (direction === 'up') {
task.dateCompleted = new Date();
task.completed = true;
} else if (direction === 'down') {
task.completed = false;
task.dateCompleted = undefined;
}
delta += _changeTaskValue(user, task, direction, times, cron); delta += _changeTaskValue(user, task, direction, times, cron);
if (direction === 'down') delta = _calculateDelta(task, direction, delta); // recalculate delta for unchecking so the gp and exp come out correctly if (direction === 'down') delta = _calculateDelta(task, direction, delta); // recalculate delta for unchecking so the gp and exp come out correctly

View File

@@ -0,0 +1,203 @@
import scoreTask from '../../../common/script/ops/scoreTask';
import {
generateUser,
generateDaily,
generateHabit,
generateTodo,
generateReward,
} from '../../helpers/common.helper';
import common from '../../../common';
import i18n from '../../../common/script/i18n';
import {
NotAuthorized,
} from '../../../common/script/libs/errors';
let EPSILON = 0.0001; // negligible distance between datapoints
/* Helper Functions */
let rewrapUser = (user) => {
user._wrapped = false;
common.wrap(user);
return user;
};
let beforeAfter = () => {
let beforeUser = generateUser();
let afterUser = _.cloneDeep(beforeUser);
rewrapUser(afterUser);
return {
beforeUser,
afterUser,
};
};
let expectGainedPoints = (beforeUser, afterUser, beforeTask, afterTask) => {
expect(afterUser.stats.hp).to.eql(50);
expect(afterUser.stats.exp).to.be.greaterThan(beforeUser.stats.exp);
expect(afterUser.stats.gp).to.be.greaterThan(beforeUser.stats.gp);
expect(afterTask.value).to.be.greaterThan(beforeTask.value);
if (afterTask.type === 'habit') {
expect(afterTask.history).to.have.length(1);
}
};
let expectClosePoints = (beforeUser, afterUser, beforeTask, task) => {
expect(Math.abs(afterUser.stats.exp - beforeUser.stats.exp)).to.be.lessThan(EPSILON);
expect(Math.abs(afterUser.stats.gp - beforeUser.stats.gp)).to.be.lessThan(EPSILON);
expect(Math.abs(task.value - beforeTask.value)).to.be.lessThan(EPSILON);
};
let _expectRoughlyEqualDates = (date1, date2) => {
expect(date1.toString()).to.eql(date2.toString());
};
describe('shared.ops.scoreTask', () => {
let ref;
beforeEach(() => {
ref = beforeAfter();
});
it('throws an error when scoring a reward if user does not have enough gold', (done) => {
let reward = generateReward({ userId: ref.afterUser._id, text: 'some reward', value: 100 });
try {
scoreTask({ user: ref.afterUser, task: reward });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.eql(i18n.t('messageNotEnoughGold'));
done();
}
});
it('checks that the streak parameters affects the score', () => {
let task = generateDaily({ userId: ref.afterUser._id, text: 'task to check streak' });
scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false });
scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false });
expect(task.streak).to.eql(2);
});
it('completes when the task direction is up', () => {
let task = generateTodo({ userId: ref.afterUser._id, text: 'todo to complete', cron: false });
scoreTask({ user: ref.afterUser, task, direction: 'up' });
expect(task.completed).to.eql(true);
_expectRoughlyEqualDates(task.dateCompleted, new Date());
});
it('uncompletes when the task direction is down', () => {
let task = generateTodo({ userId: ref.afterUser._id, text: 'todo to complete', cron: false });
scoreTask({ user: ref.afterUser, task, direction: 'down' });
expect(task.completed).to.eql(false);
expect(task.dateCompleted).to.not.exist;
});
describe('verifies that times parameter in scoring works', () => {
let habit;
beforeEach(() => {
ref = beforeAfter();
habit = generateHabit({ userId: ref.afterUser._id, text: 'some habit' });
});
it('works', () => {
let delta1, delta2, delta3;
delta1 = scoreTask({ user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false });
ref = beforeAfter();
habit = generateHabit({ userId: ref.afterUser._id, text: 'some habit' });
delta2 = scoreTask({ user: ref.afterUser, task: habit, direction: 'up', times: 4, cron: false });
ref = beforeAfter();
habit = generateHabit({ userId: ref.afterUser._id, text: 'some habit' });
delta3 = scoreTask({ user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false });
expect(Math.abs(delta1 - delta2)).to.be.greaterThan(EPSILON);
expect(Math.abs(delta1 - delta3)).to.be.lessThan(EPSILON);
});
});
describe('scores', () => {
let options = {};
let habit;
let freshDaily, daily;
let freshTodo, todo;
beforeEach(() => {
ref = beforeAfter(options);
habit = generateHabit({ userId: ref.afterUser._id, text: 'some habit' });
freshDaily = generateDaily({ userId: ref.afterUser._id, text: 'some daily' });
daily = generateDaily({ userId: ref.afterUser._id, text: 'some daily' });
freshTodo = generateTodo({ userId: ref.afterUser._id, text: 'some todo' });
todo = generateTodo({ userId: ref.afterUser._id, text: 'some todo' });
expect(habit.history.length).to.eql(0);
// before and after are the same user
expect(ref.beforeUser._id).to.exist;
expect(ref.beforeUser._id).to.eql(ref.afterUser._id);
});
context('habits', () => {
it('up', () => {
options = { user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false };
scoreTask(options);
expect(habit.history.length).to.eql(1);
expect(habit.value).to.be.greaterThan(0);
expect(ref.afterUser.stats.hp).to.eql(50);
expect(ref.afterUser.stats.exp).to.be.greaterThan(ref.beforeUser.stats.exp);
expect(ref.afterUser.stats.gp).to.be.greaterThan(ref.beforeUser.stats.gp);
});
it('down', () => {
scoreTask({user: ref.afterUser, task: habit, direction: 'down', times: 5, cron: false}, {});
expect(habit.history.length).to.eql(1);
expect(habit.value).to.be.lessThan(0);
expect(ref.afterUser.stats.hp).to.be.lessThan(ref.beforeUser.stats.hp);
expect(ref.afterUser.stats.exp).to.eql(0);
expect(ref.afterUser.stats.gp).to.eql(0);
});
});
context('dailys', () => {
it('up', () => {
expect(daily.completed).to.not.eql(true);
scoreTask({user: ref.afterUser, task: daily, direction: 'up'});
expectGainedPoints(ref.beforeUser, ref.afterUser, freshDaily, daily);
expect(daily.completed).to.eql(true);
});
it('up, down', () => {
scoreTask({user: ref.afterUser, task: daily, direction: 'up'});
scoreTask({user: ref.afterUser, task: daily, direction: 'down'});
expectClosePoints(ref.beforeUser, ref.afterUser, freshDaily, daily);
});
it('sets completed = false on direction = down', () => {
daily.completed = true;
expect(daily.completed).to.not.eql(false);
scoreTask({user: ref.afterUser, task: daily, direction: 'down'});
expect(daily.completed).to.eql(false);
});
});
context('todos', () => {
it('up', () => {
scoreTask({user: ref.afterUser, task: todo, direction: 'up'});
expectGainedPoints(ref.beforeUser, ref.afterUser, freshTodo, todo);
});
it('up, down', () => {
scoreTask({user: ref.afterUser, task: todo, direction: 'up'});
scoreTask({user: ref.afterUser, task: todo, direction: 'down'});
expectClosePoints(ref.beforeUser, ref.afterUser, freshTodo, todo);
});
});
});
});

View File

@@ -54,6 +54,13 @@ export async function generateReward (update = {}) {
return task; return task;
} }
export async function generateTodo (update = {}) {
let type = 'todo';
let task = new Tasks[type](update);
await task.save({ validateBeforeSave: false });
return task;
}
// Generates a new group. Requires a user object, which // Generates a new group. Requires a user object, which
// will will become the groups leader. Takes a details argument // will will become the groups leader. Takes a details argument
// for the initial group creation and an update argument which // for the initial group creation and an update argument which

View File

@@ -393,9 +393,6 @@ api.scoreTask = {
if (!task) throw new NotFound(res.t('taskNotFound')); if (!task) throw new NotFound(res.t('taskNotFound'));
let wasCompleted = task.completed; let wasCompleted = task.completed;
if (task.type === 'daily' || task.type === 'todo') {
task.completed = direction === 'up'; // TODO move into scoreTask
}
let delta = common.ops.scoreTask({task, user, direction}, req); let delta = common.ops.scoreTask({task, user, direction}, req);
// Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results) // Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results)