From 2d238032074b1f73e6ef016cc9feb7469f82c07b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 14 Dec 2013 12:48:43 -0700 Subject: [PATCH] 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 --- dist/habitrpg-shared.js | 91 ++++++++++++++++++++++------------------- script/index.coffee | 62 +++++++++++++++------------- 2 files changed, 83 insertions(+), 70 deletions(-) diff --git a/dist/habitrpg-shared.js b/dist/habitrpg-shared.js index 9c92fe722f..fc0e610802 100644 --- a/dist/habitrpg-shared.js +++ b/dist/habitrpg-shared.js @@ -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) { diff --git a/script/index.coffee b/script/index.coffee index d0685c6d0d..cb2530074b 100644 --- a/script/index.coffee +++ b/script/index.coffee @@ -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]++