Skip to content

Commit

Permalink
0.9.3 - Optimal Auxiliary Paths (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
charredUtensil authored Jul 18, 2024
1 parent b3fc45a commit 18f3b7d
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 70 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "groundhog",
"version": "0.9.2",
"version": "0.9.3",
"homepage": "https://charredutensil.github.io/groundhog",
"private": true,
"dependencies": {
Expand Down
9 changes: 7 additions & 2 deletions src/core/common/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ export type CavernContext = {
/**
* Add at most this many extra redundant paths.
*/
auxiliaryPathCount: number;
optimalAuxiliaryPathCount: number;
/**
* Add at most this many extra redundant paths.
*/
randomAuxiliaryPathCount: number;
/**
* Auxiliary paths will not be chosen if they make an angle less than this
* against another path.
Expand Down Expand Up @@ -269,7 +273,8 @@ const STANDARD_DEFAULTS = {
baseplateMaxOblongness: 3,
baseplateMaxRatioOfSize: 0.33,
caveCount: 20,
auxiliaryPathCount: 4,
optimalAuxiliaryPathCount: 0,
randomAuxiliaryPathCount: 4,
auxiliaryPathMinAngle: Math.PI / 4,
caveBaroqueness: 0.14,
hallBaroqueness: 0.05,
Expand Down
181 changes: 126 additions & 55 deletions src/core/transformers/00_outlines/06_weave.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,68 @@
import { Mutable } from "../../common";
import { Baseplate } from "../../models/baseplate";
import { TriangulatedCavern } from "./02_triangulate";
import { Path } from "../../models/path";

type AngleInfo = readonly number[][];
type GraphNode = {
src: Baseplate;
edges: { theta: number; distance: number; dest: Baseplate }[];
};

function getAngleInfo(paths: readonly Path[]): AngleInfo {
const result: Mutable<AngleInfo> = [];
const push = (id: number, a: Baseplate, b: Baseplate) => {
const [ax, ay] = a.center;
const [bx, by] = b.center;
function getGraph(paths: readonly Path[]): GraphNode[] {
const result: GraphNode[] = [];
const push = (
path: Path,
src: Baseplate,
next: Baseplate,
dest: Baseplate,
) => {
const [ax, ay] = src.center;
const [bx, by] = next.center;
const theta = Math.atan2(by - ay, bx - ax);
result[a.id] ||= [];
result[a.id][id] = theta;
const distance = path.snakeDistance;
(result[src.id] ||= { src, edges: [] }).edges[path.id] = {
theta,
distance,
dest,
};
};
paths.forEach((path) => {
const bps = path.baseplates;
push(path.id, bps[0], bps[1]);
push(path.id, bps[bps.length - 1], bps[bps.length - 2]);
push(path, bps[0], bps[1], bps[bps.length - 1]);
push(path, bps[bps.length - 1], bps[bps.length - 2], bps[0]);
});
return result;
}

function getAllDistances(graph: GraphNode[], paths: Path[], src: Baseplate) {
const distances: number[] = [];
const queue: Baseplate[] = [];
const result: { src: Baseplate; dest: Baseplate; distance: number }[] = [];

distances[src.id] = 0;
queue.push(src);

// Dijkstra's algorithm
while (queue.length) {
const node = queue.shift()!;
result.unshift({ src, dest: node, distance: distances[node.id] });
graph[node.id].edges.forEach(({ distance, dest }, pathId) => {
const pathKind = paths[pathId]?.kind;
if (pathKind === "spanning" || pathKind === "auxiliary") {
const d = distances[node.id] + distance;
if (distances[dest.id] === undefined) {
queue.push(dest);
distances[dest.id] = d;
} else if (d < distances[dest.id]) {
distances[dest.id] = d;
}
}
});
queue.sort((a, b) => distances[a.id] - distances[b.id]);
}

return result;
}

/** Returns the angle between two absolute angles. */
function getOffset(t1: number, t2: number): number {
const r = Math.abs(t1 - t2);
Expand All @@ -31,54 +72,84 @@ function getOffset(t1: number, t2: number): number {

export default function weave(cavern: TriangulatedCavern): TriangulatedCavern {
const rng = cavern.dice.weave;
const angleInfo = getAngleInfo(cavern.paths);
const graph = getGraph(cavern.paths);
const paths: Path[] = [];
const pathIdsByEnds: number[][] = [];
cavern.paths.forEach((path) => {
paths[path.id] = path;
(pathIdsByEnds[path.origin.id] ||= [])[path.destination.id] = path.id;
(pathIdsByEnds[path.destination.id] ||= [])[path.origin.id] = path.id;
});

const result: Path[] = [];
cavern.paths
.filter((path) => path.kind === "spanning")
.forEach((path) => (result[path.id] = path));
function minAngle(path: Path, node: GraphNode) {
const theta = node.edges[path.id].theta;
return node.edges.reduce((r, { theta: t }, pathId) => {
const pathKind = paths[pathId]?.kind;
if (pathKind === "spanning" || pathKind === "auxiliary") {
return Math.min(getOffset(theta, t), r);
}
return r;
}, Infinity);
}

let queue: { path: Path; nfo1: number[]; nfo2: number[] }[] = cavern.paths
.filter((path) => path.kind === "ambiguous")
.map((path) => {
return {
path,
nfo1: angleInfo[path.origin.id],
nfo2: angleInfo[path.destination.id],
};
});
// Delete any paths that don't form a minimum angle
function pruneByAngle() {
let ok = false;
paths
.filter((path) => path.kind === "ambiguous")
.forEach((path) => {
if (
Math.min(
minAngle(path, graph[path.origin.id]),
minAngle(path, graph[path.destination.id]),
) < cavern.context.auxiliaryPathMinAngle
) {
delete paths[path.id];
} else {
ok = true;
}
});
return ok;
}

for (let i = 0; i < cavern.context.auxiliaryPathCount; i++) {
queue = queue
.map((args) => {
const minAngle = (nfo: number[]) => {
const theta = nfo[args.path.id];
const angles = nfo
// Only look at paths that already exist in the result set.
.filter((_, id) => result[id])
.map((t) => getOffset(theta, t));
return Math.min(...angles);
};
return {
...args,
t1: minAngle(args.nfo1),
t2: minAngle(args.nfo2),
};
})
.filter(
({ t1, t2 }) =>
Math.min(t1, t2) >= cavern.context.auxiliaryPathMinAngle,
)
.sort((a, b) => a.t1 + a.t2 - b.t1 - b.t2);
if (!queue.length) {
break;
// Find the largest distance shortcut
function addBestShortcut() {
const distances = graph.map(({ src }) =>
getAllDistances(graph, paths, src),
);
while (true) {
const { src, dest } = distances
.reduce((p, c) => (p[0].distance > c[0].distance ? p : c))
.shift()!;
const path = paths[pathIdsByEnds[src.id][dest.id]];
if (path?.kind === "ambiguous") {
paths[path.id] = new Path(path.id, "auxiliary", path.baseplates);
break;
}
}
const path = queue.splice(
rng.betaInt({ a: 1, b: 5, max: queue.length }),
1,
)[0].path;
result[path.id] = new Path(path.id, "auxiliary", path.baseplates);
}

return { ...cavern, paths: result.filter((path) => path) };
function addRandomShortcut() {
const path = rng.betaChoice(
paths
.filter((path) => path.kind === "ambiguous")
.map((path) => ({
path,
oa: minAngle(path, graph[path.origin.id]),
da: minAngle(path, graph[path.destination.id]),
}))
.sort((a, b) => a.oa + a.da - b.oa - b.da),
{ a: 1, b: 5 },
).path;
paths[path.id] = new Path(path.id, "auxiliary", path.baseplates);
}

for (let i = 0; i < cavern.context.optimalAuxiliaryPathCount; i++) {
pruneByAngle() && addBestShortcut();
}
for (let i = 0; i < cavern.context.randomAuxiliaryPathCount; i++) {
pruneByAngle() && addRandomShortcut();
}

return { ...cavern, paths: paths.filter((path) => path) };
}
34 changes: 22 additions & 12 deletions src/webui/components/context_editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ function unparseSeed(v: number) {
return v.toString(16).padStart(8, "0").toUpperCase();
}

const expectedCavePlans = (contextWithDefaults: CavernContext | undefined) =>
contextWithDefaults ? contextWithDefaults.caveCount : 20;

const expectedTotalPlans = (contextWithDefaults: CavernContext | undefined) =>
contextWithDefaults
? contextWithDefaults.caveCount * 2 -
1 +
contextWithDefaults.auxiliaryPathCount
: 50;
const expectedTotalPlans = (contextWithDefaults: CavernContext) => {
const caves = contextWithDefaults.caveCount;
const spanHalls = contextWithDefaults.caveCount - 1;
const auxHalls =
contextWithDefaults.optimalAuxiliaryPathCount +
contextWithDefaults.randomAuxiliaryPathCount;
return caves + spanHalls + auxHalls;
};

type PartialContext = Partial<CavernContext> & Pick<CavernContext, "seed">;

Expand Down Expand Up @@ -161,7 +160,18 @@ export function CavernContextInput({
</div>
<div className={styles.subsection}>
<h3>Weave</h3>
<Slider of="auxiliaryPathCount" min={0} max={50} {...rest} />
<Slider
of="optimalAuxiliaryPathCount"
min={0}
max={contextWithDefaults.caveCount}
{...rest}
/>
<Slider
of="randomAuxiliaryPathCount"
min={0}
max={contextWithDefaults.caveCount}
{...rest}
/>
<Slider
of="auxiliaryPathMinAngle"
min={0}
Expand Down Expand Up @@ -190,13 +200,13 @@ export function CavernContextInput({
<Slider
of="waterLakes"
min={1}
max={expectedCavePlans(contextWithDefaults)}
max={contextWithDefaults.caveCount}
{...rest}
/>
<Slider
of="lavaLakes"
min={1}
max={expectedCavePlans(contextWithDefaults)}
max={contextWithDefaults.caveCount}
{...rest}
/>
<Slider
Expand Down

0 comments on commit 18f3b7d

Please sign in to comment.