erynwells.me/assets/scripts/nethack/dungeon.js
Eryn Wells d5296995de Get the nethack page looking good again
Update all the CSS classes and fix the layout so it looks good in the new theme.
Convert a bunch of CSS classes to BEM style.
2024-10-27 09:56:46 -06:00

758 lines
19 KiB
JavaScript

const NUMBER_OF_ROOMS = 12;
const TUNNEL_PASSES = 3;
class Cell {
static CORRIDOR = "#";
static DOOR_CLOSED = "+";
character;
characterColor;
backgroundColor;
constructor(char, charColor) {
this.character = char;
}
empty() { this.character = " "; }
floor() { this.character = "."; }
upStair() { this.character = "<"; }
downStair() { this.character = ">"; }
corridor() { this.character = Cell.CORRIDOR; }
topLeftWall() { this.character = "┌"; }
topRightWall() { this.character = "┐"; }
bottomLeftWall() { this.character = "└"; }
bottomRightWall() { this.character = "┘"; }
horizontalWall() { this.character = "─"; }
verticalWall() { this.character = "│"; }
doorClosed() { this.character = Cell.DOOR_CLOSED; }
isEmpty() { return !this.character || this.character === " "; }
isDoor() { return this.character === Cell.DOOR_CLOSED; }
isCorridor() { return this.character === Cell.CORRIDOR; }
canBecomeDoor() { return this.character === "─" || this.character === "│" }
}
class Point {
x = 0;
y = 0;
constructor(x, y) {
if (x) { this.x = x; }
if (y) { this.y = y; }
}
*neighbors() {
const x = this.x;
const y = this.y;
yield new Point(x - 1, y - 1);
yield new Point(x, y - 1);
yield new Point(x + 1, y - 1);
yield new Point(x - 1, y);
yield new Point(x + 1, y);
yield new Point(x - 1, y + 1);
yield new Point(x, y + 1);
yield new Point(x + 1, y + 1);
}
equalsPoint(other) { return this.x === other.x && this.y === other.y; }
}
class Size {
width = 0;
height = 0;
constructor(width, height) {
this.width = width;
this.height = height;
}
}
class Rect {
origin = new Point();
size = new Size();
static fromCoordinates(x, y, w, h) {
return new Rect(new Point(x, y), new Size(w, h));
}
constructor(origin, size) {
this.origin = origin;
this.size = size;
}
get minX() { return this.origin.x; }
get minY() { return this.origin.y; }
get maxX() { return this.origin.x + this.size.width; }
get maxY() { return this.origin.y + this.size.height; }
get width() { return this.size.width; }
get height() { return this.size.height; }
get area() { return this.size.width * this.size.height; }
*xCoordinates() {
for (let x = this.minX; x <= this.maxX; x++) {
yield x;
}
}
*yCoordinates() {
for (let y = this.minY; y <= this.maxY; y++) {
yield y;
}
}
insetRect(inset) {
const twiceInset = 2 * inset;
return Rect.fromCoordinates(
this.origin.x + inset,
this.origin.y + inset,
this.size.width - twiceInset,
this.size.height - twiceInset
);
}
intersects(otherRect) {
if (otherRect.minX > this.maxX)
return false;
if (otherRect.maxX < this.minX)
return false;
if (otherRect.minY > this.maxY)
return false;
if (otherRect.maxY < this.minY)
return false;
return true;
}
}
class Grid {
#size;
#cells = [];
#rooms = [];
constructor(width, height) {
this.#size = new Size(width, height);
this.#cells = new Array(width * height);
for (let i = 0; i < this.#cells.length; i++) {
this.#cells[i] = new Cell(" ");
}
}
get bounds() { return new Rect(new Point(), this.#size); }
get width() { return this.#size.width; }
get height() { return this.#size.height; }
get rooms() { return this.#rooms; }
generate(p, roomGeneratorClass, tunnelGeneratorClass) {
this.#generateRooms(p, new roomGeneratorClass(this, NUMBER_OF_ROOMS));
this.#placeStairs();
this.#digCorridors(p, new tunnelGeneratorClass(this, TUNNEL_PASSES));
}
#generateRooms(p, generator) {
generator.generate(p);
this.#rooms = generator.rooms;
}
#placeStairs() {
const indexOfRoomWithUpStairs = randomInt(this.#rooms.length);
const coordinateOfUpStair = this.#rooms[indexOfRoomWithUpStairs].randomPoint();
this.cellAt(coordinateOfUpStair.x, coordinateOfUpStair.y).upStair();
while (true) {
let indexOfRoomForDownStair = randomInt(this.#rooms.length);
if (indexOfRoomForDownStair == indexOfRoomWithUpStairs) {
continue;
}
const coordinateOfDownStair = this.#rooms[indexOfRoomForDownStair].randomPoint();
this.cellAt(coordinateOfDownStair.x, coordinateOfDownStair.y).downStair();
break;
}
}
#digCorridors(p, generator) {
generator.generate(p);
}
pointIsInBounds(pt) { return pt.x >= 0 && pt.x < this.width && pt.y >= 0 && pt.y < this.height; }
cellAt(x, y) { return this.#cells[y * this.width + x]; }
}
class Room {
#rect;
constructor(rect) {
this.#rect = rect;
}
get bounds() { return this.#rect; }
get minX() { return this.#rect.minX; }
get minY() { return this.#rect.minY; }
get maxX() { return this.#rect.maxX; }
get maxY() { return this.#rect.maxY; }
randomPoint() {
return new Point(
this.#rect.minX + 1 + randomInt(this.#rect.size.width - 2),
this.#rect.minY + 1 + randomInt(this.#rect.size.height - 2)
);
}
transformCellAt(pt, cell) {
const minX = this.minX;
const minY = this.minY;
const maxX = this.maxX;
const maxY = this.maxY;
const x = pt.x;
const y = pt.y;
if (y === minY && x === minX) { cell.topLeftWall(); }
else if (y === minY && x === maxX) { cell.topRightWall(); }
else if (y === maxY && x === minX) { cell.bottomLeftWall(); }
else if (y === maxY && x === maxX) { cell.bottomRightWall(); }
else if (y === minY || y === maxY) { cell.horizontalWall(); }
else if (x === minX || x === maxX) { cell.verticalWall(); }
else if ((x > minX && x < maxX) && (y > minY && y < maxY)) { return cell.floor(); }
}
}
class NRandomRoomsGenerator {
static MIN_ROOM_DIMENSION = 7;
static MAX_ROOM_DIMENSION = 12;
#numberOfRooms = 12;
#rooms;
#grid;
constructor(grid, numberOfRooms) {
this.#grid = grid;
if (numberOfRooms) {
this.#numberOfRooms = numberOfRooms;
}
}
get rooms() {
if (!this.#rooms) {
this.#generateRooms();
}
return this.#rooms;
}
get #bounds() { return this.#grid.bounds; }
generate(p) {
this.#generateRooms();
for (let i = 0; i < this.#rooms.length; i++) {
let room = this.#rooms[i];
for (let y = room.minY; y <= room.maxY; y++) {
for (let x = room.minX; x <= room.maxX; x++) {
let point = new Point(x, y);
let cell = this.#grid.cellAt(x, y);
room.transformCellAt(point, cell);
cell.characterColor = p.color(255);
}
}
}
}
#generateRooms() {
let rects = new Array();
const sizeRange = NRandomRoomsGenerator.MAX_ROOM_DIMENSION - NRandomRoomsGenerator.MIN_ROOM_DIMENSION;
while (rects.length < this.#numberOfRooms) {
const randomSize = new Size(
NRandomRoomsGenerator.MIN_ROOM_DIMENSION + randomInt(sizeRange),
NRandomRoomsGenerator.MIN_ROOM_DIMENSION + randomInt(sizeRange)
);
const randomOrigin = new Point(
this.#bounds.minX + randomInt(this.#bounds.maxX - randomSize.width),
this.#bounds.minY + randomInt(this.#bounds.maxY - randomSize.height)
);
const proposedRoomRect = new Rect(randomOrigin, randomSize);
// Check that the rect doesn't intersect with any other rects.
if (rects.some(e => e.intersects(proposedRoomRect))) {
continue;
}
rects.push(proposedRoomRect);
}
rects.sort((a, b) => {
if (a.origin.x < b.origin.x) { return -1; }
if (a.origin.x > b.origin.x) { return 1; }
if (a.origin.y < b.origin.y) { return -1; }
if (a.origin.y > b.origin.y) { return 1; }
return 0;
});
this.#rooms = rects.map(r => new Room(r.insetRect(1)));
}
}
class TunnelGenerator {
static MAX_DOORS = 100;
#grid;
#passes = 3;
#numberOfDoorsPlaced = 0;
constructor(grid, passes) {
this.#grid = grid;
if (passes) { this.#passes = passes; }
}
generate(p) {
console.group("Digging tunnels");
if (this.#passes >= 1) {
this.#doPassOne(p);
}
if (this.#passes >= 2) {
this.#doPassTwo(p);
}
if (this.#passes >= 3) {
this.#doPassThree(p);
}
console.groupEnd();
}
#doPassOne(p) {
console.group("Pass 1");
this.#iterateAndConnectRoomsWithOffset(p, 1);
console.groupEnd();
}
#doPassTwo(p) {
console.group("Pass 2");
this.#iterateAndConnectRoomsWithOffset(p, 2);
console.groupEnd();
}
#doPassThree(p) {
console.group("Pass 3");
this.#iterateAndConnectRoomsWithOffset(p, 3);
console.groupEnd();
}
#iterateAndConnectRoomsWithOffset(p, offset) {
let rooms = this.#grid.rooms;
const numberOfRooms = rooms.length;
for (let i = 0; i < numberOfRooms - offset; i++) {
let fromRoom = rooms[i];
let toRoom = rooms[i + offset];
let [fromPoint, toPoint] = this.#findPointFromRoomToRoom(fromRoom, toRoom);
if (!fromPoint || !toPoint) {
continue;
}
for (let neighbor of fromPoint.neighbors()) {
if (!this.#grid.pointIsInBounds(neighbor)) {
continue;
}
let cell = this.#grid.cellAt(neighbor.x, neighbor.y);
if ((neighbor.x === fromPoint.x || neighbor.y === fromPoint.y) && cell.canBecomeDoor()) {
cell.doorClosed();
this.#numberOfDoorsPlaced++;
break;
}
}
const successfullyFoundTargetPoint = this.#digCorridorFromPointToPoint(p, fromPoint, toPoint);
if (successfullyFoundTargetPoint) {
for (let neighbor of toPoint.neighbors()) {
if (!this.#grid.pointIsInBounds(neighbor)) {
continue;
}
let cell = this.#grid.cellAt(neighbor.x, neighbor.y);
if ((neighbor.x === toPoint.x || neighbor.y === toPoint.y) && cell.canBecomeDoor()) {
cell.doorClosed();
this.#numberOfDoorsPlaced++;
break;
}
}
}
}
}
/**
* Dig a corridor from fromPoint to toPoint, assuming that both points are
* adjacent to valid locations for doors on the map.
*
* This is as close a copy of dig_corridor in the Nethack source as I could
* muster. It's not exactly pretty. This method assumed
*/
#digCorridorFromPointToPoint(p, fromPoint, toPoint) {
const MAX_STEPS = 500;
if (!fromPoint || !toPoint) {
return;
}
const fromX = fromPoint.x;
const toX = toPoint.x;
const fromY = fromPoint.y;
const toY = toPoint.y;
let curX = fromX;
let curY = fromY;
let dx = 0;
let dy = 0;
// Set up the initial direction of the dig.
if (toX > curX) {
dx = 1;
} else if (toY > curY) {
dy = 1;
} else if (toX < curX) {
dx = -1;
} else {
dy = -1;
}
curX -= dx;
curY -= dy;
let steps = 0;
while (curX !== toX || curY !== toY) {
if (steps++ > MAX_STEPS) {
return false;
}
curX += dx;
curY += dy;
if (curX >= this.#grid.width - 1 || curX <= 0 || curY <= 0 || curY >= this.#grid.height - 1) {
return false;
}
let cell = this.#grid.cellAt(curX, curY);
if (cell.isEmpty()) {
cell.corridor();
} else if (!cell.isCorridor()) {
return false;
}
let dix = Math.abs(curX - toX);
let diy = Math.abs(curY - toY);
if (dix > diy && diy) {
const random = randomInt(dix - diy + 1);
if (!random) {
dix = 0;
}
} else if (diy > dix && dix) {
const random = randomInt(dix - diy + 1);
if (!random) {
diy = 0;
}
}
if (dy && dix > diy) {
const ddx = curX > toX ? -1 : 1;
let cell = this.#grid.cellAt(curX + ddx, curY);
if (cell.isEmpty() || cell.isCorridor()) {
dx = ddx;
dy = 0;
continue;
}
} else if (dx && diy > dix) {
const ddy = curY > toY ? -1 : 1;
let cell = this.#grid.cellAt(curX, curY + ddy);
if (cell.isEmpty() || cell.isCorridor()) {
dy = ddy;
dx = 0;
continue;
}
}
cell = this.#grid.cellAt(curX + dx, curY + dy);
if (cell.isEmpty() || cell.isCorridor()) {
continue;
}
if (dx) {
dx = 0;
dy = toY < curY ? -1 : 1;
} else {
dy = 0;
dx = toX < curX ? -1 : 1;
}
cell = this.#grid.cellAt(curX + dx, curY + dy);
if (cell.isEmpty() || cell.isCorridor()) {
continue;
}
dy = -dy;
dx = -dx;
}
return true;
}
#findPointFromRoomToRoom(fromRoom, toRoom) {
const fromRoomBounds = fromRoom.bounds;
const toRoomBounds = toRoom.bounds;
let foundFromPoint = false;
let foundToPoint = false;
let fromPoint;
let toPoint;
if (fromRoomBounds.maxX < toRoomBounds.minX) {
// fromRoom is farther left than toRoom
fromPoint = new Point(fromRoomBounds.maxX, fromRoomBounds.minY + 1 + randomInt(fromRoomBounds.height - 2));
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
for (let y of fromRoomBounds.yCoordinates()) {
fromPoint.y = y;
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
if (foundFromPoint) {
break;
}
}
if (!foundFromPoint) {
return [];
}
toPoint = new Point(toRoomBounds.minX, toRoomBounds.minY + 1 + randomInt(toRoomBounds.height - 2));
foundToPoint = this.#canPlaceDoorAt(toPoint);
for (let y of toRoomBounds.yCoordinates()) {
toPoint.y = y;
foundToPoint = this.#canPlaceDoorAt(toPoint);
if (foundToPoint) {
break;
}
}
if (!foundToPoint) {
return [];
}
fromPoint.x += 1;
toPoint.x -= 1;
} else if (fromRoomBounds.minX > toRoomBounds.maxX) {
// fromRoom is farther right than toRoomBounds.
fromPoint = new Point(toRoomBounds.maxX, toRoomBounds.minY + 1 + randomInt(toRoomBounds.height - 2));
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
for (let y of toRoomBounds.yCoordinates()) {
fromPoint.y = y;
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
if (foundFromPoint) {
break;
}
}
if (!foundFromPoint) {
return [];
}
toPoint = new Point(fromRoomBounds.minX, fromRoomBounds.minY + 1 + randomInt(fromRoomBounds.height - 2));
foundToPoint = this.#canPlaceDoorAt(toPoint);
for (let y of fromRoomBounds.yCoordinates()) {
toPoint.y = y;
foundToPoint = this.#canPlaceDoorAt(toPoint);
if (foundToPoint) {
break;
}
}
if (!foundToPoint) {
return [];
}
fromPoint.x -= 1;
toPoint.x += 1;
} else if (fromRoomBounds.maxY < (toRoomBounds.minY - 1)) {
// fromRoom is above toRoom
fromPoint = new Point(fromRoomBounds.minX + 1 + randomInt(fromRoomBounds.width - 2), fromRoomBounds.maxY);
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
for (let x of fromRoomBounds.xCoordinates()) {
fromPoint.x = x;
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
if (foundFromPoint) {
break;
}
}
if (!foundFromPoint) {
return [];
}
toPoint = new Point(toRoomBounds.minX + 1 + randomInt(toRoomBounds.width - 2), toRoomBounds.minY);
foundToPoint = this.#canPlaceDoorAt(toPoint);
for (let x of toRoomBounds.xCoordinates()) {
toPoint.x = x;
foundToPoint = this.#canPlaceDoorAt(toPoint);
if (foundToPoint) {
break;
}
}
if (!foundToPoint) {
return [];
}
fromPoint.y += 1;
toPoint.y -= 1;
} else if (fromRoomBounds.minY > (toRoomBounds.maxY + 1)) {
// fromRoom is below toRoom
fromPoint = new Point(toRoomBounds.minX + 1 + randomInt(toRoomBounds.width - 2), toRoomBounds.maxY);
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
for (let x of toRoomBounds.xCoordinates()) {
fromPoint.x = x;
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
if (foundFromPoint) {
break;
}
}
if (!foundFromPoint) {
return [];
}
toPoint = new Point(fromRoomBounds.minX + 1 + randomInt(fromRoomBounds.width - 2), fromRoomBounds.minY);
foundToPoint = this.#canPlaceDoorAt(toPoint);
for (let x of fromRoomBounds.xCoordinates()) {
toPoint.x = x;
foundToPoint = this.#canPlaceDoorAt(toPoint);
if (foundToPoint) {
break;
}
}
if (!foundToPoint) {
return [];
}
fromPoint.y += 1;
toPoint.y -= 1;
}
return [fromPoint, toPoint];
}
#canPlaceDoorAt(pt) {
if (this.#numberOfDoorsPlaced > TunnelGenerator.MAX_DOORS) {
return false;
}
if (!this.#grid.cellAt(pt.x, pt.y).canBecomeDoor()) {
return false;
}
for (let neighbor of pt.neighbors()) {
if (!this.#grid.pointIsInBounds(neighbor)) {
continue;
}
let cell = this.#grid.cellAt(neighbor.x, neighbor.y);
if (cell.isDoor()) {
return false;
}
}
return true;
}
}
function randomInt(n) {
max = Math.floor(n);
return Math.floor(Math.random() * max);
}
let grid;
new p5(p => {
const CELL_WIDTH = 20;
const CELL_HEIGHT = Math.floor(CELL_WIDTH * 1.3);
p.setup = () => {
const container = document.querySelector('#dungeon-background');
console.assert(container, "Missing #dungeon-background element");
canvasWidth = parseFloat(getComputedStyle(container).width);
canvasHeight = parseFloat(getComputedStyle(container).height);
let canvas = p.createCanvas(canvasWidth, canvasHeight);
canvas.canvas.removeAttribute('style');
container.appendChild(canvas.canvas);
p.pixelDensity(p.displayDensity());
p.textFont("Courier");
const gridBounds = Rect.fromCoordinates(
0, 0,
Math.max(80, Math.ceil(canvasWidth / CELL_WIDTH) - 1),
Math.max(24, Math.ceil(canvasHeight / CELL_HEIGHT) - 1)
);
console.log(`Generating grid with size ${gridBounds.size.width} x ${gridBounds.size.height}`);
grid = new Grid(gridBounds.size.width, gridBounds.size.height);
grid.generate(p, NRandomRoomsGenerator, TunnelGenerator);
};
p.draw = () => {
console.log("Drawing");
p.textSize(CELL_HEIGHT);
for (let y = 0; y < grid.height; y++) {
for (let x = 0; x < grid.width; x++) {
let cell = grid.cellAt(x, y);
let fillColor = cell.characterColor ? cell.characterColor : p.color(255);
p.fill(fillColor);
p.textAlign(p.CENTER, p.CENTER);
p.text(cell.character, x * CELL_WIDTH, y * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT);
}
}
p.noLoop();
};
}, '#dungeon-background');