@platonic-dice/dice 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +14 -1
  2. package/dist/{src/components → components}/die-history/history-cache.js +1 -1
  3. package/dist/components/die-history/index.js +2 -0
  4. package/dist/components/die-history/internal/index.js +2 -0
  5. package/dist/{src/components → components}/die-history/internal/roll-record-manager/index.js +1 -1
  6. package/dist/components/die-history/internal/roll-record-manager/internal/index.js +1 -0
  7. package/dist/{src/components → components}/die-history/internal/roll-record-manager/roll-record-manager.js +5 -4
  8. package/dist/{src/components → components}/die-history/internal/roll-record-validator.js +12 -1
  9. package/dist/{src/components → components}/die-history/roll-record-factory.js +18 -2
  10. package/dist/components/index.js +1 -0
  11. package/dist/{src/die.js → die.js} +25 -3
  12. package/dist/index.js +13 -0
  13. package/dist/types/index.js +1 -0
  14. package/package.json +4 -3
  15. package/dist/__tests__/components/die-history/history-cache.spec.js +0 -106
  16. package/dist/__tests__/components/die-history/internal/roll-record-manager/internal/roll-record-storage.spec.js +0 -40
  17. package/dist/__tests__/components/die-history/internal/roll-record-manager/roll-record-manager.spec.js +0 -112
  18. package/dist/__tests__/components/die-history/internal/roll-record-validator.spec.js +0 -38
  19. package/dist/__tests__/components/die-history/roll-record-factory.spec.js +0 -43
  20. package/dist/__tests__/die.spec.js +0 -140
  21. package/dist/__tests__/types/roll-record.types.spec.js +0 -41
  22. package/dist/src/components/die-history/index.js +0 -2
  23. package/dist/src/components/die-history/internal/index.js +0 -2
  24. package/dist/src/components/die-history/internal/roll-record-manager/internal/index.js +0 -1
  25. package/dist/src/components/index.js +0 -1
  26. package/dist/src/types/index.js +0 -1
  27. /package/dist/{src/components → components}/die-history/internal/roll-record-manager/internal/roll-record-storage.js +0 -0
  28. /package/dist/{src/types → types}/roll-record.types.js +0 -0
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Persistent dice objects with roll history and TypeScript support. This package builds on top of `@platonic-dice/core` and provides classes such as `Die` which maintain roll history, validators, and utilities for consuming applications.
4
4
 
5
+ Version 2.1.0 adds support for `rollModTest()` - combining modifiers with test evaluation in a single method call.
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -18,7 +20,12 @@ const { DieType } = require("@platonic-dice/core");
18
20
 
19
21
  const d20 = new Die(DieType.D20);
20
22
  console.log(d20.roll());
21
- console.log(d20.history.getAll());
23
+ console.log(d20.history("normal"));
24
+
25
+ // New in 2.1.0: rollModTest combines modifier and test evaluation
26
+ const result = d20.rollModTest((n) => n + 5, { testType: "skill", target: 15 });
27
+ console.log(`Modified result: ${result}`);
28
+ console.log(d20.history("modifiedTest"));
22
29
  ```
23
30
 
24
31
  TypeScript:
@@ -29,6 +36,12 @@ import { DieType } from "@platonic-dice/core";
29
36
 
30
37
  const d20 = new Die(DieType.D20);
31
38
  console.log(d20.roll());
39
+
40
+ // Apply modifier and evaluate against test conditions
41
+ const result = d20.rollModTest((n) => n + 5, {
42
+ testType: "at_least",
43
+ target: 15,
44
+ });
32
45
  ```
33
46
 
34
47
  ## Build
@@ -1,4 +1,4 @@
1
- import { RollRecordManager, DEFAULT_MAX_RECORDS } from "./internal";
1
+ import { RollRecordManager, DEFAULT_MAX_RECORDS } from "./internal/index.js";
2
2
  /**
3
3
  * A wrapper for RollRecordManager that maintains multiple, independently capped histories.
4
4
  *
@@ -0,0 +1,2 @@
1
+ export { RollRecordFactory } from "./roll-record-factory.js";
2
+ export { HistoryCache } from "./history-cache.js";
@@ -0,0 +1,2 @@
1
+ export { RollRecordManager, DEFAULT_MAX_RECORDS } from "./roll-record-manager/index.js";
2
+ export * from "./roll-record-validator.js";
@@ -1 +1 @@
1
- export { RollRecordManager, DEFAULT_MAX_RECORDS } from "./roll-record-manager";
1
+ export { RollRecordManager, DEFAULT_MAX_RECORDS } from "./roll-record-manager.js";
@@ -0,0 +1 @@
1
+ export * from "./roll-record-storage.js";
@@ -1,5 +1,5 @@
1
- import { isDieRollRecord, isModifiedDieRollRecord, isTargetDieRollRecord, stripTimestamp, } from "../roll-record-validator";
2
- import { RollRecordStorage } from "./internal";
1
+ import { isDieRollRecord, isModifiedDieRollRecord, isTargetDieRollRecord, isModifiedTestDieRollRecord, stripTimestamp, } from "../roll-record-validator.js";
2
+ import { RollRecordStorage } from "./internal/index.js";
3
3
  /**
4
4
  * Default maximum number of roll records stored.
5
5
  */
@@ -66,8 +66,9 @@ export class RollRecordManager {
66
66
  }
67
67
  if (!isDieRollRecord(record) &&
68
68
  !isModifiedDieRollRecord(record) &&
69
- !isTargetDieRollRecord(record)) {
70
- throw new TypeError("Record must be a valid DieRollRecord, ModifiedDieRollRecord, or TargetDieRollRecord");
69
+ !isTargetDieRollRecord(record) &&
70
+ !isModifiedTestDieRollRecord(record)) {
71
+ throw new TypeError("Record must be a valid DieRollRecord, ModifiedDieRollRecord, TestDieRollRecord, or ModifiedTestDieRollRecord");
71
72
  }
72
73
  this.storage.add(record);
73
74
  }
@@ -1,4 +1,5 @@
1
- import { Outcome } from "@platonic-dice/core";
1
+ import core from "@platonic-dice/core";
2
+ const { Outcome } = core;
2
3
  /**
3
4
  * Runtime type-guards for RollRecord shapes.
4
5
  * Kept intentionally small and side-effect free.
@@ -22,6 +23,15 @@ export function isTargetDieRollRecord(record) {
22
23
  typeof record.roll === "number" &&
23
24
  "outcome" in record &&
24
25
  Object.values(Outcome).includes(record.outcome) &&
26
+ record.timestamp instanceof Date &&
27
+ !("modified" in record));
28
+ }
29
+ export function isModifiedTestDieRollRecord(record) {
30
+ return (!!record &&
31
+ typeof record.roll === "number" &&
32
+ typeof record.modified === "number" &&
33
+ "outcome" in record &&
34
+ Object.values(Outcome).includes(record.outcome) &&
25
35
  record.timestamp instanceof Date);
26
36
  }
27
37
  /**
@@ -39,5 +49,6 @@ export default {
39
49
  isDieRollRecord,
40
50
  isModifiedDieRollRecord,
41
51
  isTargetDieRollRecord,
52
+ isModifiedTestDieRollRecord,
42
53
  stripTimestamp,
43
54
  };
@@ -1,5 +1,5 @@
1
- import { RollType, roll as coreRoll, rollMod as coreRollMod, rollTest as coreRollTest, } from "@platonic-dice/core";
2
- import { isDieRollRecord, isModifiedDieRollRecord, isTargetDieRollRecord, } from "./internal";
1
+ import core, { RollType, roll as coreRoll, rollMod as coreRollMod, rollTest as coreRollTest, } from "@platonic-dice/core";
2
+ import { isDieRollRecord, isModifiedDieRollRecord, isTargetDieRollRecord, isModifiedTestDieRollRecord, } from "./internal/index.js";
3
3
  /**
4
4
  * Default implementation that delegates to `@platonic-dice/core` and uses
5
5
  * the system clock. This keeps the public API simple and avoids DI.
@@ -51,5 +51,21 @@ export class RollRecordFactory {
51
51
  }
52
52
  return record;
53
53
  }
54
+ createModifiedTestRoll(dieType, modifier, testConditions, rollType, options) {
55
+ // Delegate to core rollModTest which combines modifier and test logic
56
+ // Access via core object since it's not typed in dist-types
57
+ const { base, modified, outcome } = core.rollModTest(dieType, modifier, testConditions, rollType, options);
58
+ const ts = new Date();
59
+ const record = {
60
+ roll: base,
61
+ modified,
62
+ outcome,
63
+ timestamp: ts,
64
+ };
65
+ if (!isModifiedTestDieRollRecord(record)) {
66
+ throw new Error("Factory produced invalid ModifiedTestDieRollRecord");
67
+ }
68
+ return record;
69
+ }
54
70
  }
55
71
  export default RollRecordFactory;
@@ -0,0 +1 @@
1
+ export * from "./die-history/index.js";
@@ -1,5 +1,6 @@
1
- import { DieType, RollType } from "@platonic-dice/core";
2
- import { RollRecordFactory, HistoryCache } from "@dice/components";
1
+ import core from "@platonic-dice/core";
2
+ const { DieType, RollType } = core;
3
+ import { RollRecordFactory, HistoryCache } from "./components/index.js";
3
4
  /**
4
5
  * Represents a single die with flexible history tracking.
5
6
  *
@@ -163,6 +164,26 @@ export class Die {
163
164
  this.rolls.add(record);
164
165
  return record.roll;
165
166
  }
167
+ /**
168
+ * Perform a roll with a modifier and evaluate against test conditions.
169
+ * Combines rollMod and rollTest functionality.
170
+ *
171
+ * @param {RollModifierFunction | RollModifierInstance} modifier - Numeric or functional modifier applied to the base roll.
172
+ * @param {TestConditionsInstance | { testType: TestTypeValue; [k: string]: any }} testConditions - Test conditions (plain object or normalized `TestConditions` instance).
173
+ * @param {RollTypeValue} [rollType] - Optional roll mode (`RollType.Advantage` / `RollType.Disadvantage`).
174
+ * @param {{useNaturalCrits?: boolean}} [options] - Optional configuration for natural crits behavior.
175
+ * @returns {number} The modified numeric result.
176
+ * @throws {Error} If inputs are invalid (delegated to core).
177
+ * @example
178
+ * d20.rollModTest(n => n + 5, { testType: "at_least", target: 15 });
179
+ */
180
+ rollModTest(modifier, testConditions, rollType, options) {
181
+ const record = this.recordFactory.createModifiedTestRoll(this.typeValue, modifier, testConditions, rollType, options);
182
+ this.resultValue = record.modified;
183
+ this.rolls.setActiveKey(Die.MODIFIED_TEST_KEY);
184
+ this.rolls.add(record);
185
+ return record.modified;
186
+ }
166
187
  /**
167
188
  * Retrieve roll history for a given key.
168
189
  *
@@ -173,7 +194,7 @@ export class Die {
173
194
  /**
174
195
  * Retrieve roll history for a given key.
175
196
  *
176
- * @param {string} [key=Die.NORMAL_KEY] - One of "normal" | "modifier" | "test".
197
+ * @param {string} [key=Die.NORMAL_KEY] - One of "normal" | "modifier" | "test" | "modifiedTest".
177
198
  * @param {boolean} [verbose=false] - Include timestamps when true.
178
199
  * @returns {Array} Array of roll records (timestamps included when verbose).
179
200
  */
@@ -224,3 +245,4 @@ export class Die {
224
245
  Die.NORMAL_KEY = "normal";
225
246
  Die.MODIFIER_KEY = "modifier";
226
247
  Die.TEST_KEY = "test";
248
+ Die.MODIFIED_TEST_KEY = "modifiedTest";
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @module @platonic-dice/dice
3
+ * @description
4
+ * Persistent dice objects with roll history and TypeScript support.
5
+ */
6
+ // Main Die class
7
+ export { Die } from "./die.js";
8
+ // Re-export core types and values that consumers will need
9
+ // Note: Import from default due to CommonJS/ESM interop
10
+ import core from "@platonic-dice/core";
11
+ export const { DieType, RollType } = core;
12
+ // rollModTest is available on core but not typed in dist-types, export directly
13
+ export const rollModTest = core.rollModTest;
@@ -0,0 +1 @@
1
+ export * from "./roll-record.types.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platonic-dice/dice",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Persistent dice objects with roll history and TypeScript support.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,15 +33,16 @@
33
33
  },
34
34
  "homepage": "https://github.com/sjs2k20/platonic-dice.git#readme",
35
35
  "scripts": {
36
- "build": "tsc -p tsconfig.json",
36
+ "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
37
37
  "prepublishOnly": "npm run build",
38
38
  "test": "vitest run",
39
39
  "test:watch": "vitest"
40
40
  },
41
41
  "dependencies": {
42
- "@platonic-dice/core": "^2.0.2"
42
+ "@platonic-dice/core": "^2.1.0"
43
43
  },
44
44
  "devDependencies": {
45
+ "tsc-alias": "^1.8.16",
45
46
  "vite-tsconfig-paths": "^5.1.4",
46
47
  "vitest": "^4.0.9"
47
48
  }
@@ -1,106 +0,0 @@
1
- /// <reference types="vitest" />
2
- import { describe, it, expect, beforeEach } from "vitest";
3
- import { HistoryCache } from "@dice/components/die-history";
4
- describe("RollHistoryCache", () => {
5
- let cache;
6
- beforeEach(() => {
7
- cache = new HistoryCache({
8
- maxRecordsPerKey: 3,
9
- maxKeys: 2,
10
- });
11
- });
12
- it("starts empty", () => {
13
- expect(cache.activeManager).toBeUndefined();
14
- expect(cache.getAll()).toEqual([]);
15
- });
16
- it("throws if add called before setting active key", () => {
17
- expect(() => cache.add({ roll: 1, timestamp: new Date() })).toThrow(/No active history key set/);
18
- });
19
- it("creates and sets an active key", () => {
20
- cache.setActiveKey("first");
21
- expect(cache.activeManager).toBeDefined();
22
- expect(cache.getAll()).toEqual([]);
23
- });
24
- it("adds records to the active key", () => {
25
- cache.setActiveKey("key1");
26
- const record = { roll: 5, timestamp: new Date() };
27
- cache.add(record);
28
- const all = cache.getAll(true);
29
- expect(all.length).toBe(1);
30
- expect(all[0].roll).toBe(5);
31
- });
32
- it("respects maxRecordsPerKey", () => {
33
- cache.setActiveKey("key1");
34
- const now = new Date();
35
- for (let i = 1; i <= 5; i++) {
36
- cache.add({ roll: i, timestamp: new Date(now.getTime() + i) });
37
- }
38
- const all = cache.getAll(true);
39
- expect(all.length).toBe(3); // capped
40
- expect(all[0].roll).toBe(3); // oldest removed
41
- expect(all[2].roll).toBe(5);
42
- });
43
- it("switching keys creates independent histories", () => {
44
- cache.setActiveKey("A");
45
- cache.add({ roll: 1, timestamp: new Date() });
46
- cache.setActiveKey("B");
47
- cache.add({ roll: 2, timestamp: new Date() });
48
- cache.setActiveKey("A");
49
- expect(cache.getAll(true)[0].roll).toBe(1);
50
- cache.setActiveKey("B");
51
- expect(cache.getAll(true)[0].roll).toBe(2);
52
- });
53
- it("evicts oldest key when maxKeys exceeded", () => {
54
- cache.setActiveKey("K1");
55
- cache.add({ roll: 1, timestamp: new Date() });
56
- cache.setActiveKey("K2");
57
- cache.add({ roll: 2, timestamp: new Date() });
58
- // Max keys = 2, next key should evict K1
59
- cache.setActiveKey("K3");
60
- cache.add({ roll: 3, timestamp: new Date() });
61
- const report = cache.report({ verbose: true });
62
- expect(Object.keys(report)).not.toContain("K1");
63
- expect(Object.keys(report)).toContain("K2");
64
- expect(Object.keys(report)).toContain("K3");
65
- });
66
- it("clearActive clears only the active key", () => {
67
- cache.setActiveKey("A");
68
- cache.add({ roll: 1, timestamp: new Date() });
69
- cache.setActiveKey("B");
70
- cache.add({ roll: 2, timestamp: new Date() });
71
- cache.setActiveKey("A");
72
- cache.clearActive();
73
- expect(cache.getAll(true)).toEqual([]);
74
- cache.setActiveKey("B");
75
- expect(cache.getAll(true).length).toBe(1);
76
- });
77
- it("clearAll clears everything", () => {
78
- cache.setActiveKey("X");
79
- cache.add({ roll: 1, timestamp: new Date() });
80
- cache.setActiveKey("Y");
81
- cache.add({ roll: 2, timestamp: new Date() });
82
- cache.clearAll();
83
- expect(cache.getAll()).toEqual([]);
84
- expect(Object.keys(cache.report())).toEqual([]);
85
- });
86
- it("report returns correct verbose and non-verbose data", () => {
87
- cache.setActiveKey("R1");
88
- cache.add({ roll: 10, timestamp: new Date() });
89
- const reportsVerbose = cache.report({ verbose: true });
90
- const reportsNonVerbose = cache.report({ verbose: false });
91
- expect(reportsVerbose["R1"][0]).toHaveProperty("timestamp");
92
- expect(reportsNonVerbose["R1"][0]).not.toHaveProperty("timestamp");
93
- });
94
- it("toString returns a readable summary", () => {
95
- cache.setActiveKey("Active");
96
- expect(cache.toString()).toMatch(/HistoryCache: 1 keys \(active: Active\)/);
97
- });
98
- it("toJSON returns object with arrays of RollRecords", () => {
99
- cache.setActiveKey("J");
100
- const record = { roll: 7, timestamp: new Date() };
101
- cache.add(record);
102
- const json = cache.toJSON();
103
- expect(json).toHaveProperty("J");
104
- expect(json["J"][0].roll).toBe(7);
105
- });
106
- });
@@ -1,40 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import RollRecordStorage from "@dice/components/die-history/internal/roll-record-manager/internal/roll-record-storage";
3
- describe("RollRecordStorage", () => {
4
- it("evicts oldest when capacity exceeded", () => {
5
- const s = new RollRecordStorage(2);
6
- s.add({ roll: 1, timestamp: new Date() });
7
- s.add({ roll: 2, timestamp: new Date() });
8
- s.add({ roll: 3, timestamp: new Date() });
9
- expect(s.size).toBe(2);
10
- expect(s.full.map((r) => r.roll)).toEqual([2, 3]);
11
- });
12
- it("last returns correct slice and order", () => {
13
- const s = new RollRecordStorage(10);
14
- s.add({ roll: 1, timestamp: new Date() });
15
- s.add({ roll: 2, timestamp: new Date() });
16
- s.add({ roll: 3, timestamp: new Date() });
17
- s.add({ roll: 4, timestamp: new Date() });
18
- const last2 = s.last(2);
19
- expect(last2.map((r) => r.roll)).toEqual([3, 4]);
20
- });
21
- it("getters return array copies (mutating returned arrays doesn't affect storage)", () => {
22
- const s = new RollRecordStorage(3);
23
- s.add({ roll: 1, timestamp: new Date() });
24
- const full = s.full;
25
- full.pop();
26
- expect(s.size).toBe(1);
27
- const last = s.last(1);
28
- // mutating the returned array should not affect internal storage length
29
- last.pop();
30
- expect(s.size).toBe(1);
31
- });
32
- it("throws for invalid constructor args and parameters", () => {
33
- // call via any to test runtime throw without TS compile errors
34
- expect(() => new RollRecordStorage(0)).toThrow(TypeError);
35
- const s = new RollRecordStorage(2);
36
- expect(() => s.last(0)).toThrow(TypeError);
37
- // pass null to add
38
- expect(() => s.add(null)).toThrow(TypeError);
39
- });
40
- });
@@ -1,112 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { Outcome } from "@platonic-dice/core";
3
- import { DEFAULT_MAX_RECORDS, RollRecordManager, } from "@dice/components/die-history/internal";
4
- describe("RollRecordManager", () => {
5
- let manager;
6
- const dieRoll = {
7
- roll: 5,
8
- timestamp: new Date(),
9
- };
10
- const modifiedDieRoll = {
11
- roll: 3,
12
- modified: 1,
13
- timestamp: new Date(),
14
- };
15
- const targetDieRoll = {
16
- roll: 20,
17
- outcome: Outcome.Success,
18
- timestamp: new Date(),
19
- };
20
- beforeEach(() => {
21
- manager = new RollRecordManager();
22
- });
23
- it("should initialize empty with default maxRecords", () => {
24
- expect(manager.length).toBe(0);
25
- expect(manager.maxRecordsCount).toBe(DEFAULT_MAX_RECORDS);
26
- expect(manager.full).toEqual([]);
27
- expect(manager.all).toEqual([]);
28
- });
29
- it("should add DieRollRecord correctly", () => {
30
- manager.add(dieRoll);
31
- expect(manager.length).toBe(1);
32
- expect(manager.full[0]).toEqual(dieRoll);
33
- expect(manager.all[0]).toEqual({ roll: dieRoll.roll });
34
- });
35
- it("should add ModifiedDieRollRecord correctly", () => {
36
- manager.add(modifiedDieRoll);
37
- expect(manager.length).toBe(1);
38
- expect(manager.full[0]).toEqual(modifiedDieRoll);
39
- expect(manager.all[0]).toEqual({
40
- roll: modifiedDieRoll.roll,
41
- modified: modifiedDieRoll.modified,
42
- });
43
- });
44
- it("should add TargetDieRollRecord correctly", () => {
45
- manager.add(targetDieRoll);
46
- expect(manager.length).toBe(1);
47
- expect(manager.full[0]).toEqual(targetDieRoll);
48
- expect(manager.all[0]).toEqual({
49
- roll: targetDieRoll.roll,
50
- outcome: targetDieRoll.outcome,
51
- });
52
- });
53
- it("should throw TypeError for invalid records", () => {
54
- // @ts-expect-error testing runtime validation
55
- expect(() => manager.add({})).toThrow(TypeError);
56
- // @ts-expect-error testing runtime validation
57
- expect(() => manager.add(null)).toThrow(TypeError);
58
- // @ts-expect-error testing runtime validation
59
- expect(() => manager.add({ roll: 5, foo: "bar" })).toThrow(TypeError);
60
- });
61
- it("should maintain maxRecords correctly", () => {
62
- const smallManager = new RollRecordManager(2);
63
- smallManager.add({ roll: 1, timestamp: new Date() });
64
- smallManager.add({ roll: 2, timestamp: new Date() });
65
- smallManager.add({ roll: 3, timestamp: new Date() }); // should push out oldest
66
- expect(smallManager.length).toBe(2);
67
- expect(smallManager.full.map((r) => r.roll)).toEqual([2, 3]);
68
- });
69
- it("should return last N records with last()", () => {
70
- manager.add(dieRoll);
71
- manager.add(modifiedDieRoll);
72
- manager.add(targetDieRoll);
73
- const lastOne = manager.last();
74
- expect(lastOne).toEqual([
75
- { roll: targetDieRoll.roll, outcome: targetDieRoll.outcome },
76
- ]);
77
- const lastTwo = manager.last(2);
78
- expect(lastTwo).toEqual([
79
- { roll: modifiedDieRoll.roll, modified: modifiedDieRoll.modified },
80
- { roll: targetDieRoll.roll, outcome: targetDieRoll.outcome },
81
- ]);
82
- const verboseLastTwo = manager.last(2, true);
83
- expect(verboseLastTwo).toEqual([modifiedDieRoll, targetDieRoll]);
84
- });
85
- it("should report records with report()", () => {
86
- manager.add(dieRoll);
87
- manager.add(modifiedDieRoll);
88
- manager.add(targetDieRoll);
89
- expect(manager.report()).toEqual(manager.all);
90
- expect(manager.report({ verbose: true })).toEqual(manager.full);
91
- expect(manager.report({ limit: 2 })).toEqual(manager.all.slice(-2));
92
- expect(manager.report({ limit: 2, verbose: true })).toEqual(manager.full.slice(-2));
93
- });
94
- it("should clear records with clear()", () => {
95
- manager.add(dieRoll);
96
- manager.clear();
97
- expect(manager.length).toBe(0);
98
- expect(manager.full).toEqual([]);
99
- });
100
- it("toString() should reflect last roll", () => {
101
- expect(manager.toString()).toContain("empty");
102
- manager.add(dieRoll);
103
- const str = manager.toString();
104
- expect(str).toContain("1/");
105
- expect(str).toContain(dieRoll.roll.toString());
106
- expect(str).toContain(dieRoll.timestamp.toISOString());
107
- });
108
- it("toJSON() should return full records", () => {
109
- manager.add(dieRoll);
110
- expect(manager.toJSON()).toEqual(manager.full);
111
- });
112
- });
@@ -1,38 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { Outcome } from "@platonic-dice/core";
3
- import { isDieRollRecord, isModifiedDieRollRecord, isTargetDieRollRecord, stripTimestamp, } from "@dice/components/die-history/internal";
4
- describe("RollRecord validator", () => {
5
- const now = new Date();
6
- const dieRecord = {
7
- roll: 5,
8
- timestamp: now,
9
- };
10
- const modifiedRecord = {
11
- roll: 10,
12
- modified: 12,
13
- timestamp: now,
14
- };
15
- const targetRecord = {
16
- roll: 17,
17
- outcome: Outcome.Success,
18
- timestamp: now,
19
- };
20
- it("recognises valid shapes", () => {
21
- expect(isDieRollRecord(dieRecord)).toBe(true);
22
- expect(isModifiedDieRollRecord(modifiedRecord)).toBe(true);
23
- expect(isTargetDieRollRecord(targetRecord)).toBe(true);
24
- });
25
- it("rejects invalid shapes", () => {
26
- // missing timestamp
27
- expect(isDieRollRecord({ roll: 5 })).toBe(false);
28
- // wrong modified type
29
- expect(isModifiedDieRollRecord({ roll: 5, modified: "x" })).toBe(false);
30
- // invalid outcome
31
- expect(isTargetDieRollRecord({ roll: 5, outcome: "nope" })).toBe(false);
32
- });
33
- it("stripTimestamp removes timestamp and preserves other fields", () => {
34
- const stripped = stripTimestamp(dieRecord);
35
- expect(stripped.timestamp).toBeUndefined();
36
- expect(stripped.roll).toBe(dieRecord.roll);
37
- });
38
- });
@@ -1,43 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { RollRecordFactory } from "@dice/components/die-history";
3
- import { DieType } from "@platonic-dice/core";
4
- describe("RollRecordFactory", () => {
5
- const factory = new RollRecordFactory();
6
- it("should create a valid DieRollRecord for normal rolls", () => {
7
- const record = factory.createNormalRoll(DieType.D6);
8
- expect(record).toMatchObject({
9
- roll: expect.any(Number),
10
- timestamp: expect.any(Date),
11
- });
12
- expect(record.roll).toBeGreaterThanOrEqual(1);
13
- expect(record.roll).toBeLessThanOrEqual(6);
14
- });
15
- it("should create a valid ModifiedDieRollRecord for modified rolls", () => {
16
- const record = factory.createModifiedRoll(DieType.D20, (n) => n + 2);
17
- expect(record).toMatchObject({
18
- roll: expect.any(Number),
19
- modified: expect.any(Number),
20
- timestamp: expect.any(Date),
21
- });
22
- expect(record.roll).toBeGreaterThanOrEqual(1);
23
- expect(record.roll).toBeLessThanOrEqual(20);
24
- expect(record.modified).toBe(record.roll + 2);
25
- });
26
- it("should create a valid TestDieRollRecord for test rolls", () => {
27
- const record = factory.createTestRoll(DieType.D10, {
28
- testType: "at_least",
29
- target: 5,
30
- });
31
- expect(record).toMatchObject({
32
- roll: expect.any(Number),
33
- outcome: expect.any(String),
34
- timestamp: expect.any(Date),
35
- });
36
- expect(record.roll).toBeGreaterThanOrEqual(1);
37
- expect(record.roll).toBeLessThanOrEqual(10);
38
- expect(["Success", "Failure"].map((o) => o.toLowerCase())).toContain(record.outcome.toLowerCase());
39
- });
40
- it("should throw an error for invalid RollType in createNormalRoll", () => {
41
- expect(() => factory.createNormalRoll(DieType.D6, "InvalidRollType")).toThrow(TypeError);
42
- });
43
- });
@@ -1,140 +0,0 @@
1
- /// <reference types="vitest" />
2
- import { describe, it, expect, beforeEach, vi } from "vitest";
3
- import { Die } from "@dice/die";
4
- import { DieType, RollType, roll as coreRoll, rollMod as coreRollMod, rollTest as coreRollTest, } from "@platonic-dice/core";
5
- // ------------------------
6
- // MOCK @platonic-dice/core
7
- // ------------------------
8
- vi.mock("@platonic-dice/core", async (importOriginal) => {
9
- const actual = await importOriginal();
10
- return {
11
- ...actual,
12
- roll: vi.fn(),
13
- rollMod: vi.fn(),
14
- rollTest: vi.fn(),
15
- };
16
- });
17
- // ------------------------
18
- // TEST SUITE
19
- // ------------------------
20
- describe("Die class", () => {
21
- beforeEach(() => {
22
- vi.clearAllMocks();
23
- });
24
- // ---------------------
25
- // CONSTRUCTOR VALIDATION
26
- // ---------------------
27
- it("creates a die with a valid type", () => {
28
- const die = new Die(DieType.D6);
29
- expect(die.type).toBe(DieType.D6);
30
- expect(die.result).toBe(undefined);
31
- expect(die.history("normal")).toEqual([]);
32
- });
33
- it("throws for invalid die type", () => {
34
- expect(() => new Die("INVALID")).toThrowError(/Invalid die type/);
35
- });
36
- // ---------------------
37
- // FACE COUNT
38
- // ---------------------
39
- it("returns the correct faceCount", () => {
40
- expect(new Die(DieType.D4).faceCount).toBe(4);
41
- expect(new Die(DieType.D6).faceCount).toBe(6);
42
- expect(new Die(DieType.D20).faceCount).toBe(20);
43
- });
44
- // ---------------------
45
- // BASIC ROLL
46
- // ---------------------
47
- it("rolls and records a basic DieRollRecord", () => {
48
- const die = new Die(DieType.D6);
49
- coreRoll.mockReturnValue(4);
50
- const result = die.roll();
51
- expect(result).toBe(4);
52
- expect(die.result).toBe(4);
53
- const history = die.history("normal", true);
54
- expect(history.length).toBe(1);
55
- expect(history[0]).toMatchObject({ roll: 4 });
56
- });
57
- it("passes through rollType to coreRoll()", () => {
58
- const die = new Die(DieType.D20);
59
- coreRoll.mockReturnValue(17);
60
- die.roll(RollType.Advantage);
61
- expect(coreRoll).toHaveBeenCalledWith(DieType.D20, RollType.Advantage);
62
- });
63
- it("throws for invalid rollType", () => {
64
- const die = new Die(DieType.D8);
65
- expect(() => die.roll("INVALID")).toThrowError(/Invalid roll type/);
66
- });
67
- // ---------------------
68
- // MODIFIED ROLL
69
- // ---------------------
70
- it("rollMod records ModifiedDieRollRecord", () => {
71
- const die = new Die(DieType.D6);
72
- coreRollMod.mockReturnValue({ base: 3, modified: 5 });
73
- const result = die.rollMod((n) => n + 2);
74
- expect(result).toBe(5);
75
- expect(die.result).toBe(5);
76
- const history = die.history("modifier", true);
77
- expect(history.length).toBe(1);
78
- expect(history[0]).toMatchObject({ roll: 3, modified: 5 });
79
- });
80
- // ---------------------
81
- // TEST ROLL
82
- // ---------------------
83
- it("rollTest records TargetDieRollRecord", () => {
84
- const die = new Die(DieType.D6);
85
- coreRollTest.mockReturnValue({ base: 4, outcome: "success" });
86
- const result = die.rollTest({ testType: "at_least", target: 3 });
87
- expect(result).toBe(4);
88
- expect(die.result).toBe(4);
89
- const history = die.history("test", true);
90
- expect(history.length).toBe(1);
91
- expect(history[0]).toMatchObject({ roll: 4, outcome: "success" });
92
- });
93
- // ---------------------
94
- // HISTORY ACCESS
95
- // ---------------------
96
- it("history returns timestamp-stripped records when verbose=false", () => {
97
- const die = new Die(DieType.D6);
98
- coreRoll.mockReturnValue(2);
99
- die.roll();
100
- const hist = die.history("normal", false);
101
- expect(hist[0]).not.toHaveProperty("timestamp");
102
- const histVerbose = die.history("normal", true);
103
- expect(histVerbose[0]).toHaveProperty("timestamp");
104
- });
105
- // ---------------------
106
- // RESET
107
- // ---------------------
108
- it("reset clears latest result and optionally all histories", () => {
109
- const die = new Die(DieType.D6);
110
- coreRoll.mockReturnValue(5);
111
- die.roll();
112
- die.reset();
113
- expect(die.result).toBeUndefined();
114
- die.roll();
115
- die.reset(true);
116
- expect(die.result).toBeUndefined();
117
- expect(die.history("normal")).toEqual([]);
118
- expect(die.history("modifier")).toEqual([]);
119
- expect(die.history("test")).toEqual([]);
120
- });
121
- // ---------------------
122
- // toString / toJSON
123
- // ---------------------
124
- it("toString reports correctly", () => {
125
- const die = new Die(DieType.D6);
126
- expect(die.toString()).toMatch(/not rolled yet/);
127
- coreRoll.mockReturnValue(3);
128
- die.roll();
129
- expect(die.toString()).toMatch(/latest=3/);
130
- });
131
- it("toJSON returns all histories keyed by type", () => {
132
- const die = new Die(DieType.D6);
133
- coreRoll.mockReturnValue(4);
134
- die.roll();
135
- const json = die.toJSON();
136
- expect(json).toHaveProperty("normal");
137
- expect(json.normal.length).toBe(1);
138
- expect(json.normal[0]).toMatchObject({ roll: 4 });
139
- });
140
- });
@@ -1,41 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { Outcome } from "@platonic-dice/core/entities";
3
- describe("RollRecord types (runtime shape validation)", () => {
4
- const now = new Date();
5
- const dieRecord = {
6
- roll: 5,
7
- timestamp: now,
8
- };
9
- const modifiedRecord = {
10
- roll: 10,
11
- modified: 12,
12
- timestamp: now,
13
- };
14
- const targetRecord = {
15
- roll: 17,
16
- outcome: Outcome.Success,
17
- timestamp: now,
18
- };
19
- it("should have the correct shape for DieRollRecord", () => {
20
- expect(dieRecord).toHaveProperty("roll");
21
- expect(dieRecord).toHaveProperty("timestamp");
22
- expect(dieRecord.timestamp).toBeInstanceOf(Date);
23
- expect(Object.keys(dieRecord)).toHaveLength(2);
24
- });
25
- it("should have the correct shape for ModifiedDieRollRecord", () => {
26
- expect(modifiedRecord).toHaveProperty("roll");
27
- expect(modifiedRecord).toHaveProperty("modified");
28
- expect(modifiedRecord).toHaveProperty("timestamp");
29
- expect(modifiedRecord.timestamp).toBeInstanceOf(Date);
30
- });
31
- it("should have the correct shape for TargetDieRollRecord", () => {
32
- expect(targetRecord).toHaveProperty("roll");
33
- expect(targetRecord).toHaveProperty("outcome");
34
- expect(targetRecord).toHaveProperty("timestamp");
35
- expect(targetRecord.timestamp).toBeInstanceOf(Date);
36
- });
37
- it("should be compatible with the RollRecord union", () => {
38
- const records = [dieRecord, modifiedRecord, targetRecord];
39
- expect(records).toHaveLength(3);
40
- });
41
- });
@@ -1,2 +0,0 @@
1
- export { RollRecordFactory } from "./roll-record-factory";
2
- export { HistoryCache } from "./history-cache";
@@ -1,2 +0,0 @@
1
- export { RollRecordManager, DEFAULT_MAX_RECORDS } from "./roll-record-manager";
2
- export * from "./roll-record-validator";
@@ -1 +0,0 @@
1
- export * from "./roll-record-storage";
@@ -1 +0,0 @@
1
- export * from "./die-history";
@@ -1 +0,0 @@
1
- export * from "./roll-record.types";
File without changes