HabitRPG / habitica

A habit tracker app which treats your goals like a Role Playing Game.
https://habitica.com
Other
11.95k stars 4.08k forks source link

Automatic Allocation based on Tasks does not accurately reflect tasks completed #8172

Open jimcullenaus opened 7 years ago

jimcullenaus commented 7 years ago

The scenario:

Nearly every time I level, the tasks I have completed are a majority either intelligence or perception, but strength and constitution ability do get used quite a bit.

Using Automatic Allocation, every time I level, either intelligence or perception is increased. Strength and constitution will never go up, however.

The problem:

Automatic Allocation is looking naïvely at the skill used the most number of times this level. user.stats.training for each skill is reset to zero upon level up. All previous experience in other abilities is ignored.

The solution:

Habitica should use a proportional system to decide which skill is being increased each time a user levels up.

Look at the user's current stats, and look at their total training. Training will never be reset back to zero. Grab the percentage of each stat's training of the total training, and compare that to the amount of levels in that stat as a percentage of the total levels for all stats. Whichever is the biggest difference (i.e., whichever stat is the most underrepresented relative to the training done in it) is increased upon level up.

Pseudocode implementation

Based on website/common/script/fns/autoAllocate.js, under case 'taskbased', which to my understanding is the file in which something like this would need to be implemented.

training = user.stats.training;
actual = getActualStats();
totalTraining = training.str + training.per + training.con + training.int;
totalActual = actual.str + actual.per + actual.con + actual.int;
var proportion = {
    'str': (training.str / totalTraining) - (actual.str / totalActual),
    'per': (training.per / totalTraining) - (actual.per / totalActual),
    'con': (training.con / totalTraining) - (actual.con / totalActual),
    'int': (training.int / totalTraining) - (actual.int / totalActual),
};
var suggested = _.invert(proportion)[_.max(proportion)];
return suggested || 'str';

getActualStats() would need to be replaced with something that can get an object of the user's current stats, but otherwise this script should mostly work as is. It would more accurately represent the efforts a person is actually putting in to different areas and over time the proportion of effort to stats would tend towards 1.

EDIT: From a little more checking, I think user.stats is the object needed for actual. But I can't figure out how to get at my user object from the console to test using actual data.

crookedneighbor commented 7 years ago

I'm neutral in this. @lemoness?

lemoness commented 7 years ago

It's an interesting idea! However, it would require a lot more work to convey that information to the user and to implement, and I'm not convinced that it would be worth it. I'm willing to be persuaded, however.

SabreCat commented 7 years ago

This has been around a while--I'm almost certain there's a duplicate issue for this. I do think it's a good idea to go proportional with it, but it would take some careful design work. I'd want, for instance, (a) a visual representation of the current weighting; and (b) a smart system of "decay" such that if you emphasize one stat for a long time, then deemphasize it, you're not locked in to that proportion for an uncomfortably long time thenceforth.

jimcullenaus commented 7 years ago

To convey it to the user, I don't think it would be necessary to explain in detail everything that's going on (though maybe that information could be in the Wiki). To simply say something along the lines of "Assign points based on the Strength, Intelligence, Constitution, and Perception categories associated with the tasks you complete over time." would seem adequate to me. As it is currently, the user is given almost no information about how the "Distribute points based on task activity" actually works (indeed, for a time I thought it might actually already work in the way I'm suggesting it should), so I don't entirely see the need for greater clarification within the UI. The Wiki could easily go into greater detail of the mechanism by which it works for those users who are interested.

As for the implementation, I believe that the above code (with the inclusion of user.stats) would be the only logic code necessary. I don't know exactly how 'training' works, but I'm assuming it already takes into account things like the colour of the task and the task's assigned difficulty.

A visual representation could be done in a number of ways. This Google spreadsheet shows a couple. One involves two pie charts — the user's actual stats, and their training stats, the difference between them is the inefficiency. The other is a bar graph which shows which stats are currently over or underrepresented and by how much.

Decay would be easy to add in. Just multiply each of the training values by some positive number less than 1 right before returning from the function. The smaller the number, the higher the decay. A value of between 2/3 and 4/5 would seem reasonable to me.

lynxlynxlynx commented 7 years ago

I'd go for something simpler. On level up, nullify only the training data for the stat that "won". That way the rest get a chance to win on the next level ups and even the rares ones can potentially amass enough to win the race at least once.

Alys commented 7 years ago

@lynxlynxlynx I like that idea, at least as a way to start improving this feature. It would be easy to explain to the users, which is important to reduce ongoing, repeated questions.

@jimcullenaus How do you feel about that idea?

jimcullenaus commented 7 years ago

@Alys it's an interesting idea. I'm unfortunately not nearly a good enough statistician to be able to say whether or not that would work properly.

If I had to make a guess, though, I'd say it very well might roughly approximate a proportional result given a large enough dataset, but when the number of points to be distributed is <100 like with Habitica, I'd be worried it might degenerate to something a little too close to an even distribution, rather than a proportional one. The obvious problem is that if a particular stat wins by a significant margin, the long-term impact of that is the same as if it wins by just one point, since the whole thing gets wiped.

Probably better than the current winner-takes-all system, but a long way short of a tried-and-tested method for proportional allocation like the one used in Mixed-Member Proportional voting systems, which is how the above system works.

I'm still not really sure why tis being placed on the need to explain how it works to the end user. The current system certainly doesn't provide any real explanation:

Assigns points based on the Strength, Intelligence, Constitution, and Perception categories associated with the tasks you complete.

If that's considered good enough for the current system, it would also be more than sufficient for either my proposal or @lynxlynxlynx's.

For the wiki, it would have the same section regarding the addition of training points as it does currently, but change the rest to snd its current real score gets the attribute point" and "if a player tends to carry out tasks associated with one particular attribute twice as often as another particular attribute, over time the first attribute will have twice as many points allocated to it". A mention that it is based on mixed-member proportional voting and a link to a video describing in detail how that works could also be used. Most people won't care about the details, but for those who do, that should be sufficient explanation, I would think.

lynxlynxlynx commented 7 years ago

It's also about code complexity and therefore bugs and maintenance burdens. That's why KISS approaches are so popular.

Sure, if you're one xp away from a level-up and reach it with an old red difficult todo, you'll lose some of the attribution. But is it really that important?

jimcullenaus commented 7 years ago

That's what's so beautiful about this solution though. It's really simple. It's just keeping track of two simple pie graphs and returning whatever colour from that graph has the biggest size difference. It's not adding on any new data structures that need to be kept track of, or inventing a complicated new algorithm. It's a simple, elegant way of making the stats represent with near-perfect accuracy.


The other method can easily be constructed to be massively disproportionate. You could get, say, 50 training points in physical, and have the rest be at only around 17. Physical would, of course, win. Next time around, if everything gains an even 25 points plus or minus small amounts, whichever of the three non-physical stats got the highest of those small amounts, despite the fact that physical had such a huge increase in how much it was used that it should still be the winner, since that huge amount massively outweighs the small deltas between the rest of the stats.

lynxlynxlynx commented 7 years ago

That's where we disagree — I don't care for perfect proportionality at all times. I'm looking at it at a level based timeframe, not lifetime. That can can never be really fair under any of these solutions, since you can only assign one attribute to a task, while many should affect more.

jimcullenaus commented 7 years ago

Huh, I thought I had responded to this already. Guess not.

If you don't want perfect proportionality, that's what the dampening/decay factor is for. The more the decay, the less proportional it is, and the more weight is given to the current level. But the current system of ignoring anything other than current level is just ridiculous. As I said previously, you can absolutely work your butt off at something one level, and have most of that work worth absolutely nothing because once you get it slightly more than the next best thing, it's worth zero going forward.

lynxlynxlynx commented 7 years ago

I don't think anyone is advocating for keeping the current solution.

librarianmage commented 7 years ago

Should this still be considered?

lynxlynxlynx commented 7 years ago

Why not, the algorithm is still unfair?

SabreCat commented 7 years ago

Yup, this behavior (which is common/server code) didn't change with the frontend redesign. It could still stand to be overhauled.