diff --git a/assets/styles/root.css b/assets/styles/root.css index 506975a..3cabf93 100644 --- a/assets/styles/root.css +++ b/assets/styles/root.css @@ -386,11 +386,22 @@ main { } main > :first-child, -main > article > :first-child { margin-block-start: 0; } +main > article > :first-child, +main > section > :first-child { + margin-block-start: 0; +} + main > :not(:last-child), -main > article > :not(:last-child) { margin-block-end: var(--body-item-spacing); } +main > article > :not(:last-child), +main > section > :not(:last-child) { + margin-block-end: var(--body-item-spacing); +} + main > :last-child, -main > article > :last-child { margin-block-end: 0; } +main > article > :last-child, +main > section > :last-child { + margin-block-end: 0; +} nav.bulleted > li:first-child::before { color: var(--html-color); 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 new file mode 100644 index 0000000..7a1cdaa --- /dev/null +++ b/content/series/nethack-level-generation/_index.md @@ -0,0 +1,39 @@ +--- +title: "Nethack's Level Generation" +date: 2023-02-05T09:08:07-08:00 +draft: true +--- + +I recently became interested in how Nethack generates levels. It has a distinctive feel that's uniquely Nethack, and +that can't be perfectly replicated with "room and corridor" algorithms that I've studied so far. Nethack's source code +is freely available on Github, though as I discovered while researching for this article, having it available doesn't +mean it's easy to understand. + +I'm basing this entire series on the [Nethack 3.6][nh36] branch. + +### 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: + +```c +int +a_function(a, b, c) +int a, b; +char c +{ + // ... +} +``` + +The standard Dungeons of Doom map style is a hallmark of Nethack, Hack, and Rogue. Given this long lineage, the map +generator is (I suspect) one of the older parts of the codebase. A lot of the architectural decisions reflect a differen +time and place: it strives to be extremely memory and CPU efficient, reuses global in-memory structures rather than +declaring new ones, and generally makes due with less as often as possible. This includes variable names too. + +Most of this happens in [`mklev.c`][mklevc_file]. + +Levels are always 80 columns by 21 rows in size. + +[nh36]: https://github.com/NetHack/NetHack/tree/NetHack-3.6 +[mklevc_file]: https://github.com/NetHack/NetHack/blob/NetHack-3.6/src/mklev.c diff --git a/layouts/_default/term.html b/layouts/_default/term.html index 5ae43d7..d3e74b5 100644 --- a/layouts/_default/term.html +++ b/layouts/_default/term.html @@ -7,6 +7,12 @@