diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md index 466f9ca28..747703cbf 100644 --- a/docs_build/dev/BUILD_PR.md +++ b/docs_build/dev/BUILD_PR.md @@ -1,20 +1,25 @@ -# PR_26177_001-shared-hash-foundation +# PR_26177_002-shared-noise-foundation ## Purpose -Add a small shared non-cryptographic hash foundation. +Add a small shared deterministic noise foundation. ## Source Of Truth -This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_001-shared-hash-foundation`. +This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_002-shared-noise-foundation`. + +## Stack + +- Base branch: `PR_26177_001-shared-hash-foundation` +- This PR builds on PR_001 hash utilities. ## Exact Scope -- Add `src/shared/hash/` foundation. -- Include deterministic non-crypto hash helpers. -- No browser-owned product data. -- No runtime UI changes. -- Add targeted tests for the shared hash area. +- Add `src/shared/noise/` foundation. +- Build on existing `Random`/`RandomSeed` and PR_001 hash utilities. +- Include deterministic Value, Perlin-style, Simplex-style, and Fractal-style helpers only where practical. +- Keep API small and documented. +- Add targeted tests for the shared noise area. - Create required Codex reports under `docs_build/dev/reports/`. - Create repo-structured delta ZIP under `tmp/`. @@ -22,19 +27,18 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f - `docs_build/dev/PLAN_PR.md` - `docs_build/dev/BUILD_PR.md` -- `src/shared/hash/hash.js` -- `tests/shared/HashFoundation.test.mjs` -- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation.md` -- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_branch-validation.md` -- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_requirement-checklist.md` -- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_validation-lane.md` -- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_manual-validation-notes.md` +- `src/shared/noise/noise.js` +- `tests/shared/NoiseFoundation.test.mjs` +- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md` +- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md` +- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md` +- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md` +- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md` - `docs_build/dev/reports/codex_review.diff` - `docs_build/dev/reports/codex_changed_files.txt` ## Out Of Scope -- No cryptographic hashing. - No browser-owned product data. - No runtime UI changes. - No browser storage changes. @@ -49,9 +53,9 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f Run exactly: ```powershell -node ./scripts/run-node-test-files.mjs tests/shared/HashFoundation.test.mjs -node --check src/shared/hash/hash.js -node --check tests/shared/HashFoundation.test.mjs +node ./scripts/run-node-test-files.mjs tests/shared/NoiseFoundation.test.mjs tests/shared/HashFoundation.test.mjs +node --check src/shared/noise/noise.js +node --check tests/shared/NoiseFoundation.test.mjs git diff --check ``` @@ -60,5 +64,5 @@ git diff --check Create repo-structured delta ZIP: ```text -tmp/PR_26177_001-shared-hash-foundation_delta.zip +tmp/PR_26177_002-shared-noise-foundation_delta.zip ``` diff --git a/docs_build/dev/PLAN_PR.md b/docs_build/dev/PLAN_PR.md index 835b7009a..9a9a80336 100644 --- a/docs_build/dev/PLAN_PR.md +++ b/docs_build/dev/PLAN_PR.md @@ -1,28 +1,23 @@ -# PLAN_PR: PR_26177_001-shared-hash-foundation +# PLAN_PR: PR_26177_002-shared-noise-foundation ## Purpose -Add a small shared non-cryptographic hash foundation. +Add a small shared deterministic noise foundation. ## Scope -- Add `src/shared/hash/` foundation. -- Include deterministic non-crypto hash helpers. -- Add targeted unit tests. +- Add `src/shared/noise/` foundation. +- Build on existing `RandomSeed` and PR_001 hash utilities. +- Include deterministic Value, Perlin-style, Simplex-style, and Fractal-style helpers where practical. +- Keep API small and documented. +- Add targeted tests. - No browser-owned product data. - No runtime UI changes. - No unrelated cleanup. ## Implementation Plan -1. Add `src/shared/hash/hash.js` with deterministic stable string and FNV-1a based helpers. -2. Add `tests/shared/HashFoundation.test.mjs`. -3. Validate determinism, object key ordering, seed variation, combination, and normalized hash output. +1. Add `src/shared/noise/noise.js`. +2. Add `tests/shared/NoiseFoundation.test.mjs`. +3. Validate deterministic output, seed variation, practical ranges, and permutation determinism. 4. Produce required Codex reports and repo-structured ZIP. - -## Acceptance Criteria - -- Hash helpers are deterministic for identical values. -- Object hashing is stable regardless of property insertion order. -- Helpers are documented as non-cryptographic. -- No browser storage, UI, API, database, or sample smoke changes are introduced. diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md new file mode 100644 index 000000000..283ce4b9f --- /dev/null +++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md @@ -0,0 +1,24 @@ +# PR_26177_002-shared-noise-foundation + +Date: 2026-06-26 +Scope: Shared deterministic noise foundation +Status: PASS + +## Summary + +- Added `src/shared/noise/noise.js`. +- Added deterministic value, Perlin-style, Simplex-style, fractal, and seeded permutation helpers. +- Built on PR_001 hash utilities and existing `RandomSeed`. +- Added targeted tests in `tests/shared/NoiseFoundation.test.mjs`. +- No browser-owned product data, runtime UI, API, database, or unrelated cleanup changes were made. + +## Validation + +- PASS: `node ./scripts/run-node-test-files.mjs tests/shared/NoiseFoundation.test.mjs tests/shared/HashFoundation.test.mjs`. +- PASS: `node --check src/shared/noise/noise.js`. +- PASS: `node --check tests/shared/NoiseFoundation.test.mjs`. +- PASS: `git diff --check`. + +## Artifact + +- `tmp/PR_26177_002-shared-noise-foundation_delta.zip` diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md new file mode 100644 index 000000000..3ee3ed634 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md @@ -0,0 +1,8 @@ +# PR_26177_002-shared-noise-foundation Branch Validation + +Status: PASS + +- PASS: Branch `PR_26177_002-shared-noise-foundation` was created from `PR_26177_001-shared-hash-foundation`. +- PASS: Stack base was clean before branch creation. +- PASS: One PR purpose only: shared noise foundation. +- PASS: No `start_of_day` files changed. diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md new file mode 100644 index 000000000..389348d27 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md @@ -0,0 +1,5 @@ +# PR_26177_002-shared-noise-foundation Manual Validation Notes + +Status: PASS + +Manual review confirmed the noise helpers are deterministic, small, documented, stacked on the hash foundation, and isolated from UI, browser product-data ownership, API, and database behavior. diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md new file mode 100644 index 000000000..640ae8fcb --- /dev/null +++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md @@ -0,0 +1,13 @@ +# PR_26177_002-shared-noise-foundation Requirement Checklist + +| Requirement | Status | Notes | +|---|---:|---| +| Add `src/shared/noise/` foundation | PASS | Added `src/shared/noise/noise.js`. | +| Build on Random/RandomSeed and PR_001 hash utilities | PASS | Uses `RandomSeed` and hash helpers. | +| Include deterministic Value helpers where practical | PASS | Added `valueNoise2D`. | +| Include deterministic Perlin helpers where practical | PASS | Added `perlinNoise2D`. | +| Include deterministic Simplex helpers where practical | PASS | Added `simplexNoise2D`. | +| Include deterministic Fractal helpers where practical | PASS | Added `fractalNoise2D`. | +| Keep API small and documented | PASS | Added a compact documented helper set. | +| Add targeted tests | PASS | Added `tests/shared/NoiseFoundation.test.mjs`. | +| No runtime UI changes | PASS | No UI files changed. | diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md new file mode 100644 index 000000000..b923b482f --- /dev/null +++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md @@ -0,0 +1,22 @@ +# PR_26177_002-shared-noise-foundation Validation Lane + +Status: PASS + +## Commands + +```powershell +node ./scripts/run-node-test-files.mjs tests/shared/NoiseFoundation.test.mjs tests/shared/HashFoundation.test.mjs +node --check src/shared/noise/noise.js +node --check tests/shared/NoiseFoundation.test.mjs +git diff --check +``` + +## Results + +- PASS: Targeted noise and hash tests. +- PASS: Changed JS syntax checks. +- PASS: `git diff --check`. + +## Not Run + +- Full samples smoke was not run by default. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index c986337ce..70fe1c35e 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,11 +1,11 @@ docs_build/dev/BUILD_PR.md docs_build/dev/PLAN_PR.md -docs_build/dev/reports/PR_26177_001-shared-hash-foundation.md -docs_build/dev/reports/PR_26177_001-shared-hash-foundation_branch-validation.md -docs_build/dev/reports/PR_26177_001-shared-hash-foundation_manual-validation-notes.md -docs_build/dev/reports/PR_26177_001-shared-hash-foundation_requirement-checklist.md -docs_build/dev/reports/PR_26177_001-shared-hash-foundation_validation-lane.md +docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md +docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md +docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md +docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md +docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md docs_build/dev/reports/codex_changed_files.txt docs_build/dev/reports/codex_review.diff -src/shared/hash/hash.js -tests/shared/HashFoundation.test.mjs +src/shared/noise/noise.js +tests/shared/NoiseFoundation.test.mjs diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index b27ef3989..5e77154a9 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,444 +1,447 @@ diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md -index edc9a0b75..466f9ca28 100644 +index 466f9ca28..747703cbf 100644 --- a/docs_build/dev/BUILD_PR.md +++ b/docs_build/dev/BUILD_PR.md -@@ -1,40 +1,20 @@ --# PR_26177_DELTA_056-shared-validation-assertions -+# PR_26177_001-shared-hash-foundation +@@ -1,20 +1,25 @@ +-# PR_26177_001-shared-hash-foundation ++# PR_26177_002-shared-noise-foundation ## Purpose --Move generic validation/assertion helpers out of random helper code into a shared validation module. -+Add a small shared non-cryptographic hash foundation. +-Add a small shared non-cryptographic hash foundation. ++Add a small shared deterministic noise foundation. ## 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_056-shared-validation-assertions`. -- --## OWNER Override And Team Assignment -- --OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_056-shared-validation-assertions`. -- --Team Delta owns Shared JS, runtime utilities, technical debt remediation, and runtime test coverage. -- --## Stack -- --- Base branch: `PR_26177_DELTA_055-random-seed-enhancements` --- This PR depends on the random helper module from PR_053, `Random` from PR_054, and `RandomSeed` enhancements from PR_055. -+This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_001-shared-hash-foundation`. +-This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_001-shared-hash-foundation`. ++This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth for `PR_26177_002-shared-noise-foundation`. ++ ++## Stack ++ ++- Base branch: `PR_26177_001-shared-hash-foundation` ++- This PR builds on PR_001 hash utilities. ## Exact Scope --- Create `src/shared/validation/assert.js`. --- Move generic assertion helpers from random helper code into `assert.js`. --- Include only generic reusable validation functions needed by current random helpers: -- - `assertArray` -- - `assertFiniteNumber` -- - `assertOrderedRange` --- Update random helper code to import from `src/shared/validation/assert.js`. --- Preserve existing `Random` and `RandomSeed` behavior. --- Do not change public API. --- Do not expand into unrelated validation functions yet. --- Add/update targeted unit tests if needed. --- No UI changes. --- No API/database changes. --- No unrelated cleanup. -+- Add `src/shared/hash/` foundation. -+- Include deterministic non-crypto hash helpers. -+- No browser-owned product data. -+- No runtime UI changes. -+- Add targeted tests for the shared hash area. +-- Add `src/shared/hash/` foundation. +-- Include deterministic non-crypto hash helpers. +-- No browser-owned product data. +-- No runtime UI changes. +-- Add targeted tests for the shared hash area. ++- Add `src/shared/noise/` foundation. ++- Build on existing `Random`/`RandomSeed` and PR_001 hash utilities. ++- Include deterministic Value, Perlin-style, Simplex-style, and Fractal-style helpers only where practical. ++- Keep API small and documented. ++- Add targeted tests for the shared noise area. - Create required Codex reports under `docs_build/dev/reports/`. - Create repo-structured delta ZIP under `tmp/`. -@@ -42,48 +22,43 @@ Team Delta owns Shared JS, runtime utilities, technical debt remediation, and ru +@@ -22,19 +27,18 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f - `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/validation/assert.js` --- `src/shared/math/randomHelpers.js` --- `docs_build/dev/reports/PR_26177_DELTA_056-shared-validation-assertions.md` --- `docs_build/dev/reports/PR_26177_DELTA_056-shared-validation-assertions_branch-validation.md` --- `docs_build/dev/reports/PR_26177_DELTA_056-shared-validation-assertions_requirement-checklist.md` --- `docs_build/dev/reports/PR_26177_DELTA_056-shared-validation-assertions_validation-lane.md` --- `docs_build/dev/reports/PR_26177_DELTA_056-shared-validation-assertions_manual-validation-notes.md` --- `docs_build/dev/reports/PR_26177_DELTA_056-shared-validation-assertions_instruction-compliance-checklist.md` -+- `src/shared/hash/hash.js` -+- `tests/shared/HashFoundation.test.mjs` -+- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation.md` -+- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_branch-validation.md` -+- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_requirement-checklist.md` -+- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_validation-lane.md` -+- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_manual-validation-notes.md` +-- `src/shared/hash/hash.js` +-- `tests/shared/HashFoundation.test.mjs` +-- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation.md` +-- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_branch-validation.md` +-- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_requirement-checklist.md` +-- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_validation-lane.md` +-- `docs_build/dev/reports/PR_26177_001-shared-hash-foundation_manual-validation-notes.md` ++- `src/shared/noise/noise.js` ++- `tests/shared/NoiseFoundation.test.mjs` ++- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md` ++- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md` ++- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md` ++- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md` ++- `docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md` - `docs_build/dev/reports/codex_review.diff` - `docs_build/dev/reports/codex_changed_files.txt` ## Out Of Scope --- No public API changes. --- No new unrelated validation helpers. --- No existing game logic adoption changes. --- No UI changes. -+- No cryptographic hashing. -+- No browser-owned product data. -+- No runtime UI changes. +-- No cryptographic hashing. + - No browser-owned product data. + - No runtime UI changes. - No browser storage changes. --- No API changes. --- No database changes. -+- No API/database changes. -+- No engine refactor. - - No `start_of_day` folder changes. - - No unrelated cleanup. -+- No full samples smoke by default. - - ## Validation - +@@ -49,9 +53,9 @@ This `BUILD_PR.md`, `PLAN_PR.md`, and the user request are the source of truth f Run exactly: ```powershell --node ./scripts/run-node-test-files.mjs tests/shared/RandomHelpers.test.mjs tests/shared/Random.test.mjs tests/shared/RandomSeed.test.mjs --node --check src/shared/validation/assert.js --node --check src/shared/math/randomHelpers.js -+node ./scripts/run-node-test-files.mjs tests/shared/HashFoundation.test.mjs -+node --check src/shared/hash/hash.js -+node --check tests/shared/HashFoundation.test.mjs +-node ./scripts/run-node-test-files.mjs tests/shared/HashFoundation.test.mjs +-node --check src/shared/hash/hash.js +-node --check tests/shared/HashFoundation.test.mjs ++node ./scripts/run-node-test-files.mjs tests/shared/NoiseFoundation.test.mjs tests/shared/HashFoundation.test.mjs ++node --check src/shared/noise/noise.js ++node --check tests/shared/NoiseFoundation.test.mjs git diff --check ``` --Playwright is not required because this PR does not change UI or browser runtime flows. -- - ## Artifact - +@@ -60,5 +64,5 @@ git diff --check Create repo-structured delta ZIP: ```text --tmp/PR_26177_DELTA_056-shared-validation-assertions_delta.zip -+tmp/PR_26177_001-shared-hash-foundation_delta.zip +-tmp/PR_26177_001-shared-hash-foundation_delta.zip ++tmp/PR_26177_002-shared-noise-foundation_delta.zip ``` diff --git a/docs_build/dev/PLAN_PR.md b/docs_build/dev/PLAN_PR.md -index de53f9020..835b7009a 100644 +index 835b7009a..9a9a80336 100644 --- a/docs_build/dev/PLAN_PR.md +++ b/docs_build/dev/PLAN_PR.md -@@ -1,43 +1,28 @@ --# PLAN_PR: PR_26177_DELTA_056-shared-validation-assertions -+# PLAN_PR: PR_26177_001-shared-hash-foundation +@@ -1,28 +1,23 @@ +-# PLAN_PR: PR_26177_001-shared-hash-foundation ++# PLAN_PR: PR_26177_002-shared-noise-foundation ## Purpose --Move generic validation/assertion helpers out of random helper code into a shared validation module. -- --## Owner And Assignment -- --- Team: Delta --- OWNER override approved: Continue Team Delta random utility stack with `PR_26177_DELTA_056-shared-validation-assertions`. --- Stack base: `PR_26177_DELTA_055-random-seed-enhancements`. -+Add a small shared non-cryptographic hash foundation. +-Add a small shared non-cryptographic hash foundation. ++Add a small shared deterministic noise foundation. ## Scope --- Create `src/shared/validation/assert.js`. --- Move generic assertion helpers from random helper code into `assert.js`. --- Include only generic reusable validation functions needed by current random helpers: -- - `assertArray` -- - `assertFiniteNumber` -- - `assertOrderedRange` --- Update random helper code to import from `src/shared/validation/assert.js`. --- Preserve existing `Random` and `RandomSeed` behavior. --- Do not change public API. --- Do not expand into unrelated validation functions yet. --- Add/update targeted unit tests if needed. --- No UI changes. --- No API/database changes. -+- Add `src/shared/hash/` foundation. -+- Include deterministic non-crypto hash helpers. -+- Add targeted unit tests. -+- No browser-owned product data. -+- No runtime UI changes. +-- Add `src/shared/hash/` foundation. +-- Include deterministic non-crypto hash helpers. +-- Add targeted unit tests. ++- Add `src/shared/noise/` foundation. ++- Build on existing `RandomSeed` and PR_001 hash utilities. ++- Include deterministic Value, Perlin-style, Simplex-style, and Fractal-style helpers where practical. ++- Keep API small and documented. ++- Add targeted tests. + - No browser-owned product data. + - No runtime UI changes. - No unrelated cleanup. ## Implementation Plan --1. Add `src/shared/validation/assert.js` with only the required generic assertion helpers. --2. Remove duplicated generic assertions from `src/shared/math/randomHelpers.js`. --3. Import the shared assertions from `randomHelpers.js`. --4. Run targeted random helper, `Random`, and `RandomSeed` tests. --5. Produce required PR reports and repo-structured ZIP. -+1. Add `src/shared/hash/hash.js` with deterministic stable string and FNV-1a based helpers. -+2. Add `tests/shared/HashFoundation.test.mjs`. -+3. Validate determinism, object key ordering, seed variation, combination, and normalized hash output. -+4. Produce required Codex reports and repo-structured ZIP. - - ## Acceptance Criteria - --- Existing random helper behavior is preserved. --- Existing `Random` and `RandomSeed` behavior is preserved. --- Public random APIs are unchanged. --- Shared assertion module stays limited to the current reusable validation helpers. -+- Hash helpers are deterministic for identical values. -+- Object hashing is stable regardless of property insertion order. -+- Helpers are documented as non-cryptographic. -+- No browser storage, UI, API, database, or sample smoke changes are introduced. -diff --git a/docs_build/dev/reports/PR_26177_001-shared-hash-foundation.md b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation.md +-1. Add `src/shared/hash/hash.js` with deterministic stable string and FNV-1a based helpers. +-2. Add `tests/shared/HashFoundation.test.mjs`. +-3. Validate determinism, object key ordering, seed variation, combination, and normalized hash output. ++1. Add `src/shared/noise/noise.js`. ++2. Add `tests/shared/NoiseFoundation.test.mjs`. ++3. Validate deterministic output, seed variation, practical ranges, and permutation determinism. + 4. Produce required Codex reports and repo-structured ZIP. +- +-## Acceptance Criteria +- +-- Hash helpers are deterministic for identical values. +-- Object hashing is stable regardless of property insertion order. +-- Helpers are documented as non-cryptographic. +-- No browser storage, UI, API, database, or sample smoke changes are introduced. +diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md new file mode 100644 -index 000000000..f6f311a2d +index 000000000..283ce4b9f --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation.md -@@ -0,0 +1,23 @@ -+# PR_26177_001-shared-hash-foundation ++++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation.md +@@ -0,0 +1,24 @@ ++# PR_26177_002-shared-noise-foundation + +Date: 2026-06-26 -+Scope: Shared non-cryptographic hash foundation ++Scope: Shared deterministic noise foundation +Status: PASS + +## Summary + -+- Added `src/shared/hash/hash.js`. -+- Added deterministic non-cryptographic hash helpers for stable strings, string/value hashes, combined hashes, and normalized hash values. -+- Added targeted tests in `tests/shared/HashFoundation.test.mjs`. ++- Added `src/shared/noise/noise.js`. ++- Added deterministic value, Perlin-style, Simplex-style, fractal, and seeded permutation helpers. ++- Built on PR_001 hash utilities and existing `RandomSeed`. ++- Added targeted tests in `tests/shared/NoiseFoundation.test.mjs`. +- No browser-owned product data, runtime UI, API, database, or unrelated cleanup changes were made. + +## Validation + -+- PASS: `node ./scripts/run-node-test-files.mjs tests/shared/HashFoundation.test.mjs`. -+- PASS: `node --check src/shared/hash/hash.js`. -+- PASS: `node --check tests/shared/HashFoundation.test.mjs`. ++- PASS: `node ./scripts/run-node-test-files.mjs tests/shared/NoiseFoundation.test.mjs tests/shared/HashFoundation.test.mjs`. ++- PASS: `node --check src/shared/noise/noise.js`. ++- PASS: `node --check tests/shared/NoiseFoundation.test.mjs`. +- PASS: `git diff --check`. + +## Artifact + -+- `tmp/PR_26177_001-shared-hash-foundation_delta.zip` -diff --git a/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_branch-validation.md b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_branch-validation.md ++- `tmp/PR_26177_002-shared-noise-foundation_delta.zip` +diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md new file mode 100644 -index 000000000..1b0802d30 +index 000000000..3ee3ed634 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_branch-validation.md ++++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_branch-validation.md @@ -0,0 +1,8 @@ -+# PR_26177_001-shared-hash-foundation Branch Validation ++# PR_26177_002-shared-noise-foundation Branch Validation + +Status: PASS + -+- PASS: Started from clean synchronized `main`. -+- PASS: Branch `PR_26177_001-shared-hash-foundation` was created from `main`. -+- PASS: One PR purpose only: shared hash foundation. ++- PASS: Branch `PR_26177_002-shared-noise-foundation` was created from `PR_26177_001-shared-hash-foundation`. ++- PASS: Stack base was clean before branch creation. ++- PASS: One PR purpose only: shared noise foundation. +- PASS: No `start_of_day` files changed. -diff --git a/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_manual-validation-notes.md +diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md new file mode 100644 -index 000000000..cf64ebe33 +index 000000000..389348d27 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_manual-validation-notes.md ++++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_manual-validation-notes.md @@ -0,0 +1,5 @@ -+# PR_26177_001-shared-hash-foundation Manual Validation Notes ++# PR_26177_002-shared-noise-foundation Manual Validation Notes + +Status: PASS + -+Manual review confirmed the new hash helpers are deterministic, non-cryptographic, isolated to `src/shared/hash/`, and do not add browser product-data ownership or runtime UI behavior. -diff --git a/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_requirement-checklist.md b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_requirement-checklist.md ++Manual review confirmed the noise helpers are deterministic, small, documented, stacked on the hash foundation, and isolated from UI, browser product-data ownership, API, and database behavior. +diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md new file mode 100644 -index 000000000..371fed91f +index 000000000..640ae8fcb --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_requirement-checklist.md -@@ -0,0 +1,12 @@ -+# PR_26177_001-shared-hash-foundation Requirement Checklist ++++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_requirement-checklist.md +@@ -0,0 +1,13 @@ ++# PR_26177_002-shared-noise-foundation Requirement Checklist + +| Requirement | Status | Notes | +|---|---:|---| -+| Add `src/shared/hash/` foundation | PASS | Added `src/shared/hash/hash.js`. | -+| Include deterministic non-crypto hash helpers | PASS | Added stable string, string/value hash, combined hash, and normalized helpers. | -+| No browser-owned product data | PASS | No storage or product-data files changed. | ++| Add `src/shared/noise/` foundation | PASS | Added `src/shared/noise/noise.js`. | ++| Build on Random/RandomSeed and PR_001 hash utilities | PASS | Uses `RandomSeed` and hash helpers. | ++| Include deterministic Value helpers where practical | PASS | Added `valueNoise2D`. | ++| Include deterministic Perlin helpers where practical | PASS | Added `perlinNoise2D`. | ++| Include deterministic Simplex helpers where practical | PASS | Added `simplexNoise2D`. | ++| Include deterministic Fractal helpers where practical | PASS | Added `fractalNoise2D`. | ++| Keep API small and documented | PASS | Added a compact documented helper set. | ++| Add targeted tests | PASS | Added `tests/shared/NoiseFoundation.test.mjs`. | +| No runtime UI changes | PASS | No UI files changed. | -+| One PR purpose only | PASS | Scope is limited to hash foundation. | -+| Smallest valid scoped change | PASS | No unrelated exports or integrations added. | -+| Add targeted tests | PASS | Added `tests/shared/HashFoundation.test.mjs`. | -+| Do not run full samples smoke by default | PASS | Full samples smoke was not run. | -diff --git a/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_validation-lane.md b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_validation-lane.md +diff --git a/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md new file mode 100644 -index 000000000..bfe2422a9 +index 000000000..b923b482f --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_001-shared-hash-foundation_validation-lane.md ++++ b/docs_build/dev/reports/PR_26177_002-shared-noise-foundation_validation-lane.md @@ -0,0 +1,22 @@ -+# PR_26177_001-shared-hash-foundation Validation Lane ++# PR_26177_002-shared-noise-foundation Validation Lane + +Status: PASS + +## Commands + +```powershell -+node ./scripts/run-node-test-files.mjs tests/shared/HashFoundation.test.mjs -+node --check src/shared/hash/hash.js -+node --check tests/shared/HashFoundation.test.mjs ++node ./scripts/run-node-test-files.mjs tests/shared/NoiseFoundation.test.mjs tests/shared/HashFoundation.test.mjs ++node --check src/shared/noise/noise.js ++node --check tests/shared/NoiseFoundation.test.mjs +git diff --check +``` + +## Results + -+- PASS: Targeted hash foundation test. ++- PASS: Targeted noise and hash tests. +- PASS: Changed JS syntax checks. +- PASS: `git diff --check`. + +## Not Run + +- Full samples smoke was not run by default. -diff --git a/src/shared/hash/hash.js b/src/shared/hash/hash.js +diff --git a/src/shared/noise/noise.js b/src/shared/noise/noise.js new file mode 100644 -index 000000000..e8555ba8d +index 000000000..8908d847a --- /dev/null -+++ b/src/shared/hash/hash.js -@@ -0,0 +1,108 @@ -+const FNV_OFFSET_BASIS = 0x811c9dc5; -+const FNV_PRIME = 0x01000193; -+const UINT32_RANGE = 0x100000000; -+ -+function stableObjectEntries(value) { -+ return Object.keys(value) -+ .sort() -+ .map((key) => `${JSON.stringify(key)}:${toStableHashString(value[key])}`); -+} ++++ b/src/shared/noise/noise.js +@@ -0,0 +1,163 @@ ++import { hashToUnitInterval, hashValue32 } from "../hash/hash.js"; ++import { RandomSeed } from "../math/RandomSeed.js"; + -+/** -+ * Converts a value into a deterministic string suitable for non-cryptographic hashing. -+ * -+ * @param {*} value Value to serialize. -+ * @returns {string} Stable deterministic string. -+ */ -+export function toStableHashString(value) { -+ if (value === null) { -+ return "null"; -+ } ++const SQRT3 = Math.sqrt(3); ++const SIMPLEX_F2 = 0.5 * (SQRT3 - 1); ++const SIMPLEX_G2 = (3 - SQRT3) / 6; + -+ if (Array.isArray(value)) { -+ return `[${value.map(toStableHashString).join(",")}]`; -+ } ++function fade(value) { ++ return value * value * value * (value * (value * 6 - 15) + 10); ++} + -+ if (value instanceof Date) { -+ return `date:${Number.isNaN(value.getTime()) ? "invalid" : value.toISOString()}`; -+ } ++function lerp(a, b, amount) { ++ return a + (b - a) * amount; ++} + -+ const valueType = typeof value; -+ if (valueType === "object") { -+ return `{${stableObjectEntries(value).join(",")}}`; -+ } ++function gradientDot(seed, ix, iy, x, y) { ++ const angle = hashToUnitInterval([seed, ix, iy, "gradient"]) * Math.PI * 2; ++ return Math.cos(angle) * x + Math.sin(angle) * y; ++} + -+ if (valueType === "number") { -+ if (Number.isNaN(value)) { -+ return "number:NaN"; -+ } -+ return Object.is(value, -0) ? "number:-0" : `number:${value}`; ++function simplexCorner(seed, i, j, x, y) { ++ const t = 0.5 - x * x - y * y; ++ if (t < 0) { ++ return 0; + } + -+ return `${valueType}:${String(value)}`; ++ const influence = t * t; ++ return influence * influence * gradientDot(seed, i, j, x, y); ++} ++ ++function normalizeFrequency(frequency) { ++ const normalized = Number(frequency); ++ return Number.isFinite(normalized) && normalized > 0 ? normalized : 1; +} + +/** -+ * Computes a deterministic FNV-1a 32-bit hash for a string. -+ * -+ * This helper is non-cryptographic and is intended for stable bucketing, -+ * procedural generation, and cache keys where security is not required. ++ * Creates a deterministic shuffled permutation from a seed. + * -+ * @param {*} value Value converted to string before hashing. -+ * @param {number} seed Optional unsigned 32-bit hash seed. -+ * @returns {number} Unsigned 32-bit hash. ++ * @param {*} seed Seed value. ++ * @param {number} size Permutation size. ++ * @returns {number[]} Deterministic permutation. + */ -+export function hashString32(value, seed = FNV_OFFSET_BASIS) { -+ const text = String(value ?? ""); -+ let hash = seed >>> 0; -+ -+ for (let index = 0; index < text.length; index += 1) { -+ hash ^= text.charCodeAt(index); -+ hash = Math.imul(hash, FNV_PRIME); -+ } -+ -+ return hash >>> 0; ++export function createNoisePermutation(seed = "noise", size = 256) { ++ const normalizedSize = Math.max(1, Math.floor(Number(size) || 256)); ++ return new RandomSeed(seed).shuffle(Array.from({ length: normalizedSize }, (_, index) => index)); +} + +/** -+ * Computes a deterministic non-cryptographic 32-bit hash for any JSON-like value. ++ * Returns deterministic value noise in the range [0, 1]. + * -+ * @param {*} value Value to hash. -+ * @param {number} seed Optional unsigned 32-bit hash seed. -+ * @returns {number} Unsigned 32-bit hash. ++ * @param {number} x X coordinate. ++ * @param {number} y Y coordinate. ++ * @param {{seed?: *, frequency?: number}} options Noise options. ++ * @returns {number} Value noise. + */ -+export function hashValue32(value, seed = FNV_OFFSET_BASIS) { -+ return hashString32(toStableHashString(value), seed); ++export function valueNoise2D(x, y, options = {}) { ++ const frequency = normalizeFrequency(options.frequency); ++ const nx = Number(x) * frequency; ++ const ny = Number(y) * frequency; ++ const x0 = Math.floor(nx); ++ const y0 = Math.floor(ny); ++ const tx = fade(nx - x0); ++ const ty = fade(ny - y0); ++ const seed = options.seed ?? "value"; ++ ++ const v00 = hashToUnitInterval([seed, x0, y0]); ++ const v10 = hashToUnitInterval([seed, x0 + 1, y0]); ++ const v01 = hashToUnitInterval([seed, x0, y0 + 1]); ++ const v11 = hashToUnitInterval([seed, x0 + 1, y0 + 1]); ++ ++ return lerp(lerp(v00, v10, tx), lerp(v01, v11, tx), ty); +} + +/** -+ * Combines multiple values into one deterministic non-cryptographic 32-bit hash. ++ * Returns deterministic Perlin-style gradient noise, approximately in [-1, 1]. + * -+ * @param {...*} values Values to combine. -+ * @returns {number} Unsigned 32-bit hash. ++ * @param {number} x X coordinate. ++ * @param {number} y Y coordinate. ++ * @param {{seed?: *, frequency?: number}} options Noise options. ++ * @returns {number} Perlin-style noise. + */ -+export function combineHash32(...values) { -+ return values.reduce((hash, value) => hashValue32(value, hash), FNV_OFFSET_BASIS); ++export function perlinNoise2D(x, y, options = {}) { ++ const frequency = normalizeFrequency(options.frequency); ++ const nx = Number(x) * frequency; ++ const ny = Number(y) * frequency; ++ const x0 = Math.floor(nx); ++ const y0 = Math.floor(ny); ++ const dx = nx - x0; ++ const dy = ny - y0; ++ const tx = fade(dx); ++ const ty = fade(dy); ++ const seed = options.seed ?? "perlin"; ++ ++ const n00 = gradientDot(seed, x0, y0, dx, dy); ++ const n10 = gradientDot(seed, x0 + 1, y0, dx - 1, dy); ++ const n01 = gradientDot(seed, x0, y0 + 1, dx, dy - 1); ++ const n11 = gradientDot(seed, x0 + 1, y0 + 1, dx - 1, dy - 1); ++ ++ return Math.max(-1, Math.min(1, lerp(lerp(n00, n10, tx), lerp(n01, n11, tx), ty))); +} + +/** -+ * Maps a deterministic value hash into the range [0, 1). ++ * Returns deterministic Simplex-style 2D noise, approximately in [-1, 1]. + * -+ * @param {*} value Value to hash. -+ * @param {number} seed Optional unsigned 32-bit hash seed. -+ * @returns {number} Normalized hash value. ++ * @param {number} x X coordinate. ++ * @param {number} y Y coordinate. ++ * @param {{seed?: *, frequency?: number}} options Noise options. ++ * @returns {number} Simplex-style noise. + */ -+export function hashToUnitInterval(value, seed = FNV_OFFSET_BASIS) { -+ return hashValue32(value, seed) / UINT32_RANGE; ++export function simplexNoise2D(x, y, options = {}) { ++ const frequency = normalizeFrequency(options.frequency); ++ const nx = Number(x) * frequency; ++ const ny = Number(y) * frequency; ++ const seed = options.seed ?? "simplex"; ++ const skew = (nx + ny) * SIMPLEX_F2; ++ const i = Math.floor(nx + skew); ++ const j = Math.floor(ny + skew); ++ const unskew = (i + j) * SIMPLEX_G2; ++ const x0 = nx - (i - unskew); ++ const y0 = ny - (j - unskew); ++ const i1 = x0 > y0 ? 1 : 0; ++ const j1 = x0 > y0 ? 0 : 1; ++ const x1 = x0 - i1 + SIMPLEX_G2; ++ const y1 = y0 - j1 + SIMPLEX_G2; ++ const x2 = x0 - 1 + 2 * SIMPLEX_G2; ++ const y2 = y0 - 1 + 2 * SIMPLEX_G2; ++ ++ const value = 70 * ( ++ simplexCorner(seed, i, j, x0, y0) + ++ simplexCorner(seed, i + i1, j + j1, x1, y1) + ++ simplexCorner(seed, i + 1, j + 1, x2, y2) ++ ); ++ return Math.max(-1, Math.min(1, value)); +} + +/** -+ * Maps a deterministic value hash into the range [-1, 1). ++ * Combines octaves of a deterministic noise function. + * -+ * @param {*} value Value to hash. -+ * @param {number} seed Optional unsigned 32-bit hash seed. -+ * @returns {number} Signed normalized hash value. ++ * @param {number} x X coordinate. ++ * @param {number} y Y coordinate. ++ * @param {{seed?: *, octaves?: number, frequency?: number, lacunarity?: number, persistence?: number, noise?: Function}} options Fractal options. ++ * @returns {number} Weighted fractal noise. + */ -+export function hashToSignedUnit(value, seed = FNV_OFFSET_BASIS) { -+ return hashToUnitInterval(value, seed) * 2 - 1; ++export function fractalNoise2D(x, y, options = {}) { ++ const octaves = Math.max(1, Math.floor(Number(options.octaves) || 4)); ++ const lacunarity = Number.isFinite(options.lacunarity) ? options.lacunarity : 2; ++ const persistence = Number.isFinite(options.persistence) ? options.persistence : 0.5; ++ const noise = typeof options.noise === "function" ? options.noise : valueNoise2D; ++ let frequency = normalizeFrequency(options.frequency); ++ let amplitude = 1; ++ let total = 0; ++ let amplitudeTotal = 0; ++ ++ for (let octave = 0; octave < octaves; octave += 1) { ++ const seed = hashValue32([options.seed ?? "fractal", octave]); ++ total += noise(x, y, { ...options, seed, frequency }) * amplitude; ++ amplitudeTotal += amplitude; ++ amplitude *= persistence; ++ frequency *= lacunarity; ++ } ++ ++ return amplitudeTotal === 0 ? 0 : total / amplitudeTotal; +} -diff --git a/tests/shared/HashFoundation.test.mjs b/tests/shared/HashFoundation.test.mjs +diff --git a/tests/shared/NoiseFoundation.test.mjs b/tests/shared/NoiseFoundation.test.mjs new file mode 100644 -index 000000000..a7a06c934 +index 000000000..7bee4586b --- /dev/null -+++ b/tests/shared/HashFoundation.test.mjs ++++ b/tests/shared/NoiseFoundation.test.mjs @@ -0,0 +1,44 @@ +/* +Toolbox Aid +David Quesenberry +06/26/2026 -+HashFoundation.test.mjs ++NoiseFoundation.test.mjs +*/ +import assert from "node:assert/strict"; +import { -+ combineHash32, -+ hashString32, -+ hashToSignedUnit, -+ hashToUnitInterval, -+ hashValue32, -+ toStableHashString, -+} from "../../src/shared/hash/hash.js"; ++ createNoisePermutation, ++ fractalNoise2D, ++ perlinNoise2D, ++ simplexNoise2D, ++ valueNoise2D, ++} from "../../src/shared/noise/noise.js"; ++ ++function assertInRange(value, min, max) { ++ assert.equal(Number.isFinite(value), true); ++ assert.equal(value >= min && value <= max, true, `${value} was outside ${min}..${max}`); ++} + +export function run() { -+ assert.equal(hashString32("Game Foundry"), hashString32("Game Foundry")); -+ assert.notEqual(hashString32("Game Foundry"), hashString32("Game Foundry!")); -+ -+ assert.equal( -+ toStableHashString({ b: 2, a: 1 }), -+ toStableHashString({ a: 1, b: 2 }) -+ ); -+ assert.equal(hashValue32({ b: 2, a: [1, 2, 3] }), hashValue32({ a: [1, 2, 3], b: 2 })); -+ assert.notEqual(hashValue32({ a: [1, 2, 3] }), hashValue32({ a: [1, 2, 4] })); -+ -+ assert.equal(combineHash32("a", "b", "c"), combineHash32("a", "b", "c")); -+ assert.notEqual(combineHash32("a", "b", "c"), combineHash32("a", "c", "b")); -+ assert.notEqual(hashValue32("seeded", 1), hashValue32("seeded", 2)); -+ -+ const unitValue = hashToUnitInterval("unit"); -+ assert.equal(unitValue >= 0 && unitValue < 1, true); -+ -+ const signedValue = hashToSignedUnit("signed"); -+ assert.equal(signedValue >= -1 && signedValue < 1, true); -+ -+ assert.match(toStableHashString(new Date("2026-06-26T00:00:00.000Z")), /^date:/); -+ assert.equal(toStableHashString(Number.NaN), "number:NaN"); ++ assert.equal(valueNoise2D(1.25, 2.5, { seed: "a" }), valueNoise2D(1.25, 2.5, { seed: "a" })); ++ assert.notEqual(valueNoise2D(1.25, 2.5, { seed: "a" }), valueNoise2D(1.25, 2.5, { seed: "b" })); ++ assertInRange(valueNoise2D(3.5, -2, { seed: "range" }), 0, 1); ++ ++ const perlinValue = perlinNoise2D(0.25, 0.75, { seed: "perlin", frequency: 2 }); ++ const simplexValue = simplexNoise2D(0.25, 0.75, { seed: "simplex", frequency: 2 }); ++ assert.equal(perlinValue, perlinNoise2D(0.25, 0.75, { seed: "perlin", frequency: 2 })); ++ assert.equal(simplexValue, simplexNoise2D(0.25, 0.75, { seed: "simplex", frequency: 2 })); ++ assertInRange(perlinValue, -1, 1); ++ assertInRange(simplexValue, -1, 1); ++ ++ const fractalValue = fractalNoise2D(0.5, 0.75, { seed: "fractal", octaves: 3 }); ++ assert.equal(fractalValue, fractalNoise2D(0.5, 0.75, { seed: "fractal", octaves: 3 })); ++ assertInRange(fractalValue, 0, 1); ++ ++ assert.deepEqual(createNoisePermutation("perm", 8), createNoisePermutation("perm", 8)); ++ assert.notDeepEqual(createNoisePermutation("perm", 8), createNoisePermutation("other", 8)); ++ assert.deepEqual(createNoisePermutation("size", 4).sort((a, b) => a - b), [0, 1, 2, 3]); +} + +if (import.meta.url === `file://${process.argv[1]}`) { diff --git a/src/shared/noise/noise.js b/src/shared/noise/noise.js new file mode 100644 index 000000000..8908d847a --- /dev/null +++ b/src/shared/noise/noise.js @@ -0,0 +1,163 @@ +import { hashToUnitInterval, hashValue32 } from "../hash/hash.js"; +import { RandomSeed } from "../math/RandomSeed.js"; + +const SQRT3 = Math.sqrt(3); +const SIMPLEX_F2 = 0.5 * (SQRT3 - 1); +const SIMPLEX_G2 = (3 - SQRT3) / 6; + +function fade(value) { + return value * value * value * (value * (value * 6 - 15) + 10); +} + +function lerp(a, b, amount) { + return a + (b - a) * amount; +} + +function gradientDot(seed, ix, iy, x, y) { + const angle = hashToUnitInterval([seed, ix, iy, "gradient"]) * Math.PI * 2; + return Math.cos(angle) * x + Math.sin(angle) * y; +} + +function simplexCorner(seed, i, j, x, y) { + const t = 0.5 - x * x - y * y; + if (t < 0) { + return 0; + } + + const influence = t * t; + return influence * influence * gradientDot(seed, i, j, x, y); +} + +function normalizeFrequency(frequency) { + const normalized = Number(frequency); + return Number.isFinite(normalized) && normalized > 0 ? normalized : 1; +} + +/** + * Creates a deterministic shuffled permutation from a seed. + * + * @param {*} seed Seed value. + * @param {number} size Permutation size. + * @returns {number[]} Deterministic permutation. + */ +export function createNoisePermutation(seed = "noise", size = 256) { + const normalizedSize = Math.max(1, Math.floor(Number(size) || 256)); + return new RandomSeed(seed).shuffle(Array.from({ length: normalizedSize }, (_, index) => index)); +} + +/** + * Returns deterministic value noise in the range [0, 1]. + * + * @param {number} x X coordinate. + * @param {number} y Y coordinate. + * @param {{seed?: *, frequency?: number}} options Noise options. + * @returns {number} Value noise. + */ +export function valueNoise2D(x, y, options = {}) { + const frequency = normalizeFrequency(options.frequency); + const nx = Number(x) * frequency; + const ny = Number(y) * frequency; + const x0 = Math.floor(nx); + const y0 = Math.floor(ny); + const tx = fade(nx - x0); + const ty = fade(ny - y0); + const seed = options.seed ?? "value"; + + const v00 = hashToUnitInterval([seed, x0, y0]); + const v10 = hashToUnitInterval([seed, x0 + 1, y0]); + const v01 = hashToUnitInterval([seed, x0, y0 + 1]); + const v11 = hashToUnitInterval([seed, x0 + 1, y0 + 1]); + + return lerp(lerp(v00, v10, tx), lerp(v01, v11, tx), ty); +} + +/** + * Returns deterministic Perlin-style gradient noise, approximately in [-1, 1]. + * + * @param {number} x X coordinate. + * @param {number} y Y coordinate. + * @param {{seed?: *, frequency?: number}} options Noise options. + * @returns {number} Perlin-style noise. + */ +export function perlinNoise2D(x, y, options = {}) { + const frequency = normalizeFrequency(options.frequency); + const nx = Number(x) * frequency; + const ny = Number(y) * frequency; + const x0 = Math.floor(nx); + const y0 = Math.floor(ny); + const dx = nx - x0; + const dy = ny - y0; + const tx = fade(dx); + const ty = fade(dy); + const seed = options.seed ?? "perlin"; + + const n00 = gradientDot(seed, x0, y0, dx, dy); + const n10 = gradientDot(seed, x0 + 1, y0, dx - 1, dy); + const n01 = gradientDot(seed, x0, y0 + 1, dx, dy - 1); + const n11 = gradientDot(seed, x0 + 1, y0 + 1, dx - 1, dy - 1); + + return Math.max(-1, Math.min(1, lerp(lerp(n00, n10, tx), lerp(n01, n11, tx), ty))); +} + +/** + * Returns deterministic Simplex-style 2D noise, approximately in [-1, 1]. + * + * @param {number} x X coordinate. + * @param {number} y Y coordinate. + * @param {{seed?: *, frequency?: number}} options Noise options. + * @returns {number} Simplex-style noise. + */ +export function simplexNoise2D(x, y, options = {}) { + const frequency = normalizeFrequency(options.frequency); + const nx = Number(x) * frequency; + const ny = Number(y) * frequency; + const seed = options.seed ?? "simplex"; + const skew = (nx + ny) * SIMPLEX_F2; + const i = Math.floor(nx + skew); + const j = Math.floor(ny + skew); + const unskew = (i + j) * SIMPLEX_G2; + const x0 = nx - (i - unskew); + const y0 = ny - (j - unskew); + const i1 = x0 > y0 ? 1 : 0; + const j1 = x0 > y0 ? 0 : 1; + const x1 = x0 - i1 + SIMPLEX_G2; + const y1 = y0 - j1 + SIMPLEX_G2; + const x2 = x0 - 1 + 2 * SIMPLEX_G2; + const y2 = y0 - 1 + 2 * SIMPLEX_G2; + + const value = 70 * ( + simplexCorner(seed, i, j, x0, y0) + + simplexCorner(seed, i + i1, j + j1, x1, y1) + + simplexCorner(seed, i + 1, j + 1, x2, y2) + ); + return Math.max(-1, Math.min(1, value)); +} + +/** + * Combines octaves of a deterministic noise function. + * + * @param {number} x X coordinate. + * @param {number} y Y coordinate. + * @param {{seed?: *, octaves?: number, frequency?: number, lacunarity?: number, persistence?: number, noise?: Function}} options Fractal options. + * @returns {number} Weighted fractal noise. + */ +export function fractalNoise2D(x, y, options = {}) { + const octaves = Math.max(1, Math.floor(Number(options.octaves) || 4)); + const lacunarity = Number.isFinite(options.lacunarity) ? options.lacunarity : 2; + const persistence = Number.isFinite(options.persistence) ? options.persistence : 0.5; + const noise = typeof options.noise === "function" ? options.noise : valueNoise2D; + let frequency = normalizeFrequency(options.frequency); + let amplitude = 1; + let total = 0; + let amplitudeTotal = 0; + + for (let octave = 0; octave < octaves; octave += 1) { + const seed = hashValue32([options.seed ?? "fractal", octave]); + total += noise(x, y, { ...options, seed, frequency }) * amplitude; + amplitudeTotal += amplitude; + amplitude *= persistence; + frequency *= lacunarity; + } + + return amplitudeTotal === 0 ? 0 : total / amplitudeTotal; +} diff --git a/tests/shared/NoiseFoundation.test.mjs b/tests/shared/NoiseFoundation.test.mjs new file mode 100644 index 000000000..7bee4586b --- /dev/null +++ b/tests/shared/NoiseFoundation.test.mjs @@ -0,0 +1,44 @@ +/* +Toolbox Aid +David Quesenberry +06/26/2026 +NoiseFoundation.test.mjs +*/ +import assert from "node:assert/strict"; +import { + createNoisePermutation, + fractalNoise2D, + perlinNoise2D, + simplexNoise2D, + valueNoise2D, +} from "../../src/shared/noise/noise.js"; + +function assertInRange(value, min, max) { + assert.equal(Number.isFinite(value), true); + assert.equal(value >= min && value <= max, true, `${value} was outside ${min}..${max}`); +} + +export function run() { + assert.equal(valueNoise2D(1.25, 2.5, { seed: "a" }), valueNoise2D(1.25, 2.5, { seed: "a" })); + assert.notEqual(valueNoise2D(1.25, 2.5, { seed: "a" }), valueNoise2D(1.25, 2.5, { seed: "b" })); + assertInRange(valueNoise2D(3.5, -2, { seed: "range" }), 0, 1); + + const perlinValue = perlinNoise2D(0.25, 0.75, { seed: "perlin", frequency: 2 }); + const simplexValue = simplexNoise2D(0.25, 0.75, { seed: "simplex", frequency: 2 }); + assert.equal(perlinValue, perlinNoise2D(0.25, 0.75, { seed: "perlin", frequency: 2 })); + assert.equal(simplexValue, simplexNoise2D(0.25, 0.75, { seed: "simplex", frequency: 2 })); + assertInRange(perlinValue, -1, 1); + assertInRange(simplexValue, -1, 1); + + const fractalValue = fractalNoise2D(0.5, 0.75, { seed: "fractal", octaves: 3 }); + assert.equal(fractalValue, fractalNoise2D(0.5, 0.75, { seed: "fractal", octaves: 3 })); + assertInRange(fractalValue, 0, 1); + + assert.deepEqual(createNoisePermutation("perm", 8), createNoisePermutation("perm", 8)); + assert.notDeepEqual(createNoisePermutation("perm", 8), createNoisePermutation("other", 8)); + assert.deepEqual(createNoisePermutation("size", 4).sort((a, b) => a - b), [0, 1, 2, 3]); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + run(); +}