Compare commits

...
Sign in to create a new pull request.

4 commits

5 changed files with 470 additions and 3 deletions

View file

@ -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);

View file

@ -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

View file

@ -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 => {
};

View file

@ -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

View file

@ -7,6 +7,12 @@
<h1>{{ .Title }}</h1>
</header>
{{ with .Content }}
<section>
{{ . }}
</section>
{{ end }}
<ul>
{{- range .Pages -}}
<li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>