classes: use predictable randomness. This allows us to run an op on both

client and server, and if randomness is involved (eg, item drops,
critical hits, losing gear/stats on death) - then it will be equivalent
on both client and server, while still seeming "random". This is
required for the new death mechanic. Kudos to @snicker for providing the
predictableRandom() function. @paglias we can probably stop using _.tmp
now, and simply flag on the client when user drops an item / streak
bonus. @SabreCat this will be used for crits, I'll keep you posted
This commit is contained in:
Tyler Renelle 2013-12-14 12:48:43 -07:00
parent 8a5c46da2b
commit 2d23803207
2 changed files with 83 additions and 70 deletions

View file

@ -10456,7 +10456,7 @@ var global=self;/**
},{"lodash":3}],6:[function(require,module,exports){
var process=require("__browserify_process");(function() {
var HP, XP, api, content, dayMapping, moment, preenHistory, randomVal, sanitizeOptions, _,
var HP, XP, api, content, dayMapping, moment, preenHistory, sanitizeOptions, _,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
moment = require('moment');
@ -10569,33 +10569,6 @@ var process=require("__browserify_process");(function() {
}
};
/*
------------------------------------------------------
Drop System
------------------------------------------------------
*/
/*
Get a random property from an object
http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
returns random property (the value)
*/
randomVal = function(obj) {
var count, key, result, val;
result = void 0;
count = 0;
for (key in obj) {
val = obj[key];
if (Math.random() < (1 / ++count)) {
result = val;
}
}
return result;
};
/*
------------------------------------------------------
Scoring
@ -11148,7 +11121,7 @@ var process=require("__browserify_process");(function() {
if (user.stats.lvl > 1) {
user.stats.lvl--;
}
lostStat = randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) {
lostStat = user.fns.randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) {
if (user.stats[k]) {
m[k] = k;
}
@ -11157,9 +11130,9 @@ var process=require("__browserify_process");(function() {
if (lostStat) {
user.stats[lostStat]--;
}
lostItem = randomVal(_.reduce(user.items.gear.owned, (function(m, v, k) {
lostItem = user.fns.randomVal(_.reduce(user.items.gear.owned, (function(m, v, k) {
if (v) {
m[k] = k;
m["" + k] = "" + k;
}
return m;
}), {}));
@ -11393,9 +11366,7 @@ var process=require("__browserify_process");(function() {
if (direction === 'up') {
user.fns.randomDrop({
task: task,
delta: delta,
priority: task.priority,
streak: task.streak
delta: delta
});
}
}
@ -11453,6 +11424,43 @@ var process=require("__browserify_process");(function() {
});
},
/*
Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
we need things to be "random", but technically predictable so that they don't go out-of-sync
*/
predictableRandom: function(seed) {
var x;
if (!seed || seed === Math.PI) {
seed = _.reduce(user.stats, (function(m, v) {
if (_.isNumber(v)) {
return m + v;
} else {
return m;
}
}), 0);
}
x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
},
/*
Get a random property from an object
http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
returns random property (the value)
*/
randomVal: function(obj, options) {
var count, key, result, val;
result = void 0;
count = 0;
for (key in obj) {
val = obj[key];
if (user.fns.predictableRandom(options != null ? options.seed : void 0) < (1 / ++count)) {
result = ((options != null ? options.key : void 0) ? key : val);
}
}
return result;
},
/*
This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as
user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value},
so that different consumers can implement setters their own way. Derby needs model.set(path, value) for example, where
@ -11477,8 +11485,9 @@ var process=require("__browserify_process");(function() {
}), user);
},
randomDrop: function(modifiers) {
var a, acceptableDrops, alpha, chanceMultiplier, delta, drop, max, priority, rarity, reachedDropLimit, streak, _base, _base1, _base2, _base3, _name, _name1, _name2, _ref;
delta = modifiers.delta, priority = modifiers.priority, streak = modifiers.streak;
var a, acceptableDrops, alpha, chanceMultiplier, delta, drop, max, priority, rarity, reachedDropLimit, streak, _base, _base1, _base2, _base3, _name, _name1, _name2, _ref, _ref1;
delta = modifiers.delta;
_ref = modifiers.task, priority = _ref.priority, streak = _ref.streak;
if (streak == null) {
streak = 0;
}
@ -11498,10 +11507,10 @@ var process=require("__browserify_process");(function() {
max = 0.75;
a = 0.1;
alpha = a * max * chanceMultiplier / (a * chanceMultiplier + max);
if (((_ref = user.flags) != null ? _ref.dropsEnabled : void 0) && Math.random() < alpha) {
rarity = Math.random();
if (((_ref1 = user.flags) != null ? _ref1.dropsEnabled : void 0) && user.fns.predictableRandom() < alpha) {
rarity = user.fns.predictableRandom();
if (rarity > .6) {
drop = randomVal(_.omit(content.food, 'Saddle'));
drop = user.fns.randomVal(_.omit(content.food, 'Saddle'));
if ((_base1 = user.items.food)[_name = drop.name] == null) {
_base1[_name] = 0;
}
@ -11509,7 +11518,7 @@ var process=require("__browserify_process");(function() {
drop.type = 'Food';
drop.dialog = "You've found a " + drop.text + " Food! " + drop.notes;
} else if (rarity > .3) {
drop = randomVal(content.eggs);
drop = user.fns.randomVal(content.eggs);
if ((_base2 = user.items.eggs)[_name1 = drop.name] == null) {
_base2[_name1] = 0;
}
@ -11518,7 +11527,7 @@ var process=require("__browserify_process");(function() {
drop.dialog = "You've found a " + drop.text + " Egg! " + drop.notes;
} else {
acceptableDrops = rarity < .03 ? ['Golden'] : rarity < .09 ? ['Zombie', 'CottonCandyPink', 'CottonCandyBlue'] : rarity < .18 ? ['Red', 'Shade', 'Skeleton'] : ['Base', 'White', 'Desert'];
drop = randomVal(_.pick(content.hatchingPotions, (function(v, k) {
drop = user.fns.randomVal(_.pick(content.hatchingPotions, (function(v, k) {
return __indexOf.call(acceptableDrops, k) >= 0;
})));
if ((_base3 = user.items.hatchingPotions)[_name2 = drop.name] == null) {

View file

@ -57,24 +57,6 @@ api.shouldDo = (day, repeat, options={}) ->
yesterday = moment(o.now).subtract(1,'d').day() # have to wrap o.now so as not to modify original
return repeat[dayMapping[yesterday]] # FIXME is this correct?? Do I need to do any timezone calcaulation here?
###
------------------------------------------------------
Drop System
------------------------------------------------------
###
###
Get a random property from an object
http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
returns random property (the value)
###
randomVal = (obj) ->
result = undefined
count = 0
for key, val of obj
result = val if Math.random() < (1 / ++count)
result
###
------------------------------------------------------
@ -87,8 +69,6 @@ api.tnl = (lvl) ->
else Math.round(((Math.pow(lvl, 2) * 0.25) + (10 * lvl) + 139.75) / 10) * 10
# round to nearest 10?
###
Preen history for users with > 7 history entries
This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array
@ -445,12 +425,13 @@ api.wrap = (user) ->
user.stats.lvl-- if user.stats.lvl > 1
# Lose a stat point
lostStat = randomVal _.reduce(['str','con','per','int'], ((m,k)->m[k]=k if user.stats[k];m), {})
lostStat = user.fns.randomVal _.reduce(['str','con','per','int'], ((m,k)->m[k]=k if user.stats[k];m), {})
user.stats[lostStat]-- if lostStat
# Lose a gear piece
# Note, they can actually lose item weapon_*_0 - it's 0 to buy back, no big deal
lostItem = randomVal _.reduce(user.items.gear.owned, ((m,v,k)->m[k]=k if v;m), {})
# Note the `""+` string-casting. Without this, when run on the server Mongoose returns funny objects
lostItem = user.fns.randomVal _.reduce(user.items.gear.owned, ((m,v,k)->m[""+k]=""+k if v;m), {})
if item = content.gear.flat[lostItem]
user.items.gear.owned[lostItem] = false
user.items.gear.equipped[item.type] = "#{item.type}_base_0" if user.items.gear.equipped[item.type] is lostItem
@ -644,7 +625,7 @@ api.wrap = (user) ->
# Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results)
if typeof window is 'undefined'
user.fns.randomDrop({task, delta, priority:task.priority, streak:task.streak}) if direction is 'up'
user.fns.randomDrop({task, delta}) if direction is 'up'
cb? null, req
return delta
@ -681,6 +662,28 @@ api.wrap = (user) ->
when 'potion' then 5
else 6
###
Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
we need things to be "random", but technically predictable so that they don't go out-of-sync
###
predictableRandom: (seed) ->
# Default seed is all user stats combined. Fairly safe, meh - pass in a good seed for situations where that doesn't work
seed = _.reduce(user.stats, ((m,v)->if _.isNumber(v) then m+v else m), 0) if !seed or seed is Math.PI
x = Math.sin(seed++) * 10000
x - Math.floor(x)
###
Get a random property from an object
http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
returns random property (the value)
###
randomVal: (obj, options) ->
result = undefined
count = 0
for key, val of obj
result = (if options?.key then key else val) if user.fns.predictableRandom(options?.seed) < (1 / ++count)
result
###
This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as
user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value},
@ -705,7 +708,8 @@ api.wrap = (user) ->
# ----------------------------------------------------------------------
randomDrop: (modifiers) ->
{delta, priority, streak} = modifiers
{delta} = modifiers
{priority, streak} = modifiers.task
streak ?= 0
# limit drops to 2 / day
user.items.lastDrop ?=
@ -728,14 +732,14 @@ api.wrap = (user) ->
a = 0.1 # rate of increase
alpha = a*max*chanceMultiplier/(a*chanceMultiplier+max) # current probability of drop
if user.flags?.dropsEnabled and Math.random() < alpha
if user.flags?.dropsEnabled and user.fns.predictableRandom() < alpha
# current breakdown - 1% (adjustable) chance on drop
# If they got a drop: 50% chance of egg, 50% Hatching Potion. If hatchingPotion, broken down further even further
rarity = Math.random()
rarity = user.fns.predictableRandom()
# Food: 40% chance
if rarity > .6
drop = randomVal _.omit(content.food, 'Saddle')
drop = user.fns.randomVal _.omit(content.food, 'Saddle')
user.items.food[drop.name] ?= 0
user.items.food[drop.name]+= 1
drop.type = 'Food'
@ -743,7 +747,7 @@ api.wrap = (user) ->
# Eggs: 30% chance
else if rarity > .3
drop = randomVal content.eggs
drop = user.fns.randomVal content.eggs
user.items.eggs[drop.name] ?= 0
user.items.eggs[drop.name]++
drop.type = 'Egg'
@ -763,7 +767,7 @@ api.wrap = (user) ->
# No Rarity (@see https://github.com/HabitRPG/habitrpg/issues/1048, we may want to remove rareness when we add mounts)
#drop = helpers.randomVal hatchingPotions
drop = randomVal _.pick(content.hatchingPotions, ((v,k) -> k in acceptableDrops))
drop = user.fns.randomVal _.pick(content.hatchingPotions, ((v,k) -> k in acceptableDrops))
user.items.hatchingPotions[drop.name] ?= 0
user.items.hatchingPotions[drop.name]++