From b68cc948a36093fc6fc926257560c665d7ecb2aa Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Fri, 12 Jun 2026 10:05:27 +0200 Subject: [PATCH] fix: ensure procedural guardian names are unique across floors - Modified generateGuardianName() in guardian-encounters.ts to add a cycleOffset (floor/50) that shifts prefix selection for the same elements at different 100-floor ranges - For multi-element names, also shift the title by cycleOffset to further reduce collision chance - Added regression test guardian-names-unique.test.ts with 7 test cases covering: * Previously colliding floors (170/370 crystal, 200/230 exotic convergence, 210/240 astral convergence) * All static guardian floors (10-240) uniqueness * All procedural guardian floors (250-490) uniqueness * Full range (10-490) uniqueness * Determinism check * Title format validation Fixes #376 --- docs/circular-deps.txt | 2 +- docs/dependency-graph.json | 2 +- docs/project-structure.txt | 1 + .../__tests__/guardian-names-unique.test.ts | 82 +++++++++++++++++++ src/lib/game/data/guardian-encounters.ts | 10 ++- 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/lib/game/__tests__/guardian-names-unique.test.ts diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index e952961..07ecb3b 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # Circular Dependencies -Generated: 2026-06-12T07:05:52.901Z +Generated: 2026-06-12T07:45:32.728Z Found: 4 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 88ba3c9..93a6139 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-12T07:05:50.633Z", + "generated": "2026-06-12T07:45:30.561Z", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." }, diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 61d471b..f993252 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -231,6 +231,7 @@ Mana-Loop/ │ │ │ │ ├── floor-utils.test.ts │ │ │ │ ├── floor-utils.upgraded.test.ts │ │ │ │ ├── formatting.test.ts +│ │ │ │ ├── guardian-names-unique.test.ts │ │ │ │ ├── guardian-names.test.ts │ │ │ │ ├── hasty-enchanter.test.ts │ │ │ │ ├── mana-conversion-component-deduction.test.ts diff --git a/src/lib/game/__tests__/guardian-names-unique.test.ts b/src/lib/game/__tests__/guardian-names-unique.test.ts new file mode 100644 index 0000000..61c31d9 --- /dev/null +++ b/src/lib/game/__tests__/guardian-names-unique.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from 'vitest'; +import { generateGuardianName } from '../data/guardian-encounters'; +import { getProceduralGuardian } from '../data/guardian-procedural'; +import { getGuardianForFloor } from '../data/guardian-encounters'; + +describe('guardian name uniqueness', () => { + it('generateGuardianName produces different names for same element at different 100-floor cycles', () => { + // These floors previously collided: same element, same prefix, same title + const name170 = generateGuardianName(['crystal'], 170); + const name370 = generateGuardianName(['crystal'], 370); + expect(name170).not.toBe(name370); + }); + + it('generateGuardianName produces different names for multi-element guardians at different cycles', () => { + // Floors 200/230 share elements [crystal, stellar, void] + const name200 = generateGuardianName(['crystal', 'stellar', 'void'], 200); + const name230 = generateGuardianName(['crystal', 'stellar', 'void'], 230); + expect(name200).not.toBe(name230); + + // Floors 210/240 share elements [soul, time, plasma] + const name210 = generateGuardianName(['soul', 'time', 'plasma'], 210); + const name240 = generateGuardianName(['soul', 'time', 'plasma'], 240); + expect(name210).not.toBe(name240); + }); + + it('all static guardian floors (10-240) produce unique names', () => { + const names = new Map(); + for (let floor = 10; floor <= 240; floor += 10) { + const g = getGuardianForFloor(floor); + if (g) { + const existing = names.get(g.name); + expect(existing).toBeUndefined( + `Duplicate name "${g.name}" at floors ${existing} and ${floor}`, + ); + names.set(g.name, floor); + } + } + }); + + it('all procedural guardian floors (250-490) produce unique names', () => { + const names = new Map(); + for (let floor = 250; floor <= 490; floor += 10) { + const g = getProceduralGuardian(floor); + if (g) { + const existing = names.get(g.name); + expect(existing).toBeUndefined( + `Duplicate name "${g.name}" at floors ${existing} and ${floor}`, + ); + names.set(g.name, floor); + } + } + }); + + it('all guardian floors (10-490) produce unique names', () => { + const names = new Map(); + for (let floor = 10; floor <= 490; floor += 10) { + const g = getGuardianForFloor(floor); + if (g) { + const existing = names.get(g.name); + expect(existing).toBeUndefined( + `Duplicate name "${g.name}" at floors ${existing} and ${floor}`, + ); + names.set(g.name, floor); + } + } + }); + + it('generateGuardianName is deterministic (same inputs always produce same output)', () => { + const name1 = generateGuardianName(['fire', 'water'], 250); + const name2 = generateGuardianName(['fire', 'water'], 250); + expect(name1).toBe(name2); + + const name3 = generateGuardianName(['crystal'], 170); + const name4 = generateGuardianName(['crystal'], 170); + expect(name3).toBe(name4); + }); + + it('generateGuardianName includes a title', () => { + const name = generateGuardianName(['crystal'], 170); + expect(name).toMatch(/ the (Warden|Keeper|Lord|Titan|Sovereign|Guardian|Sentinel|Champion|Overlord|Archon)$/); + }); +}); diff --git a/src/lib/game/data/guardian-encounters.ts b/src/lib/game/data/guardian-encounters.ts index 19754da..3cd450f 100644 --- a/src/lib/game/data/guardian-encounters.ts +++ b/src/lib/game/data/guardian-encounters.ts @@ -46,11 +46,17 @@ const GUARDIAN_TITLES: string[] = [ ]; export function generateGuardianName(elements: string[], floor: number = 0): string { + // Use a cycle offset to ensure the same elements at different 100-floor ranges + // produce different names. Without this, floors 170/370/570 etc. with the same + // element(s) would produce identical names because both prefix and title cycle. + const cycleOffset = Math.floor(floor / 50); const parts = elements.map((el, i) => { const prefixes = GUARDIAN_PREFIXES[el] || ['Unknown']; - return prefixes[(floor + i) % prefixes.length]; + return prefixes[(floor + i + cycleOffset) % prefixes.length]; }); - const title = GUARDIAN_TITLES[Math.floor(floor / 10) % GUARDIAN_TITLES.length]; + const titleIndex = Math.floor(floor / 10) % GUARDIAN_TITLES.length; + // For multi-element names, also shift the title to reduce collision chance + const title = GUARDIAN_TITLES[(titleIndex + (elements.length > 1 ? cycleOffset : 0)) % GUARDIAN_TITLES.length]; return `${parts.join('-')} the ${title}`; }