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 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<string, EncounterDesert[]> {
const index = new Map<string, EncounterDesert[]>();
for (const encounter of encounterDesertCsv) {
function buildEncounterIndex(src: Iterable<EncounterData>): Map<string, EncounterData[]> {
const index = new Map<string, EncounterData[]>();
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<T>(array: T[], rng: RNG): T[] {
function fisherYatesShuffle<T>(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<T>(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<EncounterData>): PointCrawlMap {
const encounters = buildEncounterIndex(src);
const layers: MapLayer[] = [];
const nodes = new Map<string, MapNode>();
@ -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<string, MapNode>,
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

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.
*/
@ -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<string, MapNode>;
/** RNG seed used for generation (for reproducibility) */
seed: number;
/** Reverse index: nodeId → parent node IDs (for fast getParent lookup) */
parentIndex?: Map<string, string[]>;
}