commit 2e55d7c3fe74115ae375ab4105133d1aad04c03d Author: Kozhilya Date: Tue Mar 12 17:28:38 2024 +0400 Initital commit 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" + } +}