fix: ensure procedural guardian names are unique across floors
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s
- 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
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# 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.
|
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_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.",
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── floor-utils.test.ts
|
│ │ │ │ ├── floor-utils.test.ts
|
||||||
│ │ │ │ ├── floor-utils.upgraded.test.ts
|
│ │ │ │ ├── floor-utils.upgraded.test.ts
|
||||||
│ │ │ │ ├── formatting.test.ts
|
│ │ │ │ ├── formatting.test.ts
|
||||||
|
│ │ │ │ ├── guardian-names-unique.test.ts
|
||||||
│ │ │ │ ├── guardian-names.test.ts
|
│ │ │ │ ├── guardian-names.test.ts
|
||||||
│ │ │ │ ├── hasty-enchanter.test.ts
|
│ │ │ │ ├── hasty-enchanter.test.ts
|
||||||
│ │ │ │ ├── mana-conversion-component-deduction.test.ts
|
│ │ │ │ ├── mana-conversion-component-deduction.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<string, number>();
|
||||||
|
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<string, number>();
|
||||||
|
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<string, number>();
|
||||||
|
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)$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -46,11 +46,17 @@ const GUARDIAN_TITLES: string[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function generateGuardianName(elements: string[], floor: number = 0): 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 parts = elements.map((el, i) => {
|
||||||
const prefixes = GUARDIAN_PREFIXES[el] || ['Unknown'];
|
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}`;
|
return `${parts.join('-')} the ${title}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user