Add nethack-rooms-and-corridors-generator; make nethack-level-generator into a folder
This commit is contained in:
parent
202d8177be
commit
77106032f1
4 changed files with 412 additions and 44 deletions
|
@ -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
|
|
|
@ -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 => {
|
||||||
|
};
|
|
@ -11,7 +11,7 @@ mean it's easy to understand.
|
||||||
|
|
||||||
I'm basing this entire series on the [Nethack 3.6][nh36] branch.
|
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
|
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:
|
of arguments defined below the function declaration:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue