Ceremonial PR: fix map gen #39

Merged
pyrex merged 14 commits from fix-mapgen into main 2025-02-23 05:41:20 +00:00
4 changed files with 194 additions and 8 deletions

View File

@ -111,6 +111,15 @@ export class Point {
let dy = other.y - this.y; let dy = other.y - this.y;
return Math.sqrt(dx * dx + dy * dy); return Math.sqrt(dx * dx + dy * dy);
} }
neighbors(): Point[] {
return [
new Point(this.x, this.y - 1),
new Point(this.x - 1, this.y),
new Point(this.x, this.y + 1),
new Point(this.x + 1, this.y),
];
}
} }
export class Size { export class Size {
@ -264,19 +273,29 @@ export class Grid<T> {
return new Grid(this.size, (xy) => cbCell(this.get(xy), xy)); return new Grid(this.size, (xy) => cbCell(this.get(xy), xy));
} }
#checkPosition(position: Point) { #invalidPosition(position: Point): boolean {
if ( return (
position.x < 0 || position.x < 0 ||
position.x >= this.size.w || position.x >= this.size.w ||
Math.floor(position.x) != position.x || Math.floor(position.x) != position.x ||
position.y < 0 || position.y < 0 ||
position.y >= this.size.h || position.y >= this.size.h ||
Math.floor(position.y) != position.y Math.floor(position.y) != position.y
) { );
}
#checkPosition(position: Point) {
if (this.#invalidPosition(position)) {
throw new Error(`invalid position for ${this.size}: ${position}`); throw new Error(`invalid position for ${this.size}: ${position}`);
} }
} }
maybeGet(position: Point): T | null {
if (this.#invalidPosition(position)) {
return null;
}
return this.#data[position.y][position.x];
}
get(position: Point): T { get(position: Point): T {
this.#checkPosition(position); this.#checkPosition(position);
return this.#data[position.y][position.x]; return this.#data[position.y][position.x];

View File

@ -6,6 +6,8 @@ import { addButton } from "./button.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { getStateManager } from "./statemanager.ts"; import { getStateManager } from "./statemanager.ts";
import { getCheckModal } from "./checkmodal.ts"; import { getCheckModal } from "./checkmodal.ts";
//import { LadderPickup } from "./pickups.ts";
// import { generateMap } from "./mapgen.ts";
type Button = { type Button = {
label: string; label: string;
@ -53,6 +55,32 @@ export class Hotbar {
enabled: true, enabled: true,
endorse: getPlayerProgress().getBlood() < 100, endorse: getPlayerProgress().getBlood() < 100,
}); });
/*
buttons.push({
label:"Cheat",
cbClick: () => {
new LadderPickup().onClick();
},
enabled: true,
endorse: false,
})
buttons.push({
label:"Dig for bad maps",
cbClick: () => {
let i = 0;
try {
for(; i < 10000; i++) {
generateMap();
}
} catch(e) {
console.log(`Map gen failed after ${i} tries.`);
}
console.log("Ten thousand maps generated successfully.");
},
enabled: true,
endorse: true,
})
*/
return buttons; return buttons;
} }

View File

@ -89,6 +89,72 @@ class Knife {
} }
} }
} }
showDebug(merged: Record<number, number>) {
if (true) {
let out = "";
let errors: string[] = [];
const size = this.#regions.size;
for (let y = 0; y < size.h; y++) {
for (let x = 0; x < size.w; x++) {
const loc = new Point(x, y);
out += (() => {
if (this.#map.get(loc).architecture == Architecture.Wall) {
return this.#sealedWalls.get(loc) ? "◘" : "█";
}
let r = this.#regions.get(loc);
if (typeof r === "number") {
const resolved = merged[r];
if (typeof resolved === "number") {
r = resolved;
} else {
errors.push(`${loc} is region ${r}, not found in merged`);
}
if (r < 0) {
return "!";
}
// 0...9 and lowercase
if (r < 36) {
return r.toString(36);
}
// uppercase
r -= 26;
if (r < 36) {
return r.toString(36).toUpperCase();
}
// Greek lowercase
r -= 36;
if (r < 25) {
return String.fromCodePoint(r + 0x3b1);
}
// Greek uppercase (there is a hole at 0x3a2)
r -= 25;
if (r < 17) {
return String.fromCodePoint(r + 0x391);
}
r -= 17;
if (r < 7) {
return String.fromCodePoint(r + 0x3a3);
}
// Hebrew
r -= 7;
if (r < 27) {
return String.fromCodePoint(r + 0x5d0);
}
// give up
return "?";
}
return "."; // room without region
})();
}
out += "\n";
}
console.log(out);
if (errors.length > 0) {
console.log(`uh-oh: \n\t${errors.join("\n\t")}`);
}
}
}
} }
export function generateMap(): LoadedNewMap { export function generateMap(): LoadedNewMap {
@ -99,6 +165,11 @@ export function generateMap(): LoadedNewMap {
if (e instanceof TryAgainException) { if (e instanceof TryAgainException) {
continue; continue;
} }
if (e instanceof BadMapError) {
console.log(`Bad map generated: ${e.message}:`);
showDebug(e.badMap);
// continue;
}
throw e; throw e;
} }
} }
@ -108,7 +179,7 @@ export function tryGenerateMap(vaultTemplates: VaultTemplate[]): LoadedNewMap {
let width = WIDTH; let width = WIDTH;
let height = HEIGHT; let height = HEIGHT;
if (width % 2 == 0 || height % 2 == 0) { if (width % 2 == 0 || height % 2 == 0) {
throw "must be odd-sized"; throw new Error("map bounds must be odd-sized");
} }
let grid = new LoadedNewMap("generated", new Size(width, height)); let grid = new LoadedNewMap("generated", new Size(width, height));
@ -484,7 +555,7 @@ function connectRegions(knife: Knife) {
); );
} }
iter++; iter++;
showDebug(knife.map); knife.showDebug(merged);
if (connectors.length == 0) { if (connectors.length == 0) {
throw new TryAgainException( throw new TryAgainException(
"couldn't figure out how to connect sections", "couldn't figure out how to connect sections",
@ -498,12 +569,15 @@ function connectRegions(knife: Knife) {
let sources: number[] = dedup(basicRegions.map((i) => merged[i])); let sources: number[] = dedup(basicRegions.map((i) => merged[i]));
let dest: number | undefined = sources.pop(); let dest: number | undefined = sources.pop();
if (dest == undefined) { if (dest == undefined) {
throw "each connector should touch more than one region"; throw new BadMapError(
`each connector should touch more than one region but ${connector} does not`,
knife.map,
);
} }
if (Math.random() > EXTRA_CONNECTOR_CHANCE) { if (Math.random() > EXTRA_CONNECTOR_CHANCE) {
// at random, don't regard them as merged // at random, don't regard them as merged
for (let i = 0; i < knife.region; i++) { for (let i = 0; i <= knife.region; i++) {
if (sources.indexOf(merged[i]) != -1) { if (sources.indexOf(merged[i]) != -1) {
merged[i] = dest; merged[i] = dest;
} }
@ -532,6 +606,12 @@ function connectRegions(knife: Knife) {
} }
connectors = connectors2; connectors = connectors2;
} }
knife.showDebug(merged);
// The map should now be fully connected.
if (!knife.map.isConnected()) {
throw new BadMapError("unconnected", knife.map);
}
} }
function growMaze(knife: Knife, start: Point) { function growMaze(knife: Knife, start: Point) {
@ -624,7 +704,7 @@ function decorateRoom(_map: LoadedNewMap, _rect: Rect) {}
function randrange(lo: number, hi: number) { function randrange(lo: number, hi: number) {
if (lo >= hi) { if (lo >= hi) {
throw `randrange: hi must be >= lo, ${hi}, ${lo}`; throw new Error(`randrange: hi must be >= lo, ${hi}, ${lo}`);
} }
return lo + Math.floor(Math.random() * (hi - lo)); return lo + Math.floor(Math.random() * (hi - lo));
@ -658,3 +738,11 @@ function showDebug(grid: LoadedNewMap) {
} }
class TryAgainException extends Error {} class TryAgainException extends Error {}
class BadMapError extends Error {
badMap: LoadedNewMap;
constructor(msg: string, badMap: LoadedNewMap) {
super(msg);
this.badMap = badMap;
}
}

View File

@ -105,6 +105,57 @@ export class LoadedNewMap {
getZoneLabel(point: Point): string | null { getZoneLabel(point: Point): string | null {
return this.#zoneLabels.get(point); return this.#zoneLabels.get(point);
} }
isConnected(): boolean {
const size = this.#size;
let reached = new Grid<boolean>(size, () => false);
// find starting location
const found: Point | null = (() => {
for (let x = 0; x < size.w; x++) {
for (let y = 0; y < size.w; y++) {
const p = new Point(x, y);
if (this.#architecture.get(p) == Architecture.Floor) {
return p;
}
}
}
return null;
})();
if (found === null) {
// technically, all open floors on the map are indeed connected
return true;
}
let stack: Point[] = [found];
reached.set(found, true);
while (stack.length > 0) {
const loc = stack.pop() as Point;
for (var p of loc.neighbors()) {
if (
this.#architecture.maybeGet(p) === Architecture.Floor &&
!reached.get(p)
) {
reached.set(p, true);
stack.push(p);
}
}
}
for (let x = 0; x < size.w; x++) {
for (let y = 0; y < size.w; y++) {
const p = new Point(x, y);
if (
this.#architecture.get(p) == Architecture.Floor &&
!reached.get(p)
) {
return false;
}
}
}
return true;
}
} }
export class CellView { export class CellView {