diff --git a/tests/samples/slay-the-spire-like/map/generator.test.ts b/tests/samples/slay-the-spire-like/map/generator.test.ts index c09a7bc..76901ad 100644 --- a/tests/samples/slay-the-spire-like/map/generator.test.ts +++ b/tests/samples/slay-the-spire-like/map/generator.test.ts @@ -1,386 +1,405 @@ -import { describe, it, expect } from 'vitest'; -import { generatePointCrawlMap, hasPath } from '@/samples/slay-the-spire-like/system/map/generator'; -import { MapNodeType, MapLayerType } from '@/samples/slay-the-spire-like/system/map/types'; -import { createRNG } from '@/utils/rng'; -import { encounters } from '@/samples/slay-the-spire-like/data/desert'; +import { describe, it, expect } from "vitest"; +import { + generatePointCrawlMap, + hasPath, +} from "@/samples/slay-the-spire-like/system/map/generator"; +import { + MapNodeType, + MapLayerType, +} from "@/samples/slay-the-spire-like/system/map/types"; +import { createRNG } from "@/utils/rng"; +import { getEncounters } from "@/samples/slay-the-spire-like/data/desert"; +const encounters = getEncounters(); -describe('generatePointCrawlMap', () => { - it('should generate a map with 10 layers', () => { - const map = generatePointCrawlMap(createRNG(123), encounters); - expect(map.layers.length).toBe(10); - }); +describe("generatePointCrawlMap", () => { + it("should generate a map with 10 layers", () => { + const map = generatePointCrawlMap(createRNG(123), encounters); + expect(map.layers.length).toBe(10); + }); - it('should have correct layer structure', () => { - const map = generatePointCrawlMap(createRNG(123), encounters); - const expectedStructure = [ - 'start', - MapLayerType.Wild, - MapLayerType.Wild, - MapLayerType.Settlement, - MapLayerType.Wild, - MapLayerType.Wild, - MapLayerType.Settlement, - MapLayerType.Wild, - MapLayerType.Wild, - 'end', - ]; + it("should have correct layer structure", () => { + const map = generatePointCrawlMap(createRNG(123), encounters); + 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]); + 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(createRNG(123), encounters); + 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(createRNG(123), encounters); + 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(createRNG(123), encounters); + 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(createRNG(123), encounters); + 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(createRNG(42), encounters); + 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(createRNG(42), encounters); + 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(createRNG(42), encounters); + 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 correct node counts per layer', () => { - const map = generatePointCrawlMap(createRNG(123), encounters); - const expectedCounts = [1, 3, 3, 4, 3, 3, 4, 3, 3, 1]; + it("should have settlement nodes connect correctly (1-2-2-1 pattern)", () => { + const map = generatePointCrawlMap(createRNG(42), encounters); + const settlementToWildTransitions = [ + { src: 3, tgt: 4 }, + { src: 6, tgt: 7 }, + ]; - for (let i = 0; i < expectedCounts.length; i++) { - expect(map.layers[i].nodeIds.length).toBe(expectedCounts[i]); + 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 Start and End nodes with correct types', () => { - const map = generatePointCrawlMap(createRNG(123), encounters); - const startNode = map.nodes.get('node-0-0'); - const endNode = map.nodes.get('node-9-0'); + it("should have all 3 wild nodes connect to End", () => { + const map = generatePointCrawlMap(createRNG(42), encounters); + const lastWildLayer = map.layers[8]; + const endNode = map.nodes.get("node-9-0"); - expect(startNode?.type).toBe(MapNodeType.Start); - expect(endNode?.type).toBe(MapNodeType.End); - }); + for (const wildId of lastWildLayer.nodeIds) { + const wildNode = map.nodes.get(wildId); + expect(wildNode?.childIds).toEqual([endNode!.id]); + } + }); - it('should have wild layers with minion/elite/event types', () => { - const map = generatePointCrawlMap(createRNG(123), encounters); - const wildLayerIndices = [1, 2, 4, 5, 7, 8]; - const validWildTypes = new Set([MapNodeType.Minion, MapNodeType.Elite, MapNodeType.Event]); + it("should have all nodes reachable from Start and can reach End", () => { + const map = generatePointCrawlMap(createRNG(123), encounters); + const startId = "node-0-0"; + const endId = "node-9-0"; - 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); - } + 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(createRNG(12345), encounters); + 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 }); } - }); + } - it('should have settlement layers with at least 1 camp, 1 shop, 1 curio', () => { - const map = generatePointCrawlMap(createRNG(123), encounters); - const settlementLayerIndices = [3, 6]; + // 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]; - for (const layerIdx of settlementLayerIndices) { - const layer = map.layers[layerIdx]; - const nodeTypes = layer.nodeIds.map(id => map.nodes.get(id)!.type); + if (s1 === s2) continue; + if (t1 === t2) continue; - expect(nodeTypes).toContain(MapNodeType.Camp); - expect(nodeTypes).toContain(MapNodeType.Shop); - expect(nodeTypes).toContain(MapNodeType.Curio); - expect(nodeTypes.length).toBe(4); + const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2); + expect(crosses).toBe(false); } - }); + } + } + }); - it('should have Start connected to all 3 wild nodes', () => { - const map = generatePointCrawlMap(createRNG(42), encounters); - const startNode = map.nodes.get('node-0-0'); - const wildLayer = map.layers[1]; + it("should not have crossing edges in wild→settlement transitions", () => { + const map = generatePointCrawlMap(createRNG(12345), encounters); + const wildToSettlementTransitions = [ + { src: 2, tgt: 3 }, + { src: 5, tgt: 6 }, + ]; - expect(startNode?.childIds.length).toBe(3); - expect(startNode?.childIds).toEqual(expect.arrayContaining(wildLayer.nodeIds)); - }); + for (const transition of wildToSettlementTransitions) { + const srcLayer = map.layers[transition.src]; + const tgtLayer = map.layers[transition.tgt]; - it('should have each wild node connect to 1 wild node in wild→wild layers', () => { - const map = generatePointCrawlMap(createRNG(42), encounters); - 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]); - } + // 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 }); } - }); + } - it('should have each wild node connect to 2 settlement nodes in wild→settlement layers', () => { - const map = generatePointCrawlMap(createRNG(42), encounters); - const wildToSettlementTransitions = [ - { src: 2, tgt: 3 }, - { src: 5, tgt: 6 }, - ]; + // 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]; - 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); - } - } + if (s1 === s2) continue; + if (t1 === t2) continue; + + const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2); + expect(crosses).toBe(false); } - }); + } + } + }); - it('should have settlement nodes connect correctly (1-2-2-1 pattern)', () => { - const map = generatePointCrawlMap(createRNG(42), encounters); - const settlementToWildTransitions = [ - { src: 3, tgt: 4 }, - { src: 6, tgt: 7 }, - ]; + it("should not have crossing edges in settlement→wild transitions", () => { + const map = generatePointCrawlMap(createRNG(12345), encounters); + 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]; + 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); - } - } + // 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 }); } - }); + } - it('should have all 3 wild nodes connect to End', () => { - const map = generatePointCrawlMap(createRNG(42), encounters); - const lastWildLayer = map.layers[8]; - const endNode = map.nodes.get('node-9-0'); + // 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]; - for (const wildId of lastWildLayer.nodeIds) { - const wildNode = map.nodes.get(wildId); - expect(wildNode?.childIds).toEqual([endNode!.id]); + if (s1 === s2) continue; + if (t1 === t2) continue; + + const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2); + expect(crosses).toBe(false); } - }); + } + } + }); - it('should have all nodes reachable from Start and can reach End', () => { - const map = generatePointCrawlMap(createRNG(123), encounters); - const startId = 'node-0-0'; - const endId = 'node-9-0'; + it("should assign encounters to all non-Start/End nodes", () => { + const map = generatePointCrawlMap(createRNG(456), encounters); - 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); + 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(createRNG(seed), encounters); + + 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 not have crossing edges in wild→wild transitions', () => { - const map = generatePointCrawlMap(createRNG(12345), encounters); - const wildToWildTransitions = [ - { src: 1, tgt: 2 }, - { src: 4, tgt: 5 }, - { src: 7, tgt: 8 }, - ]; + 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(createRNG(12345), encounters); + const wildPairIndices = [ + [1, 2], + [4, 5], + [7, 8], + ]; - for (const transition of wildToWildTransitions) { - const srcLayer = map.layers[transition.src]; - const tgtLayer = map.layers[transition.tgt]; + for (const [layer1Idx, layer2Idx] of wildPairIndices) { + const layer1 = map.layers[layer1Idx]; + const layer2 = map.layers[layer2Idx]; - // 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 }); - } - } + // 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; - // 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]; + // 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; - if (s1 === s2) continue; - if (t1 === t2) continue; + // 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); + } + }); - const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2); - expect(crosses).toBe(false); - } - } + it("should minimize adjacent repetitions in wild→wild connections", () => { + // Test that wild nodes connected by wild→wild edges have different types + const map = generatePointCrawlMap(createRNG(12345), encounters); + 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++; } - }); + } + } - it('should not have crossing edges in wild→settlement transitions', () => { - const map = generatePointCrawlMap(createRNG(12345), encounters); - 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(createRNG(12345), encounters); - 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(createRNG(456), encounters); - - 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(createRNG(seed), encounters); - - 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(createRNG(12345), encounters); - 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(createRNG(12345), encounters); - 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); - }); + // 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); + }); });