From 1ffc0518b2ccb043a9690971f2b7348efb152aa9 Mon Sep 17 00:00:00 2001 From: Pyrex Date: Sun, 23 Feb 2025 05:41:19 +0000 Subject: [PATCH] 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 Reviewed-on: https://git.chromaticdragon.app/pyrex/fledgling/pulls/39 --- src/engine/datatypes.ts | 25 +++++++++-- src/hotbar.ts | 28 ++++++++++++ src/mapgen.ts | 98 ++++++++++++++++++++++++++++++++++++++--- src/newmap.ts | 51 +++++++++++++++++++++ 4 files changed, 194 insertions(+), 8 deletions(-) diff --git a/src/engine/datatypes.ts b/src/engine/datatypes.ts index 6b012c1..273e5e4 100644 --- a/src/engine/datatypes.ts +++ b/src/engine/datatypes.ts @@ -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 { 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]; diff --git a/src/hotbar.ts b/src/hotbar.ts index 6c54b38..e396d7c 100644 --- a/src/hotbar.ts +++ b/src/hotbar.ts @@ -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; } diff --git a/src/mapgen.ts b/src/mapgen.ts index 6debb86..c275a5e 100644 --- a/src/mapgen.ts +++ b/src/mapgen.ts @@ -89,6 +89,72 @@ class Knife { } } } + + showDebug(merged: Record) { + 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; + } +} diff --git a/src/newmap.ts b/src/newmap.ts index 6e12717..4dc5c2f 100644 --- a/src/newmap.ts +++ b/src/newmap.ts @@ -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(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 {