diff --git a/src/samples/slay-the-spire-like/map/generator.ts b/src/samples/slay-the-spire-like/map/generator.ts index ed288dd..bc23b96 100644 --- a/src/samples/slay-the-spire-like/map/generator.ts +++ b/src/samples/slay-the-spire-like/map/generator.ts @@ -20,12 +20,12 @@ function buildEncounterIndex(): Map { /** Map from MapNodeType to encounter type key */ const NODE_TYPE_TO_ENCOUNTER: Partial> = { - [MapNodeType.Minion]: 'enemy', + [MapNodeType.Minion]: 'minion', [MapNodeType.Elite]: 'elite', [MapNodeType.Event]: 'event', - [MapNodeType.Camp]: 'shelter', - [MapNodeType.Shop]: 'npc', - [MapNodeType.Curio]: 'shelter', + [MapNodeType.Camp]: 'camp', + [MapNodeType.Shop]: 'shop', + [MapNodeType.Curio]: 'curio', }; /** Default map generation configuration */ @@ -125,9 +125,15 @@ export function generatePointCrawlMap(seed?: number): PointCrawlMap { const nodeIds: string[] = []; const layerNodes: MapNode[] = []; + // Pre-generate settlement types if this is a settlement layer + let settlementTypes: MapNodeType[] | undefined; + if (structure.layerType === MapLayerType.Settlement) { + settlementTypes = generateSettlementTypes(rng); + } + for (let j = 0; j < structure.count; j++) { const id = `node-${i}-${j}`; - const type = resolveNodeType(structure.layerType, j, structure.count, rng, wildPairTypes.get(i), j); + const type = resolveNodeType(structure.layerType, j, structure.count, rng, wildPairTypes.get(i), j, settlementTypes, j); const encounter = pickEncounterForNode(type, rng); const node: MapNode = { id, @@ -175,7 +181,9 @@ function resolveNodeType( _layerCount: number, rng: RNG, preGeneratedTypes?: MapNodeType[], - nodeIndex?: number + nodeIndex?: number, + settlementTypes?: MapNodeType[], + settlementIndex?: number ): MapNodeType { switch (layerType) { case 'start': @@ -189,8 +197,11 @@ function resolveNodeType( } return pickWildNodeType(rng); case MapLayerType.Settlement: - // This will be overridden by assignSettlementTypes - return MapNodeType.Camp; // placeholder + // Use pre-generated settlement types if available + if (settlementTypes && settlementIndex !== undefined) { + return settlementTypes[settlementIndex]; + } + return MapNodeType.Camp; // fallback default: return MapNodeType.Minion; } @@ -295,9 +306,22 @@ function generateOptimalWildPair( return [bestLayer1, bestLayer2]; } +/** + * Generates settlement node types ensuring at least 1 of each: camp, shop, curio. + * The 4th node is randomly chosen from the three. + * Returns shuffled array of 4 node types. + */ +function generateSettlementTypes(rng: RNG): MapNodeType[] { + const requiredTypes = [MapNodeType.Camp, MapNodeType.Shop, MapNodeType.Curio]; + const randomType = requiredTypes[rng.nextInt(3)]; + const types = [...requiredTypes, randomType]; + return fisherYatesShuffle(types, rng); +} + /** * Assigns settlement node types ensuring at least 1 of each: camp, shop, curio. * The 4th node is randomly chosen from the three. + * @deprecated Use generateSettlementTypes() during node creation instead. */ function assignSettlementTypes(nodeIds: string[], nodes: MapNode[], rng: RNG): void { // Shuffle node order to randomize which position gets which type @@ -323,10 +347,8 @@ function generateLayerEdges( nodes: Map, rng: RNG ): void { - // Assign settlement types when creating settlement layer - if (targetLayer.layerType === MapLayerType.Settlement) { - assignSettlementTypes(targetLayer.nodeIds, targetLayer.nodes, rng); - } + // Settlement types are now pre-generated during node creation + // No need to assign them here anymore const sourceType = sourceLayer.layerType; const targetType = targetLayer.layerType; 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 aa1e13f..2d3d8e7 100644 --- a/tests/samples/slay-the-spire-like/map/generator.test.ts +++ b/tests/samples/slay-the-spire-like/map/generator.test.ts @@ -287,19 +287,36 @@ describe('generatePointCrawlMap', () => { } }); - it('should assign encounters to nodes', () => { + it('should assign encounters to all non-Start/End nodes', () => { const map = generatePointCrawlMap(456); - let nodesWithEncounter = 0; for (const node of map.nodes.values()) { - if (node.encounter) { - nodesWithEncounter++; - expect(node.encounter.name).toBeTruthy(); - expect(node.encounter.description).toBeTruthy(); + 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(); } } + }); - expect(nodesWithEncounter).toBeGreaterThan(0); + 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', () => {