Let me be the first to say that this isn't something I normally do, but out of curiousity, I'll see if anyone has a good idea on how to handle a problem like this.
The application I am working on is a simulated example of the game Let's make a Deal featuring the Monty Hall problem.
I won't go into details about my implementation, but it more or less allows a user to enter a number of how many games they want to simulate, and then if an option is toggled off, the player of those x games won't switch their choice, while if it is toggled on, they will switch their choice every single instance of the game.
My object generator looks like this:
const game = function(){
this[0] = null;
this[1] = null;
this[2] = null;
this.pick = Math.floor(Math.random() * 3);
this.correctpick = Math.floor(Math.random() * 3);
this[this.correctpick] = 1;
for (let i=0; i<3; i++){
if ((this[i] !== 1) && (i !== this.pick)){
this.eliminated = i;
break;
}
}
}
const games = arg => {
let ret = [];
for(let i=0; i<arg; i++){
ret.push(new game);
}
return ret;
}
This structure generates an array which i stringify later that looks like this:
[
{
"0": 1,
"1": null,
"2": null,
"pick": 2,
"correctpick": 0,
"eliminated": 1
},
{
"0": null,
"1": null,
"2": 1,
"pick": 2,
"correctpick": 2,
"eliminated": 0
}
]
As sloppy as the constructor for game looks, the reason is because I have refactored it into having as few function calls as possible, where now I'm literally only calling Math functions at the current time (I removed any helper functions that made the code easier to read, in opt for performance).
This app can be ran both in the browser and in node (cross platform), but I have clamped the arg a user can pass into the games function to 5 million. Any longer than that and the process (or window) freezes for longer than a few seconds, or even potentially crashes.
Is there anything else I can do to increase performance if a huge number is given by a user? Also, if you need more information, I will be happy to supply it!
Thanks!
The obvious performance optimisation would be not to create and store 5 million objects at all, relieving memory pressure. Instead you'd create the objects on the fly only when you need them and throw them away immediately after. I'm not sure what your app does, but it sounds like you want to re-use the same game instances when evaluating results with the different options. In that case, you need to store them of course - but I'd advise to re-think the design and consider immediately evaluating each game with all possible options, accumulating only the results for each choice of options but not keeping all games in memory.
Apart from that, I'd recommend to simplify a bit:
You can drop that loop completely and use some clever arithmetic to find the eliminated option: this.eliminated = this.pick == this.correctpick ? +!this.pick : 3 - this.pick - this.correctpick;. Or use a simple lookup table this.eliminated = [1, 2, 1, 2, 0, 0, 1, 0, 0][this.pick * 3 + this.correctpick].
I'd avoid changing the type of the array elements from null (reference) to 1 (number). Just keep them as integers and initialise your elements with 0 instead.
Don't store 6 properties in your object that are completely redundant. You only need 2 of them: pick and correctpick - everything else can be computed on the fly from them when you need it. Precomputing and storing it would only be advantageous if the computation was heavy and the result was used often. Neither of this is the case, but keeping a low memory footprint is important (However, don't expect much from this).
Not sure about your implementation, but do you really need an Array?
How about only using results (see snippet)?
If it's blocking the browser that worries you, maybe delegating the work to a web worker is the solution for that: see this jsFiddle for a web worker version of this snippet.
(() => {
document.querySelector("#doit")
.addEventListener("click", playMontyHall().handleRequest);
function playMontyHall() {
const result = document.querySelector("#result");
const timing = document.querySelector("#timing");
const nOfGames = document.querySelector("#nGames");
const switchDoors = document.querySelector("#switchyn");
// Create a game
const game = (doSwitch) => {
const doors = [0, 1, 2];
const pick = Math.floor(Math.random() * 3);
const correctPick = Math.floor(Math.random() * 3);
const eliminated = doors.filter(v => v !== pick && v !== correctPick)[0];
return {
correctpick: correctPick,
pick: doSwitch ? doors.filter(v => v !== pick && v !== eliminated)[0] : pick,
eliminated: eliminated,
};
};
const getWinner = game => ~~(game.correctpick === game.pick);
// Sum wins using a generator function
const winningGenerator = function* (doSwitch, n) {
let wins = 0;
while (n--) {
wins += getWinner(game(doSwitch));
yield wins;
}
};
// calculate the number of succeeded games
const calculateGames = (nGames, switchAlways) => {
const funNGames = winningGenerator(switchAlways, nGames);
let numberOfWins = 0;
while (nGames--) {
numberOfWins = funNGames.next().value;
}
return numberOfWins;
}
const cleanUp = playOut => {
result.textContent =
"Playing ... (it may last a few seconds)";
timing.textContent = "";
setTimeout(playOut, 0);
};
const report = results => {
timing.textContent = `This took ${
(performance.now() - results.startTime).toFixed(3)} milliseconds`;
result.innerHTML =
`<b>${!results.switchAlways ? "Never s" : "Always s"}witching doors</b>:
${results.winners} winners out of ${results.nGames} games
(${((results.winners/+results.nGames)*100).toFixed(2)}%)`;
};
// (public) handle button click
function clickHandle() {
cleanUp(() => {
const nGames = nOfGames.value || 5000000;
const switchAlways = switchDoors.checked;
report({
switchAlways: switchAlways,
startTime: performance.now(),
winners: calculateGames(nGames, switchAlways),
nGames: nGames
});
});
}
return {
handleRequest: clickHandle
};
}
})();
body {
margin: 2em;
font: normal 12px/15px verdana, arial;
}
#timing {
color: red;
}
<p><input type="number" id="nGames" value="5000000"> N of games</p>
<p><input type="checkbox" id="switchyn"> Always switch doors</p>
<p><button id="doit">Play</button>
<p id="result"></p>
<p id="timing"></p>
Related
Edit:
The quicksort algorithm isn't actually working...😳
CODESANDBOX
SITUATION
I have a line string with up to 2000 points. I am trying to find the nearest point on the line from the user's location. This operation is executed roughly every 5 seconds and failing to optimise it would unnecessarily drain the users battery.
The source code shows that the distance operation is conducted on every point on the line string - not good for my users.
I would like to implement a quick-sort style algorithm to get the nearest point and reduce the number of the operations. The example below is working quite well and produces a point on the line with far fewer operations (for a list of 1700 points it takes 14 operations to locate the closest point).
PROBLEM
I cannot determine an elegant way to track the index of the result. Once I have the nearest point, I do not want to have to search the original list again to find it's index.
first operation - 0 | median
second - fHalf (0 | median of fHalf) | sHalf (median of list | median of sHalf)
third - at this point it becomes a mess to track
What I have so far:
import distance from "#turf/distance";
import { point } from "#turf/helpers";
const user = point([-77.037076, 38.884017]);
function getMedian(list) {
return Math.ceil(list.length / 2);
}
function getHalves(list) {
const median = getMedian(list);
const firstHalf = list.slice(0, median);
const secondHalf = list.slice(-median);
return { firstHalf, secondHalf };
}
function getClosest(list) {
const to = point(list[0]);
const meters = distance(user, to, { units: "meters" });
return meters;
}
let operations = 0; // used in development to track operations
function selectHalf(list) {
operations += 1;
const { firstHalf, secondHalf } = getHalves(list);
const firstDistance = getClosest(firstHalf);
const secondDistance = getClosest(secondHalf);
const nextList = firstDistance < secondDistance ? firstHalf : secondHalf;
if (list.length === 1)
console.log("operations:", operations, "closest point:", nextList);
else selectHalf(nextList);
}
const morePoints = `-37.0467378013181,145.1634433308106
-37.04674949407303,145.1634394751351
-37.04676521014147,145.1634369605642
-37.04678021374815,145.1634352003645
-37.04679207414114,145.1634343621742
-37.04680510800057,145.1634334401648
// Full list is available in the codesandbox link below
`
.split("\n")
.map((p) => p.split(",").map((n) => parseFloat(n)));
selectHalf(morePoints);
Regarding: https://github.com/gvasquez95/binance-trader/issues/8
I'm concerned that the actual "inner" await (in every single invocation of getPriceChange) isn't blocking asynchronous invocation of the about 1.5k calls this method has on the outer loop (getPriceChanges). As if it was blocking, then the loop would be serial processing.
From the performance view and timing, I'd say it is working as expected, as it takes just some seconds to complete the whole process but, I'd like a programming ACK of the assumptions, if possible, please.
Block codes for those that don't want to get into GitHub and, prefer the code more visible here:
Outer loop:
async function getPriceChanges (since, breakpoint) {
const changes = []
for (const [i, ticker] of tickers.entries()) {
if (!ticker.includes('BIDR') && !ticker.includes('BIFI')) {
changes.push(getPriceChange(ticker, since, breakpoint))
}
}
const res = await Promise.all(changes)
let symbol
let maxChange = 0
let slope = 0
for (const ticker of res) {
if (ticker.percentChange > maxChange && ticker.slope > Math.pow(10, -1 * NUM_DECIMALS)) {
maxChange = ticker.percentChange
symbol = ticker.symbol
slope = ticker.slope
}
}
return { symbol, maxChange: Math.round(100 * maxChange) / 100, slope: round(slope, NUM_DECIMALS) }
}
Inner getPriceChange function:
async function getPriceChange (symbol, since, breakpoint) {
const params = {
TableName: DYNAMODB_TABLE,
ProjectionExpression: '#timestamp, price',
KeyConditionExpression: 'symbol = :symbol and #timestamp > :timestamp',
ExpressionAttributeNames: {
'#timestamp': 'timestamp'
},
ExpressionAttributeValues: {
':symbol': symbol,
':timestamp': since
}
}
const res = await documentClient.query(params).promise()
const prev = []
const recent = []
const trendData = []
for (const data of res.Items) {
if (data.timestamp < breakpoint) {
prev.push(data.price)
} else {
trendData.push({ x: trendData.length, y: data.price })
recent.push(data.price)
}
}
let sumPrev = 0
let sumRecent = 0
for (const price of prev) { sumPrev += price }
for (const price of recent) { sumRecent += price }
const avgPrev = sumPrev / prev.length
const avgRecent = sumRecent / recent.length
const trend = createTrend(trendData, 'x', 'y')
return { symbol, percentChange: (100 * (avgRecent - avgPrev)) / avgRecent, slope: trend.slope }
}
Update:
Function execution takes about 30 seconds in a Node.js 14.x AWS Lambda environment, configured with 624 MB RAM and, from a cost perspective the both the DynamoDB query (ReadRequests) costs and Lambda executions are 1/50th of the actual DynamoDB Batch Put operations (WriteRequest), so optimizing the query should be a needed goal.
Your loop is correctly asynchronous. And it seems you're concerned about how many queries will be in flight at the same time. In your case, you intended for many concurrent queries, and you have done as such.
Explanation
In the simple example of ordering 10 pizzas, consider these arrays:
concurrently
This is an array of pizza orders. We can await all 10 orders to get an array of 10 pizzas. Assuming there are at least 10 delivery people working that night, there's a good chance you'll get all your pizzas within 30 minutes.
for (let i = 0; i < 10; ++i) {
orders.push(orderPizza());
}
pizzas = await Promise.all(orders);
sequentially
This is an array of pizzas. Each order is awaited as it is issued. If each pizza is delivered in 30 minutes or it's free this is likely to result in about 5 hours (10 * ~30 minutes) of waiting for that last pizza because the next pizza isn't ordered until the previous one arrives. (No need for a final Promise.all in this case as each promise is already resolved before continuing with the loop.)
for (let i = 0; i < 10; ++i) {
pizzas.push(await orderPizza());
}
Not sure I understand what your question is here exactly. Your assumptions about your code seem to be correct – I (and many others I assume) would be highly surprised if 1500 promises changed anything in their execution model.
As a general note, if you need more control over promise execution, I can recommend checking out Sindre Sorhus' collection of promise helpers. You'll find tools for limiting concurrency, allowing for promise failures, async mapping … and much more.
I'm fairly early on in building an app that can be used as an alternative to spreadsheets in a number of scenarios. It offers the user a non-tabular approach to creating and organizing the analysis. The user's instructions are transpiled to and executed by Javascript (using pure functions only). (The interface is between something like Google Blockly and straight coding in a text editor.) In place of cell ranges that you would typically use in a spreadsheet, this product uses Javascript arrays. A problem that I'm facing with this approach is (quasi-)circular calculations such as that found in a simple amortization schedule.
My (unsuccessful) attempts to resolve the issue so far involve:
lazy list evaluation (using https://github.com/dankogai/js-list-lazy)
wrapping all transpiled JS in functions to delay eval so that the user's content doesn't need to be topologically sorted (which it currently is).
Below I'll provide hopefully enough context to illustrate the issue, which can be pretty broadly extrapolated. Take the example of an amortization schedule for a mortgage:
Basically, the BOP ("Beginning of Period") Balance depends on the EOP ("End of Period") Balance from the previous period. And the EOP Balance depends on the BOP Balance from the same period. In a spreadsheet that uses ranges of contiguous cells, this isn't circular because every BOP Balance and EOP Balance is a discrete cell. However, if BOP Balance and EOP Balance (and all of the other time-series-based calcs) are arrays then there is a circular reference when trying to retrieve elements. Spreadsheet screenshots of the example are provided at the end of the post as a supplement.
An attempt to build this analysis in my app generates the following JS (which I've edited and reorganized for clarity). This code, if translated over to a spreadsheet, works just fine (see supplemental screenshots at the end of the post):
// Credit: Apache OpenOffice
function payment (rate, periods, present_value, future_value, type) {
var fv = future_value || 0
var t = type || 0
if (rate === 0) {
return -1 * ((present_value + fv) / periods)
} else {
var term = (1 + rate) ** periods // Transpiling with Babel; otherwise, use Math.pow(1 + rate, periods)
if (t === 1) {
return -1 * ((fv * rate / (term - 1) + present_value * rate / (1 - 1 / term)) / (1 + rate))
} else {
return -1 * (fv * rate / (term - 1) + present_value * rate / (1 - 1 / term))
}
}
}
var loan_principal = 1000000
var annual_interest_rate = 0.06
var interest_rate_monthly = annual_interest_rate / 12
var amortization_period_years = 25
var amortization_period_months = amortization_period_years * 12
var months_range = _.range(1, amortization_period_months + 1, 1) // See: http://underscorejs.org/#range [1, 2, 3, ... 298, 299, 300]
var bop_balance = months_range.map(function (current_element, current_list_position) {
if (current_list_position === 0) {
return loan_principal
} else {
// Along with eop_balance, this causes a graph cycle
return eop_balance[current_list_position - 1]
}
})
var monthly_payment = months_range.map(function (current_element, current_list_position) {
return payment(interest_rate_monthly, amortization_period_months, loan_principal)
})
var principal_payment = months_range.map(function (current_element, current_list_position) {
var current_mthly_pmt = monthly_payment[current_list_position]
var current_int_pmt = interest_payment[current_list_position]
return current_mthly_pmt - current_int_pmt
})
var interest_payment = months_range.map(function (current_element, current_list_position) {
if (current_list_position === 0) {
return loan_principal * interest_rate_monthly * -1
} else {
var previous_bal = eop_balance[current_list_position - 1]
return previous_bal * interest_rate_monthly * -1
}
})
var eop_balance = months_range.map(function (current_element, current_list_position) {
// This causes a graph cycle
var cur_bal = bop_balance[current_list_position]
var cur_prin_pmt = principal_payment[current_list_position]
return cur_bal + cur_prin_pmt
})
This code will not topologically sort because of the cycle between bop_balance and eop_balance. And it won't fully evaluate because of the circular reference.
Any suggestions on how to work around this general scenario? Thank you.
Supplemental Info:
Here are two views of the same spreadsheet representing the analysis:
The reliance on pure functions in the app is to try and minimize confusion for users coming from spreadsheets.
If seeing the actual app would help provide context, please feel free to visit https://www.getpinevale.com. I'm placing this at the end so it doesn't distract from the question.
You shouldn't do a double linked structure. I would suggest use a plain for loop and build up both arrays.
for(int i=0; i<range.length;i++){
if(i==0){
bop_balance[i]= loan_principal;
eop_balance[i]= somecalculatedamount(loan_principal);
}else{
bop_balance[i] = eop_balance[i-1];
eop_balance[i] = somecalculatedamount(bop_balance[i])
}
}
I don't know if my function is correct but the essential point I am trying to make:
don't link the data structures
and use a control structure that is outside of both arrays
Based on the comment below I'd make the suggestion of using reduce.
let range = [1,2,3,4];
let output = range.reduce(function(accumulator, currentValue, currentIndex) {
let balance = {};
if (currentIndex == 0) {
balance.bop = 0;
balance.eop = currentValue;
}else{
balance.bop = accumulator[currentIndex-1].eop;
balance.eop = balance.bop+5;
}
accumulator.push(balance);
return accumulator;
}, []);
console.log(output);
I have a array of about 18 000 elements. I'm creating a map application where I want to add the elements when the user zooms in to a certain level.
So when the user zooms in under 9 I loop tru the array looking for elements that is in the view.
However, it does take some time looping thru the elements, causing the map application lag each time the user zooms out and in of "level 9". Even if there are no elements to add or not, so the bottleneck is the looping I guess.
I've tried to solve it by asyncing it like:
function SearchElements(elementArr) {
var ret = new Promise(resolve => {
var arr = [];
for (var i in elementArr) {
var distanceFromCenter = getDistanceFromLatLonInKm(view.center.latitude, view.center.longitude, dynamicsEntities[i].pss_latitude, dynamicsEntities[i].pss_longitude);
var viewWidthInKm = getSceneWidthInKm(view, true);
if (distanceFromCenter > viewWidthInKm) continue;
arr.push(elementArr[i]);
}
resolve(arr);
});
return ret;
}
SearchElements(myElementsArray).Then(arr => {
// ...
});
But its still not async, this method hangs while the for loop runs.
Because you still have a tight loop that loops through all the elements in one loop, you'll always have the responsiveness issues
One way to tackle the issue is to works on chunks of the data
Note: I'm assuming elementArr is a javascript Array
function SearchElements(elementArr) {
var sliceLength = 100; // how many elements to work on at a time
var totalLength = elementArr.length;
var slices = ((totalLength + sliceLength - 1) / sliceLength) | 0; // integer
return Array.from({length:slices})
.reduce((promise, unused, outerIndex) =>
promise.then(results =>
Promise.resolve(elementArr.slice(outerIndex * sliceLength, sliceLength).map((item, innerIndex) => {
const i = outerIndex * sliceLength + innerIndex;
const distanceFromCenter = getDistanceFromLatLonInKm(view.center.latitude, view.center.longitude, dynamicsEntities[i].pss_latitude, dynamicsEntities[i].pss_longitude);
const viewWidthInKm = getSceneWidthInKm(view, true);
if (distanceFromCenter <= viewWidthInKm) {
return item; // this is like your `push`
}
// if distanceFromCenter > viewWidthInKm, return value will be `undefined`, filtered out later - this is like your `continue`
})).then(batch => results.concat(batch)) // concatenate to results
), Promise.resolve([]))
.then(results => results.filter(v => v !== undefined)); // filter out the "undefined"
}
use:
SearchElements(yourDataArray).then(results => {
// all results available here
});
My other suggestion in the comment was Web Workers (I originally called it worker threads, not sure where I got that term from) - I'm not familiar enough with Web Workers to offer a solution, however https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers and https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API should get you going
To be honest, I think this sort of heavy task would be better suited to Web Workers
I am not quite sure if the wording in the title accurately describes what I am looking to do, allow me to explain:
Suppose in a game, you get different type of points from leveling up.
But the amount of points of each type you get each level can be arbitrary.
For example,
I get 3 offensive point for every 2 level.
But I get 2 defensive point at level 2, 4, 5, and 6 say.
And perhaps 1 supportive point at every level except the first.
Now here's what I've done:
//Suppose I have my hero
var Sylin = new Hero();
Sylin.Level = 5;
//My goal is then to set
// Sylin.OffensivePoint to 6
// Sylin.DefensivePoint to 6
// Sylin.SupportivePoint to 4
Sylin.prototype.OffensivePoint = function() {
return 3*Math.floor(this.Level/2);
};
Sylin.prototype.DefensivePoint = function() {
var defPoint = 0;
for(var i=2; i <= this.Level; i++) {
//maximum level being 6
if(i==2 || i >= 4) {
defPoint += 2;
}
}
return defPoint;
};
Sylin.prototype.SupportivePoint = function() {
return this.Level - 1;
};
It's all fine and dandy, but if the maximum level is extended, the points lists will be updated and then it gets really clumsy, especially if I have things like:
2 points every 3 level, but 3 point on the 9th and 13th level or something apparently lacking in pattern so I can't always do it like what I have for OffensivePoint().
What I have in mind for this type of problems in general is a structure like so:
Level TotalPoint
. . 1 . . . . . a
. . 2 . . . . . b
. . 3 . . . . . c
. . 4 . . . . . d
. . 5 . . . . . e
and so on until the maximum level
In the code, I could then perhaps do:
Sylin.prototype.talentPoint = function() {
return readTalentPointTable(this.Level); //?
};
But then this can still get quite convoluted if there's 20 level with 5 different types of points you can get, say :/
.
.
EDIT
Ok, so I could do something like:
var OffensivePointTable = [0,0,2,2,4,6,8,8,8,10,12];
function getOffensivePoint(level) {
return OffensivePointTable[level];
}
Would it be easier if I store the data by the level in which a point is increased, or by the running total as above?
.
.
EDIT 2
Ok, can I perhaps reverse the order of the structure to look at the type first, then level?
var theTable = {
o: [0,1,0,1,1,0,0,0],
d: [0,0,2,0,2,0,2,0],
s: [0,1,2,3,4,5,6,7]}
//then your CalculateStats:
Sylin.prototype.CalculateStats = function() {
this.offensivePoint = 0;
for(var i=1; i<= this.Level; i++) {
this.offensivePoint += theTable[o][i];
}
}
You could use an object to store the amount of points to increment at each table (I didn't use your exact numbers, but you get the idea):
var LevelPoints = {
'1': {
o: 1,
d: 2,
s: 1
}
'2': {
o: 3,
d: 1,
s: 1
}
'3': {
o: 1,
d: 1,
s: 0
}
'4': {
o: 2,
d: 3,
s: 1
}
//etc.
}
For example, to access the offensive point increase at level 2, use LevelPoints['2'].o.
This requires a lot of typing I suppose, but sometimes just having all the data there makes things easier. Making your code readable to you and easy to change is always nice. It's also useful as a quick reference—if you're wondering how many offensive points will be gained at level 6, you can know immediately. No need to decipher any procedural code. Of course, this is personal preference. Procedural approaches are faster and use less memory, so it's up to you whether that's worth it. In this case the difference will be negligible, so I recommend the data-driven approach.
Also, note that I used var to set this object. Because it can be used by all instances of the Sylin constructor, setting it as an instance variable (using this) is wasteful, as it will create the object for every instance of Sylin. Using var lets them all share it, saving memory.
Alternately, you could store the running total at each level, but IMO this requires more effort for no good reason. It would take less of your time to write a function:
Sylin.prototype.CalculateStats = function() {
this.OffensivePoint = 0;
this.DefensivePoint = 0;
this.SupportivePoint = 0;
for (var i = 1; i <= this.Level; i++) {
this.OffensivePoint += LevelPoints[i].o;
this.DefensivePoint += LevelPoints[i].d;
this.SupportivePoint += LevelPoints[i].s;
}
}
Then just run this function any time the user changes the level of the character. No need to pass the level, as the function will already have access to the this.Level variable.
Why not store the points in an array of objects -
var pointsTable = [{offensivePionts: 1, defensivePoints: 1}, {offensivePoints: 1, defensivePoints: 2}]; //extend for any level
And then just get return points by referencing the corrent property -
function getOffensivePoints(level) {
return pointsTable[level]['offensivePoints'];
}
You can easily extend the datastructure with methods like addLevel etc.
Sure you could always create an hardcoded array of all points, however you could also simply hardcode the exceptions and stick with an algorithm when you can.
Just an idea... that would require your hero to keep track of his points instead of recompiling them dynamically however, but that's probably a good thing.
//have a map for exceptions
var pointExceptionsMap = {
'9': {
off: 3 //3 points of offense on level 9
}
};
Sylin.prototype.levelUp = function () {
var ex = pointExceptionsMap[++this.level];
//update offense points (should be in another function)
this.offense += (ex && typeof ex.off === 'number')?
ex.o /*exception points*/:
this.level % 2? 0 : 2; //2 points every 2 levels
};
Then to level up, you do hero.levelUp() and to get the points hero.offense. I haven't tested anything, but that's the idea. However, if you require to be able to set the level directly, you could either have a setLevel function that would call levelUp the right amount of times but, you would have to use a modifier to allow you leveling down as well.
You could also use my current idea and find an efficient way of implementing exceptionnal algorithms. For instance, you could still dynamically compile the number of offense points, and then add or remove points from that result based on exceptions. So if you need 2 points every 2 levels, but 3 for the level 9, that means adding 1 additionnal point to the compiled points. However, since when you reach higher levels, you wan to retain that exception, you would have to keep track of all added exception points as well.
EDIT: Also, nothing prevents you from using a function as a new exceptionnal algorithm instead of a simple number and if you plan to make the algorithms configurable, you can simply allow users to override the defaults. For instance, you could have a public updateOffense function that encapsulates the logic, so that it can be overriden. That would be something similar to the Strategy design pattern.
EDIT2: Here's a complete example of what I was trying to explain, hope it helps!
var Hero = (function () {
function Hero() {
this.level = 0;
this.stats = {
off: 1,
def: 0
};
}
Hero.prototype = {
statsExceptions: {
'3': {
off: 3 //get 3 points
},
'6': {
def: function () {
//some algorithm, here we just return 4 def points
return 4;
}
}
},
levelUp: function () {
++this.level;
updateStats.call(this, 1);
},
levelDown: function () {
updateStats.call(this, -1);
--this.level;
},
setLevel: function (level) {
var levelFn = 'level' + (this.level < level? 'Up' : 'Down');
while (this.level !== level) {
this[levelFn]();
}
},
statsFns: {
off: function () {
return (this.level % 2? 0 : 2);
},
def: function () {
return 1;
}
}
};
function updateStats(modifier) {
var stats = this.stats,
fns = this.statsFns,
exs = this.statsExceptions,
level = this.level,
k, ex, exType;
for (k in stats) {
if (stats.hasOwnProperty(k)) {
ex = exs[level];
ex = ex? ex[k] : void(0);
exType = typeof ex;
stats[k] += (exType === 'undefined'?
/*no exception*/
fns[k].call(this) :
/*exception*/
exType === 'function' ? ex.call(this) : ex) * modifier;
}
}
}
return Hero;
})();
var h = new Hero();
console.log(h.stats);
h.setLevel(6);
console.log(h.stats);
h.setLevel(0);
console.log(h.stats);
h.levelUp();
console.log(h.stats);
//create another type of Hero, with other rules
function ChuckNorris() {
Hero.call(this); //call parent constructor
}
ChuckNorris.prototype = Object.create(Hero.prototype);
//Chuck gets 200 offense points per level y default
ChuckNorris.prototype.statsFns.off = function () {
return 200;
};
//Not exceptions for him!
ChuckNorris.prototype.statsExceptions = {};
console.info('Chuck is coming!');
var c = new ChuckNorris();
c.setLevel(10);
console.log(c.stats);