refactor: minimize repetitions
This commit is contained in:
parent
1e5e4e9f7e
commit
ef9557cba7
|
|
@ -105,6 +105,20 @@ export function generatePointCrawlMap(seed?: number): PointCrawlMap {
|
||||||
const layers: MapLayer[] = [];
|
const layers: MapLayer[] = [];
|
||||||
const nodes = new Map<string, MapNode>();
|
const nodes = new Map<string, MapNode>();
|
||||||
|
|
||||||
|
// Pre-generate optimal types for wild layer pairs (layers 1-2, 4-5, 7-8)
|
||||||
|
const wildPairTypes = new Map<number, MapNodeType[]>();
|
||||||
|
const wildPairs = [
|
||||||
|
{ layer1: 1, layer2: 2 },
|
||||||
|
{ layer1: 4, layer2: 5 },
|
||||||
|
{ layer1: 7, layer2: 8 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pair of wildPairs) {
|
||||||
|
const [types1, types2] = generateOptimalWildPair(rng);
|
||||||
|
wildPairTypes.set(pair.layer1, types1);
|
||||||
|
wildPairTypes.set(pair.layer2, types2);
|
||||||
|
}
|
||||||
|
|
||||||
// Step 1: create layers and nodes
|
// Step 1: create layers and nodes
|
||||||
for (let i = 0; i < TOTAL_LAYERS; i++) {
|
for (let i = 0; i < TOTAL_LAYERS; i++) {
|
||||||
const structure = LAYER_STRUCTURE[i];
|
const structure = LAYER_STRUCTURE[i];
|
||||||
|
|
@ -113,7 +127,7 @@ 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);
|
const type = resolveNodeType(structure.layerType, j, structure.count, rng, wildPairTypes.get(i), j);
|
||||||
const encounter = pickEncounterForNode(type, rng);
|
const encounter = pickEncounterForNode(type, rng);
|
||||||
const node: MapNode = {
|
const node: MapNode = {
|
||||||
id,
|
id,
|
||||||
|
|
@ -159,7 +173,9 @@ function resolveNodeType(
|
||||||
layerType: MapLayerType | 'start' | 'end',
|
layerType: MapLayerType | 'start' | 'end',
|
||||||
_nodeIndex: number,
|
_nodeIndex: number,
|
||||||
_layerCount: number,
|
_layerCount: number,
|
||||||
rng: RNG
|
rng: RNG,
|
||||||
|
preGeneratedTypes?: MapNodeType[],
|
||||||
|
nodeIndex?: number
|
||||||
): MapNodeType {
|
): MapNodeType {
|
||||||
switch (layerType) {
|
switch (layerType) {
|
||||||
case 'start':
|
case 'start':
|
||||||
|
|
@ -167,6 +183,10 @@ function resolveNodeType(
|
||||||
case 'end':
|
case 'end':
|
||||||
return MapNodeType.End;
|
return MapNodeType.End;
|
||||||
case MapLayerType.Wild:
|
case MapLayerType.Wild:
|
||||||
|
// Use pre-generated types if available (from optimal pair generation)
|
||||||
|
if (preGeneratedTypes && nodeIndex !== undefined) {
|
||||||
|
return preGeneratedTypes[nodeIndex];
|
||||||
|
}
|
||||||
return pickWildNodeType(rng);
|
return pickWildNodeType(rng);
|
||||||
case MapLayerType.Settlement:
|
case MapLayerType.Settlement:
|
||||||
// This will be overridden by assignSettlementTypes
|
// This will be overridden by assignSettlementTypes
|
||||||
|
|
@ -189,6 +209,92 @@ function pickWildNodeType(rng: RNG): MapNodeType {
|
||||||
return MapNodeType.Event;
|
return MapNodeType.Event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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[]] {
|
||||||
|
const layer1Types: MapNodeType[] = [];
|
||||||
|
const layer2Types: MapNodeType[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
layer1Types.push(pickWildNodeType(rng));
|
||||||
|
layer2Types.push(pickWildNodeType(rng));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [layer1Types, layer2Types];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts repetitions in a wild layer pair.
|
||||||
|
* - sameLayer: number of duplicate types within each layer
|
||||||
|
* - adjacent: number of positions where layer1[i] === layer2[i]
|
||||||
|
* - total: sum of both
|
||||||
|
*/
|
||||||
|
function countRepetitions(
|
||||||
|
layer1Types: MapNodeType[],
|
||||||
|
layer2Types: MapNodeType[]
|
||||||
|
): { sameLayer: number; adjacent: number; total: number } {
|
||||||
|
// Count same-layer repetitions
|
||||||
|
let sameLayer = 0;
|
||||||
|
|
||||||
|
// For layer 1: count duplicates
|
||||||
|
const layer1Count = new Map<MapNodeType, number>();
|
||||||
|
for (const type of layer1Types) {
|
||||||
|
layer1Count.set(type, (layer1Count.get(type) || 0) + 1);
|
||||||
|
}
|
||||||
|
for (const count of layer1Count.values()) {
|
||||||
|
if (count > 1) sameLayer += count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For layer 2: count duplicates
|
||||||
|
const layer2Count = new Map<MapNodeType, number>();
|
||||||
|
for (const type of layer2Types) {
|
||||||
|
layer2Count.set(type, (layer2Count.get(type) || 0) + 1);
|
||||||
|
}
|
||||||
|
for (const count of layer2Count.values()) {
|
||||||
|
if (count > 1) sameLayer += count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count adjacent repetitions (wild[i] → wild[i] connection)
|
||||||
|
let adjacent = 0;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (layer1Types[i] === layer2Types[i]) {
|
||||||
|
adjacent++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sameLayer, adjacent, total: sameLayer + adjacent };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates optimal wild layer pair by trying multiple attempts and selecting the one with fewest repetitions.
|
||||||
|
*/
|
||||||
|
function generateOptimalWildPair(
|
||||||
|
rng: RNG,
|
||||||
|
attempts = 3
|
||||||
|
): [MapNodeType[], MapNodeType[]] {
|
||||||
|
let bestLayer1: MapNodeType[] = [];
|
||||||
|
let bestLayer2: MapNodeType[] = [];
|
||||||
|
let bestScore = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < attempts; i++) {
|
||||||
|
const [layer1, layer2] = generateWildPair(rng);
|
||||||
|
const score = countRepetitions(layer1, layer2);
|
||||||
|
|
||||||
|
if (score.total < bestScore) {
|
||||||
|
bestScore = score.total;
|
||||||
|
bestLayer1 = layer1;
|
||||||
|
bestLayer2 = layer2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perfect score is 0, no need to continue
|
||||||
|
if (bestScore === 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [bestLayer1, bestLayer2];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigns settlement node types ensuring at least 1 of each: camp, shop, curio.
|
* Assigns settlement node types ensuring at least 1 of each: camp, shop, curio.
|
||||||
* The 4th node is randomly chosen from the three.
|
* The 4th node is randomly chosen from the three.
|
||||||
|
|
|
||||||
|
|
@ -301,4 +301,67 @@ describe('generatePointCrawlMap', () => {
|
||||||
|
|
||||||
expect(nodesWithEncounter).toBeGreaterThan(0);
|
expect(nodesWithEncounter).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should minimize same-layer repetitions in wild layer pairs', () => {
|
||||||
|
// Test that wild layers in pairs (1-2, 4-5, 7-8) have minimal duplicate types within each layer
|
||||||
|
const map = generatePointCrawlMap(12345);
|
||||||
|
const wildPairIndices = [
|
||||||
|
[1, 2],
|
||||||
|
[4, 5],
|
||||||
|
[7, 8],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [layer1Idx, layer2Idx] of wildPairIndices) {
|
||||||
|
const layer1 = map.layers[layer1Idx];
|
||||||
|
const layer2 = map.layers[layer2Idx];
|
||||||
|
|
||||||
|
// Count repetitions in layer 1
|
||||||
|
const layer1Types = layer1.nodeIds.map(id => map.nodes.get(id)!.type);
|
||||||
|
const layer1Unique = new Set(layer1Types).size;
|
||||||
|
const layer1Repetitions = layer1Types.length - layer1Unique;
|
||||||
|
|
||||||
|
// Count repetitions in layer 2
|
||||||
|
const layer2Types = layer2.nodeIds.map(id => map.nodes.get(id)!.type);
|
||||||
|
const layer2Unique = new Set(layer2Types).size;
|
||||||
|
const layer2Repetitions = layer2Types.length - layer2Unique;
|
||||||
|
|
||||||
|
// With optimal selection, we expect fewer repetitions than pure random
|
||||||
|
// On average, random would have ~1.5 repetitions per 3-node layer
|
||||||
|
// With 3 attempts, we should typically get 0-1 repetitions
|
||||||
|
expect(layer1Repetitions + layer2Repetitions).toBeLessThanOrEqual(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should minimize adjacent repetitions in wild→wild connections', () => {
|
||||||
|
// Test that wild nodes connected by wild→wild edges have different types
|
||||||
|
const map = generatePointCrawlMap(12345);
|
||||||
|
const wildToWildPairs = [
|
||||||
|
{ src: 1, tgt: 2 },
|
||||||
|
{ src: 4, tgt: 5 },
|
||||||
|
{ src: 7, tgt: 8 },
|
||||||
|
];
|
||||||
|
|
||||||
|
let totalAdjacentRepetitions = 0;
|
||||||
|
|
||||||
|
for (const pair of wildToWildPairs) {
|
||||||
|
const srcLayer = map.layers[pair.src];
|
||||||
|
const tgtLayer = map.layers[pair.tgt];
|
||||||
|
|
||||||
|
// Each wild node connects to exactly 1 wild node in next layer (1-to-1)
|
||||||
|
for (let i = 0; i < srcLayer.nodeIds.length; i++) {
|
||||||
|
const srcNode = map.nodes.get(srcLayer.nodeIds[i])!;
|
||||||
|
const tgtId = srcNode.childIds[0];
|
||||||
|
const tgtNode = map.nodes.get(tgtId)!;
|
||||||
|
|
||||||
|
if (srcNode.type === tgtNode.type) {
|
||||||
|
totalAdjacentRepetitions++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With 3 wild pairs and 3 nodes each, that's 9 connections total
|
||||||
|
// Random would have ~3 repetitions (1/3 chance per connection)
|
||||||
|
// With optimal selection of 3 attempts, should be much lower (0-2)
|
||||||
|
expect(totalAdjacentRepetitions).toBeLessThanOrEqual(3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue