refactor: update generator
This commit is contained in:
parent
1d749f59a6
commit
c11bceeb44
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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[]>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue