
Prompt
create a single-file visually impressive html web app that displays a randomly generated dungeon (consisting of rooms, walls around rooms, empty ground, corridors and doors where corridors connect to rooms). Procedurally generate an appropriate icon set for displaying the corridor.
A system prompt was added to support web rendering
# Dungeon Crawler Generator
I'll create a visually impressive dungeon generator with procedurally generated icons. This web app will display a random dungeon with rooms, corridors, walls, and doors.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procedural Dungeon Generator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #1a1a2e, #16213e);
color: #e6e6e6;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
overflow-x: hidden;
}
header {
text-align: center;
margin: 20px 0 30px;
width: 100%;
max-width: 800px;
}
h1 {
font-size: 2.8rem;
margin-bottom: 10px;
background: linear-gradient(to right, #ff7e5f, #feb47b);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 20px;
}
.controls {
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 25px;
}
button {
background: linear-gradient(to right, #4a00e0, #8e2de2);
color: white;
border: none;
padding: 12px 25px;
border-radius: 30px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
button:active {
transform: translateY(1px);
}
.container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 1000px;
}
.dungeon-container {
background: rgba(10, 15, 30, 0.7);
border-radius: 10px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
width: 100%;
margin-bottom: 30px;
overflow: auto;
}
.dungeon {
display: grid;
grid-template-columns: repeat(30, 24px);
grid-template-rows: repeat(20, 24px);
gap: 0;
margin: 0 auto;
border: 2px solid #4a00e0;
border-radius: 5px;
background: #0d1021;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.7);
}
.tile {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: transform 0.2s;
}
.tile:hover {
transform: scale(1.1);
z-index: 10;
}
.wall {
background: linear-gradient(135deg, #2c3e50, #1a1a2e);
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
}
.ground {
background: linear-gradient(135deg, #16213e, #0f3460);
}
.room {
background: radial-gradient(circle, #1e3c72, #2a5298);
}
.corridor {
background: linear-gradient(135deg, #141e30, #243b55);
}
.door {
background: linear-gradient(135deg, #2c3e50, #34495e);
position: relative;
}
.door::after {
content: "🚪";
font-size: 16px;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
background: rgba(10, 15, 30, 0.7);
padding: 20px;
border-radius: 10px;
width: 100%;
max-width: 800px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
}
.legend-color {
width: 24px;
height: 24px;
border-radius: 4px;
}
.wall-legend { background: linear-gradient(135deg, #2c3e50, #1a1a2e); }
.ground-legend { background: linear-gradient(135deg, #16213e, #0f3460); }
.room-legend { background: radial-gradient(circle, #1e3c72, #2a5298); }
.corridor-legend { background: linear-gradient(135deg, #141e30, #243b55); }
.door-legend { background: linear-gradient(135deg, #2c3e50, #34495e); }
.stats {
display: flex;
gap: 30px;
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
}
.stat-box {
background: rgba(26, 38, 57, 0.7);
padding: 15px 25px;
border-radius: 10px;
text-align: center;
min-width: 150px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #8e2de2;
margin-top: 5px;
}
footer {
margin-top: 30px;
text-align: center;
opacity: 0.7;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.dungeon {
grid-template-columns: repeat(30, 16px);
grid-template-rows: repeat(20, 16px);
}
.tile {
width: 16px;
height: 16px;
font-size: 14px;
}
h1 {
font-size: 2rem;
}
}
</style>
</head>
<body>
<header>
<h1>Procedural Dungeon Generator</h1>
<p class="subtitle">Explore procedurally generated dungeons with rooms, corridors, and doors</p>
</header>
<div class="controls">
<button id="generateBtn">Generate New Dungeon</button>
<button id="regenerateBtn">Regenerate Icons</button>
</div>
<div class="container">
<div class="dungeon-container">
<div class="dungeon" id="dungeon"></div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-color wall-legend"></div>
<span>Wall</span>
</div>
<div class="legend-item">
<div class="legend-color ground-legend"></div>
<span>Ground</span>
</div>
<div class="legend-item">
<div class="legend-color room-legend"></div>
<span>Room</span>
</div>
<div class="legend-item">
<div class="legend-color corridor-legend"></div>
<span>Corridor</span>
</div>
<div class="legend-item">
<div class="legend-color door-legend"></div>
<span>Door</span>
</div>
</div>
<div class="stats">
<div class="stat-box">
<div>Rooms</div>
<div class="stat-value" id="roomCount">0</div>
</div>
<div class="stat-box">
<div>Corridors</div>
<div class="stat-value" id="corridorCount">0</div>
</div>
<div class="stat-box">
<div>Doors</div>
<div class="stat-value" id="doorCount">0</div>
</div>
</div>
</div>
<footer>
<p>Procedurally generated dungeon with custom corridor icons | Refresh for a new dungeon</p>
</footer>
<script>
document.addEventListener('DOMContentLoaded', () => {
const dungeonElement = document.getElementById('dungeon');
const generateBtn = document.getElementById('generateBtn');
const regenerateBtn = document.getElementById('regenerateBtn');
const roomCountElement = document.getElementById('roomCount');
const corridorCountElement = document.getElementById('corridorCount');
const doorCountElement = document.getElementById('doorCount');
// Constants
const ROWS = 20;
const COLS = 30;
const ROOM_COUNT = 8;
const MAX_ROOM_SIZE = 6;
const MIN_ROOM_SIZE = 3;
// Tile types
const TILE_TYPES = {
WALL: 'wall',
GROUND: 'ground',
ROOM: 'room',
CORRIDOR: 'corridor',
DOOR: 'door'
};
// Store dungeon grid
let grid = [];
// Store dungeon rooms
let rooms = [];
// Generate corridor icons
function generateCorridorIcons() {
const icons = [];
// Generate 10 unique corridor icons
for (let i = 0; i < 10; i++) {
// Create a unique pattern with varying elements
const elements = ['╌', '┄', '┈', '╎', '┆', '┊', '┋'];
const colors = ['#4b6cb7', '#3a7bd5', '#00d2ff', '#3a7bd5', '#4b6cb7'];
const color = colors[Math.floor(Math.random() * colors.length)];
const element = elements[Math.floor(Math.random() * elements.length)];
// Create the icon with CSS styling
const icon = `<span style="color:${color}; text-shadow: 0 0 3px rgba(255,255,255,0.5);">${element}</span>`;
icons.push(icon);
}
return icons;
}
// Initialize corridor icons
let corridorIcons = generateCorridorIcons();
// Create empty grid
function createEmptyGrid() {
const grid = [];
for (let row = 0; row < ROWS; row++) {
grid[row] = [];
for (let col = 0; col < COLS; col++) {
grid[row][col] = TILE_TYPES.WALL;
}
}
return grid;
}
// Create a room
function createRoom() {
const width = Math.floor(Math.random() * (MAX_ROOM_SIZE - MIN_ROOM_SIZE + 1)) + MIN_ROOM_SIZE;
const height = Math.floor(Math.random() * (MAX_ROOM_SIZE - MIN_ROOM_SIZE + 1)) + MIN_ROOM_SIZE;
const x = Math.floor(Math.random() * (COLS - width - 2)) + 1;
const y = Math.floor(Math.random() * (ROWS - height - 2)) + 1;
return {
x, y, width, height,
x2: x + width,
y2: y + height
};
}
// Check if room overlaps with existing rooms
function roomOverlaps(newRoom, rooms) {
for (const room of rooms) {
if (
newRoom.x < room.x2 + 1 &&
newRoom.x2 + 1 > room.x &&
newRoom.y < room.y2 + 1 &&
newRoom.y2 + 1 > room.y
) {
return true;
}
}
return false;
}
// Create rooms
function createRooms() {
rooms = [];
let attempts = 0;
const maxAttempts = 1000;
while (rooms.length < ROOM_COUNT && attempts < maxAttempts) {
const room = createRoom();
if (!roomOverlaps(room, rooms)) {
rooms.push(room);
}
attempts++;
}
}
// Carve rooms in grid
function carveRooms() {
rooms.forEach(room => {
for (let y = room.y; y < room.y2; y++) {
for (let x = room.x; x < room.x2; x++) {
grid[y][x] = TILE_TYPES.ROOM;
}
}
});
}
// Create horizontal corridor
function createHCorridor(x1, x2, y) {
const start = Math.min(x1, x2);
const end = Math.max(x1, x2);
for (let x = start; x <= end; x++) {
if (grid[y][x] === TILE_TYPES.WALL) {
grid[y][x] = TILE_TYPES.CORRIDOR;
}
}
}
// Create vertical corridor
function createVCorridor(y1, y2, x) {
const start = Math.min(y1, y2);
const end = Math.max(y1, y2);
for (let y = start; y <= end; y++) {
if (grid[y][x] === TILE_TYPES.WALL) {
grid[y][x] = TILE_TYPES.CORRIDOR;
}
}
}
// Connect rooms with corridors
function connectRooms() {
for (let i = 0; i < rooms.length - 1; i++) {
const roomA = rooms[i];
const roomB = rooms[i + 1];
// Get center points
const centerX1 = Math.floor((roomA.x + roomA.x2) / 2);
const centerY1 = Math.floor((roomA.y + roomA.y2) / 2);
const centerX2 = Math.floor((roomB.x + roomB.x2) / 2);
const centerY2 = Math.floor((roomB.y + roomB.y2) / 2);
// Randomly choose between horizontal-first or vertical-first
if (Math.random() > 0.5) {
createHCorridor(centerX1, centerX2, centerY1);
createVCorridor(centerY1, centerY2, centerX2);
} else {
createVCorridor(centerY1, centerY2, centerX1);
createHCorridor(centerX1, centerX2, centerY2);
}
// Place doors
placeDoors(roomA, roomB, centerX1, centerY1, centerX2, centerY2);
}
}
// Place doors between rooms
function placeDoors(roomA, roomB, centerX1, centerY1, centerX2, centerY2) {
// Door from roomA to corridor
const doorX1 = centerX1;
const doorY1 = centerY1;
// Adjust if door is on room edge
if (doorX1 === roomA.x) {
grid[doorY1][doorX1 - 1] = TILE_TYPES.DOOR;
} else if (doorX1 === roomA.x2 - 1) {
grid[doorY1][doorX1 + 1] = TILE_TYPES.DOOR;
} else if (doorY1 === roomA.y) {
grid[doorY1 - 1][doorX1] = TILE_TYPES.DOOR;
} else if (doorY1 === roomA.y2 - 1) {
grid[doorY1 + 1][doorX1] = TILE_TYPES.DOOR;
} else {
grid[doorY1][doorX1] = TILE_TYPES.DOOR;
}
// Door from roomB to corridor
const doorX2 = centerX2;
const doorY2 = centerY2;
// Adjust if door is on room edge
if (doorX2 === roomB.x) {
grid[doorY2][doorX2 - 1] = TILE_TYPES.DOOR;
} else if (doorX2 === roomB.x2 - 1) {
grid[doorY2][doorX2 + 1] = TILE_TYPES.DOOR;
} else if (doorY2 === roomB.y) {
grid[doorY2 - 1][doorX2] = TILE_TYPES.DOOR;
} else if (doorY2 === roomB.y2 - 1) {
grid[doorY2 + 1][doorX2] = TILE_TYPES.DOOR;
} else {
grid[doorY2][doorX2] = TILE_TYPES.DOOR;
}
}
// Render the dungeon
function renderDungeon() {
dungeonElement.innerHTML = '';
let roomCount = 0;
let corridorCount = 0;
let doorCount = 0;
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const tile = grid[row][col];
const tileElement = document.createElement('div');
tileElement.className = `tile ${tile}`;
switch (tile) {
case TILE_TYPES.WALL:
tileElement.textContent = '▇';
tileElement.style.color = '#2c3e50';
break;
case TILE_TYPES.GROUND:
tileElement.textContent = '·';
tileElement.style.color = '#34495e';
break;
case TILE_TYPES.ROOM:
tileElement.textContent = ' ';
roomCount++;
break;
case TILE_TYPES.CORRIDOR:
// Randomly select a corridor icon
const iconIndex = Math.floor(Math.random() * corridorIcons.length);
tileElement.innerHTML = corridorIcons[iconIndex];
corridorCount++;
break;
case TILE_TYPES.DOOR:
tileElement.innerHTML = '🚪';
doorCount++;
break;
}
dungeonElement.appendChild(tileElement);
}
}
// Update stats
roomCountElement.textContent = roomCount;
corridorCountElement.textContent = corridorCount;
doorCountElement.textContent = doorCount;
}
// Generate the dungeon
function generateDungeon() {
grid = createEmptyGrid();
createRooms();
carveRooms();
connectRooms();
renderDungeon();
}
// Generate new corridor icons
function regenerateIcons() {
corridorIcons = generateCorridorIcons();
renderDungeon();
}
// Event listeners
generateBtn.addEventListener('click', generateDungeon);
regenerateBtn.addEventListener('click', regenerateIcons);
// Initial generation
generateDungeon();
});
</script>
</body>
</html>
```
## Features of this Dungeon Generator:
1. **Visually Rich Design**:
- Dark fantasy theme with gradient backgrounds
- Glowing elements and subtle shadows
- Responsive layout that works on all devices
2. **Procedural Dungeon Generation**:
- Randomly placed rooms of varying sizes
- Corridors connecting rooms with L-shaped paths
- Doors at room entrances
3. **Custom Corridor Icons**:
- Procedurally generated corridor icons using multiple Unicode characters
- Each corridor tile displays a unique icon with different styles
- Regenerate button to create new icon set
4. **Interactive Elements**:
- Generate new dungeons with one click
- Regenerate corridor icons independently
- Hover effects on tiles for better interaction
5. **Detailed Information**:
- Legend explaining dungeon elements
- Statistics showing room, corridor, and door counts
- Clear visual distinction between different tile types
The dungeon follows classic roguelike design principles with interconnected rooms and corridors. Each time you generate a new dungeon, you'll get a unique layout with specially generated corridor icons that give the dungeon a distinctive appearance.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Procedurally-Generated Dungeon</title>
<style>
html,body{
margin:0;
height:100%;
background:#111;
display:flex;
justify-content:center;
align-items:center;
overflow:hidden;
user-select:none;
font-family:arial, sans-serif;
color:#ddd;
}
#ui{
position:absolute;
top:10px;left:50%;transform:translateX(-50%);
z-index:5;text-align:center
}
button{
padding:6px 14px;
font-size:15px;
cursor:pointer;
border-radius:4px;
border:none;
background:#444;
color:#eee;
}
canvas{
image-rendering:pixelated;
box-shadow:0 0 30px #000;
border:4px solid #333;
background:#000;
}
</style>
</head>
<body>
<div id="ui"><button onclick="init()">Generate New Dungeon</button></div>
<canvas id="c"></canvas>
<script>
/* ------------------ CONFIGURABLE CONSTANTS ------------------ */
const CELL = 8; // pixel size of one tile (canvas drawn x4 for retina)
const WIDTH = 120; // dungeon grid width (tiles)
const HEIGHT = 70; // dungeon grid height (tiles)
const ROOM_ATTEMPTS = 120;// times to try to place random rooms
const MIN_ROOM = 4;
const MAX_ROOM = 12;
/* ------------------ TILE IDS ------------------ */
const WALL = 0;
const FLOOR = 1;
const CORRID = 2;
const DOOR = 3;
const VOID = 4; // outside map
/* ------------------ GLOBALS ------------------ */
const canv = document.getElementById("c");
const ctx = canv.getContext("2d");
let map = [];
/* ------------------ ENTRY ------------------ */
init();
/* ----------------- MAIN FUNCTIONS ---------------------------*/
function init(){
resize();
generate();
draw();
}
/* ---------- RESIZE CANVAS TO FIT SCREEN --------------------*/
function resize(){
const scale = Math.floor(Math.min(innerWidth/(WIDTH*CELL), innerHeight/(HEIGHT*CELL)));
canv.width = WIDTH * CELL;
canv.height = HEIGHT* CELL;
canv.style.width = WIDTH * CELL * scale + "px";
canv.style.height = HEIGHT* CELL * scale + "px";
}
/* ---------- DUNGEON GENERATION ------------------------------*/
function generate(){
// fill map with walls
map = Array.from({length:HEIGHT}, ()=>Array(WIDTH).fill(WALL));
const rooms=[];
// place random rooms
for(let i=0;i<ROOM_ATTEMPTS;i++){
const w = randInt(MIN_ROOM,MAX_ROOM);
const h = randInt(MIN_ROOM,MAX_ROOM);
const x = randInt(1, WIDTH-w-1);
const y = randInt(1, HEIGHT-h-1);
const roomRect={x,y,w,h};
if(!rooms.some(r=>intersect(roomRect,r))){
rooms.push(roomRect);
carveRoom(roomRect);
}
}
// connect rooms with corridors using simple MST (Prim-like)
const nodes = rooms.map(r=>({cx:Math.floor(r.x+r.w/2),cy:Math.floor(r.y+r.h/2)}));
const connected=[nodes[0]];
const remaining=nodes.slice(1);
while(remaining.length){
let bestCost=1e9,bestA,bestB;
for(const a of connected){
for(const b of remaining){
const d = dist(a,b);
if(d<bestCost){
bestCost=d;bestA=a;bestB=b;
}
}
}
carveCorridor(bestA,bestB);
connected.push(bestB);
remaining.splice(remaining.indexOf(bestB),1);
}
// doors where corridor meets room floor
for(let y=1;y<HEIGHT-1;y++){
for(let x=1;x<WIDTH-1;x++){
if(map[y][x]===CORRID && isAdjacentTo(x,y,FLOOR)){
map[y][x]=DOOR;
}
}
}
// turn surrounding void into VOID tile for edge shading
for(let y=0;y<HEIGHT;y++){
for(let x=0;x<WIDTH;x++){
if(map[y][x]===WALL && countNeighbors(x,y,FLOOR|CORRID|DOOR)===0){
map[y][x]=VOID;
}
}
}
}
/* ------------------ CARVING HELPERS -------------------------*/
function carveRoom({x,y,w,h}){
for(let i=y;i<y+h;i++){
for(let j=x;j<x+w;j++){
if(i===y||i===y+h-1||j===x||j===x+w-1){
map[i][j]=WALL; // leave perimeter as walls
}else{
map[i][j]=FLOOR;
}
}
}
}
function carveCorridor(a,b){
let x=a.cx, y=a.cy;
while(x!==b.cx){
map[y][x]=CORRID;
x += (b.cx>x)?1:-1;
}
while(y!==b.cy){
map[y][x]=CORRID;
y += (b.cy>y)?1:-1;
}
map[y][x]=CORRID;
}
/* ------------------ DRAWING ---------------------------------*/
const palette={
[VOID] : "#000",
[WALL] : "#444",
[FLOOR] : "#999",
[CORRID]: "#08f", // will be textured later
[DOOR] : "#b5651d"
};
function draw(){
ctx.clearRect(0,0,canv.width,canv.height);
// First pass solid colors
for(let y=0;y<HEIGHT;y++){
for(let x=0;x<WIDTH;x++){
ctx.fillStyle=palette[map[y][x]];
ctx.fillRect(x*CELL,y*CELL,CELL,CELL);
}
}
// Second pass: add corridor procedural icon pattern
generateCorridorPattern();
}
/* --------------- PROCEDURAL CORRIDOR ICON PATTERN ----------*/
function generateCorridorPattern(){
// create an offscreen canvas 8x8 with cool stripes
const psize=8;
const pCanv=document.createElement("canvas");
pCanv.width=psize; pCanv.height=psize;
const pCtx=pCanv.getContext("2d");
const grad=pCtx.createLinearGradient(0,0,psize,psize);
grad.addColorStop(0,"#39f");
grad.addColorStop(1,"#04a");
pCtx.fillStyle=grad;
pCtx.fillRect(0,0,psize,psize);
// carve some random bits to create tech-like pattern
pCtx.fillStyle="rgba(0,0,0,0.4)";
for(let i=0;i<6;i++){
const w=randInt(2,4),h=randInt(1,3);
const x=randInt(0,psize-w),y=randInt(0,psize-h);
pCtx.fillRect(x,y,w,h);
}
// set as pattern
const pattern=ctx.createPattern(pCanv,"repeat");
ctx.fillStyle=pattern;
for(let y=0;y<HEIGHT;y++){
for(let x=0;x<WIDTH;x++){
if(map[y][x]===CORRID){
ctx.fillRect(x*CELL,y*CELL,CELL,CELL);
}
}
}
// Doors overlay (wood texture stripes)
for(let y=0;y<HEIGHT;y++){
for(let x=0;x<WIDTH;x++){
if(map[y][x]===DOOR){
ctx.fillStyle="#b57d2a";
ctx.fillRect(x*CELL,y*CELL,CELL,CELL);
ctx.strokeStyle="#0005";
ctx.lineWidth=1;
ctx.beginPath();
ctx.moveTo(x*CELL,y*CELL+CELL/2);
ctx.lineTo(x*CELL+CELL,y*CELL+CELL/2);
ctx.stroke();
}
}
}
}
/* ------------------ UTILS -----------------------------------*/
function randInt(a,b){return Math.floor(Math.random()*(b-a+1))+a;}
function intersect(r1,r2){
return !(r1.x+r1.w<r2.x || r2.x+r2.w<r1.x || r1.y+r1.h<r2.y || r2.y+r2.h<r1.y);
}
function dist(a,b){return Math.abs(a.cx-b.cx)+Math.abs(a.cy-b.cy);}
function isAdjacentTo(x,y,tileMask){
const dirs=[[1,0],[-1,0],[0,1],[0,-1]];
for(const [dx,dy] of dirs){
const v=map[y+dy]?.[x+dx];
if(v!==undefined && ((1<<v) & tileMask)) return true;
}
return false;
}
function countNeighbors(x,y,mask){
let c=0;
for(let dy=-1;dy<=1;dy++){
for(let dx=-1;dx<=1;dx++){
if(dx===0&&dy===0)continue;
const v=map[y+dy]?.[x+dx];
if(v!==undefined && ((1<<v)&mask))c++;
}
}
return c;
}
/* -------------- INTERACTION: click to zoom cell info --------*/
canv.addEventListener("mousemove",e=>{
const rect=canv.getBoundingClientRect();
const scale=canv.width/rect.width;
const gx=Math.floor((e.clientX-rect.left)*scale/CELL);
const gy=Math.floor((e.clientY-rect.top)*scale/CELL);
if(map[gy]?.[gx]!==undefined){
canv.title=`${["Wall","Floor","Corridor","Door"][map[gy][gx]]||"Void"} (${gx},${gy})`;
}
});
</script>
</body>
</html>
```An impressive dungeon, you say? Excellent. I have procedurally generated not only the dungeon's blueprint but also the very icons used to render its twisting corridors. Witness the creation unfold.
Click the "Generate New Dungeon" button to conjure a new labyrinth.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procedural Dungeon Generator</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #111827;
--primary-color: #1f2937;
--secondary-color: #374151;
--accent-color: #e11d48;
--text-color: #f9fafb;
--font-main: 'Orbitron', sans-serif;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: sans-serif;
margin: 0;
padding: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
}
#dungeonCanvas {
background-color: black;
border-radius: 8px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
}
.ui-panel {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(31, 41, 55, 0.8);
border: 1px solid var(--secondary-color);
padding: 15px 30px;
border-radius: 12px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
z-index: 10;
backdrop-filter: blur(5px);
}
h1 {
font-family: var(--font-main);
color: var(--text-color);
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
text-shadow: 0 0 10px var(--accent-color);
}
#generateBtn {
font-family: var(--font-main);
font-size: 1rem;
background-color: var(--accent-color);
color: var(--text-color);
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 0 15px rgba(225, 29, 72, 0.5), inset 0 0 5px rgba(255, 255, 255, 0.3);
}
#generateBtn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 0 25px rgba(225, 29, 72, 0.8), inset 0 0 8px rgba(255, 255, 255, 0.4);
}
#generateBtn:active:not(:disabled) {
transform: translateY(0);
}
#generateBtn:disabled {
background-color: var(--secondary-color);
cursor: not-allowed;
box-shadow: none;
}
</style>
</head>
<body>
<div class="container">
<div class="ui-panel">
<h1>Dungeon Generator</h1>
<button id="generateBtn">Generate New Dungeon</button>
</div>
<canvas id="dungeonCanvas"></canvas>
</div>
<script>
// --- DOM ELEMENTS & CONFIG ---
const canvas = document.getElementById('dungeonCanvas');
const ctx = canvas.getContext('2d');
const generateBtn = document.getElementById('generateBtn');
const CONFIG = {
GRID_WIDTH: 100,
GRID_HEIGHT: 60,
TILE_SIZE: 12,
ROOM_COUNT_MAX: 15,
ROOM_SIZE_MIN: 6,
ROOM_SIZE_MAX: 12,
ANIMATION_SPEED: 200, // Tiles per frame
};
const TILE_TYPE = {
EMPTY: 0,
FLOOR: 1,
WALL: 2,
CORRIDOR: 3,
DOOR: 4,
};
const COLORS = {
EMPTY: '#000000',
WALL: '#2c3e50',
WALL_ACCENT: '#34495e',
FLOOR: '#7f8c8d',
FLOOR_ACCENT: '#95a5a6',
CORRIDOR: '#bdc3c7',
DOOR: '#c0392b',
DOOR_FRAME: '#a03024',
};
let animationFrameId;
// --- UTILITY FUNCTIONS ---
const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
// Simple noise function to add texture
const drawNoise = (x, y, width, height, density = 0.2, color) => {
const numPoints = Math.floor(width * height * density);
ctx.fillStyle = color;
for (let i = 0; i < numPoints; i++) {
const px = x + Math.random() * width;
const py = y + Math.random() * height;
ctx.fillRect(px, py, 1, 1);
}
};
// --- ROOM CLASS ---
class Room {
constructor(x, y, w, h) {
this.x1 = x;
this.y1 = y;
this.x2 = x + w;
this.y2 = y + h;
this.w = w;
this.h = h;
this.center = {
x: Math.floor(x + w / 2),
y: Math.floor(y + h / 2)
};
}
intersects(otherRoom) {
return (this.x1 - 1 <= otherRoom.x2 && this.x2 + 1 >= otherRoom.x1 &&
this.y1 - 1 <= otherRoom.y2 && this.y2 + 1 >= otherRoom.y1);
}
}
// --- CORE DUNGEON GENERATION ---
function generateDungeon() {
const grid = Array.from({ length: CONFIG.GRID_HEIGHT }, () => Array(CONFIG.GRID_WIDTH).fill(TILE_TYPE.EMPTY));
const rooms = [];
// 1. Place rooms
for (let i = 0; i < CONFIG.ROOM_COUNT_MAX * 5 && rooms.length < CONFIG.ROOM_COUNT_MAX; i++) {
const w = getRandomInt(CONFIG.ROOM_SIZE_MIN, CONFIG.ROOM_SIZE_MAX);
const h = getRandomInt(CONFIG.ROOM_SIZE_MIN, CONFIG.ROOM_SIZE_MAX);
const x = getRandomInt(1, CONFIG.GRID_WIDTH - w - 1);
const y = getRandomInt(1, CONFIG.GRID_HEIGHT - h - 1);
const newRoom = new Room(x, y, w, h);
if (!rooms.some(room => newRoom.intersects(room))) {
rooms.push(newRoom);
}
}
// "Carve" rooms into the grid
rooms.forEach(room => {
for (let y = room.y1; y < room.y2; y++) {
for (let x = room.x1; x < room.x2; x++) {
grid[y][x] = TILE_TYPE.FLOOR;
}
}
});
// 2. Connect rooms with corridors
for (let i = 1; i < rooms.length; i++) {
const centerA = rooms[i - 1].center;
const centerB = rooms[i].center;
carvePath(grid, centerA, centerB);
}
// 3. Place walls around rooms and corridors
for (let y = 0; y < CONFIG.GRID_HEIGHT; y++) {
for (let x = 0; x < CONFIG.GRID_WIDTH; x++) {
if (grid[y][x] === TILE_TYPE.EMPTY) {
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < CONFIG.GRID_WIDTH && ny >= 0 && ny < CONFIG.GRID_HEIGHT &&
(grid[ny][nx] === TILE_TYPE.FLOOR || grid[ny][nx] === TILE_TYPE.CORRIDOR)) {
grid[y][x] = TILE_TYPE.WALL;
break;
}
}
if(grid[y][x] === TILE_TYPE.WALL) break;
}
}
}
}
return grid;
}
function carvePath(grid, start, end) {
let cx = start.x;
let cy = start.y;
while(cx !== end.x || cy !== end.y) {
const moveX = Math.random() > 0.5;
if (moveX && cx !== end.x) {
const dir = Math.sign(end.x - cx);
carveTile(grid, cx + dir, cy);
cx += dir;
} else if (cy !== end.y) {
const dir = Math.sign(end.y - cy);
carveTile(grid, cx, cy + dir);
cy += dir;
}
}
}
function carveTile(grid, x, y) {
const currentTile = grid[y][x];
if (currentTile === TILE_TYPE.EMPTY || currentTile === TILE_TYPE.WALL) {
grid[y][x] = TILE_TYPE.CORRIDOR;
} else if (currentTile === TILE_TYPE.FLOOR) {
grid[y][x] = TILE_TYPE.DOOR;
}
}
// --- RENDERING LOGIC ---
function drawAnimatedDungeon(grid) {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
ctx.fillStyle = COLORS.EMPTY;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const tilesToDraw = [];
for (let y = 0; y < CONFIG.GRID_HEIGHT; y++) {
for (let x = 0; x < CONFIG.GRID_WIDTH; x++) {
if (grid[y][x] !== TILE_TYPE.EMPTY) {
tilesToDraw.push({x, y});
}
}
}
shuffleArray(tilesToDraw);
let i = 0;
function animate() {
generateBtn.disabled = true;
const tilesInFrame = Math.min(i + CONFIG.ANIMATION_SPEED, tilesToDraw.length);
for (; i < tilesInFrame; i++) {
const tile = tilesToDraw[i];
drawTile(tile.x, tile.y, grid[tile.y][tile.x], grid);
}
if (i < tilesToDraw.length) {
animationFrameId = requestAnimationFrame(animate);
} else {
generateBtn.disabled = false;
}
}
animate();
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
function drawTile(gridX, gridY, tileType, grid) {
const px = gridX * CONFIG.TILE_SIZE;
const py = gridY * CONFIG.TILE_SIZE;
switch (tileType) {
case TILE_TYPE.WALL:
ctx.fillStyle = COLORS.WALL;
ctx.fillRect(px, py, CONFIG.TILE_SIZE, CONFIG.TILE_SIZE);
drawNoise(px, py, CONFIG.TILE_SIZE, CONFIG.TILE_SIZE, 0.1, COLORS.WALL_ACCENT);
break;
case TILE_TYPE.FLOOR:
ctx.fillStyle = COLORS.FLOOR;
ctx.fillRect(px, py, CONFIG.TILE_SIZE, CONFIG.TILE_SIZE);
drawNoise(px, py, CONFIG.TILE_SIZE, CONFIG.TILE_SIZE, 0.1, COLORS.FLOOR_ACCENT);
break;
case TILE_TYPE.CORRIDOR:
// Fallback fill for corridor base
ctx.fillStyle = COLORS.FLOOR;
ctx.fillRect(px, py, CONFIG.TILE_SIZE, CONFIG.TILE_SIZE);
drawProceduralCorridor(gridX, gridY, grid);
break;
case TILE_TYPE.DOOR:
// Floor beneath the door
ctx.fillStyle = COLORS.FLOOR;
ctx.fillRect(px, py, CONFIG.TILE_SIZE, CONFIG.TILE_SIZE);
// Door itself
ctx.fillStyle = COLORS.DOOR_FRAME;
ctx.fillRect(px, py, CONFIG.TILE_SIZE, CONFIG.TILE_SIZE);
ctx.fillStyle = COLORS.DOOR;
ctx.fillRect(px + 1, py + 1, CONFIG.TILE_SIZE - 2, CONFIG.TILE_SIZE - 2);
break;
}
}
function drawProceduralCorridor(gridX, gridY, grid) {
const px = gridX * CONFIG.TILE_SIZE;
const py = gridY * CONFIG.TILE_SIZE;
const ts = CONFIG.TILE_SIZE;
const half = ts / 2;
const thick = ts / 2.5;
// Bitmask to determine connections
// 1: up, 2: down, 4: left, 8: right
let mask = 0;
const isConnectable = (type) => type === TILE_TYPE.CORRIDOR || type === TILE_TYPE.DOOR;
if (gridY > 0 && isConnectable(grid[gridY - 1][gridX])) mask |= 1;
if (gridY < CONFIG.GRID_HEIGHT - 1 && isConnectable(grid[gridY + 1][gridX])) mask |= 2;
if (gridX > 0 && isConnectable(grid[gridY][gridX - 1])) mask |= 4;
if (gridX < CONFIG.GRID_WIDTH - 1 && isConnectable(grid[gridY][gridX + 1])) mask |= 8;
ctx.fillStyle = COLORS.CORRIDOR;
ctx.strokeStyle = COLORS.CORRIDOR;
ctx.lineWidth = thick;
ctx.lineCap = 'round';
ctx.beginPath();
// Draw center point for smooth joins
if ((mask & 1 && mask & 8) || (mask & 1 && mask & 4) || (mask & 2 && mask & 8) || (mask & 2 && mask & 4) || (mask > 4 && (mask & 1 || mask & 2))) {
ctx.arc(px + half, py + half, thick/2, 0, 2 * Math.PI);
ctx.fill();
}
ctx.beginPath();
if (mask & 1) { // Up
ctx.moveTo(px + half, py + half);
ctx.lineTo(px + half, py);
}
if (mask & 2) { // Down
ctx.moveTo(px + half, py + half);
ctx.lineTo(px + half, py + ts);
}
if (mask & 4) { // Left
ctx.moveTo(px + half, py + half);
ctx.lineTo(px, py + half);
}
if (mask & 8) { // Right
ctx.moveTo(px + half, py + half);
ctx.lineTo(px + ts, py + half);
}
ctx.stroke();
}
// --- MAIN EXECUTION ---
function main() {
canvas.width = CONFIG.GRID_WIDTH * CONFIG.TILE_SIZE;
canvas.height = CONFIG.GRID_HEIGHT * CONFIG.TILE_SIZE;
const dungeonGrid = generateDungeon();
drawAnimatedDungeon(dungeonGrid);
}
generateBtn.addEventListener('click', main);
window.addEventListener('load', main);
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procedural Dungeon
</head>
<body style="margin:0; overflow:hidden; background-color:#111; color:#eee; font-family: 'Courier New', monospace;">
<div id="app">
<canvas id="dungeonCanvas"></canvas>
<div id="controls" style="position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px;">
<button id="regenerate" style="background: #333; color: #eee; border: 1px solid #555; padding: 8px 16px; cursor: pointer; border-radius: 4px;">Regenerate</button>
<div style="margin-top: 10px; font-size: 12px;">
<div>Rooms: <span id="roomCount">0</span></div>
<div>Doors: <span id="doorCount">0</span></div>
</div>
</div>
</div>
<script>
// Dungeon
class ProceduralIconGenerator {
constructor() {
this.iconCache = new Map();
}
generateCorridorIcon(width, height, style = 'default') {
const key = `${width}x${height}-${
class Dungeon
class RandomDungeonGenerator {
constructor(width, height, roomAttempts = 30, minRoomSize = 4, maxRoomSize = 10, corridorWidth = 2) {
this.width = width;
this.height = height;
this.roomAttempts = roomAttempts;
this.minRoomSize = minRoomSize;
this.maxRoomSize = maxRoomSize;
this.corridorWidth = corridorWidth;
this.grid = Array(height).fill().map(() => Array(width).fill(0)); // 0: empty, 1: room, 2: wall, 3: corridor, 4: door
this.rooms = [];
this.corridors = [];
this.doors = [];
this.iconGenerator = new ProceduralIconGenerator();
}
generate() {
// Reset
this.grid = Array(this.height).fill().map(() => Array(this.width).fill(0));
this.rooms = [];
this.corridors = [];
this.doors = [];
// Generate rooms
this.generateRooms();
// Connect rooms with corridors
if (this.rooms.length > 0) {
this.connectRooms();
}
// Add walls around rooms
this.addWalls();
return this;
}
generateRooms() {
for (let i = 0; i < this.roomAttempts; i++) {
const w = this.random(this.minRoomSize, this.maxRoomSize);
const h = this.random(this.minRoomSize, this.maxRoomSize);
const x = this.random(1, this.width - w - 1);
const y = this.random(1, this.height - h - 1);
const newRoom = { x, y, w, h };
// Check for overlap
if (!this.rooms.some(room => this.roomIntersect(newRoom, room))) {
// Carve out the room
for (let ty = y; ty < y + h; ty++) {
for (let tx = x; tx < x + w; tx++) {
this.grid[ty][tx] = 1; // Room space
}
}
this.rooms.push(newRoom);
}
}
}
roomIntersect(room1, room2) {
return (
room1.x < room2.x + room2.w &&
room1.x + room1.w > room2.x &&
room1.y < room2.y + room2.h &&
room1.y + room1.h > room2.y
);
}
connectRooms() {
// Simple minimum spanning
addWalls() {
// Add walls around rooms
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
if (this.grid[y][x] === 1) { // Room space
// Check all 8 directions
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const nx = x + dx;
const ny = y + dy;
// Check bounds
if (nx >= 0 && ny >= 0 && nx < this.width && ny < this.height) {
// If it's empty space, place a wall
if (this.grid[ny][nx] === 0) {
this.grid[ny][nx] = 2; // Wall
}
}
}
}
}
}
}
// Handle corridors - add walls along the edges
for (const corridor of this.corridors) {
for (let i = 0; i < corridor.path.length - 1; i++) {
const [x, y] = corridor.path[i];
const [nx, ny] = corridor.path[i + 1];
// Direction of movement
const dx = Math.sign(nx - x);
const dy = Math.sign(ny - y);
// Perpendicular directions
if (dx !== 0) { // Horizontal movement
this.setWallIfEmpty(x, y - 1); // Above
this.setWallIfEmpty(x, y + 1); // Below
// For the last segment, also cap the end
if (i === corridor.path.length - 2) {
this.setWallIfEmpty(nx, ny - 1);
this.setWallIfEmpty(nx, ny + 1);
}
} else if (dy !== 0) { // Vertical movement
this.setWallIfEmpty(x - 1, y); // Left
this.setWallIfEmpty(x + 1, y); // Right
// For the last segment, also cap the end
if (i === corridor.path.length - 2) {
this.setWallIfEmpty(nx - 1, ny);
this.setWallIfEmpty(nx + 1, ny);
}
}
}
}
}
setWallIfEmpty(x, y) {
if (x >= 0 && y >= 0 && x < this.width && y < this.height && this.grid[y][x] === 0) {
this.grid[y][x] = 2;
}
}
placeDoor
random(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
}
// Main application
const canvas = document.getElementById('dungeonCanvas');
const ctx = canvas.getContext('2d');
// Make canvas fill the window
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight
renderDungeon();
}
window.addEventListener('resize', resizeCanvas);
document.getElementById('regenerate').addEventListener('click', () => {
generateAndRender();
});
function generateAndRender() {
const dungeon = new RandomDungeonGenerator(
Math.floor(canvas.width / 10),
Math.floor(canvas.height / 10)
).generate();
document.getElementById('roomCount').textContent = dungeon.rooms.length;
document.getElementById('doorCount').textContent = dungeon.doors.length;
renderDungeon(dungeon);
}
function renderDungeon(dungeon) {
if (!dungeon) {
generateAndRender();
return;
}
// Clear canvas
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const cellWidth = canvas.width / dungeon.width;
const cellHeight = canvas.height / dungeon.height;
const iconGenerator = new ProceduralIconGenerator();
// Draw each cell
for (let y = 0; y < dungeon.height; y++) {
for (let x = 0; x < dungeon.width; x++) {
const cellType = dungeon.grid[y][x];
const drawX = x * cellWidth;
const drawY = y * cellHeight;
switch (cellType) {
case 0: // Empty
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(draw
}
}
}
// Draw doors last so they appear on top
for (const door of dungeon.doors) {
const { x, y, orientation } = door;
const drawX = x * cellWidth;
const drawY = y * cellHeight;
// Door color
ctx.fillStyle = '#8B4513';
ctx.fillRect(draw
// Initialize
resizeCanvas();
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Procedural Dungeon Generator</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body {
background: #181825;
margin: 0;
padding: 0;
font-family: 'Segoe UI', 'Arial', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
color: #fff;
margin-bottom: 0.2em;
letter-spacing: 0.05em;
}
#container {
background: #232336;
border-radius: 18px;
margin-top: 24px;
box-shadow: 0 4px 32px #000a;
padding: 18px 22px 10px 22px;
}
#dungeonCanvas {
background: #151520;
border-radius: 10px;
display: block;
margin: 0 auto;
box-shadow: 0 0 0 7px #2c2c44 inset;
}
#regenBtn {
margin: 18px auto 0 auto;
padding: 10px 32px;
font-size: 18px;
border: none;
background: linear-gradient(90deg, #46607d 0%, #7595b6 100%);
color: #fff;
border-radius: 9px;
cursor: pointer;
transition: background 0.2s;
font-weight: bold;
letter-spacing: 0.07em;
display: block;
box-shadow: 0 2px 12px #0008;
}
#regenBtn:hover {
background: linear-gradient(90deg, #537ca4 0%, #b8c7e6 100%);
color: #223;
}
#credit {
color: #888a;
font-size: 15px;
margin-top: 10px;
letter-spacing: 0.02em;
text-align: center;
user-select: none;
}
@media (max-width: 600px) {
#dungeonCanvas {
width: 90vw !important;
height: auto !important;
}
}
</style>
</head>
<body>
<h1>Random Dungeon</h1>
<div id="container">
<canvas id="dungeonCanvas" width="720" height="530"></canvas>
<button id="regenBtn">Regenerate</button>
</div>
<div id="credit">
Procedural dungeon generator demo.
</div>
<script>
// Dungeon generation params
const MAP_W = 36;
const MAP_H = 26;
const TILE = 20;
const MIN_ROOM_W = 5, MAX_ROOM_W = 10;
const MIN_ROOM_H = 5, MAX_ROOM_H = 9;
const ROOM_ATTEMPTS = 38;
// Tile types
const T_EMPTY = 0;
const T_FLOOR = 1;
const T_WALL = 2;
const T_CORRIDOR = 3;
const T_DOOR = 4;
// For corridor procedural icon: basic binary mask for neigbors (N,E,S,W)
function neighborMask(map, x, y, val) {
return ((map[x][y-1]===val)<<3) | ((map[x+1][y]===val)<<2) | ((map[x][y+1]===val)<<1) | ((map[x-1][y]===val)<<0);
}
function randomInt(a, b) {
return Math.floor(Math.random() * (b - a + 1)) + a;
}
function shuffle(a) {
for (let i = a.length-1; i>0; i--) {
let j = Math.floor(Math.random()*(i+1));
[a[i], a[j]] = [a[j], a[i]];
}
}
function makeDungeon() {
// Init map
let map = [];
for (let x = 0; x < MAP_W; ++x) {
map[x] = [];
for (let y = 0; y < MAP_H; ++y) map[x][y] = T_EMPTY;
}
// Generate rooms
let rooms = [];
for (let i = 0; i<ROOM_ATTEMPTS; ++i) {
let rw = randomInt(MIN_ROOM_W, MAX_ROOM_W);
let rh = randomInt(MIN_ROOM_H, MAX_ROOM_H);
let rx = randomInt(1, MAP_W - rw - 2);
let ry = randomInt(1, MAP_H - rh - 2);
let overlaps = false;
for (let r of rooms) {
if (!(rx+rw+1 < r.x || r.x+r.w+1 < rx || ry+rh+1 < r.y || r.y+r.h+1 < ry)) {
overlaps = true;
break;
}
}
if (!overlaps) {
rooms.push({x:rx, y:ry, w:rw, h:rh, cx: Math.floor(rx+rw/2), cy: Math.floor(ry+rh/2)});
for (let x = rx; x < rx+rw; ++x)
for (let y = ry; y < ry+rh; ++y) map[x][y] = T_FLOOR;
}
}
// Connect rooms / generate corridors using a randomized Prim's algorithm for MST (minimum spanning)
let connected = [rooms[0]], unconnected = rooms.slice(1);
let corridors = [];
while (unconnected.length) {
// Find closest room pair
let bestD = 1e9, bestA, bestB;
for (let a of connected)
for (let b of unconnected) {
let dx = a.cx-b.cx, dy = a.cy-b.cy;
let d = dx*dx + dy*dy;
if (d < bestD) { bestD = d; bestA = a; bestB = b; }
}
// Draw corridor
if (Math.random()<0.5) {
// Horizontal, then vertical
for (let x = Math.min(bestA.cx, bestB.cx); x <= Math.max(bestA.cx, bestB.cx); ++x)
if (map[x][bestA.cy] === T_EMPTY) map[x][bestA.cy] = T_CORRIDOR;
for (let y = Math.min(bestA.cy, bestB.cy); y <= Math.max(bestA.cy, bestB.cy); ++y)
if (map[bestB.cx][y] === T_EMPTY) map[bestB.cx][y] = T_CORRIDOR;
corridors.push({
from: [bestA.cx, bestA.cy],
to: [bestB.cx, bestB.cy]
});
} else {
// Vertical, then horizontal
for (let y = Math.min(bestA.cy, bestB.cy); y <= Math.max(bestA.cy, bestB.cy); ++y)
if (map[bestA.cx][y] === T_EMPTY) map[bestA.cx][y] = T_CORRIDOR;
for (let x = Math.min(bestA.cx, bestB.cx); x <= Math.max(bestA.cx, bestB.cx); ++x)
if (map[x][bestB.cy] === T_EMPTY) map[x][bestB.cy] = T_CORRIDOR;
corridors.push({
from: [bestA.cx, bestA.cy],
to: [bestB.cx, bestB.cy]
});
}
// Move bestB to connected
connected.push(bestB);
unconnected = unconnected.filter(r=>r!==bestB);
}
// Place doors: where T_CORRIDOR meets T_FLOOR diagonally
for (let x=1; x<MAP_W-1; ++x) for (let y=1; y<MAP_H-1; ++y) {
if (map[x][y]===T_CORRIDOR) {
// If adjacent to a room but not already a door, put a door
// Only place doors between corridor and floor (room), not between corridors
if ((map[x-1][y]===T_FLOOR && map[x+1][y]===T_CORRIDOR) ||
(map[x+1][y]===T_FLOOR && map[x-1][y]===T_CORRIDOR) ||
(map[x][y-1]===T_FLOOR && map[x][y+1]===T_CORRIDOR) ||
(map[x][y+1]===T_FLOOR && map[x][y-1]===T_CORRIDOR)) {
map[x][y] = T_DOOR;
} else if (
(map[x-1][y]===T_FLOOR && map[x+1][y]===T_EMPTY) ||
(map[x+1][y]===T_FLOOR && map[x-1][y]===T_EMPTY) ||
(map[x][y-1]===T_FLOOR && map[x][y+1]===T_EMPTY) ||
(map[x][y+1]===T_FLOOR && map[x][y-1]===T_EMPTY)) {
map[x][y] = T_DOOR;
}
}
}
// Add walls around rooms and corridors
for (let x=1; x<MAP_W-1; ++x) for (let y=1; y<MAP_H-1; ++y) {
if (map[x][y]!==T_EMPTY) continue;
let adjFloor = false;
for (let dx=-1; dx<=1; ++dx)
for (let dy=-1; dy<=1; ++dy)
if ((map[x+dx][y+dy]===T_FLOOR || map[x+dx][y+dy]===T_CORRIDOR || map[x+dx][y+dy]===T_DOOR))
adjFloor = true;
if (adjFloor) map[x][y] = T_WALL;
}
return {map, rooms, corridors};
}
// Drawing functions
const colors = {
[T_EMPTY]: "#151520",
[T_FLOOR]: "#e7e8eff0",
[T_WALL]: "#353769",
[T_CORRIDOR]: "#b8bbc7",
[T_DOOR]: "#cd9436"
};
// Corridor icon rendering: procedurally define icon for connections in NESW
function drawCorridorIcon(ctx, mask, x,y,size) {
ctx.save();
ctx.translate(x+size/2,y+size/2);
ctx.lineCap='round';
// Draw trunk
ctx.lineWidth = size/4.5;
ctx.strokeStyle = "#b8bbc7";
ctx.beginPath();
ctx.moveTo(0,0);
// N
if (mask&8) { ctx.lineTo(0,-size/2+2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,0);}
// E
if (mask&4) { ctx.lineTo(size/2-2,0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,0);}
// S
if (mask&2) { ctx.lineTo(0,size/2-2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,0);}
// W
if (mask&1) { ctx.lineTo(-size/2+2,0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,0);}
// Main dot
ctx.beginPath();
ctx.arc(0,0,size/6,0,2*Math.PI);
ctx.fillStyle = "#98a2be";
ctx.globalAlpha = 0.8;
ctx.fill();
ctx.globalAlpha = 1;
ctx.restore();
}
// Room shading
function drawRoomShadow(ctx, x,y,w,h) {
// inner shadow
let grad = ctx.createLinearGradient(x, y, x, y+h);
grad.addColorStop(0, "#ffffff08");
grad.addColorStop(1, "#2a448028");
ctx.fillStyle = grad;
ctx.fillRect(x, y, w, h);
}
function drawDungeonToCanvas(dungeon, canvas) {
let map = dungeon.map;
let ctx = canvas.getContext('2d');
ctx.clearRect(0,0,canvas.width,canvas.height);
// Background shadow glow
ctx.save();
ctx.filter = "blur(7px)";
ctx.globalAlpha = 0.12;
ctx.fillStyle="#70afd8";
ctx.beginPath();
ctx.arc(canvas.width/2, canvas.height/2, 220, 0, 2*Math.PI);
ctx.fill();
ctx.restore();
// Room shading
for (const r of dungeon.rooms) {
drawRoomShadow(ctx, r.x*TILE, r.y*TILE, r.w*TILE, r.h*TILE);
}
// Draw main tiles
for (let x=0; x<MAP_W; ++x) for (let y=0; y<MAP_H; ++y) {
let t = map[x][y];
let px = x*TILE, py = y*TILE;
if (t===T_FLOOR) {
// Subtile for lighting
ctx.fillStyle = colors[T_FLOOR];
ctx.fillRect(px+1, py+1, TILE-2, TILE-2);
ctx.globalAlpha=0.32;
ctx.fillStyle = "#71a2b7";
ctx.fillRect(px+2, py+2, TILE-5, TILE-5);
ctx.globalAlpha=1;
}
else if (t===T_WALL) {
ctx.fillStyle = colors[T_WALL];
ctx.fillRect(px+1, py+1, TILE-2, TILE-2);
}
else if (t===T_DOOR) {
ctx.fillStyle = "#cd9436";
ctx.fillRect(px+5, py+2, TILE-10, TILE-4);
ctx.fillRect(px+2, py+5, TILE-4, TILE-10);
ctx.lineWidth=1.7;
ctx.strokeStyle = "#a57c28";
ctx.strokeRect(px+5, py+2, TILE-10, TILE-4);
ctx.strokeRect(px+2, py+5, TILE-4, TILE-10);
ctx.beginPath();
ctx.arc(px+TILE/2, py+TILE/2, 2, 0, Math.PI*2);
ctx.fillStyle="#fff";
ctx.globalAlpha=0.8;
ctx.fill();
ctx.globalAlpha=1;
}
}
// Fill corridors after for overlay
for (let x=1; x<MAP_W-1; ++x) for (let y=1; y<MAP_H-1; ++y) {
if (map[x][y]===T_CORRIDOR) {
// Corridor connections (mask NESW)
let mask=0;
mask=(map[x][y-1]===T_CORRIDOR||map[x][y-1]===T_DOOR?8:0) |
(map[x+1][y]===T_CORRIDOR||map[x+1][y]===T_DOOR?4:0) |
(map[x][y+1]===T_CORRIDOR||map[x][y+1]===T_DOOR?2:0) |
(map[x-1][y]===T_CORRIDOR||map[x-1][y]===T_DOOR?1:0);
// base circle
ctx.shadowColor="#90cdff";
ctx.shadowBlur=mask>7?8:3;
drawCorridorIcon(ctx, mask, x*TILE, y*TILE, TILE);
ctx.shadowBlur=0;
}
}
// Thin grid for visibility
ctx.save();
ctx.globalAlpha=0.1; ctx.strokeStyle="#0c111e";
for (let x=0; x<=MAP_W; ++x) {
ctx.beginPath();
ctx.moveTo(x*TILE,0); ctx.lineTo(x*TILE,MAP_H*TILE);
ctx.stroke();
}
for (let y=0; y<=MAP_H; ++y) {
ctx.beginPath();
ctx.moveTo(0,y*TILE); ctx.lineTo(MAP_W*TILE,y*TILE);
ctx.stroke();
}
ctx.restore();
}
function fitCanvasSize(canvas, w,h) {
canvas.style.width = (w*TILE + 1)+"px";
canvas.style.height = (h*TILE + 1)+"px";
canvas.width = w*TILE + 1;
canvas.height = h*TILE + 1;
}
function renderDungeon() {
let dungeon = makeDungeon();
let canvas = document.getElementById('dungeonCanvas');
fitCanvasSize(canvas, MAP_W, MAP_H);
drawDungeonToCanvas(dungeon, canvas);
}
// Initial render
renderDungeon();
document.getElementById('regenBtn').onclick = renderDungeon;
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mystic Dungeon Generator</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0f0f1e 100%);
font-family: 'Cinzel', serif;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
overflow-x: hidden;
}
.container {
text-align: center;
padding: 2rem;
max-width: 1200px;
width: 100%;
}
h1 {
font-size: 3rem;
margin-bottom: 1rem;
background: linear-gradient(45deg, #ffd700, #ff6b6b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 30px rgba(255, 215, 0, 0.5);
letter-spacing: 2px;
}
.subtitle {
font-size: 1.2rem;
color: #9ca3af;
margin-bottom: 2rem;
font-style: italic;
}
.dungeon-container {
background: rgba(255, 255, 255, 0.05);
padding: 2rem;
border-radius: 1rem;
box-shadow:
0 0 50px rgba(255, 215, 0, 0.1),
inset 0 0 20px rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
margin: 0 auto;
display: inline-block;
max-width: 100%;
overflow: auto;
}
#dungeon {
display: grid;
gap: 0.5px;
background: #1a1a2e;
padding: 20px;
border-radius: 10px;
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.5);
}
.cell {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.3s ease;
position: relative;
}
.wall {
background: #2a2a3e;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);
}
.floor {
background: #1a1a1e;
}
.corridor {
background: #2d1b1b;
box-shadow: 0 0 10px rgba(139, 69, 19, 0.3);
}
.door {
animation: glow 2s ease-in-out infinite alternate;
}
@keyframes glow {
from {
background: #8b4513;
box-shadow: 0 0 5px #8b4513;
}
to {
background: #d2691e;
box-shadow: 0 0 15px #d2691e;
}
}
.room {
background: #16213e;
box-shadow: inset 0 0 10px rgba(255, 215, 0, 0.1);
}
.hero {
position: absolute;
font-size: 20px;
animation: pulse 1.5s ease-in-out infinite;
z-index: 10;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.generate-btn {
margin-top: 2rem;
padding: 1rem 2rem;
font-size: 1.2rem;
font-family: 'Cinzel', serif;
background: linear-gradient(45deg, #ffd700, #ff6b6b);
border: none;
border-radius: 50px;
color: #0f0f1e;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4);
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 600;
}
.generate-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(255, 215, 0, 0.6);
}
.generate-btn:active {
transform: translateY(0);
}
.stats {
margin-top: 1rem;
display: flex;
gap: 2rem;
justify-content: center;
font-size: 1.1rem;
color: #9ca3af;
}
.stat-item {
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
border: 1px solid rgba(255, 215, 0, 0.3);
}
.stat-value {
color: #ffd700;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<h1>Mystic Dungeon Generator</h1>
<p class="subtitle">Uncover the secrets hidden beneath the earth...</p>
<div class="dungeon-container">
<div id="dungeon"></div>
</div>
<button class="generate-btn" onclick="generateDungeon()">Generate New Dungeon</button>
<div class="stats">
<div class="stat-item">
Rooms: <span class="stat-value" id="roomCount">-</span>
</div>
<div class="stat-item">
Corridors: <span class="stat-value" id="corridorCount">-</span>
</div>
<div class="stat-item">
Doors: <span class="stat-value" id="doorCount">-</span>
</div>
</div>
</div>
<script>
const WIDTH = 50;
const HEIGHT = 30;
const MIN_ROOM_SIZE = 3;
const MAX_ROOM_SIZE = 9;
const MAX_ROOMS = 15;
const MIN_ROOMS = 8;
class Room {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.centerX = Math.floor(x + width / 2);
this.centerY = Math.floor(y + height / 2);
}
}
let dungeon = [];
let rooms = [];
let corridorCount = 0;
let doorCount = 0;
function initializeDungeon() {
dungeon = Array(HEIGHT).fill().map(() => Array(WIDTH).fill('wall'));
}
function canPlaceRoom(room) {
if (room.x < 1 || room.y < 1 ||
room.x + room.width + 1 >= WIDTH ||
room.y + room.height + 1 >= HEIGHT) {
return false;
}
for (let y = room.y - 1; y <= room.y + room.height + 1; y++) {
for (let x = room.x - 1; x <= room.x + room.width + 1; x++) {
if (y >= 0 && y < HEIGHT && x >= 0 && x < WIDTH && dungeon[y][x] !== 'wall') {
return false;
}
}
}
return true;
}
function createRoom(room) {
for (let y = room.y; y < room.y + room.height; y++) {
for (let x = room.x; x < room.x + room.width; x++) {
dungeon[y][x] = 'room';
}
}
rooms.push(room);
}
function generateRooms() {
rooms = [];
const numRooms = Math.floor(Math.random() * (MAX_ROOMS - MIN_ROOMS)) + MIN_ROOMS;
for (let i = 0; i < numRooms; i++) {
let attempts = 0;
while (attempts < 100) {
const width = Math.floor(Math.random() * (MAX_ROOM_SIZE - MIN_ROOM_SIZE)) + MIN_ROOM_SIZE;
const height = Math.floor(Math.random() * (MAX_ROOM_SIZE - MIN_ROOM_SIZE)) + MIN_ROOM_SIZE;
const x = Math.floor(Math.random() * (WIDTH - width - 2)) + 1;
const y = Math.floor(Math.random() * (HEIGHT - height - 2)) + 1;
const room = new Room(x, y, width, height);
if (canPlaceRoom(room)) {
createRoom(room);
break;
}
attempts++;
}
}
}
function getIcon(type, x, y) {
const icons = {
wall: '█',
floor: '·',
room: '░',
corridor: '▒',
door: '▓',
hero: '@',
empty: ' '
};
if (type === 'corridor') {
// Smart corridor icons based on neighbors
let neighbors = 0;
const left = x > 0 && dungeon[y][x-1] && (dungeon[y][x-1] === 'corridor' || dungeon[y][x-1] === 'door');
const right = x < WIDTH-1 && dungeon[y][x+1] && (dungeon[y][x+1] === 'corridor' || dungeon[y][x+1] === 'door');
const up = y > 0 && dungeon[y-1][x] && (dungeon[y-1][x] === 'corridor' || dungeon[y-1][x] === 'door');
const down = y < HEIGHT-1 && dungeon[y+1][x] && (dungeon[y+1][x] === 'corridor' || dungeon[y+1][x] === 'door');
if (left && right && !up && !down) return '═';
if (up && down && !left && !right) return '║';
if (left && up && !right && !down) return '╝';
if (left && down && !up && !right) return '╗';
if (right && up && !left && !down) return '╚';
if (right && down && !up && !left) return '╔';
if (left && right && up && !down) return '╩';
if (left && right && down && !up) return '╦';
if (up && down && left && !right) return '╣';
if (up && down && right && !left) return '╠';
if (up && down && left && right) return '╬';
}
return icons[type] || icons.empty;
}
function createCorridor(startRoom, endRoom) {
const startX = startRoom.centerX;
const startY = startRoom.centerY;
const endX = endRoom.centerX;
const endY = endRoom.centerY;
// Horizontal then vertical
let xPos = startX;
let yPos = startY;
// Horizontal
const dirX = endX > startX ? 1 : -1;
while (xPos !== endX) {
if (dungeon[yPos][xPos] === 'wall') {
dungeon[yPos][xPos] = 'corridor';
corridorCount++;
} else if (dungeon[yPos][xPos] === 'room') {
dungeon[yPos][xPos] = 'door';
doorCount++;
xPos += dirX;
break;
}
xPos += dirX;
}
// Vertical
const dirY = endY > startY ? 1 : -1;
while (yPos !== endY) {
if (dungeon[yPos][xPos] === 'wall') {
dungeon[yPos][xPos] = 'corridor';
corridorCount++;
} else if (dungeon[yPos][xPos] === 'room') {
dungeon[yPos][xPos] = 'door';
doorCount++;
yPos += dirY;
break;
}
yPos += dirY;
}
}
function connectRooms() {
corridorCount = 0;
doorCount = 0;
// Sort rooms by x coordinate
rooms.sort((a, b) => a.x - b.x);
// Connect adjacent rooms
for (let i = 0; i < rooms.length - 1; i++) {
createCorridor(rooms[i], rooms[i + 1]);
}
// Connect far rooms with random corridors
const extraConnections = Math.floor(rooms.length / 3);
for (let i = 0; i < extraConnections; i++) {
const r1 = Math.floor(Math.random() * rooms.length);
let r2 = Math.floor(Math.random() * rooms.length);
while (r1 === r2) {
r2 = Math.floor(Math.random() * rooms.length);
}
if (Math.abs(rooms[r1].x - rooms[r2].x) > 10 || Math.abs(rooms[r1].y - rooms[r2].y) > 10) {
createCorridor(rooms[r1], rooms[r2]);
}
}
}
function renderDungeon() {
const dungeonDiv = document.getElementById('dungeon');
dungeonDiv.innerHTML = '';
dungeonDiv.style.gridTemplateColumns = `repeat(${WIDTH}, 1fr)`;
for (let y = 0; y < HEIGHT; y++) {
for (let x = 0; x < WIDTH; x++) {
const cell = document.createElement('div');
cell.className = `cell ${dungeon[y][x]}`;
cell.innerHTML = getIcon(dungeon[y][x], x, y);
cell.title = `(${x}, ${y})`;
// Add entrance marker
if (y === rooms[0]?.centerY && x === rooms[0]?.centerX) {
const hero = document.createElement('span');
hero.className = 'hero';
hero.innerHTML = '@';
cell.appendChild(hero);
}
dungeonDiv.appendChild(cell);
}
}
// Update stats
document.getElementById('roomCount').textContent = rooms.length;
document.getElementById('corridorCount').textContent = corridorCount;
document.getElementById('doorCount').textContent = doorCount;
}
function generateDungeon() {
initializeDungeon();
generateRooms();
connectRooms();
renderDungeon();
}
// Generate dungeon on page load
window.addEventListener('load', generateDungeon);
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Procedural Dungeon Generator</title>
<style>
body, html { margin: 0; padding: 0; background: #222; color: #eee; font-family: sans-serif; overflow: hidden; }
#canvas { display: block; margin: 0 auto; background: #111; }
#controls {
position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
z-index: 10; background: rgba(0,0,0,0.6); padding: 10px; border-radius: 4px;
}
button {
background: #333; color: #eee; border: 1px solid #555; padding: 6px 12px; margin: 0 5px;
cursor: pointer; border-radius: 3px;
}
button:hover { background: #444; }
</style>
</head>
<body>
<div id="controls">
<button id="regen">Regenerate Dungeon</button>
</div>
<canvas id="canvas"></canvas>
<script>
(() => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const TILE = 32;
const COLS = 25;
const ROWS = 18;
canvas.width = COLS * TILE;
canvas.height = ROWS * TILE;
// Dungeon data: 0-empty, 1-floor, 2-wall, 3-door
let grid;
function initGrid() {
grid = Array.from({ length: ROWS }, () => Array(COLS).fill(0));
}
// Room structure
class Room {
constructor(x,y,w,h){
this.x = x; this.y = y; this.w = w; this.h = h;
this.center = { x: x + Math.floor(w/2), y: y + Math.floor(h/2) };
}
intersects(other){
return !(this.x+this.w < other.x || other.x+other.w < this.x ||
this.y+this.h < other.y || other.y+other.h < this.y);
}
}
function placeRooms() {
const rooms = [];
const MAX_ROOMS = 8;
const MIN_SIZE = 4;
const MAX_SIZE = 8;
for(let i=0;i<MAX_ROOMS;i++){
const w = rand(MIN_SIZE, MAX_SIZE);
const h = rand(MIN_SIZE, MAX_SIZE);
const x = rand(1, COLS - w - 1);
const y = rand(1, ROWS - h - 1);
const newRoom = new Room(x,y,w,h);
if(rooms.every(r=>!r.intersects(newRoom))){
carveRoom(newRoom);
rooms.push(newRoom);
}
}
return rooms;
}
function carveRoom(room) {
for(let y=room.y; y<room.y+room.h; y++){
for(let x=room.x; x<room.x+room.w; x++){
grid[y][x] = 1;
}
}
// walls
for(let y=room.y-1; y<=room.y+room.h; y++){
grid[y][room.x-1] = 2;
grid[y][room.x+room.w] = 2;
}
for(let x=room.x-1; x<=room.x+room.w; x++){
grid[room.y-1][x] = 2;
grid[room.y+room.h][x] = 2;
}
}
function connectRooms(rooms) {
// simple MST by nearest-center
const connected = [rooms[0]];
const remaining = rooms.slice(1);
while(remaining.length){
let bestDist=Infinity, from, toIdx=0;
connected.forEach(r1=>{
remaining.forEach((r2,i)=>{
const dx=r1.center.x-r2.center.x;
const dy=r1.center.y-r2.center.y;
const d=dx*dx+dy*dy;
if(d<bestDist){ bestDist=d; from=r1; toIdx=i; }
});
});
const to = remaining.splice(toIdx,1)[0];
carveCorridor(from.center, to.center);
connected.push(to);
}
}
function carveCorridor(c1, c2) {
let x=c1.x, y=c1.y;
while(x!==c2.x) {
grid[y][x] = (grid[y][x]===0?1:grid[y][x]);
x += (c2.x>x?1:-1);
}
while(y!==c2.y) {
grid[y][x] = (grid[y][x]===0?1:grid[y][x]);
y += (c2.y>y?1:-1);
}
// place doors where corridor meets wall
placeDoor(c1, {x, y});
placeDoor(c2, {x:c1.x, y:c1.y});
}
function placeDoor(cen, end) {
// find first wall next to corridor at room boundary
const dx = Math.sign(end.x - cen.x), dy = Math.sign(end.y - cen.y);
let x = end.x, y = end.y;
for(let i=0;i<3;i++){
if(grid[y][x]===2){
grid[y][x]=3;
break;
}
x+=dx; y+=dy;
if(x<0||x>=COLS||y<0||y>=ROWS)break;
}
}
function rand(min,max){ return Math.floor(Math.random()*(max-min+1))+min; }
// draw icons for corridor segments
const icons = {};
function makeIcons() {
const size = TILE;
const off = 4;
const c = document.createElement('canvas');
c.width=c.height=size;
const g = c.getContext('2d');
g.fillStyle='#333'; g.fillRect(0,0,size,size);
g.strokeStyle='#888'; g.lineWidth=4; g.lineCap='round';
// straight
g.clearRect(0,0,size,size);
g.beginPath();
g.moveTo(size/2, off);
g.lineTo(size/2, size-off);
g.stroke();
icons['|'] = c.toDataURL();
// reset
g.clearRect(0,0,size,size);
// horizontal
g.beginPath();
g.moveTo(off, size/2);
g.lineTo(size-off, size/2);
g.stroke();
icons['-'] = c.toDataURL();
// corners
const dirs = ['ul','ur','dl','dr'];
dirs.forEach(dir=>{
g.clearRect(0,0,size,size);
g.beginPath();
if(dir==='ul'){
g.moveTo(size/2, size/2);
g.lineTo(size/2, off);
g.moveTo(size/2, size/2);
g.lineTo(off, size/2);
}
if(dir==='ur'){
g.moveTo(size/2, size/2);
g.lineTo(size/2, off);
g.moveTo(size/2, size/2);
g.lineTo(size-off, size/2);
}
if(dir==='dl'){
g.moveTo(size/2, size/2);
g.lineTo(size/2, size-off);
g.moveTo(size/2, size/2);
g.lineTo(off, size/2);
}
if(dir==='dr'){
g.moveTo(size/2, size/2);
g.lineTo(size/2, size-off);
g.moveTo(size/2, size/2);
g.lineTo(size-off, size/2);
}
g.stroke();
icons[dir] = c.toDataURL();
});
}
function draw() {
ctx.clearRect(0,0,canvas.width,canvas.height);
for(let y=0;y<ROWS;y++){
for(let x=0;x<COLS;x++){
const v = grid[y][x];
const px = x*TILE, py = y*TILE;
if(v===0) {
ctx.fillStyle = '#111'; ctx.fillRect(px,py,TILE,TILE);
} else if(v===1) {
ctx.fillStyle = '#444'; ctx.fillRect(px,py,TILE,TILE);
} else if(v===2) {
ctx.fillStyle = '#666'; ctx.fillRect(px,py,TILE,TILE);
} else if(v===3) {
ctx.fillStyle = '#884'; ctx.fillRect(px,py,TILE,TILE);
}
}
}
// overlay corridor icons
for(let y=0;y<ROWS;y++){
for(let x=0;x<COLS;x++){
if(grid[y][x]===1){
// check neighbors in 4 directions
const up = y>0 && grid[y-1][x]>=1;
const down = y<ROWS-1 && grid[y+1][x]>=1;
const left = x>0 && grid[y][x-1]>=1;
const right = x<COLS-1 && grid[y][x+1]>=1;
let key = null;
if(up && down && !left && !right) key='|';
else if(left && right && !up && !down) key='-';
else if(up && right) key='dr';
else if(up && left) key='dl';
else if(down && right) key='ur';
else if(down && left) key='ul';
if(key){
const img = new Image();
img.src = icons[key];
ctx.drawImage(img, x*TILE, y*TILE, TILE, TILE);
}
}
}
}
}
function generate() {
initGrid();
const rooms = placeRooms();
connectRooms(rooms);
draw();
}
document.getElementById('regen').onclick = generate;
makeIcons();
generate();
})();
</script>
</body>
</html>
```# Visually Impressive Procedurally Generated Dungeon
I'll create a canvas-based dungeon generator that meets your requirements. The application will include:
- Algorithmically generated rooms with surrounding walls
- Organic connecting corridors
- Doors where corridors meet rooms
- Procedurally rendered icons for all dungeon elements
## Final Implementation
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procedural Dungeon Generator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #1a1a2e, #16213e);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
color: #e6e6e6;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 25px;
z-index: 2;
max-width: 800px;
}
h1 {
font-size: 3rem;
margin-bottom: 10px;
text-shadow: 0 0 10px rgba(50, 150, 255, 0.7);
background: linear-gradient(to right, #f8f9fa, #4cc9f0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: 1px;
}
.subtitle {
font-size: 1.2rem;
color: #a3b4cc;
margin-bottom: 30px;
}
.container {
position: relative;
background: rgba(0, 10, 30, 0.6);
border-radius: 15px;
border: 2px solid rgba(80, 120, 200, 0.3);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
padding: 25px;
z-index: 2;
max-width: 100%;
}
.canvas-container {
position: relative;
border: 4px solid rgba(80, 120, 200, 0.4);
border-radius: 8px;
overflow: hidden;
margin-bottom: 25px;
box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.5);
}
canvas {
display: block;
background: #0a1429;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 20px;
flex-wrap: wrap;
}
button {
background: linear-gradient(135deg, #4361ee, #3a0ca3);
color: white;
border: none;
padding: 12px 28px;
font-size: 1rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
letter-spacing: 0.5px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.3);
background: linear-gradient(135deg, #4895ef, #4361ee);
}
button:active {
transform: translateY(1px);
}
.legend {
display: flex;
justify-content: center;
gap: 25px;
margin-top: 25px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.95rem;
}
.legend-icon {
width: 24px;
height: 24px;
border-radius: 3px;
}
.info {
text-align: center;
margin-top: 20px;
color: #8fa3c4;
font-size: 0.9rem;
max-width: 600px;
}
.watermark {
position: absolute;
bottom: 20px;
font-size: 14px;
color: rgba(180, 190, 220, 0.6);
z-index: 1;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.controls {
flex-direction: column;
align-items: center;
}
button {
width: 100%;
max-width: 300px;
}
}
</style>
</head>
<body>
<div class="header">
<h1>Dungeon Dimensions</h1>
<p class="subtitle">A procedurally generated labyrinth of rooms and corridors</p>
</div>
<div class="container">
<div class="canvas-container">
<canvas id="dungeonCanvas" width="800" height="500"></canvas>
</div>
<div class="controls">
<button id="generateBtn">Generate New Dungeon</button>
<button id="walkBtn">Toggle Walk Animation</button>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-icon" style="background: #4361ee;"></div>
<span>Doors</span>
</div>
<div class="legend-item">
<div class="legend-icon" style="background: #4cc9f0;"></div>
<span>Corridors</span>
</div>
<div class="legend-item">
<div class="legend-icon" style="background: #8d99ae;"></div>
<span>Rooms</span>
</div>
<div class="legend-item">
<div class="legend-icon" style="background: #6a040f;"></div>
<span>Walls</span>
</div>
</div>
<p class="info">Each dungeon is uniquely generated with connecting rooms, winding corridors, and dungeon doors.</p>
</div>
<p class="watermark">Procedurally Generated Dungeon | Algorithmic Art</p>
<script>
// Constants for dungeon settings
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 500;
const CELL_SIZE = 25; // pixels per dungeon cell
const TYPES = {
WALL: 0,
ROOM: 1,
CORRIDOR: 2,
DOOR: 3
};
// Animation settings
let animationInterval;
let animationSpeed = 100; // ms
let isAnimating = false;
// Select DOM elements
const canvas = document.getElementById('dungeonCanvas');
const ctx = canvas.getContext('2d');
const generateBtn = document.getElementById('generateBtn');
const walkBtn = document.getElementById('walkBtn');
// Initialize
let dungeon = [];
let rooms = [];
let dungeonWidth = Math.floor(CANVAS_WIDTH / CELL_SIZE);
let dungeonHeight = Math.floor(CANVAS_HEIGHT / CELL_SIZE);
// Initialize the dungeon array
function initDungeon() {
dungeon = Array(dungeonHeight).fill().map(() => Array(dungeonWidth).fill(TYPES.WALL));
}
// Generate a room with random position and size
function generateRoom() {
// Ensure rooms are at least 3x3 and leave border space
const minSize = 3;
const maxSize = 8;
const margin = 2;
const width = Math.floor(Math.random() * (maxSize - minSize + 1)) + minSize;
const height = Math.floor(Math.random() * (maxSize - minSize + 1)) + minSize;
const x = Math.floor(Math.random() * (dungeonWidth - width - margin * 2)) + margin;
const y = Math.floor(Math.random() * (dungeonHeight - height - margin * 2)) + margin;
return { x, y, width, height };
}
// Check if a new room overlaps with existing rooms
function canPlaceRoom(newRoom) {
// Add 1 cell margin between rooms
for (let room of rooms) {
if (newRoom.x + newRoom.width + 1 > room.x &&
newRoom.x < room.x + room.width + 1 &&
newRoom.y + newRoom.height + 1 > room.y &&
newRoom.y < room.y + room.height + 1) {
return false;
}
}
return true;
}
// Place the room in the dungeon grid
function placeRoom(room) {
for (let y = room.y; y < room.y + room.height; y++) {
for (let x = room.x; x < room.x + room.width; x++) {
if (x >= 0 && x < dungeonWidth && y >= 0 && y < dungeonHeight) {
dungeon[y][x] = TYPES.ROOM;
}
}
}
}
// Procedurally generate different corridor patterns
function createCorridorTexture() {
const textureCanvas = document.createElement('canvas');
textureCanvas.width = CELL_SIZE;
textureCanvas.height = CELL_SIZE;
const txCtx = textureCanvas.getContext('2d');
// Base corridor color
txCtx.fillStyle = '#0a1625';
txCtx.fillRect(0, 0, CELL_SIZE, CELL_SIZE);
// Large irregular corridor pattern
txCtx.fillStyle = '#4cc9f0';
const patternCount = 5 + Math.floor(Math.random() * 4);
for (let i = 0; i < patternCount; i++) {
txCtx.beginPath();
// Choose pattern type
const patternType = Math.floor(Math.random() * 4);
const x = Math.random() * CELL_SIZE;
const y = Math.random() * CELL_SIZE;
const size = 2 + Math.random() * (CELL_SIZE / 3);
if (patternType === 0) {
// Wavy horizontal lines
txCtx.moveTo(0, y);
txCtx.bezierCurveTo(
x, y - size,
x + size*2, y - size,
CELL_SIZE, y
);
txCtx.bezierCurveTo(
x + size*2, y + size,
x, y + size,
0, y
);
txCtx.closePath();
txCtx.fill();
} else if (patternType === 1) {
// Scanlines effect
txCtx.fillRect(
Math.random() * CELL_SIZE,
Math.random() * CELL_SIZE,
CELL_SIZE * 0.8,
1
);
} else if (patternType === 2) {
// Dots pattern
txCtx.beginPath();
txCtx.arc(x, y, size * 0.2, 0, Math.PI * 2);
txCtx.fill();
} else {
// Arch pattern
txCtx.beginPath();
txCtx.moveTo(x, y);
txCtx.bezierCurveTo(
x - size, y - size,
x + size, y - size,
x + size*2, y
);
txCtx.strokeStyle = '#4cc9f0';
txCtx.lineWidth = 1;
txCtx.stroke();
}
}
return textureCanvas;
}
// Draw a vertical corridor segment
function drawVerticalCorridor(x, y1, y2) {
const startY = Math.min(y1, y2);
const height = Math.abs(y1 - y2);
for (let y = startY; y < startY + height; y++) {
if (y >= 0 && y < dungeonHeight && x >= 0 && x < dungeonWidth) {
// Skip drawing over rooms
if (dungeon[y][x] !== TYPES.ROOM) {
dungeon[y][x] = TYPES.CORRIDOR;
}
}
}
}
// Draw a horizontal corridor segment
function drawHorizontalCorridor(y, x1, x2) {
const startX = Math.min(x1, x2);
const width = Math.abs(x1 - x2);
for (let x = startX; x < startX + width; x++) {
if (y >= 0 && y < dungeonHeight && x >= 0 && x < dungeonWidth) {
// Skip drawing over rooms
if (dungeon[y][x] !== TYPES.ROOM) {
dungeon[y][x] = TYPES.CORRIDOR;
}
}
}
}
// Connect the rooms with corridors
function connectRooms() {
for (let i = 0; i < rooms.length - 1; i++) {
const room1 = rooms[i];
const room2 = rooms[i + 1];
const x1 = Math.floor(room1.x + room1.width / 2);
const y1 = Math.floor(room1.y + room1.height / 2);
const x2 = Math.floor(room2.x + room2.width / 2);
const y2 = Math.floor(room2.y + room2.height / 2);
// Randomly choose to go horizontal first or vertical first
if (Math.random() > 0.5) {
drawHorizontalCorridor(y1, x1, x2);
drawVerticalCorridor(x2, y1, y2);
} else {
drawVerticalCorridor(x1, y1, y2);
drawHorizontalCorridor(y2, x1, x2);
}
}
}
// Add doors at room entrances
function addDoors() {
// Iterate through each corridor and check neighbors to find junctions with rooms
for (let y = 1; y < dungeonHeight - 1; y++) {
for (let x = 1; x < dungeonWidth - 1; x++) {
if (dungeon[y][x] === TYPES.CORRIDOR) {
// Check adjacent cells for rooms
const north = dungeon[y - 1][x];
const south = dungeon[y + 1][x];
const west = dungeon[y][x - 1];
const east = dungeon[y][x + 1];
// If corridor is adjacent to a room, place a door in between
if (north === TYPES.ROOM) {
dungeon[y - 1][x] = TYPES.DOOR;
}
if (south === TYPES.ROOM) {
dungeon[y + 1][x] = TYPES.DOOR;
}
if (west === TYPES.ROOM) {
dungeon[y][x - 1] = TYPES.DOOR;
}
if (east === TYPES.ROOM) {
dungeon[y][x + 1] = TYPES.DOOR;
}
}
}
}
}
// Generate a new dungeon layout
function generateDungeon() {
// Clear any animation
if (animationInterval) {
clearInterval(animationInterval);
isAnimating = false;
}
initDungeon();
rooms = [];
// Try to place several rooms with collision checks
let roomAttempts = 0;
while (rooms.length < 12 && roomAttempts < 200) {
roomAttempts++;
const newRoom = generateRoom();
if (canPlaceRoom(newRoom)) {
placeRoom(newRoom);
rooms.push(newRoom);
}
}
// Finally connect rooms and add doors
connectRooms();
addDoors();
// Draw the new dungeon
renderDungeon();
}
// Create a torch illumination effect
function createTorchEffect() {
if (!isAnimating) return;
ctx.fillStyle = 'rgba(10, 20, 40, 0.1)';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Flickering torch effects on door tiles
for (let y = 0; y < dungeonHeight; y++) {
for (let x = 0; x < dungeonWidth; x++) {
if (dungeon[y][x] === TYPES.DOOR) {
const brightness = 0.8 + Math.random() * 0.2;
const glowColor = `rgba(67, 97, 238, ${brightness})`;
ctx.shadowColor = glowColor;
ctx.shadowBlur = 15 + Math.random() * 10;
ctx.fillStyle = glowColor;
ctx.fillRect(x * CELL_SIZE + 1, y * CELL_SIZE + 1, CELL_SIZE - 2, CELL_SIZE - 2);
}
}
}
// Reset shadow effects
ctx.shadowBlur = 0;
}
// Render the dungeon
function renderDungeon() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Draw each cell with the appropriate style
for (let y = 0; y < dungeonHeight; y++) {
for (let x = 0; x < dungeonWidth; x++) {
const cellType = dungeon[y][x];
switch (cellType) {
case TYPES.ROOM:
ctx.fillStyle = ctx.createPattern(createRoomTexture(), 'repeat');
break;
case TYPES.CORRIDOR:
ctx.fillStyle = ctx.createPattern(createCorridorTexture(), 'repeat');
break;
case TYPES.DOOR:
ctx.fillStyle = '#4361ee';
break;
case TYPES.WALL:
ctx.fillStyle = '#6a040f';
break;
}
if (cellType !== TYPES.WALL) {
ctx.fillRect(x * CELL_SIZE + 1, y * CELL_SIZE + 1, CELL_SIZE - 2, CELL_SIZE - 2);
} else {
// Draw walls with embossed effect
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
// Simulate 3D effect for walls
ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, 2);
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, 2, CELL_SIZE);
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE + CELL_SIZE - 2, CELL_SIZE, 2);
ctx.fillRect(x * CELL_SIZE + CELL_SIZE - 2, y * CELL_SIZE, 2, CELL_SIZE);
}
}
}
// If animating walks, add the torch effects
if (isAnimating) {
createTorchEffect();
}
}
// Create a textured pattern for room floors
function createRoomTexture() {
const textureCanvas = document.createElement('canvas');
textureCanvas.width = CELL_SIZE;
textureCanvas.height = CELL_SIZE;
const txCtx = textureCanvas.getContext('2d');
// Stone tile base
txCtx.fillStyle = '#2b2d42';
txCtx.fillRect(0, 0, CELL_SIZE, CELL_SIZE);
// Draw stone pattern
txCtx.fillStyle = '#8d99ae';
for(let i = 0; i < 10; i++) {
txCtx.fillRect(
Math.random() * CELL_SIZE,
Math.random() * CELL_SIZE,
1 + Math.random() * (CELL_SIZE/10),
1 + Math.random() * (CELL_SIZE/10)
);
}
return textureCanvas;
}
// Toggle animation for walking through the dungeon
function toggleWalkAnimation() {
isAnimating = !isAnimating;
if (isAnimating) {
animationInterval = setInterval(renderDungeon, animationSpeed);
} else if (animationInterval) {
clearInterval(animationInterval);
renderDungeon();
}
}
// Attach event listeners
generateBtn.addEventListener('click', generateDungeon);
walkBtn.addEventListener('click', toggleWalkAnimation);
// Generate the initial dungeon
generateDungeon();
</script>
</body>
</html>
```
## Key Features
This visually striking dungeon generator includes:
1. **Procedurally Generated Dungeon**:
- Algorithmically creates random room layouts
- Connects rooms with organic corridors
- Places doors at room entrances
2. **Dynamic Textures**:
- Unique corridor patterns generated procedurally
- Stone textures for room floors
- Rich 3D-like wall rendering
3. **Interactive Experience**:
- Generate new dungeons with one click
- Toggle walking animation with flickering torches
- Responsive design that works on different screen sizes
4. **Visual Effects**:
- Dark blue and purple gradient background
- Glowing torch effects on door animations
- 3D-like wall rendering with shadow effects
- Stylish UI with glass-morphism container
5. **Intuitive Controls**:
- Legend explaining dungeon elements
- Large buttons for easy interaction
- Clean, professional layout
The corridors are procedurally generated with a unique art style showing complex patterns rather than plain textures, making each dungeon visually distinct and interesting.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dungeon Generator</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: 'Courier New', monospace;
background: linear-gradient(135deg, #2c1810, #1a0f0a);
color: #d4af37;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
text-align: center;
}
h1 {
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
margin-bottom: 20px;
}
.controls {
margin-bottom: 20px;
}
button {
background: linear-gradient(45deg, #8b4513, #a0522d);
color: #fff;
border: 2px solid #654321;
padding: 10px 20px;
margin: 0 10px;
cursor: pointer;
border-radius: 5px;
font-family: inherit;
font-weight: bold;
transition: all 0.3s;
}
button:hover {
background: linear-gradient(45deg, #a0522d, #b8860b);
transform: translateY(-2px);
}
.dungeon-container {
background: #1a1a1a;
border: 3px solid #654321;
border-radius: 10px;
padding: 20px;
margin: 0 auto;
box-shadow: 0 0 20px rgba(212, 175, 55, 0.3);
overflow: auto;
}
.dungeon {
display: inline-block;
border: 2px solid #444;
background: #000;
}
.cell {
width: 16px;
height: 16px;
display: inline-block;
position: relative;
}
.floor {
background: #2a2a2a;
}
.wall {
background: linear-gradient(45deg, #444, #666);
border: 1px solid #333;
box-sizing: border-box;
}
.door {
background: radial-gradient(circle, #8b4513, #654321);
border: 1px solid #a0522d;
box-sizing: border-box;
}
.corridor {
background: #1a1a1a;
position: relative;
}
.corridor::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 2px;
background: linear-gradient(90deg, #666, #999, #666);
border-radius: 1px;
}
.corridor.vertical::after {
width: 2px;
height: 8px;
background: linear-gradient(0deg, #666, #999, #666);
}
.corridor.junction::after {
width: 6px;
height: 6px;
border-radius: 50%;
background: radial-gradient(circle, #888, #555);
}
.corridor.corner::after {
width: 6px;
height: 6px;
border-radius: 0 0 50% 0;
background: linear-gradient(45deg, #666, #999);
}
.corridor.corner.tl::after { border-radius: 0 0 50% 0; }
.corridor.corner.tr::after { border-radius: 0 0 0 50%; }
.corridor.corner.bl::after { border-radius: 0 50% 0 0; }
.corridor.corner.br::after { border-radius: 50% 0 0 0; }
.legend {
margin-top: 20px;
text-align: left;
display: inline-block;
}
.legend-item {
display: flex;
align-items: center;
margin: 5px 0;
}
.legend-icon {
width: 20px;
height: 20px;
margin-right: 10px;
border: 1px solid #444;
}
</style>
</head>
<body>
<div class="container">
<h1>🏰 Procedural Dungeon Generator</h1>
<div class="controls">
<button onclick="generateDungeon()">Generate New Dungeon</button>
<button onclick="changeSize('small')">Small (40x30)</button>
<button onclick="changeSize('medium')">Medium (60x40)</button>
<button onclick="changeSize('large')">Large (80x50)</button>
</div>
<div class="dungeon-container">
<div id="dungeon" class="dungeon"></div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-icon wall"></div>
<span>Wall</span>
</div>
<div class="legend-item">
<div class="legend-icon floor"></div>
<span>Room Floor</span>
</div>
<div class="legend-item">
<div class="legend-icon corridor"></div>
<span>Corridor</span>
</div>
<div class="legend-item">
<div class="legend-icon door"></div>
<span>Door</span>
</div>
</div>
</div>
<script>
class DungeonGenerator {
constructor(width, height) {
this.width = width;
this.height = height;
this.grid = [];
this.rooms = [];
this.corridors = [];
}
initialize() {
this.grid = Array(this.height).fill().map(() => Array(this.width).fill(0));
this.rooms = [];
this.corridors = [];
}
generateRooms() {
const numRooms = Math.floor(Math.random() * 8) + 5;
for (let i = 0; i < numRooms; i++) {
let attempts = 0;
while (attempts < 50) {
const width = Math.floor(Math.random() * 8) + 4;
const height = Math.floor(Math.random() * 6) + 4;
const x = Math.floor(Math.random() * (this.width - width - 2)) + 1;
const y = Math.floor(Math.random() * (this.height - height - 2)) + 1;
if (this.canPlaceRoom(x, y, width, height)) {
this.placeRoom(x, y, width, height);
this.rooms.push({ x, y, width, height });
break;
}
attempts++;
}
}
}
canPlaceRoom(x, y, width, height) {
for (let dy = -1; dy <= height + 1; dy++) {
for (let dx = -1; dx <= width + 1; dx++) {
if (x + dx >= 0 && x + dx < this.width &&
y + dy >= 0 && y + dy < this.height) {
if (this.grid[y + dy][x + dx] !== 0) {
return false;
}
}
}
}
return true;
}
placeRoom(x, y, width, height) {
// Place walls
for (let dy = 0; dy < height; dy++) {
for (let dx = 0; dx < width; dx++) {
if (dx === 0 || dx === width - 1 || dy === 0 || dy === height - 1) {
this.grid[y + dy][x + dx] = 1; // wall
} else {
this.grid[y + dy][x + dx] = 2; // floor
}
}
}
}
connectRooms() {
for (let i = 0; i < this.rooms.length - 1; i++) {
const roomA = this.rooms[i];
const roomB = this.rooms[i + 1];
this.createCorridor(roomA, roomB);
}
// Add some extra connections for complexity
const extraConnections = Math.floor(this.rooms.length / 3);
for (let i = 0; i < extraConnections; i++) {
const roomA = this.rooms[Math.floor(Math.random() * this.rooms.length)];
const roomB = this.rooms[Math.floor(Math.random() * this.rooms.length)];
if (roomA !== roomB) {
this.createCorridor(roomA, roomB);
}
}
}
createCorridor(roomA, roomB) {
const centerA = {
x: Math.floor(roomA.x + roomA.width / 2),
y: Math.floor(roomA.y + roomA.height / 2)
};
const centerB = {
x: Math.floor(roomB.x + roomB.width / 2),
y: Math.floor(roomB.y + roomB.height / 2)
};
// L-shaped corridor
if (Math.random() > 0.5) {
this.createHorizontalCorridor(centerA.x, centerB.x, centerA.y);
this.createVerticalCorridor(centerB.x, centerA.y, centerB.y);
} else {
this.createVerticalCorridor(centerA.x, centerA.y, centerB.y);
this.createHorizontalCorridor(centerA.x, centerB.x, centerB.y);
}
}
createHorizontalCorridor(x1, x2, y) {
const minX = Math.min(x1, x2);
const maxX = Math.max(x1, x2);
for (let x = minX; x <= maxX; x++) {
if (this.grid[y][x] === 0) {
this.grid[y][x] = 3; // corridor
} else if (this.grid[y][x] === 1) {
this.grid[y][x] = 4; // door
}
}
}
createVerticalCorridor(x, y1, y2) {
const minY = Math.min(y1, y2);
const maxY = Math.max(y1, y2);
for (let y = minY; y <= maxY; y++) {
if (this.grid[y][x] === 0) {
this.grid[y][x] = 3; // corridor
} else if (this.grid[y][x] === 1) {
this.grid[y][x] = 4; // door
}
}
}
generate() {
this.initialize();
this.generateRooms();
this.connectRooms();
}
getCorridorType(x, y) {
if (this.grid[y][x] !== 3) return '';
const neighbors = {
up: y > 0 && this.grid[y-1][x] === 3,
down: y < this.height-1 && this.grid[y+1][x] === 3,
left: x > 0 && this.grid[y][x-1] === 3,
right: x < this.width-1 && this.grid[y][x+1] === 3
};
const connections = Object.values(neighbors).filter(Boolean).length;
if (connections >= 3) return 'junction';
if (connections === 2) {
if ((neighbors.up && neighbors.down) || (neighbors.left && neighbors.right)) {
return neighbors.up && neighbors.down ? 'vertical' : '';
}
// Corner detection
if (neighbors.up && neighbors.left) return 'corner tl';
if (neighbors.up && neighbors.right) return 'corner tr';
if (neighbors.down && neighbors.left) return 'corner bl';
if (neighbors.down && neighbors.right) return 'corner br';
}
return neighbors.up || neighbors.down ? 'vertical' : '';
}
}
let currentSize = { width: 60, height: 40 };
function generateDungeon() {
const generator = new DungeonGenerator(currentSize.width, currentSize.height);
generator.generate();
renderDungeon(generator);
}
function renderDungeon(generator) {
const dungeonElement = document.getElementById('dungeon');
dungeonElement.innerHTML = '';
dungeonElement.style.width = (generator.width * 16) + 'px';
for (let y = 0; y < generator.height; y++) {
for (let x = 0; x < generator.width; x++) {
const cell = document.createElement('div');
cell.className = 'cell';
const cellType = generator.grid[y][x];
switch (cellType) {
case 0:
// Empty space - no additional class needed
break;
case 1:
cell.classList.add('wall');
break;
case 2:
cell.classList.add('floor');
break;
case 3:
cell.classList.add('corridor');
const corridorType = generator.getCorridorType(x, y);
if (corridorType) {
cell.className += ' ' + corridorType;
}
break;
case 4:
cell.classList.add('door');
break;
}
dungeonElement.appendChild(cell);
}
dungeonElement.appendChild(document.createElement('br'));
}
}
function changeSize(size) {
switch (size) {
case 'small':
currentSize = { width: 40, height: 30 };
break;
case 'medium':
currentSize = { width: 60, height: 40 };
break;
case 'large':
currentSize = { width: 80, height: 50 };
break;
}
generateDungeon();
}
// Generate initial dungeon
generateDungeon();
</script>
</body>
</html>
```
This web app creates a visually impressive procedural dungeon generator with the following features:
🏰 **Visual Elements:**
- Dark fantasy theme with golden accents
- Gradient backgrounds and glowing effects
- Responsive design with styled buttons
🗺️ **Dungeon Generation:**
- Random room placement with collision detection
- L-shaped corridors connecting rooms
- Intelligent door placement where corridors meet room walls
- Multiple dungeon sizes (Small, Medium, Large)
🛤️ **Procedural Corridor Icons:**
- Straight horizontal and vertical corridor segments
- Corner pieces for direction changes
- Junction nodes where multiple corridors meet
- All rendered using CSS pseudo-elements with gradients
🎮 **Interactive Features:**
- Generate new dungeons on demand
- Switch between different dungeon sizes
- Visual legend explaining tile types
The algorithm creates realistic dungeon layouts by:
1. Placing rooms with proper spacing
2. Connecting room centers with L-shaped corridors
3. Converting wall tiles to doors where corridors intersect
4. Adding visual variety with different corridor segment types
Click "Generate New Dungeon" to create endless variations!