import { describe, it, expect } from 'vitest'; import { generatePointCrawlMap, hasPath } from '@/samples/slay-the-spire-like/map/generator'; import { MapNodeType, MapLayerType } from '@/samples/slay-the-spire-like/map/types'; describe('generatePointCrawlMap', () => { it('should generate a map with 10 layers', () => { const map = generatePointCrawlMap(123); expect(map.layers.length).toBe(10); }); it('should have correct layer structure', () => { const map = generatePointCrawlMap(123); const expectedStructure = [ 'start', MapLayerType.Wild, MapLayerType.Wild, MapLayerType.Settlement, MapLayerType.Wild, MapLayerType.Wild, MapLayerType.Settlement, MapLayerType.Wild, MapLayerType.Wild, 'end', ]; for (let i = 0; i < expectedStructure.length; i++) { expect(map.layers[i].layerType).toBe(expectedStructure[i]); } }); it('should have correct node counts per layer', () => { const map = generatePointCrawlMap(123); const expectedCounts = [1, 3, 3, 4, 3, 3, 4, 3, 3, 1]; for (let i = 0; i < expectedCounts.length; i++) { expect(map.layers[i].nodeIds.length).toBe(expectedCounts[i]); } }); it('should have Start and End nodes with correct types', () => { const map = generatePointCrawlMap(123); const startNode = map.nodes.get('node-0-0'); const endNode = map.nodes.get('node-9-0'); expect(startNode?.type).toBe(MapNodeType.Start); expect(endNode?.type).toBe(MapNodeType.End); }); it('should have wild layers with minion/elite/event types', () => { const map = generatePointCrawlMap(123); const wildLayerIndices = [1, 2, 4, 5, 7, 8]; const validWildTypes = new Set([MapNodeType.Minion, MapNodeType.Elite, MapNodeType.Event]); for (const layerIdx of wildLayerIndices) { const layer = map.layers[layerIdx]; for (const nodeId of layer.nodeIds) { const node = map.nodes.get(nodeId); expect(node).toBeDefined(); expect(validWildTypes.has(node!.type)).toBe(true); } } }); it('should have settlement layers with at least 1 camp, 1 shop, 1 curio', () => { const map = generatePointCrawlMap(123); const settlementLayerIndices = [3, 6]; for (const layerIdx of settlementLayerIndices) { const layer = map.layers[layerIdx]; const nodeTypes = layer.nodeIds.map(id => map.nodes.get(id)!.type); expect(nodeTypes).toContain(MapNodeType.Camp); expect(nodeTypes).toContain(MapNodeType.Shop); expect(nodeTypes).toContain(MapNodeType.Curio); expect(nodeTypes.length).toBe(4); } }); it('should have Start connected to all 3 wild nodes', () => { const map = generatePointCrawlMap(42); const startNode = map.nodes.get('node-0-0'); const wildLayer = map.layers[1]; expect(startNode?.childIds.length).toBe(3); expect(startNode?.childIds).toEqual(expect.arrayContaining(wildLayer.nodeIds)); }); it('should have each wild node connect to 1 wild node in wild→wild layers', () => { const map = generatePointCrawlMap(42); const wildToWildTransitions = [ { src: 1, tgt: 2 }, { src: 4, tgt: 5 }, { src: 7, tgt: 8 }, ]; for (const transition of wildToWildTransitions) { const srcLayer = map.layers[transition.src]; for (const srcId of srcLayer.nodeIds) { const srcNode = map.nodes.get(srcId); expect(srcNode?.childIds.length).toBe(1); const childLayer = map.layers[transition.tgt]; expect(childLayer.nodeIds).toContain(srcNode!.childIds[0]); } } }); it('should have each wild node connect to 2 settlement nodes in wild→settlement layers', () => { const map = generatePointCrawlMap(42); const wildToSettlementTransitions = [ { src: 2, tgt: 3 }, { src: 5, tgt: 6 }, ]; for (const transition of wildToSettlementTransitions) { const srcLayer = map.layers[transition.src]; for (const srcId of srcLayer.nodeIds) { const srcNode = map.nodes.get(srcId); expect(srcNode?.childIds.length).toBe(2); const childLayer = map.layers[transition.tgt]; for (const childId of srcNode!.childIds) { expect(childLayer.nodeIds).toContain(childId); } } } }); it('should have settlement nodes connect correctly (1-2-2-1 pattern)', () => { const map = generatePointCrawlMap(42); const settlementToWildTransitions = [ { src: 3, tgt: 4 }, { src: 6, tgt: 7 }, ]; for (const transition of settlementToWildTransitions) { const srcLayer = map.layers[transition.src]; const tgtLayer = map.layers[transition.tgt]; // First and last settlement connect to 1 wild const firstSettlement = map.nodes.get(srcLayer.nodeIds[0]); expect(firstSettlement?.childIds.length).toBe(1); const lastSettlement = map.nodes.get(srcLayer.nodeIds[3]); expect(lastSettlement?.childIds.length).toBe(1); // Middle two settlements connect to 2 wilds for (let i = 1; i <= 2; i++) { const midSettlement = map.nodes.get(srcLayer.nodeIds[i]); expect(midSettlement?.childIds.length).toBe(2); for (const childId of midSettlement!.childIds) { expect(tgtLayer.nodeIds).toContain(childId); } } } }); it('should have all 3 wild nodes connect to End', () => { const map = generatePointCrawlMap(42); const lastWildLayer = map.layers[8]; const endNode = map.nodes.get('node-9-0'); for (const wildId of lastWildLayer.nodeIds) { const wildNode = map.nodes.get(wildId); expect(wildNode?.childIds).toEqual([endNode!.id]); } }); it('should have all nodes reachable from Start and can reach End', () => { const map = generatePointCrawlMap(123); const startId = 'node-0-0'; const endId = 'node-9-0'; for (const nodeId of map.nodes.keys()) { if (nodeId === startId || nodeId === endId) continue; expect(hasPath(map, startId, nodeId)).toBe(true); expect(hasPath(map, nodeId, endId)).toBe(true); } }); it('should not have crossing edges in wild→wild transitions', () => { const map = generatePointCrawlMap(12345); const wildToWildTransitions = [ { src: 1, tgt: 2 }, { src: 4, tgt: 5 }, { src: 7, tgt: 8 }, ]; for (const transition of wildToWildTransitions) { const srcLayer = map.layers[transition.src]; const tgtLayer = map.layers[transition.tgt]; // Collect edges as pairs of indices const edges: Array<{ srcIndex: number; tgtIndex: number }> = []; for (let s = 0; s < srcLayer.nodeIds.length; s++) { const srcNode = map.nodes.get(srcLayer.nodeIds[s]); for (const tgtId of srcNode!.childIds) { const t = tgtLayer.nodeIds.indexOf(tgtId); edges.push({ srcIndex: s, tgtIndex: t }); } } // Check for crossings for (let e1 = 0; e1 < edges.length; e1++) { for (let e2 = e1 + 1; e2 < edges.length; e2++) { const { srcIndex: s1, tgtIndex: t1 } = edges[e1]; const { srcIndex: s2, tgtIndex: t2 } = edges[e2]; if (s1 === s2) continue; if (t1 === t2) continue; const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2); expect(crosses).toBe(false); } } } }); it('should not have crossing edges in wild→settlement transitions', () => { const map = generatePointCrawlMap(12345); const wildToSettlementTransitions = [ { src: 2, tgt: 3 }, { src: 5, tgt: 6 }, ]; for (const transition of wildToSettlementTransitions) { const srcLayer = map.layers[transition.src]; const tgtLayer = map.layers[transition.tgt]; // Collect edges as pairs of indices const edges: Array<{ srcIndex: number; tgtIndex: number }> = []; for (let s = 0; s < srcLayer.nodeIds.length; s++) { const srcNode = map.nodes.get(srcLayer.nodeIds[s]); for (const tgtId of srcNode!.childIds) { const t = tgtLayer.nodeIds.indexOf(tgtId); edges.push({ srcIndex: s, tgtIndex: t }); } } // Check for crossings for (let e1 = 0; e1 < edges.length; e1++) { for (let e2 = e1 + 1; e2 < edges.length; e2++) { const { srcIndex: s1, tgtIndex: t1 } = edges[e1]; const { srcIndex: s2, tgtIndex: t2 } = edges[e2]; if (s1 === s2) continue; if (t1 === t2) continue; const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2); expect(crosses).toBe(false); } } } }); it('should not have crossing edges in settlement→wild transitions', () => { const map = generatePointCrawlMap(12345); const settlementToWildTransitions = [ { src: 3, tgt: 4 }, { src: 6, tgt: 7 }, ]; for (const transition of settlementToWildTransitions) { const srcLayer = map.layers[transition.src]; const tgtLayer = map.layers[transition.tgt]; // Collect edges as pairs of indices const edges: Array<{ srcIndex: number; tgtIndex: number }> = []; for (let s = 0; s < srcLayer.nodeIds.length; s++) { const srcNode = map.nodes.get(srcLayer.nodeIds[s]); for (const tgtId of srcNode!.childIds) { const t = tgtLayer.nodeIds.indexOf(tgtId); edges.push({ srcIndex: s, tgtIndex: t }); } } // Check for crossings for (let e1 = 0; e1 < edges.length; e1++) { for (let e2 = e1 + 1; e2 < edges.length; e2++) { const { srcIndex: s1, tgtIndex: t1 } = edges[e1]; const { srcIndex: s2, tgtIndex: t2 } = edges[e2]; if (s1 === s2) continue; if (t1 === t2) continue; const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2); expect(crosses).toBe(false); } } } }); it('should assign encounters to all non-Start/End nodes', () => { const map = generatePointCrawlMap(456); for (const node of map.nodes.values()) { if (node.type === MapNodeType.Start || node.type === MapNodeType.End) { // Start and End nodes should not have encounters expect(node.encounter).toBeUndefined(); } else { // All other nodes (minion/elite/event/camp/shop/curio) must have encounters expect(node.encounter, `Node ${node.id} (${node.type}) should have encounter data`).toBeDefined(); expect(node.encounter!.name).toBeTruthy(); expect(node.encounter!.description).toBeTruthy(); } } }); it('should assign encounters to all nodes across multiple seeds', () => { // Test multiple seeds to ensure no random failure for (let seed = 0; seed < 20; seed++) { const map = generatePointCrawlMap(seed); for (const node of map.nodes.values()) { if (node.type === MapNodeType.Start || node.type === MapNodeType.End) { continue; } expect(node.encounter, `Seed ${seed}: Node ${node.id} (${node.type}) missing encounter`).toBeDefined(); expect(node.encounter!.name).toBeTruthy(); expect(node.encounter!.description).toBeTruthy(); } } }); it('should minimize same-layer repetitions in wild layer pairs', () => { // Test that wild layers in pairs (1-2, 4-5, 7-8) have minimal duplicate types within each layer const map = generatePointCrawlMap(12345); const wildPairIndices = [ [1, 2], [4, 5], [7, 8], ]; for (const [layer1Idx, layer2Idx] of wildPairIndices) { const layer1 = map.layers[layer1Idx]; const layer2 = map.layers[layer2Idx]; // Count repetitions in layer 1 const layer1Types = layer1.nodeIds.map(id => map.nodes.get(id)!.type); const layer1Unique = new Set(layer1Types).size; const layer1Repetitions = layer1Types.length - layer1Unique; // Count repetitions in layer 2 const layer2Types = layer2.nodeIds.map(id => map.nodes.get(id)!.type); const layer2Unique = new Set(layer2Types).size; const layer2Repetitions = layer2Types.length - layer2Unique; // With optimal selection, we expect fewer repetitions than pure random // On average, random would have ~1.5 repetitions per 3-node layer // With 3 attempts, we should typically get 0-1 repetitions expect(layer1Repetitions + layer2Repetitions).toBeLessThanOrEqual(2); } }); it('should minimize adjacent repetitions in wild→wild connections', () => { // Test that wild nodes connected by wild→wild edges have different types const map = generatePointCrawlMap(12345); const wildToWildPairs = [ { src: 1, tgt: 2 }, { src: 4, tgt: 5 }, { src: 7, tgt: 8 }, ]; let totalAdjacentRepetitions = 0; for (const pair of wildToWildPairs) { const srcLayer = map.layers[pair.src]; const tgtLayer = map.layers[pair.tgt]; // Each wild node connects to exactly 1 wild node in next layer (1-to-1) for (let i = 0; i < srcLayer.nodeIds.length; i++) { const srcNode = map.nodes.get(srcLayer.nodeIds[i])!; const tgtId = srcNode.childIds[0]; const tgtNode = map.nodes.get(tgtId)!; if (srcNode.type === tgtNode.type) { totalAdjacentRepetitions++; } } } // With 3 wild pairs and 3 nodes each, that's 9 connections total // Random would have ~3 repetitions (1/3 chance per connection) // With optimal selection of 3 attempts, should be much lower (0-2) expect(totalAdjacentRepetitions).toBeLessThanOrEqual(3); }); });