test: add Vitest + 30 dice-engine tests
- Install vitest and @vitest/coverage-v8 - Add test scripts to package.json - Create vitest.config.ts - Add 30 comprehensive tests for parseDiceNotation, rollDice, rollAdvantage, buildNotation, isAdvancedNotation - Fix isAdvancedNotation regex to detect ro (reroll once)
This commit is contained in:
Generated
+510
-22
@@ -12,6 +12,10 @@
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"astro": "^4.16.0",
|
||||
"tailwindcss": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@astrojs/compiler": {
|
||||
@@ -363,6 +367,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
@@ -831,7 +845,6 @@
|
||||
"version": "0.128.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz",
|
||||
"integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
@@ -847,7 +860,6 @@
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -863,7 +875,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -879,7 +890,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -895,7 +905,6 @@
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -911,7 +920,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -927,7 +935,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -943,7 +950,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -959,7 +965,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -975,7 +980,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -991,7 +995,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -1007,7 +1010,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -1023,7 +1025,6 @@
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -1036,7 +1037,6 @@
|
||||
"wasm32"
|
||||
],
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
@@ -1057,7 +1057,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -1073,7 +1072,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
@@ -1081,8 +1079,7 @@
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.18",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz",
|
||||
"integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==",
|
||||
"peer": true
|
||||
"integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw=="
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.3.0",
|
||||
@@ -1472,6 +1469,13 @@
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
|
||||
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
|
||||
@@ -1760,6 +1764,17 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
@@ -1773,6 +1788,13 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
@@ -1835,6 +1857,162 @@
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
|
||||
"integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz",
|
||||
"integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"@vitest/utils": "4.1.5",
|
||||
"ast-v8-to-istanbul": "^1.0.0",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.2.0",
|
||||
"magicast": "^0.5.2",
|
||||
"obug": "^2.1.1",
|
||||
"std-env": "^4.0.0-rc.1",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "4.1.5",
|
||||
"vitest": "4.1.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8/node_modules/magicast": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz",
|
||||
"integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz",
|
||||
"integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.1.5",
|
||||
"@vitest/utils": "4.1.5",
|
||||
"chai": "^6.2.2",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz",
|
||||
"integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.1.5",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz",
|
||||
"integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz",
|
||||
"integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.5",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz",
|
||||
"integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.5",
|
||||
"@vitest/utils": "4.1.5",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz",
|
||||
"integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz",
|
||||
"integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
@@ -1941,6 +2119,35 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz",
|
||||
"integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"estree-walker": "^3.0.3",
|
||||
"js-tokens": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/astro": {
|
||||
"version": "4.16.19",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-4.16.19.tgz",
|
||||
@@ -2597,6 +2804,16 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
@@ -2952,6 +3169,16 @@
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@@ -2995,7 +3222,6 @@
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -3146,6 +3372,16 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-html": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
|
||||
@@ -3468,6 +3704,52 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports/node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
@@ -3873,6 +4155,22 @@
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-table": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
||||
@@ -4726,6 +5024,17 @@
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
|
||||
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||
@@ -4881,6 +5190,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -5252,7 +5568,6 @@
|
||||
"version": "1.0.0-rc.18",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz",
|
||||
"integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.128.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.18"
|
||||
@@ -5437,6 +5752,13 @@
|
||||
"@types/hast": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
@@ -5509,6 +5831,20 @@
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
||||
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stdin-discarder": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||
@@ -5585,6 +5921,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
|
||||
@@ -5602,6 +5951,13 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
@@ -5611,7 +5967,6 @@
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
@@ -5623,6 +5978,16 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -5911,7 +6276,6 @@
|
||||
"version": "8.0.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz",
|
||||
"integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
@@ -5997,6 +6361,113 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz",
|
||||
"integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.1.5",
|
||||
"@vitest/mocker": "4.1.5",
|
||||
"@vitest/pretty-format": "4.1.5",
|
||||
"@vitest/runner": "4.1.5",
|
||||
"@vitest/snapshot": "4.1.5",
|
||||
"@vitest/spy": "4.1.5",
|
||||
"@vitest/utils": "4.1.5",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"expect-type": "^1.3.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^4.0.0-rc.1",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.1.0",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.1.5",
|
||||
"@vitest/browser-preview": "4.1.5",
|
||||
"@vitest/browser-webdriverio": "4.1.5",
|
||||
"@vitest/coverage-istanbul": "4.1.5",
|
||||
"@vitest/coverage-v8": "4.1.5",
|
||||
"@vitest/ui": "4.1.5",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-istanbul": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-v8": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/es-module-lexer": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
||||
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vitest/node_modules/tinyexec": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
|
||||
"integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/web-namespaces": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
|
||||
@@ -6025,6 +6496,23 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/widest-line": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
|
||||
|
||||
+8
-1
@@ -5,12 +5,19 @@
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
"preview": "astro preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"astro": "^4.16.0",
|
||||
"tailwindcss": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
parseDiceNotation,
|
||||
rollDice,
|
||||
rollAdvantage,
|
||||
buildNotation,
|
||||
isAdvancedNotation,
|
||||
} from "./dice-engine";
|
||||
|
||||
describe("parseDiceNotation", () => {
|
||||
it('parses basic notation "d6"', () => {
|
||||
const parsed = parseDiceNotation("d6");
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.count).toBe(1);
|
||||
expect(parsed!.sides).toBe(6);
|
||||
expect(parsed!.modifier).toBe(0);
|
||||
expect(parsed!.keepDrop).toBeNull();
|
||||
expect(parsed!.explode.active).toBe(false);
|
||||
expect(parsed!.reroll.active).toBe(false);
|
||||
});
|
||||
|
||||
it('parses "2d20+3"', () => {
|
||||
const parsed = parseDiceNotation("2d20+3");
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.count).toBe(2);
|
||||
expect(parsed!.sides).toBe(20);
|
||||
expect(parsed!.modifier).toBe(3);
|
||||
});
|
||||
|
||||
it('parses "3d8-1"', () => {
|
||||
const parsed = parseDiceNotation("3d8-1");
|
||||
expect(parsed!.count).toBe(3);
|
||||
expect(parsed!.sides).toBe(8);
|
||||
expect(parsed!.modifier).toBe(-1);
|
||||
});
|
||||
|
||||
it('parses "4d6kh3"', () => {
|
||||
const parsed = parseDiceNotation("4d6kh3");
|
||||
expect(parsed!.keepDrop).toEqual({ type: "kh", count: 3 });
|
||||
});
|
||||
|
||||
it('parses "5d6dl2"', () => {
|
||||
const parsed = parseDiceNotation("5d6dl2");
|
||||
expect(parsed!.keepDrop).toEqual({ type: "dl", count: 2 });
|
||||
});
|
||||
|
||||
it('parses exploding "3d6!"', () => {
|
||||
const parsed = parseDiceNotation("3d6!");
|
||||
expect(parsed!.explode.active).toBe(true);
|
||||
expect(parsed!.explode.penetrating).toBe(false);
|
||||
expect(parsed!.explode.threshold).toBe(6);
|
||||
});
|
||||
|
||||
it('parses penetrating explode "2d10!p"', () => {
|
||||
const parsed = parseDiceNotation("2d10!p");
|
||||
expect(parsed!.explode.active).toBe(true);
|
||||
expect(parsed!.explode.penetrating).toBe(true);
|
||||
});
|
||||
|
||||
it('parses explode with custom threshold "2d6!>4"', () => {
|
||||
const parsed = parseDiceNotation("2d6!>4");
|
||||
expect(parsed!.explode.active).toBe(true);
|
||||
expect(parsed!.explode.threshold).toBe(4);
|
||||
});
|
||||
|
||||
it('parses reroll "2d6r1"', () => {
|
||||
const parsed = parseDiceNotation("2d6r1");
|
||||
expect(parsed!.reroll.active).toBe(true);
|
||||
expect(parsed!.reroll.once).toBe(false);
|
||||
expect(parsed!.reroll.values).toContain(1);
|
||||
});
|
||||
|
||||
it('parses reroll once "2d6ro1"', () => {
|
||||
const parsed = parseDiceNotation("2d6ro1");
|
||||
expect(parsed!.reroll.active).toBe(true);
|
||||
expect(parsed!.reroll.once).toBe(true);
|
||||
});
|
||||
|
||||
it('parses reroll less-than "2d6r<3"', () => {
|
||||
const parsed = parseDiceNotation("2d6r<3");
|
||||
expect(parsed!.reroll.active).toBe(true);
|
||||
expect(parsed!.reroll.values).toContain(1);
|
||||
expect(parsed!.reroll.values).toContain(2);
|
||||
expect(parsed!.reroll.values).not.toContain(3);
|
||||
});
|
||||
|
||||
it("ignores whitespace", () => {
|
||||
const parsed = parseDiceNotation(" 2 d 20 + 5 ");
|
||||
expect(parsed!.count).toBe(2);
|
||||
expect(parsed!.sides).toBe(20);
|
||||
expect(parsed!.modifier).toBe(5);
|
||||
});
|
||||
|
||||
it("returns null for invalid notation", () => {
|
||||
expect(parseDiceNotation("")).toBeNull();
|
||||
expect(parseDiceNotation("abc")).toBeNull();
|
||||
expect(parseDiceNotation("2d6+1+2")).toBeNull();
|
||||
expect(parseDiceNotation("0d6")).toBeNull();
|
||||
expect(parseDiceNotation("21d6")).toBeNull();
|
||||
expect(parseDiceNotation("2d0")).toBeNull();
|
||||
expect(parseDiceNotation("2d6kh0")).toBeNull();
|
||||
expect(parseDiceNotation("2d6kh2")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollDice", () => {
|
||||
let mathRandomSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
afterEach(() => {
|
||||
mathRandomSpy?.mockRestore();
|
||||
});
|
||||
|
||||
it("rolls basic dice", () => {
|
||||
mathRandomSpy = vi.spyOn(Math, "random").mockReturnValue(0.5);
|
||||
const parsed = parseDiceNotation("3d6")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice).toHaveLength(3);
|
||||
expect(result.kept).toHaveLength(3);
|
||||
expect(result.dropped).toHaveLength(0);
|
||||
expect(result.total).toBe(12); // each die: floor(0.5*6)+1 = 4, 3*4 = 12
|
||||
});
|
||||
|
||||
it("applies modifier", () => {
|
||||
mathRandomSpy = vi.spyOn(Math, "random").mockReturnValue(0.5);
|
||||
const parsed = parseDiceNotation("1d20+5")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.total).toBe(16); // 11 + 5
|
||||
});
|
||||
|
||||
it("keeps highest rolls (kh)", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.1) // 1
|
||||
.mockReturnValueOnce(0.9) // 6
|
||||
.mockReturnValueOnce(0.5); // 4
|
||||
|
||||
const parsed = parseDiceNotation("3d6kh2")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.kept).toHaveLength(2);
|
||||
expect(result.dropped).toHaveLength(1);
|
||||
expect(result.dropped[0].dropped).toBe(true);
|
||||
expect(result.total).toBe(10); // 6 + 4
|
||||
});
|
||||
|
||||
it("drops highest rolls (dh)", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.1) // 1
|
||||
.mockReturnValueOnce(0.9) // 6
|
||||
.mockReturnValueOnce(0.5); // 4
|
||||
|
||||
const parsed = parseDiceNotation("3d6dh1")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.kept).toHaveLength(2);
|
||||
expect(result.dropped).toHaveLength(1);
|
||||
expect(result.dropped[0].value).toBe(6);
|
||||
expect(result.total).toBe(5); // 1 + 4
|
||||
});
|
||||
|
||||
it("handles exploding dice", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.99) // first roll: 6 (max, triggers explode)
|
||||
.mockReturnValueOnce(0.99) // explosion 1: 6 (triggers again)
|
||||
.mockReturnValueOnce(0.1); // explosion 2: 1 (stops)
|
||||
|
||||
const parsed = parseDiceNotation("1d6!")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice[0].exploded).toBe(true);
|
||||
expect(result.dice[0].explosions).toEqual([6, 1]);
|
||||
expect(result.total).toBe(13); // 6 + 6 + 1
|
||||
});
|
||||
|
||||
it("handles penetrating explode", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.99) // first roll: 6 (explode)
|
||||
.mockReturnValueOnce(0.99); // explosion: 6 -> penetrating -> 5, then 5 < 6 stops
|
||||
|
||||
const parsed = parseDiceNotation("1d6!p")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice[0].explosions).toEqual([5]);
|
||||
expect(result.total).toBe(11); // 6 + 5
|
||||
});
|
||||
|
||||
it("handles reroll", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.0) // first roll: 1 (reroll)
|
||||
.mockReturnValueOnce(0.5); // reroll: 4
|
||||
|
||||
const parsed = parseDiceNotation("1d6r1")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice[0].rerolledFrom).toBe(1);
|
||||
expect(result.dice[0].value).toBe(4);
|
||||
});
|
||||
|
||||
it("handles reroll once (ro)", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.0) // first: 1 (reroll once)
|
||||
.mockReturnValueOnce(0.0); // second: 1 (stays, no more rerolls)
|
||||
|
||||
const parsed = parseDiceNotation("1d6ro1")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice[0].rerolledFrom).toBe(1);
|
||||
expect(result.dice[0].value).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollAdvantage", () => {
|
||||
let mathRandomSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
afterEach(() => {
|
||||
mathRandomSpy?.mockRestore();
|
||||
});
|
||||
|
||||
it("returns highest for advantage", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.1) // 3
|
||||
.mockReturnValueOnce(0.9); // 19
|
||||
|
||||
const result = rollAdvantage(20, 0, true);
|
||||
expect(result.total).toBe(19);
|
||||
expect(result.advantageRolls).toEqual([3, 19]);
|
||||
});
|
||||
|
||||
it("returns lowest for disadvantage", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.9) // 19
|
||||
.mockReturnValueOnce(0.1); // 3
|
||||
|
||||
const result = rollAdvantage(20, 2, false);
|
||||
expect(result.total).toBe(5); // 3 + 2
|
||||
expect(result.advantageRolls).toEqual([19, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildNotation", () => {
|
||||
it("builds basic notation", () => {
|
||||
expect(buildNotation(1, 6, 0)).toBe("1d6");
|
||||
expect(buildNotation(2, 20, 0)).toBe("2d20");
|
||||
});
|
||||
|
||||
it("adds positive modifier", () => {
|
||||
expect(buildNotation(2, 6, 3)).toBe("2d6+3");
|
||||
});
|
||||
|
||||
it("adds negative modifier", () => {
|
||||
expect(buildNotation(2, 6, -2)).toBe("2d6-2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAdvancedNotation", () => {
|
||||
it("returns false for basic notation", () => {
|
||||
expect(isAdvancedNotation("2d6")).toBe(false);
|
||||
expect(isAdvancedNotation("1d20+5")).toBe(false);
|
||||
expect(isAdvancedNotation("3d8-1")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for keep/drop", () => {
|
||||
expect(isAdvancedNotation("4d6kh3")).toBe(true);
|
||||
expect(isAdvancedNotation("5d6dl2")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for explode", () => {
|
||||
expect(isAdvancedNotation("3d6!")).toBe(true);
|
||||
expect(isAdvancedNotation("2d10!p")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for reroll", () => {
|
||||
expect(isAdvancedNotation("2d6r1")).toBe(true);
|
||||
expect(isAdvancedNotation("2d6ro1")).toBe(true);
|
||||
expect(isAdvancedNotation("2d6r<3")).toBe(true);
|
||||
});
|
||||
});
|
||||
+90
-34
@@ -2,7 +2,7 @@
|
||||
// Supports: XdY±Z, kh/kl/dh/dl, ! exploding, r reroll, advantage/disadvantage
|
||||
// Caps: explode chain ≤100, reroll recursion ≤1000
|
||||
|
||||
type KeepDrop = { type: 'kh' | 'kl' | 'dh' | 'dl'; count: number } | null;
|
||||
type KeepDrop = { type: "kh" | "kl" | "dh" | "dl"; count: number } | null;
|
||||
|
||||
type Explode = {
|
||||
active: boolean;
|
||||
@@ -14,7 +14,7 @@ type Reroll = {
|
||||
active: boolean;
|
||||
values: Set<number>;
|
||||
once: boolean; // "ro" = once only; "r" = recursive
|
||||
operator: 'eq' | 'lt' | 'gt';
|
||||
operator: "eq" | "lt" | "gt";
|
||||
};
|
||||
|
||||
export interface Parsed {
|
||||
@@ -32,7 +32,7 @@ export interface DieResult {
|
||||
value: number;
|
||||
original: number;
|
||||
exploded: boolean;
|
||||
explosions: number[]; // chained explosion rolls
|
||||
explosions: number[]; // chained explosion rolls
|
||||
rerolledFrom: number | null;
|
||||
dropped: boolean;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ const REROLL_CAP = 1000;
|
||||
|
||||
/** Remove all whitespace and lowercase for parsing */
|
||||
function normalize(raw: string): string {
|
||||
return raw.replace(/\s+/g, '').toLowerCase();
|
||||
return raw.replace(/\s+/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +67,7 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
const main = s.match(/^(\d*)d(\d+)(.*)$/);
|
||||
if (!main) return null;
|
||||
|
||||
const count = main[1] === '' ? 1 : parseInt(main[1], 10);
|
||||
const count = main[1] === "" ? 1 : parseInt(main[1], 10);
|
||||
const sides = parseInt(main[2], 10);
|
||||
const rest = main[3];
|
||||
|
||||
@@ -81,7 +81,10 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
if (kdMatch) {
|
||||
const countKD = parseInt(kdMatch[2], 10);
|
||||
if (countKD < 1 || countKD >= count) return null;
|
||||
keepDrop = { type: kdMatch[1] as 'kh' | 'kl' | 'dh' | 'dl', count: countKD };
|
||||
keepDrop = {
|
||||
type: kdMatch[1] as "kh" | "kl" | "dh" | "dl",
|
||||
count: countKD,
|
||||
};
|
||||
remaining = remaining.slice(kdMatch[0].length);
|
||||
}
|
||||
|
||||
@@ -90,8 +93,8 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
const expMatch = remaining.match(/^(!p|!)(>?)(\d*)/);
|
||||
if (expMatch) {
|
||||
explode.active = true;
|
||||
explode.penetrating = expMatch[1] === '!p';
|
||||
if (expMatch[2] === '>' && expMatch[3]) {
|
||||
explode.penetrating = expMatch[1] === "!p";
|
||||
if (expMatch[2] === ">" && expMatch[3]) {
|
||||
explode.threshold = parseInt(expMatch[3], 10);
|
||||
if (explode.threshold < 2 || explode.threshold > sides) return null;
|
||||
} else {
|
||||
@@ -101,21 +104,26 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
}
|
||||
|
||||
// Reroll: rN, roN, r<N, ro<N
|
||||
let reroll: Reroll = { active: false, values: new Set(), once: false, operator: 'eq' };
|
||||
let reroll: Reroll = {
|
||||
active: false,
|
||||
values: new Set(),
|
||||
once: false,
|
||||
operator: "eq",
|
||||
};
|
||||
const rrMatch = remaining.match(/^(ro|r)([<>]?)(\d+)/);
|
||||
if (rrMatch) {
|
||||
reroll.active = true;
|
||||
reroll.once = rrMatch[1] === 'ro';
|
||||
const op = rrMatch[2] as '' | '<' | '>';
|
||||
reroll.once = rrMatch[1] === "ro";
|
||||
const op = rrMatch[2] as "" | "<" | ">";
|
||||
const val = parseInt(rrMatch[3], 10);
|
||||
if (op === '<') {
|
||||
reroll.operator = 'lt';
|
||||
if (op === "<") {
|
||||
reroll.operator = "lt";
|
||||
for (let i = 1; i < val && i < sides; i++) reroll.values.add(i);
|
||||
} else if (op === '>') {
|
||||
reroll.operator = 'gt';
|
||||
} else if (op === ">") {
|
||||
reroll.operator = "gt";
|
||||
for (let i = val + 1; i <= sides; i++) reroll.values.add(i);
|
||||
} else {
|
||||
reroll.operator = 'eq';
|
||||
reroll.operator = "eq";
|
||||
reroll.values.add(val);
|
||||
}
|
||||
remaining = remaining.slice(rrMatch[0].length);
|
||||
@@ -133,7 +141,16 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
// If anything remains unparsed, it's invalid
|
||||
if (remaining.length > 0) return null;
|
||||
|
||||
return { count, sides, modifier, keepDrop, explode, reroll, advantage: false, disadvantage: false };
|
||||
return {
|
||||
count,
|
||||
sides,
|
||||
modifier,
|
||||
keepDrop,
|
||||
explode,
|
||||
reroll,
|
||||
advantage: false,
|
||||
disadvantage: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Roll a single die with optional reroll and explode logic */
|
||||
@@ -194,34 +211,47 @@ export function rollDice(parsed: Parsed): RollResult {
|
||||
|
||||
if (parsed.keepDrop) {
|
||||
const sorted = [...dice].map((d, i) => ({ die: d, idx: i }));
|
||||
if (parsed.keepDrop.type === 'kh') {
|
||||
if (parsed.keepDrop.type === "kh") {
|
||||
sorted.sort((a, b) => b.die.value - a.die.value);
|
||||
const keepIndices = new Set(sorted.slice(0, parsed.keepDrop.count).map(x => x.idx));
|
||||
const keepIndices = new Set(
|
||||
sorted.slice(0, parsed.keepDrop.count).map((x) => x.idx),
|
||||
);
|
||||
kept = dice.filter((_, i) => keepIndices.has(i));
|
||||
dropped = dice.filter((_, i) => !keepIndices.has(i));
|
||||
} else if (parsed.keepDrop.type === 'kl') {
|
||||
} else if (parsed.keepDrop.type === "kl") {
|
||||
sorted.sort((a, b) => a.die.value - b.die.value);
|
||||
const keepIndices = new Set(sorted.slice(0, parsed.keepDrop.count).map(x => x.idx));
|
||||
const keepIndices = new Set(
|
||||
sorted.slice(0, parsed.keepDrop.count).map((x) => x.idx),
|
||||
);
|
||||
kept = dice.filter((_, i) => keepIndices.has(i));
|
||||
dropped = dice.filter((_, i) => !keepIndices.has(i));
|
||||
} else if (parsed.keepDrop.type === 'dh') {
|
||||
} else if (parsed.keepDrop.type === "dh") {
|
||||
sorted.sort((a, b) => b.die.value - a.die.value);
|
||||
const dropIndices = new Set(sorted.slice(0, parsed.keepDrop.count).map(x => x.idx));
|
||||
const dropIndices = new Set(
|
||||
sorted.slice(0, parsed.keepDrop.count).map((x) => x.idx),
|
||||
);
|
||||
kept = dice.filter((_, i) => !dropIndices.has(i));
|
||||
dropped = dice.filter((_, i) => dropIndices.has(i));
|
||||
} else if (parsed.keepDrop.type === 'dl') {
|
||||
} else if (parsed.keepDrop.type === "dl") {
|
||||
sorted.sort((a, b) => a.die.value - b.die.value);
|
||||
const dropIndices = new Set(sorted.slice(0, parsed.keepDrop.count).map(x => x.idx));
|
||||
const dropIndices = new Set(
|
||||
sorted.slice(0, parsed.keepDrop.count).map((x) => x.idx),
|
||||
);
|
||||
kept = dice.filter((_, i) => !dropIndices.has(i));
|
||||
dropped = dice.filter((_, i) => dropIndices.has(i));
|
||||
}
|
||||
}
|
||||
|
||||
dropped.forEach(d => { d.dropped = true; });
|
||||
dropped.forEach((d) => {
|
||||
d.dropped = true;
|
||||
});
|
||||
|
||||
// Calculate total
|
||||
let total = kept.reduce((sum, d) => sum + d.value, 0);
|
||||
total += kept.reduce((sum, d) => sum + d.explosions.reduce((s, v) => s + v, 0), 0);
|
||||
total += kept.reduce(
|
||||
(sum, d) => sum + d.explosions.reduce((s, v) => s + v, 0),
|
||||
0,
|
||||
);
|
||||
total += parsed.modifier;
|
||||
|
||||
return {
|
||||
@@ -230,30 +260,56 @@ export function rollDice(parsed: Parsed): RollResult {
|
||||
dropped,
|
||||
modifier: parsed.modifier,
|
||||
total,
|
||||
notation: '', // filled by caller
|
||||
notation: "", // filled by caller
|
||||
advantageRolls: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Roll with advantage or disadvantage (2dX keep highest/lowest) */
|
||||
export function rollAdvantage(sides: number, modifier: number, advantage: boolean): RollResult {
|
||||
export function rollAdvantage(
|
||||
sides: number,
|
||||
modifier: number,
|
||||
advantage: boolean,
|
||||
): RollResult {
|
||||
const r1 = Math.floor(Math.random() * sides) + 1;
|
||||
const r2 = Math.floor(Math.random() * sides) + 1;
|
||||
const keptVal = advantage ? Math.max(r1, r2) : Math.min(r1, r2);
|
||||
|
||||
return {
|
||||
dice: [{ value: keptVal, original: keptVal, exploded: false, explosions: [], rerolledFrom: null, dropped: false }],
|
||||
kept: [{ value: keptVal, original: keptVal, exploded: false, explosions: [], rerolledFrom: null, dropped: false }],
|
||||
dice: [
|
||||
{
|
||||
value: keptVal,
|
||||
original: keptVal,
|
||||
exploded: false,
|
||||
explosions: [],
|
||||
rerolledFrom: null,
|
||||
dropped: false,
|
||||
},
|
||||
],
|
||||
kept: [
|
||||
{
|
||||
value: keptVal,
|
||||
original: keptVal,
|
||||
exploded: false,
|
||||
explosions: [],
|
||||
rerolledFrom: null,
|
||||
dropped: false,
|
||||
},
|
||||
],
|
||||
dropped: [],
|
||||
modifier,
|
||||
total: keptVal + modifier,
|
||||
notation: '',
|
||||
notation: "",
|
||||
advantageRolls: [r1, r2],
|
||||
};
|
||||
}
|
||||
|
||||
/** Build notation string from parsed object (basic only, for simple sync) */
|
||||
export function buildNotation(count: number, sides: number, modifier: number): string {
|
||||
export function buildNotation(
|
||||
count: number,
|
||||
sides: number,
|
||||
modifier: number,
|
||||
): string {
|
||||
let s = `${count}d${sides}`;
|
||||
if (modifier > 0) s += `+${modifier}`;
|
||||
else if (modifier < 0) s += modifier;
|
||||
@@ -263,5 +319,5 @@ export function buildNotation(count: number, sides: number, modifier: number): s
|
||||
/** Check if notation contains advanced features beyond basic XdY±Z */
|
||||
export function isAdvancedNotation(raw: string): boolean {
|
||||
const s = normalize(raw);
|
||||
return /(kh|kl|dh|dl|!|[<>]?r\d)/.test(s);
|
||||
return /(kh|kl|dh|dl|!|ro?[<>]?\d)/.test(s);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user