refactor: update generator

This commit is contained in:
hypercross 2026-04-17 12:10:10 +08:00
parent 1d749f59a6
commit c11bceeb44
2 changed files with 24 additions and 39 deletions

View File

@ -1,16 +1,11 @@
import { Mulberry32RNG, type RNG } from '@/utils/rng'; import { ReadonlyRNG } from '@/utils/rng';
import encounterDesertCsvAccessor, { type EncounterDesert } from '../data/encounterDesert.csv';
import { MapNodeType, MapLayerType } from './types'; import { MapNodeType, MapLayerType } from './types';
import type { MapLayer, MapNode, PointCrawlMap, MapGenerationConfig } from './types'; import type { MapLayer, MapNode, PointCrawlMap, MapGenerationConfig } from './types';
import {EncounterData} from "@/samples/slay-the-spire-like/system/types";
const encounterDesertCsv = encounterDesertCsvAccessor(); function buildEncounterIndex(src: Iterable<EncounterData>): Map<string, EncounterData[]> {
const index = new Map<string, EncounterData[]>();
/** Pre-indexed encounters by type */ for (const encounter of src) {
const encountersByType = buildEncounterIndex();
function buildEncounterIndex(): Map<string, EncounterDesert[]> {
const index = new Map<string, EncounterDesert[]>();
for (const encounter of encounterDesertCsv) {
const type = encounter.type; const type = encounter.type;
if (!index.has(type)) { if (!index.has(type)) {
index.set(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. * Fisher-Yates shuffle algorithm for unbiased random permutation.
* Mutates the array in place and returns it. * Mutates the array in place and returns it.
*/ */
function fisherYatesShuffle<T>(array: T[], rng: RNG): T[] { function fisherYatesShuffle<T>(array: T[], rng: ReadonlyRNG): T[] {
for (let i = array.length - 1; i > 0; i--) { for (let i = array.length - 1; i > 0; i--) {
const j = rng.nextInt(i + 1); const j = rng.nextInt(i + 1);
[array[i], array[j]] = [array[j], array[i]]; [array[i], array[j]] = [array[j], array[i]];
@ -75,11 +70,7 @@ function fisherYatesShuffle<T>(array: T[], rng: RNG): T[] {
* Picks a random encounter for the given node type. * Picks a random encounter for the given node type.
* Returns undefined if no matching encounter exists. * Returns undefined if no matching encounter exists.
*/ */
function pickEncounterForNode(type: MapNodeType, rng: RNG): EncounterDesert | undefined { function pickEncounterForNode(pool: EncounterData[] | undefined, rng: ReadonlyRNG): EncounterData | undefined {
const encounterType = NODE_TYPE_TO_ENCOUNTER[type];
if (!encounterType) return undefined;
const pool = encountersByType.get(encounterType);
if (!pool || pool.length === 0) return undefined; if (!pool || pool.length === 0) return undefined;
return pool[rng.nextInt(pool.length)]; 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 * - Each settlement layer has at least 1 of each: camp, shop, curio
* - Wild nodes connect to 1 wild node or 2 settlement nodes * - Wild nodes connect to 1 wild node or 2 settlement nodes
* *
* @param seed Random seed for reproducibility
*/ */
export function generatePointCrawlMap(seed?: number): PointCrawlMap { export function generatePointCrawlMap(rng: ReadonlyRNG, src: Iterable<EncounterData>): PointCrawlMap {
const rng = new Mulberry32RNG(seed ?? Date.now()); const encounters = buildEncounterIndex(src);
const actualSeed = rng.getSeed();
const layers: MapLayer[] = []; const layers: MapLayer[] = [];
const nodes = new Map<string, MapNode>(); const nodes = new Map<string, MapNode>();
@ -136,13 +124,13 @@ export function generatePointCrawlMap(seed?: number): PointCrawlMap {
for (let j = 0; j < structure.count; j++) { for (let j = 0; j < structure.count; j++) {
const id = `node-${i}-${j}`; const id = `node-${i}-${j}`;
const type = resolveNodeType(structure.layerType, j, structure.count, rng, wildPairTypes.get(i), j, settlementTypes, 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 = { const node: MapNode = {
id, id,
layerIndex: i, layerIndex: i,
type, type,
childIds: [], childIds: [],
...(encounter ? { encounter: { name: encounter.name, description: encounter.description } } : {}), encounter
}; };
nodes.set(id, node); nodes.set(id, node);
nodeIds.push(id); 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', layerType: MapLayerType | 'start' | 'end',
_nodeIndex: number, _nodeIndex: number,
_layerCount: number, _layerCount: number,
rng: RNG, rng: ReadonlyRNG,
preGeneratedTypes?: MapNodeType[], preGeneratedTypes?: MapNodeType[],
nodeIndex?: number, nodeIndex?: number,
settlementTypes?: MapNodeType[], settlementTypes?: MapNodeType[],
@ -213,7 +201,7 @@ function resolveNodeType(
* Picks a random type for a wild node based on configured weights. * Picks a random type for a wild node based on configured weights.
* Default: minion: 50%, elite: 25%, event: 25% * Default: minion: 50%, elite: 25%, event: 25%
*/ */
function pickWildNodeType(rng: RNG): MapNodeType { function pickWildNodeType(rng: ReadonlyRNG): MapNodeType {
const weights = DEFAULT_CONFIG.wildNodeTypeWeights; const weights = DEFAULT_CONFIG.wildNodeTypeWeights;
const roll = rng.nextInt(100); 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). * Generates random types for a pair of wild layers (3 nodes each).
* Returns two arrays of 3 node types 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 layer1Types: MapNodeType[] = [];
const layer2Types: 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. * Generates optimal wild layer pair by trying multiple attempts and selecting the one with fewest repetitions.
*/ */
function generateOptimalWildPair( function generateOptimalWildPair(
rng: RNG, rng: ReadonlyRNG,
attempts = 3 attempts = 3
): [MapNodeType[], MapNodeType[]] { ): [MapNodeType[], MapNodeType[]] {
let bestLayer1: MapNodeType[] = []; let bestLayer1: MapNodeType[] = [];
@ -313,7 +301,7 @@ function generateOptimalWildPair(
* The 4th node is randomly chosen from the three. * The 4th node is randomly chosen from the three.
* Returns shuffled array of 4 node types. * 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 requiredTypes = [MapNodeType.Camp, MapNodeType.Shop, MapNodeType.Curio];
const randomType = requiredTypes[rng.nextInt(3)]; const randomType = requiredTypes[rng.nextInt(3)];
const types = [...requiredTypes, randomType]; const types = [...requiredTypes, randomType];
@ -325,7 +313,7 @@ function generateSettlementTypes(rng: RNG): MapNodeType[] {
* The 4th node is randomly chosen from the three. * The 4th node is randomly chosen from the three.
* @deprecated Use generateSettlementTypes() during node creation instead. * @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 // Shuffle node order to randomize which position gets which type
const shuffledIndices = fisherYatesShuffle([0, 1, 2, 3], rng); const shuffledIndices = fisherYatesShuffle([0, 1, 2, 3], rng);
@ -347,7 +335,7 @@ function generateLayerEdges(
sourceLayer: MapLayer, sourceLayer: MapLayer,
targetLayer: MapLayer, targetLayer: MapLayer,
nodes: Map<string, MapNode>, nodes: Map<string, MapNode>,
rng: RNG rng: ReadonlyRNG
): void { ): void {
// Settlement types are now pre-generated during node creation // Settlement types are now pre-generated during node creation
// No need to assign them here anymore // No need to assign them here anymore
@ -402,7 +390,7 @@ function connectWildToWild(
function connectWildToSettlement( function connectWildToSettlement(
wildLayer: MapLayer, wildLayer: MapLayer,
settlementLayer: MapLayer, settlementLayer: MapLayer,
_rng: RNG _rng: ReadonlyRNG
): void { ): void {
// Non-crossing connection pattern: each wild connects to 2 adjacent settlements. // 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]} // 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( function connectSettlementToWild(
settlementLayer: MapLayer, settlementLayer: MapLayer,
wildLayer: MapLayer, wildLayer: MapLayer,
_rng: RNG _rng: ReadonlyRNG
): void { ): void {
// Non-crossing pattern: s0→w0, s1→w0,w1, s2→w1,w2, s3→w2 // Non-crossing pattern: s0→w0, s1→w0,w1, s2→w1,w2, s3→w2
// This pattern guarantees no crossings because when edges are sorted by // This pattern guarantees no crossings because when edges are sorted by

View File

@ -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. * 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 */ /** IDs of nodes in the next layer this node connects to */
childIds: string[]; childIds: string[];
/** Encounter data assigned to this node (from encounter CSV) */ /** Encounter data assigned to this node (from encounter CSV) */
encounter?: { encounter?: EncounterData;
name: string;
description: string;
};
} }
/** /**
@ -61,8 +60,6 @@ export interface PointCrawlMap {
layers: MapLayer[]; layers: MapLayer[];
/** All nodes keyed by ID */ /** All nodes keyed by ID */
nodes: Map<string, MapNode>; nodes: Map<string, MapNode>;
/** RNG seed used for generation (for reproducibility) */
seed: number;
/** Reverse index: nodeId → parent node IDs (for fast getParent lookup) */ /** Reverse index: nodeId → parent node IDs (for fast getParent lookup) */
parentIndex?: Map<string, string[]>; parentIndex?: Map<string, string[]>;
} }