├── .gitignore ├── .travis.yml ├── test ├── .eslintrc └── smoke.spec.js ├── index.js ├── .eslintrc ├── lib ├── result │ ├── Generic.js │ ├── Counter.js │ ├── MapCounter.js │ ├── PayoutCounter.js │ ├── MapPayoutCounter.js │ └── PayoutStandardDeviationCounter.js ├── Results.js └── Simulator.js ├── package.json ├── LICENSE ├── example.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0.0" 4 | - "5.0.0" 5 | - "stable" -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-expressions": [0], 4 | }, 5 | "env": { 6 | "node": true, 7 | "mocha": true, 8 | "jasmine": true 9 | } 10 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Simulator: require("./lib/Simulator"), 3 | Results: { 4 | Generic: require("./lib/result/Generic"), 5 | Counter: require("./lib/result/Counter"), 6 | PayoutCounter: require("./lib/result/PayoutCounter"), 7 | PayoutStandardDeviationCounter: require("./lib/result/PayoutStandardDeviationCounter"), 8 | MapCounter: require("./lib/result/MapCounter"), 9 | MapPayoutCounter: require("./lib/result/MapPayoutCounter") 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "rules": { 7 | "quotes": [1, "double"], 8 | "no-undef": [2], 9 | "no-underscore-dangle": [0], /* 'private' class members */ 10 | "no-multi-spaces": [0], 11 | "no-spaced-func": [0], 12 | "no-multiple-empty-lines": [2], 13 | "no-trailing-spaces": [2], 14 | "max-statements": [2, 22], 15 | "complexity": [2, 8] 16 | }, 17 | "extends": "eslint:recommended", 18 | } -------------------------------------------------------------------------------- /lib/result/Generic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class GenericResult { 4 | constructor(formatter) { 5 | this.formatter = formatter; 6 | } 7 | } 8 | 9 | GenericResult.GenericFormatter = class GenericFormatter { 10 | setAccuracy(digits) { 11 | this.settings.accuracyInDigits = digits; 12 | return this; 13 | } 14 | 15 | formatPercent(n, N) { 16 | return (n / N * 100).toFixed(2) + "%"; 17 | } 18 | 19 | formatOneIn(n, N) { 20 | return "1 in " + (N / n).toFixed(2); 21 | } 22 | 23 | formatMoney(amount) { 24 | return `£${amount.toFixed(2)}`; 25 | } 26 | } 27 | 28 | module.exports = GenericResult; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monte-carlo", 3 | "version": "1.1.0", 4 | "description": "Monte Carlo simulator for real money games", 5 | "author": "Gamevy", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "repository": "https://github.com/Gamevy/monte-carlo", 9 | "scripts": { 10 | "pretest": "eslint lib test example.js index.js", 11 | "test": "mocha --recursive test" 12 | }, 13 | "engines": { 14 | "node": ">=4.0.0" 15 | }, 16 | "engine-strict": true, 17 | "dependencies": { 18 | "mersenne-twister": "^1.0.1", 19 | "stats-lite": "^2.0.1", 20 | "underscore": "^1.8.3" 21 | }, 22 | "devDependencies": { 23 | "chai": "^3.4.1", 24 | "eslint": "^1.10.1", 25 | "mocha": "^2.3.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/Results.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let _ = require("underscore"); 4 | 5 | module.exports = class Results { 6 | constructor() { 7 | this.orderOfNames = []; 8 | } 9 | 10 | add(name, resultModule) { 11 | this[name] = resultModule; 12 | this.orderOfNames.push(name); 13 | return this; 14 | } 15 | 16 | format(N) { 17 | return _.flatten(this.orderOfNames.map((name) => { 18 | let output = this[name].format(N); 19 | if (_.isArray(output)) { 20 | output = output.map((line) => { return " " + line; }); 21 | output.unshift(name + ":"); 22 | return output; 23 | } else { 24 | return [name + ": " + output.toString()]; 25 | } 26 | })); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/result/Counter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let GenericResult = require("./Generic.js"); 4 | 5 | class Counter extends GenericResult { 6 | constructor(formatter) { 7 | formatter = formatter || new Counter.OutcomeFormatter() 8 | super(formatter) 9 | this.n = 0; 10 | } 11 | 12 | increase(by) { 13 | if (by === undefined) { 14 | by = 1; 15 | } 16 | this.n += by; 17 | } 18 | 19 | format(N) { 20 | return this.formatter.format(this.n, N); 21 | } 22 | } 23 | 24 | Counter.OutcomeFormatter = class extends GenericResult.GenericFormatter { 25 | format(n, N) { 26 | return `${n} (${this.formatPercent(n, N)}) - ${this.formatOneIn(n, N)}`; 27 | } 28 | }; 29 | 30 | Counter.EventFormatter = class extends GenericResult.GenericFormatter { 31 | format(n, N) { 32 | return `${(n / N).toFixed(2)} per game`; 33 | } 34 | }; 35 | 36 | module.exports = Counter; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Gamevy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/result/MapCounter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let GenericResult = require("./Generic.js"); 4 | let Counter = require("./Counter.js"); 5 | let _ = require("underscore"); 6 | 7 | class MapCounter extends GenericResult { 8 | constructor(keys, formatter) { 9 | formatter = formatter || new MapCounter.DefaultFormatter(); 10 | super(formatter); 11 | this.map = {}; 12 | this.order = keys; 13 | _.each(keys, (key) => { 14 | this.map[key] = new Counter(); 15 | }); 16 | } 17 | 18 | increase(key, by) { 19 | if (by === undefined) { 20 | by = 1; 21 | } 22 | if (this.map[key]) { 23 | this.map[key].increase(by); 24 | } else { 25 | throw new Error(`MapCounter: Key "${key}" not set up`); 26 | } 27 | } 28 | 29 | format(N) { 30 | return this.formatter.format(this.order, this.map, N); 31 | } 32 | } 33 | 34 | MapCounter.DefaultFormatter = class extends GenericResult.GenericFormatter { 35 | format(order, map, N) { 36 | return order.map((key) => key + ": " + map[key].format(N)); 37 | } 38 | } 39 | 40 | module.exports = MapCounter; 41 | -------------------------------------------------------------------------------- /lib/result/PayoutCounter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let GenericResult = require("./Generic.js"); 4 | let Counter = require("./Counter.js"); 5 | 6 | class PayoutCounter extends Counter { 7 | constructor(formatter) { 8 | formatter = formatter || new PayoutCounter.DefaultFormatter(); 9 | 10 | super(formatter); 11 | this.n = 0; 12 | this.amount = 0; 13 | } 14 | 15 | increase(by) { 16 | if (by > 0) { 17 | this.amount += by; 18 | } 19 | this.n += 1; 20 | } 21 | 22 | format(N) { 23 | return this.formatter.format(this.n, this.amount, N); 24 | } 25 | } 26 | 27 | PayoutCounter.DefaultFormatter = class extends GenericResult.GenericFormatter { 28 | format(n, amount, N) { 29 | return `${this.formatMoney(amount / N)} per game, ${this.formatMoney(amount / n)} per won game (${n} won games), total of ${this.formatMoney(amount)}`; 30 | } 31 | }; 32 | 33 | PayoutCounter.ListFormatter = class extends GenericResult.GenericFormatter { 34 | format(n, amount, N) { 35 | return `${n} (${this.formatPercent(n, N)}) - ${this.formatOneIn(n, N)} - Avg Payout: ${this.formatMoney(amount / n)} (total: ${this.formatMoney(amount)})`; 36 | } 37 | }; 38 | 39 | module.exports = PayoutCounter; 40 | -------------------------------------------------------------------------------- /lib/result/MapPayoutCounter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let GenericResult = require("./Generic.js"); 4 | let PayoutCounter = require("./PayoutCounter.js"); 5 | let _ = require("underscore"); 6 | 7 | class MapPayoutCounter extends GenericResult { 8 | constructor(keys, formatter) { 9 | formatter = formatter || new MapPayoutCounter.DefaultFormatter() 10 | super(formatter); 11 | this.map = {}; 12 | this.order = keys; 13 | this.sumIsEnabled = false; 14 | _.each(keys, (key) => { 15 | this.map[key] = new PayoutCounter( new PayoutCounter.ListFormatter() ); 16 | }); 17 | } 18 | 19 | enableSum() { 20 | this.order.push("SUM"); 21 | this.map.SUM = new PayoutCounter( new PayoutCounter.ListFormattor() ); 22 | this.sumIsEnabled = true; 23 | } 24 | 25 | increase(key, by) { 26 | if (by === undefined) { 27 | by = 1; 28 | } 29 | if (this.map[key]) { 30 | this.map[key].increase(by); 31 | if (this.sumIsEnabled) { 32 | this.map.SUM.increase(by); 33 | } 34 | } else { 35 | throw new Error(`MapPayoutCounter: Key "${key}" not set up`); 36 | } 37 | } 38 | 39 | format(N) { 40 | return this.formatter.format(this.order, this.map, N); 41 | 42 | } 43 | } 44 | 45 | MapPayoutCounter.DefaultFormatter = class extends GenericResult.GenericFormatter { 46 | format(order, map, N) { 47 | return order.map((key) => key + ": " + map[key].format(N)); 48 | } 49 | }; 50 | 51 | module.exports = MapPayoutCounter; 52 | 53 | -------------------------------------------------------------------------------- /lib/result/PayoutStandardDeviationCounter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let GenericResult = require("./Generic.js"); 4 | let Counter = require("./Counter.js"); 5 | let stats = require("stats-lite"); 6 | 7 | class PayoutStandardDeviationCounter extends Counter { 8 | constructor(formatter) { 9 | formatter = formatter || new PayoutStandardDeviationCounter.DefaultFormatter(); 10 | 11 | super(formatter); 12 | this.payouts = []; 13 | } 14 | 15 | increase(by) { 16 | this.payouts.push(by); 17 | } 18 | 19 | format(N) { 20 | let n = this.payouts.length; 21 | let amount = stats.sum(this.payouts); 22 | 23 | // stdev needs all outcomes, so if "lost" games won't call increase we need to fix that 24 | let fullListOfOutcomes = this.payouts.slice(); 25 | for (let i = 0; i < N - fullListOfOutcomes.length; i++) { 26 | fullListOfOutcomes.push(0); 27 | } 28 | let stdev = stats.stdev(fullListOfOutcomes); 29 | return this.formatter.format(n, amount, N, stdev); 30 | } 31 | } 32 | 33 | PayoutStandardDeviationCounter.DefaultFormatter = class extends GenericResult.GenericFormatter { 34 | format(n, amount, N, stdev) { 35 | return `${this.formatMoney(amount / N)} per game, ${this.formatMoney(amount / n)} per won game (${n} won games), total of ${this.formatMoney(amount)} (STDDEV: ${this.formatMoney(stdev)})`; 36 | } 37 | }; 38 | 39 | PayoutStandardDeviationCounter.ListFormatter = class extends GenericResult.GenericFormatter { 40 | format(n, amount, N, stdev) { 41 | return `${n} (${this.formatPercent(n, N)}) - ${this.formatOneIn(n, N)} - Avg Payout: ${this.formatMoney(amount / n)} (total: ${this.formatMoney(amount)}, STDDEV: ${this.formatMoney(stdev)})`; 42 | } 43 | }; 44 | 45 | module.exports = PayoutStandardDeviationCounter; 46 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | let MonteCarlo = require("./index.js"); // Use "monte-carlo" outside this repo 5 | let _ = require("underscore"); 6 | 7 | class ImpossibleQuizSimulator extends MonteCarlo.Simulator { 8 | before(results) { 9 | results.add("wins", new MonteCarlo.Results.Counter()); 10 | results.add("payout", new MonteCarlo.Results.PayoutStandardDeviationCounter()); 11 | } 12 | 13 | createGameState(rules) { 14 | let questions = _.map(_.range(rules.questions), () => { 15 | return {possible: this.randomDouble() > rules.chanceOfImpossibleQuestion}; 16 | }); 17 | 18 | return { 19 | questions: questions 20 | } 21 | } 22 | 23 | game(rules, gameState, results, skillOutcome) { 24 | let lost = false; 25 | 26 | while (!lost && gameState.questions.length) { 27 | let question = gameState.questions.pop(); 28 | if (!question.possible || !skillOutcome()) { 29 | lost = true; 30 | } 31 | } 32 | 33 | if (!lost) { 34 | if (rules.amountsToBeWon) { 35 | results.payout.increase(this.shuffle(rules.amountsToBeWon)[0]); 36 | } else { 37 | results.payout.increase(this.random(rules.amountToBeWonMin, rules.amountToBeWonMax)); 38 | } 39 | results.wins.increase(); 40 | } 41 | } 42 | } 43 | 44 | let simulator = new ImpossibleQuizSimulator({ N: 10000 }); 45 | 46 | simulator.run("10 questions", { 47 | chanceOfImpossibleQuestion: 0.05, 48 | questions: 10, 49 | amountToBeWonMin: 50, 50 | amountToBeWonMax: 100 51 | }); 52 | 53 | simulator.run("8 questions", { 54 | chanceOfImpossibleQuestion: 0.05, 55 | questions: 8, 56 | amountToBeWonMin: 25, 57 | amountToBeWonMax: 50 58 | }); 59 | 60 | simulator.run("8 questions, fixed amounts", { 61 | chanceOfImpossibleQuestion: 0.05, 62 | questions: 8, 63 | amountsToBeWon: [1, 10, 25, 50, 100] 64 | }); 65 | -------------------------------------------------------------------------------- /test/smoke.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let MonteCarlo = require("../index"); 4 | let _ = require("underscore"); 5 | let expect = require("chai").expect; 6 | 7 | class TestSimulator extends MonteCarlo.Simulator { 8 | before(results) { 9 | results.add("wins", new MonteCarlo.Results.Counter()); 10 | results.add("payout", new MonteCarlo.Results.PayoutCounter()); 11 | results.add("payoutStDev", new MonteCarlo.Results.PayoutStandardDeviationCounter()); 12 | } 13 | 14 | createGameState(rules) { 15 | let questions = _.map(_.range(rules.questions), function() { 16 | return {possible: Math.random() > rules.chanceOfImpossibleQuestion}; 17 | }); 18 | 19 | return { 20 | questions: questions 21 | } 22 | } 23 | 24 | game(rules, gameState, results, skillOutcome) { 25 | let lost = false; 26 | 27 | while (!lost && gameState.questions.length) { 28 | let question = gameState.questions.pop(); 29 | if (!question.possible || !skillOutcome()) { 30 | lost = true; 31 | } 32 | } 33 | 34 | if (!lost) { 35 | results.payout.increase(rules.amountToBeWon); 36 | results.payoutStDev.increase(rules.amountToBeWon); 37 | results.wins.increase(); 38 | } 39 | } 40 | } 41 | 42 | describe("Smoke test", () => { 43 | it("does a normal run with slow clone", () => { 44 | let simulator = new TestSimulator({ N: 10000, slowClone: true }); 45 | 46 | simulator.run("10 questions", { 47 | chanceOfImpossibleQuestion: 0.05, 48 | questions: 10, 49 | amountToBeWon: 100 50 | }); 51 | 52 | }); 53 | 54 | it("does a normal run without slow clone", () => { 55 | let simulator = new TestSimulator({ N: 10000, slowClone: false }); 56 | 57 | simulator.run("10 questions", { 58 | chanceOfImpossibleQuestion: 0.05, 59 | questions: 10, 60 | amountToBeWon: 100 61 | }); 62 | }); 63 | 64 | it("does a normal run without percent skill level", () => { 65 | let simulator = new TestSimulator({ N: 1, skillLevelInPercents: false }); 66 | 67 | simulator.game = (rules, gameState, results, skill) => { 68 | expect(typeof skill).to.equal("number"); 69 | } 70 | 71 | simulator.run("10 questions", {}); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monte Carlo simulator for real money games 2 | 3 | [![Build Status](https://travis-ci.org/Gamevy/monte-carlo.svg?branch=master)](https://travis-ci.org/Gamevy/monte-carlo) 4 | [![Dependency Status](https://david-dm.org/Gamevy/monte-carlo.svg)](https://david-dm.org/marekventur/dependency-updater) 5 | [![devDependency Status](https://david-dm.org/Gamevy/monte-carlo/dev-status.svg)](https://david-dm.org/marekventur/dependency-updater#info=devDependencies) 6 | 7 | This is a oppinonated framework for calculating chances of outcomes for games based on a simulation. This can be used to model rules and paytables a test them for win rates and average player return. 8 | 9 | It's especially useful for games that combine chance elements with skill (like quizzes) and real money gambling and have rules that make it hard to work out the results mathematically. 10 | 11 | Uses ES6 and is therefore Node >4.0.0 only. 12 | 13 | ## Features 14 | 15 | * Optimised for speed 16 | * Simulates different skill levels (By default 50%, 75% and 100% correct) 17 | * Build-in support for collections and presenting results 18 | * Progress indicator 19 | * Exposed RNG operations (```randomDouble()```, ```random(min, max)``` and ```shuffle(list)```) based on Mersenne Twister for better randomness 20 | 21 | ## Example 22 | 23 | What's the chance of answering N questions correctly in a row if there's a 5% chance of hitting a question that's impossible to answer? 24 | 25 | ```javascript 26 | #!/usr/bin/env node 27 | "use strict"; 28 | 29 | let MonteCarlo = require("./index.js"); // Use "monte-carlo" outside this repo 30 | let _ = require("underscore"); 31 | 32 | class ImpossibleQuizSimulator extends MonteCarlo.Simulator { 33 | before(results) { 34 | results.add("wins", new MonteCarlo.Results.Counter()); 35 | results.add("payout", new MonteCarlo.Results.PayoutStandardDeviationCounter()); 36 | } 37 | 38 | createGameState(rules) { 39 | let questions = _.map(_.range(rules.questions), () => { 40 | return {possible: this.randomDouble() > rules.chanceOfImpossibleQuestion}; 41 | }); 42 | 43 | return { 44 | questions: questions 45 | } 46 | } 47 | 48 | game(rules, gameState, results, skillOutcome) { 49 | let lost = false; 50 | 51 | while (!lost && gameState.questions.length) { 52 | let question = gameState.questions.pop(); 53 | if (!question.possible || !skillOutcome()) { 54 | lost = true; 55 | } 56 | } 57 | 58 | if (!lost) { 59 | if (rules.amountsToBeWon) { 60 | results.payout.increase(this.shuffle(rules.amountsToBeWon)[0]); 61 | } else { 62 | results.payout.increase(this.random(rules.amountToBeWonMin, rules.amountToBeWonMax)); 63 | } 64 | results.wins.increase(); 65 | } 66 | } 67 | } 68 | 69 | let simulator = new ImpossibleQuizSimulator({ N: 10000 }); 70 | 71 | simulator.run("10 questions", { 72 | chanceOfImpossibleQuestion: 0.05, 73 | questions: 10, 74 | amountToBeWonMin: 50, 75 | amountToBeWonMax: 100 76 | }); 77 | 78 | simulator.run("8 questions", { 79 | chanceOfImpossibleQuestion: 0.05, 80 | questions: 8, 81 | amountToBeWonMin: 25, 82 | amountToBeWonMax: 50 83 | }); 84 | 85 | simulator.run("8 questions, fixed amounts", { 86 | chanceOfImpossibleQuestion: 0.05, 87 | questions: 8, 88 | amountsToBeWon: [1, 10, 25, 50, 100] 89 | }); 90 | 91 | 92 | ``` 93 | 94 | Result: 95 | ``` 96 | ==== 10 questions ==== 97 | N = 10000 98 | speed: 8475games/sec, total 1.18sec 99 | 100 | == Skill Level: 100% == 101 | 102 | wins: 6014 (60.14%) - 1 in 1.66 103 | payout: £45.15 per game, £75.07 per won game (6014 won games), total of £451483.00 (STDDEV: £34.88) 104 | 105 | == Skill Level: 75% == 106 | 107 | wins: 318 (3.18%) - 1 in 31.45 108 | payout: £2.42 per game, £75.95 per won game (318 won games), total of £24151.00 (STDDEV: £18.65) 109 | 110 | == Skill Level: 50% == 111 | 112 | wins: 3 (0.03%) - 1 in 3333.33 113 | payout: £0.02 per game, £67.00 per won game (3 won games), total of £201.00 (STDDEV: £1.68) 114 | 115 | 116 | ==== 8 questions ==== 117 | N = 10000 118 | speed: 13316games/sec, total 0.751sec 119 | 120 | == Skill Level: 100% == 121 | 122 | wins: 6534 (65.34%) - 1 in 1.53 123 | payout: £24.44 per game, £37.40 per won game (6534 won games), total of £244376.00 (STDDEV: £16.65) 124 | 125 | == Skill Level: 75% == 126 | 127 | wins: 647 (6.47%) - 1 in 15.46 128 | payout: £2.45 per game, £37.92 per won game (647 won games), total of £24535.00 (STDDEV: £12.68) 129 | 130 | == Skill Level: 50% == 131 | 132 | wins: 30 (0.30%) - 1 in 333.33 133 | payout: £0.11 per game, £38.03 per won game (30 won games), total of £1141.00 (STDDEV: £2.99) 134 | 135 | 136 | ==== 8 questions, fixed amounts ==== 137 | N = 10000 138 | speed: 14472games/sec, total 0.691sec 139 | 140 | == Skill Level: 100% == 141 | 142 | wins: 6655 (66.55%) - 1 in 1.50 143 | payout: £25.24 per game, £37.92 per won game (6655 won games), total of £252381.00 (STDDEV: £35.35) 144 | 145 | == Skill Level: 75% == 146 | 147 | wins: 689 (6.89%) - 1 in 14.51 148 | payout: £2.56 per game, £37.16 per won game (689 won games), total of £25602.00 (STDDEV: £17.83) 149 | 150 | == Skill Level: 50% == 151 | 152 | wins: 26 (0.26%) - 1 in 384.62 153 | payout: £0.08 per game, £30.58 per won game (26 won games), total of £795.00 (STDDEV: £2.92) 154 | ``` 155 | 156 | In other words: The game tested isn't a great real money game. 157 | -------------------------------------------------------------------------------- /lib/Simulator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let Results = require("./Results"); 4 | let MersenneTwister = require("mersenne-twister"); 5 | 6 | /*eslint-disable no-console */ 7 | module.exports = class Simulator { 8 | constructor(options) { 9 | this.twister = new MersenneTwister(); 10 | this._setOptions(options); 11 | 12 | // progress bar settings 13 | this.enableProgess = (options.enableProgess === undefined) ? Boolean(process.stdin.isTTY) : options.enableProgess; 14 | this.progressBarWidth = options.progressBarWidth || 10; 15 | 16 | // set to true if your cloneGameState() function is slower than your createGameState() 17 | this.slowClone = (options.slowClone === undefined) ? false : options.slowClone; 18 | if (this.skillLevels.length === 1) { 19 | this.slowClone = true; 20 | } 21 | } 22 | 23 | _setOptions(options) { 24 | options = options || {}; 25 | this.skillLevels = options.skillLevels || [1, 0.75, 0.5]; 26 | this.skillLevelInPercents = (options.skillLevelInPercents === undefined) ? true : options.skillLevelInPercents; 27 | this.N = options.N || 10000; 28 | } 29 | 30 | setN(N) { 31 | this.N = N; 32 | return this; 33 | } 34 | 35 | benchmark(rules) { 36 | let repetitions = 10000; 37 | console.log("=== Benchmark ==="); 38 | console.log(); 39 | 40 | let startTimeCreateGameState = Date.now(); 41 | let someGameState = null; 42 | for (let i=0; i Math.random() < 0.5 ); 60 | } 61 | let durationGame = Date.now() - startGame; 62 | console.log(`game + cloneGameState: ${durationGame / repetitions}ms/game`); 63 | console.log(`game : ${(durationGame - durationCloneGameState) / repetitions}ms/game`); 64 | } 65 | 66 | _warnAboutMissingOverrides() { 67 | if (!this.createGameState) { console.error("Please implement createGameState()"); } 68 | if (!this.game) { console.error("Please implement game(rules, gameState, results, skillOutcome)"); } 69 | if (!this.before) { console.error("Please implement before(results, rules)"); } 70 | } 71 | 72 | _createSkillOutcomesObject(results, rules) { 73 | let skillOutcomes = {}; 74 | 75 | this.skillLevels.forEach((skillLevel, index) => { 76 | this.before(results[index], rules); 77 | 78 | if (this.skillLevelInPercents) { 79 | skillOutcomes[index] = () => { 80 | if (skillLevel === 1) { 81 | return true; 82 | } 83 | return Math.random() < skillLevel; 84 | }; 85 | } else { 86 | skillOutcomes[index] = skillLevel; 87 | } 88 | }); 89 | 90 | return skillOutcomes; 91 | } 92 | 93 | _printOutcome(name, startTime, results) { 94 | if (name) { 95 | console.log(`==== ${name} ====`); 96 | } 97 | console.log(`N = ${this.N}`); 98 | 99 | let endTime = new Date().getTime(); 100 | let duration = (endTime - startTime) / 1000; 101 | console.log(`speed: ${Math.round(this.N / duration)}games/sec, total ${duration}sec`); 102 | console.log(); 103 | 104 | this.skillLevels.forEach((skillLevel, index) => { 105 | if (this.skillLevels.length > 1) { 106 | if (this.skillLevelInPercents) { 107 | console.log(`== Skill Level: ${Math.round(skillLevel * 100)}% ==`); 108 | } else { 109 | console.log(`== Skill Level: ${skillLevel} ==`); 110 | } 111 | console.log(); 112 | } 113 | 114 | console.log(results[index].format(this.N).join("\n")); 115 | console.log(); 116 | }); 117 | } 118 | 119 | _calculateResultSlowClone(results, rules, skillOutcomes) { 120 | let progressPoint = Math.floor(this.N / this.progressBarWidth); 121 | 122 | for (let j=0; j new Results()); 154 | let skillOutcomes = this._createSkillOutcomesObject(results, rules); 155 | 156 | if (this.slowClone) { 157 | this._calculateResultSlowClone(results, rules, skillOutcomes); 158 | } else { 159 | this._calculateResult(results, rules, skillOutcomes); 160 | } 161 | 162 | if (this.enableProgess) { 163 | this.clearProgress(); 164 | } 165 | 166 | this._printOutcome(name, startTime, results); 167 | } 168 | 169 | // override this for a more efficent clone 170 | cloneGameState(gameState) { 171 | return JSON.parse(JSON.stringify(gameState)); 172 | } 173 | 174 | /* Progress bar */ 175 | progressStep(n, N) { 176 | let current = Math.ceil(n / N * this.progressBarWidth); 177 | process.stderr.cursorTo(0); 178 | process.stderr.write("["); 179 | for (let i=0; i