Compare commits
4 commits
main
...
posts/neth
Author | SHA1 | Date | |
---|---|---|---|
77106032f1 | |||
202d8177be | |||
b5768770de | |||
7c0ee2b021 |
5 changed files with 470 additions and 3 deletions
|
@ -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);
|
||||
|
|
|
@ -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
|
|
@ -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 => {
|
||||
};
|
39
content/series/nethack-level-generation/_index.md
Normal file
39
content/series/nethack-level-generation/_index.md
Normal 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
|
|
@ -7,6 +7,12 @@
|
|||
<h1>{{ .Title }}</h1>
|
||||
</header>
|
||||
|
||||
{{ with .Content }}
|
||||
<section>
|
||||
{{ . }}
|
||||
</section>
|
||||
{{ end }}
|
||||
|
||||
<ul>
|
||||
{{- range .Pages -}}
|
||||
<li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue