From c11bceeb442f4a39ab9f0dc9bae6a7a62cd792ba Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 17 Apr 2026 12:10:10 +0800 Subject: [PATCH] refactor: update generator --- .../system/map/generator.ts | 54 ++++++++----------- .../slay-the-spire-like/system/map/types.ts | 9 ++-- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/src/samples/slay-the-spire-like/system/map/generator.ts b/src/samples/slay-the-spire-like/system/map/generator.ts index d343dff..0f851de 100644 --- a/src/samples/slay-the-spire-like/system/map/generator.ts +++ b/src/samples/slay-the-spire-like/system/map/generator.ts @@ -1,16 +1,11 @@ -import { Mulberry32RNG, type RNG } from '@/utils/rng'; -import encounterDesertCsvAccessor, { type EncounterDesert } from '../data/encounterDesert.csv'; +import { ReadonlyRNG } from '@/utils/rng'; import { MapNodeType, MapLayerType } from './types'; import type { MapLayer, MapNode, PointCrawlMap, MapGenerationConfig } from './types'; +import {EncounterData} from "@/samples/slay-the-spire-like/system/types"; -const encounterDesertCsv = encounterDesertCsvAccessor(); - -/** Pre-indexed encounters by type */ -const encountersByType = buildEncounterIndex(); - -function buildEncounterIndex(): Map { - const index = new Map(); - for (const encounter of encounterDesertCsv) { +function buildEncounterIndex(src: Iterable): Map { + const index = new Map(); + for (const encounter of src) { const type = encounter.type; if (!index.has(type)) { index.set(type, []); @@ -63,7 +58,7 @@ const LAYER_STRUCTURE: Array<{ layerType: MapLayerType | 'start' | 'end'; count: * Fisher-Yates shuffle algorithm for unbiased random permutation. * Mutates the array in place and returns it. */ -function fisherYatesShuffle(array: T[], rng: RNG): T[] { +function fisherYatesShuffle(array: T[], rng: ReadonlyRNG): T[] { for (let i = array.length - 1; i > 0; i--) { const j = rng.nextInt(i + 1); [array[i], array[j]] = [array[j], array[i]]; @@ -75,11 +70,7 @@ function fisherYatesShuffle(array: T[], rng: RNG): T[] { * Picks a random encounter for the given node type. * Returns undefined if no matching encounter exists. */ -function pickEncounterForNode(type: MapNodeType, rng: RNG): EncounterDesert | undefined { - const encounterType = NODE_TYPE_TO_ENCOUNTER[type]; - if (!encounterType) return undefined; - - const pool = encountersByType.get(encounterType); +function pickEncounterForNode(pool: EncounterData[] | undefined, rng: ReadonlyRNG): EncounterData | undefined { if (!pool || pool.length === 0) return undefined; return pool[rng.nextInt(pool.length)]; @@ -98,12 +89,9 @@ const TOTAL_LAYERS = 10; * - Each settlement layer has at least 1 of each: camp, shop, curio * - Wild nodes connect to 1 wild node or 2 settlement nodes * - * @param seed Random seed for reproducibility */ -export function generatePointCrawlMap(seed?: number): PointCrawlMap { - const rng = new Mulberry32RNG(seed ?? Date.now()); - const actualSeed = rng.getSeed(); - +export function generatePointCrawlMap(rng: ReadonlyRNG, src: Iterable): PointCrawlMap { + const encounters = buildEncounterIndex(src); const layers: MapLayer[] = []; const nodes = new Map(); @@ -136,13 +124,13 @@ export function generatePointCrawlMap(seed?: number): PointCrawlMap { 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, settlementTypes, j); - const encounter = pickEncounterForNode(type, rng); + const encounter = pickEncounterForNode(encounters.get(NODE_TYPE_TO_ENCOUNTER[type]!), rng); const node: MapNode = { id, layerIndex: i, type, childIds: [], - ...(encounter ? { encounter: { name: encounter.name, description: encounter.description } } : {}), + encounter }; nodes.set(id, node); nodeIds.push(id); @@ -171,7 +159,7 @@ export function generatePointCrawlMap(seed?: number): PointCrawlMap { } } - return { layers, nodes, seed: actualSeed, parentIndex }; + return { layers, nodes, parentIndex }; } /** @@ -181,7 +169,7 @@ function resolveNodeType( layerType: MapLayerType | 'start' | 'end', _nodeIndex: number, _layerCount: number, - rng: RNG, + rng: ReadonlyRNG, preGeneratedTypes?: MapNodeType[], nodeIndex?: number, settlementTypes?: MapNodeType[], @@ -213,7 +201,7 @@ function resolveNodeType( * Picks a random type for a wild node based on configured weights. * Default: minion: 50%, elite: 25%, event: 25% */ -function pickWildNodeType(rng: RNG): MapNodeType { +function pickWildNodeType(rng: ReadonlyRNG): MapNodeType { const weights = DEFAULT_CONFIG.wildNodeTypeWeights; const roll = rng.nextInt(100); @@ -226,7 +214,7 @@ function pickWildNodeType(rng: RNG): MapNodeType { * Generates random types for a pair of wild layers (3 nodes each). * Returns two arrays of 3 node types each. */ -function generateWildPair(rng: RNG): [MapNodeType[], MapNodeType[]] { +function generateWildPair(rng: ReadonlyRNG): [MapNodeType[], MapNodeType[]] { const layer1Types: MapNodeType[] = []; const layer2Types: MapNodeType[] = []; @@ -284,7 +272,7 @@ function countRepetitions( * Generates optimal wild layer pair by trying multiple attempts and selecting the one with fewest repetitions. */ function generateOptimalWildPair( - rng: RNG, + rng: ReadonlyRNG, attempts = 3 ): [MapNodeType[], MapNodeType[]] { let bestLayer1: MapNodeType[] = []; @@ -313,7 +301,7 @@ function generateOptimalWildPair( * The 4th node is randomly chosen from the three. * Returns shuffled array of 4 node types. */ -function generateSettlementTypes(rng: RNG): MapNodeType[] { +function generateSettlementTypes(rng: ReadonlyRNG): MapNodeType[] { const requiredTypes = [MapNodeType.Camp, MapNodeType.Shop, MapNodeType.Curio]; const randomType = requiredTypes[rng.nextInt(3)]; const types = [...requiredTypes, randomType]; @@ -325,7 +313,7 @@ function generateSettlementTypes(rng: RNG): MapNodeType[] { * 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 { +function assignSettlementTypes(nodeIds: string[], nodes: MapNode[], rng: ReadonlyRNG): void { // Shuffle node order to randomize which position gets which type const shuffledIndices = fisherYatesShuffle([0, 1, 2, 3], rng); @@ -347,7 +335,7 @@ function generateLayerEdges( sourceLayer: MapLayer, targetLayer: MapLayer, nodes: Map, - rng: RNG + rng: ReadonlyRNG ): void { // Settlement types are now pre-generated during node creation // No need to assign them here anymore @@ -402,7 +390,7 @@ function connectWildToWild( function connectWildToSettlement( wildLayer: MapLayer, settlementLayer: MapLayer, - _rng: RNG + _rng: ReadonlyRNG ): void { // Non-crossing connection pattern: each wild connects to 2 adjacent settlements. // Pattern: w[0]→{s[0],s[1]}, w[1]→{s[1],s[2]}, w[2]→{s[2],s[3]} @@ -427,7 +415,7 @@ function connectWildToSettlement( function connectSettlementToWild( settlementLayer: MapLayer, wildLayer: MapLayer, - _rng: RNG + _rng: ReadonlyRNG ): void { // Non-crossing pattern: s0→w0, s1→w0,w1, s2→w1,w2, s3→w2 // This pattern guarantees no crossings because when edges are sorted by diff --git a/src/samples/slay-the-spire-like/system/map/types.ts b/src/samples/slay-the-spire-like/system/map/types.ts index 5797bd3..c9f756f 100644 --- a/src/samples/slay-the-spire-like/system/map/types.ts +++ b/src/samples/slay-the-spire-like/system/map/types.ts @@ -1,3 +1,5 @@ +import {EncounterData} from "@/samples/slay-the-spire-like/system/types"; + /** * Types of nodes that can appear on the point crawl map. */ @@ -33,10 +35,7 @@ export interface MapNode { /** IDs of nodes in the next layer this node connects to */ childIds: string[]; /** Encounter data assigned to this node (from encounter CSV) */ - encounter?: { - name: string; - description: string; - }; + encounter?: EncounterData; } /** @@ -61,8 +60,6 @@ export interface PointCrawlMap { layers: MapLayer[]; /** All nodes keyed by ID */ nodes: Map; - /** RNG seed used for generation (for reproducibility) */ - seed: number; /** Reverse index: nodeId → parent node IDs (for fast getParent lookup) */ parentIndex?: Map; }