Ceremonial PR: fix map gen (#39)

improve errors

merge state debug dumper

use detailed debugging in map gen

more distinct wall chars

not all sealed walls are walls. okay

handle negative region IDs

also catches some missed semis

stop using the "dark shade" character for standard walls

now uses inverse bullet for sealed walls and full block otherwise

also show final result with region numbers

fix fencepost error when merging regions

map connectedness checker (floodfill)

check for connectedness in mapgen

add commented-out cheat and test buttons

looks like mapgen is now fixed. here are the buttons I used to test it

autoformat code

Merge branch 'main' into fix-mapgen

Co-authored-by: Kistaro Windrider <kistaro@gmail.com>
Reviewed-on: #39
This commit is contained in:
Pyrex 2025-02-23 05:41:19 +00:00
parent bd1cff68e6
commit 1ffc0518b2
4 changed files with 194 additions and 8 deletions

View File

@ -111,6 +111,15 @@ export class Point {
let dy = other.y - this.y;
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 {
@ -264,19 +273,29 @@ export class Grid<T> {
return new Grid(this.size, (xy) => cbCell(this.get(xy), xy));
}
#checkPosition(position: Point) {
if (
#invalidPosition(position: Point): boolean {
return (
position.x < 0 ||
position.x >= this.size.w ||
Math.floor(position.x) != position.x ||
position.y < 0 ||
position.y >= this.size.h ||
Math.floor(position.y) != position.y
) {
);
}
#checkPosition(position: Point) {
if (this.#invalidPosition(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 {
this.#checkPosition(position);
return this.#data[position.y][position.x];

View File

@ -6,6 +6,8 @@ import { addButton } from "./button.ts";
import { getPlayerProgress } from "./playerprogress.ts";
import { getStateManager } from "./statemanager.ts";
import { getCheckModal } from "./checkmodal.ts";
//import { LadderPickup } from "./pickups.ts";
// import { generateMap } from "./mapgen.ts";
type Button = {
label: string;
@ -53,6 +55,32 @@ export class Hotbar {
enabled: true,
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;
}

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 {
@ -99,6 +165,11 @@ export function generateMap(): LoadedNewMap {
if (e instanceof TryAgainException) {
continue;
}
if (e instanceof BadMapError) {
console.log(`Bad map generated: ${e.message}:`);
showDebug(e.badMap);
// continue;
}
throw e;
}
}
@ -108,7 +179,7 @@ export function tryGenerateMap(vaultTemplates: VaultTemplate[]): LoadedNewMap {
let width = WIDTH;
let height = HEIGHT;
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));
@ -484,7 +555,7 @@ function connectRegions(knife: Knife) {
);
}
iter++;
showDebug(knife.map);
knife.showDebug(merged);
if (connectors.length == 0) {
throw new TryAgainException(
"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 dest: number | undefined = sources.pop();
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) {
// 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) {
merged[i] = dest;
}
@ -532,6 +606,12 @@ function connectRegions(knife: Knife) {
}
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) {
@ -624,7 +704,7 @@ function decorateRoom(_map: LoadedNewMap, _rect: Rect) {}
function randrange(lo: number, hi: number) {
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));
@ -658,3 +738,11 @@ function showDebug(grid: LoadedNewMap) {
}
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 {
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 {