diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md index 2c940b3d8..2f3f00812 100644 --- a/docs_build/dev/BUILD_PR.md +++ b/docs_build/dev/BUILD_PR.md @@ -1,43 +1,39 @@ -# PR_26177_DELTA_054-random-utility +# PR_26177_DELTA_055-random-seed-enhancements ## Purpose -Add a nondeterministic `Random` utility with the same public convenience API shape as `RandomSeed`. +Enhance `RandomSeed` with matching procedural convenience methods. ## Source Of Truth -This `BUILD_PR.md`, `PLAN_PR.md`, the user request, and `docs_build/dev/ProjectInstructions.zip` are the source of truth for `PR_26177_DELTA_054-random-utility`. +This `BUILD_PR.md`, `PLAN_PR.md`, the user request, and `docs_build/dev/ProjectInstructions.zip` are the source of truth for `PR_26177_DELTA_055-random-seed-enhancements`. ## OWNER Override And Team Assignment -OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_054-random-utility`. +OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_055-random-seed-enhancements`. Team Delta owns Shared JS, runtime utilities, technical debt remediation, and runtime test coverage. ## Stack -- Base branch: `PR_26177_DELTA_053-random-shared-helpers` -- This PR depends on the internal random helper module from PR_053. +- Base branch: `PR_26177_DELTA_054-random-utility` +- This PR depends on the shared helper module from PR_053 and the stack context from PR_054. ## Exact Scope -- Add `Random` utility. -- Include: - - `Random.next()` - - `Random.nextInt(min, max)` - - `Random.nextFloat(min, max)` - - `Random.pick(array)` - - `Random.shuffle(array)` - - `Random.chance(percent)` - - `Random.weightedPick(weightedItems)` - - `Random.uuid()` -- Prefer `crypto.getRandomValues()` when available. -- Use `Math.random()` only as compatibility fallback. -- No deterministic seed support in `Random`. -- No browser storage. +- Update `RandomSeed` to use shared helper logic where appropriate. +- Add: + - `shuffle(array)` + - `chance(percent)` + - `weightedPick(weightedItems)` + - `saveState()` + - `restoreState(state)` +- Preserve existing `RandomSeed` sequence compatibility. +- Same seed must still reproduce same sequence. +- Reseeding must still reproduce same sequence. +- Add targeted unit tests for new methods. +- No adoption changes in existing game logic. - No UI changes. -- Add JSDoc. -- Add targeted unit tests. - Create required Codex reports under `docs_build/dev/reports/`. - Create repo-structured delta ZIP under `tmp/`. @@ -47,22 +43,21 @@ Team Delta owns Shared JS, runtime utilities, technical debt remediation, and ru - `docs_build/dev/BUILD_PR.md` - `docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md` - `docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md` -- `src/shared/math/Random.js` -- `tests/shared/Random.test.mjs` -- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility.md` -- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_branch-validation.md` -- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_requirement-checklist.md` -- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_validation-lane.md` -- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_manual-validation-notes.md` -- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_instruction-compliance-checklist.md` +- `src/shared/math/RandomSeed.js` +- `tests/shared/RandomSeed.test.mjs` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md` - `docs_build/dev/reports/codex_review.diff` - `docs_build/dev/reports/codex_changed_files.txt` ## Out Of Scope -- No deterministic seed support in `Random`. +- No seeded `next()` algorithm changes. - No existing game logic adoption changes. -- No existing `Math.random()` call-site replacements. - No UI changes. - No browser storage changes. - No API changes. @@ -75,9 +70,9 @@ Team Delta owns Shared JS, runtime utilities, technical debt remediation, and ru Run exactly: ```powershell -node ./scripts/run-node-test-files.mjs tests/shared/Random.test.mjs tests/shared/RandomHelpers.test.mjs -node --check src/shared/math/Random.js -node --check tests/shared/Random.test.mjs +node ./scripts/run-node-test-files.mjs tests/shared/RandomSeed.test.mjs tests/shared/RandomHelpers.test.mjs +node --check src/shared/math/RandomSeed.js +node --check tests/shared/RandomSeed.test.mjs git diff --check ``` @@ -88,5 +83,5 @@ Playwright is not required because this PR does not change UI or browser runtime Create repo-structured delta ZIP: ```text -tmp/PR_26177_DELTA_054-random-utility_delta.zip +tmp/PR_26177_DELTA_055-random-seed-enhancements_delta.zip ``` diff --git a/docs_build/dev/PLAN_PR.md b/docs_build/dev/PLAN_PR.md index c2b19c33e..87120e264 100644 --- a/docs_build/dev/PLAN_PR.md +++ b/docs_build/dev/PLAN_PR.md @@ -1,46 +1,42 @@ -# PLAN_PR: PR_26177_DELTA_054-random-utility +# PLAN_PR: PR_26177_DELTA_055-random-seed-enhancements ## Purpose -Add a nondeterministic `Random` utility with the same public convenience API shape as `RandomSeed`. +Enhance `RandomSeed` with matching procedural convenience methods. ## Owner And Assignment - Team: Delta -- OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_054-random-utility`. -- Stack base: `PR_26177_DELTA_053-random-shared-helpers`. +- OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_055-random-seed-enhancements`. +- Stack base: `PR_26177_DELTA_054-random-utility`. ## Scope -- Add `Random` utility. -- Include: - - `Random.next()` - - `Random.nextInt(min, max)` - - `Random.nextFloat(min, max)` - - `Random.pick(array)` - - `Random.shuffle(array)` - - `Random.chance(percent)` - - `Random.weightedPick(weightedItems)` - - `Random.uuid()` -- Prefer `crypto.getRandomValues()` when available. -- Use `Math.random()` only as compatibility fallback. -- No deterministic seed support in `Random`. -- No browser storage. +- Update `RandomSeed` to use shared helper logic where appropriate. +- Add: + - `shuffle(array)` + - `chance(percent)` + - `weightedPick(weightedItems)` + - `saveState()` + - `restoreState(state)` +- Preserve existing `RandomSeed` sequence compatibility. +- Same seed must still reproduce same sequence. +- Reseeding must still reproduce same sequence. +- Add targeted unit tests for new methods. +- No adoption changes in existing game logic. - No UI changes. -- Add JSDoc. -- Add targeted unit tests. ## Implementation Plan -1. Add `src/shared/math/Random.js`. -2. Reuse internal helper functions from `src/shared/math/randomHelpers.js`. -3. Add `tests/shared/Random.test.mjs`. -4. Validate crypto preference, Math fallback, utility methods, UUID shape, and absence of seed API. +1. Update `src/shared/math/RandomSeed.js` to reuse `randomHelpers.js` for procedural helper methods. +2. Keep the existing seeded `next()` algorithm unchanged. +3. Add state save/restore methods. +4. Extend `tests/shared/RandomSeed.test.mjs` with sequence compatibility, new method, and state tests. 5. Produce required PR reports and repo-structured ZIP. ## Acceptance Criteria -- `Random` exposes the required static convenience methods. -- `Random` does not expose deterministic seed support. -- `Random` uses `crypto.getRandomValues()` when available. -- `Math.random()` is used only when crypto random values are unavailable. +- Existing seeded sequence values remain compatible. +- Same seed and reseeding behavior remain deterministic. +- New procedural methods use the same seeded random stream. +- State save/restore resumes the sequence from the saved point. diff --git a/docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md b/docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md index a546aa1d5..9dcd7244d 100644 --- a/docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md +++ b/docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md @@ -31,7 +31,7 @@ If a team has no assignment, no active branch, and no active PR, it is inactive | Team Alfa | none | none | none | Available | Active ownership lane | | Team Bravo | none | none | none | Available | Active ownership lane | | Team Charlie | none | none | none | Available | Active ownership lane | -| Team Delta | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | Active | OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_054-random-utility | +| Team Delta | PR_26177_DELTA_055-random-seed-enhancements | PR_26177_DELTA_055-random-seed-enhancements | PR_26177_DELTA_055-random-seed-enhancements | Active | OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_055-random-seed-enhancements | | Team Golf | none | none | none | Available | Replacement active ownership lane for retired Team Gamma | | Team OWNER | none | none | none | Available | Governance Phase 1 complete | diff --git a/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md b/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md index ed14e49b5..3b49412c4 100644 --- a/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md +++ b/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md @@ -7,7 +7,7 @@ | Team Alfa | none | none | none | Available | | Team Bravo | none | none | none | Available | | Team Charlie | none | none | none | Available | -| Team Delta | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | Active | +| Team Delta | PR_26177_DELTA_055-random-seed-enhancements | PR_26177_DELTA_055-random-seed-enhancements | PR_26177_DELTA_055-random-seed-enhancements | Active | | Team Golf | none | none | none | Available | | Team OWNER | none | none | none | Available | @@ -50,13 +50,13 @@ Current OWNER clarification: Status: Active -Active assignment: PR_26177_DELTA_054-random-utility. +Active assignment: PR_26177_DELTA_055-random-seed-enhancements. -Active branch: PR_26177_DELTA_054-random-utility. +Active branch: PR_26177_DELTA_055-random-seed-enhancements. -Active PR: PR_26177_DELTA_054-random-utility. +Active PR: PR_26177_DELTA_055-random-seed-enhancements. -OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_054-random-utility. +OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_055-random-seed-enhancements. ## Team Bravo diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md new file mode 100644 index 000000000..06cb0f4d3 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md @@ -0,0 +1,50 @@ +# PR_26177_DELTA_055-random-seed-enhancements + +Date: 2026-06-26 +Team: Delta +Scope: RandomSeed procedural enhancements and targeted unit tests +Status: PASS + +## Summary + +- Updated `RandomSeed` to use shared helper logic for `nextInt`, `nextFloat`, and `pick`. +- Added seeded procedural methods: `shuffle`, `chance`, and `weightedPick`. +- Added `saveState()` and `restoreState(state)` for deterministic sequence checkpointing. +- Preserved the existing `RandomSeed.next()` algorithm. +- Added a hardcoded compatibility check for the existing `RandomSeed(42)` sequence. +- Verified same-seed and reseeding reproducibility still pass. +- Added targeted tests for the new methods and state restore behavior. +- No existing game logic adoption changes or UI changes were made. + +## Branch Validation + +PASS. Branch `PR_26177_DELTA_055-random-seed-enhancements` was created from `PR_26177_DELTA_054-random-utility`. + +## Changed Files + +- `docs_build/dev/PLAN_PR.md` +- `docs_build/dev/BUILD_PR.md` +- `docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md` +- `docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md` +- `src/shared/math/RandomSeed.js` +- `tests/shared/RandomSeed.test.mjs` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md` +- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md` +- `docs_build/dev/reports/codex_changed_files.txt` +- `docs_build/dev/reports/codex_review.diff` + +## Validation + +- PASS: `node ./scripts/run-node-test-files.mjs tests/shared/RandomSeed.test.mjs tests/shared/RandomHelpers.test.mjs` +- PASS: `node --check src/shared/math/RandomSeed.js` +- PASS: `node --check tests/shared/RandomSeed.test.mjs` +- PASS: `git diff --check` +- SKIP: Playwright was not run because this PR does not change UI or browser runtime flows. + +## Artifact + +- `tmp/PR_26177_DELTA_055-random-seed-enhancements_delta.zip` diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md new file mode 100644 index 000000000..76dc5c9ae --- /dev/null +++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md @@ -0,0 +1,15 @@ +# PR_26177_DELTA_055-random-seed-enhancements Branch Validation + +Status: PASS + +## Start Gates + +- PASS: PR_055 branch was created from `PR_26177_DELTA_054-random-utility`. +- PASS: Stack base was clean before branch creation. +- PASS: Branch name is `PR_26177_DELTA_055-random-seed-enhancements`. + +## Scope Confirmation + +- PASS: Work is assigned to Team Delta. +- PASS: Work is limited to `RandomSeed` procedural enhancements, targeted tests, PR docs, reports, and ZIP packaging. +- PASS: No existing game logic adoption changes, UI changes, API changes, database changes, or unrelated cleanup were made. diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md new file mode 100644 index 000000000..b5eb1a3c5 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md @@ -0,0 +1,11 @@ +# PR_26177_DELTA_055-random-seed-enhancements Instruction Compliance Checklist + +| Instruction | Status | Notes | +|---|---:|---| +| Continue stacked Random PR sequence | PASS | Branch was created from PR_054. | +| Keep one PR purpose only | PASS | Scope is RandomSeed enhancements. | +| Preserve sequence compatibility | PASS | Existing `RandomSeed(42)` sequence is asserted in tests. | +| No adoption changes in existing game logic | PASS | No call sites were changed. | +| No UI changes | PASS | No UI files changed. | +| Produce required reports | PASS | Reports were added under `docs_build/dev/reports/`. | +| Produce repo-structured ZIP under `tmp/` | PASS | ZIP path is `tmp/PR_26177_DELTA_055-random-seed-enhancements_delta.zip`. | diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md new file mode 100644 index 000000000..441a079e9 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md @@ -0,0 +1,11 @@ +# PR_26177_DELTA_055-random-seed-enhancements Manual Validation Notes + +Status: PASS + +Manual review confirmed: + +- `RandomSeed.next()` was not changed. +- The hardcoded `RandomSeed(42)` sequence remains compatible with PR_052 behavior. +- New procedural methods consume the same seeded stream through shared helpers. +- `saveState()` and `restoreState(state)` are in-memory and serializable; no browser storage was added. +- No UI, API, database, or existing game logic adoption files changed. diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md new file mode 100644 index 000000000..a65ee9a91 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md @@ -0,0 +1,16 @@ +# PR_26177_DELTA_055-random-seed-enhancements Requirement Checklist + +| Requirement | Status | Notes | +|---|---:|---| +| Update `RandomSeed` to use shared helper logic where appropriate | PASS | `nextInt`, `nextFloat`, `pick`, and new methods use `randomHelpers.js`. | +| Add `shuffle(array)` | PASS | Added seeded shuffle method. | +| Add `chance(percent)` | PASS | Added seeded chance method. | +| Add `weightedPick(weightedItems)` | PASS | Added seeded weighted pick method. | +| Add `saveState()` | PASS | Added serializable state capture. | +| Add `restoreState(state)` | PASS | Added state restore with validation. | +| Preserve existing `RandomSeed` sequence compatibility | PASS | Hardcoded `RandomSeed(42)` sequence check passes. | +| Same seed still reproduces same sequence | PASS | Existing targeted test still passes. | +| Reseeding still reproduces same sequence | PASS | Existing targeted test still passes. | +| Add targeted unit tests for new methods | PASS | Extended `tests/shared/RandomSeed.test.mjs`. | +| No adoption changes in existing game logic | PASS | No game logic call sites changed. | +| No UI changes | PASS | No UI files changed. | diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md new file mode 100644 index 000000000..d2203f234 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md @@ -0,0 +1,24 @@ +# PR_26177_DELTA_055-random-seed-enhancements Validation Lane + +Status: PASS + +## Commands + +```powershell +node ./scripts/run-node-test-files.mjs tests/shared/RandomSeed.test.mjs tests/shared/RandomHelpers.test.mjs +node --check src/shared/math/RandomSeed.js +node --check tests/shared/RandomSeed.test.mjs +git diff --check +``` + +## Results + +- PASS: `tests/shared/RandomSeed.test.mjs` +- PASS: `tests/shared/RandomHelpers.test.mjs` +- PASS: `src/shared/math/RandomSeed.js` syntax check +- PASS: `tests/shared/RandomSeed.test.mjs` syntax check +- PASS: `git diff --check` + +## Playwright + +SKIP. Playwright was not run because this PR does not change UI or browser runtime flows. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index a74a7832a..25c946493 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -2,13 +2,13 @@ docs_build/dev/BUILD_PR.md docs_build/dev/PLAN_PR.md docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md -docs_build/dev/reports/PR_26177_DELTA_054-random-utility.md -docs_build/dev/reports/PR_26177_DELTA_054-random-utility_branch-validation.md -docs_build/dev/reports/PR_26177_DELTA_054-random-utility_instruction-compliance-checklist.md -docs_build/dev/reports/PR_26177_DELTA_054-random-utility_manual-validation-notes.md -docs_build/dev/reports/PR_26177_DELTA_054-random-utility_requirement-checklist.md -docs_build/dev/reports/PR_26177_DELTA_054-random-utility_validation-lane.md +docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md +docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md +docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md +docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md +docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md +docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md docs_build/dev/reports/codex_changed_files.txt docs_build/dev/reports/codex_review.diff -src/shared/math/Random.js -tests/shared/Random.test.mjs +src/shared/math/RandomSeed.js +tests/shared/RandomSeed.test.mjs diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 7360627e0..6ab211802 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,212 +1,219 @@ diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md -index 0a265576e..2c940b3d8 100644 +index 2c940b3d8..2f3f00812 100644 --- a/docs_build/dev/BUILD_PR.md +++ b/docs_build/dev/BUILD_PR.md -@@ -1,31 +1,42 @@ --# PR_26177_DELTA_053-random-shared-helpers -+# PR_26177_DELTA_054-random-utility +@@ -1,43 +1,39 @@ +-# PR_26177_DELTA_054-random-utility ++# PR_26177_DELTA_055-random-seed-enhancements ## Purpose --Create shared internal helper logic for random utility operations. -+Add a nondeterministic `Random` utility with the same public convenience API shape as `RandomSeed`. +-Add a nondeterministic `Random` utility with the same public convenience API shape as `RandomSeed`. ++Enhance `RandomSeed` with matching procedural convenience methods. ## Source Of Truth --This `BUILD_PR.md`, `PLAN_PR.md`, the user request, and `docs_build/dev/ProjectInstructions.zip` are the source of truth for `PR_26177_DELTA_053-random-shared-helpers`. -+This `BUILD_PR.md`, `PLAN_PR.md`, the user request, and `docs_build/dev/ProjectInstructions.zip` are the source of truth for `PR_26177_DELTA_054-random-utility`. +-This `BUILD_PR.md`, `PLAN_PR.md`, the user request, and `docs_build/dev/ProjectInstructions.zip` are the source of truth for `PR_26177_DELTA_054-random-utility`. ++This `BUILD_PR.md`, `PLAN_PR.md`, the user request, and `docs_build/dev/ProjectInstructions.zip` are the source of truth for `PR_26177_DELTA_055-random-seed-enhancements`. ## OWNER Override And Team Assignment --OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_053-random-shared-helpers`. -+OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_054-random-utility`. +-OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_054-random-utility`. ++OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_055-random-seed-enhancements`. Team Delta owns Shared JS, runtime utilities, technical debt remediation, and runtime test coverage. -+## Stack -+ -+- Base branch: `PR_26177_DELTA_053-random-shared-helpers` -+- This PR depends on the internal random helper module from PR_053. -+ + ## Stack + +-- Base branch: `PR_26177_DELTA_053-random-shared-helpers` +-- This PR depends on the internal random helper module from PR_053. ++- Base branch: `PR_26177_DELTA_054-random-utility` ++- This PR depends on the shared helper module from PR_053 and the stack context from PR_054. + ## Exact Scope --- Add internal/shared helper functions for: -- - `nextInt(randomNext, min, max)` -- - `nextFloat(randomNext, min, max)` -- - `pick(randomNext, array)` -- - `shuffle(randomNext, array)` -- - `chance(randomNext, percent)` -- - `weightedPick(randomNext, weightedItems)` --- Helper must consume a `randomNext` function returning float `>= 0` and `< 1`. --- Do not expose this as Creator-facing API. --- Do not change existing `RandomSeed` behavior. -+- Add `Random` utility. -+- Include: -+ - `Random.next()` -+ - `Random.nextInt(min, max)` -+ - `Random.nextFloat(min, max)` -+ - `Random.pick(array)` -+ - `Random.shuffle(array)` -+ - `Random.chance(percent)` -+ - `Random.weightedPick(weightedItems)` -+ - `Random.uuid()` -+- Prefer `crypto.getRandomValues()` when available. -+- Use `Math.random()` only as compatibility fallback. -+- No deterministic seed support in `Random`. -+- No browser storage. -+- No UI changes. -+- Add JSDoc. - - Add targeted unit tests. +-- Add `Random` utility. +-- Include: +- - `Random.next()` +- - `Random.nextInt(min, max)` +- - `Random.nextFloat(min, max)` +- - `Random.pick(array)` +- - `Random.shuffle(array)` +- - `Random.chance(percent)` +- - `Random.weightedPick(weightedItems)` +- - `Random.uuid()` +-- Prefer `crypto.getRandomValues()` when available. +-- Use `Math.random()` only as compatibility fallback. +-- No deterministic seed support in `Random`. +-- No browser storage. ++- Update `RandomSeed` to use shared helper logic where appropriate. ++- Add: ++ - `shuffle(array)` ++ - `chance(percent)` ++ - `weightedPick(weightedItems)` ++ - `saveState()` ++ - `restoreState(state)` ++- Preserve existing `RandomSeed` sequence compatibility. ++- Same seed must still reproduce same sequence. ++- Reseeding must still reproduce same sequence. ++- Add targeted unit tests for new methods. ++- No adoption changes in existing game logic. + - No UI changes. +-- Add JSDoc. +-- Add targeted unit tests. - Create required Codex reports under `docs_build/dev/reports/`. - Create repo-structured delta ZIP under `tmp/`. -@@ -36,21 +47,21 @@ Team Delta owns Shared JS, runtime utilities, technical debt remediation, and ru + +@@ -47,22 +43,21 @@ Team Delta owns Shared JS, runtime utilities, technical debt remediation, and ru - `docs_build/dev/BUILD_PR.md` - `docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md` - `docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md` --- `src/shared/math/randomHelpers.js` --- `tests/shared/RandomHelpers.test.mjs` --- `docs_build/dev/reports/PR_26177_DELTA_053-random-shared-helpers.md` --- `docs_build/dev/reports/PR_26177_DELTA_053-random-shared-helpers_branch-validation.md` --- `docs_build/dev/reports/PR_26177_DELTA_053-random-shared-helpers_requirement-checklist.md` --- `docs_build/dev/reports/PR_26177_DELTA_053-random-shared-helpers_validation-lane.md` --- `docs_build/dev/reports/PR_26177_DELTA_053-random-shared-helpers_manual-validation-notes.md` --- `docs_build/dev/reports/PR_26177_DELTA_053-random-shared-helpers_instruction-compliance-checklist.md` -+- `src/shared/math/Random.js` -+- `tests/shared/Random.test.mjs` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_branch-validation.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_requirement-checklist.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_validation-lane.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_manual-validation-notes.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_instruction-compliance-checklist.md` +-- `src/shared/math/Random.js` +-- `tests/shared/Random.test.mjs` +-- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility.md` +-- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_branch-validation.md` +-- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_requirement-checklist.md` +-- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_validation-lane.md` +-- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_manual-validation-notes.md` +-- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_instruction-compliance-checklist.md` ++- `src/shared/math/RandomSeed.js` ++- `tests/shared/RandomSeed.test.mjs` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md` - `docs_build/dev/reports/codex_review.diff` - `docs_build/dev/reports/codex_changed_files.txt` ## Out Of Scope --- No Creator-facing API exposure. --- No existing `RandomSeed` behavior changes. -+- No deterministic seed support in `Random`. -+- No existing game logic adoption changes. - - No existing `Math.random()` call-site replacements. +-- No deterministic seed support in `Random`. ++- No seeded `next()` algorithm changes. + - No existing game logic adoption changes. +-- No existing `Math.random()` call-site replacements. - No UI changes. - No browser storage changes. -@@ -64,9 +75,9 @@ Team Delta owns Shared JS, runtime utilities, technical debt remediation, and ru + - No API changes. +@@ -75,9 +70,9 @@ Team Delta owns Shared JS, runtime utilities, technical debt remediation, and ru Run exactly: ```powershell --node ./scripts/run-node-test-files.mjs tests/shared/RandomHelpers.test.mjs tests/shared/RandomSeed.test.mjs --node --check src/shared/math/randomHelpers.js --node --check tests/shared/RandomHelpers.test.mjs -+node ./scripts/run-node-test-files.mjs tests/shared/Random.test.mjs tests/shared/RandomHelpers.test.mjs -+node --check src/shared/math/Random.js -+node --check tests/shared/Random.test.mjs +-node ./scripts/run-node-test-files.mjs tests/shared/Random.test.mjs tests/shared/RandomHelpers.test.mjs +-node --check src/shared/math/Random.js +-node --check tests/shared/Random.test.mjs ++node ./scripts/run-node-test-files.mjs tests/shared/RandomSeed.test.mjs tests/shared/RandomHelpers.test.mjs ++node --check src/shared/math/RandomSeed.js ++node --check tests/shared/RandomSeed.test.mjs git diff --check ``` -@@ -77,5 +88,5 @@ Playwright is not required because this PR does not change UI or browser runtime +@@ -88,5 +83,5 @@ Playwright is not required because this PR does not change UI or browser runtime Create repo-structured delta ZIP: ```text --tmp/PR_26177_DELTA_053-random-shared-helpers_delta.zip -+tmp/PR_26177_DELTA_054-random-utility_delta.zip +-tmp/PR_26177_DELTA_054-random-utility_delta.zip ++tmp/PR_26177_DELTA_055-random-seed-enhancements_delta.zip ``` diff --git a/docs_build/dev/PLAN_PR.md b/docs_build/dev/PLAN_PR.md -index 55fef9a6a..c2b19c33e 100644 +index c2b19c33e..87120e264 100644 --- a/docs_build/dev/PLAN_PR.md +++ b/docs_build/dev/PLAN_PR.md -@@ -1,40 +1,46 @@ --# PLAN_PR: PR_26177_DELTA_053-random-shared-helpers -+# PLAN_PR: PR_26177_DELTA_054-random-utility +@@ -1,46 +1,42 @@ +-# PLAN_PR: PR_26177_DELTA_054-random-utility ++# PLAN_PR: PR_26177_DELTA_055-random-seed-enhancements ## Purpose --Create shared internal helper logic for random utility operations. -+Add a nondeterministic `Random` utility with the same public convenience API shape as `RandomSeed`. +-Add a nondeterministic `Random` utility with the same public convenience API shape as `RandomSeed`. ++Enhance `RandomSeed` with matching procedural convenience methods. ## Owner And Assignment - Team: Delta --- OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_053-random-shared-helpers`. --- Ownership fit: Team Delta owns Shared JS, runtime utilities, technical debt remediation, and runtime test coverage. -+- OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_054-random-utility`. -+- Stack base: `PR_26177_DELTA_053-random-shared-helpers`. +-- OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_054-random-utility`. +-- Stack base: `PR_26177_DELTA_053-random-shared-helpers`. ++- OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_055-random-seed-enhancements`. ++- Stack base: `PR_26177_DELTA_054-random-utility`. ## Scope --- Add internal/shared helper functions for: -- - `nextInt(randomNext, min, max)` -- - `nextFloat(randomNext, min, max)` -- - `pick(randomNext, array)` -- - `shuffle(randomNext, array)` -- - `chance(randomNext, percent)` -- - `weightedPick(randomNext, weightedItems)` --- Helper functions must consume a `randomNext` function returning a float `>= 0` and `< 1`. --- Do not expose these helpers as Creator-facing API. --- Do not change existing `RandomSeed` behavior. -+- Add `Random` utility. -+- Include: -+ - `Random.next()` -+ - `Random.nextInt(min, max)` -+ - `Random.nextFloat(min, max)` -+ - `Random.pick(array)` -+ - `Random.shuffle(array)` -+ - `Random.chance(percent)` -+ - `Random.weightedPick(weightedItems)` -+ - `Random.uuid()` -+- Prefer `crypto.getRandomValues()` when available. -+- Use `Math.random()` only as compatibility fallback. -+- No deterministic seed support in `Random`. -+- No browser storage. -+- No UI changes. -+- Add JSDoc. - - Add targeted unit tests. +-- Add `Random` utility. +-- Include: +- - `Random.next()` +- - `Random.nextInt(min, max)` +- - `Random.nextFloat(min, max)` +- - `Random.pick(array)` +- - `Random.shuffle(array)` +- - `Random.chance(percent)` +- - `Random.weightedPick(weightedItems)` +- - `Random.uuid()` +-- Prefer `crypto.getRandomValues()` when available. +-- Use `Math.random()` only as compatibility fallback. +-- No deterministic seed support in `Random`. +-- No browser storage. ++- Update `RandomSeed` to use shared helper logic where appropriate. ++- Add: ++ - `shuffle(array)` ++ - `chance(percent)` ++ - `weightedPick(weightedItems)` ++ - `saveState()` ++ - `restoreState(state)` ++- Preserve existing `RandomSeed` sequence compatibility. ++- Same seed must still reproduce same sequence. ++- Reseeding must still reproduce same sequence. ++- Add targeted unit tests for new methods. ++- No adoption changes in existing game logic. + - No UI changes. +-- Add JSDoc. +-- Add targeted unit tests. ## Implementation Plan --1. Add `src/shared/math/randomHelpers.js`. --2. Add `tests/shared/RandomHelpers.test.mjs`. --3. Validate helper behavior and input guards with targeted unit tests. --4. Preserve current `RandomSeed` implementation and tests unchanged. -+1. Add `src/shared/math/Random.js`. -+2. Reuse internal helper functions from `src/shared/math/randomHelpers.js`. -+3. Add `tests/shared/Random.test.mjs`. -+4. Validate crypto preference, Math fallback, utility methods, UUID shape, and absence of seed API. +-1. Add `src/shared/math/Random.js`. +-2. Reuse internal helper functions from `src/shared/math/randomHelpers.js`. +-3. Add `tests/shared/Random.test.mjs`. +-4. Validate crypto preference, Math fallback, utility methods, UUID shape, and absence of seed API. ++1. Update `src/shared/math/RandomSeed.js` to reuse `randomHelpers.js` for procedural helper methods. ++2. Keep the existing seeded `next()` algorithm unchanged. ++3. Add state save/restore methods. ++4. Extend `tests/shared/RandomSeed.test.mjs` with sequence compatibility, new method, and state tests. 5. Produce required PR reports and repo-structured ZIP. ## Acceptance Criteria --- Helpers use only the supplied `randomNext` source. --- Integer, float, pick, shuffle, chance, and weighted pick operations are covered. --- Invalid `randomNext`, ranges, arrays, percentages, and weighted item inputs reject predictably. --- Existing `RandomSeed` behavior remains unchanged. -+- `Random` exposes the required static convenience methods. -+- `Random` does not expose deterministic seed support. -+- `Random` uses `crypto.getRandomValues()` when available. -+- `Math.random()` is used only when crypto random values are unavailable. +-- `Random` exposes the required static convenience methods. +-- `Random` does not expose deterministic seed support. +-- `Random` uses `crypto.getRandomValues()` when available. +-- `Math.random()` is used only when crypto random values are unavailable. ++- Existing seeded sequence values remain compatible. ++- Same seed and reseeding behavior remain deterministic. ++- New procedural methods use the same seeded random stream. ++- State save/restore resumes the sequence from the saved point. diff --git a/docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md b/docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md -index cf0a714b8..a546aa1d5 100644 +index a546aa1d5..9dcd7244d 100644 --- a/docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md +++ b/docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md @@ -31,7 +31,7 @@ If a team has no assignment, no active branch, and no active PR, it is inactive | Team Alfa | none | none | none | Available | Active ownership lane | | Team Bravo | none | none | none | Available | Active ownership lane | | Team Charlie | none | none | none | Available | Active ownership lane | --| Team Delta | PR_26177_DELTA_053-random-shared-helpers | PR_26177_DELTA_053-random-shared-helpers | PR_26177_DELTA_053-random-shared-helpers | Active | OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_053-random-shared-helpers | -+| Team Delta | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | Active | OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_054-random-utility | +-| Team Delta | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | Active | OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_054-random-utility | ++| Team Delta | PR_26177_DELTA_055-random-seed-enhancements | PR_26177_DELTA_055-random-seed-enhancements | PR_26177_DELTA_055-random-seed-enhancements | Active | OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_055-random-seed-enhancements | | Team Golf | none | none | none | Available | Replacement active ownership lane for retired Team Gamma | | Team OWNER | none | none | none | Available | Governance Phase 1 complete | diff --git a/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md b/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md -index bd334569a..ed14e49b5 100644 +index ed14e49b5..3b49412c4 100644 --- a/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md +++ b/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md @@ -7,7 +7,7 @@ | Team Alfa | none | none | none | Available | | Team Bravo | none | none | none | Available | | Team Charlie | none | none | none | Available | --| Team Delta | PR_26177_DELTA_053-random-shared-helpers | PR_26177_DELTA_053-random-shared-helpers | PR_26177_DELTA_053-random-shared-helpers | Active | -+| Team Delta | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | Active | +-| Team Delta | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | PR_26177_DELTA_054-random-utility | Active | ++| Team Delta | PR_26177_DELTA_055-random-seed-enhancements | PR_26177_DELTA_055-random-seed-enhancements | PR_26177_DELTA_055-random-seed-enhancements | Active | | Team Golf | none | none | none | Available | | Team OWNER | none | none | none | Available | @@ -214,46 +221,250 @@ index bd334569a..ed14e49b5 100644 Status: Active --Active assignment: PR_26177_DELTA_053-random-shared-helpers. -+Active assignment: PR_26177_DELTA_054-random-utility. +-Active assignment: PR_26177_DELTA_054-random-utility. ++Active assignment: PR_26177_DELTA_055-random-seed-enhancements. --Active branch: PR_26177_DELTA_053-random-shared-helpers. -+Active branch: PR_26177_DELTA_054-random-utility. +-Active branch: PR_26177_DELTA_054-random-utility. ++Active branch: PR_26177_DELTA_055-random-seed-enhancements. --Active PR: PR_26177_DELTA_053-random-shared-helpers. -+Active PR: PR_26177_DELTA_054-random-utility. +-Active PR: PR_26177_DELTA_054-random-utility. ++Active PR: PR_26177_DELTA_055-random-seed-enhancements. --OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_053-random-shared-helpers. -+OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_054-random-utility. +-OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_054-random-utility. ++OWNER override approved: Continue Team Delta random utility stack with PR_26177_DELTA_055-random-seed-enhancements. ## Team Bravo -diff --git a/docs_build/dev/reports/PR_26177_DELTA_054-random-utility.md b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility.md + +diff --git a/src/shared/math/RandomSeed.js b/src/shared/math/RandomSeed.js +index 19d948644..606bc0738 100644 +--- a/src/shared/math/RandomSeed.js ++++ b/src/shared/math/RandomSeed.js +@@ -1,3 +1,12 @@ ++import { ++ chance as randomChance, ++ nextFloat as randomNextFloat, ++ nextInt as randomNextInt, ++ pick as randomPick, ++ shuffle as randomShuffle, ++ weightedPick as randomWeightedPick, ++} from "./randomHelpers.js"; ++ + const UINT32_RANGE = 0x100000000; + const FNV_OFFSET_BASIS = 0x811c9dc5; + const FNV_PRIME = 0x01000193; +@@ -14,21 +23,6 @@ function normalizeSeedValue(value) { + return hash >>> 0; + } + +-function assertFiniteNumber(value, name) { +- if (!Number.isFinite(value)) { +- throw new TypeError(`${name} must be a finite number.`); +- } +-} +- +-function assertOrderedRange(min, max) { +- assertFiniteNumber(min, "min"); +- assertFiniteNumber(max, "max"); +- +- if (max < min) { +- throw new RangeError("max must be greater than or equal to min."); +- } +-} +- + /** + * Deterministic seeded pseudo-random number generator for repeatable game and + * tool workflows. `RandomSeed` is intentionally opt-in and does not replace +@@ -76,15 +70,7 @@ export class RandomSeed { + * @returns {number} A deterministic integer inside the requested range. + */ + nextInt(min, max) { +- assertOrderedRange(min, max); +- const lower = Math.ceil(min); +- const upper = Math.floor(max); +- +- if (upper < lower) { +- throw new RangeError("integer range must include at least one integer."); +- } +- +- return Math.floor(this.next() * (upper - lower + 1)) + lower; ++ return randomNextInt(() => this.next(), min, max); + } + + /** +@@ -95,8 +81,7 @@ export class RandomSeed { + * @returns {number} A deterministic floating-point number inside the range. + */ + nextFloat(min, max) { +- assertOrderedRange(min, max); +- return min + this.next() * (max - min); ++ return randomNextFloat(() => this.next(), min, max); + } + + /** +@@ -107,15 +92,65 @@ export class RandomSeed { + * @returns {T} The selected array item. + */ + pick(array) { +- if (!Array.isArray(array)) { +- throw new TypeError("array must be an array."); +- } ++ return randomPick(() => this.next(), array); ++ } ++ ++ /** ++ * Returns a deterministically shuffled copy of an array. ++ * ++ * @template T ++ * @param {T[]} array Source array to shuffle. ++ * @returns {T[]} Shuffled copy. ++ */ ++ shuffle(array) { ++ return randomShuffle(() => this.next(), array); ++ } + +- if (array.length === 0) { +- throw new RangeError("array must contain at least one item."); ++ /** ++ * Returns true when the deterministic roll falls within the percent chance. ++ * ++ * @param {number} percent Percent chance from 0 through 100. ++ * @returns {boolean} Whether the chance roll succeeds. ++ */ ++ chance(percent) { ++ return randomChance(() => this.next(), percent); ++ } ++ ++ /** ++ * Selects one deterministic weighted item. ++ * ++ * @template T ++ * @param {{item?: T, value?: T, weight: number}[]} weightedItems Weighted item entries. ++ * @returns {T} Selected item. ++ */ ++ weightedPick(weightedItems) { ++ return randomWeightedPick(() => this.next(), weightedItems); ++ } ++ ++ /** ++ * Captures the current generator state for later restoration. ++ * ++ * @returns {{state: number}} Serializable generator state. ++ */ ++ saveState() { ++ return { state: this._state >>> 0 }; ++ } ++ ++ /** ++ * Restores a generator state previously returned by `saveState()`. ++ * ++ * @param {{state: number}} state Saved generator state. ++ * @returns {RandomSeed} This generator instance for chaining. ++ */ ++ restoreState(state) { ++ const stateValue = Number(state?.state); ++ ++ if (!Number.isInteger(stateValue) || stateValue < 0 || stateValue > 0xffffffff) { ++ throw new RangeError("state.state must be an unsigned 32-bit integer."); + } + +- return array[this.nextInt(0, array.length - 1)]; ++ this._state = stateValue >>> 0; ++ return this; + } + } + +diff --git a/tests/shared/RandomSeed.test.mjs b/tests/shared/RandomSeed.test.mjs +index b61a44157..23b5f6459 100644 +--- a/tests/shared/RandomSeed.test.mjs ++++ b/tests/shared/RandomSeed.test.mjs +@@ -16,6 +16,15 @@ export function run() { + const second = new RandomSeed("level-1"); + assert.deepEqual(takeSequence(first, 5), takeSequence(second, 5)); + ++ assert.deepEqual(takeSequence(new RandomSeed(42), 6), [ ++ 0.3077305785845965, ++ 0.3676118436269462, ++ 0.23133554426021874, ++ 0.01758907549083233, ++ 0.009130497695878148, ++ 0.2082449474837631, ++ ]); ++ + first.seed("level-1"); + second.seed("different-level"); + assert.notDeepEqual(takeSequence(first, 5), takeSequence(second, 5)); +@@ -46,10 +55,44 @@ export function run() { + Array.from({ length: 8 }, () => pickerB.pick(pickSource)) + ); + ++ const shuffleSource = ["a", "b", "c", "d"]; ++ const shuffleA = new RandomSeed("shuffle-seed"); ++ const shuffleB = new RandomSeed("shuffle-seed"); ++ assert.deepEqual(shuffleA.shuffle(shuffleSource), shuffleB.shuffle(shuffleSource)); ++ assert.deepEqual(shuffleSource, ["a", "b", "c", "d"]); ++ ++ const chanceA = new RandomSeed("chance-seed"); ++ const chanceB = new RandomSeed("chance-seed"); ++ assert.deepEqual( ++ Array.from({ length: 8 }, () => chanceA.chance(35)), ++ Array.from({ length: 8 }, () => chanceB.chance(35)) ++ ); ++ ++ const weightedItems = [ ++ { item: "common", weight: 7 }, ++ { item: "rare", weight: 3 }, ++ ]; ++ const weightedA = new RandomSeed("weighted-seed"); ++ const weightedB = new RandomSeed("weighted-seed"); ++ assert.deepEqual( ++ Array.from({ length: 8 }, () => weightedA.weightedPick(weightedItems)), ++ Array.from({ length: 8 }, () => weightedB.weightedPick(weightedItems)) ++ ); ++ ++ const stateSeed = new RandomSeed("state-seed"); ++ stateSeed.next(); ++ const savedState = stateSeed.saveState(); ++ const expectedAfterState = takeSequence(stateSeed, 4); ++ assert.equal(stateSeed.restoreState(savedState), stateSeed); ++ assert.deepEqual(takeSequence(stateSeed, 4), expectedAfterState); ++ + assert.throws(() => new RandomSeed("empty").pick([]), RangeError); + assert.throws(() => new RandomSeed("bad-array").pick("not-array"), TypeError); + assert.throws(() => new RandomSeed("bad-int").nextInt(5, 2), RangeError); + assert.throws(() => new RandomSeed("bad-float").nextFloat(0, Number.POSITIVE_INFINITY), TypeError); ++ assert.throws(() => new RandomSeed("bad-chance").chance(101), RangeError); ++ assert.throws(() => new RandomSeed("bad-weight").weightedPick([{ item: "none", weight: 0 }]), RangeError); ++ assert.throws(() => new RandomSeed("bad-state").restoreState({ state: -1 }), RangeError); + } + + if (import.meta.url === `file://${process.argv[1]}`) { +diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md new file mode 100644 -index 000000000..0d1641316 +index 000000000..06cb0f4d3 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility.md ++++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md @@ -0,0 +1,50 @@ -+# PR_26177_DELTA_054-random-utility ++# PR_26177_DELTA_055-random-seed-enhancements + +Date: 2026-06-26 +Team: Delta -+Scope: Nondeterministic shared Random utility and targeted unit tests ++Scope: RandomSeed procedural enhancements and targeted unit tests +Status: PASS + +## Summary + -+- Added `Random` as a nondeterministic shared random utility in `src/shared/math/Random.js`. -+- Added static convenience methods matching the public shape requested: `next`, `nextInt`, `nextFloat`, `pick`, `shuffle`, `chance`, `weightedPick`, and `uuid`. -+- Reused internal helper logic from PR_053. -+- Preferred `crypto.getRandomValues()` when available. -+- Used `Math.random()` only as compatibility fallback when crypto random values are unavailable. -+- Did not add deterministic seed support to `Random`. -+- Added targeted unit tests in `tests/shared/Random.test.mjs`. -+- No browser storage, UI, API, database, or existing game logic adoption changes were made. ++- Updated `RandomSeed` to use shared helper logic for `nextInt`, `nextFloat`, and `pick`. ++- Added seeded procedural methods: `shuffle`, `chance`, and `weightedPick`. ++- Added `saveState()` and `restoreState(state)` for deterministic sequence checkpointing. ++- Preserved the existing `RandomSeed.next()` algorithm. ++- Added a hardcoded compatibility check for the existing `RandomSeed(42)` sequence. ++- Verified same-seed and reseeding reproducibility still pass. ++- Added targeted tests for the new methods and state restore behavior. ++- No existing game logic adoption changes or UI changes were made. + +## Branch Validation + -+PASS. Branch `PR_26177_DELTA_054-random-utility` was created from `PR_26177_DELTA_053-random-shared-helpers`. ++PASS. Branch `PR_26177_DELTA_055-random-seed-enhancements` was created from `PR_26177_DELTA_054-random-utility`. + +## Changed Files + @@ -261,411 +472,132 @@ index 000000000..0d1641316 +- `docs_build/dev/BUILD_PR.md` +- `docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md` +- `docs_build/dev/ProjectInstructions/team_assignments/ACTIVE_TEAM_REGISTRY.md` -+- `src/shared/math/Random.js` -+- `tests/shared/Random.test.mjs` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_branch-validation.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_requirement-checklist.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_validation-lane.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_manual-validation-notes.md` -+- `docs_build/dev/reports/PR_26177_DELTA_054-random-utility_instruction-compliance-checklist.md` ++- `src/shared/math/RandomSeed.js` ++- `tests/shared/RandomSeed.test.mjs` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md` ++- `docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md` +- `docs_build/dev/reports/codex_changed_files.txt` +- `docs_build/dev/reports/codex_review.diff` + +## Validation + -+- PASS: `node ./scripts/run-node-test-files.mjs tests/shared/Random.test.mjs tests/shared/RandomHelpers.test.mjs` -+- PASS: `node --check src/shared/math/Random.js` -+- PASS: `node --check tests/shared/Random.test.mjs` ++- PASS: `node ./scripts/run-node-test-files.mjs tests/shared/RandomSeed.test.mjs tests/shared/RandomHelpers.test.mjs` ++- PASS: `node --check src/shared/math/RandomSeed.js` ++- PASS: `node --check tests/shared/RandomSeed.test.mjs` +- PASS: `git diff --check` +- SKIP: Playwright was not run because this PR does not change UI or browser runtime flows. + +## Artifact + -+- `tmp/PR_26177_DELTA_054-random-utility_delta.zip` -diff --git a/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_branch-validation.md b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_branch-validation.md ++- `tmp/PR_26177_DELTA_055-random-seed-enhancements_delta.zip` +diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md new file mode 100644 -index 000000000..8915ce567 +index 000000000..76dc5c9ae --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_branch-validation.md ++++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_branch-validation.md @@ -0,0 +1,15 @@ -+# PR_26177_DELTA_054-random-utility Branch Validation ++# PR_26177_DELTA_055-random-seed-enhancements Branch Validation + +Status: PASS + +## Start Gates + -+- PASS: PR_054 branch was created from `PR_26177_DELTA_053-random-shared-helpers`. ++- PASS: PR_055 branch was created from `PR_26177_DELTA_054-random-utility`. +- PASS: Stack base was clean before branch creation. -+- PASS: Branch name is `PR_26177_DELTA_054-random-utility`. ++- PASS: Branch name is `PR_26177_DELTA_055-random-seed-enhancements`. + +## Scope Confirmation + +- PASS: Work is assigned to Team Delta. -+- PASS: Work is limited to nondeterministic Random utility, targeted tests, PR docs, reports, and ZIP packaging. -+- PASS: No deterministic seed support, UI, browser storage, API, database, or unrelated cleanup changes were made. -diff --git a/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_instruction-compliance-checklist.md b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_instruction-compliance-checklist.md ++- PASS: Work is limited to `RandomSeed` procedural enhancements, targeted tests, PR docs, reports, and ZIP packaging. ++- PASS: No existing game logic adoption changes, UI changes, API changes, database changes, or unrelated cleanup were made. +diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md new file mode 100644 -index 000000000..4c8ffc09a +index 000000000..b5eb1a3c5 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_instruction-compliance-checklist.md ++++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_instruction-compliance-checklist.md @@ -0,0 +1,11 @@ -+# PR_26177_DELTA_054-random-utility Instruction Compliance Checklist ++# PR_26177_DELTA_055-random-seed-enhancements Instruction Compliance Checklist + +| Instruction | Status | Notes | +|---|---:|---| -+| Continue stacked Random PR sequence | PASS | Branch was created from PR_053. | -+| Keep one PR purpose only | PASS | Scope is nondeterministic Random utility. | -+| Use shared helper logic from PR_053 | PASS | Random delegates convenience methods to `randomHelpers.js`. | -+| Do not add deterministic seed support | PASS | No seed API or seed state was added. | -+| No UI/browser storage/API/database changes | PASS | No such files or behaviors changed. | ++| Continue stacked Random PR sequence | PASS | Branch was created from PR_054. | ++| Keep one PR purpose only | PASS | Scope is RandomSeed enhancements. | ++| Preserve sequence compatibility | PASS | Existing `RandomSeed(42)` sequence is asserted in tests. | ++| No adoption changes in existing game logic | PASS | No call sites were changed. | ++| No UI changes | PASS | No UI files changed. | +| Produce required reports | PASS | Reports were added under `docs_build/dev/reports/`. | -+| Produce repo-structured ZIP under `tmp/` | PASS | ZIP path is `tmp/PR_26177_DELTA_054-random-utility_delta.zip`. | -diff --git a/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_manual-validation-notes.md ++| Produce repo-structured ZIP under `tmp/` | PASS | ZIP path is `tmp/PR_26177_DELTA_055-random-seed-enhancements_delta.zip`. | +diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md new file mode 100644 -index 000000000..e7a29b57a +index 000000000..441a079e9 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_manual-validation-notes.md ++++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_manual-validation-notes.md @@ -0,0 +1,11 @@ -+# PR_26177_DELTA_054-random-utility Manual Validation Notes ++# PR_26177_DELTA_055-random-seed-enhancements Manual Validation Notes + +Status: PASS + +Manual review confirmed: + -+- `Random` is nondeterministic and does not support seeding. -+- `Random` prefers `crypto.getRandomValues()` and falls back to `Math.random()` only when crypto random values are unavailable. -+- Public methods match the requested convenience API shape. -+- `Random` does not use browser storage and is not wired into UI or existing game logic. -+- Tests mock crypto and Math fallback sources and restore globals after assertions. -diff --git a/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_requirement-checklist.md b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_requirement-checklist.md ++- `RandomSeed.next()` was not changed. ++- The hardcoded `RandomSeed(42)` sequence remains compatible with PR_052 behavior. ++- New procedural methods consume the same seeded stream through shared helpers. ++- `saveState()` and `restoreState(state)` are in-memory and serializable; no browser storage was added. ++- No UI, API, database, or existing game logic adoption files changed. +diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md new file mode 100644 -index 000000000..3a4232a4b +index 000000000..a65ee9a91 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_requirement-checklist.md -@@ -0,0 +1,20 @@ -+# PR_26177_DELTA_054-random-utility Requirement Checklist ++++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_requirement-checklist.md +@@ -0,0 +1,16 @@ ++# PR_26177_DELTA_055-random-seed-enhancements Requirement Checklist + +| Requirement | Status | Notes | +|---|---:|---| -+| Add `Random` utility | PASS | Added `src/shared/math/Random.js`. | -+| Include `Random.next()` | PASS | Static method added. | -+| Include `Random.nextInt(min, max)` | PASS | Static method added using shared helper. | -+| Include `Random.nextFloat(min, max)` | PASS | Static method added using shared helper. | -+| Include `Random.pick(array)` | PASS | Static method added using shared helper. | -+| Include `Random.shuffle(array)` | PASS | Static method added using shared helper. | -+| Include `Random.chance(percent)` | PASS | Static method added using shared helper. | -+| Include `Random.weightedPick(weightedItems)` | PASS | Static method added using shared helper. | -+| Include `Random.uuid()` | PASS | Static RFC 4122 v4 UUID method added. | -+| Prefer `crypto.getRandomValues()` when available | PASS | `Random.next()` and `Random.uuid()` use crypto random values when present. | -+| Use `Math.random()` only as compatibility fallback | PASS | Fallback path is used only when crypto random values are unavailable. | -+| No deterministic seed support in `Random` | PASS | No `seed` method or seed state added. | -+| No browser storage | PASS | No storage usage added. | ++| Update `RandomSeed` to use shared helper logic where appropriate | PASS | `nextInt`, `nextFloat`, `pick`, and new methods use `randomHelpers.js`. | ++| Add `shuffle(array)` | PASS | Added seeded shuffle method. | ++| Add `chance(percent)` | PASS | Added seeded chance method. | ++| Add `weightedPick(weightedItems)` | PASS | Added seeded weighted pick method. | ++| Add `saveState()` | PASS | Added serializable state capture. | ++| Add `restoreState(state)` | PASS | Added state restore with validation. | ++| Preserve existing `RandomSeed` sequence compatibility | PASS | Hardcoded `RandomSeed(42)` sequence check passes. | ++| Same seed still reproduces same sequence | PASS | Existing targeted test still passes. | ++| Reseeding still reproduces same sequence | PASS | Existing targeted test still passes. | ++| Add targeted unit tests for new methods | PASS | Extended `tests/shared/RandomSeed.test.mjs`. | ++| No adoption changes in existing game logic | PASS | No game logic call sites changed. | +| No UI changes | PASS | No UI files changed. | -+| Add JSDoc | PASS | Added class and public method JSDoc. | -+| Add targeted unit tests | PASS | Added `tests/shared/Random.test.mjs`. | -diff --git a/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_validation-lane.md b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_validation-lane.md +diff --git a/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md new file mode 100644 -index 000000000..09eb20a34 +index 000000000..d2203f234 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_DELTA_054-random-utility_validation-lane.md ++++ b/docs_build/dev/reports/PR_26177_DELTA_055-random-seed-enhancements_validation-lane.md @@ -0,0 +1,24 @@ -+# PR_26177_DELTA_054-random-utility Validation Lane ++# PR_26177_DELTA_055-random-seed-enhancements Validation Lane + +Status: PASS + +## Commands + +```powershell -+node ./scripts/run-node-test-files.mjs tests/shared/Random.test.mjs tests/shared/RandomHelpers.test.mjs -+node --check src/shared/math/Random.js -+node --check tests/shared/Random.test.mjs ++node ./scripts/run-node-test-files.mjs tests/shared/RandomSeed.test.mjs tests/shared/RandomHelpers.test.mjs ++node --check src/shared/math/RandomSeed.js ++node --check tests/shared/RandomSeed.test.mjs +git diff --check +``` + +## Results + -+- PASS: `tests/shared/Random.test.mjs` ++- PASS: `tests/shared/RandomSeed.test.mjs` +- PASS: `tests/shared/RandomHelpers.test.mjs` -+- PASS: `src/shared/math/Random.js` syntax check -+- PASS: `tests/shared/Random.test.mjs` syntax check ++- PASS: `src/shared/math/RandomSeed.js` syntax check ++- PASS: `tests/shared/RandomSeed.test.mjs` syntax check +- PASS: `git diff --check` + +## Playwright + +SKIP. Playwright was not run because this PR does not change UI or browser runtime flows. -diff --git a/src/shared/math/Random.js b/src/shared/math/Random.js -new file mode 100644 -index 000000000..84f38782a ---- /dev/null -+++ b/src/shared/math/Random.js -@@ -0,0 +1,151 @@ -+import { -+ chance as randomChance, -+ nextFloat as randomNextFloat, -+ nextInt as randomNextInt, -+ pick as randomPick, -+ shuffle as randomShuffle, -+ weightedPick as randomWeightedPick, -+} from "./randomHelpers.js"; -+ -+const UINT32_RANGE = 0x100000000; -+const UUID_BYTE_LENGTH = 16; -+ -+function getCryptoSource() { -+ const cryptoSource = globalThis.crypto; -+ return cryptoSource && typeof cryptoSource.getRandomValues === "function" ? cryptoSource : null; -+} -+ -+function randomUint32() { -+ const cryptoSource = getCryptoSource(); -+ if (cryptoSource) { -+ const values = new Uint32Array(1); -+ cryptoSource.getRandomValues(values); -+ return values[0] >>> 0; -+ } -+ -+ return Math.floor(Math.random() * UINT32_RANGE) >>> 0; -+} -+ -+function fillRandomBytes(bytes) { -+ const cryptoSource = getCryptoSource(); -+ if (cryptoSource) { -+ cryptoSource.getRandomValues(bytes); -+ return bytes; -+ } -+ -+ for (let index = 0; index < bytes.length; index += 1) { -+ bytes[index] = Math.floor(Math.random() * 256); -+ } -+ -+ return bytes; -+} -+ -+function byteToHex(byte) { -+ return byte.toString(16).padStart(2, "0"); -+} -+ -+function formatUuid(bytes) { -+ bytes[6] = (bytes[6] & 0x0f) | 0x40; -+ bytes[8] = (bytes[8] & 0x3f) | 0x80; -+ -+ const hex = Array.from(bytes, byteToHex); -+ return [ -+ hex.slice(0, 4).join(""), -+ hex.slice(4, 6).join(""), -+ hex.slice(6, 8).join(""), -+ hex.slice(8, 10).join(""), -+ hex.slice(10, 16).join(""), -+ ].join("-"); -+} -+ -+/** -+ * Nondeterministic random utility for convenience operations that do not need -+ * seeded repeatability. `Random` prefers `crypto.getRandomValues()` and uses -+ * `Math.random()` only as a compatibility fallback. -+ */ -+export class Random { -+ /** -+ * Returns a nondeterministic number in the range [0, 1). -+ * -+ * @returns {number} Nondeterministic random number. -+ */ -+ static next() { -+ return randomUint32() / UINT32_RANGE; -+ } -+ -+ /** -+ * Returns a nondeterministic integer between min and max, inclusive. -+ * -+ * @param {number} min Inclusive lower bound. -+ * @param {number} max Inclusive upper bound. -+ * @returns {number} Random integer in the requested range. -+ */ -+ static nextInt(min, max) { -+ return randomNextInt(() => Random.next(), min, max); -+ } -+ -+ /** -+ * Returns a nondeterministic floating-point number in the range [min, max). -+ * -+ * @param {number} min Inclusive lower bound. -+ * @param {number} max Exclusive upper bound. -+ * @returns {number} Random float in the requested range. -+ */ -+ static nextFloat(min, max) { -+ return randomNextFloat(() => Random.next(), min, max); -+ } -+ -+ /** -+ * Selects one nondeterministic item from a non-empty array. -+ * -+ * @template T -+ * @param {T[]} array Source items. -+ * @returns {T} Selected item. -+ */ -+ static pick(array) { -+ return randomPick(() => Random.next(), array); -+ } -+ -+ /** -+ * Returns a nondeterministically shuffled copy of an array. -+ * -+ * @template T -+ * @param {T[]} array Source items. -+ * @returns {T[]} Shuffled copy. -+ */ -+ static shuffle(array) { -+ return randomShuffle(() => Random.next(), array); -+ } -+ -+ /** -+ * Returns true when a nondeterministic roll falls within the percent chance. -+ * -+ * @param {number} percent Percent chance from 0 through 100. -+ * @returns {boolean} Whether the chance roll succeeds. -+ */ -+ static chance(percent) { -+ return randomChance(() => Random.next(), percent); -+ } -+ -+ /** -+ * Picks one nondeterministic weighted item. -+ * -+ * @template T -+ * @param {{item?: T, value?: T, weight: number}[]} weightedItems Weighted item entries. -+ * @returns {T} Selected item. -+ */ -+ static weightedPick(weightedItems) { -+ return randomWeightedPick(() => Random.next(), weightedItems); -+ } -+ -+ /** -+ * Creates an RFC 4122 version 4 UUID using the same nondeterministic source. -+ * -+ * @returns {string} Random UUID string. -+ */ -+ static uuid() { -+ return formatUuid(fillRandomBytes(new Uint8Array(UUID_BYTE_LENGTH))); -+ } -+} -+ -+export default Random; -diff --git a/tests/shared/Random.test.mjs b/tests/shared/Random.test.mjs -new file mode 100644 -index 000000000..1559aa1ca ---- /dev/null -+++ b/tests/shared/Random.test.mjs -@@ -0,0 +1,112 @@ -+/* -+Toolbox Aid -+David Quesenberry -+06/26/2026 -+Random.test.mjs -+*/ -+import assert from "node:assert/strict"; -+import { Random } from "../../src/shared/math/Random.js"; -+ -+const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; -+ -+function sequence(values) { -+ let index = 0; -+ return () => values[index++ % values.length]; -+} -+ -+function setCrypto(value) { -+ Object.defineProperty(globalThis, "crypto", { -+ configurable: true, -+ enumerable: true, -+ value, -+ }); -+} -+ -+async function withRandomSources({ cryptoSource, mathValues }, callback) { -+ const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto"); -+ const originalMathRandom = Math.random; -+ -+ if (Object.hasOwn({ cryptoSource }, "cryptoSource")) { -+ setCrypto(cryptoSource); -+ } -+ -+ if (mathValues) { -+ Math.random = sequence(mathValues); -+ } -+ -+ try { -+ await callback(); -+ } finally { -+ if (cryptoDescriptor) { -+ Object.defineProperty(globalThis, "crypto", cryptoDescriptor); -+ } else { -+ delete globalThis.crypto; -+ } -+ Math.random = originalMathRandom; -+ } -+} -+ -+export async function run() { -+ let cryptoCalls = 0; -+ await withRandomSources( -+ { -+ cryptoSource: { -+ getRandomValues(values) { -+ cryptoCalls += 1; -+ if (values instanceof Uint32Array) { -+ values[0] = 0x80000000; -+ return values; -+ } -+ -+ for (let index = 0; index < values.length; index += 1) { -+ values[index] = index; -+ } -+ return values; -+ }, -+ }, -+ }, -+ async () => { -+ assert.equal(Random.next(), 0.5); -+ assert.equal(Random.uuid(), "00010203-0405-4607-8809-0a0b0c0d0e0f"); -+ assert.equal(cryptoCalls >= 2, true); -+ } -+ ); -+ -+ await withRandomSources({ cryptoSource: undefined, mathValues: [0.25] }, async () => { -+ assert.equal(Random.next(), 0.25); -+ }); -+ -+ await withRandomSources( -+ { -+ cryptoSource: undefined, -+ mathValues: [0.5, 0.25, 0.9, 0.1, 0.6, 0.2, 0.49, 0.5, 0.8], -+ }, -+ async () => { -+ assert.equal(Random.nextInt(0, 10), 5); -+ assert.equal(Random.nextFloat(10, 20), 12.5); -+ assert.equal(Random.pick(["a", "b", "c"]), "c"); -+ assert.deepEqual(Random.shuffle(["a", "b", "c", "d"]), ["c", "d", "b", "a"]); -+ assert.equal(Random.chance(50), true); -+ assert.equal(Random.chance(50), false); -+ assert.equal( -+ Random.weightedPick([ -+ { item: "common", weight: 7 }, -+ { item: "rare", weight: 3 }, -+ ]), -+ "rare" -+ ); -+ } -+ ); -+ -+ await withRandomSources({ cryptoSource: undefined, mathValues: Array(16).fill(0.5) }, async () => { -+ assert.match(Random.uuid(), UUID_V4_PATTERN); -+ }); -+ -+ assert.equal(typeof Random.seed, "undefined"); -+ assert.throws(() => Random.pick([]), RangeError); -+ assert.throws(() => Random.chance(-1), RangeError); -+} -+ -+if (import.meta.url === `file://${process.argv[1]}`) { -+ await run(); -+} diff --git a/src/shared/math/RandomSeed.js b/src/shared/math/RandomSeed.js index 19d948644..606bc0738 100644 --- a/src/shared/math/RandomSeed.js +++ b/src/shared/math/RandomSeed.js @@ -1,3 +1,12 @@ +import { + chance as randomChance, + nextFloat as randomNextFloat, + nextInt as randomNextInt, + pick as randomPick, + shuffle as randomShuffle, + weightedPick as randomWeightedPick, +} from "./randomHelpers.js"; + const UINT32_RANGE = 0x100000000; const FNV_OFFSET_BASIS = 0x811c9dc5; const FNV_PRIME = 0x01000193; @@ -14,21 +23,6 @@ function normalizeSeedValue(value) { return hash >>> 0; } -function assertFiniteNumber(value, name) { - if (!Number.isFinite(value)) { - throw new TypeError(`${name} must be a finite number.`); - } -} - -function assertOrderedRange(min, max) { - assertFiniteNumber(min, "min"); - assertFiniteNumber(max, "max"); - - if (max < min) { - throw new RangeError("max must be greater than or equal to min."); - } -} - /** * Deterministic seeded pseudo-random number generator for repeatable game and * tool workflows. `RandomSeed` is intentionally opt-in and does not replace @@ -76,15 +70,7 @@ export class RandomSeed { * @returns {number} A deterministic integer inside the requested range. */ nextInt(min, max) { - assertOrderedRange(min, max); - const lower = Math.ceil(min); - const upper = Math.floor(max); - - if (upper < lower) { - throw new RangeError("integer range must include at least one integer."); - } - - return Math.floor(this.next() * (upper - lower + 1)) + lower; + return randomNextInt(() => this.next(), min, max); } /** @@ -95,8 +81,7 @@ export class RandomSeed { * @returns {number} A deterministic floating-point number inside the range. */ nextFloat(min, max) { - assertOrderedRange(min, max); - return min + this.next() * (max - min); + return randomNextFloat(() => this.next(), min, max); } /** @@ -107,15 +92,65 @@ export class RandomSeed { * @returns {T} The selected array item. */ pick(array) { - if (!Array.isArray(array)) { - throw new TypeError("array must be an array."); - } + return randomPick(() => this.next(), array); + } + + /** + * Returns a deterministically shuffled copy of an array. + * + * @template T + * @param {T[]} array Source array to shuffle. + * @returns {T[]} Shuffled copy. + */ + shuffle(array) { + return randomShuffle(() => this.next(), array); + } - if (array.length === 0) { - throw new RangeError("array must contain at least one item."); + /** + * Returns true when the deterministic roll falls within the percent chance. + * + * @param {number} percent Percent chance from 0 through 100. + * @returns {boolean} Whether the chance roll succeeds. + */ + chance(percent) { + return randomChance(() => this.next(), percent); + } + + /** + * Selects one deterministic weighted item. + * + * @template T + * @param {{item?: T, value?: T, weight: number}[]} weightedItems Weighted item entries. + * @returns {T} Selected item. + */ + weightedPick(weightedItems) { + return randomWeightedPick(() => this.next(), weightedItems); + } + + /** + * Captures the current generator state for later restoration. + * + * @returns {{state: number}} Serializable generator state. + */ + saveState() { + return { state: this._state >>> 0 }; + } + + /** + * Restores a generator state previously returned by `saveState()`. + * + * @param {{state: number}} state Saved generator state. + * @returns {RandomSeed} This generator instance for chaining. + */ + restoreState(state) { + const stateValue = Number(state?.state); + + if (!Number.isInteger(stateValue) || stateValue < 0 || stateValue > 0xffffffff) { + throw new RangeError("state.state must be an unsigned 32-bit integer."); } - return array[this.nextInt(0, array.length - 1)]; + this._state = stateValue >>> 0; + return this; } } diff --git a/tests/shared/RandomSeed.test.mjs b/tests/shared/RandomSeed.test.mjs index b61a44157..23b5f6459 100644 --- a/tests/shared/RandomSeed.test.mjs +++ b/tests/shared/RandomSeed.test.mjs @@ -16,6 +16,15 @@ export function run() { const second = new RandomSeed("level-1"); assert.deepEqual(takeSequence(first, 5), takeSequence(second, 5)); + assert.deepEqual(takeSequence(new RandomSeed(42), 6), [ + 0.3077305785845965, + 0.3676118436269462, + 0.23133554426021874, + 0.01758907549083233, + 0.009130497695878148, + 0.2082449474837631, + ]); + first.seed("level-1"); second.seed("different-level"); assert.notDeepEqual(takeSequence(first, 5), takeSequence(second, 5)); @@ -46,10 +55,44 @@ export function run() { Array.from({ length: 8 }, () => pickerB.pick(pickSource)) ); + const shuffleSource = ["a", "b", "c", "d"]; + const shuffleA = new RandomSeed("shuffle-seed"); + const shuffleB = new RandomSeed("shuffle-seed"); + assert.deepEqual(shuffleA.shuffle(shuffleSource), shuffleB.shuffle(shuffleSource)); + assert.deepEqual(shuffleSource, ["a", "b", "c", "d"]); + + const chanceA = new RandomSeed("chance-seed"); + const chanceB = new RandomSeed("chance-seed"); + assert.deepEqual( + Array.from({ length: 8 }, () => chanceA.chance(35)), + Array.from({ length: 8 }, () => chanceB.chance(35)) + ); + + const weightedItems = [ + { item: "common", weight: 7 }, + { item: "rare", weight: 3 }, + ]; + const weightedA = new RandomSeed("weighted-seed"); + const weightedB = new RandomSeed("weighted-seed"); + assert.deepEqual( + Array.from({ length: 8 }, () => weightedA.weightedPick(weightedItems)), + Array.from({ length: 8 }, () => weightedB.weightedPick(weightedItems)) + ); + + const stateSeed = new RandomSeed("state-seed"); + stateSeed.next(); + const savedState = stateSeed.saveState(); + const expectedAfterState = takeSequence(stateSeed, 4); + assert.equal(stateSeed.restoreState(savedState), stateSeed); + assert.deepEqual(takeSequence(stateSeed, 4), expectedAfterState); + assert.throws(() => new RandomSeed("empty").pick([]), RangeError); assert.throws(() => new RandomSeed("bad-array").pick("not-array"), TypeError); assert.throws(() => new RandomSeed("bad-int").nextInt(5, 2), RangeError); assert.throws(() => new RandomSeed("bad-float").nextFloat(0, Number.POSITIVE_INFINITY), TypeError); + assert.throws(() => new RandomSeed("bad-chance").chance(101), RangeError); + assert.throws(() => new RandomSeed("bad-weight").weightedPick([{ item: "none", weight: 0 }]), RangeError); + assert.throws(() => new RandomSeed("bad-state").restoreState({ state: -1 }), RangeError); } if (import.meta.url === `file://${process.argv[1]}`) {