From 77106032f171c17b9dd68797f8e795dc9f35bb36 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 12 Feb 2023 10:31:48 -0800 Subject: [PATCH] Add nethack-rooms-and-corridors-generator; make nethack-level-generator into a folder --- .../2023/nethack-level-generator/index.md | 43 --- .../index.md | 64 ++++ .../sketch.js | 347 ++++++++++++++++++ .../series/nethack-level-generation/_index.md | 2 +- 4 files changed, 412 insertions(+), 44 deletions(-) delete mode 100644 content/blog/2023/nethack-level-generator/index.md create mode 100644 content/blog/2023/nethack-rooms-and-corridors-generator/index.md create mode 100644 content/blog/2023/nethack-rooms-and-corridors-generator/sketch.js diff --git a/content/blog/2023/nethack-level-generator/index.md b/content/blog/2023/nethack-level-generator/index.md deleted file mode 100644 index ce29f87..0000000 --- a/content/blog/2023/nethack-level-generator/index.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: "Nethack's Rooms & Corridors Generator" -date: 2023-02-05T09:08:07-08:00 -draft: true -series: "Nethack Level Generation" -categories: Tech -tags: [Nethack, Programming] ---- - -## Rectangles - -## Placing Rooms - -[`makerooms`][makerooms_func] uses a "random rect" algorithm to create up to `MAXNROFROOMS` rooms. It iteratively -generates a random rectancle - -`create_room` - -`sort_rooms` - -## Digging Corridors - -[`makecorridors`][makecorridors_func] - -[`join`][join_func] - -`dig_corridor` - -## Special Features - -`make_niches` - -`do_vault` - -`create_vault` - -## Special Rooms - -[nh36]: https://github.com/NetHack/NetHack/tree/NetHack-3.6 -[mklevc_file]: https://github.com/NetHack/NetHack/blob/NetHack-3.6/src/mklev.c -[makerooms_func]: https://github.com/NetHack/NetHack/blob/59b117c655731bdf1f8b92c57bdb786119927f3a/src/mklev.c#L223 -[makecorridors_func]: https://github.com/NetHack/NetHack/blob/59b117c655731bdf1f8b92c57bdb786119927f3a/src/mklev.c#L319 -[join_func]: https://github.com/NetHack/NetHack/blob/59b117c655731bdf1f8b92c57bdb786119927f3a/src/mklev.c#L244 diff --git a/content/blog/2023/nethack-rooms-and-corridors-generator/index.md b/content/blog/2023/nethack-rooms-and-corridors-generator/index.md new file mode 100644 index 0000000..ab775d5 --- /dev/null +++ b/content/blog/2023/nethack-rooms-and-corridors-generator/index.md @@ -0,0 +1,64 @@ +--- +title: "Nethack's Rooms & Corridors Generator" +date: 2023-02-05T09:08:07-08:00 +draft: true +series: "Nethack Level Generation" +categories: Tech +tags: [Nethack, Programming] +--- + +This post is one of a series of posts about Nethack's level generation +algorithms. You might want to start [at the beginning][series]. + +## Rectangles + +Nethack builds a list of random rectangles each time it generates new level. The +list is initialized with a single rect the size of the map, 80 by 21. As new +rectangles are needed, larger ones are broken down into smaller ones by the +`split_rects` function in such a way that no rectangle overlaps any other. Up to +50 rectangeles can be generated this way, placed randomly around the map, and +each one is no smaller than 4 by 3. + +## Placing Rooms + +[`makerooms`][makerooms_func] uses the rectangle list above to create up to 50 +rooms. If enough rooms have been generated, it will also try to generate a +[vault](#vaults). + +In each turn of the loop, `makerooms` calls [`create_room`][create_room_func], +which tries to create a room. + +`sort_rooms` + +## Digging Corridors + +After all the rooms have been placed, the level generator places corridors. It +tries to connect as many rooms to each other as possible, up to a maximum number +of doors. + +[`makecorridors`][makecorridors_func] + +[`join`][join_func] + +`dig_corridor` + +## Niches + +Niches are small, one tile rooms behind locked, hidden doors. The code refers to +these as niches, though I've most often heard players refer to them as closets. + +`make_niches` + +## Vaults + +`do_vault` + +`create_vault` + +## Special Rooms + +[series]: {{< ref "/series/nethack-level-generation" >}} +[makerooms_func]: https://github.com/NetHack/NetHack/blob/59b117c655731bdf1f8b92c57bdb786119927f3a/src/mklev.c#L223 +[create_room_func]: https://github.com/NetHack/NetHack/blob/59b117c655731bdf1f8b92c57bdb786119927f3a/src/sp_lev.c#L1127 +[makecorridors_func]: https://github.com/NetHack/NetHack/blob/59b117c655731bdf1f8b92c57bdb786119927f3a/src/mklev.c#L319 +[join_func]: https://github.com/NetHack/NetHack/blob/59b117c655731bdf1f8b92c57bdb786119927f3a/src/mklev.c#L244 diff --git a/content/blog/2023/nethack-rooms-and-corridors-generator/sketch.js b/content/blog/2023/nethack-rooms-and-corridors-generator/sketch.js new file mode 100644 index 0000000..879321d --- /dev/null +++ b/content/blog/2023/nethack-rooms-and-corridors-generator/sketch.js @@ -0,0 +1,347 @@ +"use strict"; + +/// @see ROWNO +const NUMBER_OF_ROWS = 21; +/// @see COLNO +const NUMBER_OF_COLS = 80; +/// @see XLIM +const RECT_WIDTH_LIMIT = 4; +/// @see YLIM +const RECT_HEIGHT_LIMIT = 3; +/// @see MAXRECT +const MAX_NUMBER_OF_RECTS = 50; + +export class Rect { + lowX = 0; + lowY = 0; + highX = 0; + highY = 0; + + constructor(lx = 0, ly = 0, hx = 0, hy = 0) { + if (lx) { this.lowX = lx; } + if (ly) { this.lowY = ly; } + if (hx) { this.highX = hx; } + if (hy) { this.highY = hy; } + } +} + +/** + * A re-implementation of NetHack's rect.c, hewing as close to the implementation as possible. + */ +export class Rects { + #rects = []; + #numberOfRects = 0; + + #maxNumbersOfRects; + + constructor(numberOfSlots = MAX_NUMBER_OF_RECTS) { + this.#maxNumbersOfRects = numberOfSlots; + this.initialize(new Rect(0, 0, NUMBER_OF_ROWS, NUMBER_OF_COLS)); + } + + get numberOfRects() { + return this.#numberOfRects; + } + + /// @see init_rect + initialize(baseRect) { + this.#rects = new Array(this.#maxNumbersOfRects); + if (baseRect) { + this.#rects[0] = baseRect; + this.#numberOfRects = 1; + } + } + + /// @see get_rect + get(rect) { + for (let i = 0; i < this.#numberOfRects; i++) { + let storedRect = this.#rects[i]; + if ( rect.lowX >= storedRect.lowX + && rect.lowY >= storedRect.lowY + && rect.highX <= storedRect.highX + && rect.highY <= storedRect.highY) + { + return storedRect; + } + } + + return null; + } + + /// @see get_rect_ind + getIndex(rect) { + for (let i = 0; i < this.#numberOfRects; i++) { + let storedRect = this.#rects[i]; + if ( rect.lowx === storedRect.lowX + && rect.lowY === storedRect.lowY + && rect.highX === storedRect.highX + && rect.highY === storedRect.highY) + { + return i; + } + } + + return undefined; + } + + /// @see add_rect + add(rect) { + if (this.#numberOfRects > this.#maxNumbersOfRects) { + console.error("Exceeded maximum number of rects"); + return; + } + + if (this.get(rect)) { + return; + } + + this.#rects[this.#numberOfRects] = rect; + this.#numberOfRects++; + + console.debug(`Added rect (n = ${this.#numberOfRects})`, rect); + } + + /// @see remove_rect + remove(rect) { + let indexOfRect = this.getIndex(rect); + + if (indexOfRect < 0) { + return; + } + + this.#rects[indexOfRect] = this.#rects[--this.#numberOfRects]; + + console.debug(`Removed rect (n = ${this.#numberOfRects})`, rect); + } + + /// @see rnd_rect + random() { + assert(this.#numberOfRects > 0); + + const numberOfRects = this.#numberOfRects; + return numberOfRects > 0 ? this.#rects[randomInt(numberOfRects)] : undefined; + } + + /// @see intersect + intersect(rectA, rectB) { + if ( rectB.lowX > rectA.highX + || rectB.lowY > rectA.highY + || rectB.highX < rectA.lowX + || rectB.highY < rectA.lowY) + { + return null; + } + + let intersectingRect = new Rect( + rectB.lowX > rectA.lowX ? rectB.lowX : rectA.lowX, + rectB.lowY > rectA.lowY ? rectB.lowY : rectA.lowY, + rectB.highX > rectA.highX ? rectA.highX : rectB.highX, + rectB.highY > rectA.highY ? rectA.highY : rectB.highY + ); + + if ( intersectingRect.lowX > intersectingRect.highX + || intersectingRect.lowY > intersectingRect.highY) + { + return null; + } + + return intersectingRect; + } + + /// @see split_rects + split(rectA, rectB) { + let outputRect; + + let oldRect = rectA; + this.remove(rectA); + + for (let i = this.#numberOfRects - 1; i >= 0; i--) { + let storedRect = this.#rects[i]; + let intersectingRect = this.intersect(storedRect, rectB); + if (intersectingRect) { + outputRect = this.split(storedRect); + } + } + + const rectHeightLimitTimes2 = 2 * RECT_HEIGHT_LIMIT; + const rectHeightLimitPlus1 = RECT_HEIGHT_LIMIT + 1; + + if (rectB.lowY - oldRect.lowY - 1 > (oldRect.highY < (NUMBER_OF_ROWS - 1) ? rectHeightLimitTimes2 : rectHeightLimitPlus1) + 4) { + outputRect = oldRect; + outputRect.highY = rectB.lowY - 1; + this.add(outputRect); + } + + if (rectB.lowY - oldRect.lowX - 1 > (oldRect.highX < NUMBER_OF_COLS - 1 ? rectHeightLimitTimes2 : rectHeightLimitPlus1) + 4) { + outputRect = oldRect; + outputRect.highX = rectB.lowX - 2; + this.add(outputRect); + } + + if (oldRect.highY - rectB.highY - 1 > (oldRect.lowY > 0 ? rectHeightLimitTimes2 : rectHeightLimitPlus1) + 4) { + outputRect = oldRect; + outputRect.lowY = rectB.highY + 2; + this.add(outputRect); + } + + if (oldRect.highX - rectB.highX - 1 > (oldRect.lowX > 0 ? rectHeightLimitTimes2 : rectHeightLimitPlus1) + 4) { + outputRect = oldRect; + outputRect.lowX = rectB.highX + 2; + this.add(outputRect); + } + } +} + +export class SpecialLevel { + #rects; + #rooms = []; + // TODO: What is this? + #smeq = []; + #numberOfRooms = 0; + + constructor(rects) { + this.#rects = rects; + this.#numberOfRooms = 0; + } + + createRoom(x, y, width, height, xAlignment, yAlignment, roomType, isLit) { + // TODO: roomType + let isVault = false; + + // TODO: isLit + + let xAbs; + let yAbs; + let xTmp; + let yTmp; + let widthTmp; + let heightTmp; + + let tryCount = 0; + let rectA; + let rectB; + + do { + xTmp = x; + yTmp = y; + heightTmp = height; + widthTmp = width; + + let xAlignmentTmp = xAlignment; + let yAlignmentTmp = yAlignment; + + if ( (xTmp === null + && yTmp === null + && heightTmp === null + && widthTmp === null + && xAlignmentTmp === null + && yAlignmentTmp === null) + || isVault) + { + rectA = this.#rects.random(); + if (!rectA) { + console.error("No more rects..."); + return false; + } + + let lx = rectA.lowX; + let ly = rectA.lowY; + let hx = rectA.highX; + let hy = rectA.highY; + + let dx; + let dy; + if (isVault) { + dx = 1; + dy = 1; + } else { + dx = 2 + randomInt((hx - lx > 28) ? 12 : 8); + dy = 2 + randomInt(4); + if (dx * dy > 50) { + dy = 50 / dx; + } + } + + let xBorder; + if (lx > 0 && hx < NUMBER_OF_COLS - 1) { + xBorder = 2 * RECT_WIDTH_LIMIT; + } else { + xBorder = RECT_WIDTH_LIMIT + 1; + } + + let yBorder; + if (ly > 0 && hy < NUMBER_OF_ROWS - 1) { + yBorder = 2 * RECT_WIDTH_LIMIT; + } else { + yBorder = RECT_WIDTH_LIMIT + 1; + } + + if (hx - lx < dx + 3 + xBorder || hy - ly < dy + 3 + yBorder) { + rectA = null; + continue; + } + + let xAbs = lx + + (lx > 0 ? RECT_WIDTH_LIMIT : 3) + + randomInt(hx - (lx > 0 ? lx : 3) - dx - xBorder + 1); + let yAbs = ly + + (ly > 0 ? RECT_HEIGHT_LIMIT : 2) + + randomInt(hy - (ly > 0 ? ly : 2) - dy - yBorder + 1); + if (ly === 0 && hy >= (NUMBER_OF_ROWS - 1) && (!this.#numberOfRooms || !randomInt(this.#numberOfRooms)) && (yAbs + dy > NUMBER_OF_ROWS / 2)) { + yAbs = randomIntPlusOffset(3, 2); + if (this.#numberOfRooms < 4 && dy > 1) { + dy--; + } + } + + if (!this.checkRoom(xAbs, dx, yAbs, dy, isVault)) { + rectA = null; + continue; + } + + let widthTmp = dx + 1; + let heightTmp = dy + 1; + rectB = new Rect(xAbs - 1, yAbs - 1, xAbs + widthTmp, yAbs + heightTmp); + } else { + console.error("Not implemented yet!"); + } + + } while (++tryCount < 100 && !rectA); + + if (!rectA) { + return false; + } + + this.#rects.split(rectA, rectB); + + if (!isVault) { + this.#smeq[this.#numberOfRooms] = this.#numberOfRooms; + this.addRoom(xAbs, yAbs, xAbs + widthTmp - 1, yAbs + heightTmp - 1, isLit, roomType, false); + } else { + this.#rooms[this.#numberOfRooms].lx = xAbs; + this.#rooms[this.#numberOfRooms].ly = yAbs; + } + + return true; + } + + checkRoom(x, dx, y, dy, isVault) { + return true; + } + + addRoom(x, y, width, height, isLit, roomType, isVault) { + let room = this.#rooms[this.#numberOfRooms]; + } +} + +function randomInt(n) { + const max = Math.floor(n); + return Math.floor(Math.random() * max); +} + +function randomIntPlusOffset(n, offset) { + return randomInt(n) + offset; +} + +const randomRect = p => { +}; diff --git a/content/series/nethack-level-generation/_index.md b/content/series/nethack-level-generation/_index.md index b262c6e..7a1cdaa 100644 --- a/content/series/nethack-level-generation/_index.md +++ b/content/series/nethack-level-generation/_index.md @@ -11,7 +11,7 @@ mean it's easy to understand. I'm basing this entire series on the [Nethack 3.6][nh36] branch. -## Basics +### Basics It's written in C, using C89 style function declarations. You see a lot of functions defined like this, with the types of arguments defined below the function declaration: