import { describe, it, expect } from 'vitest'; import { getRoomsForFloor, generateSpireRoomType, generateSpireFloorState, getSpireEnemyArmor, getSpireEnemyBarrier, calcInsight, getSpireRoomTypeDisplay, SPIRE_CONFIG, } from '../utils/spire-utils'; import { isGuardianFloor, getGuardianForFloor, getGuardianHP, generateGuardianName, getAllGuardianFloors } from '../data/guardian-encounters'; // ─── Spire Utils ───────────────────────────────────────────────────────────── describe('getRoomsForFloor', () => { it('should return at least minRoomsPerFloor for non-guardian floors (seeded)', () => { for (let floor = 1; floor <= 50; floor++) { if (floor % 10 === 0) continue; const rooms = getRoomsForFloor(floor, floor * 12345); expect(rooms).toBeGreaterThanOrEqual(SPIRE_CONFIG.minRoomsPerFloor); expect(rooms).toBeLessThanOrEqual(SPIRE_CONFIG.minRoomsPerFloor + 10 + 2); } }); it('should return 1 room for guardian floors', () => { expect(getRoomsForFloor(10)).toBe(1); expect(getRoomsForFloor(20)).toBe(1); expect(getRoomsForFloor(100)).toBe(1); }); it('should return deterministic results with same seed', () => { for (let floor = 1; floor <= 50; floor++) { if (floor % 10 === 0) continue; const seed = floor * 12345; const a = getRoomsForFloor(floor, seed); const b = getRoomsForFloor(floor, seed); expect(a).toBe(b); } }); it('should return more rooms for higher non-guardian floors (seeded)', () => { const lowFloor = getRoomsForFloor(3, 3 * 12345); const highFloor = getRoomsForFloor(79, 79 * 12345); expect(highFloor).toBeGreaterThanOrEqual(lowFloor); }); }); describe('generateSpireRoomType', () => { it('should return guardian for last room on guardian floors', () => { const totalRooms = getRoomsForFloor(10); const roomType = generateSpireRoomType(10, totalRooms - 1, totalRooms); expect(roomType).toBe('guardian'); }); it('should return valid room types for any room (spec §4.3)', () => { const validTypes = ['combat', 'swarm', 'speed', 'guardian', 'puzzle', 'recovery', 'library', 'treasure']; for (const floor of [1, 5, 15, 25]) { for (let ri = 0; ri < 10; ri++) { const roomType = generateSpireRoomType(floor, ri, 10); expect(validTypes).toContain(roomType); } } }); it('should never return guardian for non-last room on guardian floors', () => { for (let i = 0; i < 50; i++) { const roomType = generateSpireRoomType(50, 0, 10); expect(roomType).not.toBe('guardian'); } }); it('should return puzzle on every 7th floor for exactly one room', () => { // Floor 7 should have exactly one puzzle room let puzzleCount = 0; for (let ri = 0; ri < 17; ri++) { const roomType = generateSpireRoomType(7, ri, 10); if (roomType === 'puzzle') puzzleCount++; } expect(puzzleCount).toBe(1); }); it('should produce deterministic results for same floor/roomIndex', () => { for (let i = 0; i < 20; i++) { const a = generateSpireRoomType(15, 5, 10); const b = generateSpireRoomType(15, 5, 10); expect(a).toBe(b); } }); }); describe('generateSpireFloorState', () => { it('should generate guardian floor for floor 10', () => { const state = generateSpireFloorState(10, 0, 1); expect(state.roomType).toBe('guardian'); expect(state.enemies.length).toBe(1); expect(state.enemies[0].name).toBeTruthy(); }); it('should generate valid floor state with roomType', () => { // With seeded RNG, room 0 on floor 5 may be any valid type. // Just verify the state is structurally valid. const state = generateSpireFloorState(5, 0, 8); expect(state.roomType).toBeTruthy(); expect(state.enemies).toBeDefined(); if (state.enemies.length > 0) { expect(state.enemies[0].hp).toBeGreaterThan(0); expect(state.enemies[0].maxHP).toBeGreaterThan(0); } }); it('should generate combat rooms with enemies having correct HP', () => { // Test multiple floors/rooms to find a combat room let foundCombat = false; for (const floor of [3, 11, 23, 37, 49]) { for (let ri = 0; ri < 12; ri++) { const state = generateSpireFloorState(floor, ri, 10); if (state.roomType === 'combat') { expect(state.enemies.length).toBe(1); expect(state.enemies[0].hp).toBeGreaterThan(0); expect(state.enemies[0].maxHP).toBe(state.enemies[0].hp); foundCombat = true; break; } } if (foundCombat) break; } expect(foundCombat).toBe(true); }); it('should generate swarm floor with multiple enemies', () => { // Test multiple rooms to find a swarm room let foundSwarm = false; for (let ri = 1; ri < 15; ri++) { const state = generateSpireFloorState(20, ri, 12); if (state.roomType === 'swarm') { expect(state.enemies.length).toBeGreaterThanOrEqual(3); foundSwarm = true; break; } } expect(foundSwarm).toBe(true); }); it('should generate library rooms with no enemies', () => { // Test multiple rooms to find a library room let foundLibrary = false; for (const floor of [1, 5, 11, 19, 23]) { for (let ri = 1; ri < 15; ri++) { const state = generateSpireFloorState(floor, ri, 12); if (state.roomType === 'library') { expect(state.enemies.length).toBe(0); expect(state.libraryProgress).toBeDefined(); foundLibrary = true; break; } } if (foundLibrary) break; } expect(foundLibrary).toBe(true); }); }); describe('getSpireEnemyArmor', () => { it('should return 0 for floors below 10', () => { for (let i = 0; i < 20; i++) { const armor = getSpireEnemyArmor(5); expect(armor).toBeGreaterThanOrEqual(0); expect(armor).toBeLessThanOrEqual(0.3); } }); it('should return values between 0 and 0.3', () => { for (let floor = 1; floor <= 100; floor++) { const armor = getSpireEnemyArmor(floor); expect(armor).toBeGreaterThanOrEqual(0); expect(armor).toBeLessThanOrEqual(0.3); } }); }); describe('getSpireEnemyBarrier', () => { it('should return 0 for floors below 15', () => { for (let i = 0; i < 10; i++) { const barrier = getSpireEnemyBarrier(10, 'fire'); expect(barrier).toBeGreaterThanOrEqual(0); } }); it('should return values between 0 and 0.3', () => { for (let floor = 15; floor <= 100; floor++) { const barrier = getSpireEnemyBarrier(floor, 'fire'); expect(barrier).toBeGreaterThanOrEqual(0); expect(barrier).toBeLessThanOrEqual(0.3000000001); } }); }); describe('calcInsight', () => { it('should return positive insight for any floor', () => { expect(calcInsight(1, false)).toBeGreaterThan(0); expect(calcInsight(10, true)).toBeGreaterThan(0); expect(calcInsight(50, false)).toBeGreaterThan(0); }); it('should give more insight for guardian floors', () => { const normal = calcInsight(10, false); const guardian = calcInsight(10, true); expect(guardian).toBeGreaterThan(normal); }); it('should scale with floor number', () => { const low = calcInsight(5, false); const high = calcInsight(50, false); expect(high).toBeGreaterThan(low); }); }); describe('getSpireRoomTypeDisplay', () => { it('should return display info for all room types', () => { const types = ['combat', 'swarm', 'speed', 'guardian', 'puzzle', 'recovery', 'library', 'treasure']; for (const type of types) { const display = getSpireRoomTypeDisplay(type as any); expect(display.label).toBeTruthy(); expect(display.icon).toBeTruthy(); expect(display.color).toBeTruthy(); } }); it('should return unknown for invalid room type', () => { const display = getSpireRoomTypeDisplay('invalid' as any); expect(display.label).toBe('Unknown'); }); }); // ─── Guardian Encounters ───────────────────────────────────────────────────── describe('isGuardianFloor', () => { it('should return true for every 10th floor', () => { expect(isGuardianFloor(10)).toBe(true); expect(isGuardianFloor(20)).toBe(true); expect(isGuardianFloor(100)).toBe(true); expect(isGuardianFloor(150)).toBe(true); }); it('should return false for non-10th floors', () => { expect(isGuardianFloor(1)).toBe(false); expect(isGuardianFloor(15)).toBe(false); expect(isGuardianFloor(99)).toBe(false); }); }); describe('getGuardianForFloor (unified lookup)', () => { it('should return base element guardians for floors 10-70', () => { expect(getGuardianForFloor(10)!.element).toEqual(['fire']); expect(getGuardianForFloor(20)!.element).toEqual(['water']); expect(getGuardianForFloor(30)!.element).toEqual(['air']); expect(getGuardianForFloor(40)!.element).toEqual(['earth']); expect(getGuardianForFloor(50)!.element).toEqual(['light']); expect(getGuardianForFloor(60)!.element).toEqual(['dark']); expect(getGuardianForFloor(70)!.element).toEqual(['death']); }); it('should return utility guardian for floor 80', () => { expect(getGuardianForFloor(80)!.element).toEqual(['transference']); }); it('should return composite guardians for floors 90-110', () => { expect(getGuardianForFloor(90)!.element).toEqual(['metal']); expect(getGuardianForFloor(100)!.element).toEqual(['sand']); expect(getGuardianForFloor(110)!.element).toEqual(['lightning']); }); it('should return multi-element guardians for tier 3 floors', () => { expect(getGuardianForFloor(130)!.element).toEqual(['metal', 'fire', 'earth']); expect(getGuardianForFloor(140)!.element).toEqual(['sand', 'earth', 'water']); expect(getGuardianForFloor(150)!.element).toEqual(['lightning', 'fire', 'air']); }); it('should return exotic guardians for floors 170-200', () => { expect(getGuardianForFloor(170)!.element).toEqual(['crystal']); expect(getGuardianForFloor(180)!.element).toEqual(['stellar']); expect(getGuardianForFloor(190)!.element).toEqual(['void']); }); it('should return multi-element exotic convergence for floor 200', () => { const g200 = getGuardianForFloor(200); expect(g200).not.toBeNull(); expect(g200!.element.length).toBeGreaterThan(1); expect(g200!.element).toContain('crystal'); expect(g200!.element).toContain('stellar'); expect(g200!.element).toContain('void'); }); it('should return guardians for procedural floors 210+', () => { const g210 = getGuardianForFloor(210); expect(g210).not.toBeNull(); expect(g210!.element.length).toBeGreaterThanOrEqual(2); }); it('should return null for non-guardian floors', () => { expect(getGuardianForFloor(1)).toBeNull(); expect(getGuardianForFloor(15)).toBeNull(); expect(getGuardianForFloor(95)).toBeNull(); }); }); describe('getGuardianHP', () => { it('should return positive HP', () => { expect(getGuardianHP(10)).toBeGreaterThan(0); expect(getGuardianHP(100)).toBeGreaterThan(0); expect(getGuardianHP(200)).toBeGreaterThan(0); }); it('should scale with floor', () => { const low = getGuardianHP(10); const high = getGuardianHP(100); expect(high).toBeGreaterThan(low); }); }); describe('generateGuardianName', () => { it('should generate non-empty names', () => { for (const element of ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death']) { const name = generateGuardianName([element]); expect(name).toBeTruthy(); expect(name.length).toBeGreaterThan(0); } }); it('should include a title', () => { const name = generateGuardianName(['fire']); expect(name).toContain(' the '); }); }); describe('getAllGuardianFloors', () => { it('should include base guardian floors', () => { const floors = getAllGuardianFloors(); expect(floors).toContain(10); expect(floors).toContain(20); expect(floors).toContain(100); }); it('should be sorted', () => { const floors = getAllGuardianFloors(); for (let i = 1; i < floors.length; i++) { expect(floors[i]).toBeGreaterThan(floors[i - 1]); } }); it('should include procedural floors', () => { const floors = getAllGuardianFloors(); expect(floors).toContain(210); expect(floors).toContain(250); expect(floors).toContain(300); expect(floors).toContain(400); }); });