diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json index 90bc0b2..062b55f 100644 --- a/.sisyphus/boulder.json +++ b/.sisyphus/boulder.json @@ -8,7 +8,7 @@ "plan_name": "critical-refactor", "status": "active", "started_at": "2026-05-13T16:25:19.266Z", - "updated_at": "2026-05-13T16:33:52.668Z", + "updated_at": "2026-05-13T16:42:05.595Z", "session_ids": [ "ses_1de6ede59ffexUwt5C0sddEWwR" ], @@ -29,6 +29,19 @@ "updated_at": "2026-05-13T16:33:52.668Z", "ended_at": "2026-05-13T16:33:52.668Z", "elapsed_ms": 363792 + }, + "todo:4": { + "task_key": "todo:4", + "task_label": "4", + "task_title": "Create ResultBox.astro shared component", + "session_id": "ses_1ddce8961ffeoaXaB2Rxgt684X", + "agent": "Sisyphus-Junior", + "category": "visual-engineering", + "updated_at": "2026-05-13T16:42:05.595Z", + "started_at": "2026-05-13T16:40:57.448Z", + "status": "completed", + "ended_at": "2026-05-13T16:42:05.595Z", + "elapsed_ms": 68147 } } } @@ -36,7 +49,7 @@ "active_plan": "/home/emil/Desktop/Coding/AI/Randify.pro/.sisyphus/plans/critical-refactor.md", "started_at": "2026-05-13T16:25:19.266Z", "status": "active", - "updated_at": "2026-05-13T16:33:52.668Z", + "updated_at": "2026-05-13T16:42:05.595Z", "session_ids": [ "ses_1de6ede59ffexUwt5C0sddEWwR" ], @@ -57,6 +70,19 @@ "updated_at": "2026-05-13T16:33:52.668Z", "ended_at": "2026-05-13T16:33:52.668Z", "elapsed_ms": 363792 + }, + "todo:4": { + "task_key": "todo:4", + "task_label": "4", + "task_title": "Create ResultBox.astro shared component", + "session_id": "ses_1ddce8961ffeoaXaB2Rxgt684X", + "agent": "Sisyphus-Junior", + "category": "visual-engineering", + "updated_at": "2026-05-13T16:42:05.595Z", + "started_at": "2026-05-13T16:40:57.448Z", + "status": "completed", + "ended_at": "2026-05-13T16:42:05.595Z", + "elapsed_ms": 68147 } }, "agent": "atlas" diff --git a/.sisyphus/plans/critical-refactor.md b/.sisyphus/plans/critical-refactor.md index 3d397c1..b4755e3 100644 --- a/.sisyphus/plans/critical-refactor.md +++ b/.sisyphus/plans/critical-refactor.md @@ -336,7 +336,7 @@ Max Concurrent: 8 (Wave 3) - Message: `feat(lib): add client i18n helper with tests` - Files: `src/lib/client/i18n.ts`, `src/lib/client/i18n.test.ts` -- [ ] 4. Create ResultBox.astro shared component +- [x] 4. Create ResultBox.astro shared component **What to do**: - Create `src/components/ResultBox.astro` with: @@ -382,7 +382,7 @@ Max Concurrent: 8 (Wave 3) **Commit**: YES (groups with Task 5) -- [ ] 5. Create CopyButton.astro shared component +- [x] 5. Create CopyButton.astro shared component **What to do**: - Create `src/components/CopyButton.astro` with: @@ -728,7 +728,7 @@ Max Concurrent: 8 (Wave 3) - Message: `refactor(dice): deduplicate dice engine by importing from lib` - Files: `src/components/generators/DiceGenerator.astro` -- [ ] 34. Update dice-engine.ts to use crypto random +- [x] 34. Update dice-engine.ts to use crypto random **What to do**: - Replace all `Math.random()` in `src/lib/dice-engine.ts` with `random.ts` functions @@ -852,7 +852,7 @@ Max Concurrent: 8 (Wave 3) - Message: `ci(deploy): add test and lint gates, fix SSH security` - Files: `.github/workflows/deploy.yml` -- [ ] 37. Verify CI blocks deploy on failure +- [x] 37. Verify CI blocks deploy on failure **What to do**: - Verify updated `deploy.yml` workflow syntax diff --git a/src/components/CopyButton.astro b/src/components/CopyButton.astro new file mode 100644 index 0000000..3927c77 --- /dev/null +++ b/src/components/CopyButton.astro @@ -0,0 +1,70 @@ +--- +import { useT } from "@/i18n/translations"; +import type { Lang } from "@/i18n/translations"; + +interface Props { + text: string; + label?: string; +} + +const { text, label } = Astro.props; +const lang = (Astro.currentLocale as Lang) || "en"; +const T = useT(lang); +--- + + + + diff --git a/src/components/ResultBox.astro b/src/components/ResultBox.astro new file mode 100644 index 0000000..2b6925d --- /dev/null +++ b/src/components/ResultBox.astro @@ -0,0 +1,118 @@ +--- +export interface Props { + result: string; + label?: string; +} + +const { result, label } = Astro.props; +import { useT } from "../i18n/translations"; +import type { Lang } from "../i18n/translations"; + +const lang = (Astro.currentLocale as Lang) || "en"; +const T = useT(lang); +--- + +
+ { + label && ( +
{label}
+ ) + } +
+
+ + {result} + + +
+ +
+ {T.copied} +
+ + diff --git a/src/lib/dice-engine.test.ts b/src/lib/dice-engine.test.ts index c53c430..c4b4c86 100644 --- a/src/lib/dice-engine.test.ts +++ b/src/lib/dice-engine.test.ts @@ -6,6 +6,11 @@ import { buildNotation, isAdvancedNotation, } from "./dice-engine"; +import { randomInt } from "./client/random"; + +vi.mock("./client/random", () => ({ + randomInt: vi.fn(), +})); describe("parseDiceNotation", () => { it('parses basic notation "d6"', () => { @@ -104,25 +109,23 @@ describe("parseDiceNotation", () => { }); describe("rollDice", () => { - let mathRandomSpy: ReturnType; - afterEach(() => { - mathRandomSpy?.mockRestore(); + vi.mocked(randomInt).mockReset(); }); it("rolls basic dice", () => { - mathRandomSpy = vi.spyOn(Math, "random").mockReturnValue(0.5); + vi.mocked(randomInt).mockReturnValue(4); 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 + expect(result.total).toBe(12); }); it("applies modifier", () => { - mathRandomSpy = vi.spyOn(Math, "random").mockReturnValue(0.5); + vi.mocked(randomInt).mockReturnValue(11); const parsed = parseDiceNotation("1d20+5")!; const result = rollDice(parsed); @@ -130,11 +133,10 @@ describe("rollDice", () => { }); it("keeps highest rolls (kh)", () => { - mathRandomSpy = vi - .spyOn(Math, "random") - .mockReturnValueOnce(0.1) // 1 - .mockReturnValueOnce(0.9) // 6 - .mockReturnValueOnce(0.5); // 4 + vi.mocked(randomInt) + .mockReturnValueOnce(1) + .mockReturnValueOnce(6) + .mockReturnValueOnce(4); const parsed = parseDiceNotation("3d6kh2")!; const result = rollDice(parsed); @@ -146,11 +148,10 @@ describe("rollDice", () => { }); it("drops highest rolls (dh)", () => { - mathRandomSpy = vi - .spyOn(Math, "random") - .mockReturnValueOnce(0.1) // 1 - .mockReturnValueOnce(0.9) // 6 - .mockReturnValueOnce(0.5); // 4 + vi.mocked(randomInt) + .mockReturnValueOnce(1) + .mockReturnValueOnce(6) + .mockReturnValueOnce(4); const parsed = parseDiceNotation("3d6dh1")!; const result = rollDice(parsed); @@ -162,11 +163,10 @@ describe("rollDice", () => { }); 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) + vi.mocked(randomInt) + .mockReturnValueOnce(6) + .mockReturnValueOnce(6) + .mockReturnValueOnce(1); const parsed = parseDiceNotation("1d6!")!; const result = rollDice(parsed); @@ -177,10 +177,9 @@ describe("rollDice", () => { }); 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 + vi.mocked(randomInt) + .mockReturnValueOnce(6) + .mockReturnValueOnce(6); const parsed = parseDiceNotation("1d6!p")!; const result = rollDice(parsed); @@ -190,10 +189,9 @@ describe("rollDice", () => { }); it("handles reroll", () => { - mathRandomSpy = vi - .spyOn(Math, "random") - .mockReturnValueOnce(0.0) // first roll: 1 (reroll) - .mockReturnValueOnce(0.5); // reroll: 4 + vi.mocked(randomInt) + .mockReturnValueOnce(1) + .mockReturnValueOnce(4); const parsed = parseDiceNotation("1d6r1")!; const result = rollDice(parsed); @@ -203,10 +201,9 @@ describe("rollDice", () => { }); 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) + vi.mocked(randomInt) + .mockReturnValueOnce(1) + .mockReturnValueOnce(1); const parsed = parseDiceNotation("1d6ro1")!; const result = rollDice(parsed); @@ -217,17 +214,14 @@ describe("rollDice", () => { }); describe("rollAdvantage", () => { - let mathRandomSpy: ReturnType; - afterEach(() => { - mathRandomSpy?.mockRestore(); + vi.mocked(randomInt).mockReset(); }); it("returns highest for advantage", () => { - mathRandomSpy = vi - .spyOn(Math, "random") - .mockReturnValueOnce(0.1) // 3 - .mockReturnValueOnce(0.9); // 19 + vi.mocked(randomInt) + .mockReturnValueOnce(3) + .mockReturnValueOnce(19); const result = rollAdvantage(20, 0, true); expect(result.total).toBe(19); @@ -235,13 +229,12 @@ describe("rollAdvantage", () => { }); it("returns lowest for disadvantage", () => { - mathRandomSpy = vi - .spyOn(Math, "random") - .mockReturnValueOnce(0.9) // 19 - .mockReturnValueOnce(0.1); // 3 + vi.mocked(randomInt) + .mockReturnValueOnce(19) + .mockReturnValueOnce(3); const result = rollAdvantage(20, 2, false); - expect(result.total).toBe(5); // 3 + 2 + expect(result.total).toBe(5); expect(result.advantageRolls).toEqual([19, 3]); }); }); diff --git a/src/lib/dice-engine.ts b/src/lib/dice-engine.ts index 7e0f921..9ac7fcd 100644 --- a/src/lib/dice-engine.ts +++ b/src/lib/dice-engine.ts @@ -2,6 +2,8 @@ // Supports: XdY±Z, kh/kl/dh/dl, ! exploding, r reroll, advantage/disadvantage // Caps: explode chain ≤100, reroll recursion ≤1000 +import { randomInt } from "./client/random"; + type KeepDrop = { type: "kh" | "kl" | "dh" | "dl"; count: number } | null; type Explode = { @@ -159,7 +161,7 @@ export function parseDiceNotation(raw: string): Parsed | null { /** Roll a single die with optional reroll and explode logic */ function rollDie(sides: number, explode: Explode, reroll: Reroll): DieResult { - let value = Math.floor(Math.random() * sides) + 1; + let value = randomInt(1, sides); const original = value; let rerolledFrom: number | null = null; const explosions: number[] = []; @@ -170,7 +172,7 @@ function rollDie(sides: number, explode: Explode, reroll: Reroll): DieResult { rerolledFrom = value; let rerollCount = 0; while (reroll.values.has(value) && rerollCount < REROLL_CAP) { - value = Math.floor(Math.random() * sides) + 1; + value = randomInt(1, sides); rerollCount++; if (reroll.once) break; } @@ -181,7 +183,7 @@ function rollDie(sides: number, explode: Explode, reroll: Reroll): DieResult { exploded = true; let chain = 0; while (chain < EXPLODE_CAP) { - let next = Math.floor(Math.random() * sides) + 1; + let next = randomInt(1, sides); if (explode.penetrating) { next = Math.max(1, next - 1); // penetrating subtracts 1 } @@ -275,8 +277,8 @@ export function rollAdvantage( modifier: number, advantage: boolean, ): RollResult { - const r1 = Math.floor(Math.random() * sides) + 1; - const r2 = Math.floor(Math.random() * sides) + 1; + const r1 = randomInt(1, sides); + const r2 = randomInt(1, sides); const keptVal = advantage ? Math.max(r1, r2) : Math.min(r1, r2); return {