diff --git a/assets/scripts/nethack/dungeon.js b/assets/scripts/nethack/dungeon.js index 9c2f706..75d06e0 100644 --- a/assets/scripts/nethack/dungeon.js +++ b/assets/scripts/nethack/dungeon.js @@ -1,4 +1,11 @@ +const DEBUG = true; +const NUMBER_OF_ROOMS = 12; +const TUNNEL_PASSES = 2; + class Cell { + static CORRIDOR = "#"; + static DOOR_CLOSED = "+"; + character; characterColor; backgroundColor; @@ -7,16 +14,24 @@ class Cell { this.character = char; } + empty() { this.character = " "; } floor() { this.character = "."; } upStair() { this.character = "<"; } downStair() { this.character = ">"; } - cooridor() { 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 { @@ -24,9 +39,25 @@ class Point { y = 0; constructor(x, y) { - this.x = x; - this.y = 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 { @@ -56,6 +87,9 @@ class Rect { 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; } insetRect(inset) { @@ -89,7 +123,6 @@ class Rect { class Grid { #size; #cells = []; - #rooms = []; constructor(width, height) { @@ -101,32 +134,20 @@ class Grid { } } - get width() { - return this.#size.width; - } + 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; } - get height() { - return this.#size.height; - } - - generate(p, roomGenerator) { - this.#generateRooms(p, roomGenerator); + 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; - - for (let room of this.#rooms) { - 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.cellAt(x, y); - room.transformCellAt(point, cell); - cell.characterColor = p.color(255); - } - } - } } #placeStairs() { @@ -147,9 +168,13 @@ class Grid { } } - cellAt(x, y) { - return this.#cells[y * this.width + x]; + #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 { @@ -159,6 +184,8 @@ class Room { 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; } @@ -196,23 +223,16 @@ class NRandomRoomsGenerator { #numberOfRooms = 12; #rooms; + #grid; - #bounds; - - constructor(bounds, numberOfRooms) { - if (bounds) { - this.#bounds = bounds; - } + constructor(grid, numberOfRooms) { + this.#grid = grid; if (numberOfRooms) { this.#numberOfRooms = numberOfRooms; } } - get numberOfRooms() { - return this.#numberOfRooms; - } - get rooms() { if (!this.#rooms) { this.#generateRooms(); @@ -221,6 +241,32 @@ class NRandomRoomsGenerator { 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); + + if (DEBUG) { + if (x == room.minX + 1 && y == room.minY + 1) { + cell.character = (i % 10).toString(); + cell.characterColor = p.color(255, 128, 255); + } + } + } + } + } + } + #generateRooms() { let rects = new Array(); @@ -247,10 +293,304 @@ class NRandomRoomsGenerator { 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); + + for (let neighbor of fromPoint.neighbors()) { + 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()) { + 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; + + console.log(`Digging a corridor from (${fromX}, ${fromY}) to (${toX}, ${toY})`); + + 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; + } + + console.log("dx, dy", dx, dy); + 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 fromPoint; + let toPoint; + if (fromRoomBounds.maxX < (toRoomBounds.minX - 1)) { + // fromRoom is farther left than toRoomBounds. + do { + fromPoint = new Point(fromRoomBounds.maxX, fromRoomBounds.minY + 1 + randomInt(fromRoomBounds.height - 2)); + } while (!this.#canPlaceDoorAt(fromPoint)); + + do { + toPoint = new Point(toRoomBounds.minX, toRoomBounds.minY + 1 + randomInt(toRoomBounds.height - 2)); + } while (!this.#canPlaceDoorAt(toPoint)); + + fromPoint.x += 1; + toPoint.x -= 1; + } else if (fromRoomBounds.minX > (toRoomBounds.maxX + 1)) { + // fromRoom is farther right than toRoomBounds. + do { + fromPoint = new Point(toRoomBounds.maxX, toRoomBounds.minY + 1 + randomInt(toRoomBounds.height - 2)); + } while (!this.#canPlaceDoorAt(fromPoint)); + + do { + toPoint = new Point(fromRoomBounds.minX, fromRoomBounds.minY + 1 + randomInt(fromRoomBounds.height - 2)); + } while (!this.#canPlaceDoorAt(toPoint)); + + fromPoint.x -= 1; + toPoint.x += 1; + } else if (fromRoomBounds.maxY < (toRoomBounds.minY - 1)) { + // fromRoom is above toRoom + do { + fromPoint = new Point(fromRoomBounds.minX + 1 + randomInt(fromRoomBounds.width - 2), fromRoomBounds.maxY); + } while (!this.#canPlaceDoorAt(fromPoint)); + + do { + toPoint = new Point(toRoomBounds.minX + 1 + randomInt(toRoomBounds.width - 2), toRoomBounds.minY); + } while (!this.#canPlaceDoorAt(toPoint)); + + fromPoint.y += 1; + toPoint.y -= 1; + } else if (fromRoomBounds.minY > (toRoomBounds.maxY + 1)) { + // fromRoom is below toRoom + do { + fromPoint = new Point(toRoomBounds.minX + 1 + randomInt(toRoomBounds.width - 2), toRoomBounds.maxY); + } while (!this.#canPlaceDoorAt(fromPoint)); + + do { + toPoint = new Point(fromRoomBounds.minX + 1 + randomInt(fromRoomBounds.width - 2), fromRoomBounds.minY); + } while (!this.#canPlaceDoorAt(toPoint)); + + 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); @@ -282,7 +622,7 @@ new p5(p => { 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, new NRandomRoomsGenerator(gridBounds)); + grid.generate(p, NRandomRoomsGenerator, TunnelGenerator); } p.draw = () => {