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'); 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.ceil(canvasWidth / CELL_WIDTH) - 1, 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 = () => { 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');