From 2e55d7c3fe74115ae375ab4105133d1aad04c03d Mon Sep 17 00:00:00 2001 From: Kozhilya Date: Tue, 12 Mar 2024 17:28:38 +0400 Subject: [PATCH] Initital commit --- .gitignore | 2 + Checker.js | 125 ++++++++++++++++++++++++++++ ResultStatistics.js | 94 +++++++++++++++++++++ RollCheck.js | 18 ++++ Stat.js | 38 +++++++++ StatRoll.js | 33 ++++++++ main.js | 33 ++++++++ package-lock.json | 199 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 16 ++++ 9 files changed, 558 insertions(+) create mode 100644 .gitignore create mode 100644 Checker.js create mode 100644 ResultStatistics.js create mode 100644 RollCheck.js create mode 100644 Stat.js create mode 100644 StatRoll.js create mode 100644 main.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4926076 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +node_modules/ \ No newline at end of file diff --git a/Checker.js b/Checker.js new file mode 100644 index 0000000..8b0bf9b --- /dev/null +++ b/Checker.js @@ -0,0 +1,125 @@ +const { log } = require("console"); +const RollCheck = require("./RollCheck"); +const StatRoll = require("./StatRoll"); +const { stat } = require("fs"); +const ResultStatistics = require("./ResultStatistics"); + + +module.exports = class Checker { + /** + * @type {RollCheck[]} + */ + checks = []; + + /** + * @type {int[]} + */ + amounts; + + /** + * @type {int} + */ + attemts; + + /** + * @type {boolean} + */ + #isPrepared = false; + + /** + * @type {ResultStatistics} + */ + statistic_all; + + /** + * @type {ResultStatistics} + */ + statistic_passed; + + variants = {}; + + full_mask; + + /** + * @param {string} name + * @param {(roll: StatRoll) => boolean} fn + */ + addCheck(name, fn) { + if (this.#isPrepared) { + console.log("Cannot add check while prepared"); + } + + this.checks.push(new RollCheck(name, fn)); + } + + prepare() { + this.amounts = Array.from({length: Math.pow(2, this.checks.length)}, _ => 0); + this.#isPrepared = true; + this.attemts = 0; + + this.statistic_all = new ResultStatistics(); + this.statistic_passed = new ResultStatistics(); + } + + /** + * + * @param {StatRoll} statRoll + */ + test(statRoll) { + let mask = 0; + + for (let i = 0; i < this.checks.length; i++) { + const check = this.checks[i]; + const checkResult = check.test(statRoll); + + mask += checkResult ? Math.pow(2, i) : 0; + } + + this.amounts[mask] += 1; + this.attemts += 1; + + this.statistic_all.add(statRoll); + + if (mask + 1 === this.amounts.length) { + this.statistic_passed.add(statRoll); + } + } + + printResults() { + this.statistic_all.print(this.attemts); + + const resultPerCheck = []; + for (let i = 0; i < this.checks.length; i++) { + const check = this.checks[i]; + const amount = this.amounts.filter((_, mask) => (mask >> i) % 2 > 0).reduce((a, s) => s + a); + + resultPerCheck.push({ + check: check.name, + successes: amount, + successes_percent: 100 * amount / this.attemts, + fails: this.attemts - amount, + fails_percent: 100 * (this.attemts - amount) / this.attemts, + }) + } + console.table(resultPerCheck); + + + const result = []; + + for (let mask = 0; mask < this.amounts.length; mask++) { + const amount = this.amounts[mask]; + + result.push({ + mask: mask.toString(2), + successes: this.checks.filter((_, i) => (mask >> i) % 2 > 0).map(f => f.name).join(', '), + fails: this.checks.filter((_, i) => (mask >> i) % 2 === 0).map(f => f.name).join(', '), + amount: amount, + percent: 100 * amount / this.attemts, + }); + } + + console.table(result); + + this.statistic_passed.print(this.amounts[this.amounts.length - 1]); + } +} \ No newline at end of file diff --git a/ResultStatistics.js b/ResultStatistics.js new file mode 100644 index 0000000..f86a6fc --- /dev/null +++ b/ResultStatistics.js @@ -0,0 +1,94 @@ +const StatRoll = require("./StatRoll"); +const { table } = require('table'); + + +module.exports = class ResultStatistics { + dices = {}; + stats = {}; + counts = {}; + sums = {}; + + constructor() { + this.dices = {}; + this.counts[0] = {}; + for (let i = 1; i <= 6; i++) { + this.dices[i] = 0; + + this.counts[i] = {}; + } + + this.stats = {}; + for (let j = 3; j <= 18; j++) { + this.stats[j] = 0; + + for (let i = 0; i <= 6; i++) { + this.counts[i][j] = 0; + } + } + + this.sums = {}; + } + + /** + * @param {StatRoll} statRoll + */ + add(statRoll) { + for (let i = 0; i < statRoll.rolls.length; i++) { + const stat = statRoll.rolls[i]; + + // this.stats[stat.result] += 1; + + for (const dice of stat.dices) { + this.dices[dice] += 1; + } + } + + for (let j = 3; j <= 18; j++) { + let count = statRoll.rolls.filter(r => r.result === j).length; + + this.counts[count][j] += 1; + + this.stats[j] += count ? 1 : 0; + } + + this.sums[statRoll.sum] = (this.sums[statRoll.sum] ?? 0) + 1; + } + + print(norm = 1) { + const rows = []; + + rows.push([ '', ...Object.keys(this.stats) ]); + + rows.push([ 'Probability', ...Object.values(this.stats).map(v => perc(v, norm)) ]); + rows.push([ 'Missing', ...Object.values(this.counts[0]).map(v => perc(v, norm)) ]); + + for (let i = 1; i <= 6; i++) { + const counts = this.counts[i]; + + rows.push([ i, ...Object.values(this.counts[i]).map(v => perc(v, norm)) ]); + } + + console.log(table(rows, { + drawHorizontalLine: (i, rc) => [0, 1, 3, rc].indexOf(i) >= 0, + })); + + + const sums = {}; + const spl = 5; + const from = Math.floor(Object.keys(this.sums).reduce((a, m) => Math.min(a, m)) / spl); + const to = Math.floor(Object.keys(this.sums).reduce((a, m) => Math.max(a, m)) / spl); + + for (let i = from; i <= to; i++) { + const min = spl * i, max = spl * (i + 1) - 1; + const f = Object.entries(this.sums).filter(([sum, _]) => (sum >= min) && (sum <= max)); + sums[`${min}-${max}`] = perc(f.length ? f.map(([_, count]) => count).reduce((count, sum) => sum + count) : 0, norm); + } + + console.log(table([ Object.keys(sums), Object.values(sums) ])) + } + +} + +function perc(a, N) { + return (a == 0) ? '' : `${(100 * a / N).toLocaleString('ru-RU', { maximumFractionDigits: 2, minimumFractionDigits: 2 })}%`; +} \ No newline at end of file diff --git a/RollCheck.js b/RollCheck.js new file mode 100644 index 0000000..71e5ac1 --- /dev/null +++ b/RollCheck.js @@ -0,0 +1,18 @@ +const StatRoll = require("./StatRoll"); + +module.exports = class RollCheck { + name; + fn; + + constructor(name, fn) { + this.name = name; + this.fn = fn; + } + + /** + * @param {StatRoll} statRoll + */ + test(statRoll) { + return this.fn(statRoll); + } +} \ No newline at end of file diff --git a/Stat.js b/Stat.js new file mode 100644 index 0000000..de8a309 --- /dev/null +++ b/Stat.js @@ -0,0 +1,38 @@ +'use strict'; + +const Chance = require("chance"); + +module.exports = class Stat { + /** + * @type {number{4}} + */ + dices; + + /** + * @type {number} + */ + result + + constructor(diceString) { + this.dices = Stat.chance(diceString); + // this.dices = Array.from({length: 4}, _ => Math.floor(Math.random() * 6 + 1)); + + this.dices.sort().reverse(); + + this.result = this.dices.reduce((a, s) => s + a, 0) - Math.min(...this.dices); + } + + toString() { + return `${this.result} [${this.dices.join(',')}]`; + } + + static #chance; + + static chance(diceString) { + if (!this.#chance) { + this.#chance = new Chance(); + } + + return this.#chance.rpg(diceString); + } +} \ No newline at end of file diff --git a/StatRoll.js b/StatRoll.js new file mode 100644 index 0000000..150e8fb --- /dev/null +++ b/StatRoll.js @@ -0,0 +1,33 @@ +'use strict'; + +const Stat = require("./Stat.js"); + +module.exports = class StatRoll { + /** + * @prop {Stat[6]} rolls + */ + rolls; + + /** + * @prop {number} sum + */ + sum; + + constructor(rolls) { + this.rolls = Array.from({length: 6}, _ => new Stat("4d6")); + + this.rolls.sort((r1, r2) => r1.result - r2.result).reverse(); + + this.sum = this.rolls.map(r => r.result).reduce((r, s) => s += r); + } + + + toString() { + return `{${this.rolls.join(' / ')}}`; + } + + + toShortString() { + return `{${this.rolls.map(r => r.result).join('/')}}`; + } +} \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..e72d129 --- /dev/null +++ b/main.js @@ -0,0 +1,33 @@ +'use strict'; + +const Checker = require("./Checker.js"); +const StatRoll = require("./StatRoll.js"); +const ProgressBar = require('progress'); + +const checker = new Checker(); + +checker.addCheck("min70", roll => roll.sum >= 70); +checker.addCheck("lows", roll => roll.rolls.filter(r => r.result < 10).length < 2); +checker.addCheck("highs", roll => roll.rolls.filter(r => r.result >= 15).length >= 2); + +checker.prepare(); + + +const N = 1000000; +// const N = 10; + +const bar = new ProgressBar('[:bar] :rate/bps :percent :etas', { total: N }); + +for (let i = 0; i < N; i++) { + const roll = new StatRoll(); + + checker.test(roll); + + bar.tick(); + + // console.log(roll.toString()); +} + +// console.log(checker); + +checker.printResults(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c18e654 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,199 @@ +{ + "name": "dnd_stat_roll_distribution", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dnd_stat_roll_distribution", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chance": "^1.1.11", + "progress": "^2.0.3", + "table": "^6.8.1" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/chance": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.11.tgz", + "integrity": "sha512-kqTg3WWywappJPqtgrdvbA380VoXO2eu9VCV895JgbyHsaErXdyHK9LOZ911OvAk6L0obK7kDk9CGs8+oBawVA==" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..686f9cf --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "dnd_stat_roll_distribution", + "version": "1.0.0", + "description": "", + "main": "main.js", + "scripts": { + "main": "node main.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "chance": "^1.1.11", + "progress": "^2.0.3", + "table": "^6.8.1" + } +}