2026-04-13 11:55:57 +08:00
|
|
|
import { describe, it, expect } from 'vitest';
|
2026-04-13 14:56:33 +08:00
|
|
|
import { generatePointCrawlMap, hasPath } from '@/samples/slay-the-spire-like/map/generator';
|
|
|
|
|
import { MapNodeType, MapLayerType } from '@/samples/slay-the-spire-like/map/types';
|
2026-04-13 11:55:57 +08:00
|
|
|
|
|
|
|
|
describe('generatePointCrawlMap', () => {
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should generate a map with 10 layers', () => {
|
2026-04-13 11:55:57 +08:00
|
|
|
const map = generatePointCrawlMap(123);
|
2026-04-13 14:56:33 +08:00
|
|
|
expect(map.layers.length).toBe(10);
|
2026-04-13 11:55:57 +08:00
|
|
|
});
|
|
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should have correct layer structure', () => {
|
|
|
|
|
const map = generatePointCrawlMap(123);
|
|
|
|
|
const expectedStructure = [
|
|
|
|
|
'start',
|
|
|
|
|
MapLayerType.Wild,
|
|
|
|
|
MapLayerType.Wild,
|
|
|
|
|
MapLayerType.Settlement,
|
|
|
|
|
MapLayerType.Wild,
|
|
|
|
|
MapLayerType.Wild,
|
|
|
|
|
MapLayerType.Settlement,
|
|
|
|
|
MapLayerType.Wild,
|
|
|
|
|
MapLayerType.Wild,
|
|
|
|
|
'end',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < expectedStructure.length; i++) {
|
|
|
|
|
expect(map.layers[i].layerType).toBe(expectedStructure[i]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should have correct node counts per layer', () => {
|
|
|
|
|
const map = generatePointCrawlMap(123);
|
|
|
|
|
const expectedCounts = [1, 3, 3, 4, 3, 3, 4, 3, 3, 1];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < expectedCounts.length; i++) {
|
|
|
|
|
expect(map.layers[i].nodeIds.length).toBe(expectedCounts[i]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should have Start and End nodes with correct types', () => {
|
2026-04-13 11:55:57 +08:00
|
|
|
const map = generatePointCrawlMap(123);
|
|
|
|
|
const startNode = map.nodes.get('node-0-0');
|
2026-04-13 14:56:33 +08:00
|
|
|
const endNode = map.nodes.get('node-9-0');
|
2026-04-13 11:55:57 +08:00
|
|
|
|
|
|
|
|
expect(startNode?.type).toBe(MapNodeType.Start);
|
2026-04-13 14:56:33 +08:00
|
|
|
expect(endNode?.type).toBe(MapNodeType.End);
|
2026-04-13 11:55:57 +08:00
|
|
|
});
|
|
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should have wild layers with minion/elite/event types', () => {
|
|
|
|
|
const map = generatePointCrawlMap(123);
|
|
|
|
|
const wildLayerIndices = [1, 2, 4, 5, 7, 8];
|
|
|
|
|
const validWildTypes = new Set([MapNodeType.Minion, MapNodeType.Elite, MapNodeType.Event]);
|
|
|
|
|
|
|
|
|
|
for (const layerIdx of wildLayerIndices) {
|
|
|
|
|
const layer = map.layers[layerIdx];
|
|
|
|
|
for (const nodeId of layer.nodeIds) {
|
|
|
|
|
const node = map.nodes.get(nodeId);
|
|
|
|
|
expect(node).toBeDefined();
|
|
|
|
|
expect(validWildTypes.has(node!.type)).toBe(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-13 11:55:57 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should have settlement layers with at least 1 camp, 1 shop, 1 curio', () => {
|
|
|
|
|
const map = generatePointCrawlMap(123);
|
|
|
|
|
const settlementLayerIndices = [3, 6];
|
2026-04-13 11:55:57 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
for (const layerIdx of settlementLayerIndices) {
|
|
|
|
|
const layer = map.layers[layerIdx];
|
|
|
|
|
const nodeTypes = layer.nodeIds.map(id => map.nodes.get(id)!.type);
|
|
|
|
|
|
|
|
|
|
expect(nodeTypes).toContain(MapNodeType.Camp);
|
|
|
|
|
expect(nodeTypes).toContain(MapNodeType.Shop);
|
|
|
|
|
expect(nodeTypes).toContain(MapNodeType.Curio);
|
|
|
|
|
expect(nodeTypes.length).toBe(4);
|
2026-04-13 11:55:57 +08:00
|
|
|
}
|
2026-04-13 14:56:33 +08:00
|
|
|
});
|
2026-04-13 11:55:57 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should have Start connected to all 3 wild nodes', () => {
|
|
|
|
|
const map = generatePointCrawlMap(42);
|
|
|
|
|
const startNode = map.nodes.get('node-0-0');
|
|
|
|
|
const wildLayer = map.layers[1];
|
2026-04-13 11:55:57 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
expect(startNode?.childIds.length).toBe(3);
|
|
|
|
|
expect(startNode?.childIds).toEqual(expect.arrayContaining(wildLayer.nodeIds));
|
2026-04-13 11:55:57 +08:00
|
|
|
});
|
|
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should have each wild node connect to 1 wild node in wild→wild layers', () => {
|
|
|
|
|
const map = generatePointCrawlMap(42);
|
|
|
|
|
const wildToWildTransitions = [
|
|
|
|
|
{ src: 1, tgt: 2 },
|
|
|
|
|
{ src: 4, tgt: 5 },
|
|
|
|
|
{ src: 7, tgt: 8 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const transition of wildToWildTransitions) {
|
|
|
|
|
const srcLayer = map.layers[transition.src];
|
|
|
|
|
for (const srcId of srcLayer.nodeIds) {
|
|
|
|
|
const srcNode = map.nodes.get(srcId);
|
|
|
|
|
expect(srcNode?.childIds.length).toBe(1);
|
|
|
|
|
const childLayer = map.layers[transition.tgt];
|
|
|
|
|
expect(childLayer.nodeIds).toContain(srcNode!.childIds[0]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-13 11:55:57 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should have each wild node connect to 2 settlement nodes in wild→settlement layers', () => {
|
|
|
|
|
const map = generatePointCrawlMap(42);
|
|
|
|
|
const wildToSettlementTransitions = [
|
|
|
|
|
{ src: 2, tgt: 3 },
|
|
|
|
|
{ src: 5, tgt: 6 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const transition of wildToSettlementTransitions) {
|
|
|
|
|
const srcLayer = map.layers[transition.src];
|
|
|
|
|
for (const srcId of srcLayer.nodeIds) {
|
|
|
|
|
const srcNode = map.nodes.get(srcId);
|
|
|
|
|
expect(srcNode?.childIds.length).toBe(2);
|
|
|
|
|
const childLayer = map.layers[transition.tgt];
|
|
|
|
|
for (const childId of srcNode!.childIds) {
|
|
|
|
|
expect(childLayer.nodeIds).toContain(childId);
|
2026-04-13 11:55:57 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-13 12:46:11 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should have settlement nodes connect correctly (1-2-2-1 pattern)', () => {
|
2026-04-13 12:46:11 +08:00
|
|
|
const map = generatePointCrawlMap(42);
|
2026-04-13 14:56:33 +08:00
|
|
|
const settlementToWildTransitions = [
|
|
|
|
|
{ src: 3, tgt: 4 },
|
|
|
|
|
{ src: 6, tgt: 7 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const transition of settlementToWildTransitions) {
|
|
|
|
|
const srcLayer = map.layers[transition.src];
|
|
|
|
|
const tgtLayer = map.layers[transition.tgt];
|
|
|
|
|
|
|
|
|
|
// First and last settlement connect to 1 wild
|
|
|
|
|
const firstSettlement = map.nodes.get(srcLayer.nodeIds[0]);
|
|
|
|
|
expect(firstSettlement?.childIds.length).toBe(1);
|
|
|
|
|
const lastSettlement = map.nodes.get(srcLayer.nodeIds[3]);
|
|
|
|
|
expect(lastSettlement?.childIds.length).toBe(1);
|
|
|
|
|
|
|
|
|
|
// Middle two settlements connect to 2 wilds
|
|
|
|
|
for (let i = 1; i <= 2; i++) {
|
|
|
|
|
const midSettlement = map.nodes.get(srcLayer.nodeIds[i]);
|
|
|
|
|
expect(midSettlement?.childIds.length).toBe(2);
|
|
|
|
|
for (const childId of midSettlement!.childIds) {
|
|
|
|
|
expect(tgtLayer.nodeIds).toContain(childId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-13 12:46:11 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should have all 3 wild nodes connect to End', () => {
|
|
|
|
|
const map = generatePointCrawlMap(42);
|
|
|
|
|
const lastWildLayer = map.layers[8];
|
|
|
|
|
const endNode = map.nodes.get('node-9-0');
|
2026-04-13 12:46:11 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
for (const wildId of lastWildLayer.nodeIds) {
|
|
|
|
|
const wildNode = map.nodes.get(wildId);
|
|
|
|
|
expect(wildNode?.childIds).toEqual([endNode!.id]);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-13 12:46:11 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should have all nodes reachable from Start and can reach End', () => {
|
|
|
|
|
const map = generatePointCrawlMap(123);
|
|
|
|
|
const startId = 'node-0-0';
|
|
|
|
|
const endId = 'node-9-0';
|
2026-04-13 12:46:11 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
for (const nodeId of map.nodes.keys()) {
|
|
|
|
|
if (nodeId === startId || nodeId === endId) continue;
|
|
|
|
|
expect(hasPath(map, startId, nodeId)).toBe(true);
|
|
|
|
|
expect(hasPath(map, nodeId, endId)).toBe(true);
|
2026-04-13 12:46:11 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
it('should not have crossing edges in wild→wild transitions', () => {
|
2026-04-13 12:46:11 +08:00
|
|
|
const map = generatePointCrawlMap(12345);
|
2026-04-13 14:56:33 +08:00
|
|
|
const wildToWildTransitions = [
|
|
|
|
|
{ src: 1, tgt: 2 },
|
|
|
|
|
{ src: 4, tgt: 5 },
|
|
|
|
|
{ src: 7, tgt: 8 },
|
|
|
|
|
];
|
2026-04-13 12:46:11 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
for (const transition of wildToWildTransitions) {
|
|
|
|
|
const srcLayer = map.layers[transition.src];
|
|
|
|
|
const tgtLayer = map.layers[transition.tgt];
|
2026-04-13 12:46:11 +08:00
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
// Collect edges as pairs of indices
|
2026-04-13 12:46:11 +08:00
|
|
|
const edges: Array<{ srcIndex: number; tgtIndex: number }> = [];
|
2026-04-13 14:56:33 +08:00
|
|
|
for (let s = 0; s < srcLayer.nodeIds.length; s++) {
|
|
|
|
|
const srcNode = map.nodes.get(srcLayer.nodeIds[s]);
|
2026-04-13 12:46:11 +08:00
|
|
|
for (const tgtId of srcNode!.childIds) {
|
2026-04-13 14:56:33 +08:00
|
|
|
const t = tgtLayer.nodeIds.indexOf(tgtId);
|
2026-04-13 12:46:11 +08:00
|
|
|
edges.push({ srcIndex: s, tgtIndex: t });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 14:56:33 +08:00
|
|
|
// Check for crossings
|
2026-04-13 17:24:57 +08:00
|
|
|
for (let e1 = 0; e1 < edges.length; e1++) {
|
|
|
|
|
for (let e2 = e1 + 1; e2 < edges.length; e2++) {
|
|
|
|
|
const { srcIndex: s1, tgtIndex: t1 } = edges[e1];
|
|
|
|
|
const { srcIndex: s2, tgtIndex: t2 } = edges[e2];
|
|
|
|
|
|
|
|
|
|
if (s1 === s2) continue;
|
|
|
|
|
if (t1 === t2) continue;
|
|
|
|
|
|
|
|
|
|
const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2);
|
|
|
|
|
expect(crosses).toBe(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not have crossing edges in wild→settlement transitions', () => {
|
|
|
|
|
const map = generatePointCrawlMap(12345);
|
|
|
|
|
const wildToSettlementTransitions = [
|
|
|
|
|
{ src: 2, tgt: 3 },
|
|
|
|
|
{ src: 5, tgt: 6 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const transition of wildToSettlementTransitions) {
|
|
|
|
|
const srcLayer = map.layers[transition.src];
|
|
|
|
|
const tgtLayer = map.layers[transition.tgt];
|
|
|
|
|
|
|
|
|
|
// Collect edges as pairs of indices
|
|
|
|
|
const edges: Array<{ srcIndex: number; tgtIndex: number }> = [];
|
|
|
|
|
for (let s = 0; s < srcLayer.nodeIds.length; s++) {
|
|
|
|
|
const srcNode = map.nodes.get(srcLayer.nodeIds[s]);
|
|
|
|
|
for (const tgtId of srcNode!.childIds) {
|
|
|
|
|
const t = tgtLayer.nodeIds.indexOf(tgtId);
|
|
|
|
|
edges.push({ srcIndex: s, tgtIndex: t });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for crossings
|
|
|
|
|
for (let e1 = 0; e1 < edges.length; e1++) {
|
|
|
|
|
for (let e2 = e1 + 1; e2 < edges.length; e2++) {
|
|
|
|
|
const { srcIndex: s1, tgtIndex: t1 } = edges[e1];
|
|
|
|
|
const { srcIndex: s2, tgtIndex: t2 } = edges[e2];
|
|
|
|
|
|
|
|
|
|
if (s1 === s2) continue;
|
|
|
|
|
if (t1 === t2) continue;
|
|
|
|
|
|
|
|
|
|
const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2);
|
|
|
|
|
expect(crosses).toBe(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not have crossing edges in settlement→wild transitions', () => {
|
|
|
|
|
const map = generatePointCrawlMap(12345);
|
|
|
|
|
const settlementToWildTransitions = [
|
|
|
|
|
{ src: 3, tgt: 4 },
|
|
|
|
|
{ src: 6, tgt: 7 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const transition of settlementToWildTransitions) {
|
|
|
|
|
const srcLayer = map.layers[transition.src];
|
|
|
|
|
const tgtLayer = map.layers[transition.tgt];
|
|
|
|
|
|
|
|
|
|
// Collect edges as pairs of indices
|
|
|
|
|
const edges: Array<{ srcIndex: number; tgtIndex: number }> = [];
|
|
|
|
|
for (let s = 0; s < srcLayer.nodeIds.length; s++) {
|
|
|
|
|
const srcNode = map.nodes.get(srcLayer.nodeIds[s]);
|
|
|
|
|
for (const tgtId of srcNode!.childIds) {
|
|
|
|
|
const t = tgtLayer.nodeIds.indexOf(tgtId);
|
|
|
|
|
edges.push({ srcIndex: s, tgtIndex: t });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for crossings
|
2026-04-13 12:46:11 +08:00
|
|
|
for (let e1 = 0; e1 < edges.length; e1++) {
|
|
|
|
|
for (let e2 = e1 + 1; e2 < edges.length; e2++) {
|
|
|
|
|
const { srcIndex: s1, tgtIndex: t1 } = edges[e1];
|
|
|
|
|
const { srcIndex: s2, tgtIndex: t2 } = edges[e2];
|
|
|
|
|
|
|
|
|
|
if (s1 === s2) continue;
|
2026-04-13 14:56:33 +08:00
|
|
|
if (t1 === t2) continue;
|
2026-04-13 12:46:11 +08:00
|
|
|
|
|
|
|
|
const crosses = (s1 < s2 && t1 > t2) || (s1 > s2 && t1 < t2);
|
|
|
|
|
expect(crosses).toBe(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-13 14:56:33 +08:00
|
|
|
|
|
|
|
|
it('should assign encounters to nodes', () => {
|
|
|
|
|
const map = generatePointCrawlMap(456);
|
|
|
|
|
|
|
|
|
|
let nodesWithEncounter = 0;
|
|
|
|
|
for (const node of map.nodes.values()) {
|
|
|
|
|
if (node.encounter) {
|
|
|
|
|
nodesWithEncounter++;
|
|
|
|
|
expect(node.encounter.name).toBeTruthy();
|
|
|
|
|
expect(node.encounter.description).toBeTruthy();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(nodesWithEncounter).toBeGreaterThan(0);
|
|
|
|
|
});
|
2026-04-13 21:18:06 +08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
2026-04-13 11:55:57 +08:00
|
|
|
});
|